From a04fcbf7796482604bdee9f1cf4a1b7d2d336c9b Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:41:26 -0700 Subject: [PATCH] fix(telegram): widen transport-error redaction to all remaining raw exception sites Extends @AlexFucuson9's 3-site fix (#58594) across the full adapter: every logger call and SendResult.error that interpolates a raw PTB exception now routes through _redact_telegram_error_text(). Covers polling conflict/retry/network ladders, overflow-split edits, draft sends, prompt/approval/clarify/picker sends, media send fallbacks, media cache failures, reactions, and chat-info lookups (48 additional sites). Telegram Bot API exceptions embed the token in the request URL (/bot/), so any raw str(exc) is a leak surface. Adds regression tests for SendResult.error redaction (update prompt, clarify) and delete_message debug-log redaction. --- plugins/platforms/telegram/adapter.py | 102 +++++++++--------- .../gateway/test_telegram_error_redaction.py | 58 ++++++++++ 2 files changed, 109 insertions(+), 51 deletions(-) diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 950ef7ca53bb..4127765f207c 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -1222,7 +1222,7 @@ class TelegramAdapter(BasePlatformAdapter): "retrying without reply/topic anchor: %s", self.name, media_label, - send_err, + _redact_telegram_error_text(send_err), ) if reset_media is not None: reset_media() @@ -1883,12 +1883,12 @@ class TelegramAdapter(BasePlatformAdapter): self._rich_draft_disabled = True logger.debug( "[%s] sendRichMessageDraft unsupported (%s) — using legacy drafts", - self.name, exc, + self.name, _redact_telegram_error_text(exc), ) else: logger.debug( "[%s] sendRichMessageDraft transient failure (%s) — legacy draft this frame", - self.name, exc, + self.name, _redact_telegram_error_text(exc), ) return False @@ -2155,7 +2155,7 @@ class TelegramAdapter(BasePlatformAdapter): if self._polling_error_task and not self._polling_error_task.done(): logger.debug( "[%s] Telegram polling recovery already scheduled; ignoring %s: %s", - self.name, reason, error, + self.name, reason, _redact_telegram_error_text(error), ) return self._send_path_degraded = True @@ -2188,7 +2188,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.warning( "[%s] deleteWebhook failed with a recoverable network error; " "continuing to polling so getUpdates/retry can recover: %s", - self.name, err, + self.name, _redact_telegram_error_text(err), ) self._send_path_degraded = True return False @@ -2227,7 +2227,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.warning( "[%s] Telegram polling bootstrap conflict; gateway stays alive " "while conflict retry runs: %s", - self.name, err, + self.name, _redact_telegram_error_text(err), ) loop = asyncio.get_running_loop() self._polling_error_task = loop.create_task(self._handle_polling_conflict(err)) @@ -2722,7 +2722,7 @@ class TelegramAdapter(BasePlatformAdapter): "held open on Telegram's servers. Waiting %ds for it to expire. " "Error: %s", self.name, self._polling_conflict_count, MAX_CONFLICT_RETRIES, - RETRY_DELAY, error, + RETRY_DELAY, _redact_telegram_error_text(error), ) # Stop the local updater cleanly before sleeping. If it's already # stopped (e.g. PTB raised before updater.running was set) this is @@ -2779,7 +2779,7 @@ class TelegramAdapter(BasePlatformAdapter): "[%s] Telegram polling retry %d/%d failed: %s. " "Scheduling next attempt.", self.name, self._polling_conflict_count, MAX_CONFLICT_RETRIES, - retry_err, + _redact_telegram_error_text(retry_err), ) # Schedule the next retry rather than returning silently. # Returning here without either restarting polling or setting @@ -2818,7 +2818,7 @@ class TelegramAdapter(BasePlatformAdapter): ) logger.error( "[%s] %s Original error: %s", - self.name, message, error, + self.name, message, _redact_telegram_error_text(error), ) # Snapshot whether we are the call that actually transitions to fatal. # A concurrent retry task scheduled by an earlier conflict may already @@ -2895,7 +2895,7 @@ class TelegramAdapter(BasePlatformAdapter): else: logger.warning( "[%s] Failed to create DM topic '%s' in chat %s: %s", - self.name, name, chat_id, e, + self.name, name, chat_id, _redact_telegram_error_text(e), ) return None @@ -3213,7 +3213,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.warning( "[%s] Could not register Telegram command menu: %s", self.name, - e, + _redact_telegram_error_text(e), exc_info=True, ) @@ -3600,12 +3600,12 @@ class TelegramAdapter(BasePlatformAdapter): self._background_tasks.add(self._polling_error_task) self._polling_error_task.add_done_callback(self._background_tasks.discard) elif self._looks_like_network_error(error): - logger.warning("[%s] Telegram network error, scheduling reconnect: %s", self.name, error) + logger.warning("[%s] Telegram network _redact_telegram_error_text(error), scheduling reconnect: %s", self.name, error) self._polling_error_task = loop.create_task(self._handle_polling_network_error(error)) self._background_tasks.add(self._polling_error_task) self._polling_error_task.add_done_callback(self._background_tasks.discard) else: - logger.error("[%s] Telegram polling error: %s", self.name, error, exc_info=True) + logger.error("[%s] Telegram polling _redact_telegram_error_text(error): %s", self.name, error, exc_info=True) # Store reference for retry use in _handle_polling_conflict self._polling_error_callback_ref = _polling_error_callback @@ -3684,7 +3684,7 @@ class TelegramAdapter(BasePlatformAdapter): except Exception as e: logger.debug( "[%s] Failed to set bot status indicator to %r: %s", - self.name, text, e, + self.name, text, _redact_telegram_error_text(e), ) async def _cancel_pending_delivery_tasks(self) -> None: @@ -4498,7 +4498,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.warning( "[%s] Overflow split: MarkdownV2 first-chunk edit " "failed, falling back to plain text: %s", - self.name, fmt_err, + self.name, _redact_telegram_error_text(fmt_err), ) await self._bot.edit_message_text( chat_id=normalize_telegram_chat_id(chat_id), @@ -4520,9 +4520,9 @@ class TelegramAdapter(BasePlatformAdapter): else: logger.error( "[%s] Overflow split: first-chunk edit failed: %s", - self.name, e, exc_info=True, + self.name, _redact_telegram_error_text(e), exc_info=True, ) - return SendResult(success=False, error=str(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) # Step 2 — send each remaining chunk as a continuation message, # threaded as a reply to the previous so the user sees them as a @@ -4589,7 +4589,7 @@ class TelegramAdapter(BasePlatformAdapter): except Exception as _retry_err: logger.warning( "[%s] Overflow continuation no-reply retry failed: %s", - self.name, _retry_err, + self.name, _redact_telegram_error_text(_retry_err), ) sent_msg = None break @@ -4598,7 +4598,7 @@ class TelegramAdapter(BasePlatformAdapter): continue logger.warning( "[%s] Overflow continuation send failed: %s", - self.name, send_err, + self.name, _redact_telegram_error_text(send_err), ) sent_msg = None break @@ -4669,7 +4669,7 @@ class TelegramAdapter(BasePlatformAdapter): except Exception as e: logger.debug( "[%s] Failed to delete Telegram message %s: %s", - self.name, message_id, e, + self.name, message_id, _redact_telegram_error_text(e), ) return False @@ -4770,14 +4770,14 @@ class TelegramAdapter(BasePlatformAdapter): logger.debug( "[%s] sendMessageDraft MarkdownV2 rejected, retrying " "as plain text (chat=%s draft_id=%s): %s", - self.name, chat_id, draft_id, e, + self.name, chat_id, draft_id, _redact_telegram_error_text(e), ) continue logger.debug( "[%s] sendMessageDraft failed (chat=%s draft_id=%s): %s", self.name, chat_id, draft_id, e, ) - return SendResult(success=False, error=str(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) return SendResult(success=False, error="draft_rejected") @@ -4860,8 +4860,8 @@ class TelegramAdapter(BasePlatformAdapter): ) return SendResult(success=True, message_id=str(msg.message_id)) except Exception as e: - logger.warning("[%s] send_update_prompt failed: %s", self.name, e) - return SendResult(success=False, error=str(e)) + logger.warning("[%s] send_update_prompt failed: %s", self.name, _redact_telegram_error_text(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) async def send_exec_approval( self, chat_id: str, command: str, session_key: str, @@ -4939,8 +4939,8 @@ class TelegramAdapter(BasePlatformAdapter): return SendResult(success=True, message_id=str(msg.message_id)) except Exception as e: - logger.warning("[%s] send_exec_approval failed: %s", self.name, e) - return SendResult(success=False, error=str(e)) + logger.warning("[%s] send_exec_approval failed: %s", self.name, _redact_telegram_error_text(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) async def send_slash_confirm( self, chat_id: str, title: str, message: str, session_key: str, @@ -4987,8 +4987,8 @@ class TelegramAdapter(BasePlatformAdapter): self._slash_confirm_state[confirm_id] = session_key return SendResult(success=True, message_id=str(msg.message_id)) except Exception as e: - logger.warning("[%s] send_slash_confirm failed: %s", self.name, e) - return SendResult(success=False, error=str(e)) + logger.warning("[%s] send_slash_confirm failed: %s", self.name, _redact_telegram_error_text(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) async def send_clarify( self, @@ -5069,8 +5069,8 @@ class TelegramAdapter(BasePlatformAdapter): self._clarify_state[clarify_id] = session_key return SendResult(success=True, message_id=str(msg.message_id)) except Exception as e: - logger.warning("[%s] send_clarify failed: %s", self.name, e) - return SendResult(success=False, error=str(e)) + logger.warning("[%s] send_clarify failed: %s", self.name, _redact_telegram_error_text(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) async def send_model_picker( self, @@ -5141,8 +5141,8 @@ class TelegramAdapter(BasePlatformAdapter): return SendResult(success=True, message_id=str(msg.message_id)) except Exception as e: - logger.warning("[%s] send_model_picker failed: %s", self.name, e) - return SendResult(success=False, error=str(e)) + logger.warning("[%s] send_model_picker failed: %s", self.name, _redact_telegram_error_text(e)) + return SendResult(success=False, error=_redact_telegram_error_text(e)) _PROVIDER_PAGE_SIZE = 10 _MODEL_PAGE_SIZE = 8 @@ -6219,7 +6219,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.error( "[%s] Failed to send Telegram voice/audio, falling back to base adapter: %s", self.name, - e, + _redact_telegram_error_text(e), exc_info=True, ) return await super().send_voice(chat_id, audio_path, caption, reply_to, metadata=metadata) @@ -6345,7 +6345,7 @@ class TelegramAdapter(BasePlatformAdapter): except Exception as e: logger.warning( "[%s] send_media_group failed (chunk %d/%d), falling back to per-image: %s", - self.name, chunk_idx + 1, len(chunks), e, + self.name, chunk_idx + 1, len(chunks), _redact_telegram_error_text(e), exc_info=True, ) # Fallback: send each photo in this chunk individually @@ -6427,7 +6427,7 @@ class TelegramAdapter(BasePlatformAdapter): "[%s] Failed to send Telegram local image as photo, " "trying document fallback: %s", self.name, - e, + _redact_telegram_error_text(e), exc_info=True, ) # Fallback to sending as document (file) — no dimension limit, @@ -6607,7 +6607,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.warning( "[%s] URL-based send_photo failed, trying file upload: %s", self.name, - e, + _redact_telegram_error_text(e), exc_info=True, ) # Fallback: download and upload as file (supports up to 10MB) @@ -6691,7 +6691,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.error( "[%s] Failed to send Telegram animation, falling back to photo: %s", self.name, - e, + _redact_telegram_error_text(e), exc_info=True, ) # Fallback: try as a regular photo @@ -6778,7 +6778,7 @@ class TelegramAdapter(BasePlatformAdapter): logger.debug( "[%s] Failed to send Telegram typing indicator: %s", self.name, - e, + _redact_telegram_error_text(e), exc_info=True, ) @@ -6811,7 +6811,7 @@ class TelegramAdapter(BasePlatformAdapter): "[%s] Failed to get Telegram chat info for %s: %s", self.name, chat_id, - e, + _redact_telegram_error_text(e), exc_info=True, ) return {"name": str(chat_id), "type": "dm", "error": str(e)} @@ -7503,7 +7503,7 @@ class TelegramAdapter(BasePlatformAdapter): filename = os.path.basename(getattr(file_obj, "file_path", "") or "") cached = cache_media_bytes(data, filename=filename, mime_type=mime, default_kind=kind) except Exception as exc: - logger.warning("[Telegram] Failed to cache observed group media: %s", exc, exc_info=True) + logger.warning("[Telegram] Failed to cache observed group media: %s", _redact_telegram_error_text(exc), exc_info=True) return if cached is None: @@ -7551,7 +7551,7 @@ class TelegramAdapter(BasePlatformAdapter): filename = os.path.basename(getattr(file_obj, "file_path", "") or "") cached = cache_media_bytes(data, filename=filename, mime_type=mime, default_kind=kind) except Exception as exc: - logger.warning("[Telegram] Failed to cache replied-to media: %s", exc, exc_info=True) + logger.warning("[Telegram] Failed to cache replied-to media: %s", _redact_telegram_error_text(exc), exc_info=True) return if cached is None: @@ -7794,7 +7794,7 @@ class TelegramAdapter(BasePlatformAdapter): self._forum_command_registered.add(chat_id) logger.info("[%s] Lazy-registered %d commands for forum chat %s", self.name, len(bot_commands), chat_id) except Exception as e: - logger.warning("[%s] Forum command lazy-registration failed: %s", self.name, e) + logger.warning("[%s] Forum command lazy-registration failed: %s", self.name, _redact_telegram_error_text(e)) def _effective_update_message(self, update: Update) -> Optional[Message]: """Return the message-like payload for normal messages and channel posts. @@ -8141,7 +8141,7 @@ class TelegramAdapter(BasePlatformAdapter): return except Exception as e: - logger.warning("[Telegram] Failed to cache photo: %s", e, exc_info=True) + logger.warning("[Telegram] Failed to cache photo: %s", _redact_telegram_error_text(e), exc_info=True) await self._surface_media_cache_failure(msg, event, "photo", e) # Download voice/audio messages to cache for STT transcription @@ -8160,7 +8160,7 @@ class TelegramAdapter(BasePlatformAdapter): event.media_types = ["audio/ogg"] logger.info("[Telegram] Cached user voice at %s", cached_path) except Exception as e: - logger.warning("[Telegram] Failed to cache voice: %s", e, exc_info=True) + logger.warning("[Telegram] Failed to cache voice: %s", _redact_telegram_error_text(e), exc_info=True) await self._surface_media_cache_failure(msg, event, "voice message", e) elif msg.audio: try: @@ -8177,7 +8177,7 @@ class TelegramAdapter(BasePlatformAdapter): event.media_types = ["audio/mp3"] logger.info("[Telegram] Cached user audio at %s", cached_path) except Exception as e: - logger.warning("[Telegram] Failed to cache audio: %s", e, exc_info=True) + logger.warning("[Telegram] Failed to cache audio: %s", _redact_telegram_error_text(e), exc_info=True) await self._surface_media_cache_failure(msg, event, "audio file", e) elif msg.video: @@ -8201,7 +8201,7 @@ class TelegramAdapter(BasePlatformAdapter): event.media_types = [SUPPORTED_VIDEO_TYPES.get(ext, "video/mp4")] logger.info("[Telegram] Cached user video at %s", cached_path) except Exception as e: - logger.warning("[Telegram] Failed to cache video: %s", e, exc_info=True) + logger.warning("[Telegram] Failed to cache video: %s", _redact_telegram_error_text(e), exc_info=True) await self._surface_media_cache_failure(msg, event, "video file", e) # Download document files to cache for agent processing @@ -8248,7 +8248,7 @@ class TelegramAdapter(BasePlatformAdapter): try: cached_path = cache_image_from_bytes(bytes(image_bytes), ext=image_ext) except ValueError as e: - logger.warning("[Telegram] Failed to cache image document: %s", e, exc_info=True) + logger.warning("[Telegram] Failed to cache image document: %s", _redact_telegram_error_text(e), exc_info=True) event.text = ( f"Image document '{original_filename or doc_mime or ext or 'unknown'}' " "could not be read as an image." @@ -8334,7 +8334,7 @@ class TelegramAdapter(BasePlatformAdapter): pass except Exception as e: - logger.warning("[Telegram] Failed to cache document: %s", e, exc_info=True) + logger.warning("[Telegram] Failed to cache document: %s", _redact_telegram_error_text(e), exc_info=True) await self._surface_media_cache_failure( msg, event, "attachment", e, display_name=getattr(doc, "file_name", None) or None, @@ -8451,7 +8451,7 @@ class TelegramAdapter(BasePlatformAdapter): emoji, set_name, ) except Exception as e: - logger.warning("[Telegram] Sticker analysis error: %s", e, exc_info=True) + logger.warning("[Telegram] Sticker analysis error: %s", _redact_telegram_error_text(e), exc_info=True) event.text = build_sticker_injection( f"a sticker with emoji {emoji}" if emoji else "a sticker", emoji, set_name, @@ -8815,7 +8815,7 @@ class TelegramAdapter(BasePlatformAdapter): ) return True except Exception as e: - logger.debug("[%s] set_message_reaction failed (%s): %s", self.name, emoji, e) + logger.debug("[%s] set_message_reaction failed (%s): %s", self.name, emoji, _redact_telegram_error_text(e)) return False async def _clear_reactions(self, chat_id: str, message_id: str) -> bool: @@ -8836,7 +8836,7 @@ class TelegramAdapter(BasePlatformAdapter): ) return True except Exception as e: - logger.debug("[%s] clear reactions failed: %s", self.name, e) + logger.debug("[%s] clear reactions failed: %s", self.name, _redact_telegram_error_text(e)) return False async def on_processing_start(self, event: MessageEvent) -> None: diff --git a/tests/gateway/test_telegram_error_redaction.py b/tests/gateway/test_telegram_error_redaction.py index 0944ad8c6378..92a8f7665db2 100644 --- a/tests/gateway/test_telegram_error_redaction.py +++ b/tests/gateway/test_telegram_error_redaction.py @@ -130,3 +130,61 @@ async def test_send_video_failure_redacts_token_in_log(monkeypatch, caplog, tmp_ logged = "\n".join(r.getMessage() for r in caplog.records) assert _SECRET_TOKEN not in logged assert "Failed to send video" in logged + + +_SECRET_SEND_URL = f"https://api.telegram.org/bot{_SECRET_TOKEN}/sendMessage" + + +@pytest.mark.asyncio +async def test_send_update_prompt_failure_redacts_token_in_result_and_log(caplog): + """A send_update_prompt() transport exception embedding the bot token URL + must not reach the warning log or SendResult.error unredacted.""" + adapter = _make_connected_adapter() + adapter._send_message_with_thread_fallback = AsyncMock( + side_effect=RuntimeError(f"Timed out requesting {_SECRET_SEND_URL}") + ) + + with caplog.at_level("WARNING"): + result = await adapter.send_update_prompt("123", "restart?") + + assert result.success is False + assert _SECRET_TOKEN not in (result.error or "") + assert "***" in (result.error or "") + logged = "\n".join(r.getMessage() for r in caplog.records) + assert _SECRET_TOKEN not in logged + + +@pytest.mark.asyncio +async def test_send_clarify_failure_redacts_token_in_result_and_log(caplog): + """A send_clarify() transport exception embedding the bot token URL must + not reach the warning log or SendResult.error unredacted.""" + adapter = _make_connected_adapter() + adapter._send_message_with_thread_fallback = AsyncMock( + side_effect=RuntimeError(f"Timed out requesting {_SECRET_SEND_URL}") + ) + + with caplog.at_level("WARNING"): + result = await adapter.send_clarify("123", "q?", ["a", "b"], "cid", "sess") + + assert result.success is False + assert _SECRET_TOKEN not in (result.error or "") + logged = "\n".join(r.getMessage() for r in caplog.records) + assert _SECRET_TOKEN not in logged + + +@pytest.mark.asyncio +async def test_delete_message_failure_redacts_token_in_log(caplog): + """A delete_message() transport exception embedding the bot token URL + must not reach the debug log unredacted.""" + adapter = _make_connected_adapter() + adapter._bot.delete_message = AsyncMock( + side_effect=RuntimeError(f"Bad Request: {_SECRET_SEND_URL}") + ) + + with caplog.at_level("DEBUG"): + ok = await adapter.delete_message("123", "55") + + assert ok is False + logged = "\n".join(r.getMessage() for r in caplog.records) + assert _SECRET_TOKEN not in logged + assert "***" in logged