app
Wiki Notify for Google Chat
Public Made by Adomby adom
Turn wiki.adom.inc notification emails into a live Google Chat board. One card, one row per issue - newest first, OPEN/CLOSED status, origination date, actor, and a View link - continuously updated in your own Chat space via your own webhook. Emails are swept out of your inbox (trash, archive, or label). Closed items linger until 6am the next day, then drop off. Delegates all auth to adom-google; the only secret is your webhook URL, kept in your private config.
← Commit history
Publish 1.8.0
5 files changed
+107−4
README.md+12@@ -125,6 +125,17 @@ inbox. A failed board post never sweeps the email; it retries next pass. a `+N earlier` note). Once a day at `purgeHour` - the same moment closed items are cleared - every item's thread is trimmed to just its most recent comment so the board never bloats.+- **Waiting on someone else** - an open item on another member's page (and no+ mention of you) shows as WAITING; if it is still waiting at `purgeHour` the+ following day it leaves the board like closed items do - but it is retained+ in state, so the moment the other party acts it reappears (and if their+ action closes it, it shows CLOSED). The subtitle counts these as+ "quietly waiting".+- **Weekly waiting digest** - Sundays at `purgeHour` (your `timezone`), a+ brief separate message lists everything currently waiting: item, link,+ whose action is needed, days waiting. Last week's digest is swap-deleted.+ Disable with `config set weeklySummary false`; post on demand with+ `wiki-notify-gchat weekly`. - **Closed** - row shows CLOSED (gray) and sticks around so you can enjoy it, then is removed at `purgeHour` (default 6am, your `timezone`) the next day. - The board caps at `maxRows` (default 40); older items collapse into a@@ -147,6 +158,7 @@ inbox. A failed board post never sweeps the email; it retries next pass. | `style` | `table` | `table` (item ref + status left, details right, divider-ruled rows) or `blocks` (per-item blocks with View buttons) | | `meName` | `You` | your first name, used in the involvement line ("Drew opened this") | | `fullName` | auto (To header) | your wiki author name (e.g. "Drew Owens"), used to detect pages you authored |+| `weeklySummary` | `true` | Sunday waiting-on-others digest at purgeHour | ## Staying alive and staying current (zero maintenance)
bin/__pycache__/wiki-notify-gchatcpython-312.pyc⋯ 1 unchanged line ⋯
bin/wiki-notify-gchat+93−2@@ -47,6 +47,7 @@ DEFAULT_CONFIG = { "style": "table", "meName": "You", "fullName": "",+ "weeklySummary": True, } OPEN_COLOR, CLOSED_COLOR = "#22c55e", "#64abff" # CLOSED = Adom blue (text-safe --blue)@@ -496,6 +497,67 @@ def purge_items(items, cfg): return dropped +def hide_deadline(ts_ms, cfg, tz):+ """purgeHour on the day after ts - the same boundary closed items use."""+ base = datetime.fromtimestamp(ts_ms / 1000, tz)+ return (base + timedelta(days=1)).replace(+ hour=int(cfg.get("purgeHour", 6)), minute=0, second=0, microsecond=0)+++def is_hidden_waiting(it, cfg, tz, now):+ """WAITING items leave the board at purgeHour the day after their last+ activity, like closed items - but they stay in state (never deleted), so+ the moment the other party acts, the new lastAt makes them visible again+ and they keep feeding the weekly summary while hidden."""+ return triage(it) == "waiting" and now >= hide_deadline(it["lastAt"], cfg, tz)+++def weekly_summary(state, cfg, force=False):+ """Sunday at purgeHour (user tz): post a brief 'waiting on others' digest -+ item, link, whose action is needed, how long it has been waiting. Posted as+ its own message; the previous week's summary is swap-deleted."""+ if not cfg.get("weeklySummary", True) and not force:+ return False+ tz = tzinfo_of(cfg)+ now = datetime.now(tz)+ today = now.strftime("%Y-%m-%d")+ if not force:+ if now.weekday() != 6 or now.hour < int(cfg.get("purgeHour", 6)):+ return False+ if state.get("lastWeeklySummary") == today:+ return False+ state["lastWeeklySummary"] = today+ waiting = [it for it in state.get("items", {}).values() if triage(it) == "waiting"]+ if not waiting:+ save_state(state)+ log_line("OK weekly summary: nothing waiting, skipped post")+ return False+ waiting.sort(key=lambda it: it["lastAt"]) # longest-waiting first+ lines = []+ for it in waiting:+ item = it["page"] + (f" #{it['num']}" if it["num"] else "")+ ref = f'<a href="{it["url"]}">{item}</a>' if it.get("url") else item+ who = it.get("pageAuthor") or "-"+ days = max(0, int((now - datetime.fromtimestamp(it["lastAt"] / 1000, tz)).days))+ lines.append(f"{ref} · waiting on {who} · {days}d")+ card = {"cardsV2": [{"cardId": "wiki-notify-weekly", "card": {+ "header": {"title": "Waiting on others",+ "subtitle": f"weekly summary · {len(waiting)} item(s) · {now.strftime('%b %-d')}"},+ "sections": [{"widgets": [{"textParagraph": {"text": "<br>".join(lines)}}]}],+ }}]}+ name = webhook_post(cfg, card)+ old = state.get("weeklyMsg", "")+ state["weeklyMsg"] = name+ save_state(state)+ if old:+ try:+ gapi(f"{CHAT}/{old}", "DELETE")+ except RuntimeError as e:+ log_line(f"WARN could not delete old weekly summary {old}: {e}")+ log_line(f"OK weekly summary posted ({len(waiting)} waiting) {name}")+ return True++ def trim_comments_daily(state, cfg): """Once a day at purgeHour (same moment closed items are cleared), collapse every item's comment thread to just its most recent comment so boards do@@ -533,6 +595,9 @@ def render_card(items, cfg, max_rows=None, max_comments=None): tz = tzinfo_of(cfg) # NEEDS YOU floats to the top, then WAITING, then CLOSED; newest-first within each. rows = sorted(items.values(), key=lambda it: (TRIAGE_RANK[triage(it)], -it["lastAt"]))+ now = datetime.now(tz)+ n_hidden = sum(1 for it in rows if is_hidden_waiting(it, cfg, tz, now))+ rows = [it for it in rows if not is_hidden_waiting(it, cfg, tz, now)] n_needs = sum(1 for it in rows if triage(it) == "needs_you") n_wait = sum(1 for it in rows if triage(it) == "waiting") n_closed = sum(1 for it in rows if triage(it) == "closed")@@ -551,7 +616,8 @@ def render_card(items, cfg, max_rows=None, max_comments=None): widgets = [{"textParagraph": {"text": "No wiki notifications. Inbox clean."}}] subtitle = (f'<font color="{NEEDS_COLOR}">{n_needs} need you</font> · ' f'<font color="{WAITING_COLOR}">{n_wait} waiting</font> · {n_closed} closed'- f' · updated {datetime.now(tz).strftime("%b %-d %H:%M")}')+ + (f' · {n_hidden} quietly waiting' if n_hidden else '')+ + f' · updated {datetime.now(tz).strftime("%b %-d %H:%M")}') return {"cardsV2": [{"cardId": "wiki-notify-board", "card": { "header": {"title": cfg.get("boardTitle", "Wiki Notifications"), "subtitle": subtitle}, "sections": [{"widgets": widgets}],@@ -876,10 +942,14 @@ def cmd_run(as_json, dry_run=False, quiet=False): if dropped: save_state(state) log_line(f"OK purged {len(dropped)} closed item(s): {', '.join(dropped)}")+ before_trim = state.get("lastCommentTrim") trimmed = 0 if dry_run else trim_comments_daily(state, cfg) if trimmed: save_state(state) log_line(f"OK daily comment trim: removed {trimmed} older comment(s)")+ # the daily boundary is also when WAITING items hide - force a repost so+ # the transition renders even when the items themselves did not change+ daily_fired = (not dry_run) and state.get("lastCommentTrim") != before_trim board_changed = False if not dry_run: try:@@ -887,7 +957,8 @@ def cmd_run(as_json, dry_run=False, quiet=False): enrich_issue_meta(state, me_full) sync_issue_status(state, cfg) annotate_page_ownership(state, me_full)- board_changed = push_board(cfg, state)+ weekly_summary(state, cfg)+ board_changed = push_board(cfg, state, force=daily_fired) except Exception as e: errors.append(f"board: {e}") log_line(f"ERROR board {e}")@@ -1142,6 +1213,7 @@ def cmd_uninstall_cron(as_json): INT_KEYS = {"pollSeconds", "purgeHour", "maxRows"}+BOOL_KEYS = {"weeklySummary"} def mask_webhook(url):@@ -1172,6 +1244,10 @@ def cmd_config(as_json, args): key, value = args[1], " ".join(args[2:]) if key not in DEFAULT_CONFIG: die(f"unknown key '{key}'", [f"keys: {', '.join(DEFAULT_CONFIG)}"], as_json)+ if key in BOOL_KEYS:+ if str(value).lower() not in ("true", "false"):+ die(f"{key} needs true or false", [f"e.g. `config set {key} true`"], as_json)+ value = str(value).lower() == "true" if key in INT_KEYS: try: value = int(value)@@ -1195,6 +1271,19 @@ def cmd_config(as_json, args): die(f"unknown config subcommand '{args[0]}'", ["Use `config show` or `config set <key> <value>`."], as_json) +def cmd_weekly(as_json):+ """Post the waiting-on-others summary now (normally fires Sundays at+ purgeHour automatically from the run loop)."""+ cfg = load_config(as_json)+ state = load_state()+ posted = weekly_summary(state, cfg, force=True)+ if posted:+ emit("OK: weekly summary posted.")+ else:+ emit("OK: nothing waiting - no summary posted.")+ emit("Hint: the automatic summary fires Sundays at purgeHour in your timezone.")++ def cmd_install(as_json): target = Path.home() / ".local" / "bin" / "wiki-notify-gchat" target.parent.mkdir(parents=True, exist_ok=True)@@ -1216,6 +1305,7 @@ Commands: run one poll pass: ingest + sweep emails, purge, update the board card backfill seed the board from past emails (default 30 days, incl. trash) - no sweeping board force-repost the board card (--dry-run prints the card JSON)+ weekly post the waiting-on-others summary now (auto: Sundays at purgeHour) status board contents summary + recent activity watch poll continuously in the foreground install-cron poll from cron + wire cold-boot self-healing (@reboot, bashrc, Claude hook)@@ -1247,6 +1337,7 @@ def main(): "run": lambda: cmd_run(as_json, dry, quiet), "backfill": lambda: cmd_backfill(as_json, days, dry), "board": lambda: cmd_board(as_json, dry),+ "weekly": lambda: cmd_weekly(as_json), "status": lambda: cmd_status(as_json), "watch": lambda: cmd_watch(as_json), "install-cron": lambda: cmd_install_cron(as_json),
package.json+1−1@@ -2,7 +2,7 @@ "slug": "wiki-notify-gchat", "title": "Wiki Notify for Google Chat", "type": "app",- "version": "1.7.2",+ "version": "1.8.0", "description": "Turn wiki.adom.inc notification emails into a live Google Chat board. One card, one row per issue - newest first, OPEN/CLOSED status, origination date, actor, and a View link - continuously updated in your own Chat space via your own webhook. Emails are swept out of your inbox (trash, archive, or label). Closed items linger until 6am the next day, then drop off. Delegates all auth to adom-google; the only secret is your webhook URL, kept in your private config.", "tags": [ "cli",
page.json+1−1@@ -2,7 +2,7 @@ "slug": "wiki-notify-gchat", "title": "Wiki Notify for Google Chat", "type": "app",- "version": "1.7.2",+ "version": "1.8.0", "description": "Turn wiki.adom.inc notification emails into a live Google Chat board. One card, one row per issue - newest first, OPEN/CLOSED status, origination date, actor, and a View link - continuously updated in your own Chat space via your own webhook. Emails are swept out of your inbox (trash, archive, or label). Closed items linger until 6am the next day, then drop off. Delegates all auth to adom-google; the only secret is your webhook URL, kept in your private config.", "tags": [ "cli",