Device-test round 2: 4 in-app fixes + Matrix intake cleanup (v0.1.0:99)
Grant's real-phone testing surfaced seven items; this lands six (the seventh, in-app camera card intake, is planned in docs/handoffs/in-app-card-intake-plan.md). CRM half — ships in the s9pk (v0.1.0:99): - Intake fuzzy match no longer over-indexes on generic firm words. _name_similarity now compares DISTINCTIVE tokens only (generic descriptors — "Investment Group", "Capital", "Family Office" — stripped via _GENERIC_ORG_WORDS) for both the difflib ratio and the Jaccard, so "Fortitude Investment Group" stops surfacing Aether/Russell while "Aether Capital" still surfaces "Aether Investment Group". +2 regression cases. - Mobile grid "Last contact"/staleness sort is reversible. SortSheet gains opt-in dir/onToggleDir; other surfaces (Contacts/Pipeline) are untouched. - Mobile "Edit investor" prefills a contact's saved email. GET /api/fundraising/state heals a blank grid pill email from the linked classic contact (fundraising_contacts.contact_id -> contacts.email), fill-only, by pill order then name; the next one-row save persists it. +test_grid_email_heal.py. - Mobile quick-log pencil icon renders. iOS collapses a sole, centered, attribute-only -sized flex-child <svg>; .quicklog-btn svg now gets explicit CSS width/height + flex:none (the pattern the working bottom-tab/sort-pill icons use). The v97 fix only changed color. Matrix intake bot — ships on the Spark (bot-only, NOT the s9pk): - Approve/reject now redacts the whole intake thread (card + ack + main-timeline nudge + the user's own photo/note), mirroring the email-review room; redact_thread takes the room as an arg and matches replies by m.thread OR m.in_reply_to (so the nudge clears). No more in-Matrix confirmation after a commit (the thread vanishing is the ack). Needs the bot to hold a redact/moderator power level in the intake room. - New one-time backend/matrix_intake/redact_intake.py clears the room's pre-existing backlog (dry-run default; --apply). Tests 42/42 green; frontend render-smoke green. Frontend fixes are inspection + render -smoke verified (on-device confirm pending); the bot redaction is live-smoke only.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-time maintenance: clear the intake room's backlog of resolved/stale messages.
|
||||
|
||||
Going forward the bot redacts each intake thread when it's approved/rejected (bot card + ack +
|
||||
nudge + the user's own note/photo). This clears the messages that piled up BEFORE that shipped.
|
||||
|
||||
The intake room is single-purpose and the bot keeps **no durable pending state** (its proposal
|
||||
store is in-memory and is lost on every restart), so nothing in the room is "still live" after a
|
||||
restart — every message in it is safe to redact. This walks the room history and redacts every
|
||||
m.room.message event (text + business-card images), bot's and humans' alike.
|
||||
|
||||
Redacting another user's message (the humans' notes/photos) needs the bot to hold a **redact /
|
||||
moderator power level** in the intake room — without it those just no-op and linger (the bot's own
|
||||
messages still clear). Make the bot a moderator of the intake room in Element first.
|
||||
|
||||
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_intake.py
|
||||
docker compose run --rm matrix-intake python -u backend/matrix_intake/redact_intake.py --apply
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from nio import AsyncClient, MessageDirection
|
||||
|
||||
import settings
|
||||
|
||||
MAX_PAGES = 50 # 50 * 100 events is far more history than this room holds
|
||||
|
||||
|
||||
async def main(apply):
|
||||
mx = settings.matrix_settings()
|
||||
intake_room = mx.get("intake_room")
|
||||
if not intake_room:
|
||||
print("MATRIX_INTAKE_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:
|
||||
sync = await client.sync(timeout=10000, full_state=False)
|
||||
token = sync.next_batch
|
||||
targets = [] # (event_id, label)
|
||||
seen = set()
|
||||
for _ in range(MAX_PAGES):
|
||||
resp = await client.room_messages(intake_room, start=token,
|
||||
direction=MessageDirection.back, limit=100)
|
||||
chunk = getattr(resp, "chunk", None)
|
||||
if not chunk:
|
||||
break
|
||||
for ev in chunk:
|
||||
src = getattr(ev, "source", None) or {}
|
||||
if src.get("type") != "m.room.message":
|
||||
continue # only chat messages + images; leave membership/state events alone
|
||||
eid = getattr(ev, "event_id", None)
|
||||
if not eid or eid in seen:
|
||||
continue
|
||||
seen.add(eid)
|
||||
content = src.get("content") or {}
|
||||
if not content:
|
||||
continue # already redacted (content stripped) — skip
|
||||
msgtype = content.get("msgtype") or "?"
|
||||
body = (content.get("body", "") or "").replace("\n", " ")
|
||||
who = "bot " if getattr(ev, "sender", None) == mx["user_id"] else "user"
|
||||
targets.append((eid, f"{who} [{msgtype}] {body[:60]}"))
|
||||
token = getattr(resp, "end", None)
|
||||
if not token:
|
||||
break
|
||||
|
||||
print(f"messages to clear in the intake room: {len(targets)}")
|
||||
fails = 0
|
||||
for eid, label in targets:
|
||||
print(("APPLY redact " if apply else "WOULD redact ") + eid + " :: " + label)
|
||||
if apply:
|
||||
r = await client.room_redact(intake_room, eid, reason="retroactive intake-room cleanup")
|
||||
if not hasattr(r, "event_id"):
|
||||
fails += 1
|
||||
print(f" ! redact failed (need mod power for others' messages?): {r}")
|
||||
print(("done — redacted " if apply else "dry run — would redact ")
|
||||
+ f"{len(targets) - (fails if apply else 0)}/{len(targets)} event(s)"
|
||||
+ (f"; {fails} failed" if apply and fails else "") + ".")
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main(apply="--apply" in sys.argv[1:]))
|
||||
Reference in New Issue
Block a user