import { discordBotRequest } from './bot.ts' import { boundedInteger, inputRecord, optionalBoolean, optionalSnowflake, snowflake } from './validation.ts' type DiscordThread = { id?: string name?: string parent_id?: string owner_id?: string message_count?: number member_count?: number thread_metadata?: { archived?: boolean locked?: boolean create_timestamp?: string archive_timestamp?: string } } function simplifyThread(thread: DiscordThread) { return { id: thread.id ?? null, name: thread.name ?? null, parentChannelId: thread.parent_id ?? null, ownerId: thread.owner_id ?? null, messageCount: thread.message_count ?? null, memberCount: thread.member_count ?? null, archived: thread.thread_metadata?.archived === true, locked: thread.thread_metadata?.locked === true, createdAt: thread.thread_metadata?.create_timestamp ?? null, archiveTimestamp: thread.thread_metadata?.archive_timestamp ?? null, } } async function fetchArchivedPublicThreads( input: Record, channelId: string, limit: number, maxPages: number, ) { const threads: DiscordThread[] = [] let before: string | null = null for (let page = 0; page < maxPages && threads.length < limit; page += 1) { const pageLimit = Math.min(100, limit - threads.length) const result = (await discordBotRequest('/channels/' + channelId + '/threads/archived/public', input, { query: { limit: pageLimit, before: before ?? undefined, }, })) as { threads?: DiscordThread[]; has_more?: boolean } const pageThreads = Array.isArray(result?.threads) ? result.threads : [] threads.push(...pageThreads) if (!result?.has_more || pageThreads.length === 0) break before = pageThreads[pageThreads.length - 1]?.thread_metadata?.archive_timestamp ?? null if (!before) break } return threads } /** * List active and archived public threads under a Discord channel. * * Requires both `channelId` and `guildId`. Optional `ownerId` filters to * threads created by that user. * * @param params.secretName - Bot-token secret; default `discordBotToken`. */ export default async function listChannelThreads(params: Record = {}) { const input = inputRecord(params) const channelId = snowflake(input, 'channelId', ['channel_id', 'parentChannelId', 'parent_channel_id']) const guildId = snowflake(input, 'guildId', ['guild_id']) const ownerId = optionalSnowflake(input, 'ownerId', ['owner_id']) const includeArchived = optionalBoolean(input, 'includeArchived') ?? optionalBoolean(input, 'include_archived') ?? true const limit = boundedInteger(input.limit, 100, 1, 500, 'limit') const maxPages = boundedInteger(input.maxPages, 5, 1, 10, 'maxPages') const allThreads = new Map() const active = (await discordBotRequest('/guilds/' + guildId + '/threads/active', input)) as { threads?: DiscordThread[] } for (const thread of Array.isArray(active?.threads) ? active.threads : []) { if (thread?.parent_id === channelId && typeof thread.id === 'string') allThreads.set(thread.id, thread) } if (includeArchived) { for (const thread of await fetchArchivedPublicThreads(input, channelId, limit, maxPages)) { if (typeof thread.id === 'string') allThreads.set(thread.id, thread) } } const threads = [...allThreads.values()] .filter((thread) => !ownerId || thread.owner_id === ownerId) .map(simplifyThread) .sort((left, right) => String(right.createdAt || right.archiveTimestamp || '').localeCompare( String(left.createdAt || left.archiveTimestamp || ''), ), ) .slice(0, limit) return { channelId, guildId, ownerId: ownerId ?? null, includeArchived, count: threads.length, threads, } }