From 670efe190ef7e7d37331265eedb5aa3efa7b6002 Mon Sep 17 00:00:00 2001 From: almeidx Date: Thu, 21 Mar 2024 23:12:35 +0000 Subject: [PATCH] feat: polls --- packages/core/src/api/index.ts | 7 +- packages/core/src/api/poll.ts | 49 ++++++++ packages/core/src/client.ts | 4 + .../src/client/actions/ActionsManager.js | 2 + .../src/client/actions/MessagePollVoteAdd.js | 34 ++++++ .../client/actions/MessagePollVoteRemove.js | 34 ++++++ .../handlers/MESSAGE_POLL_VOTE_ADD.js | 5 + .../handlers/MESSAGE_POLL_VOTE_REMOVE.js | 5 + .../src/client/websocket/handlers/index.js | 2 + packages/discord.js/src/errors/ErrorCodes.js | 4 + packages/discord.js/src/errors/Messages.js | 2 + packages/discord.js/src/index.js | 3 + packages/discord.js/src/structures/Message.js | 11 ++ packages/discord.js/src/structures/Poll.js | 113 ++++++++++++++++++ .../discord.js/src/structures/PollAnswer.js | 76 ++++++++++++ .../src/structures/PollAnswerResult.js | 26 ++++ packages/discord.js/src/util/APITypes.js | 5 + packages/discord.js/src/util/Events.js | 4 + packages/discord.js/test/polls.js | 52 ++++++++ packages/discord.js/typings/index.d.ts | 43 +++++++ 20 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/api/poll.ts create mode 100644 packages/discord.js/src/client/actions/MessagePollVoteAdd.js create mode 100644 packages/discord.js/src/client/actions/MessagePollVoteRemove.js create mode 100644 packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_ADD.js create mode 100644 packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_REMOVE.js create mode 100644 packages/discord.js/src/structures/Poll.js create mode 100644 packages/discord.js/src/structures/PollAnswer.js create mode 100644 packages/discord.js/src/structures/PollAnswerResult.js create mode 100644 packages/discord.js/test/polls.js diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 7054d8625d94b..98dec7eb4f051 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -7,6 +7,7 @@ import { InteractionsAPI } from './interactions.js'; import { InvitesAPI } from './invite.js'; import { MonetizationAPI } from './monetization.js'; import { OAuth2API } from './oauth2.js'; +import { PollAPI } from "./poll.js"; import { RoleConnectionsAPI } from './roleConnections.js'; import { StageInstancesAPI } from './stageInstances.js'; import { StickersAPI } from './sticker.js'; @@ -23,6 +24,7 @@ export * from './interactions.js'; export * from './invite.js'; export * from './monetization.js'; export * from './oauth2.js'; +export * from './poll.js'; export * from './roleConnections.js'; export * from './stageInstances.js'; export * from './sticker.js'; @@ -48,6 +50,8 @@ export class API { public readonly oauth2: OAuth2API; + public readonly poll: PollAPI; + public readonly roleConnections: RoleConnectionsAPI; public readonly stageInstances: StageInstancesAPI; @@ -69,8 +73,9 @@ export class API { this.guilds = new GuildsAPI(rest); this.invites = new InvitesAPI(rest); this.monetization = new MonetizationAPI(rest); - this.roleConnections = new RoleConnectionsAPI(rest); this.oauth2 = new OAuth2API(rest); + this.poll = new PollAPI(rest); + this.roleConnections = new RoleConnectionsAPI(rest); this.stageInstances = new StageInstancesAPI(rest); this.stickers = new StickersAPI(rest); this.threads = new ThreadsAPI(rest); diff --git a/packages/core/src/api/poll.ts b/packages/core/src/api/poll.ts new file mode 100644 index 0000000000000..52d02dbf12cc2 --- /dev/null +++ b/packages/core/src/api/poll.ts @@ -0,0 +1,49 @@ +/* eslint-disable jsdoc/check-param-names */ + +import { makeURLSearchParams, type RequestData, type REST } from '@discordjs/rest'; +import type { Snowflake } from 'discord-api-types/v10'; + +type RESTGetAPIPollAnswerVotersQuery = any; +type RESTGetAPIPollAnswerVotersResult = any; +type RESTPostAPIPollExpireResult = any; + +export class PollAPI { + public constructor(private readonly rest: REST) {} + + /** + * Gets the list of users that voted for a specific answer in a poll + * + * @see {@link https://discord.com/developers/docs/resources/poll#get-answer-voters} + * @param channelId - The id of the channel containing the message + * @param messageId - The id of the message containing the poll + * @param answerId - The id of the answer to get voters for + * @param query - The query for getting the list of voters + * @param options - The options for getting the list of voters + */ + public async getAnswerVoters( + channelId: Snowflake, + messageId: Snowflake, + answerId: number, + query: RESTGetAPIPollAnswerVotersQuery, + { signal }: Pick = {}, + ) { + return this.rest.get(`/channels/${channelId}/polls/${messageId}/answers/${answerId}`, { + signal, + query: makeURLSearchParams(query), + }) as Promise; + } + + /** + * Immediately expires (i.e. ends) a poll + * + * @see {@link https://discord.com/developers/docs/resources/poll#expire-poll} + * @param channelId - The id of the channel containing the message + * @param messageId - The id of the message containing the poll + * @param options - The options for expiring the poll + */ + public async expirePoll(channelId: Snowflake, messageId: Snowflake, { signal }: Pick = {}) { + return this.rest.post(`/channels/${channelId}/polls/${messageId}/expire`, { + signal, + }) as Promise; + } +} diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index f39fa434044ec..0ab7003dabaf7 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -92,6 +92,8 @@ export interface WithIntrinsicProps extends IntrinsicProps { data: Data; } +type GatewayMessagePollVoteData = any; + export interface MappedEvents { [GatewayDispatchEvents.ApplicationCommandPermissionsUpdate]: [ WithIntrinsicProps, @@ -143,6 +145,8 @@ export interface MappedEvents { [GatewayDispatchEvents.MessageCreate]: [WithIntrinsicProps]; [GatewayDispatchEvents.MessageDelete]: [WithIntrinsicProps]; [GatewayDispatchEvents.MessageDeleteBulk]: [WithIntrinsicProps]; + ['MESSAGE_POLL_VOTE_ADD']: [WithIntrinsicProps]; + ['MESSAGE_POLL_VOTE_REMOVE']: [WithIntrinsicProps]; [GatewayDispatchEvents.MessageReactionAdd]: [WithIntrinsicProps]; [GatewayDispatchEvents.MessageReactionRemove]: [WithIntrinsicProps]; [GatewayDispatchEvents.MessageReactionRemoveAll]: [WithIntrinsicProps]; diff --git a/packages/discord.js/src/client/actions/ActionsManager.js b/packages/discord.js/src/client/actions/ActionsManager.js index 7b903f907ba61..dd305a94804a4 100644 --- a/packages/discord.js/src/client/actions/ActionsManager.js +++ b/packages/discord.js/src/client/actions/ActionsManager.js @@ -54,6 +54,8 @@ class ActionsManager { this.register(require('./MessageCreate')); this.register(require('./MessageDelete')); this.register(require('./MessageDeleteBulk')); + this.register(require('./MessagePollVoteAdd')); + this.register(require('./MessagePollVoteRemove')); this.register(require('./MessageReactionAdd')); this.register(require('./MessageReactionRemove')); this.register(require('./MessageReactionRemoveAll')); diff --git a/packages/discord.js/src/client/actions/MessagePollVoteAdd.js b/packages/discord.js/src/client/actions/MessagePollVoteAdd.js new file mode 100644 index 0000000000000..c7361123aac74 --- /dev/null +++ b/packages/discord.js/src/client/actions/MessagePollVoteAdd.js @@ -0,0 +1,34 @@ +'use strict'; + +const Action = require('./Action'); +const Events = require('../../util/Events'); + +class MessagePollVoteAddAction extends Action { + handle(data) { + const channel = this.getChannel(data); + if (!channel?.isTextBased()) return false; + + const message = this.getMessage(data, channel); + if (!message) return false; + + const { poll } = message; + + const answer = poll.answers.get(data.answer_id); + if (!answer) { + console.log('???'); + return false; + } + + /** + * Emitted whenever a user votes in a poll. + * @event Client#messagePollVoteAdd + * @param {PollAnswer} pollAnswer The answer that was voted on + * @param {Snowflake} userId The id of the user that voted + */ + this.client.emit(Events.MessagePollVoteAdd, answer, data.user_id); + + return { poll }; + } +} + +module.exports = MessagePollVoteAddAction; diff --git a/packages/discord.js/src/client/actions/MessagePollVoteRemove.js b/packages/discord.js/src/client/actions/MessagePollVoteRemove.js new file mode 100644 index 0000000000000..b42a45d9aa19d --- /dev/null +++ b/packages/discord.js/src/client/actions/MessagePollVoteRemove.js @@ -0,0 +1,34 @@ +'use strict'; + +const Action = require('./Action'); +const Events = require('../../util/Events'); + +class MessagePollVoteRemoveAction extends Action { + handle(data) { + const channel = this.getChannel(data); + if (!channel?.isTextBased()) return false; + + const message = this.getMessage(data, channel); + if (!message) return false; + + const { poll } = message; + + const answer = poll.answers.get(data.answer_id); + if (!answer) { + console.log('???'); + return false; + } + + /** + * Emitted whenever a user removes their vote in a poll. + * @event Client#messagePollVoteRemove + * @param {PollAnswer} pollAnswer The answer where the vote was removed + * @param {Snowflake} userId The id of the user that removed their vote + */ + this.client.emit(Events.MessagePollVoteRemove, answer, data.user_id); + + return { poll }; + } +} + +module.exports = MessagePollVoteRemoveAction; diff --git a/packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_ADD.js b/packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_ADD.js new file mode 100644 index 0000000000000..807597bbff7f4 --- /dev/null +++ b/packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_ADD.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessagePollVoteAdd.handle(packet.d); +}; diff --git a/packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_REMOVE.js b/packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_REMOVE.js new file mode 100644 index 0000000000000..3dee4843277b0 --- /dev/null +++ b/packages/discord.js/src/client/websocket/handlers/MESSAGE_POLL_VOTE_REMOVE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessagePollVoteRemove.handle(packet.d); +}; diff --git a/packages/discord.js/src/client/websocket/handlers/index.js b/packages/discord.js/src/client/websocket/handlers/index.js index 0ed4b4065a0cc..e18b0196e1ec4 100644 --- a/packages/discord.js/src/client/websocket/handlers/index.js +++ b/packages/discord.js/src/client/websocket/handlers/index.js @@ -40,6 +40,8 @@ const handlers = Object.fromEntries([ ['MESSAGE_CREATE', require('./MESSAGE_CREATE')], ['MESSAGE_DELETE', require('./MESSAGE_DELETE')], ['MESSAGE_DELETE_BULK', require('./MESSAGE_DELETE_BULK')], + ['MESSAGE_POLL_VOTE_ADD', require('./MESSAGE_POLL_VOTE_ADD')], + ['MESSAGE_POLL_VOTE_REMOVE', require('./MESSAGE_POLL_VOTE_REMOVE')], ['MESSAGE_REACTION_ADD', require('./MESSAGE_REACTION_ADD')], ['MESSAGE_REACTION_REMOVE', require('./MESSAGE_REACTION_REMOVE')], ['MESSAGE_REACTION_REMOVE_ALL', require('./MESSAGE_REACTION_REMOVE_ALL')], diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index 5640b1165e243..0b89adf67fc50 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -176,6 +176,8 @@ * @property {'SweepFilterReturn'} SweepFilterReturn * @property {'EntitlementCreateInvalidOwner'} EntitlementCreateInvalidOwner + + * @property {'PollAlreadyExpired'} PollAlreadyExpired */ const keys = [ @@ -329,6 +331,8 @@ const keys = [ 'GuildForumMessageRequired', 'EntitlementCreateInvalidOwner', + + 'PollAlreadyExpired', ]; // JSDoc for IntelliSense purposes diff --git a/packages/discord.js/src/errors/Messages.js b/packages/discord.js/src/errors/Messages.js index b3c0514dfa61d..a71b81bccea21 100644 --- a/packages/discord.js/src/errors/Messages.js +++ b/packages/discord.js/src/errors/Messages.js @@ -169,6 +169,8 @@ const Messages = { [DjsErrorCodes.EntitlementCreateInvalidOwner]: 'You must provide either a guild or a user to create an entitlement, but not both', + + [DjsErrorCodes.PollAlreadyExpired]: 'This poll has already expired.', }; module.exports = Messages; diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 8e927d16250a6..f15cfb94563b8 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -168,6 +168,9 @@ exports.NewsChannel = require('./structures/NewsChannel'); exports.OAuth2Guild = require('./structures/OAuth2Guild'); exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel'); exports.PermissionOverwrites = require('./structures/PermissionOverwrites'); +exports.Poll = require('./structures/Poll').Poll; +exports.PollAnswer = require('./structures/PollAnswer').PollAnswer; +exports.PollAnswerResult = require('./structures/PollAnswerResult').PollAnswerResult; exports.Presence = require('./structures/Presence').Presence; exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionEmoji = require('./structures/ReactionEmoji'); diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 86e898a8d56f0..2b024e29a9c78 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -17,6 +17,7 @@ const Embed = require('./Embed'); const InteractionCollector = require('./InteractionCollector'); const Mentions = require('./MessageMentions'); const MessagePayload = require('./MessagePayload'); +const { Poll } = require('./Poll.js'); const ReactionCollector = require('./ReactionCollector'); const { Sticker } = require('./Sticker'); const { DiscordjsError, ErrorCodes } = require('../errors'); @@ -406,6 +407,16 @@ class Message extends Base { } else { this.interaction ??= null; } + + if (data.poll) { + /** + * The poll that was sent with the message + * @type {?Poll} + */ + this.poll = new Poll(this.client, data.poll, this); + } else { + this.poll ??= null; + } } /** diff --git a/packages/discord.js/src/structures/Poll.js b/packages/discord.js/src/structures/Poll.js new file mode 100644 index 0000000000000..1592d15e867e0 --- /dev/null +++ b/packages/discord.js/src/structures/Poll.js @@ -0,0 +1,113 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); +const { PollAnswer } = require('./PollAnswer'); +const { PollAnswerResult } = require('./PollAnswerResult'); +const { DiscordjsError } = require('../errors/DJSError'); +const { ErrorCodes } = require('../errors/index'); + +/** + * Represents a Poll + * @extends {Base} + */ +class Poll extends Base { + constructor(client, data, message) { + super(client); + + /** + * The message that started this poll + * @name Poll#message + * @type {Message} + * @readonly + */ + + Object.defineProperty(this, 'message', { value: message }); + + this._patch(data); + } + + _patch(data) { + /** + * The question text of this poll + * @type {string} + */ + this.question = data.question.text; + + /** + * The answers of this poll + * @type {Collection} + */ + this.answers = data.answers.reduce( + (acc, answer) => acc.set(answer.answer_id, new PollAnswer(this.client, answer, this)), + new Collection(), + ); + + /** + * The timestamp when this poll expires + * @type {number} + */ + this.expiresTimestamp = Date.parse(data.expiry); + + /** + * Whether this poll allows multiple answers + * @type {boolean} + */ + this.allowMultiselect = data.allow_multiselect; + + /** + * The layout type of this poll + * @type {PollLayoutType} + */ + this.layoutType = data.layout_type; + + if (data.results) { + /** + * @typedef {Object} PollResults + * @property {Collection} answerCounts The counts of each answer + * @property {boolean} isFinalized Whether the results are finalized + */ + + /** + * The results of this poll + * @type {?PollResults} + */ + this.results = { + answerCounts: data.results.answer_counts.reduce( + (acc, result) => acc.set(result.id, new PollAnswerResult(result)), + new Collection(), + ), + isFinalized: data.results.is_finalized, + }; + } else { + this.results = null; + } + } + + /** + * The date when this poll expires + * @type {Date} + * @readonly + */ + get expiresAt() { + return new Date(this.expiresTimestamp); + } + + /** + * End this poll + * @returns {Promise} + */ + async end() { + if (Date.now() > this.expiresTimestamp) { + throw new DiscordjsError(ErrorCodes.PollAlreadyExpired); + } + + const message = await this.client.rest.post(`/channels/${this.message.channel.id}/poll/${this.message.id}/expire`); + + this.message._patch(message); + + return this.message; + } +} + +exports.Poll = Poll; diff --git a/packages/discord.js/src/structures/PollAnswer.js b/packages/discord.js/src/structures/PollAnswer.js new file mode 100644 index 0000000000000..df1ed583c0920 --- /dev/null +++ b/packages/discord.js/src/structures/PollAnswer.js @@ -0,0 +1,76 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const Base = require('./Base'); +const { Emoji } = require('./Emoji'); + +/** + * Represents an answer to a {@link Poll} + * @extends {Base} + */ +class PollAnswer extends Base { + constructor(client, data, poll) { + super(client); + + /** + * The {@link Poll} this answer is part of + * @name PollAnswer#poll + * @type {Poll} + * @readonly + */ + Object.defineProperty(this, 'poll', { value: poll }); + + /** + * The id of this answer + * @type {number} + */ + this.id = data.answer_id; + + /** + * The text of this answer + * @type {?string} + */ + this.text = data.poll_media.text ?? null; + + /** + * The raw emoji of this answer + * @name PollAnswer#_emoji + * @type {?APIPartialEmoji} + * @private + */ + Object.defineProperty(this, '_emoji', { value: data.poll_media.emoji ?? null }); + + this._patch(data); + } + + /** + * The emoji of this answer + * @type {?(GuildEmoji|Emoji)} + */ + get emoji() { + if (!this._emoji || (!this._emoji.id && !this._emoji.name)) return null; + return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji); + } + + /** + * @typedef {Object} FetchPollVotersOptions + * @property {number} [limit] The maximum number of voters to fetch + * @property {Snowflake} [after] The user id to fetch voters after + */ + + /** + * Fetches the users that voted for this answer + * @param {FetchPollVotersOptions} [options={}] The options for fetching voters + * @returns {Promise>} + */ + async fetchVoters({ after, limit } = {}) { + const voters = await this.client.rest.get( + `/channels/${this.poll.message.channel.id}/polls/${this.poll.message.id}/answers/${this.id}`, + { query: makeURLSearchParams({ limit, after }) }, + ); + return voters.users.reduce((acc, user) => acc.set(user.id, this.client.users._add(user, false)), new Collection()); + } +} + +exports.PollAnswer = PollAnswer; diff --git a/packages/discord.js/src/structures/PollAnswerResult.js b/packages/discord.js/src/structures/PollAnswerResult.js new file mode 100644 index 0000000000000..b136fd35e70df --- /dev/null +++ b/packages/discord.js/src/structures/PollAnswerResult.js @@ -0,0 +1,26 @@ +'use strict'; + +/** + * Represents the results of a {@link PollAnswer} + */ +class PollAnswerResult { + constructor(data) { + /** + * The id of the {@link PollAnswer} this result is for + * @type {number} + */ + this.id = data.id; + + this._patch(data); + } + + _patch(data) { + /** + * The count of votes for this answer + * @type {number} + */ + this.count = data.count; + } +} + +exports.PollAnswerResult = PollAnswerResult; diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 344092010f723..176e59046420e 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -450,6 +450,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-payloads/common#PermissionFlagsBits} */ +/** + * @external PollLayoutType + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/PollLayoutType} + */ + /** * @external RoleFlags * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/RoleFlags} diff --git a/packages/discord.js/src/util/Events.js b/packages/discord.js/src/util/Events.js index 416270add89f3..a8ee63f66a56c 100644 --- a/packages/discord.js/src/util/Events.js +++ b/packages/discord.js/src/util/Events.js @@ -53,6 +53,8 @@ * @property {string} MessageBulkDelete messageDeleteBulk * @property {string} MessageCreate messageCreate * @property {string} MessageDelete messageDelete + * @property {string} MessagePollVoteAdd messagePollVoteAdd + * @property {string} MessagePollVoteRemove messagePollVoteRemove * @property {string} MessageReactionAdd messageReactionAdd * @property {string} MessageReactionRemove messageReactionRemove * @property {string} MessageReactionRemoveAll messageReactionRemoveAll @@ -138,6 +140,8 @@ module.exports = { MessageBulkDelete: 'messageDeleteBulk', MessageCreate: 'messageCreate', MessageDelete: 'messageDelete', + MessagePollVoteAdd: 'messagePollVoteAdd', + MessagePollVoteRemove: 'messagePollVoteRemove', MessageReactionAdd: 'messageReactionAdd', MessageReactionRemove: 'messageReactionRemove', MessageReactionRemoveAll: 'messageReactionRemoveAll', diff --git a/packages/discord.js/test/polls.js b/packages/discord.js/test/polls.js new file mode 100644 index 0000000000000..96180832eba6a --- /dev/null +++ b/packages/discord.js/test/polls.js @@ -0,0 +1,52 @@ +'use strict'; + +const { token, owner } = require('./auth.js'); +const { Client, Events, codeBlock, GatewayIntentBits } = require('../src'); + +const client = new Client({ + intents: + GatewayIntentBits.Guilds | + GatewayIntentBits.GuildMessages | + GatewayIntentBits.GuildMessageReactions | + GatewayIntentBits.MessageContent, +}); + +client.on('raw', console.log); + +client.on(Events.ClientReady, async () => { + const channel = client.channels.cache.get('545018353958256662'); + const message = await channel.messages.fetch('1220489796825382973'); + + console.dir(message.poll, { depth: Infinity }); + + const answer = message.poll.answers.first(); + const voters = await answer.fetchVoters(); + console.dir(voters); +}); + +client.on(Events.MessagePollVoteAdd, (answer, userId) => { + console.log(`User ${userId} voted for answer ${answer.id}`); +}); + +client.on(Events.MessagePollVoteRemove, (answer, userId) => { + console.log(`User ${userId} removed their vote for answer ${answer.id}`); +}); + +client.on(Events.MessageCreate, async message => { + const prefix = `<@${client.user.id}> `; + + if (message.author.id !== owner || !message.content.startsWith(prefix)) return; + let res; + try { + res = await eval(message.content.slice(prefix.length)); + if (typeof res !== 'string') res = require('node:util').inspect(res); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err.stack); + res = err.message; + } + + await message.channel.send(codeBlock('js', res)); +}); + +client.login(token); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index aa96d1a9136b1..f6fb04f6a8d56 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2586,6 +2586,45 @@ export class Presence extends Base { public equals(presence: Presence): boolean; } +export interface PollResults { + answerCounts: ReadonlyCollection; + isFinalized: boolean; +} + +export class Poll extends Base { + private constructor(client: Client, data: any, message: Message); + public readonly message: Message; + public question: string; + public answers: Collection; + public expiresTimestamp: number; + public get expiresAt(): Date; + public allowMultiselect: boolean; + public layoutType: any; + public results: PollResults; + public end(): Promise; +} + +export interface FetchPollVotersOptions { + after?: Snowflake; + limit?: number; +} + +export class PollAnswer extends Base { + private constructor(client: Client, data: any, poll: Poll); + private _emoji: APIPartialEmoji | null; + public readonly poll: Poll; + public id: number; + public text: string | null; + public get emoji(): GuildEmoji | Emoji | null; + public fetchVoters(options?: FetchPollVotersOptions): Promise>; +} + +export class PollAnswerResult { + private constructor(data: any); + public id: number; + public count: number; +} + export class ReactionCollector extends Collector { public constructor(message: Message, options?: ReactionCollectorOptions); private _handleChannelDeletion(channel: NonThreadGuildBasedChannel): void; @@ -5139,6 +5178,8 @@ export interface ClientEvents { inviteDelete: [invite: Invite]; messageCreate: [message: Message]; messageDelete: [message: Message | PartialMessage]; + messagePollVoteAdd: [pollAnswer: PollAnswer, userId: Snowflake]; + messagePollVoteRemove: [pollAnswer: PollAnswer, userId: Snowflake]; messageReactionRemoveAll: [ message: Message | PartialMessage, reactions: ReadonlyCollection, @@ -5364,6 +5405,8 @@ export enum Events { MessageDelete = 'messageDelete', MessageUpdate = 'messageUpdate', MessageBulkDelete = 'messageDeleteBulk', + MessagePollVoteAdd = 'messagePollVoteAdd', + MessagePollVoteRemove = 'messagePollVoteRemove', MessageReactionAdd = 'messageReactionAdd', MessageReactionRemove = 'messageReactionRemove', MessageReactionRemoveAll = 'messageReactionRemoveAll',