import { parseReleasePost, RELEASE_NEWS_CHANNEL, RELEASE_WEBHOOK_ID, type ReleasePost, } from "../shared/changelog"; export async function readChangelogFeed({ token, channel = RELEASE_NEWS_CHANNEL, webhookId = RELEASE_WEBHOOK_ID, before, request = fetch, }: { token: string; channel?: string; webhookId?: string; before?: string; request?: typeof fetch; }) { if (!/^\d{17,20}$/.test(channel) || (before && !/^\d{17,20}$/.test(before))) throw new Error("Invalid news channel or cursor"); const url = new URL( `https://discord.com/api/v10/channels/${channel}/messages`, ); url.searchParams.set("limit", "100"); if (before) url.searchParams.set("before", before); // Reading history only. No Discord write endpoint is used by this feature. const response = await request(url.toString(), { method: "GET", headers: { Authorization: `Bot ${token}` }, next: { revalidate: 60 }, signal: AbortSignal.timeout(10_000), redirect: "error", }); if (!response.ok) throw new Error(`News feed unavailable (${response.status})`); const posts: ReleasePost[] = await response.json(); if ( !Array.isArray(posts) || posts.some( (post) => typeof post.content !== "string" || post.channel_id !== channel || !/^\d{17,20}$/.test(post.id), ) ) throw new Error("Invalid news response"); if ( posts.some((post) => post.webhook_id === webhookId) && posts .filter((post) => post.webhook_id === webhookId) .every((post) => !post.content.trim()) ) throw new Error( "Release text is unavailable; check Discord Message Content access", ); const entries = posts .flatMap((post) => { const entry = parseReleasePost(post, webhookId); return entry ? [entry] : []; }) .sort( (a, b) => b.publishedAt.localeCompare(a.publishedAt) || (BigInt(a.id) > BigInt(b.id) ? -1 : 1), ); const oldest = posts.reduce( (id, post) => (!id || BigInt(post.id) < BigInt(id) ? post.id : id), undefined, ); return { entries, before: posts.length === 100 && oldest !== before ? oldest : undefined, }; }