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:
1
backend/telegram/__init__.py
Normal file
1
backend/telegram/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Telegram module
|
||||
77
backend/telegram/bot_chat.py
Normal file
77
backend/telegram/bot_chat.py
Normal 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
105
backend/telegram/client.py
Normal 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()
|
||||
Reference in New Issue
Block a user