Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/bot/message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class MessageIODeps:
send_animation: Callable[..., Optional[int]]
send_photo: Callable[..., Optional[int]]
send_video: Callable[..., Optional[int]]
delete_msg: Callable[[str, str], None]
delete_msg: Callable[[str, str], bool]
edit_message: Callable[[str, str, str], None]
admin_report: Callable[[str, Optional[Exception], Optional[Dict[str, Any]]], None]

Expand Down Expand Up @@ -205,7 +205,7 @@ class MessageHandlerDeps:
send_animation: Callable[..., Optional[int]]
send_photo: Callable[..., Optional[int]]
send_video: Callable[..., Optional[int]]
delete_msg: Callable[[str, str], None]
delete_msg: Callable[[str, str], bool]
edit_message: Callable[[str, str, str], None]
admin_report: Callable[[str, Optional[Exception], Optional[Dict[str, Any]]], None]
get_bot_message_metadata: Callable[[Any, str, Any], Optional[Dict[str, Any]]]
Expand Down
4 changes: 2 additions & 2 deletions api/bot/message_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class LinkReplacementDeps(Protocol):
@property
def link_service(self) -> LinkServiceProtocol: ...

def delete_msg(self, chat_id: str, message_id: str) -> None: ...
def delete_msg(self, chat_id: str, message_id: str) -> bool: ...

def send_msg(self, *args: Any, **kwargs: Any) -> Optional[int]: ...

Expand Down Expand Up @@ -83,8 +83,8 @@ def send_replacement(target_reply_id: Optional[str]) -> Optional[int]:
return deps.send_msg(chat_id, fixed_text, buttons=original_links)

if link_mode == "delete":
deps.delete_msg(chat_id, message_id)
sent_message_id = send_replacement(reply_id)
deps.delete_msg(chat_id, message_id)
if sent_message_id is not None:
deps.save_message_to_redis(
chat_id,
Expand Down
43 changes: 35 additions & 8 deletions api/bot/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import re
import time
from dataclasses import dataclass
from os import environ
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
Expand Down Expand Up @@ -226,11 +227,13 @@ def __init__(
self,
telegram_request: Callable[..., Tuple[Optional[Dict[str, Any]], Optional[str]]],
message_has_domain_link: Optional[Callable[[str, str], bool]] = None,
sleep: Callable[[float], None] = time.sleep,
) -> None:
self._telegram_request = telegram_request
self._message_has_domain_link = (
message_has_domain_link or _message_has_domain_link
)
self._sleep = sleep

def request(self, endpoint: str, **kwargs: Any) -> Any:
return self._telegram_request(endpoint, **kwargs)
Expand Down Expand Up @@ -386,14 +389,38 @@ def edit_photo(
return True
return error is None and bool(payload_response)

def delete_message(self, chat_id: str, msg_id: str) -> None:
self._telegram_request(
"deleteMessage",
method="GET",
params={"chat_id": chat_id, "message_id": msg_id},
log_errors=False,
expect_json=False,
)
def delete_message(self, chat_id: str, msg_id: str) -> bool:
for attempt in range(3):
payload, error = self._telegram_request(
"deleteMessage",
method="GET",
params={"chat_id": chat_id, "message_id": msg_id},
log_errors=False,
)
if error is None and payload and payload.get("result") is True:
return True

description = error or "unexpected Telegram response"
if "message to delete not found" in description.lower():
return True

error_code = payload.get("error_code") if payload else None
transient = payload is None or error_code == 429 or (
isinstance(error_code, int) and error_code >= 500
)
if transient and attempt < 2:
retry_after = (payload or {}).get("parameters", {}).get("retry_after")
delay = min(float(retry_after), 2.0) if retry_after else 0.25 * 2**attempt
self._sleep(delay)
continue

print(
f"Telegram deleteMessage failed for chat={chat_id} "
f"message={msg_id}: {description}"
)
return False

return False

def send_animation(
self,
Expand Down
2 changes: 2 additions & 0 deletions tests/test_message_ai_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ def test_message_links_handle_link_replacement_delete_mode_stores_fixed_context(
)

assert handled is True
method_names = [call_args[0] for call_args in deps.method_calls]
assert method_names.index("send_msg") < method_names.index("delete_msg")
deps.delete_msg.assert_called_once_with("555", "100")
deps.send_msg.assert_called_once_with(
"555",
Expand Down
41 changes: 38 additions & 3 deletions tests/test_telegram_gateway.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from api.bot.telegram import TelegramGateway
import json
from unittest.mock import MagicMock

from api.bot.telegram import TelegramGateway


class FakeTelegramRequest:
Expand Down Expand Up @@ -54,16 +56,49 @@ def test_send_message_enables_html_for_linked_polymarket_title():


def test_delete_message_delegates_to_telegram_request():
request = FakeTelegramRequest(({"ok": True}, None))
request = FakeTelegramRequest(({"ok": True, "result": True}, None))
gateway = TelegramGateway(telegram_request=request)

gateway.delete_message("123", "10")
result = gateway.delete_message("123", "10")

assert result is True
assert request.calls[0][0] == "deleteMessage"
assert request.calls[0][1]["method"] == "GET"
assert request.calls[0][1]["params"] == {"chat_id": "123", "message_id": "10"}


def test_delete_message_retries_transient_failure():
request = MagicMock(
side_effect=[
(None, "timed out"),
({"ok": False, "error_code": 503}, "temporarily unavailable"),
({"ok": True, "result": True}, None),
]
)
sleep = MagicMock()
gateway = TelegramGateway(telegram_request=request, sleep=sleep)

assert gateway.delete_message("123", "10") is True
assert request.call_count == 3
assert [call.args[0] for call in sleep.call_args_list] == [0.25, 0.5]


def test_delete_message_does_not_retry_permanent_failure(capsys):
request = MagicMock(
return_value=(
{"ok": False, "error_code": 400},
"Bad Request: message can't be deleted",
)
)
sleep = MagicMock()
gateway = TelegramGateway(telegram_request=request, sleep=sleep)

assert gateway.delete_message("123", "10") is False
assert request.call_count == 1
sleep.assert_not_called()
assert "message can't be deleted" in capsys.readouterr().out


def test_send_photo_delegates_to_telegram_request():
request = FakeTelegramRequest(({"ok": True, "result": {"message_id": 77}}, None))
gateway = TelegramGateway(telegram_request=request)
Expand Down
Loading