summary refs log tree commit diff
diff options
context:
space:
mode:
authorCyberL1 <cyber.hf18@gmail.com>2026-06-25 21:18:04 +0200
committerRory& <root@rory.gay>2026-06-26 16:42:33 +0200
commitcaf74417b587406c24d80850cf78ab004906717c (patch)
tree7b883ad593d72e1b32745d29681468ecadf5fe66
parentfix: check if message is a poll (diff)
downloadserver-ts-caf74417b587406c24d80850cf78ab004906717c.tar.xz
feat: add poll ended messages
-rw-r--r--assets/openapi.json1
-rw-r--r--assets/schemas.json1
-rw-r--r--src/api/routes/channels/#channel_id/polls/#poll_id/expire.ts94
-rw-r--r--src/api/util/handlers/Message.ts93
-rw-r--r--src/schemas/api/messages/Embeds.ts1
-rw-r--r--src/util/imports/Polls.ts25
-rw-r--r--src/util/imports/index.ts1
7 files changed, 214 insertions, 2 deletions
diff --git a/assets/openapi.json b/assets/openapi.json

index f9f6f540..424ef6d8 100644 --- a/assets/openapi.json +++ b/assets/openapi.json
@@ -11472,6 +11472,7 @@ "gifv", "image", "link", + "poll_result", "rich", "video" ], diff --git a/assets/schemas.json b/assets/schemas.json
index a6a73abf..c49f1375 100644 --- a/assets/schemas.json +++ b/assets/schemas.json
@@ -12183,6 +12183,7 @@ "gifv", "image", "link", + "poll_result", "rich", "video" ], 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 29f57c69..a24af990 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
@@ -19,7 +19,9 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api/util/handlers/route"; import { Message } from "#database"; -import { DiscordApiErrors, emitEvent, MessageUpdateEvent } from "#util"; +import { DiscordApiErrors, emitEvent, MessageUpdateEvent, pendingPolls } from "#util"; +import { EmbedType, MessageReferenceType, MessageType, PollAnswerCount } from "#schemas"; +import { sendMessage } from "#api/util"; const router: Router = Router({ mergeParams: true }); @@ -43,13 +45,101 @@ router.post("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res message.poll.expiry = new Date(); await message.save(); - res.send(message); await emitEvent({ channel_id, data: message.toJSON(), event: "MESSAGE_UPDATE", } satisfies MessageUpdateEvent); + + if (!message.poll.results) { + return; + } + + const allAnswerCounts = message.poll.results.answer_counts as unknown as (Omit<PollAnswerCount, "me_voted"> & { 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); + + res.send(message); }); export default router; diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 53360ed7..9a4ce43e 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts
@@ -37,6 +37,7 @@ import { makeObjectErrorContent, MessageCreateEvent, MessageFlags, + pendingPolls, Permissions, ROLE_MENTION, Snowflake, @@ -58,6 +59,7 @@ import { MessageCreateSchema, MessageReferenceType, MessageType, + PollAnswerCount, Reaction, ReadStateType, UnfurledMediaItem, @@ -494,6 +496,97 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { 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<PollAnswerCount, "me_voted"> & { 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 }); } if (opts.poll?.answers) { diff --git a/src/schemas/api/messages/Embeds.ts b/src/schemas/api/messages/Embeds.ts
index 0f6019a6..ba27accb 100644 --- a/src/schemas/api/messages/Embeds.ts +++ b/src/schemas/api/messages/Embeds.ts
@@ -55,6 +55,7 @@ export enum EmbedType { gifv = "gifv", article = "article", link = "link", + poll_result = "poll_result", } export interface EmbedImage { diff --git a/src/util/imports/Polls.ts b/src/util/imports/Polls.ts new file mode 100644
index 00000000..bad10085 --- /dev/null +++ b/src/util/imports/Polls.ts
@@ -0,0 +1,25 @@ +/* + 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 <https://www.gnu.org/licenses/>. +*/ + +import { Snowflake } from "@spacebar/util"; + +interface PendingPoll { + timeout: NodeJS.Timeout; +} + +export const pendingPolls = new Map<Snowflake, PendingPoll>(); diff --git a/src/util/imports/index.ts b/src/util/imports/index.ts
index 5e690d4a..a2162208 100644 --- a/src/util/imports/index.ts +++ b/src/util/imports/index.ts
@@ -19,3 +19,4 @@ export * from "./OrmUtils"; export * from "./Jimp"; export * from "./Interactions"; +export * from "./Polls";