"""Windows notifications from the Harbor server, so the human hears about questions and urgent messages even with the chat page closed. Off by default; switched on from the board (stored per project in the database).""" import base64 import os import subprocess import sys from xml.sax.saxutils import escape APP_ID = r"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe" def toast_script(title, body): xml = ('' + escape(str(title)[:80]) + "" + escape(str(body)[:300]) + "") xml = xml.replace("'", "''") # inside a PowerShell single-quoted string return ("[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] " "| Out-Null\n" "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null\n" "$xml = New-Object Windows.Data.Xml.Dom.XmlDocument\n" f"$xml.LoadXml('{xml}')\n" "$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)\n" f"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{APP_ID}').Show($toast)\n") def toast(title, body): """Show a Windows notification; returns True when PowerShell accepted it.""" if sys.platform != "win32" or os.environ.get("HARBOR_NO_TOASTS"): return False encoded = base64.b64encode(toast_script(title, body).encode("utf-16-le")).decode("ascii") try: r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], capture_output=True, timeout=20, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) return r.returncode == 0 except (OSError, subprocess.SubprocessError): return False def pending_for_human(h, limit=3): """Messages to the human that deserve a notification and were not notified yet (questions, urgent, direct).""" row = h.conn.execute("SELECT value FROM meta WHERE key='toast_last'").fetchone() last = int(row["value"]) if row else 0 rows = [h._message_dict(r) for r in h.conn.execute( h.MESSAGE_SELECT + " JOIN deliveries d ON d.message_id=m.id AND d.agent=? WHERE d.read_at IS NULL AND m.id>? " "ORDER BY m.id LIMIT 200", (h.human, last))] worth = sorted((m for m in rows if m["recipients"] != ["All"] or m["importance"] == "urgent" or m["kind"] == "question"), key=lambda m: m["id"]) top = max([m["id"] for m in rows], default=last) return worth[-limit:], top def enabled(h): row = h.conn.execute("SELECT value FROM meta WHERE key='toasts'").fetchone() return bool(row and row["value"] == "on") def set_enabled(h, on): top = h.conn.execute("SELECT COALESCE(MAX(id), 0) FROM messages").fetchone()[0] def do(c): c.execute("INSERT INTO meta(key, value) VALUES ('toasts', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", ("on" if on else "off",)) c.execute("INSERT INTO meta(key, value) VALUES ('toast_last', ?) ON CONFLICT(key) DO UPDATE SET " "value=excluded.value", (str(top),)) # never notify the backlog h._write(do) def mark(h, top): h._write(lambda c: c.execute("INSERT INTO meta(key, value) VALUES ('toast_last', ?) ON CONFLICT(key) DO UPDATE " "SET value=excluded.value", (str(top),)))