From 751bfbd164c35f10a9e7bc002d5e216f5622e4e7 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Mon, 23 Feb 2026 12:00:10 +0200 Subject: [PATCH] fix: encrypted file handler + summary heading/markup fixes - Add RoomEncryptedFile handler for PDFs/docs in encrypted rooms - Tell summary LLM not to include headings (prevents duplicate) - Strip
after block elements in _md_to_html Co-Authored-By: Claude Opus 4.6 --- bot.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index e7747cc..a78f7f0 100644 --- a/bot.py +++ b/bot.py @@ -21,6 +21,7 @@ from nio import ( LoginResponse, InviteMemberEvent, MegolmEvent, + RoomEncryptedFile, RoomEncryptedImage, RoomMessageFile, RoomMessageImage, @@ -339,6 +340,7 @@ class Bot: self.client.add_event_callback(self.on_image_message, RoomMessageImage) self.client.add_event_callback(self.on_encrypted_image_message, RoomEncryptedImage) self.client.add_event_callback(self.on_file_message, RoomMessageFile) + self.client.add_event_callback(self.on_encrypted_file_message, RoomEncryptedFile) self.client.add_event_callback(self.on_room_unknown, RoomMessageUnknown) self.client.add_response_callback(self.on_sync, SyncResponse) self.client.add_to_device_callback(self.on_key_verification, KeyVerificationStart) @@ -1041,6 +1043,95 @@ class Bot: finally: await self.client.room_typing(room.room_id, typing_state=False) + async def on_encrypted_file_message(self, room, event: RoomEncryptedFile): + """Handle encrypted file messages: decrypt and process like on_file_message.""" + if event.sender == BOT_USER: + return + if not self._sync_token_received: + return + server_ts = event.server_timestamp / 1000 + if time.time() - server_ts > 30: + return + + source = event.source or {} + content = source.get("content", {}) + filename = content.get("body", "file") + ext = os.path.splitext(filename.lower())[1] + + is_pdf = ext == ".pdf" + is_docx = ext == ".docx" + is_text = ext in self._TEXT_EXTENSIONS + + if not (is_pdf or is_docx or is_text): + return + + await self._load_room_settings(room.room_id) + + is_dm = room.member_count == 2 + if not is_dm: + body = (event.body or "").strip() + bot_display = self.client.user_id.split(":")[0].lstrip("@") + mentioned = ( + BOT_USER in body + or f"@{bot_display}" in body.lower() + or bot_display.lower() in body.lower() + ) + if not mentioned: + return + + if not self.llm: + await self._send_text(room.room_id, "LLM not configured (LITELLM_BASE_URL not set).") + return + + mxc_url = event.url + if not mxc_url: + return + try: + resp = await self.client.download(mxc=mxc_url) + if not hasattr(resp, "body"): + logger.warning("Encrypted file download failed for %s", mxc_url) + return + file_bytes = decrypt_attachment(resp.body, event.key["k"], event.hashes["sha256"], event.iv) + except Exception: + logger.exception("Failed to download/decrypt encrypted file %s", mxc_url) + return + + if is_pdf: + extracted = self._extract_pdf_text(file_bytes) + doc_type = "pdf" + elif is_docx: + extracted = self._extract_docx_text(file_bytes) + doc_type = "text" + else: + extracted = self._extract_text_file(file_bytes) + doc_type = "text" + + if not extracted: + await self._send_text(room.room_id, f"I couldn't extract any text from that file ({filename}).") + return + + if len(extracted) > 50000: + extracted = extracted[:50000] + "\n\n[... truncated, file too long ...]" + + docs = self._room_document_context.setdefault(room.room_id, []) + docs.append({ + "type": doc_type, + "filename": filename, + "text": extracted, + "timestamp": time.time(), + }) + if len(docs) > 5: + del docs[:-5] + + label = "PDF" if is_pdf else "Word document" if is_docx else "file" + user_message = f'The user sent a {label} named "{filename}". Here is the extracted text:\n\n{extracted}\n\nPlease summarize or answer questions about this document.' + + await self.client.room_typing(room.room_id, typing_state=True) + try: + await self._respond_with_ai(room, user_message, sender=event.sender) + finally: + await self.client.room_typing(room.room_id, typing_state=False) + @staticmethod def _extract_pdf_text(pdf_bytes: bytes) -> str: """Extract text from PDF bytes using pymupdf.""" @@ -1503,6 +1594,9 @@ class Bot: safe = re.sub(r"^# (.+)$", r"

\1

", safe, flags=re.MULTILINE) # Line breaks safe = safe.replace("\n", "
") + # Remove redundant
after block elements + safe = re.sub(r"()(
)+", r"\1", safe) + safe = re.sub(r"()(
)+", r"\1", safe) return safe async def _generate_and_send_image(self, room_id: str, prompt: str): @@ -1577,7 +1671,8 @@ class Bot: {"role": "system", "content": ( "Fasse das folgende Anruf-Transkript kurz und praegnant zusammen. " "Nenne die wichtigsten besprochenen Punkte, Entscheidungen und offene Fragen. " - "Antworte in der Sprache des Gespraechs. Maximal 5-8 Saetze." + "Antworte in der Sprache des Gespraechs. Maximal 5-8 Saetze. " + "Keine Ueberschrift, kein Markdown-Heading — beginne direkt mit dem Text." )}, {"role": "user", "content": transcript_text}, ],