9044641b08
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.
74 lines
3.4 KiB
Python
74 lines
3.4 KiB
Python
#!/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:]))
|