2026-02-13 19:06:28 -07:00
|
|
|
"""
|
|
|
|
|
Telegram adapter for ajarbot.
|
|
|
|
|
|
|
|
|
|
Uses python-telegram-bot library for async Telegram Bot API integration.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-02-22 21:19:28 -07:00
|
|
|
import asyncio
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
import logging
|
2026-02-13 19:06:28 -07:00
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
from telegram import Bot, Update
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
from telegram.error import NetworkError, TelegramError
|
2026-02-13 19:06:28 -07:00
|
|
|
from telegram.ext import (
|
|
|
|
|
Application,
|
|
|
|
|
CommandHandler,
|
|
|
|
|
ContextTypes,
|
|
|
|
|
MessageHandler,
|
|
|
|
|
filters,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from adapters.base import (
|
|
|
|
|
AdapterCapabilities,
|
|
|
|
|
AdapterConfig,
|
|
|
|
|
BaseAdapter,
|
|
|
|
|
InboundMessage,
|
|
|
|
|
MessageType,
|
|
|
|
|
OutboundMessage,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TelegramAdapter(BaseAdapter):
|
|
|
|
|
"""
|
|
|
|
|
Telegram adapter using python-telegram-bot.
|
|
|
|
|
|
|
|
|
|
Configuration required:
|
|
|
|
|
- bot_token: Telegram Bot API Token (from @BotFather)
|
|
|
|
|
|
|
|
|
|
Optional settings:
|
|
|
|
|
- allowed_users: List of user IDs allowed to interact (for privacy)
|
|
|
|
|
- parse_mode: "HTML" or "Markdown" (default: "Markdown")
|
|
|
|
|
"""
|
|
|
|
|
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
_RECONNECT_DELAYS = [5, 15, 30, 60, 120] # seconds between startup retries
|
|
|
|
|
|
2026-02-13 19:06:28 -07:00
|
|
|
def __init__(self, config: AdapterConfig) -> None:
|
|
|
|
|
super().__init__(config)
|
|
|
|
|
self.application: Optional[Application] = None
|
|
|
|
|
self.bot: Optional[Bot] = None
|
2026-02-22 21:19:28 -07:00
|
|
|
self._polling_task: Optional[asyncio.Task] = None
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
self._watchdog_task: Optional[asyncio.Task] = None
|
|
|
|
|
self._logger = logging.getLogger(__name__)
|
2026-02-13 19:06:28 -07:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def platform_name(self) -> str:
|
|
|
|
|
return "telegram"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def capabilities(self) -> AdapterCapabilities:
|
|
|
|
|
return AdapterCapabilities(
|
|
|
|
|
supports_threads=False,
|
|
|
|
|
supports_reactions=True,
|
|
|
|
|
supports_media=True,
|
|
|
|
|
supports_files=True,
|
|
|
|
|
supports_markdown=True,
|
|
|
|
|
max_message_length=4096,
|
|
|
|
|
chunking_strategy="markdown",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def validate_config(self) -> bool:
|
|
|
|
|
"""Validate Telegram configuration."""
|
|
|
|
|
if not self.config.credentials:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
bot_token = self.config.credentials.get("bot_token", "")
|
|
|
|
|
return bool(bot_token and len(bot_token) > 20)
|
|
|
|
|
|
|
|
|
|
async def start(self) -> None:
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
"""Start the Telegram bot with retry on network failures."""
|
2026-02-13 19:06:28 -07:00
|
|
|
if not self.validate_config():
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Invalid Telegram configuration. Need bot_token"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
bot_token = self.config.credentials["bot_token"]
|
|
|
|
|
|
|
|
|
|
self.application = (
|
|
|
|
|
Application.builder().token(bot_token).build()
|
|
|
|
|
)
|
|
|
|
|
self.bot = self.application.bot
|
|
|
|
|
|
|
|
|
|
self._register_handlers()
|
|
|
|
|
|
|
|
|
|
print("[Telegram] Starting bot...")
|
|
|
|
|
await self.application.initialize()
|
|
|
|
|
await self.application.start()
|
2026-02-22 21:19:28 -07:00
|
|
|
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
# start_polling() starts internal Updater machinery and returns quickly.
|
|
|
|
|
# Use updater.running (not task state) to detect if polling is alive.
|
|
|
|
|
await self.application.updater.start_polling(
|
|
|
|
|
allowed_updates=Update.ALL_TYPES,
|
|
|
|
|
drop_pending_updates=True,
|
2026-02-13 19:06:28 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.is_running = True
|
|
|
|
|
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
# Verify connectivity with retries — get_me() fails on DNS errors at boot.
|
|
|
|
|
me = await self._get_me_with_retry()
|
|
|
|
|
if me:
|
|
|
|
|
print(f"[Telegram] Bot started: @{me.username} ({me.first_name})")
|
|
|
|
|
else:
|
|
|
|
|
print("[Telegram] Bot started (could not verify identity — network may be degraded)")
|
|
|
|
|
|
|
|
|
|
# Watchdog restarts polling if the task dies unexpectedly.
|
|
|
|
|
self._watchdog_task = asyncio.create_task(self._polling_watchdog())
|
|
|
|
|
|
|
|
|
|
async def _get_me_with_retry(self):
|
|
|
|
|
"""Call get_me() with exponential backoff. Returns None if all attempts fail."""
|
|
|
|
|
for attempt, delay in enumerate(self._RECONNECT_DELAYS, 1):
|
|
|
|
|
try:
|
|
|
|
|
return await self.bot.get_me()
|
|
|
|
|
except NetworkError as e:
|
|
|
|
|
if attempt <= len(self._RECONNECT_DELAYS):
|
|
|
|
|
print(f"[Telegram] Network error on startup (attempt {attempt}): {e} — retrying in {delay}s")
|
|
|
|
|
await asyncio.sleep(delay)
|
|
|
|
|
# Last attempt with no retry after
|
|
|
|
|
try:
|
|
|
|
|
return await self.bot.get_me()
|
|
|
|
|
except NetworkError as e:
|
|
|
|
|
print(f"[Telegram] Could not reach Telegram API after {len(self._RECONNECT_DELAYS)+1} attempts: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def _polling_watchdog(self) -> None:
|
|
|
|
|
"""Monitor the Updater and restart polling if it stops unexpectedly."""
|
|
|
|
|
# Give polling a moment to fully initialise before we start watching.
|
|
|
|
|
await asyncio.sleep(15)
|
|
|
|
|
while self.is_running:
|
|
|
|
|
await asyncio.sleep(30)
|
|
|
|
|
if not self.is_running:
|
|
|
|
|
break
|
|
|
|
|
if not self.application or self.application.updater.running:
|
|
|
|
|
continue # All good
|
|
|
|
|
|
|
|
|
|
self._logger.warning("[Telegram] Updater is no longer running — attempting restart")
|
|
|
|
|
print("[Telegram] Polling dropped — restarting...")
|
|
|
|
|
for attempt, delay in enumerate(self._RECONNECT_DELAYS, 1):
|
|
|
|
|
await asyncio.sleep(delay)
|
|
|
|
|
if not self.is_running:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
await self.application.updater.start_polling(
|
|
|
|
|
allowed_updates=Update.ALL_TYPES,
|
|
|
|
|
drop_pending_updates=False,
|
|
|
|
|
)
|
|
|
|
|
print(f"[Telegram] Polling restarted (attempt {attempt})")
|
|
|
|
|
break
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"[Telegram] Restart attempt {attempt} failed: {e}")
|
2026-02-13 19:06:28 -07:00
|
|
|
|
|
|
|
|
async def stop(self) -> None:
|
|
|
|
|
"""Stop the Telegram bot."""
|
feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements:
- RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector
- Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection
- Rich conversation storage for notable turns; compact_conversation truncates long user messages
- Task-type classifier (query/action/analysis/creative) for observation tagging
- Nested sub-agent visibility: deep delegations now register against the main agent's manager
Child safety (Gabriel profile):
- child_safety.py: filtering, audit logging, prompt constants for restricted sessions
- .kiro/specs/child-safety-profile: requirements, design, tasks specs
- GABRIEL_BOT_PROPOSAL.md: initial proposal doc
- Reduced context window (10 msgs) and tutor-mode identity for restricted users
Telegram adapter:
- Polling watchdog: auto-restarts updater if polling drops unexpectedly
- get_me() with exponential-backoff retry on NetworkError at startup
- Correct stop() ordering: signal watchdog before cancelling tasks
Email / Gmail:
- send_email: supports file attachments (attachments list param)
- get_email: surfaces attachment metadata in response
Scheduled tasks / weather:
- Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively
- New scheduled tasks and scheduler state persistence
Discord:
- adapters/discord/__init__.py scaffold
- discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config)
Infrastructure:
- n8n workflow exports (garvis_webhook, content_pipeline variants)
- memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs
- UCS C240 migration plan doc
- requirements.txt: new deps
- .claude/settings.json, fix_hooks.py: hook/permission tuning
2026-04-23 07:54:01 -06:00
|
|
|
self.is_running = False # Signal watchdog to exit before cancelling tasks
|
|
|
|
|
|
|
|
|
|
if self._watchdog_task and not self._watchdog_task.done():
|
|
|
|
|
self._watchdog_task.cancel()
|
|
|
|
|
try:
|
|
|
|
|
await self._watchdog_task
|
|
|
|
|
except (asyncio.CancelledError, Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
2026-02-13 19:06:28 -07:00
|
|
|
if self.application:
|
|
|
|
|
print("[Telegram] Stopping bot...")
|
|
|
|
|
await self.application.updater.stop()
|
|
|
|
|
await self.application.stop()
|
|
|
|
|
await self.application.shutdown()
|
2026-02-22 21:19:28 -07:00
|
|
|
|
|
|
|
|
print("[Telegram] Bot stopped")
|
2026-02-13 19:06:28 -07:00
|
|
|
|
|
|
|
|
def _register_handlers(self) -> None:
|
|
|
|
|
"""Register Telegram message handlers."""
|
|
|
|
|
|
|
|
|
|
async def handle_message(
|
|
|
|
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Handle incoming text messages."""
|
|
|
|
|
if not update.message or not update.message.text:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not self._is_user_allowed(update.effective_user.id):
|
|
|
|
|
await update.message.reply_text(
|
|
|
|
|
"Sorry, you are not authorized to use this bot."
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
user = update.effective_user
|
|
|
|
|
message = update.message
|
|
|
|
|
|
|
|
|
|
reply_to_id = None
|
|
|
|
|
if message.reply_to_message:
|
|
|
|
|
reply_to_id = str(message.reply_to_message.message_id)
|
|
|
|
|
|
|
|
|
|
# Sanitize username: replace spaces/special chars with underscores
|
|
|
|
|
raw_username = user.username or user.first_name or str(user.id)
|
|
|
|
|
sanitized_username = "".join(
|
|
|
|
|
c if c.isalnum() or c in "-_" else "_" for c in raw_username
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
inbound_msg = InboundMessage(
|
|
|
|
|
platform="telegram",
|
|
|
|
|
user_id=str(user.id),
|
|
|
|
|
username=sanitized_username,
|
|
|
|
|
text=message.text,
|
|
|
|
|
channel_id=str(message.chat.id),
|
|
|
|
|
thread_id=None,
|
|
|
|
|
reply_to_id=reply_to_id,
|
|
|
|
|
message_type=MessageType.TEXT,
|
|
|
|
|
metadata={
|
|
|
|
|
"message_id": message.message_id,
|
|
|
|
|
"chat_type": message.chat.type,
|
|
|
|
|
"date": (
|
|
|
|
|
message.date.isoformat()
|
|
|
|
|
if message.date
|
|
|
|
|
else None
|
|
|
|
|
),
|
|
|
|
|
"user_full_name": user.full_name,
|
|
|
|
|
"is_bot": user.is_bot,
|
|
|
|
|
},
|
|
|
|
|
raw=update,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self._dispatch_message(inbound_msg)
|
|
|
|
|
|
|
|
|
|
async def handle_start(
|
|
|
|
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Handle /start command."""
|
|
|
|
|
await update.message.reply_text(
|
|
|
|
|
"Hello! I'm an AI assistant bot.\n\n"
|
|
|
|
|
"Just send me a message and I'll respond!"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def handle_help(
|
|
|
|
|
update: Update, context: ContextTypes.DEFAULT_TYPE
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Handle /help command."""
|
|
|
|
|
await update.message.reply_text(
|
|
|
|
|
"*Ajarbot Help*\n\n"
|
|
|
|
|
"I'm an AI assistant. You can:\n"
|
|
|
|
|
"- Send me messages and I'll respond\n"
|
|
|
|
|
"- Have natural conversations\n"
|
|
|
|
|
"- Ask me questions\n\n"
|
|
|
|
|
"Commands:\n"
|
|
|
|
|
"/start - Start the bot\n"
|
|
|
|
|
"/help - Show this help message",
|
|
|
|
|
parse_mode="Markdown",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.application.add_handler(
|
|
|
|
|
CommandHandler("start", handle_start)
|
|
|
|
|
)
|
|
|
|
|
self.application.add_handler(
|
|
|
|
|
CommandHandler("help", handle_help)
|
|
|
|
|
)
|
|
|
|
|
self.application.add_handler(
|
|
|
|
|
MessageHandler(
|
|
|
|
|
filters.TEXT & ~filters.COMMAND, handle_message
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def send_message(
|
|
|
|
|
self, message: OutboundMessage
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""Send a message to Telegram."""
|
|
|
|
|
if not self.bot:
|
|
|
|
|
return {"success": False, "error": "Bot not started"}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
chat_id = int(message.channel_id)
|
|
|
|
|
parse_mode = "Markdown"
|
|
|
|
|
if self.config.settings:
|
|
|
|
|
parse_mode = self.config.settings.get(
|
|
|
|
|
"parse_mode", "Markdown"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
chunks = self.chunk_text(message.text)
|
|
|
|
|
results: List[Dict[str, Any]] = []
|
|
|
|
|
|
|
|
|
|
for chunk in chunks:
|
|
|
|
|
reply_to_id = (
|
|
|
|
|
int(message.reply_to_id)
|
|
|
|
|
if message.reply_to_id
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-13 23:38:44 -07:00
|
|
|
try:
|
|
|
|
|
sent_message = await self.bot.send_message(
|
|
|
|
|
chat_id=chat_id,
|
|
|
|
|
text=chunk,
|
|
|
|
|
parse_mode=parse_mode,
|
|
|
|
|
reply_to_message_id=reply_to_id,
|
|
|
|
|
)
|
|
|
|
|
except TelegramError:
|
|
|
|
|
# Markdown parse errors are common with LLM-generated
|
|
|
|
|
# text (unbalanced *, _, etc). Fall back to plain text.
|
|
|
|
|
sent_message = await self.bot.send_message(
|
|
|
|
|
chat_id=chat_id,
|
|
|
|
|
text=chunk,
|
|
|
|
|
parse_mode=None,
|
|
|
|
|
reply_to_message_id=reply_to_id,
|
|
|
|
|
)
|
2026-02-13 19:06:28 -07:00
|
|
|
|
|
|
|
|
results.append({
|
|
|
|
|
"message_id": sent_message.message_id,
|
|
|
|
|
"chat_id": sent_message.chat_id,
|
|
|
|
|
"date": (
|
|
|
|
|
sent_message.date.isoformat()
|
|
|
|
|
if sent_message.date
|
|
|
|
|
else None
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"message_id": results[0]["message_id"],
|
|
|
|
|
"chunks_sent": len(chunks),
|
|
|
|
|
"results": results,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except TelegramError as e:
|
|
|
|
|
print(f"[Telegram] Error sending message: {e}")
|
|
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
async def send_reaction(
|
|
|
|
|
self, channel_id: str, message_id: str, emoji: str
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""Send a reaction to a message."""
|
|
|
|
|
if not self.bot:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await self.bot.set_message_reaction(
|
|
|
|
|
chat_id=int(channel_id),
|
|
|
|
|
message_id=int(message_id),
|
|
|
|
|
reaction=emoji,
|
|
|
|
|
)
|
|
|
|
|
return True
|
|
|
|
|
except TelegramError as e:
|
|
|
|
|
print(f"[Telegram] Error adding reaction: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def send_typing_indicator(self, channel_id: str) -> None:
|
|
|
|
|
"""Show typing indicator."""
|
|
|
|
|
if not self.bot:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await self.bot.send_chat_action(
|
|
|
|
|
chat_id=int(channel_id), action="typing"
|
|
|
|
|
)
|
|
|
|
|
except TelegramError as e:
|
|
|
|
|
print(f"[Telegram] Error sending typing indicator: {e}")
|
|
|
|
|
|
2026-03-01 14:34:24 -07:00
|
|
|
async def send_file(
|
|
|
|
|
self,
|
|
|
|
|
channel_id: str,
|
|
|
|
|
file_path: str,
|
|
|
|
|
caption: Optional[str] = None,
|
|
|
|
|
thread_id: Optional[str] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""Send a file (image or document) to Telegram."""
|
|
|
|
|
if not self.bot:
|
|
|
|
|
return {"success": False, "error": "Bot not started"}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
path = Path(file_path)
|
|
|
|
|
|
|
|
|
|
if not path.exists():
|
|
|
|
|
return {"success": False, "error": f"File not found: {file_path}"}
|
|
|
|
|
|
|
|
|
|
chat_id = int(channel_id)
|
|
|
|
|
reply_to = int(thread_id) if thread_id else None
|
|
|
|
|
ext = path.suffix.lower()
|
|
|
|
|
|
|
|
|
|
# Send as photo for images, document for others
|
|
|
|
|
if ext in [".png", ".jpg", ".jpeg", ".gif", ".webp"]:
|
|
|
|
|
with open(path, "rb") as photo:
|
|
|
|
|
sent = await self.bot.send_photo(
|
|
|
|
|
chat_id=chat_id,
|
|
|
|
|
photo=photo,
|
|
|
|
|
caption=caption,
|
|
|
|
|
reply_to_message_id=reply_to,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
with open(path, "rb") as document:
|
|
|
|
|
sent = await self.bot.send_document(
|
|
|
|
|
chat_id=chat_id,
|
|
|
|
|
document=document,
|
|
|
|
|
caption=caption,
|
|
|
|
|
reply_to_message_id=reply_to,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"message_id": sent.message_id,
|
|
|
|
|
"file_path": file_path,
|
|
|
|
|
}
|
|
|
|
|
except TelegramError as e:
|
|
|
|
|
print(f"[Telegram] Error sending file: {e}")
|
|
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"[Telegram] Unexpected error sending file: {e}")
|
|
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
2026-02-13 19:06:28 -07:00
|
|
|
async def health_check(self) -> Dict[str, Any]:
|
|
|
|
|
"""Perform health check."""
|
|
|
|
|
base_health = await super().health_check()
|
|
|
|
|
|
|
|
|
|
if not self.bot:
|
|
|
|
|
return {**base_health, "details": "Bot not initialized"}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
me = await self.bot.get_me()
|
|
|
|
|
return {
|
|
|
|
|
**base_health,
|
|
|
|
|
"bot_id": me.id,
|
|
|
|
|
"username": me.username,
|
|
|
|
|
"first_name": me.first_name,
|
|
|
|
|
"can_join_groups": me.can_join_groups,
|
|
|
|
|
"can_read_all_group_messages": (
|
|
|
|
|
me.can_read_all_group_messages
|
|
|
|
|
),
|
|
|
|
|
"connected": True,
|
|
|
|
|
}
|
|
|
|
|
except TelegramError as e:
|
|
|
|
|
return {
|
|
|
|
|
**base_health,
|
|
|
|
|
"healthy": False,
|
|
|
|
|
"error": str(e),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _is_user_allowed(self, user_id: int) -> bool:
|
|
|
|
|
"""Check if user is allowed to interact with the bot."""
|
|
|
|
|
if not self.config.settings:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
allowed_users = self.config.settings.get("allowed_users", [])
|
|
|
|
|
if not allowed_users:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
return user_id in allowed_users
|
|
|
|
|
|
|
|
|
|
def chunk_text(self, text: str) -> List[str]:
|
|
|
|
|
"""
|
|
|
|
|
Override chunk_text for Telegram-specific markdown handling.
|
|
|
|
|
|
|
|
|
|
Preserves markdown code blocks and formatting.
|
|
|
|
|
"""
|
|
|
|
|
max_len = self.capabilities.max_message_length
|
|
|
|
|
|
|
|
|
|
if len(text) <= max_len:
|
|
|
|
|
return [text]
|
|
|
|
|
|
|
|
|
|
chunks: List[str] = []
|
|
|
|
|
current_chunk = ""
|
|
|
|
|
|
|
|
|
|
# Split by code blocks first to preserve them
|
|
|
|
|
parts = text.split("```")
|
|
|
|
|
in_code_block = False
|
|
|
|
|
|
|
|
|
|
for part in parts:
|
|
|
|
|
if in_code_block:
|
|
|
|
|
code_block = f"```{part}```"
|
|
|
|
|
if len(current_chunk) + len(code_block) > max_len:
|
|
|
|
|
if current_chunk:
|
|
|
|
|
chunks.append(current_chunk)
|
|
|
|
|
chunks.append(code_block)
|
|
|
|
|
current_chunk = ""
|
|
|
|
|
else:
|
|
|
|
|
current_chunk += code_block
|
|
|
|
|
else:
|
|
|
|
|
# Regular text - split by paragraphs
|
|
|
|
|
paragraphs = part.split("\n\n")
|
|
|
|
|
for para in paragraphs:
|
|
|
|
|
if len(current_chunk) + len(para) + 2 > max_len:
|
|
|
|
|
if current_chunk:
|
|
|
|
|
chunks.append(current_chunk.strip())
|
|
|
|
|
current_chunk = para + "\n\n"
|
|
|
|
|
else:
|
|
|
|
|
current_chunk += para + "\n\n"
|
|
|
|
|
|
|
|
|
|
in_code_block = not in_code_block
|
|
|
|
|
|
|
|
|
|
if current_chunk.strip():
|
|
|
|
|
chunks.append(current_chunk.strip())
|
|
|
|
|
|
|
|
|
|
return chunks if chunks else [text]
|