diff --git a/src/api/Server.ts b/src/api/Server.ts index 84a145e37..2bf143504 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -28,8 +28,7 @@ import { Monitoring } from "../util/monitoring/Monitoring"; import { BcryptWorkerPool } from "../util/util/workers/bcrypt/BcryptWorkerPool"; import { Authentication, CORS, ImageProxy, BodyParser, ErrorHandler, initRateLimits, initTranslation } from "./middlewares"; import { initInstance } from "./util/handlers/Instance"; -import { route, sendMessage } from "./util"; -import { EmbedType, MessageReferenceType, MessageType, PollAnswerCount } from "@spacebar/schemas"; +import { route, addPendingPoll } from "./util"; const ASSETS_FOLDER = path.join(__dirname, "..", "..", "assets"); const PUBLIC_ASSETS_FOLDER = path.join(ASSETS_FOLDER, "public"); @@ -198,99 +197,7 @@ export class SpacebarServer extends Server { return; } - pendingPolls.set(message.id, { - timeout: setTimeout( - async () => { - if (!message.poll?.results) { - return; - } - - const allAnswerCounts = message.poll.results.answer_counts as unknown as (Omit & { voters: string[] })[]; - - const totalVotes = allAnswerCounts.map((a) => a.voters).length; - const winningAnswerCounts = allAnswerCounts.filter((a) => (a.count * totalVotes) / 100); - - const pollResultsMessage = { - type: MessageType.POLL_RESULT, - channel_id: message.channel_id, - author_id: message.author_id, - message_reference: { - type: MessageReferenceType.DEFAULT, - message_id: message.id, - channel_id: message.channel_id, - }, - embeds: [ - { - type: EmbedType.poll_result, - id: message.id, - fields: [ - { - name: "poll_question_text", - value: message.poll.question.text!, - }, - { - name: "total_votes", - value: totalVotes.toString(), - }, - ], - }, - ], - }; - - if (winningAnswerCounts) { - const winningAnswer = message.poll.answers.find((a) => a.answer_id === Number(winningAnswerCounts[0]?.id))!; - - if (winningAnswerCounts.length === 0) { - pollResultsMessage.embeds[0].fields.push({ - name: "victor_answer_votes", - value: "0", - }); - } else if (winningAnswerCounts.length === 1) { - pollResultsMessage.embeds[0].fields.push( - { - name: "victor_answer_votes", - value: winningAnswerCounts[0].count.toString(), - }, - { - name: "victor_answer_id", - value: winningAnswerCounts[0].id, - }, - { - name: "victor_answer_text", - value: winningAnswer.poll_media.text!, - }, - ); - } else if (winningAnswerCounts.length > 1) { - pollResultsMessage.embeds[0].fields.push({ - name: "victor_answer_votes", - value: winningAnswerCounts[0].count.toString(), - }); - } - - if (winningAnswer?.poll_media.emoji) { - pollResultsMessage.embeds[0].fields.push( - { - name: "victor_answer_emoji_id", - value: winningAnswer.poll_media.emoji.id!.toString()!, - }, - { - name: "victor_answer_emoji_name", - value: winningAnswer.poll_media.emoji.name!, - }, - { - name: "victor_answer_emoji_animated", - value: `${winningAnswer.poll_media.emoji.animated}`, - }, - ); - } - } - - await sendMessage(pollResultsMessage); - pendingPolls.delete(message.id); - }, - new Date(message.poll.expiry).getTime() - Date.now(), - ), - }); + addPendingPoll(message, new Date(message.poll.expiry).getTime() - Date.now()); } function isReady(req: Request, res: Response) { diff --git a/src/api/routes/channels/#channel_id/polls/#poll_id/expire.ts b/src/api/routes/channels/#channel_id/polls/#poll_id/expire.ts index 6a9bc4363..e4544db53 100644 --- a/src/api/routes/channels/#channel_id/polls/#poll_id/expire.ts +++ b/src/api/routes/channels/#channel_id/polls/#poll_id/expire.ts @@ -20,8 +20,7 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api/util/handlers/route"; import { Message } from "@spacebar/database"; import { DiscordApiErrors, emitEvent, MessageUpdateEvent, pendingPolls } from "@spacebar/util"; -import { EmbedType, MessageReferenceType, MessageType, PollAnswerCount } from "@spacebar/schemas"; -import { sendMessage } from "@spacebar/api/util"; +import { generatePollResultsMessage, sendMessage } from "@spacebar/api/util"; const router: Router = Router({ mergeParams: true }); @@ -52,93 +51,10 @@ router.post("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res event: "MESSAGE_UPDATE", } satisfies MessageUpdateEvent); - if (!message.poll.results) { - return; - } - - const allAnswerCounts = message.poll.results.answer_counts as unknown as (Omit & { voters: string[] })[]; - - const totalVotes = allAnswerCounts.map((a) => a.voters).length; - const winningAnswerCounts = allAnswerCounts.filter((a) => (a.count * totalVotes) / 100); - - const pollResultsMessage = { - type: MessageType.POLL_RESULT, - channel_id: message.channel_id, - author_id: message.author_id, - message_reference: { - type: MessageReferenceType.DEFAULT, - message_id: message.id, - channel_id: message.channel_id, - }, - embeds: [ - { - type: EmbedType.poll_result, - id: message.id, - fields: [ - { - name: "poll_question_text", - value: message.poll.question.text!, - }, - { - name: "total_votes", - value: totalVotes.toString(), - }, - ], - }, - ], - }; - - if (winningAnswerCounts) { - const winningAnswer = message.poll.answers.find((a) => a.answer_id === Number(winningAnswerCounts[0]?.id))!; - - if (winningAnswerCounts.length === 0) { - pollResultsMessage.embeds[0].fields.push({ - name: "victor_answer_votes", - value: "0", - }); - } else if (winningAnswerCounts.length === 1) { - pollResultsMessage.embeds[0].fields.push( - { - name: "victor_answer_votes", - value: winningAnswerCounts[0].count.toString(), - }, - { - name: "victor_answer_id", - value: winningAnswerCounts[0].id, - }, - { - name: "victor_answer_text", - value: winningAnswer.poll_media.text!, - }, - ); - } else if (winningAnswerCounts.length > 1) { - pollResultsMessage.embeds[0].fields.push({ - name: "victor_answer_votes", - value: winningAnswerCounts[0].count.toString(), - }); - } - - if (winningAnswer?.poll_media.emoji) { - pollResultsMessage.embeds[0].fields.push( - { - name: "victor_answer_emoji_id", - value: winningAnswer.poll_media.emoji.id!.toString()!, - }, - { - name: "victor_answer_emoji_name", - value: winningAnswer.poll_media.emoji.name!, - }, - { - name: "victor_answer_emoji_animated", - value: `${winningAnswer.poll_media.emoji.animated}`, - }, - ); - } - } - + const pollResultsMessage = await generatePollResultsMessage(message); await sendMessage(pollResultsMessage); - pendingPolls.delete(message.id); + pendingPolls.delete(message.id); res.send(message); }); diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 554d669e1..d3ce0da7d 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -37,7 +37,6 @@ import { makeObjectErrorContent, MessageCreateEvent, MessageFlags, - pendingPolls, Permissions, ROLE_MENTION, Snowflake, @@ -59,12 +58,12 @@ import { MessageCreateSchema, MessageReferenceType, MessageType, - PollAnswerCount, Reaction, ReadStateType, UnfurledMediaItem, v1CompTypes, } from "@spacebar/schemas"; +import { addPendingPoll } from "../utility/polls"; const allow_empty = false; // TODO: check webhook, application, system author, stickers @@ -497,97 +496,7 @@ export async function handleMessage(opts: MessageOptions): Promise { if (opts.poll?.duration) { message.poll.expiry = new Date(Date.now() + opts.poll.duration * 3600000); - - const pollTimeout = setTimeout(async () => { - if (!message.poll?.results) { - return; - } - - const allAnswerCounts = message.poll.results.answer_counts as unknown as (Omit & { voters: string[] })[]; - - const totalVotes = allAnswerCounts.map((a) => a.voters).length; - const winningAnswerCounts = allAnswerCounts.filter((a) => (a.count * totalVotes) / 100); - - const pollResultsMessage = { - type: MessageType.POLL_RESULT, - channel_id: message.channel_id, - author_id: message.author_id, - message_reference: { - type: MessageReferenceType.DEFAULT, - message_id: message.id, - channel_id: message.channel_id, - }, - embeds: [ - { - type: EmbedType.poll_result, - id: message.id, - fields: [ - { - name: "poll_question_text", - value: message.poll.question.text!, - }, - { - name: "total_votes", - value: totalVotes.toString(), - }, - ], - }, - ], - }; - - if (winningAnswerCounts) { - const winningAnswer = message.poll.answers.find((a) => a.answer_id === Number(winningAnswerCounts[0]?.id))!; - - if (winningAnswerCounts.length === 0) { - pollResultsMessage.embeds[0].fields.push({ - name: "victor_answer_votes", - value: "0", - }); - } else if (winningAnswerCounts.length === 1) { - pollResultsMessage.embeds[0].fields.push( - { - name: "victor_answer_votes", - value: winningAnswerCounts[0].count.toString(), - }, - { - name: "victor_answer_id", - value: winningAnswerCounts[0].id, - }, - { - name: "victor_answer_text", - value: winningAnswer.poll_media.text!, - }, - ); - } else if (winningAnswerCounts.length > 1) { - pollResultsMessage.embeds[0].fields.push({ - name: "victor_answer_votes", - value: winningAnswerCounts[0].count.toString(), - }); - } - - if (winningAnswer?.poll_media.emoji) { - pollResultsMessage.embeds[0].fields.push( - { - name: "victor_answer_emoji_id", - value: winningAnswer.poll_media.emoji.id!.toString()!, - }, - { - name: "victor_answer_emoji_name", - value: winningAnswer.poll_media.emoji.name!, - }, - { - name: "victor_answer_emoji_animated", - value: `${winningAnswer.poll_media.emoji.animated}`, - }, - ); - } - } - - await sendMessage(pollResultsMessage); - pendingPolls.delete(message.id); - }, opts.poll.duration * 3600000); - - pendingPolls.set(message.id, { timeout: pollTimeout }); + addPendingPoll(message, opts.poll.duration * 3600000); } if (opts.poll?.answers) { @@ -689,7 +598,7 @@ export async function sendMessage(opts: MessageOptions) { } type MessageOptionAttachment = MessageCreateAttachment | MessageCreateCloudAttachment | Attachment; -interface MessageOptions extends MessageCreateSchema { +export interface MessageOptions extends MessageCreateSchema { id?: string; type?: MessageType; pinned?: boolean; diff --git a/src/api/util/index.ts b/src/api/util/index.ts index b04af8d15..b57d263ac 100644 --- a/src/api/util/index.ts +++ b/src/api/util/index.ts @@ -23,3 +23,4 @@ export * from "./handlers/route"; export * from "./handlers/Voice"; export * from "./utility/captcha"; export * from "./utility/EmbedHandlers"; +export * from "./utility/polls"; diff --git a/src/api/util/utility/polls.ts b/src/api/util/utility/polls.ts new file mode 100644 index 000000000..48c941219 --- /dev/null +++ b/src/api/util/utility/polls.ts @@ -0,0 +1,136 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { EmbedType, MessageReferenceType, MessageType, PollAnswerCount } from "@spacebar/schemas"; +import { pendingPolls } from "@spacebar/util"; +import { MessageOptions, sendMessage } from "../handlers/Message"; +import { Message } from "@spacebar/database"; + +export async function generatePollResultsMessage(options: MessageOptions): Promise { + const message = await Message.create({ + ...options, + message_reference: options.message_reference ?? undefined, + poll: options.poll, + // sticker_items: stickers, + // guild_id: options.guild_id, + channel_id: options.channel_id, + attachments: [], + embeds: options.embeds || [], + reactions: [], + type: MessageType.POLL_RESULT, + mentions: [], + components: [], + }); + + if (!message.poll?.results) { + return {}; + } + + const allAnswerCounts = message.poll.results.answer_counts as unknown as (Omit & { voters: string[] })[]; + + const totalVotes = allAnswerCounts.map((a) => a.voters).length; + const winningAnswerCounts = allAnswerCounts.filter((a) => (a.count * totalVotes) / 100); + + const pollResultsMessage = { + type: MessageType.POLL_RESULT, + channel_id: message.channel_id, + author_id: message.author_id, + message_reference: { + type: MessageReferenceType.DEFAULT, + message_id: message.id, + channel_id: message.channel_id, + }, + embeds: [ + { + type: EmbedType.poll_result, + id: message.id, + fields: [ + { + name: "poll_question_text", + value: message.poll.question.text!, + }, + { + name: "total_votes", + value: totalVotes.toString(), + }, + ], + }, + ], + }; + + if (winningAnswerCounts) { + const winningAnswer = message.poll.answers.find((a) => a.answer_id === Number(winningAnswerCounts[0]?.id))!; + + if (winningAnswerCounts.length === 0) { + pollResultsMessage.embeds[0].fields.push({ + name: "victor_answer_votes", + value: "0", + }); + } else if (winningAnswerCounts.length === 1) { + pollResultsMessage.embeds[0].fields.push( + { + name: "victor_answer_votes", + value: winningAnswerCounts[0].count.toString(), + }, + { + name: "victor_answer_id", + value: winningAnswerCounts[0].id, + }, + { + name: "victor_answer_text", + value: winningAnswer.poll_media.text!, + }, + ); + } else if (winningAnswerCounts.length > 1) { + pollResultsMessage.embeds[0].fields.push({ + name: "victor_answer_votes", + value: winningAnswerCounts[0].count.toString(), + }); + } + + if (winningAnswer?.poll_media.emoji) { + pollResultsMessage.embeds[0].fields.push( + { + name: "victor_answer_emoji_id", + value: winningAnswer.poll_media.emoji.id!.toString()!, + }, + { + name: "victor_answer_emoji_name", + value: winningAnswer.poll_media.emoji.name!, + }, + { + name: "victor_answer_emoji_animated", + value: `${winningAnswer.poll_media.emoji.animated}`, + }, + ); + } + } + + return pollResultsMessage; +} + +export async function addPendingPoll(message: Message, timeoutTime: number) { + pendingPolls.set(message.id, { + timeout: setTimeout(async () => { + const pollResultsMessage = await generatePollResultsMessage(message); + + await sendMessage(pollResultsMessage); + pendingPolls.delete(message.id); + }, timeoutTime), + }); +}