Add one-time tool to redact resolved review cards

Cards decided before the auto-redact behavior shipped are already 'closed' in
the CRM, so the bot's to_close sweep never redacts them. redact_resolved.py walks
the review room, keeps cards still pending (CRM 'open' list), and redacts the
rest. Dry-run by default; --apply to act. Run via docker compose on the Spark.
This commit is contained in:
Keysat
2026-06-18 12:09:48 -05:00
parent a10889b10b
commit 9044641b08
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""One-time maintenance: redact already-resolved email-proposal review cards.
The bot redacts a card when it's decided going forward, but cards that were decided BEFORE that
behavior shipped (e.g. smoke-test remnants) are already `closed` in the CRM, so the normal
to_close sweep never touches them. This walks the review room's history, finds the bot's own
"proposed grid note" cards, and redacts every one that is NOT still pending (i.e. not in the CRM
`open` work-list) — leaving the room showing only what still needs handling.
Safe by default: prints what it WOULD redact and does nothing. Pass --apply to actually redact.
Run on the Spark via the bot's own creds/image:
docker compose run --rm matrix-intake python -u backend/matrix_intake/redact_resolved.py
docker compose run --rm matrix-intake python -u backend/matrix_intake/redact_resolved.py --apply
"""
import asyncio
import sys
from nio import AsyncClient, MessageDirection
import crm_client
import settings
CARD_MARKER = "📧 Proposed" # present in every review card (old and dash-framed)
MAX_PAGES = 30 # 30 * 100 events is far more history than this room holds
async def main(apply):
mx = settings.matrix_settings()
review_room = settings.email_review_room()
if not review_room:
print("MATRIX_EMAIL_REVIEW_ROOM is not set — nothing to do.")
return
client = AsyncClient(mx["homeserver"], mx["user_id"])
client.restore_login(user_id=mx["user_id"], device_id=mx["device_id"], access_token=mx["token"])
try:
# Cards still pending (must be KEPT) — their thread-root event id is the card event id.
open_ids = {it["event_id"] for it in crm_client.list_email_proposals().get("open", []) if it.get("event_id")}
print(f"pending cards to keep: {len(open_ids)}")
sync = await client.sync(timeout=10000, full_state=False)
token = sync.next_batch
cards = {} # event_id -> snippet (dedup across pages)
for _ in range(MAX_PAGES):
resp = await client.room_messages(review_room, start=token,
direction=MessageDirection.back, limit=100)
chunk = getattr(resp, "chunk", None)
if not chunk:
break
for ev in chunk:
if getattr(ev, "sender", None) != mx["user_id"]:
continue
body = getattr(ev, "body", "") or ""
if CARD_MARKER in body:
cards[ev.event_id] = body.replace("\n", " ")[:70]
token = getattr(resp, "end", None)
if not token:
break
to_redact = [(eid, snip) for eid, snip in cards.items() if eid not in open_ids]
print(f"bot cards found: {len(cards)}; resolved (to redact): {len(to_redact)}")
for eid, snip in to_redact:
print(("APPLY redact " if apply else "WOULD redact ") + eid + " :: " + snip)
if apply:
r = await client.room_redact(review_room, eid, reason="retroactive cleanup of resolved cards")
if not hasattr(r, "event_id"):
print(f" ! redact failed: {r}")
print(("done — redacted " if apply else "dry run — would redact ") + f"{len(to_redact)} card(s).")
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main(apply="--apply" in sys.argv[1:]))