Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/discord

src/list-channel-threads.ts

107 lines · 3.6 KB · TypeScript
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,
	}
}