calcurse: one-click Google login renewal
On expiry calcurse-sync starts calcurse-relogin, a notification that opens the consent page when clicked and catches the OAuth code on 127.0.0.1:8749. dunst's left-click now runs a notification's action before closing it.
This commit is contained in:
4 files changed
+121
-8
No files matched your search
@@ -300,7 +300,7 @@
|
|||||||
# * context_all: Open context menu for all notifications.
|
# * context_all: Open context menu for all notifications.
|
||||||
# These values can be strung together for each mouse event, and
|
# These values can be strung together for each mouse event, and
|
||||||
# will be executed in sequence.
|
# will be executed in sequence.
|
||||||
mouse_left_click = close_current
|
mouse_left_click = do_action, close_current
|
||||||
mouse_middle_click = do_action, close_current
|
mouse_middle_click = do_action, close_current
|
||||||
mouse_right_click = close_all
|
mouse_right_click = close_all
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Ask to renew the calcurse Google Calendar login
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# simple: the prompt waits for a click without a start timeout
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/calcurse-relogin --prompt
|
||||||
Executable
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Renew calcurse-caldav's Google login: catch the OAuth redirect on 127.0.0.1 and hand the code to calcurse-sync.
|
||||||
|
|
||||||
|
--prompt shows a notification first and logs in when it is clicked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CONFIG = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "calcurse" / "caldav" / "config"
|
||||||
|
WAIT_SECONDS = 300
|
||||||
|
|
||||||
|
|
||||||
|
def notify(summary: str, body: str = "", *extra: str) -> str:
|
||||||
|
"""Send a calcurse notification; with actions in extra, wait and return the chosen action's name."""
|
||||||
|
cmd = ["notify-send", "-a", "calcurse", "-i", "x-office-calendar", *extra, summary, body]
|
||||||
|
return subprocess.run(cmd, capture_output=True, text=True).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_port() -> int:
|
||||||
|
parser = configparser.RawConfigParser()
|
||||||
|
parser.read(CONFIG)
|
||||||
|
uri = urllib.parse.urlparse(parser.get("OAuth2", "RedirectURI", fallback=""))
|
||||||
|
if uri.hostname != "127.0.0.1" or uri.port is None:
|
||||||
|
sys.exit(f"RedirectURI in {CONFIG} must be http://127.0.0.1:<port>")
|
||||||
|
return uri.port
|
||||||
|
|
||||||
|
|
||||||
|
def ui_running() -> bool:
|
||||||
|
ttys = subprocess.run(["ps", "-C", "calcurse", "-o", "tty="], capture_output=True, text=True).stdout.split()
|
||||||
|
return any(tty != "?" for tty in ttys)
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectHandler(BaseHTTPRequestHandler):
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
|
||||||
|
def do_GET(self) -> None:
|
||||||
|
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||||
|
for key in ("code", "error"):
|
||||||
|
if key in query:
|
||||||
|
RedirectHandler.result[key] = query[key][0]
|
||||||
|
done = bool(RedirectHandler.result)
|
||||||
|
self.send_response(200 if done else 404)
|
||||||
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
self.end_headers()
|
||||||
|
if done:
|
||||||
|
self.wfile.write(b"calcurse: login received, you can close this tab.\n")
|
||||||
|
|
||||||
|
def log_message(self, format: str, *args: object) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def login() -> int:
|
||||||
|
if ui_running():
|
||||||
|
notify("Google Calendar login", "Close calcurse first, then log in again.", "-u", "critical")
|
||||||
|
return 1
|
||||||
|
server = HTTPServer(("127.0.0.1", redirect_port()), RedirectHandler)
|
||||||
|
# With an expired login calcurse-caldav opens the consent page and exits; with a valid one it just syncs.
|
||||||
|
if subprocess.run(["calcurse-caldav"], capture_output=True).returncode == 0:
|
||||||
|
notify("Google Calendar login", "Already logged in.")
|
||||||
|
return 0
|
||||||
|
deadline = time.monotonic() + WAIT_SECONDS
|
||||||
|
while not RedirectHandler.result and (left := deadline - time.monotonic()) > 0:
|
||||||
|
server.timeout = left
|
||||||
|
server.handle_request()
|
||||||
|
server.server_close()
|
||||||
|
code = RedirectHandler.result.get("code")
|
||||||
|
if not code:
|
||||||
|
reason = RedirectHandler.result.get("error", "no answer from the browser")
|
||||||
|
notify("Google Calendar login failed", reason, "-u", "critical")
|
||||||
|
return 1
|
||||||
|
# The user manager's PATH lacks ~/.local/bin: call calcurse-sync from this script's folder.
|
||||||
|
calcurse_sync = Path(__file__).resolve().with_name("calcurse-sync")
|
||||||
|
sync = subprocess.run([calcurse_sync, "--authcode", code], capture_output=True, text=True)
|
||||||
|
if sync.returncode != 0:
|
||||||
|
last_line = (sync.stdout.strip().splitlines() or [""])[-1]
|
||||||
|
notify("Google Calendar login failed", last_line, "-u", "critical")
|
||||||
|
return 1
|
||||||
|
notify("Google Calendar login renewed", "Synced again.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
parser.add_argument("--prompt", action="store_true", help="ask with a notification before logging in")
|
||||||
|
if parser.parse_args().prompt:
|
||||||
|
body = "Click to log in again, then click Allow in the browser."
|
||||||
|
if notify("Google Calendar login expired", body, "-u", "critical", "-A", "default=Log in") != "default":
|
||||||
|
return 0
|
||||||
|
return login()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Sync calcurse with Google Calendar via calcurse-caldav; notify once when the weekly OAuth login expires.
|
# Sync calcurse with Google Calendar via calcurse-caldav; start the login prompt when the weekly OAuth login expires.
|
||||||
# --now: called from the UI's pre-load hook (skip the UI check, shorter timeout, no daemon restart).
|
# --now: called from the UI's pre-load hook (skip the UI check, shorter timeout, no daemon restart).
|
||||||
|
# --authcode CODE: finish a login (from calcurse-relogin, which checks for the UI itself).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
data=${XDG_DATA_HOME:-$HOME/.local/share}/calcurse
|
data=${XDG_DATA_HOME:-$HOME/.local/share}/calcurse
|
||||||
state=${XDG_STATE_HOME:-$HOME/.local/state}/calcurse-sync
|
state=${XDG_STATE_HOME:-$HOME/.local/state}/calcurse-sync
|
||||||
expired_flag=$state/login-expired
|
expired_flag=$state/login-expired
|
||||||
now=false
|
now=false
|
||||||
[[ ${1:-} == --now ]] && now=true
|
authcode=()
|
||||||
|
case ${1:-} in
|
||||||
|
--now) now=true ;;
|
||||||
|
--authcode) authcode=(--authcode "$2") ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# The UI keeps the data in memory, so syncing under it would lose changes; its post-save hook syncs after it exits.
|
# The UI keeps the data in memory, so syncing under it would lose changes; its post-save hook syncs after it exits.
|
||||||
ui_running() {
|
ui_running() {
|
||||||
@@ -15,22 +20,22 @@ ui_running() {
|
|||||||
ps -C calcurse -o tty= | grep -qv '?'
|
ps -C calcurse -o tty= | grep -qv '?'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! $now && ui_running; then
|
if ! $now && ((${#authcode[@]} == 0)) && ui_running; then
|
||||||
echo "calcurse UI is running, skipping"
|
echo "calcurse UI is running, skipping"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$state"
|
mkdir -p "$state"
|
||||||
# BROWSER=true: an expired login must not open browser tabs on its own.
|
# BROWSER=true: an expired login must not open browser tabs on its own.
|
||||||
out=$(BROWSER=true timeout "$($now && echo 15 || echo 60)" calcurse-caldav 2>&1)
|
out=$(BROWSER=true timeout "$($now && echo 15 || echo 60)" calcurse-caldav "${authcode[@]}" 2>&1)
|
||||||
rc=$?
|
rc=$?
|
||||||
printf '%s\n' "$out"
|
printf '%s\n' "$out"
|
||||||
|
|
||||||
if ((rc != 0)); then
|
if ((rc != 0)); then
|
||||||
if grep -q 'Access token is missing or refresh token is expired' <<<"$out"; then
|
if grep -q 'Access token is missing or refresh token is expired' <<<"$out"; then
|
||||||
if [[ ! -e $expired_flag ]]; then
|
# Prompt via calcurse-relogin, again daily if the prompt was dismissed.
|
||||||
notify-send -a calcurse -u critical -i x-office-calendar "Google Calendar login expired" \
|
if [[ ! -e $expired_flag || -n $(find "$expired_flag" -mmin +1440) ]]; then
|
||||||
"Run calcurse-caldav in a terminal, allow access in the browser, then calcurse-caldav --authcode '<code>'."
|
systemctl --user start --no-block calcurse-relogin.service
|
||||||
touch "$expired_flag"
|
touch "$expired_flag"
|
||||||
fi
|
fi
|
||||||
elif grep -q 'Another synchronization instance' <<<"$out"; then
|
elif grep -q 'Another synchronization instance' <<<"$out"; then
|
||||||
|
|||||||
Reference in new issue
Block a user