mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Fix/telegram login (#4)
* Add UserProvider to App component and enhance user data fetching logic - Integrated UserProvider into the App component to manage user state. - Updated UserProvider to fetch current user data on token availability and handle token expiration by clearing the token on fetch failure. - Improved user data fetching logic for better error handling and user experience. * Enhance Telegram connection handling and error management - Added logic to handle Telegram account mismatch, providing user feedback and resetting connection state. - Improved user verification during authentication to ensure the logged-in account matches the Telegram account. - Refactored connection status checks and error handling for better user experience. - Introduced a method to clear session and disconnect when account mismatch occurs. - Updated polling mechanism for authentication status to ensure timely updates. This update improves the robustness of the Telegram connection process and enhances user feedback during authentication failures. * ran prettier * Refactor socket and auth state management for user-specific handling - Updated socket state management to support multiple users by introducing user-specific selectors and actions. - Refactored connection status handling in the socket service to dispatch user-specific status updates. - Enhanced onboarding state management to track completion per user, allowing for more granular control. - Introduced new selectors for socket and auth states to improve readability and maintainability. - Updated relevant components to utilize the new user-specific state management. This update improves the application's ability to handle multiple users and enhances the overall user experience during onboarding and socket connections. * Enhance MTProto client session management and introduce user-specific session handling - Updated the MTProtoService to manage sessions per user by storing session data under a user-specific key. - Modified the initialize method to accept a userId parameter, allowing for user-specific session initialization and management. - Improved session loading and saving logic to ensure proper handling of user sessions. - Added a utility function to generate session keys based on userId for better maintainability. This update enhances the application's ability to handle multiple user sessions effectively. * Refactor Telegram state management for user-specific handling - Introduced user-specific state management in the Telegram store, allowing for better handling of multiple users. - Updated selectors to retrieve state based on the current user, improving data encapsulation and reducing global state dependencies. - Enhanced reducers and thunks to ensure actions are dispatched with user context, maintaining user-specific data integrity. - Modified persistence configuration to store Telegram state scoped by user, ensuring isolated state management. This update significantly improves the application's ability to manage multiple user sessions effectively and enhances overall user experience. * Enhance MTProtoService connection handling with user-specific initialization - Updated the checkConnection method to accept an optional userId parameter, allowing for user-specific connection initialization. - Modified the connection logic to ensure proper initialization only occurs when a userId is provided, improving session management. - Refactored related thunks to use a consistent naming convention for userId parameters, enhancing code clarity. This update improves the handling of user sessions in the MTProtoService, aligning with recent enhancements in user-specific state management. * Refactor Telegram connection handling for user-specific management - Updated the TelegramConnectionModal to utilize user-specific identifiers for connection status and authentication processes, enhancing session management. - Refactored related components and selectors to ensure userId is consistently passed and utilized, improving clarity and maintainability. - Enhanced the ConnectionsPanel and ConnectStep to check for saved sessions based on userId, allowing for better handling of multiple user connections. This update significantly improves the application's ability to manage user-specific Telegram connections and enhances the overall user experience. * Refactor Telegram connection logic to simplify session management - Removed unnecessary userId checks and localStorage interactions from ConnectionsPanel and ConnectStep components, streamlining the connection status determination. - Updated MTProtoService to manage session data through Redux instead of localStorage, enhancing consistency and maintainability. - Improved session loading and saving logic to focus solely on Redux state, ensuring better integration with user-specific session handling. This update enhances the clarity and efficiency of the Telegram connection management process, aligning with recent improvements in user-specific state management. * Update CLAUDE.md to clarify localStorage usage and emphasize Redux for state management - Revised documentation to discourage the use of localStorage and sessionStorage for app state, advocating for Redux and Redux Persist instead. - Added guidelines for removing existing localStorage usage and migrating relevant data to Redux. - Updated service layer documentation to reflect changes in session management, specifying that session data is now stored in Redux rather than localStorage. - Enhanced clarity on deep link handling and the prevention of infinite reload loops. This update aligns with recent improvements in user-specific state management and reinforces best practices for state handling. * Enhance onboarding process with error handling and API integration - Updated the Onboarding component to include an API call for marking onboarding as complete, with error handling to provide user feedback. - Refactored the GetStartedStep component to support asynchronous completion handling, including loading state and error messages. - Introduced a new onboardingComplete method in the userApi service to facilitate the onboarding completion process. This update improves the user experience during onboarding by ensuring proper error management and feedback. * Enhance TelegramConnectionModal with socket connection management and improved initialization flow - Integrated socket connection handling in the TelegramConnectionModal to ensure the socket is connected when the modal opens, enhancing real-time features. - Refactored the initialization logic to streamline the connection process, including user verification and error handling. - Introduced a reference to track the QR code flow state, preventing multiple initiations and improving user experience during authentication. This update improves the reliability of the Telegram connection process and enhances user feedback during the onboarding experience. * Refactor Telegram connection checks for improved logic consistency - Updated the ConnectionsPanel and ConnectStep components to refine the logic for determining if a Telegram connection is established, ensuring that both session string and authentication status are required for a valid connection. - Simplified className definitions in the UI components for better readability and maintainability. This update enhances the clarity of connection status checks and improves the overall user experience during the onboarding process. * Refactor Telegram authentication flow and improve error handling - Simplified the TelegramLoginButton component by removing complex authentication logic and replacing it with a direct link to the Telegram bot for login. - Enhanced the Login page to consume a login token from the URL, providing better error handling and user feedback during the authentication process. - Introduced a new API service for consuming login tokens, ensuring secure retrieval of JWT tokens from the backend. This update streamlines the authentication experience and improves error management, enhancing overall user experience during login.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { apiClient } from "../apiClient";
|
||||
|
||||
interface ConsumeLoginTokenResponse {
|
||||
success: boolean;
|
||||
data: { jwtToken: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume a verified login token and return the JWT.
|
||||
* POST /telegram/login-tokens/:token/consume (no auth required)
|
||||
*/
|
||||
export async function consumeLoginToken(loginToken: string): Promise<string> {
|
||||
const response = await apiClient.post<ConsumeLoginTokenResponse>(
|
||||
`/telegram/login-tokens/${encodeURIComponent(loginToken)}/consume`,
|
||||
undefined,
|
||||
{ requireAuth: false },
|
||||
);
|
||||
if (!response.success || !response.data?.jwtToken) {
|
||||
throw new Error("Login token invalid or expired");
|
||||
}
|
||||
return response.data.jwtToken;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { apiClient } from '../apiClient';
|
||||
import type { GetMeResponse, User } from '../../types/api';
|
||||
import { apiClient } from "../apiClient";
|
||||
import type { GetMeResponse, User } from "../../types/api";
|
||||
|
||||
/**
|
||||
* User API endpoints
|
||||
@@ -10,10 +10,21 @@ export const userApi = {
|
||||
* GET /telegram/me
|
||||
*/
|
||||
getMe: async (): Promise<User> => {
|
||||
const response = await apiClient.get<GetMeResponse>('/telegram/me');
|
||||
const response = await apiClient.get<GetMeResponse>("/telegram/me");
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Failed to fetch user data');
|
||||
throw new Error(response.error || "Failed to fetch user data");
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark onboarding complete for the current user.
|
||||
* POST /telegram/settings/onboarding-complete
|
||||
*/
|
||||
onboardingComplete: async (): Promise<void> => {
|
||||
await apiClient.post<{ success: boolean; data: unknown }>(
|
||||
"/telegram/settings/onboarding-complete",
|
||||
{},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
+33
-23
@@ -1,8 +1,8 @@
|
||||
import { BACKEND_URL } from '../utils/config';
|
||||
import type { ApiError } from '../types/api';
|
||||
import { store } from '../store';
|
||||
import { BACKEND_URL } from "../utils/config";
|
||||
import type { ApiError } from "../types/api";
|
||||
import { store } from "../store";
|
||||
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
|
||||
interface RequestOptions {
|
||||
method?: HttpMethod;
|
||||
@@ -34,7 +34,7 @@ class ApiClient {
|
||||
*/
|
||||
private buildHeaders(options: RequestOptions): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
@@ -54,9 +54,9 @@ class ApiClient {
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestOptions = {}
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const { method = 'GET', body, requireAuth = true } = options;
|
||||
const { method = "GET", body, requireAuth = true } = options;
|
||||
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const headers = this.buildHeaders({ ...options, requireAuth });
|
||||
@@ -66,7 +66,7 @@ class ApiClient {
|
||||
headers,
|
||||
};
|
||||
|
||||
if (body && method !== 'GET') {
|
||||
if (body && method !== "GET") {
|
||||
config.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ class ApiClient {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
// Handle non-JSON responses
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
@@ -88,21 +88,25 @@ class ApiClient {
|
||||
if (!response.ok) {
|
||||
const error: ApiError = data.error
|
||||
? { success: false, error: data.error, message: data.message }
|
||||
: { success: false, error: `HTTP ${response.status}: ${response.statusText}` };
|
||||
: {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${response.statusText}`,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
// Re-throw API errors as-is
|
||||
if (error && typeof error === 'object' && 'error' in error) {
|
||||
if (error && typeof error === "object" && "error" in error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wrap network/other errors
|
||||
throw {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
error:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
} as ApiError;
|
||||
}
|
||||
}
|
||||
@@ -110,8 +114,11 @@ class ApiClient {
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
async get<T>(endpoint: string, options?: Omit<RequestOptions, 'method' | 'body'>): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: 'GET' });
|
||||
async get<T>(
|
||||
endpoint: string,
|
||||
options?: Omit<RequestOptions, "method" | "body">,
|
||||
): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: "GET" });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,9 +127,9 @@ class ApiClient {
|
||||
async post<T>(
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
options?: Omit<RequestOptions, 'method' | 'body'>
|
||||
options?: Omit<RequestOptions, "method" | "body">,
|
||||
): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: 'POST', body });
|
||||
return this.request<T>(endpoint, { ...options, method: "POST", body });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,9 +138,9 @@ class ApiClient {
|
||||
async put<T>(
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
options?: Omit<RequestOptions, 'method' | 'body'>
|
||||
options?: Omit<RequestOptions, "method" | "body">,
|
||||
): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: 'PUT', body });
|
||||
return this.request<T>(endpoint, { ...options, method: "PUT", body });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,16 +149,19 @@ class ApiClient {
|
||||
async patch<T>(
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
options?: Omit<RequestOptions, 'method' | 'body'>
|
||||
options?: Omit<RequestOptions, "method" | "body">,
|
||||
): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: 'PATCH', body });
|
||||
return this.request<T>(endpoint, { ...options, method: "PATCH", body });
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
async delete<T>(endpoint: string, options?: Omit<RequestOptions, 'method' | 'body'>): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: 'DELETE' });
|
||||
async delete<T>(
|
||||
endpoint: string,
|
||||
options?: Omit<RequestOptions, "method" | "body">,
|
||||
): Promise<T> {
|
||||
return this.request<T>(endpoint, { ...options, method: "DELETE" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+129
-71
@@ -1,8 +1,10 @@
|
||||
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';
|
||||
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";
|
||||
import { store } from "../store";
|
||||
import { setSessionString } from "../store/telegram";
|
||||
|
||||
type LoginOptions = UserAuthParams | BotAuthParams;
|
||||
|
||||
@@ -11,7 +13,8 @@ class MTProtoService {
|
||||
private client: TelegramClient | undefined;
|
||||
private isInitialized = false;
|
||||
private isConnected = false;
|
||||
private sessionString = '';
|
||||
private sessionString = "";
|
||||
private userId: string | null = null;
|
||||
private readonly apiId: number;
|
||||
private readonly apiHash: string;
|
||||
|
||||
@@ -19,7 +22,9 @@ class MTProtoService {
|
||||
// Private constructor to enforce singleton
|
||||
// Load API credentials from config once
|
||||
if (!TELEGRAM_API_ID || !TELEGRAM_API_HASH) {
|
||||
throw new Error('TELEGRAM_API_ID and TELEGRAM_API_HASH must be configured');
|
||||
throw new Error(
|
||||
"TELEGRAM_API_ID and TELEGRAM_API_HASH must be configured",
|
||||
);
|
||||
}
|
||||
this.apiId = TELEGRAM_API_ID;
|
||||
this.apiHash = TELEGRAM_API_HASH;
|
||||
@@ -33,30 +38,39 @@ class MTProtoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the MTProto client with API credentials
|
||||
* Initialize the MTProto client with API credentials.
|
||||
* Session is stored in Redux (telegram.byUser[userId].sessionString).
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isInitialized && this.client) {
|
||||
console.log('MTProto client already initialized');
|
||||
async initialize(userId: string): Promise<void> {
|
||||
if (this.isInitialized && this.client && this.userId === userId) {
|
||||
return;
|
||||
}
|
||||
if (this.isInitialized && this.userId !== null && this.userId !== userId) {
|
||||
await this.clearSessionAndDisconnect(this.userId);
|
||||
}
|
||||
|
||||
const sessionString = this.loadSession() || '';
|
||||
this.userId = userId;
|
||||
const sessionString = this.loadSession() || "";
|
||||
|
||||
try {
|
||||
const stringSession = new StringSession(sessionString);
|
||||
this.sessionString = sessionString;
|
||||
|
||||
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.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;
|
||||
console.log('MTProto client initialized successfully');
|
||||
console.log("MTProto client initialized successfully");
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize MTProto client:', error);
|
||||
console.error("Failed to initialize MTProto client:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -66,28 +80,30 @@ class MTProtoService {
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (!this.client) {
|
||||
throw new Error('MTProto client not initialized. Call initialize() first.');
|
||||
throw new Error(
|
||||
"MTProto client not initialized. Call initialize() first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isConnected) {
|
||||
console.log('Already connected to Telegram');
|
||||
console.log("Already connected to Telegram");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.connect();
|
||||
this.isConnected = true;
|
||||
console.log('Connected to Telegram successfully');
|
||||
console.log("Connected to Telegram successfully");
|
||||
|
||||
// Save session string if it changed
|
||||
const newSessionString = this.client.session.save() as string | undefined;
|
||||
if (newSessionString && newSessionString !== this.sessionString) {
|
||||
this.sessionString = newSessionString;
|
||||
this.saveSession(newSessionString);
|
||||
console.log('Session updated and saved');
|
||||
console.log("Session updated and saved");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Telegram:', error);
|
||||
console.error("Failed to connect to Telegram:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -97,7 +113,9 @@ class MTProtoService {
|
||||
*/
|
||||
async start(options: LoginOptions): Promise<void> {
|
||||
if (!this.client) {
|
||||
throw new Error('MTProto client not initialized. Call initialize() first.');
|
||||
throw new Error(
|
||||
"MTProto client not initialized. Call initialize() first.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -108,10 +126,10 @@ class MTProtoService {
|
||||
if (newSessionString && newSessionString !== this.sessionString) {
|
||||
this.sessionString = newSessionString;
|
||||
this.saveSession(newSessionString);
|
||||
console.log('Authentication successful, session saved');
|
||||
console.log("Authentication successful, session saved");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication failed:', error);
|
||||
console.error("Authentication failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -122,10 +140,13 @@ class MTProtoService {
|
||||
async signInWithQrCode(
|
||||
qrCodeCallback: (qrCode: { token: Buffer; expires: number }) => void,
|
||||
passwordCallback?: (hint?: string) => Promise<string>,
|
||||
onError?: (err: Error) => Promise<boolean> | void
|
||||
onError?: (err: Error) => Promise<boolean> | void,
|
||||
): Promise<unknown> {
|
||||
console.log("signInWithQrCode");
|
||||
if (!this.client) {
|
||||
throw new Error('MTProto client not initialized. Call initialize() first.');
|
||||
throw new Error(
|
||||
"MTProto client not initialized. Call initialize() first.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -143,8 +164,11 @@ class MTProtoService {
|
||||
// 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
|
||||
const errorMessage = err.message || '';
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED') && passwordCallback) {
|
||||
const errorMessage = err.message || "";
|
||||
if (
|
||||
errorMessage.includes("SESSION_PASSWORD_NEEDED") &&
|
||||
passwordCallback
|
||||
) {
|
||||
// Don't stop - let the password callback handle it
|
||||
if (onError) {
|
||||
const result = await onError(err);
|
||||
@@ -152,15 +176,15 @@ class MTProtoService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (onError) {
|
||||
const result = await onError(err);
|
||||
return result ?? false;
|
||||
}
|
||||
console.error('QR code auth error:', err);
|
||||
console.error("QR code auth error:", err);
|
||||
return false;
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Save session after successful login
|
||||
@@ -168,25 +192,28 @@ class MTProtoService {
|
||||
if (newSessionString && newSessionString !== this.sessionString) {
|
||||
this.sessionString = newSessionString;
|
||||
this.saveSession(newSessionString);
|
||||
console.log('QR code authentication successful, session saved');
|
||||
console.log("QR code authentication successful, session saved");
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
// If it's a password needed error, the password callback should have been invoked
|
||||
// by the library. If we're here, it means either:
|
||||
// 1. The password callback wasn't provided
|
||||
// 2. The password callback failed
|
||||
// 3. The library threw before invoking the callback
|
||||
// In any case, we should let the caller handle it
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED')) {
|
||||
console.log('SESSION_PASSWORD_NEEDED - password callback should handle this');
|
||||
if (errorMessage.includes("SESSION_PASSWORD_NEEDED")) {
|
||||
console.log(
|
||||
"SESSION_PASSWORD_NEEDED - password callback should handle this",
|
||||
);
|
||||
// Don't log as error - this is expected when 2FA is enabled
|
||||
// The password callback should be invoked by the library
|
||||
} else {
|
||||
console.error('QR code authentication failed:', error);
|
||||
console.error("QR code authentication failed:", error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -198,7 +225,9 @@ class MTProtoService {
|
||||
*/
|
||||
getClient(): TelegramClient {
|
||||
if (!this.client || !this.isInitialized) {
|
||||
throw new Error('MTProto client not initialized. Call initialize() first.');
|
||||
throw new Error(
|
||||
"MTProto client not initialized. Call initialize() first.",
|
||||
);
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
@@ -229,11 +258,11 @@ class MTProtoService {
|
||||
* This calls getMe() which also updates the user's online status on Telegram
|
||||
* Automatically initializes and connects if needed
|
||||
*/
|
||||
async checkConnection(): Promise<boolean> {
|
||||
async checkConnection(userId?: string): Promise<boolean> {
|
||||
try {
|
||||
// Initialize if not already initialized
|
||||
if (!this.isInitialized || !this.client) {
|
||||
await this.initialize();
|
||||
if (!userId) return false;
|
||||
await this.initialize(userId);
|
||||
}
|
||||
|
||||
// Connect if not already connected
|
||||
@@ -255,9 +284,11 @@ class MTProtoService {
|
||||
} 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`);
|
||||
console.debug(
|
||||
`Telegram connection check: FLOOD_WAIT ${error.seconds}s`,
|
||||
);
|
||||
} else {
|
||||
console.warn('Telegram connection check failed:', error);
|
||||
console.warn("Telegram connection check failed:", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -271,14 +302,34 @@ class MTProtoService {
|
||||
try {
|
||||
await this.client.disconnect();
|
||||
this.isConnected = false;
|
||||
console.log('Disconnected from Telegram');
|
||||
console.log("Disconnected from Telegram");
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting from Telegram:', error);
|
||||
console.error("Error disconnecting from Telegram:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear session from Redux, disconnect, and reset client state.
|
||||
* Use when the logged-in Telegram account does not match the app user (e.g. after QR connect).
|
||||
*/
|
||||
async clearSessionAndDisconnect(userId?: string): Promise<void> {
|
||||
const uid = userId ?? this.userId;
|
||||
if (uid) {
|
||||
try {
|
||||
store.dispatch(setSessionString({ userId: uid, sessionString: null }));
|
||||
} catch (e) {
|
||||
console.warn("Failed to clear Telegram session from Redux:", e);
|
||||
}
|
||||
}
|
||||
await this.disconnect();
|
||||
this.client = undefined;
|
||||
this.isInitialized = false;
|
||||
this.sessionString = "";
|
||||
this.userId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message using the client with FLOOD_WAIT handling
|
||||
*/
|
||||
@@ -287,7 +338,7 @@ class MTProtoService {
|
||||
if (!this.isClientConnected()) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
|
||||
return this.handleFloodWait(async () => {
|
||||
await client.sendMessage(entity, { message });
|
||||
});
|
||||
@@ -303,7 +354,7 @@ class MTProtoService {
|
||||
private async handleFloodWait<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries = 3,
|
||||
retryCount = 0
|
||||
retryCount = 0,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
@@ -311,26 +362,32 @@ class MTProtoService {
|
||||
// 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.`);
|
||||
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.`);
|
||||
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})`);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -345,7 +402,7 @@ class MTProtoService {
|
||||
*/
|
||||
async withFloodWaitHandling<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
maxRetries = 3,
|
||||
): Promise<T> {
|
||||
return this.handleFloodWait(operation, maxRetries);
|
||||
}
|
||||
@@ -353,41 +410,42 @@ class MTProtoService {
|
||||
/**
|
||||
* Invoke a raw Telegram API method with FLOOD_WAIT handling
|
||||
*/
|
||||
async invoke<T = unknown>(request: Parameters<TelegramClient['invoke']>[0]): Promise<T> {
|
||||
async invoke<T = unknown>(
|
||||
request: Parameters<TelegramClient["invoke"]>[0],
|
||||
): Promise<T> {
|
||||
const client = this.getClient();
|
||||
if (!this.isClientConnected()) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
|
||||
return this.handleFloodWait(async () => {
|
||||
return client.invoke(request) as Promise<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load session from localStorage
|
||||
*/
|
||||
private loadSession(): string | null {
|
||||
try {
|
||||
return localStorage.getItem('telegram_session');
|
||||
if (!this.userId) return null;
|
||||
const state = store.getState();
|
||||
const u = state.telegram.byUser[this.userId];
|
||||
return u?.sessionString ?? null;
|
||||
} catch (error) {
|
||||
console.error('Failed to load session from localStorage:', error);
|
||||
console.error("Failed to load Telegram session from Redux:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save session to localStorage
|
||||
*/
|
||||
private saveSession(session: string): void {
|
||||
try {
|
||||
localStorage.setItem('telegram_session', session);
|
||||
if (!this.userId) return;
|
||||
store.dispatch(
|
||||
setSessionString({ userId: this.userId, sessionString: session }),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save session to localStorage:', error);
|
||||
console.error("Failed to save Telegram session to Redux:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const mtprotoService = MTProtoService.getInstance();
|
||||
export default mtprotoService;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { BACKEND_URL } from "../utils/config";
|
||||
import { store } from "../store";
|
||||
import { setStatus, setSocketId, reset } from "../store/socketSlice";
|
||||
import {
|
||||
setStatusForUser,
|
||||
setSocketIdForUser,
|
||||
resetForUser,
|
||||
} from "../store/socketSlice";
|
||||
|
||||
function getSocketUserId(): string {
|
||||
return store.getState().user.user?._id ?? "__pending__";
|
||||
}
|
||||
|
||||
class SocketService {
|
||||
private socket: Socket | null = null;
|
||||
@@ -34,8 +42,9 @@ class SocketService {
|
||||
|
||||
this.token = token;
|
||||
|
||||
// Update status to connecting
|
||||
store.dispatch(setStatus("connecting"));
|
||||
store.dispatch(
|
||||
setStatusForUser({ userId: getSocketUserId(), status: "connecting" }),
|
||||
);
|
||||
|
||||
const backendUrl = BACKEND_URL;
|
||||
|
||||
@@ -67,8 +76,9 @@ class SocketService {
|
||||
// Connection event handlers
|
||||
this.socket.on("connect", () => {
|
||||
const socketId = this.socket?.id || null;
|
||||
store.dispatch(setStatus("connected"));
|
||||
store.dispatch(setSocketId(socketId));
|
||||
const uid = getSocketUserId();
|
||||
store.dispatch(setStatusForUser({ userId: uid, status: "connected" }));
|
||||
store.dispatch(setSocketIdForUser({ userId: uid, socketId }));
|
||||
});
|
||||
|
||||
this.socket.on("ready", () => {
|
||||
@@ -80,12 +90,14 @@ class SocketService {
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", () => {
|
||||
store.dispatch(setStatus("disconnected"));
|
||||
store.dispatch(setSocketId(null));
|
||||
const uid = getSocketUserId();
|
||||
store.dispatch(setStatusForUser({ userId: uid, status: "disconnected" }));
|
||||
store.dispatch(setSocketIdForUser({ userId: uid, socketId: null }));
|
||||
});
|
||||
|
||||
this.socket.on("connect_error", () => {
|
||||
store.dispatch(setStatus("disconnected"));
|
||||
const uid = getSocketUserId();
|
||||
store.dispatch(setStatusForUser({ userId: uid, status: "disconnected" }));
|
||||
});
|
||||
|
||||
this.socket.connect();
|
||||
@@ -99,7 +111,7 @@ class SocketService {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.token = null;
|
||||
store.dispatch(reset());
|
||||
store.dispatch(resetForUser({ userId: getSocketUserId() }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user