Files
erika/lib/leaderboard/discord.ts
T

79 lines
2.1 KiB
TypeScript

import "server-only";
export type DiscordVoiceChannel = {
id: string;
name: string;
type: 2 | 13;
position: number;
};
type DiscordChannelPayload = {
id?: unknown;
name?: unknown;
type?: unknown;
position?: unknown;
};
const CHANNEL_CACHE_TTL_MS = 5 * 60_000;
const channelCache: {
data: DiscordVoiceChannel[] | null;
expiresAt: number;
} = {
data: null,
expiresAt: 0,
};
export function parseCurrentVoiceChannels(payload: unknown): DiscordVoiceChannel[] {
if (!Array.isArray(payload)) {
throw new Error("Discord returned an invalid channel list.");
}
return payload
.filter((channel): channel is DiscordChannelPayload => {
if (!channel || typeof channel !== "object") return false;
const type = (channel as DiscordChannelPayload).type;
return type === 2 || type === 13;
})
.filter((channel) => (
typeof channel.id === "string"
&& typeof channel.name === "string"
))
.map((channel) => ({
id: channel.id as string,
name: channel.name as string,
type: channel.type as 2 | 13,
position: typeof channel.position === "number" ? channel.position : 0,
}))
.sort((a, b) => a.position - b.position || a.name.localeCompare(b.name, "th"));
}
export async function getCurrentDiscordVoiceChannels() {
if (channelCache.data && Date.now() < channelCache.expiresAt) {
return channelCache.data;
}
const token = process.env.DISCORD_BOT_TOKEN;
const guildId = process.env.DISCORD_GUILD_ID;
if (!token || !guildId) {
throw new Error("Discord channel access is not configured.");
}
const response = await fetch(
`https://discord.com/api/v10/guilds/${guildId}/channels`,
{
headers: { Authorization: `Bot ${token}` },
cache: "no-store",
signal: AbortSignal.timeout(5_000),
},
);
if (!response.ok) {
throw new Error(`Discord channel request failed with status ${response.status}.`);
}
const channels = parseCurrentVoiceChannels(await response.json());
channelCache.data = channels;
channelCache.expiresAt = Date.now() + CHANNEL_CACHE_TTL_MS;
return channels;
}