Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,34 @@ function formatErrorMessage(error) {
return String(error);
}

function parseRequiredWaitSeconds(error) {
const text = formatErrorMessage(error);
const waitMatch = /wait of (\d+) seconds is required/i.exec(text);
if (waitMatch) {
return Number(waitMatch[1]);
}
const floodWaitMatch = /FLOOD_WAIT_(\d+)/i.exec(text);
if (floodWaitMatch) {
return Number(floodWaitMatch[1]);
}
return null;
}

async function refreshDialogsWithRetry(messageSyncService, options = {}) {
const maxWaitSeconds = options.maxWaitSeconds ?? 30;
try {
return await messageSyncService.refreshChannelsFromDialogs();
} catch (error) {
const waitSeconds = parseRequiredWaitSeconds(error);
if (!waitSeconds || waitSeconds > maxWaitSeconds) {
throw error;
}
console.log(`Rate limited while seeding dialogs. Waiting ${waitSeconds}s and retrying once...`);
await delay(waitSeconds * 1000);
return await messageSyncService.refreshChannelsFromDialogs();
}
}

function readVersion() {
try {
const pkgPath = new URL('./package.json', import.meta.url);
Expand Down Expand Up @@ -1257,7 +1285,7 @@ async function runAuthStatus(globalFlags) {
}
return;
}
const { telegramClient } = createTelegramClient({ storeDir, config });
const { telegramClient } = createTelegramClient({ storeDir, config, disableUpdates: true });
let messageSyncService = null;
let search = { enabled: null };
let archiveError = null;
Expand Down Expand Up @@ -1334,7 +1362,7 @@ async function runAuthLogout(globalFlags) {
}
const config = await ensureStoreConfig(storeDir);
release = acquireStoreLock(storeDir);
({ telegramClient } = createTelegramClient({ storeDir, config }));
({ telegramClient } = createTelegramClient({ storeDir, config, disableUpdates: true }));
try {
const loginSuccess = await telegramClient.login();
if (!loginSuccess) {
Expand Down Expand Up @@ -1383,6 +1411,7 @@ async function runAuthLogin(globalFlags, options = {}) {
config,
forceSms: options.forceSms,
useQr: options.qr,
disableUpdates: !options.follow,
}));
try {
const loginSuccess = await telegramClient.login();
Expand All @@ -1391,13 +1420,17 @@ async function runAuthLogin(globalFlags, options = {}) {
}
let dialogCount = null;
let archiveError = null;
try {
({ messageSyncService } = createMessageSyncService(telegramClient, { storeDir }));
dialogCount = await messageSyncService.refreshChannelsFromDialogs();
} catch (error) {
archiveError = formatErrorMessage(error);
if (options.follow) {
throw new Error(`Authenticated, but archive sync could not start: ${archiveError}`);
if (timeoutMs && !options.follow) {
archiveError = 'Skipped dialog bootstrap because auth is running with a wall-clock timeout. Re-run without --timeout to seed dialogs.';
} else {
try {
({ messageSyncService } = createMessageSyncService(telegramClient, { storeDir }));
dialogCount = await refreshDialogsWithRetry(messageSyncService);
} catch (error) {
archiveError = formatErrorMessage(error);
if (options.follow) {
throw new Error(`Authenticated, but archive sync could not start: ${archiveError}`);
}
}
}
if (options.follow) {
Expand Down
6 changes: 5 additions & 1 deletion core/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export function createTelegramClient(options = {}) {
config.apiHash,
config.phoneNumber,
sessionPath,
{ forceSms: options.forceSms ?? false, useQr: options.useQr ?? false },
{
forceSms: options.forceSms ?? false,
useQr: options.useQr ?? false,
disableUpdates: options.disableUpdates ?? false,
},
);

return {
Expand Down
209 changes: 124 additions & 85 deletions telegram-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,13 +431,18 @@ class TelegramClient {
}

_createClient() {
return new MtCuteClient({
const clientOptions = {
apiId: this.apiId,
apiHash: this.apiHash,
storage: this.sessionPath,
platform: createPlatform(),
updates: this.updatesConfig,
});
};
if (this.options.disableUpdates) {
clientOptions.disableUpdates = true;
} else {
clientOptions.updates = this.updatesConfig;
}
return new MtCuteClient(clientOptions);
}

_isAuthKeyUnregisteredError(error) {
Expand All @@ -447,6 +452,23 @@ class TelegramClient {
return code === 401 && message.includes('AUTH_KEY_UNREGISTERED');
}

_isSessionResetError(error) {
if (!error) return false;
const message = (error.errorMessage || error.text || error.message || '').toUpperCase();
return message.includes('SESSION IS RESET');
}

async _recreateClient() {
try {
await this.client.destroy();
} catch (error) {
console.warn('[warning] failed to destroy MTProto client during reset:', error?.message || error);
}
this.client = this._createClient();
this.updatesRunning = false;
this.rawUpdateHandler = null;
}

async _resetSessionAndClient() {
const sessionFiles = [this.sessionPath, `${this.sessionPath}-wal`, `${this.sessionPath}-shm`];
for (const filePath of sessionFiles) {
Expand All @@ -458,14 +480,7 @@ class TelegramClient {
}
}
}
try {
await this.client.destroy();
} catch (error) {
console.warn('[warning] failed to destroy MTProto client during reset:', error?.message || error);
}
this.client = this._createClient();
this.updatesRunning = false;
this.rawUpdateHandler = null;
await this._recreateClient();
}

_isUnauthorizedError(error) {
Expand Down Expand Up @@ -561,73 +576,84 @@ class TelegramClient {
});
}

async login(retriedAfterReset = false) {
try {
if (await this._isAuthorized()) {
console.log('Existing session is valid.');
return true;
}

if (!this.options.useQr && !this.phoneNumber) {
throw new Error('TELEGRAM_PHONE_NUMBER is not configured.');
}
_buildStartParams() {
const startParams = {
password: async () => {
const value = await this._askHiddenQuestion('Enter your 2FA password (leave empty if not enabled): ');
return value.length ? value : undefined;
},
};

const startParams = {
password: async () => {
const value = await this._askHiddenQuestion('Enter your 2FA password (leave empty if not enabled): ');
return value.length ? value : undefined;
},
if (this.options.useQr) {
startParams.qrCodeHandler = (url, expiresAt) => {
const expiresLabel = expiresAt instanceof Date && !Number.isNaN(expiresAt.getTime())
? expiresAt.toISOString()
: 'unknown';
console.log('\nScan this QR code in Telegram: Settings -> Devices -> Link Desktop Device');
qrcode.generate(url, { small: true }, (rendered) => {
console.log(rendered);
});
console.log(`QR login URL: ${url}`);
console.log(`QR expires at: ${expiresLabel}`);
};

if (this.options.useQr) {
startParams.qrCodeHandler = (url, expiresAt) => {
const expiresLabel = expiresAt instanceof Date && !Number.isNaN(expiresAt.getTime())
? expiresAt.toISOString()
: 'unknown';
console.log('\nScan this QR code in Telegram: Settings -> Devices -> Link Desktop Device');
qrcode.generate(url, { small: true }, (rendered) => {
console.log(rendered);
});
console.log(`QR login URL: ${url}`);
console.log(`QR expires at: ${expiresLabel}`);
};
} else {
startParams.phone = this.phoneNumber;
startParams.code = async () => await this._askQuestion('Enter the code you received: ');
startParams.codeSentCallback = async (sentCode) => {
if (this.options.forceSms && (sentCode.type === 'app' || sentCode.type === 'email')) {
try {
await this.client.resendCode({ phone: this.phoneNumber, phoneCodeHash: sentCode.phoneCodeHash });
console.log('Code re-sent via SMS.');
} catch (e) {
const msg = (e.text || e.message || '').toUpperCase();
if (msg.includes('SEND_CODE_UNAVAILABLE')) {
console.log('SMS unavailable for this number. Please use the code sent via app.');
} else {
console.log(`Could not request SMS (${e.text || e.message}). Using code sent via ${sentCode.type}.`);
}
} else {
startParams.phone = this.phoneNumber;
startParams.code = async () => await this._askQuestion('Enter the code you received: ');
startParams.codeSentCallback = async (sentCode) => {
if (this.options.forceSms && (sentCode.type === 'app' || sentCode.type === 'email')) {
try {
await this.client.resendCode({ phone: this.phoneNumber, phoneCodeHash: sentCode.phoneCodeHash });
console.log('Code re-sent via SMS.');
} catch (e) {
const msg = (e.text || e.message || '').toUpperCase();
if (msg.includes('SEND_CODE_UNAVAILABLE')) {
console.log('SMS unavailable for this number. Please use the code sent via app.');
} else {
console.log(`Could not request SMS (${e.text || e.message}). Using code sent via ${sentCode.type}.`);
}
} else {
console.log(`The confirmation code has been sent via ${sentCode.type}.`);
}
};
} else {
console.log(`The confirmation code has been sent via ${sentCode.type}.`);
}
};
}

return startParams;
}

async login(retriedAfterReset = false, retriedAfterSessionReset = false) {
try {
const hasExistingSession = await this._isAuthorized();

if (!hasExistingSession && !this.options.useQr && !this.phoneNumber) {
throw new Error('TELEGRAM_PHONE_NUMBER is not configured.');
}

await this.client.start(startParams);
await this.client.start(this._buildStartParams());

console.log('Logged in successfully!');
console.log(hasExistingSession ? 'Existing session is valid.' : 'Logged in successfully!');
return true;
} catch (error) {
if (!retriedAfterReset && this._isAuthKeyUnregisteredError(error)) {
console.log('Detected AUTH_KEY_UNREGISTERED. Resetting local session and retrying login once...');
try {
await this._resetSessionAndClient();
return await this.login(true);
return await this.login(true, retriedAfterSessionReset);
} catch (resetError) {
console.error('Failed to recover from AUTH_KEY_UNREGISTERED:', resetError);
return false;
}
}
if (!retriedAfterSessionReset && this._isSessionResetError(error)) {
console.log('Detected session reset during login. Recreating MTProto client and retrying once...');
try {
await this._recreateClient();
return await this.login(retriedAfterReset, true);
} catch (resetError) {
console.error('Failed to recover from session reset:', resetError);
return false;
}
}
console.error('Error during login:', error);
return false;
}
Expand All @@ -651,36 +677,49 @@ class TelegramClient {
return true;
}

async listDialogs(limit = 50) {
await this.ensureLogin();
const effectiveLimit = limit && limit > 0 ? limit : Infinity;
const results = [];
async listDialogs(limit = 50, retriedAfterSessionReset = false) {
try {
await this.ensureLogin();
const effectiveLimit = limit && limit > 0 ? limit : Infinity;
const results = [];

for await (const dialog of this.client.iterDialogs({})) {
const peer = dialog.peer;
if (!peer) continue;
for await (const dialog of this.client.iterDialogs({})) {
const peer = dialog.peer;
if (!peer) continue;

const id = peer.id.toString();
const username = 'username' in peer ? peer.username ?? null : null;
const chatType = typeof peer.chatType === 'string' ? peer.chatType : null;
const isForum = typeof peer.isForum === 'boolean' ? peer.isForum : null;
const isGroup = typeof peer.isGroup === 'boolean' ? peer.isGroup : null;
results.push({
id,
type: normalizePeerType(peer),
title: peer.displayName || 'Unknown',
username,
chatType,
isForum,
isGroup,
});
const id = peer.id.toString();
const username = 'username' in peer ? peer.username ?? null : null;
const chatType = typeof peer.chatType === 'string' ? peer.chatType : null;
const isForum = typeof peer.isForum === 'boolean' ? peer.isForum : null;
const isGroup = typeof peer.isGroup === 'boolean' ? peer.isGroup : null;
results.push({
id,
type: normalizePeerType(peer),
title: peer.displayName || 'Unknown',
username,
chatType,
isForum,
isGroup,
});

if (results.length >= effectiveLimit) {
break;
if (results.length >= effectiveLimit) {
break;
}
}
}

return results;
return results;
} catch (error) {
if (!retriedAfterSessionReset && this._isSessionResetError(error)) {
console.log('Detected session reset while listing dialogs. Recreating MTProto client and retrying once...');
await this._recreateClient();
const loginSuccess = await this.login();
if (!loginSuccess) {
throw new Error('Failed to restore session after dialog fetch reset.');
}
return this.listDialogs(limit, true);
}
throw error;
}
}

async searchPeers(query, limit = 50) {
Expand Down
Loading
Loading