fix: use loadChats API and load chat folders for complete chat discovery
Some checks failed
continuous-integration/drone/push Build is failing

- Switch from getChats pagination to loadChats (the TDLib-recommended
  API) which properly loads all chats into TDLib's cache and signals
  completion with a 404 error
- Discover and load chat folders via getChatFolders so chats in
  user-created folders are included
- Load from main + archive + all folders in both worker startup and
  getAccountChats channel discovery
- After loading, use getChats with high limit to retrieve all cached IDs
- This ensures private chats, 1-on-1 conversations, Saved Messages,
  basic groups, and archived/folder chats are all discoverable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 22:38:49 +01:00
parent aef76828ef
commit 1425db8774
2 changed files with 130 additions and 82 deletions

View File

@@ -34,95 +34,123 @@ export async function getAccountChats(
log.warn("Failed to get current user via getMe"); log.warn("Failed to get current user via getMe");
} }
// Load ALL chats from both main and archive lists by paginating getChats. // First, load all chats into TDLib's cache using loadChats (the proper API).
// TDLib's getChats returns batches — keep calling until it returns // loadChats returns 404 when all chats have been loaded.
// an empty list, which signals all chats have been loaded. // Then use getChats to retrieve the IDs for enrichment.
// Load from main, archive, AND chat folders to cover all chat types.
const folderLists: { _: "chatListFolder"; chat_folder_id: number }[] = [];
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const folders = (await client.invoke({ _: "getChatFolders" })) as any;
if (folders?.chat_folders) {
for (const f of folders.chat_folders) {
folderLists.push({ _: "chatListFolder", chat_folder_id: f.id });
}
}
} catch {
// getChatFolders may not be available in older TDLib versions
}
const chatLists: Record<string, unknown>[] = [
{ _: "chatListMain" },
{ _: "chatListArchive" },
...folderLists,
];
// Phase 1: Load all chats into TDLib's cache
for (const chatList of chatLists) {
try {
for (let page = 0; page < 500; page++) {
await withFloodWait(
() => client.invoke({ _: "loadChats", chat_list: chatList, limit: 100 }),
"loadChats"
);
}
} catch {
// 404 = all chats loaded (expected), or unsupported list type
}
}
// Phase 2: Retrieve chat IDs and enrich with details
const seenChatIds = new Set<number>(); const seenChatIds = new Set<number>();
for (const chatList of [ for (const chatList of chatLists) {
{ _: "chatListMain" as const }, // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ _: "chatListArchive" as const }, let result: { chat_ids: number[] };
]) { try {
const MAX_PAGES = 500; // support up to 50,000 chats per list result = (await withFloodWait(
for (let page = 0; page < MAX_PAGES; page++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (await withFloodWait(
() => client.invoke({ () => client.invoke({
_: "getChats", _: "getChats",
chat_list: chatList, chat_list: chatList,
limit: 100, limit: 50000,
}), }),
"getChats" "getChats"
)) as { chat_ids: number[] }; )) as { chat_ids: number[] };
} catch {
continue;
}
if (!result.chat_ids || result.chat_ids.length === 0) { if (!result.chat_ids || result.chat_ids.length === 0) continue;
break;
}
for (const chatId of result.chat_ids) { for (const chatId of result.chat_ids) {
if (seenChatIds.has(chatId)) continue; if (seenChatIds.has(chatId)) continue;
seenChatIds.add(chatId); seenChatIds.add(chatId);
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const chat = (await withFloodWait( const chat = (await withFloodWait(
() => client.invoke({ () => client.invoke({
_: "getChat", _: "getChat",
chat_id: chatId, chat_id: chatId,
}), }),
"getChat" "getChat"
)) as any; )) as any;
const chatType = chat.type?._; const chatType = chat.type?._;
let type: TelegramChatInfo["type"] = "other"; let type: TelegramChatInfo["type"] = "other";
let isForum = false; let isForum = false;
let title = chat.title ?? `Chat ${chatId}`; let title = chat.title ?? `Chat ${chatId}`;
if (chatType === "chatTypeSupergroup") { if (chatType === "chatTypeSupergroup") {
// Get supergroup details to check if it's a channel or group try {
try { // eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any const sg = (await withFloodWait(
const sg = (await withFloodWait( () => client.invoke({
() => client.invoke({ _: "getSupergroup",
_: "getSupergroup", supergroup_id: chat.type.supergroup_id,
supergroup_id: chat.type.supergroup_id, }),
}), "getSupergroup"
"getSupergroup" )) as any;
)) as any;
type = sg.is_channel ? "channel" : "supergroup"; type = sg.is_channel ? "channel" : "supergroup";
isForum = sg.is_forum ?? false; isForum = sg.is_forum ?? false;
} catch { } catch {
type = "supergroup"; type = "supergroup";
} }
} else if (chatType === "chatTypeBasicGroup") { } else if (chatType === "chatTypeBasicGroup") {
type = "group"; type = "group";
} else if (chatType === "chatTypePrivate" || chatType === "chatTypeSecret") { } else if (chatType === "chatTypePrivate" || chatType === "chatTypeSecret") {
type = "private"; type = "private";
// Label the self-chat as "Saved Messages" if (selfUserId !== null && chat.type?.user_id === selfUserId) {
if (selfUserId !== null && chat.type?.user_id === selfUserId) { title = "Saved Messages";
title = "Saved Messages";
}
} }
chats.push({
chatId: BigInt(chatId),
title,
type,
isForum,
});
} catch (err) {
log.warn({ chatId, err }, "Failed to get chat details, skipping");
} }
}
await sleep(config.apiDelayMs); chats.push({
chatId: BigInt(chatId),
title,
type,
isForum,
});
} catch (err) {
log.warn({ chatId, err }, "Failed to get chat details, skipping");
}
} }
} }
log.info( log.info(
{ total: chats.length }, { total: chats.length },
"Fetched all chats from Telegram (main + archive)" "Fetched all chats from Telegram (main + archive + folders)"
); );
return chats; return chats;

View File

@@ -335,25 +335,45 @@ export async function runWorkerForAccount(
phone: account.phone, phone: account.phone,
}); });
// Load the full chat list so TDLib knows about all chats. // Load all chats into TDLib's local cache using loadChats (the recommended API).
// Without this, getChat/searchChatMessages fail with "Chat not found". // Without this, getChat/searchChatMessages fail with "Chat not found".
// TDLib returns chats in batches — keep calling until empty. // loadChats returns a 404 when all chats have been loaded — that's the stop signal.
// Load from both main and archive lists to cover older/archived chats. // Load from main, archive, AND chat folders to cover all chat types.
for (const chatList of [ {
{ _: "chatListMain" as const }, // Discover chat folders first
{ _: "chatListArchive" as const }, const folderLists: { _: "chatListFolder"; chat_folder_id: number }[] = [];
]) {
try { try {
for (let page = 0; page < 500; page++) { const folders = await client.invoke({ _: "getChatFolders" }) as {
const chatResult = await client.invoke({ chat_folders?: { id: number }[];
_: "getChats", };
chat_list: chatList, if (folders.chat_folders) {
limit: 100, for (const f of folders.chat_folders) {
}) as { chat_ids?: number[] }; folderLists.push({ _: "chatListFolder", chat_folder_id: f.id });
if (!chatResult.chat_ids || chatResult.chat_ids.length === 0) break; }
} }
} catch { } catch {
// Ignore — chat list may already be loaded // getChatFolders may not be available in older TDLib versions
}
const chatLists: Record<string, unknown>[] = [
{ _: "chatListMain" },
{ _: "chatListArchive" },
...folderLists,
];
for (const chatList of chatLists) {
try {
for (let page = 0; page < 500; page++) {
await client.invoke({
_: "loadChats",
chat_list: chatList,
limit: 100,
});
// loadChats returns ok — keep going until 404
}
} catch {
// 404 = all chats loaded (expected), or unsupported list type
}
} }
} }