/** * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable @typescript-eslint/naming-convention -- we are making raw API calls. */ /* eslint-disable max-lines-per-function -- Chunked sending requires more logic. */ /* eslint-disable max-statements -- Chunked sending requires more statements. */ /* eslint-disable no-await-in-loop -- Sequential chunk posting requires awaiting each request. */ import { chunkContent } from "../utils/chunkContent.js"; import type { AnnouncementType } from "../interfaces/announcementType.js"; const discordLimit = 2000; const channelIds: Record = { community: "1386105484313886820", company: "1422472775695728661", products: "1386105452881776661", }; const roleIds: Record, string> = { community: "1386107941224054895", products: "1386107909699666121", }; const getAnnouncementPing = (type: AnnouncementType): string => { return type === "company" ? "@everyone" : `<@&${roleIds[type]}>`; }; /** * Forwards an announcement to our Discord server. * Sends content in sequential messages if it exceeds the 2000 character limit. * @param title - The title of the announcement. * @param content - The main body of the announcement. * @param type - Whether the announcement is for a product or community. * @returns A message indicating the success or failure of the operation. */ export const announceOnDiscord = async( title: string, content: string, type: AnnouncementType, ): Promise => { const channelId = channelIds[type]; const ping = getAnnouncementPing(type); const firstMessagePrefix = `# ${title}\n\n`; const firstMessageSuffix = `\n-# ${ping}`; const firstChunkLimit = discordLimit - firstMessagePrefix.length - firstMessageSuffix.length; const chunks = chunkContent(content, firstChunkLimit); const firstChunk = chunks[0] ?? ""; const remainingChunks = chunks.slice(1); const messageRequest = await fetch( `https://discord.com/api/v10/channels/${channelId}/messages`, { body: JSON.stringify({ allowed_mentions: { parse: [ "users", "roles" ] }, content: `${firstMessagePrefix}${firstChunk}${firstMessageSuffix}`, }), headers: { "Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`, "Content-Type": "application/json", }, method: "POST", }, ); if (messageRequest.status !== 200) { return `Failed to send message to Discord. Status: ${messageRequest.status.toString()} ${messageRequest.statusText}`; } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- fetch does not accept generics. const message = await messageRequest.json() as { id?: string }; if (message.id === undefined) { return `Failed to parse message ID, cannot crosspost. ${JSON.stringify(message)}`; } const crosspostRequest = await fetch( `https://discord.com/api/v10/channels/${channelId}/messages/${message.id}/crosspost`, { headers: { "Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`, "Content-Type": "application/json", }, method: "POST", }, ); if (!crosspostRequest.ok) { return `Failed to crosspost message to Discord. Status: ${crosspostRequest.status.toString()} ${crosspostRequest.statusText}`; } for (const chunk of remainingChunks) { const chunkRequest = await fetch( `https://discord.com/api/v10/channels/${channelId}/messages`, { body: JSON.stringify({ content: chunk, }), headers: { "Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`, "Content-Type": "application/json", }, method: "POST", }, ); if (!chunkRequest.ok) { return `Failed to send continuation chunk to Discord. Status: ${chunkRequest.status.toString()} ${chunkRequest.statusText}`; } } return "Successfully sent and published message to Discord."; };