/** * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { isValidString } from "../utils/typeguards.js"; /** * Forwards an announcement to our Mastodon account. * @param content - The main body of the announcement. * @returns A message indicating the success or failure of the operation. */ // eslint-disable-next-line max-lines-per-function, max-statements, complexity -- This is a big function. export const announceOnMastodon = async( content: Array, ): Promise => { if ( process.env.MASTODON_INSTANCE_URL === undefined || process.env.MASTODON_ACCESS_TOKEN === undefined ) { return "Mastodon credentials are not set."; } const [ firstPost, ...restOfPosts ] = content; const failedReplies: Array = []; if (firstPost === undefined) { return "No posts to send to Mastodon."; } const instanceUrl = process.env.MASTODON_INSTANCE_URL.replace(/\/$/, ""); const accessToken = process.env.MASTODON_ACCESS_TOKEN; const apiUrl = `${instanceUrl}/api/v1/statuses`; const headers = { // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name. "Authorization": `Bearer ${accessToken}`, // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name. "Content-Type": "application/json", }; const firstPostResponse = await fetch(apiUrl, { body: JSON.stringify({ status: firstPost }), headers: headers, method: "POST", }).catch((error: unknown) => { return error instanceof Error ? error.message : String(error); }); if (typeof firstPostResponse === "string") { return `Failed to send initial post to Mastodon. ${firstPostResponse}`; } if (!firstPostResponse.ok) { const errorText = await firstPostResponse.text().catch(() => { return firstPostResponse.statusText; }); return `Failed to send initial post to Mastodon. Status: ${firstPostResponse.status.toString()} ${errorText}`; } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Fetch does not accept generics. const firstPostData = await firstPostResponse.json() as { id?: string }; if (firstPostData.id === undefined) { return `Failed to parse initial post ID from Mastodon. ${JSON.stringify(firstPostData)}`; } let inReplyToId = firstPostData.id; for (const post of restOfPosts) { // eslint-disable-next-line no-await-in-loop -- We need to do this sequentially. const replyResponse = await fetch(apiUrl, { body: JSON.stringify({ // eslint-disable-next-line @typescript-eslint/naming-convention -- API requirement. in_reply_to_id: inReplyToId, status: post, }), headers: headers, method: "POST", }).catch((error: unknown) => { return error instanceof Error ? error.message : String(error); }); if (typeof replyResponse === "string") { failedReplies.push(post); continue; } if (!replyResponse.ok) { failedReplies.push(post); continue; } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, no-await-in-loop -- Fetch does not accept generics. const replyData = await replyResponse.json() as { id?: string }; if (isValidString(replyData.id)) { inReplyToId = replyData.id; continue; } failedReplies.push(post); } return `Successfully sent initial post to Mastodon. ${failedReplies.length > 0 ? `Failed to send ${failedReplies.length.toString()} replies: ${failedReplies.join(", ")}` : `All ${(content.length - 1).toString()} replies were sent successfully.`}`; };