Redact whole review threads on decision (replies too)
The bot was granted a redact/mod power level in the review room, so it can now clear a resolved thread entirely, not just the card: redact_thread redacts the card root then scans recent history for its m.thread replies (the human's yes/no + any bot messages) and redacts those too, so decided threads drop out of the threads view, not only the main timeline. Drops the in-thread confirmation on a successful decision (the thread clearing is the ack; a confirmation would keep the thread alive). redact_resolved.py extended to also clear replies of already- resolved threads for the one-time backfill. Bot-only; no s9pk change.
This commit is contained in:
@@ -11,7 +11,7 @@ Lifts matrix-bridge's prime-then-listen + threaded-reply plumbing. Config: repo
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from nio import AsyncClient, MatrixRoom, RoomMessageText
|
||||
from nio import AsyncClient, MatrixRoom, MessageDirection, RoomMessageText
|
||||
|
||||
import crm_client
|
||||
import email_proposals
|
||||
@@ -27,6 +27,7 @@ UNCLEAR_HELP = (
|
||||
)
|
||||
|
||||
EMAIL_POLL_SEC = 20 # how often the bot polls the CRM for new/decided email-activity proposals
|
||||
MAX_THREAD_SCAN_PAGES = 8 # how far back to scan for a resolved thread's replies before redacting
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -163,14 +164,42 @@ async def main():
|
||||
await say(room_id, "I didn't catch that.\n\n" + proposals.render_disambiguation(proposal), root)
|
||||
|
||||
async def redact_card(event_id):
|
||||
"""Remove a decided card from the room so only undecided ones remain. Redacting our OWN
|
||||
card needs no special power; in Element a redacted message drops out of the timeline. (To
|
||||
also wipe the human's yes/no reply for a fully-empty thread, give the bot a redact/mod
|
||||
power level — not required for this.)"""
|
||||
"""Redact one event (best-effort). Redacting our OWN message needs no special power;
|
||||
redacting someone else's reply needs the bot to hold a redact/mod power level."""
|
||||
try:
|
||||
await client.room_redact(review_room, event_id, reason="proposal resolved")
|
||||
except Exception as exc:
|
||||
print(f"matrix-intake: could not redact card {event_id}: {exc}", flush=True)
|
||||
print(f"matrix-intake: could not redact {event_id}: {exc}", flush=True)
|
||||
|
||||
async def redact_thread(root):
|
||||
"""Clear a resolved thread: redact the card AND every reply under it, so the thread drops
|
||||
out of the threads view (not just the main timeline). The card is ours (always redactable);
|
||||
the human's yes/no reply needs the bot's redact/mod power — if it lacks power that redact
|
||||
just no-ops and the reply lingers. Finds replies by scanning recent room history for
|
||||
m.thread events pointing at this root (the triggering reply is already synced, so a
|
||||
backward scan from the current token includes it)."""
|
||||
await redact_card(root)
|
||||
token = getattr(client, "next_batch", None)
|
||||
if not token:
|
||||
return
|
||||
try:
|
||||
scanned = 0
|
||||
for _ in range(MAX_THREAD_SCAN_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:
|
||||
rel = ((getattr(ev, "source", None) or {}).get("content", {}) or {}).get("m.relates_to") or {}
|
||||
if rel.get("rel_type") == "m.thread" and rel.get("event_id") == root:
|
||||
await redact_card(ev.event_id)
|
||||
token = getattr(resp, "end", None)
|
||||
scanned += len(chunk)
|
||||
if not token or scanned > 1000:
|
||||
break
|
||||
except Exception as exc:
|
||||
print(f"matrix-intake: thread reply cleanup failed for {root}: {exc}", flush=True)
|
||||
|
||||
async def handle_email_reply(room_id, root, text):
|
||||
"""An in-thread reply to a CRM-drafted email-proposal card: yes commits, no dismisses, and
|
||||
@@ -190,8 +219,9 @@ async def main():
|
||||
email_threads[root] = item # restore for retry
|
||||
await say(room_id, email_proposals.frame(f"⚠️ couldn't add it ({str(exc)[:200]}). Reply **yes** to retry, **no** to dismiss."), root)
|
||||
return
|
||||
await say(room_id, email_proposals.frame(f"✅ Added to the grid for **{item.get('investor_name') or 'the investor'}**."), root)
|
||||
await redact_card(root)
|
||||
# Success → clear the whole thread (card + replies). No confirmation: the thread
|
||||
# vanishing is the acknowledgment, and a confirmation reply would keep it alive.
|
||||
await redact_thread(root)
|
||||
elif decision == "reject":
|
||||
email_threads.pop(root, None)
|
||||
try:
|
||||
@@ -200,8 +230,7 @@ async def main():
|
||||
email_threads[root] = item
|
||||
await say(room_id, email_proposals.frame(f"⚠️ couldn't dismiss it ({str(exc)[:200]}). Try again."), root)
|
||||
return
|
||||
await say(room_id, email_proposals.frame("🗑️ Dismissed — nothing added to the grid."), root)
|
||||
await redact_card(root)
|
||||
await redact_thread(root)
|
||||
else:
|
||||
try:
|
||||
new_note = await asyncio.to_thread(email_proposals.revise_note, item.get("note") or "", text)
|
||||
@@ -244,12 +273,12 @@ async def main():
|
||||
"note": it.get("proposed_note") or ""}
|
||||
except Exception as exc:
|
||||
print(f"matrix-intake: failed to post email proposal {it.get('id')}: {exc}", flush=True)
|
||||
for it in lists["to_close"]: # decided on the web → remove the card, then close
|
||||
for it in lists["to_close"]: # decided on the web → clear the thread, then close
|
||||
ev = it.get("event_id")
|
||||
if not ev:
|
||||
continue
|
||||
try:
|
||||
await redact_card(ev)
|
||||
await redact_thread(ev)
|
||||
await asyncio.to_thread(crm_client.mark_email_proposal_closed, it["id"])
|
||||
email_threads.pop(ev, None)
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user