#!/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:") 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())