agnes-the-ai-analyst/services/telegram_bot/sender.py
Petr f2d3d156e3 Move standalone services from server/ to services/
Extract 4 self-contained services into services/ module:
- server/telegram_bot/ -> services/telegram_bot/
- server/ws_gateway/ -> services/ws_gateway/
- server/corporate_memory/ -> services/corporate_memory/
- server/session_collector.py -> services/session_collector/

Each service now has its own systemd/ directory with .service and .timer files.
deploy.sh updated to auto-discover service units from services/*/systemd/*.

server/ now contains only deployment infrastructure (deploy.sh, setup scripts,
bin/ management tools, sudoers, nginx config).

All imports updated: webapp/app.py, server/bin/ scripts, systemd ExecStart paths.
2026-03-09 12:54:30 +01:00

173 lines
5.6 KiB
Python

"""
Telegram Bot API sender - sends messages and photos via HTTP API.
"""
import logging
import httpx
from . import config
logger = logging.getLogger(__name__)
BASE_URL = "https://api.telegram.org/bot{token}"
def _api_url(method: str) -> str:
"""Build Telegram Bot API URL."""
return f"{BASE_URL.format(token=config.TELEGRAM_BOT_TOKEN)}/{method}"
async def send_message(
chat_id: int,
text: str,
parse_mode: str = "Markdown",
) -> bool:
"""Send a text message to a Telegram chat. Returns True on success."""
# Truncate if too long
if len(text) > config.MAX_MESSAGE_LENGTH:
text = text[: config.MAX_MESSAGE_LENGTH - 3] + "..."
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
_api_url("sendMessage"),
json={
"chat_id": chat_id,
"text": text,
"parse_mode": parse_mode,
},
)
if resp.status_code == 200 and resp.json().get("ok"):
return True
logger.error(f"sendMessage failed: {resp.status_code} {resp.text}")
# Retry without parse_mode if Markdown parsing failed
if resp.status_code == 400 and "parse" in resp.text.lower():
resp2 = await client.post(
_api_url("sendMessage"),
json={"chat_id": chat_id, "text": text},
)
return resp2.status_code == 200 and resp2.json().get("ok")
return False
except Exception:
logger.exception(f"Failed to send message to chat_id {chat_id}")
return False
async def send_message_with_buttons(
chat_id: int,
text: str,
buttons: list[list[dict]],
parse_mode: str = "Markdown",
) -> bool:
"""Send a text message with inline keyboard buttons. Returns True on success.
buttons format: [[{"text": "Label", "callback_data": "data"}], ...]
Each inner list is a row of buttons.
"""
if len(text) > config.MAX_MESSAGE_LENGTH:
text = text[: config.MAX_MESSAGE_LENGTH - 3] + "..."
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
_api_url("sendMessage"),
json={
"chat_id": chat_id,
"text": text,
"parse_mode": parse_mode,
"reply_markup": {"inline_keyboard": buttons},
},
)
if resp.status_code == 200 and resp.json().get("ok"):
return True
logger.error(f"sendMessage (buttons) failed: {resp.status_code} {resp.text}")
return False
except Exception:
logger.exception(f"Failed to send message with buttons to chat_id {chat_id}")
return False
async def answer_callback_query(
callback_query_id: str,
text: str = "",
) -> bool:
"""Answer a callback query (acknowledge button press)."""
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
_api_url("answerCallbackQuery"),
json={
"callback_query_id": callback_query_id,
"text": text,
},
)
return resp.status_code == 200
except Exception:
logger.exception("Failed to answer callback query")
return False
async def send_photo(
chat_id: int,
photo_path: str,
caption: str = "",
parse_mode: str = "Markdown",
) -> bool:
"""Send a photo to a Telegram chat. Returns True on success."""
if caption and len(caption) > config.MAX_CAPTION_LENGTH:
caption = caption[: config.MAX_CAPTION_LENGTH - 3] + "..."
try:
async with httpx.AsyncClient(timeout=60) as client:
with open(photo_path, "rb") as photo_file:
data = {"chat_id": str(chat_id)}
if caption:
data["caption"] = caption
data["parse_mode"] = parse_mode
resp = await client.post(
_api_url("sendPhoto"),
data=data,
files={"photo": ("chart.png", photo_file, "image/png")},
)
if resp.status_code == 200 and resp.json().get("ok"):
return True
logger.error(f"sendPhoto failed: {resp.status_code} {resp.text}")
return False
except Exception:
logger.exception(f"Failed to send photo to chat_id {chat_id}")
return False
async def get_updates(offset: int = 0) -> tuple[list[dict], int]:
"""Long-poll for updates. Returns (updates, new_offset)."""
try:
async with httpx.AsyncClient(timeout=config.POLL_TIMEOUT_SECONDS + 10) as client:
resp = await client.get(
_api_url("getUpdates"),
params={
"offset": offset,
"timeout": config.POLL_TIMEOUT_SECONDS,
"allowed_updates": '["message","callback_query"]',
},
)
if resp.status_code != 200:
logger.error(f"getUpdates failed: {resp.status_code}")
return [], offset
data = resp.json()
if not data.get("ok"):
return [], offset
updates = data.get("result", [])
if updates:
new_offset = updates[-1]["update_id"] + 1
return updates, new_offset
return [], offset
except httpx.TimeoutException:
return [], offset
except Exception:
logger.exception("getUpdates error")
return [], offset