"""Matrix plumbing lifted from matrix-bridge (src/bot.py): message splitting, thread-root detection, and a threaded-reply sender. Kept dependency-light so the rest of the bot is testable without a live homeserver.""" MAX_MSG_CHARS = 30000 # well under Matrix's ~64KB event cap def split_message(text, limit=MAX_MSG_CHARS): """Split text into <=limit-char chunks on newline boundaries (no truncation).""" if len(text) <= limit: return [text] chunks, buf = [], "" for line in text.splitlines(keepends=True): while len(line) > limit: if buf: chunks.append(buf) buf = "" chunks.append(line[:limit]) line = line[limit:] if len(buf) + len(line) > limit: chunks.append(buf) buf = "" buf += line if buf: chunks.append(buf) return chunks def thread_root_of(event): """Return the thread root event_id if this message is a threaded reply, else None.""" relates = (getattr(event, "source", None) or {}).get("content", {}).get("m.relates_to") or {} if relates.get("rel_type") == "m.thread": return relates.get("event_id") return None def thread_content(text, thread_root): """Build an m.room.message content dict, threaded under thread_root when given.""" content = {"msgtype": "m.text", "body": text} if thread_root: content["m.relates_to"] = { "rel_type": "m.thread", "event_id": thread_root, "is_falling_back": True, "m.in_reply_to": {"event_id": thread_root}, } return content def make_say(client): """Return an async say(room_id, text, thread_root=None) bound to a matrix-nio client.""" async def say(room_id, text, thread_root=None): for chunk in split_message(text): await client.room_send(room_id, "m.room.message", thread_content(chunk, thread_root)) return say def reply_content(text, reply_to_event_id): """Build a plain (non-threaded) reply: shows in the MAIN timeline as a reply to reply_to_event_id, unlike thread_content() which lands the message inside a thread.""" content = {"msgtype": "m.text", "body": text} if reply_to_event_id: content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_event_id}} return content def make_reply(client): """Return an async reply(room_id, text, reply_to) that posts a plain main-timeline reply — the brief 'proposed X — see thread' nudge alongside the in-thread proposal card.""" async def reply(room_id, text, reply_to): for chunk in split_message(text): await client.room_send(room_id, "m.room.message", reply_content(chunk, reply_to)) return reply