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<string, unknown>,
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<string, unknown> = {}) {
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<string, DiscordThread>()
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,
}
}