Initial commit: Telegram Web Client with bot chat sync

- Backend: FastAPI + Telethon v2 WebSocket server
- Frontend: React + TypeScript + Vite + Zustand
- Features: Phone auth, 2FA, real-time bot chat
- Fix: Use chats= instead of from_users= to sync messages from all devices
- Config: BOT_USERNAME=AnvilForgeBot

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-05 13:55:22 +09:00
commit e610a45fcf
34 changed files with 1898 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Telegram module

View File

@@ -0,0 +1,77 @@
from typing import Optional, Callable, List
from telethon import events
from telethon.tl.types import Message
from config import BOT_USERNAME
from telegram.client import TelegramClientWrapper
from models.schemas import MessageResponse
class BotChatManager:
def __init__(self, client_wrapper: TelegramClientWrapper):
self.client_wrapper = client_wrapper
self.bot_entity = None
self._message_callback: Optional[Callable] = None
async def init_bot_chat(self):
"""Initialize chat with the bot"""
client = self.client_wrapper.client
if not client:
raise RuntimeError("Client not initialized")
# Resolve bot entity
self.bot_entity = await client.get_entity(BOT_USERNAME)
# Set up event handler for all messages in bot chat (incoming and outgoing)
@client.on(events.NewMessage(chats=self.bot_entity))
async def handle_bot_message(event: events.NewMessage):
if self._message_callback:
msg = event.message
response = MessageResponse(
id=msg.id,
text=msg.text,
date=msg.date,
is_outgoing=msg.out,
sender_name="You" if msg.out else BOT_USERNAME
)
await self._message_callback(response)
return self.bot_entity
def set_message_callback(self, callback: Callable):
"""Set callback for incoming messages"""
self._message_callback = callback
async def send_message(self, text: str) -> MessageResponse:
"""Send message to the bot"""
client = self.client_wrapper.client
if not client or not self.bot_entity:
raise RuntimeError("Bot chat not initialized")
msg = await client.send_message(self.bot_entity, text)
return MessageResponse(
id=msg.id,
text=msg.text,
date=msg.date,
is_outgoing=True,
sender_name="You"
)
async def get_history(self, limit: int = 50) -> List[MessageResponse]:
"""Get chat history with the bot"""
client = self.client_wrapper.client
if not client or not self.bot_entity:
raise RuntimeError("Bot chat not initialized")
messages = []
async for msg in client.iter_messages(self.bot_entity, limit=limit):
messages.append(MessageResponse(
id=msg.id,
text=msg.text,
date=msg.date,
is_outgoing=msg.out,
sender_name="You" if msg.out else BOT_USERNAME
))
# Return in chronological order
return list(reversed(messages))

105
backend/telegram/client.py Normal file
View File

@@ -0,0 +1,105 @@
import os
import asyncio
from typing import Optional, Callable, Any
from telethon import TelegramClient
from telethon.sessions import StringSession
from telethon.errors import SessionPasswordNeededError, PhoneCodeInvalidError
from telethon.tl.types import User
from config import API_ID, API_HASH, SESSION_DIR
class TelegramClientWrapper:
def __init__(self, session_id: str):
self.session_id = session_id
self.session_file = os.path.join(SESSION_DIR, f"{session_id}.session")
self.client: Optional[TelegramClient] = None
self.phone: Optional[str] = None
self.phone_code_hash: Optional[str] = None
self._message_callback: Optional[Callable] = None
async def init_client(self) -> TelegramClient:
"""Initialize or restore Telegram client"""
os.makedirs(SESSION_DIR, exist_ok=True)
# Use file-based session for persistence
self.client = TelegramClient(
self.session_file,
API_ID,
API_HASH,
system_version="4.16.30-vxCUSTOM"
)
await self.client.connect()
return self.client
async def is_authorized(self) -> bool:
"""Check if client is authorized"""
if not self.client:
await self.init_client()
return await self.client.is_user_authorized()
async def send_code(self, phone: str) -> str:
"""Send verification code to phone"""
if not self.client:
await self.init_client()
self.phone = phone
result = await self.client.send_code_request(phone)
self.phone_code_hash = result.phone_code_hash
return self.phone_code_hash
async def sign_in_with_code(self, phone: str, code: str, phone_code_hash: str) -> dict:
"""Sign in with verification code"""
if not self.client:
await self.init_client()
try:
user = await self.client.sign_in(phone, code, phone_code_hash=phone_code_hash)
return {
"success": True,
"user_id": user.id,
"username": user.username,
"first_name": user.first_name
}
except SessionPasswordNeededError:
return {
"success": False,
"needs_password": True,
"message": "Two-factor authentication required"
}
except PhoneCodeInvalidError:
return {
"success": False,
"needs_password": False,
"message": "Invalid verification code"
}
async def sign_in_with_password(self, password: str) -> dict:
"""Sign in with 2FA password"""
if not self.client:
await self.init_client()
try:
user = await self.client.sign_in(password=password)
return {
"success": True,
"user_id": user.id,
"username": user.username,
"first_name": user.first_name
}
except Exception as e:
return {
"success": False,
"message": str(e)
}
async def get_me(self) -> Optional[User]:
"""Get current user info"""
if not self.client or not await self.is_authorized():
return None
return await self.client.get_me()
async def disconnect(self):
"""Disconnect client"""
if self.client:
await self.client.disconnect()