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,201 @@
import { useEffect, useRef, useCallback } from 'react';
import { useChatStore } from '../stores/chatStore';
import type { Message } from '../types';
const WS_URL = import.meta.env.DEV
? 'ws://localhost:8000/ws'
: `wss://${window.location.host}/ws`;
export function useWebSocket() {
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | undefined>(undefined);
const {
setSessionId,
setConnected,
setAuthStep,
setPhone,
setPhoneCodeHash,
setUser,
addMessage,
setMessages,
setLoading,
setError,
} = useChatStore();
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) return;
const ws = new WebSocket(WS_URL);
wsRef.current = ws;
ws.onopen = () => {
console.log('WebSocket connected');
setConnected(true);
setError(null);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setConnected(false);
// Reconnect after 3 seconds
reconnectTimeoutRef.current = window.setTimeout(() => {
connect();
}, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setError('Connection error');
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleMessage(message);
} catch (e) {
console.error('Failed to parse message:', e);
}
};
}, []);
const handleMessage = useCallback(
(message: { type: string; data: Record<string, unknown> }) => {
const { type, data } = message;
switch (type) {
case 'connected':
setSessionId(data.session_id as string);
// Check auth status
send({ type: 'check_auth', data: {} });
break;
case 'auth_status':
if (data.authenticated) {
setUser({
id: data.user_id as number,
username: data.username as string,
firstName: data.first_name as string,
});
setAuthStep('authenticated');
// Get chat history
send({ type: 'get_history', data: {} });
} else {
setAuthStep('phone');
}
setLoading(false);
break;
case 'code_sent':
setPhone(data.phone as string);
setPhoneCodeHash(data.phone_code_hash as string);
setAuthStep('code');
setLoading(false);
break;
case 'need_password':
setAuthStep('password');
setLoading(false);
break;
case 'auth_success':
setUser({
id: data.user_id as number,
username: data.username as string,
firstName: data.first_name as string,
});
setAuthStep('authenticated');
setLoading(false);
// Get chat history
send({ type: 'get_history', data: {} });
break;
case 'auth_error':
setError(data.message as string);
setLoading(false);
break;
case 'history':
setMessages((data.messages as Message[]) || []);
setLoading(false);
break;
case 'message_sent':
addMessage(data as unknown as Message);
break;
case 'new_message':
addMessage(data as unknown as Message);
break;
case 'error':
setError(data.message as string);
setLoading(false);
break;
default:
console.log('Unknown message type:', type);
}
},
[]
);
const send = useCallback((message: { type: string; data: Record<string, unknown> }) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
}
}, []);
const sendCode = useCallback((phone: string) => {
setLoading(true);
send({ type: 'send_code', data: { phone } });
}, [send]);
const verifyCode = useCallback(
(code: string) => {
const { phone, phoneCodeHash } = useChatStore.getState();
setLoading(true);
send({
type: 'verify_code',
data: { phone, code, phone_code_hash: phoneCodeHash },
});
},
[send]
);
const verifyPassword = useCallback(
(password: string) => {
setLoading(true);
send({ type: 'verify_password', data: { password } });
},
[send]
);
const sendMessage = useCallback(
(text: string) => {
send({ type: 'send_message', data: { text } });
},
[send]
);
useEffect(() => {
connect();
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (wsRef.current) {
wsRef.current.close();
}
};
}, [connect]);
return {
sendCode,
verifyCode,
verifyPassword,
sendMessage,
};
}