mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
Implement TelegramProvider for improved connection management and refactor TelegramConnectionModal
- Introduced TelegramProvider to manage the Telegram MTProto connection, handling initialization and connection status updates. - Refactored TelegramConnectionModal to remove unused authStatus selector, streamlining the component. - Enhanced MTProtoService with FLOOD_WAIT handling for connection checks and message sending, improving reliability during Telegram interactions. - Updated telegramSlice to prevent concurrent authentication checks, optimizing performance during user authentication. These changes enhance the application's ability to manage Telegram connections effectively, providing a smoother user experience during authentication and interaction.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
setAuthError,
|
||||
setConnectionStatus,
|
||||
} from '../store/telegramSlice';
|
||||
import { selectIsInitialized, selectConnectionStatus, selectAuthStatus } from '../store/telegramSelectors';
|
||||
import { selectIsInitialized, selectConnectionStatus } from '../store/telegramSelectors';
|
||||
import { mtprotoService } from '../services/mtprotoService';
|
||||
|
||||
interface TelegramConnectionModalProps {
|
||||
@@ -25,7 +25,6 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
const dispatch = useAppDispatch();
|
||||
const isInitialized = useAppSelector(selectIsInitialized);
|
||||
const connectionStatus = useAppSelector(selectConnectionStatus);
|
||||
const authStatus = useAppSelector(selectAuthStatus);
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<ConnectionStep>('qr');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useRef, useMemo, createContext, useContext, ReactNode } from 'react';
|
||||
import { useAppSelector, useAppDispatch } from '../store/hooks';
|
||||
import { selectIsAuthenticated, selectIsInitialized, selectConnectionStatus } from '../store/telegramSelectors';
|
||||
import { initializeTelegram, connectTelegram } from '../store/telegramSlice';
|
||||
import { mtprotoService } from '../services/mtprotoService';
|
||||
|
||||
// Helper to check if there's a saved session in localStorage
|
||||
const hasSavedSession = (): boolean => {
|
||||
try {
|
||||
return !!localStorage.getItem('telegram_session');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
interface TelegramContextType {
|
||||
isAuthenticated: boolean;
|
||||
isInitialized: boolean;
|
||||
connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||
checkConnection: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const TelegramContext = createContext<TelegramContextType | undefined>(undefined);
|
||||
|
||||
export const useTelegram = () => {
|
||||
const context = useContext(TelegramContext);
|
||||
if (!context) {
|
||||
throw new Error('useTelegram must be used within TelegramProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface TelegramProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* TelegramProvider manages the Telegram MTProto connection
|
||||
* - Initializes when authenticated
|
||||
* - Connects automatically
|
||||
* - Provides Telegram context to children
|
||||
*/
|
||||
const TelegramProvider = ({ children }: TelegramProviderProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||
const isInitialized = useAppSelector(selectIsInitialized);
|
||||
const connectionStatus = useAppSelector(selectConnectionStatus);
|
||||
const sessionString = useAppSelector((state) => state.telegram.sessionString);
|
||||
const initializedRef = useRef(false);
|
||||
const connectedRef = useRef(false);
|
||||
const setupInProgressRef = useRef(false);
|
||||
const setupCompleteRef = useRef(false);
|
||||
|
||||
// Memoize hasSession to prevent unnecessary recalculations
|
||||
const hasSession = useMemo(() => {
|
||||
return !!sessionString || hasSavedSession();
|
||||
}, [sessionString]);
|
||||
|
||||
// Initialize Telegram when authenticated or when we have a saved session
|
||||
useEffect(() => {
|
||||
// Reset refs if we're not authenticated and have no session
|
||||
if (!isAuthenticated && !hasSession) {
|
||||
initializedRef.current = false;
|
||||
connectedRef.current = false;
|
||||
setupInProgressRef.current = false;
|
||||
setupCompleteRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If setup is already complete and everything is connected, don't run again
|
||||
if (setupCompleteRef.current && isInitialized && connectionStatus === 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent multiple simultaneous setup attempts
|
||||
if (setupInProgressRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setupTelegram = async () => {
|
||||
// Mark setup as in progress
|
||||
setupInProgressRef.current = true;
|
||||
|
||||
try {
|
||||
// Initialize if not already initialized
|
||||
if (!isInitialized && !initializedRef.current) {
|
||||
initializedRef.current = true;
|
||||
await dispatch(initializeTelegram()).unwrap();
|
||||
setupInProgressRef.current = false;
|
||||
return; // Exit early, will re-run when isInitialized updates
|
||||
}
|
||||
|
||||
// Connect if not already connected
|
||||
if (isInitialized && connectionStatus !== 'connected' && !connectedRef.current) {
|
||||
connectedRef.current = true;
|
||||
await dispatch(connectTelegram()).unwrap();
|
||||
setupInProgressRef.current = false;
|
||||
return; // Exit early, will re-run when connectionStatus updates
|
||||
}
|
||||
|
||||
// Setup complete - mark as done
|
||||
setupInProgressRef.current = false;
|
||||
setupCompleteRef.current = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to setup Telegram:', error);
|
||||
initializedRef.current = false;
|
||||
connectedRef.current = false;
|
||||
setupInProgressRef.current = false;
|
||||
setupCompleteRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
setupTelegram();
|
||||
}, [isAuthenticated, hasSession, isInitialized, connectionStatus, dispatch]);
|
||||
|
||||
// Check connection status periodically to keep user online
|
||||
useEffect(() => {
|
||||
if ((!isAuthenticated && !hasSession) || !isInitialized || connectionStatus !== 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkConnection = async () => {
|
||||
try {
|
||||
await mtprotoService.checkConnection();
|
||||
} catch (error) {
|
||||
console.warn('Telegram connection check failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately, then every 20 seconds
|
||||
checkConnection();
|
||||
const interval = setInterval(checkConnection, 20000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAuthenticated, hasSession, isInitialized, connectionStatus]);
|
||||
|
||||
const checkConnection = async (): Promise<boolean> => {
|
||||
try {
|
||||
return await mtprotoService.checkConnection();
|
||||
} catch (error) {
|
||||
console.warn('Connection check failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const value: TelegramContextType = {
|
||||
isAuthenticated: isAuthenticated || hasSession,
|
||||
isInitialized,
|
||||
connectionStatus,
|
||||
checkConnection,
|
||||
};
|
||||
|
||||
return <TelegramContext.Provider value={value}>{children}</TelegramContext.Provider>;
|
||||
};
|
||||
|
||||
export default TelegramProvider;
|
||||
+115
-10
@@ -1,6 +1,7 @@
|
||||
import { TelegramClient } from 'telegram';
|
||||
import { StringSession } from 'telegram/sessions';
|
||||
import type { UserAuthParams, BotAuthParams } from 'telegram/client/auth';
|
||||
import { FloodWaitError } from 'telegram/errors';
|
||||
import { TELEGRAM_API_ID, TELEGRAM_API_HASH } from '../utils/config';
|
||||
|
||||
type LoginOptions = UserAuthParams | BotAuthParams;
|
||||
@@ -48,6 +49,8 @@ class MTProtoService {
|
||||
|
||||
this.client = new TelegramClient(stringSession, this.apiId, this.apiHash, {
|
||||
connectionRetries: 5,
|
||||
requestRetries: 5,
|
||||
floodSleepThreshold: 60, // Auto-retry FLOOD_WAIT errors up to 60 seconds
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
@@ -77,7 +80,7 @@ class MTProtoService {
|
||||
console.log('Connected to Telegram successfully');
|
||||
|
||||
// Save session string if it changed
|
||||
const newSessionString = this.client.session.save();
|
||||
const newSessionString = this.client.session.save() as string | undefined;
|
||||
if (newSessionString && newSessionString !== this.sessionString) {
|
||||
this.sessionString = newSessionString;
|
||||
this.saveSession(newSessionString);
|
||||
@@ -101,7 +104,7 @@ class MTProtoService {
|
||||
await this.client.start(options);
|
||||
|
||||
// Save session after successful login
|
||||
const newSessionString = this.client.session.save();
|
||||
const newSessionString = this.client.session.save() as string | undefined;
|
||||
if (newSessionString && newSessionString !== this.sessionString) {
|
||||
this.sessionString = newSessionString;
|
||||
this.saveSession(newSessionString);
|
||||
@@ -136,7 +139,7 @@ class MTProtoService {
|
||||
qrCodeCallback(qrCode);
|
||||
},
|
||||
password: passwordCallback,
|
||||
onError: async (err: Error) => {
|
||||
onError: async (err: Error): Promise<boolean> => {
|
||||
// If password callback is provided and we get SESSION_PASSWORD_NEEDED,
|
||||
// the password callback should handle it, but if onError is called first,
|
||||
// we need to let it through
|
||||
@@ -145,14 +148,14 @@ class MTProtoService {
|
||||
// Don't stop - let the password callback handle it
|
||||
if (onError) {
|
||||
const result = await onError(err);
|
||||
return result;
|
||||
return result ?? false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
const result = await onError(err);
|
||||
return result;
|
||||
return result ?? false;
|
||||
}
|
||||
console.error('QR code auth error:', err);
|
||||
return false;
|
||||
@@ -161,7 +164,7 @@ class MTProtoService {
|
||||
);
|
||||
|
||||
// Save session after successful login
|
||||
const newSessionString = this.client.session.save();
|
||||
const newSessionString = this.client.session.save() as string | undefined;
|
||||
if (newSessionString && newSessionString !== this.sessionString) {
|
||||
this.sessionString = newSessionString;
|
||||
this.saveSession(newSessionString);
|
||||
@@ -221,6 +224,45 @@ class MTProtoService {
|
||||
return this.sessionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check connection status and update user online status
|
||||
* This calls getMe() which also updates the user's online status on Telegram
|
||||
* Automatically initializes and connects if needed
|
||||
*/
|
||||
async checkConnection(): Promise<boolean> {
|
||||
try {
|
||||
// Initialize if not already initialized
|
||||
if (!this.isInitialized || !this.client) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// Connect if not already connected
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
const isAuthorized = await this.client!.checkAuthorization();
|
||||
if (!isAuthorized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Call getMe() to check connection and update online status with FLOOD_WAIT handling
|
||||
await this.handleFloodWait(async () => {
|
||||
await this.client!.getMe();
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Don't log FLOOD_WAIT as a warning - it's expected behavior
|
||||
if (error instanceof FloodWaitError) {
|
||||
console.debug(`Telegram connection check: FLOOD_WAIT ${error.seconds}s`);
|
||||
} else {
|
||||
console.warn('Telegram connection check failed:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Telegram
|
||||
*/
|
||||
@@ -238,25 +280,88 @@ class MTProtoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message using the client
|
||||
* Send a message using the client with FLOOD_WAIT handling
|
||||
*/
|
||||
async sendMessage(entity: string, message: string): Promise<void> {
|
||||
const client = this.getClient();
|
||||
if (!this.isClientConnected()) {
|
||||
await this.connect();
|
||||
}
|
||||
await client.sendMessage(entity, { message });
|
||||
|
||||
return this.handleFloodWait(async () => {
|
||||
await client.sendMessage(entity, { message });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a raw Telegram API method
|
||||
* Handle FLOOD_WAIT errors by waiting and retrying
|
||||
* @param operation The async operation to execute
|
||||
* @param maxRetries Maximum number of retry attempts (default: 3)
|
||||
* @param retryCount Current retry count (internal use)
|
||||
* @returns The result of the operation
|
||||
*/
|
||||
private async handleFloodWait<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries = 3,
|
||||
retryCount = 0
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
// Check if it's a FLOOD_WAIT error
|
||||
if (error instanceof FloodWaitError) {
|
||||
const waitSeconds = error.seconds;
|
||||
|
||||
// If wait time is too long (more than 5 minutes), throw error
|
||||
if (waitSeconds > 300) {
|
||||
throw new Error(`FLOOD_WAIT: Too long wait time (${waitSeconds}s). Please try again later.`);
|
||||
}
|
||||
|
||||
// If we've exceeded max retries, throw error
|
||||
if (retryCount >= maxRetries) {
|
||||
throw new Error(`FLOOD_WAIT: Maximum retries exceeded. Wait ${waitSeconds}s before trying again.`);
|
||||
}
|
||||
|
||||
console.warn(`FLOOD_WAIT: Waiting ${waitSeconds} seconds before retry (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
|
||||
// Wait for the specified time (convert to milliseconds)
|
||||
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
|
||||
|
||||
// Retry the operation
|
||||
return this.handleFloodWait(operation, maxRetries, retryCount + 1);
|
||||
}
|
||||
|
||||
// If it's not a FLOOD_WAIT error, rethrow it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an operation with FLOOD_WAIT error handling
|
||||
* This is a public utility method that can be used to wrap any Telegram API call
|
||||
* @param operation The async operation to execute
|
||||
* @param maxRetries Maximum number of retry attempts (default: 3)
|
||||
* @returns The result of the operation
|
||||
*/
|
||||
async withFloodWaitHandling<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
): Promise<T> {
|
||||
return this.handleFloodWait(operation, maxRetries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a raw Telegram API method with FLOOD_WAIT handling
|
||||
*/
|
||||
async invoke<T = unknown>(request: Parameters<TelegramClient['invoke']>[0]): Promise<T> {
|
||||
const client = this.getClient();
|
||||
if (!this.isClientConnected()) {
|
||||
await this.connect();
|
||||
}
|
||||
return client.invoke(request) as Promise<T>;
|
||||
|
||||
return this.handleFloodWait(async () => {
|
||||
return client.invoke(request) as Promise<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+47
-14
@@ -186,9 +186,25 @@ export const connectTelegram = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
// Global flag to prevent concurrent checkAuthStatus calls
|
||||
let isCheckingAuth = false;
|
||||
let lastCheckTime = 0;
|
||||
const MIN_CHECK_INTERVAL = 5000; // 5 seconds minimum between checks
|
||||
|
||||
export const checkAuthStatus = createAsyncThunk(
|
||||
'telegram/checkAuthStatus',
|
||||
async (_, { rejectWithValue }) => {
|
||||
async (_, { rejectWithValue, getState }) => {
|
||||
// Prevent concurrent calls
|
||||
const now = Date.now();
|
||||
if (isCheckingAuth && now - lastCheckTime < MIN_CHECK_INTERVAL) {
|
||||
// Return current state instead of making another call
|
||||
const state = getState() as { telegram: { authStatus: string; currentUser: TelegramUser | null } };
|
||||
return state.telegram.currentUser || null;
|
||||
}
|
||||
|
||||
isCheckingAuth = true;
|
||||
lastCheckTime = now;
|
||||
|
||||
try {
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
@@ -196,20 +212,26 @@ export const checkAuthStatus = createAsyncThunk(
|
||||
const isAuthorized = await client.checkAuthorization();
|
||||
|
||||
if (!isAuthorized) {
|
||||
isCheckingAuth = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only call getMe() if we're authorized
|
||||
// Only call getMe() if we're authorized, with FLOOD_WAIT handling
|
||||
try {
|
||||
const me = await client.getMe();
|
||||
const me = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
return client.getMe();
|
||||
});
|
||||
isCheckingAuth = false;
|
||||
return me;
|
||||
} catch (error) {
|
||||
// If getMe() fails, we're not actually authorized
|
||||
// This can happen if the session is invalid
|
||||
console.warn('getMe() failed, user not authenticated:', error);
|
||||
isCheckingAuth = false;
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
isCheckingAuth = false;
|
||||
// If checkAuthorization itself fails, we're definitely not authenticated
|
||||
// Don't treat this as an error, just return null
|
||||
if (error instanceof Error && error.message.includes('AUTH_KEY_UNREGISTERED')) {
|
||||
@@ -227,7 +249,9 @@ export const fetchChats = createAsyncThunk(
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const client = mtprotoService.getClient();
|
||||
const dialogs = await client.getDialogs({ limit: 100 });
|
||||
const dialogs = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
return client.getDialogs({ limit: 100 });
|
||||
});
|
||||
return dialogs;
|
||||
} catch (error) {
|
||||
return rejectWithValue(
|
||||
@@ -247,7 +271,9 @@ export const fetchMessages = createAsyncThunk(
|
||||
const client = mtprotoService.getClient();
|
||||
// Implementation depends on GramJS API
|
||||
// This is a placeholder - adjust based on actual API
|
||||
const messages = await client.getMessages(chatId, { limit, offsetId });
|
||||
const messages = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
return client.getMessages(chatId, { limit, offsetId });
|
||||
});
|
||||
return { chatId, messages };
|
||||
} catch (error) {
|
||||
return rejectWithValue(
|
||||
@@ -485,15 +511,22 @@ const telegramSlice = createSlice({
|
||||
if (action.payload) {
|
||||
state.authStatus = 'authenticated';
|
||||
// Convert Api.User to TelegramUser
|
||||
// This is a placeholder - adjust based on actual API response
|
||||
state.currentUser = {
|
||||
id: String(action.payload.id),
|
||||
firstName: action.payload.firstName || '',
|
||||
lastName: action.payload.lastName,
|
||||
username: action.payload.username,
|
||||
isBot: Boolean(action.payload.bot),
|
||||
accessHash: action.payload.accessHash?.toString(),
|
||||
};
|
||||
// Handle both TelegramUser (from cached state) and User (from API)
|
||||
const payload = action.payload as TelegramUser | { id: number | string; firstName?: string; lastName?: string; username?: string; bot?: boolean; accessHash?: bigint | string };
|
||||
if ('isBot' in payload) {
|
||||
// Already a TelegramUser
|
||||
state.currentUser = payload;
|
||||
} else {
|
||||
// Convert from API User format
|
||||
state.currentUser = {
|
||||
id: String(payload.id),
|
||||
firstName: payload.firstName || '',
|
||||
lastName: payload.lastName,
|
||||
username: payload.username,
|
||||
isBot: Boolean(payload.bot),
|
||||
accessHash: payload.accessHash?.toString(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
state.authStatus = 'not_authenticated';
|
||||
state.currentUser = null;
|
||||
|
||||
Reference in New Issue
Block a user