From 6ad4e1d2e863c08ca1dc882e7b594c8f3fe79da6 Mon Sep 17 00:00:00 2001 From: Fleny Date: Sun, 28 Apr 2024 13:42:19 +0200 Subject: [PATCH] feat(bot,rest,types): Add polls support (#3542) * Add polls support * Add default for limit on GetPollAnswerVotes * Apply code rewiew suggestion Co-authored-by: LTS20050703 --------- Co-authored-by: LTS20050703 --- packages/bot/src/bot.ts | 4 +- packages/bot/src/handlers.ts | 2 + packages/bot/src/handlers/index.ts | 1 + .../handlers/poll/MESSAGE_POLL_VOTE_ADD.ts | 16 +++ .../handlers/poll/MESSAGE_POLL_VOTE_REMOVE.ts | 16 +++ packages/bot/src/handlers/poll/index.ts | 2 + packages/bot/src/transformers.ts | 59 +++++++++ packages/bot/src/transformers/poll.ts | 105 ++++++++++++++++ packages/bot/src/typings.ts | 2 + packages/rest/src/manager.ts | 9 ++ packages/rest/src/routes.ts | 16 +++ packages/rest/src/types.ts | 30 +++++ packages/rest/src/typings/routes.ts | 5 + packages/types/src/camel.ts | 12 ++ packages/types/src/discord.ts | 118 ++++++++++++++++++ packages/types/src/discordeno.ts | 33 +++++ packages/types/src/shared.ts | 14 +++ 17 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_ADD.ts create mode 100644 packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_REMOVE.ts create mode 100644 packages/bot/src/handlers/poll/index.ts create mode 100644 packages/bot/src/transformers/poll.ts diff --git a/packages/bot/src/bot.ts b/packages/bot/src/bot.ts index d882eb4d2..3cb0ba2a6 100644 --- a/packages/bot/src/bot.ts +++ b/packages/bot/src/bot.ts @@ -13,7 +13,7 @@ import type { AutoModerationActionExecution } from './transformers/automodAction import type { AutoModerationRule } from './transformers/automodRule.js' import type { Channel } from './transformers/channel.js' import type { Emoji } from './transformers/emoji.js' -import { type Entitlement } from './transformers/entitlement.js' +import type { Entitlement } from './transformers/entitlement.js' import type { Guild } from './transformers/guild.js' import type { Integration } from './transformers/integration.js' import type { Interaction } from './transformers/interaction.js' @@ -247,4 +247,6 @@ export interface EventHandlers { entitlementCreate: (entitlement: Entitlement) => unknown entitlementUpdate: (entitlement: Entitlement) => unknown entitlementDelete: (entitlement: Entitlement) => unknown + messagePollVoteAdd: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown + messagePollVoteRemove: (payload: { userId: bigint; channelId: bigint; messageId: bigint; guildId?: bigint; answerId: number }) => unknown } diff --git a/packages/bot/src/handlers.ts b/packages/bot/src/handlers.ts index d97d51cd5..6b76d043b 100644 --- a/packages/bot/src/handlers.ts +++ b/packages/bot/src/handlers.ts @@ -69,6 +69,8 @@ export function createBotGatewayHandlers( ENTITLEMENT_CREATE: options.ENTITLEMENT_CREATE ?? handlers.handleEntitlementCreate, ENTITLEMENT_UPDATE: options.ENTITLEMENT_UPDATE ?? handlers.handleEntitlementUpdate, ENTITLEMENT_DELETE: options.ENTITLEMENT_DELETE ?? handlers.handleEntitlementDelete, + MESSAGE_POLL_VOTE_ADD: options.MESSAGE_POLL_VOTE_ADD ?? handlers.handleMessagePollVoteAdd, + MESSAGE_POLL_VOTE_REMOVE: options.MESSAGE_POLL_VOTE_REMOVE ?? handlers.handleMessagePollVoteRemove, } } diff --git a/packages/bot/src/handlers/index.ts b/packages/bot/src/handlers/index.ts index 29085efe1..bfaa705b5 100644 --- a/packages/bot/src/handlers/index.ts +++ b/packages/bot/src/handlers/index.ts @@ -8,6 +8,7 @@ export * from './invites/index.js' export * from './members/index.js' export * from './messages/index.js' export * from './misc/index.js' +export * from './poll/index.js' export * from './roles/index.js' export * from './voice/index.js' export * from './webhooks/index.js' diff --git a/packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_ADD.ts b/packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_ADD.ts new file mode 100644 index 000000000..a76248aaf --- /dev/null +++ b/packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_ADD.ts @@ -0,0 +1,16 @@ +import type { DiscordGatewayPayload, DiscordPollVoteAdd } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleMessagePollVoteAdd(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.messagePollVoteAdd) return + + const payload = data.d as DiscordPollVoteAdd + + bot.events.messagePollVoteAdd({ + userId: bot.transformers.snowflake(payload.user_id), + channelId: bot.transformers.snowflake(payload.channel_id), + messageId: bot.transformers.snowflake(payload.message_id), + guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined, + answerId: payload.answer_id, + }) +} diff --git a/packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_REMOVE.ts b/packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_REMOVE.ts new file mode 100644 index 000000000..8d853b20b --- /dev/null +++ b/packages/bot/src/handlers/poll/MESSAGE_POLL_VOTE_REMOVE.ts @@ -0,0 +1,16 @@ +import type { DiscordGatewayPayload, DiscordPollVoteRemove } from '@discordeno/types' +import type { Bot } from '../../index.js' + +export async function handleMessagePollVoteRemove(bot: Bot, data: DiscordGatewayPayload): Promise { + if (!bot.events.messagePollVoteRemove) return + + const payload = data.d as DiscordPollVoteRemove + + bot.events.messagePollVoteRemove({ + userId: bot.transformers.snowflake(payload.user_id), + channelId: bot.transformers.snowflake(payload.channel_id), + messageId: bot.transformers.snowflake(payload.message_id), + guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined, + answerId: payload.answer_id, + }) +} diff --git a/packages/bot/src/handlers/poll/index.ts b/packages/bot/src/handlers/poll/index.ts new file mode 100644 index 000000000..74fc15476 --- /dev/null +++ b/packages/bot/src/handlers/poll/index.ts @@ -0,0 +1,2 @@ +export * from './MESSAGE_POLL_VOTE_ADD.js' +export * from './MESSAGE_POLL_VOTE_REMOVE.js' diff --git a/packages/bot/src/transformers.ts b/packages/bot/src/transformers.ts index a8d99e925..25f4e1b39 100644 --- a/packages/bot/src/transformers.ts +++ b/packages/bot/src/transformers.ts @@ -31,6 +31,8 @@ import type { DiscordInviteStageInstance, DiscordMember, DiscordMessage, + DiscordPoll, + DiscordPollMedia, DiscordPresenceUpdate, DiscordRole, DiscordScheduledEvent, @@ -85,6 +87,7 @@ import { transformInvite, type Invite } from './transformers/invite.js' import { transformMember, type Member } from './transformers/member.js' import { transformMessage, type Message } from './transformers/message.js' import { transformGuildOnboarding, type GuildOnboarding } from './transformers/onboarding.js' +import { transformPoll, transformPollMedia, type Poll, type PollMedia } from './transformers/poll.js' import { transformPresence, type PresenceUpdate } from './transformers/presence.js' import { transformAllowedMentionsToDiscordAllowedMentions } from './transformers/reverse/allowedMentions.js' import { transformCreateApplicationCommandToDiscordCreateApplicationCommand } from './transformers/reverse/createApplicationCommand.js' @@ -165,6 +168,8 @@ export interface Transformers { guildOnboarding: (bot: Bot, payload: DiscordGuildOnboarding, onboarding: GuildOnboarding) => any entitlement: (bot: Bot, payload: DiscordEntitlement, entitlement: Entitlement) => any sku: (bot: Bot, payload: DiscordSku, sku: Sku) => any + poll: (bot: Bot, payload: DiscordPoll, poll: Poll) => any + pollMedia: (bot: Bot, payload: DiscordPollMedia, pollMedia: PollMedia) => any } desiredProperties: { attachment: { @@ -506,6 +511,28 @@ export interface Transformers { sessionId: boolean userId: boolean } + poll: { + question: boolean + answers: { + answerId: boolean + pollMedia: boolean + } + expiry: boolean + allowMultiselect: boolean + layoutType: boolean + results: { + isFinalized: boolean + answerCounts: { + id: boolean + count: boolean + meVoted: boolean + } + } + } + pollMedia: { + text: boolean + emoji: boolean + } } reverse: { allowedMentions: (bot: Bot, payload: AllowedMentions) => DiscordAllowedMentions @@ -569,6 +596,8 @@ export interface Transformers { guildOnboarding: (bot: Bot, payload: DiscordGuildOnboarding) => GuildOnboarding entitlement: (bot: Bot, payload: DiscordEntitlement) => Entitlement sku: (bot: Bot, payload: DiscordSku) => Sku + poll: (bot: Bot, payload: DiscordPoll) => Poll + pollMedia: (bot: Bot, payload: DiscordPollMedia) => PollMedia } export interface CreateTransformerOptions { @@ -719,6 +748,12 @@ export function createTransformers(options: Partial, opts?: Create sku(bot, payload, sku) { return sku }, + poll(bot, payload, poll) { + return poll + }, + pollMedia(bot, payload, pollMedia) { + return pollMedia + }, }, desiredProperties: { attachment: { @@ -1060,6 +1095,28 @@ export function createTransformers(options: Partial, opts?: Create sessionId: opts?.defaultDesiredPropertiesValue ?? false, userId: opts?.defaultDesiredPropertiesValue ?? false, }, + poll: { + question: opts?.defaultDesiredPropertiesValue ?? false, + answers: { + answerId: opts?.defaultDesiredPropertiesValue ?? false, + pollMedia: opts?.defaultDesiredPropertiesValue ?? false, + }, + expiry: opts?.defaultDesiredPropertiesValue ?? false, + layoutType: opts?.defaultDesiredPropertiesValue ?? false, + allowMultiselect: opts?.defaultDesiredPropertiesValue ?? false, + results: { + isFinalized: opts?.defaultDesiredPropertiesValue ?? false, + answerCounts: { + id: opts?.defaultDesiredPropertiesValue ?? false, + count: opts?.defaultDesiredPropertiesValue ?? false, + meVoted: opts?.defaultDesiredPropertiesValue ?? false, + }, + }, + }, + pollMedia: { + text: opts?.defaultDesiredPropertiesValue ?? false, + emoji: opts?.defaultDesiredPropertiesValue ?? false, + }, }, reverse: { allowedMentions: options.reverse?.allowedMentions ?? transformAllowedMentionsToDiscordAllowedMentions, @@ -1123,5 +1180,7 @@ export function createTransformers(options: Partial, opts?: Create guildOnboarding: options.guildOnboarding ?? transformGuildOnboarding, entitlement: options.entitlement ?? transformEntitlement, sku: options.sku ?? transformSku, + poll: options.poll ?? transformPoll, + pollMedia: options.pollMedia ?? transformPollMedia, } } diff --git a/packages/bot/src/transformers/poll.ts b/packages/bot/src/transformers/poll.ts new file mode 100644 index 000000000..fff538e94 --- /dev/null +++ b/packages/bot/src/transformers/poll.ts @@ -0,0 +1,105 @@ +import type { DiscordEmoji, DiscordPoll, DiscordPollLayoutType, DiscordPollMedia } from '@discordeno/types' +import type { Bot, Emoji } from '../index.js' + +export function transformPoll(bot: Bot, payload: DiscordPoll): Poll { + const props = bot.transformers.desiredProperties.poll + const poll = {} as Poll + + if (props.question && payload.question) poll.question = bot.transformers.pollMedia(bot, payload.question) + if (props.answers && payload.answers) + poll.answers = payload.answers.map((x) => ({ answerId: x.answer_id, pollMedia: bot.transformers.pollMedia(bot, x.poll_media) })) + if (props.expiry && payload.expiry) poll.expiry = Date.parse(payload.expiry) + if (props.allowMultiselect && payload.allow_multiselect) poll.allowMultiselect = payload.allow_multiselect + if (props.layoutType) poll.layoutType = payload.layout_type + if (props.results && payload.results) { + poll.results = {} as PollResult + + if (props.results.isFinalized && payload.results.is_finalized) poll.results.isFinalized = payload.results.is_finalized + if (props.results.answerCounts && payload.results.answer_counts) + poll.results.answerCounts = payload.results.answer_counts.map((x) => ({ id: x.id, count: x.count, meVoted: x.me_voted })) + } + + return bot.transformers.customizers.poll(bot, payload, poll) +} + +export function transformPollMedia(bot: Bot, payload: DiscordPollMedia): PollMedia { + const props = bot.transformers.desiredProperties.pollMedia + const pollMedia = {} as PollMedia + + if (props.text && payload.text) pollMedia.text = payload.text + if (props.emoji && payload.emoji) pollMedia.emoji = bot.transformers.emoji(bot, payload.emoji as DiscordEmoji) + + return bot.transformers.customizers.pollMedia(bot, payload, pollMedia) +} + +export interface Poll { + /** The question of the poll. Only `text` is supported. */ + question: PollMedia + /** Each of the answers available in the poll. There is a maximum of 10 answers per poll. */ + answers: PollAnswer[] + /** + * The time when the poll ends. + * + * @remarks + * `expiry` is marked as nullable to support non-expiring polls in the future, but all polls have an expiry currently. + */ + expiry: number | null + /** Whether a user can select multiple answers */ + allowMultiselect: boolean + /** The layout type of the poll */ + layoutType: DiscordPollLayoutType + /** + * The results of the poll + * + * @remarks + * This value will not be sent by discord under specific conditions where they don't fetch them on their backend. When this value is missing it should be interpreted as "Unknown results" and not as "No results" + * The results may not be totally accurate while the poll has not ended. When it ends discord will re-calculate all the results and set {@link DiscordPollResult.is_finalized} to true + */ + results?: PollResult +} + +export interface PollMedia { + /** + * The text of the field + * + * @remarks + * `text` should always be non-null for both questions and answers, but this is subject to changes. + * The maximum length of `text` is 300 for the question, and 55 for any answer. + */ + text?: string + /** + * The emoji of the field + * + * @remarks + * When creating a poll answer with an emoji, one only needs to send either the `id` (custom emoji) or `name` (default emoji) as the only field. + */ + emoji?: Partial +} + +export interface PollAnswer { + /** + * The id of the answer + * + * @remarks + * This id labels each answer. It starts at 1 and goes up sequentially. Discord recommend against depending on this sequence as it is an implementation detail. + */ + answerId: number + /** The data of the answer */ + pollMedia: PollMedia +} + +export interface PollResult { + /** Whether the votes have been precisely counted */ + isFinalized: boolean + /** The counts for each answer */ + answerCounts: PollAnswerCount[] +} + +export interface PollAnswerCount { + /** The {@link PollAnswer.answerId | answerId} */ + id: number + /** The number of votes for this answer */ + count: number + /** Whether the current user voted for this answer */ + meVoted: boolean +} diff --git a/packages/bot/src/typings.ts b/packages/bot/src/typings.ts index 5bc030d62..aa15b4d91 100644 --- a/packages/bot/src/typings.ts +++ b/packages/bot/src/typings.ts @@ -207,6 +207,8 @@ export interface BotGatewayHandlerOptions { ENTITLEMENT_CREATE: typeof handlers.handleEntitlementCreate ENTITLEMENT_UPDATE: typeof handlers.handleEntitlementUpdate ENTITLEMENT_DELETE: typeof handlers.handleEntitlementDelete + MESSAGE_POLL_VOTE_ADD: typeof handlers.handleMessagePollVoteAdd + MESSAGE_POLL_VOTE_REMOVE: typeof handlers.handleMessagePollVoteRemove } export enum MessageFlags { diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 4083fe6de..d56428fb2 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -40,6 +40,7 @@ import { type DiscordMemberWithUser, type DiscordMessage, type DiscordPartialGuild, + type DiscordPollResult, type DiscordPrunedCount, type DiscordRole, type DiscordScheduledEvent, @@ -1352,6 +1353,14 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await rest.post(rest.routes.channels.threads.all(channelId), { body, reason }) }, + async getPollAnswerVoters(channelId, messageId, answerId, options) { + return await rest.get(rest.routes.channels.polls.votes(channelId, messageId, answerId, options)) + }, + + async endPoll(channelId, messageId) { + return await rest.post(rest.routes.channels.polls.expire(channelId, messageId)) + }, + async syncGuildTemplate(guildId) { return await rest.put(rest.routes.guilds.templates.all(guildId)) }, diff --git a/packages/rest/src/routes.ts b/packages/rest/src/routes.ts index 17e04973d..eb942cf1f 100644 --- a/packages/rest/src/routes.ts +++ b/packages/rest/src/routes.ts @@ -218,6 +218,22 @@ export function createRoutes(): RestRoutes { typing: (channelId) => { return `/channels/${channelId}/typing` }, + + polls: { + votes: (channelId, messageId, answerId, options) => { + let url = `/channels/${channelId}/polls/${messageId}/answers/${answerId}?` + + if (options) { + if (options.after) url += `after=${options.after}` + if (options.limit) url += `&limit=${options.limit}` + } + + return url + }, + expire: (channelId, messageId) => { + return `/channels/${channelId}/polls/${messageId}/expire` + }, + }, }, // Guild Endpoints diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index ef4e9ad1f..80e23c240 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -36,6 +36,7 @@ import type { CamelizedDiscordMessage, CamelizedDiscordModifyGuildWelcomeScreen, CamelizedDiscordPartialGuild, + CamelizedDiscordPollResult, CamelizedDiscordPrunedCount, CamelizedDiscordRole, CamelizedDiscordScheduledEvent, @@ -97,6 +98,7 @@ import type { GetGuildPruneCountQuery, GetInvite, GetMessagesOptions, + GetPollAnswerVotes, GetReactions, GetScheduledEventUsers, GetScheduledEvents, @@ -2544,6 +2546,34 @@ export interface RestManager { * @see {@link https://discord.com/developers/docs/resources/channel#start-thread-without-message} */ startThreadWithoutMessage: (channelId: BigString, options: StartThreadWithoutMessage, reason?: string) => Promise + /** + * Get a list of users that voted for this specific answer. + * + * @param channelId - The ID of the channel in which the message with the poll lives + * @param messageId - The ID of the message in which the poll lives + * @param answerId - The ID of the answer to get the users that voted that answer + * @param options - The options for the request + * @returns The list of users that voted for the specific answer. + */ + getPollAnswerVoters: ( + channelId: BigString, + messageId: BigString, + answerId: number, + options?: GetPollAnswerVotes, + ) => Promise + /** + * Immediately ends the poll. + * + * @param channelId - The ID of the channel in which the message with the poll lives + * @param messageId - The ID of the message in which the poll lives + * @returns The message with the expired poll + * + * @remarks + * You cannot end polls from other users. + * + * Fires a _Message Update_ gateway event + */ + endPoll: (channelId: BigString, messageId: BigString) => Promise /** * Synchronises a template with the current state of a guild. * diff --git a/packages/rest/src/typings/routes.ts b/packages/rest/src/typings/routes.ts index df7ae0e03..57679ef5e 100644 --- a/packages/rest/src/typings/routes.ts +++ b/packages/rest/src/typings/routes.ts @@ -6,6 +6,7 @@ import type { GetGuildPruneCountQuery, GetInvite, GetMessagesOptions, + GetPollAnswerVotes, GetReactions, GetScheduledEventUsers, GetUserGuilds, @@ -103,6 +104,10 @@ export interface RestRoutes { /** Route for handling a specific reaction on a message. */ message: (channelId: BigString, messageId: BigString, emoji: string, options?: GetReactions) => string } + polls: { + votes: (channelId: BigString, messageId: BigString, answerId: number, options?: GetPollAnswerVotes) => string + expire: (channelId: BigString, messageId: BigString) => string + } } /** Routes for guild related endpoints. */ guilds: { diff --git a/packages/types/src/camel.ts b/packages/types/src/camel.ts index 4ea655208..46cf91159 100644 --- a/packages/types/src/camel.ts +++ b/packages/types/src/camel.ts @@ -58,6 +58,7 @@ import type { DiscordFollowedChannel, DiscordForumTag, DiscordGatewayPayload, + DiscordGetAnswerVotesResponse, DiscordGetGatewayBot, DiscordGuild, DiscordGuildApplicationCommandPermissions, @@ -118,6 +119,11 @@ import type { DiscordOptionalAuditEntryInfo, DiscordOverwrite, DiscordPartialGuild, + DiscordPoll, + DiscordPollAnswer, + DiscordPollAnswerCount, + DiscordPollMedia, + DiscordPollResult, DiscordPresenceUpdate, DiscordPrunedCount, DiscordReaction, @@ -227,6 +233,12 @@ export interface CamelizedDiscordChannelMention extends Camelize {} export interface CamelizedDiscordMessageActivity extends Camelize {} export interface CamelizedDiscordMessageReference extends Camelize {} +export interface CamelizedDiscordPoll extends Camelize {} +export interface CamelizedDiscordPollMedia extends Camelize {} +export interface CamelizedDiscordPollAnswer extends Camelize {} +export interface CamelizedDiscordPollAnswerCount extends Camelize {} +export interface CamelizedDiscordPollResult extends Camelize {} +export interface CamelizedDiscordGetAnswerVotesResponse extends Camelize {} export interface CamelizedDiscordSticker extends Camelize {} export interface CamelizedDiscordMessageInteraction extends Camelize {} export type CamelizedDiscordMessageComponents = Camelize diff --git a/packages/types/src/discord.ts b/packages/types/src/discord.ts index 4bffbd4ec..a16b5e7ca 100644 --- a/packages/types/src/discord.ts +++ b/packages/types/src/discord.ts @@ -1331,6 +1331,8 @@ export interface DiscordMessage { sticker_items?: DiscordStickerItem[] /** A generally increasing integer (there may be gaps or duplicates) that represents the approximate position of the message in a thread, it can be used to estimate the relative position of the message in a thread in company with `total_message_sent` on parent thread */ position?: number + /** The poll object */ + poll?: DiscordPoll } /** https://discord.com/developers/docs/resources/channel#channel-mention-object */ @@ -1392,6 +1394,122 @@ export interface DiscordMessageReference { fail_if_not_exists: boolean } +/** https://discord.com/developers/docs/resources/poll#poll-object */ +export interface DiscordPoll { + /** The question of the poll. Only `text` is supported. */ + question: DiscordPollMedia + /** Each of the answers available in the poll. There is a maximum of 10 answers per poll. */ + answers: DiscordPollAnswer[] + /** + * The time when the poll ends. + * + * @remarks + * `expiry` is marked as nullable to support non-expiring polls in the future, but all polls have an expiry currently. + */ + expiry: string | null + /** Whether a user can select multiple answers */ + allow_multiselect: boolean + /** The layout type of the poll */ + layout_type: DiscordPollLayoutType + /** + * The results of the poll + * + * @remarks + * This value will not be sent by discord under specific conditions where they don't fetch them on their backend. When this value is missing it should be interpreted as "Unknown results" and not as "No results" + * The results may not be totally accurate while the poll has not ended. When it ends discord will re-calculate all the results and set {@link DiscordPollResult.is_finalized} to true + */ + results?: DiscordPollResult +} + +/** https://discord.com/developers/docs/resources/poll#layout-type */ +export enum DiscordPollLayoutType { + /** The default layout */ + Default = 1, +} + +/** https://discord.com/developers/docs/resources/poll#poll-media-object */ +export interface DiscordPollMedia { + /** + * The text of the field + * + * @remarks + * `text` should always be non-null for both questions and answers, but this is subject to changes. + * The maximum length of `text` is 300 for the question, and 55 for any answer. + */ + text?: string + /** + * The emoji of the field + * + * @remarks + * When creating a poll answer with an emoji, one only needs to send either the `id` (custom emoji) or `name` (default emoji) as the only field. + */ + emoji?: Partial +} + +/** https://discord.com/developers/docs/resources/poll#poll-answer-object */ +export interface DiscordPollAnswer { + /** + * The id of the answer + * + * @remarks + * This id labels each answer. It starts at 1 and goes up sequentially. Discord recommend against depending on this value as is a implementation detail. + */ + answer_id: number + /** The data of the answer */ + poll_media: DiscordPollMedia +} + +export interface DiscordPollAnswerCount { + /** The {@link DiscordPollAnswer.answer_id | answer_id} */ + id: number + /** The number of votes for this answer */ + count: number + /** Whether the current user voted for this answer */ + me_voted: boolean +} + +/** https://discord.com/developers/docs/resources/poll#poll-results-object */ +export interface DiscordPollResult { + /** Whether the votes have been precisely counted */ + is_finalized: boolean + /** The counts for each answer */ + answer_counts: DiscordPollAnswerCount[] +} + +/** https://discord.com/developers/docs/resources/poll#get-answer-voters-response-body */ +export interface DiscordGetAnswerVotesResponse { + /** Users who voted for this answer */ + users: DiscordUser[] +} + +/** https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add */ +export interface DiscordPollVoteAdd { + /** ID of the user. Usually a snowflake */ + user_id: string + /** ID of the channel. Usually a snowflake */ + channel_id: string + /** ID of the message. Usually a snowflake */ + message_id: string + /** ID of the guild. Usually a snowflake */ + guild_id?: string + /** ID of the answer. */ + answer_id: number +} + +/** https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-remove */ +export interface DiscordPollVoteRemove { + /** ID of the user. Usually a snowflake */ + user_id: string + /** ID of the channel. Usually a snowflake */ + channel_id: string + /** ID of the message. Usually a snowflake */ + message_id: string + /** ID of the guild. Usually a snowflake */ + guild_id?: string + /** ID of the answer. */ + answer_id: number +} + /** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-structure */ export interface DiscordSticker { /** [Id of the sticker](https://discord.com/developers/docs/reference#image-formatting) */ diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index 41402ff21..1e8c9bc14 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -12,6 +12,9 @@ import type { DiscordGuildOnboardingPrompt, DiscordInstallParams, DiscordMessageFlag, + DiscordPollAnswer, + DiscordPollLayoutType, + DiscordPollMedia, DiscordRole, } from './discord.js' import type { @@ -79,6 +82,8 @@ export interface CreateMessageOptions { flags?: DiscordMessageFlag /** If true and nonce is present, it will be checked for uniqueness in the past few minutes. If another message was created by the same author with the same nonce, that message will be returned and no new message will be created. */ enforceNonce?: boolean + /** A poll object */ + poll?: CreatePoll } export type MessageComponents = ActionRow[] @@ -742,6 +747,8 @@ export interface ExecuteWebhook { allowedMentions?: AllowedMentions /** the components to include with the message */ components?: MessageComponents + /** A poll object */ + poll?: CreatePoll } export interface GetWebhookMessageOptions { @@ -1313,3 +1320,29 @@ export interface EditApplication { */ tags?: string[] } + +/** https://discord.com/developers/docs/resources/poll#poll-create-request-object */ +export interface CreatePoll { + /** The question of the poll. Only `text` is supported. */ + question: Camelize + /** Each of the answers available in the poll, up to 10 */ + answers: Array, 'answerId'>> + /** Number of hours the poll should be open for, up to 7 days */ + duration: number + /** Whether a user can select multiple answers */ + allowMultiselect: boolean + /** The layout type of the poll */ + layoutType?: DiscordPollLayoutType +} + +/** https://discord.com/developers/docs/resources/poll#get-answer-voters-query-string-params */ +export interface GetPollAnswerVotes { + /** Get users after this user ID */ + after?: BigString + /** + * Max number of users to return (1-100) + * + * @default 25 + */ + limit?: number +} diff --git a/packages/types/src/shared.ts b/packages/types/src/shared.ts index 65c324ca9..9ecf1f73a 100644 --- a/packages/types/src/shared.ts +++ b/packages/types/src/shared.ts @@ -671,6 +671,8 @@ export enum BitwisePermissionFlags { USE_EXTERNAL_SOUNDS = 0x0000200000000000, /** Allows sending voice messages */ SEND_VOICE_MESSAGES = 0x0000400000000000, + /** Allows sending polls */ + SEND_POLLS = 0x0002000000000000, } export type PermissionStrings = keyof typeof BitwisePermissionFlags @@ -799,6 +801,8 @@ export type GatewayDispatchEventNames = | 'ENTITLEMENT_CREATE' | 'ENTITLEMENT_UPDATE' | 'ENTITLEMENT_DELETE' + | 'MESSAGE_POLL_VOTE_ADD' + | 'MESSAGE_POLL_VOTE_REMOVE' export type GatewayEventNames = GatewayDispatchEventNames | 'READY' | 'RESUMED' @@ -935,6 +939,16 @@ export enum GatewayIntents { * - AUTO_MODERATION_ACTION_EXECUTION */ AutoModerationExecution = 1 << 21, + /** + * - MESSAGE_POLL_VOTE_ADD + * - MESSAGE_POLL_VOTE_REMOVE + */ + GuildMessagePolls = 1 << 24, + /** + * - MESSAGE_POLL_VOTE_ADD + * - MESSAGE_POLL_VOTE_REMOVE + */ + DirectMessagePolls = 1 << 25, } /** https://discord.com/developers/docs/topics/gateway#list-of-intents */