feat: polls
Added support for polls similar to discord.js v14 (including class, event). Breaking change: Do not use and remove MessagePoll.
This commit is contained in:
@@ -50,6 +50,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'));
|
||||
|
||||
33
src/client/actions/MessagePollVoteAdd.js
Normal file
33
src/client/actions/MessagePollVoteAdd.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class MessagePollVoteAddAction extends Action {
|
||||
handle(data) {
|
||||
const channel = this.getChannel(data);
|
||||
if (!channel?.isText()) 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) return false;
|
||||
|
||||
answer.voteCount++;
|
||||
|
||||
/**
|
||||
* 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.MESSAGE_POLL_VOTE_ADD, answer, data.user_id);
|
||||
|
||||
return { poll };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessagePollVoteAddAction;
|
||||
33
src/client/actions/MessagePollVoteRemove.js
Normal file
33
src/client/actions/MessagePollVoteRemove.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
|
||||
class MessagePollVoteRemoveAction extends Action {
|
||||
handle(data) {
|
||||
const channel = this.getChannel(data);
|
||||
if (!channel?.isText()) 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) return false;
|
||||
|
||||
answer.voteCount--;
|
||||
|
||||
/**
|
||||
* 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.MESSAGE_POLL_VOTE_REMOVE, answer, data.user_id);
|
||||
|
||||
return { poll };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessagePollVoteRemoveAction;
|
||||
@@ -1,22 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
/**
|
||||
* Poll Vote Structure
|
||||
* @see {@link https://docs.discord.sex/resources/message#poll-results-structure}
|
||||
* @typedef {Object} MessagePollUserVote
|
||||
* @property {Snowflake} user_id ID of the user
|
||||
* @property {Snowflake} channel_id ID of the channel
|
||||
* @property {Snowflake} message_id ID of the message
|
||||
* @property {?Snowflake} guild_id ID of the guild
|
||||
* @property {number} answer_id ID of the answer
|
||||
*/
|
||||
/**
|
||||
* Emitted when a user votes on a poll. If the poll allows multiple selection, one event will be sent per answer.
|
||||
* @event Client#messagePollVoteAdd
|
||||
* @param {MessagePollUserVote} data Raw data
|
||||
*/
|
||||
client.emit(Events.MESSAGE_POLL_VOTE_ADD, data);
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessagePollVoteAdd.handle(packet.d);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { Events } = require('../../../util/Constants');
|
||||
|
||||
module.exports = (client, { d: data }) => {
|
||||
/**
|
||||
* Emitted when a user removes their vote on a poll. If the poll allows for multiple selections, one event will be sent per answer.
|
||||
* @event Client#messagePollVoteRemove
|
||||
* @param {MessagePollUserVote} data Raw data
|
||||
*/
|
||||
client.emit(Events.MESSAGE_POLL_VOTE_REMOVE, data);
|
||||
module.exports = (client, packet) => {
|
||||
client.actions.MessagePollVoteRemove.handle(packet.d);
|
||||
};
|
||||
|
||||
@@ -208,6 +208,7 @@ const Messages = {
|
||||
STREAM_CONNECTION_READONLY: 'Cannot send data to a read-only stream',
|
||||
STREAM_CANNOT_JOIN: 'Cannot join a stream to itself',
|
||||
VOICE_USER_NOT_STREAMING: 'User is not streaming',
|
||||
POLL_ALREADY_EXPIRED: 'This poll has already expired.',
|
||||
};
|
||||
|
||||
for (const [name, message] of Object.entries(Messages)) register(name, message);
|
||||
|
||||
@@ -163,4 +163,5 @@ exports.SpotifyRPC = require('./structures/Presence').SpotifyRPC;
|
||||
exports.WebEmbed = require('./structures/WebEmbed');
|
||||
exports.DiscordAuthWebsocket = require('./util/RemoteAuth');
|
||||
exports.PurchasedFlags = require('./util/PurchasedFlags');
|
||||
exports.MessagePoll = require('./structures/MessagePoll');
|
||||
exports.Poll = require('./structures/Poll').Poll;
|
||||
exports.PollAnswer = require('./structures/PollAnswer').PollAnswer;
|
||||
|
||||
@@ -8,7 +8,7 @@ const MessageAttachment = require('./MessageAttachment');
|
||||
const Embed = require('./MessageEmbed');
|
||||
const Mentions = require('./MessageMentions');
|
||||
const MessagePayload = require('./MessagePayload');
|
||||
const MessagePoll = require('./MessagePoll');
|
||||
const { Poll } = require('./Poll');
|
||||
const ReactionCollector = require('./ReactionCollector');
|
||||
const { Sticker } = require('./Sticker');
|
||||
const Application = require('./interfaces/Application');
|
||||
@@ -257,14 +257,14 @@ class Message extends Base {
|
||||
this.webhookId ??= null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The poll that was sent with the message
|
||||
* @type {?MessagePoll}
|
||||
*/
|
||||
if ('poll' in data) {
|
||||
this.poll = new MessagePoll(data.poll, this.client);
|
||||
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;
|
||||
this.poll ??= null;
|
||||
}
|
||||
|
||||
if ('application' in data) {
|
||||
@@ -941,32 +941,6 @@ class Message extends Base {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately ends the poll. You cannot end polls from other users.
|
||||
* @returns {Promise<this>}
|
||||
* @deprecated Using MessageManager#endPoll(messageId) instead
|
||||
*/
|
||||
endPoll() {
|
||||
return this.channel.messages.endPoll(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of users that voted for this specific answer.
|
||||
* @param {number} answerId Answer Id
|
||||
* @param {Snowflake} [afterUserId] Get users after this user ID
|
||||
* @param {number} [limit=25] Max number of users to return (1-100, default 25)
|
||||
* @returns {Promise<Collection<Snowflake, User>>}
|
||||
* @deprecated Using MessageManager#fetchPollAnswerVoters({ messageId, answerId, after, limit }) instead
|
||||
*/
|
||||
getAnswerVoter(answerId, afterUserId, limit = 25) {
|
||||
return this.channel.messages.fetchPollAnswerVoters({
|
||||
messageId: this.id,
|
||||
answerId,
|
||||
after: afterUserId,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch this message.
|
||||
* @param {boolean} [force=true] Whether to skip the cache check and request the API
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
const { Buffer } = require('node:buffer');
|
||||
const BaseMessageComponent = require('./BaseMessageComponent');
|
||||
const MessageEmbed = require('./MessageEmbed');
|
||||
const MessagePoll = require('./MessagePoll');
|
||||
const { RangeError } = require('../errors');
|
||||
const ActivityFlags = require('../util/ActivityFlags');
|
||||
const { PollLayoutTypes } = require('../util/Constants');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const MessageFlags = require('../util/MessageFlags');
|
||||
const Util = require('../util/Util');
|
||||
@@ -220,6 +220,24 @@ class MessagePayload {
|
||||
};
|
||||
}
|
||||
|
||||
let poll;
|
||||
if (this.options.poll) {
|
||||
poll = {
|
||||
question: {
|
||||
text: this.options.poll.question.text,
|
||||
},
|
||||
answers: this.options.poll.answers.map(answer => ({
|
||||
poll_media: { text: answer.text, emoji: Util.resolvePartialEmoji(answer.emoji) },
|
||||
})),
|
||||
duration: this.options.poll.duration,
|
||||
allow_multiselect: this.options.poll.allowMultiselect,
|
||||
layout_type:
|
||||
typeof this.options.poll.layoutType == 'number'
|
||||
? this.options.poll.layoutType
|
||||
: PollLayoutTypes[this.options.poll.layoutType],
|
||||
};
|
||||
}
|
||||
|
||||
this.data = {
|
||||
activity,
|
||||
content,
|
||||
@@ -237,7 +255,7 @@ class MessagePayload {
|
||||
sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
|
||||
thread_name: threadName,
|
||||
applied_tags: appliedTags,
|
||||
poll: this.options.poll instanceof MessagePoll ? this.options.poll.toJSON() : this.options.poll,
|
||||
poll,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { MessagePollLayoutTypes } = require('../util/Constants');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* Represents the poll object has a lot of levels and nested structures. It was also designed to support future extensibility, so some fields may appear to be more complex than necessary.
|
||||
*/
|
||||
class MessagePoll {
|
||||
/**
|
||||
* @param {Object} data Message poll to clone or raw data
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data = {}) {
|
||||
if (data?.constructor?.name == 'MessagePoll') data = data.toJSON();
|
||||
/**
|
||||
* The poll media object is a common object that backs both the question and answers. For now, `question` only supports `text`, while `answers` can have an optional `emoji`.
|
||||
* @see {@link https://docs.discord.sex/resources/message#poll-media-structure}
|
||||
* @typedef {Object} MessagePollMedia
|
||||
* @property {?string} text The text of the field (max 300 characters for question, 55 characters for answer)
|
||||
* @property {?RawEmoji} emoji The emoji of the field
|
||||
*/
|
||||
|
||||
if ('question' in data) {
|
||||
/**
|
||||
* The question of the poll
|
||||
* @type {?MessagePollMedia}
|
||||
*/
|
||||
this.question = this._resolvePollMedia(data.question);
|
||||
} else {
|
||||
this.question ??= null;
|
||||
}
|
||||
|
||||
if (data.answers?.length) {
|
||||
/**
|
||||
* The answers available in the poll
|
||||
* @type {Collection<number, MessagePollMedia>}
|
||||
*/
|
||||
this.answers = new Collection();
|
||||
|
||||
data.answers.forEach((obj, index) => {
|
||||
this.answers.set(obj?.answer_id || index + 1, this._resolvePollMedia(obj.poll_media));
|
||||
});
|
||||
} else {
|
||||
this.answers ??= new Collection();
|
||||
}
|
||||
|
||||
if ('layout_type' in data) {
|
||||
/**
|
||||
* The layout type of the poll
|
||||
* @type {?MessagePollLayoutTypes}
|
||||
*/
|
||||
this.layoutType = MessagePollLayoutTypes[data.layout_type];
|
||||
} else {
|
||||
this.layoutType ??= MessagePollLayoutTypes[1]; // Default type
|
||||
}
|
||||
|
||||
if ('allow_multiselect' in data) {
|
||||
/**
|
||||
* Whether a user can select multiple answers
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.allowMultiSelect = !!data.allow_multiselect;
|
||||
} else {
|
||||
this.allowMultiSelect ??= false;
|
||||
}
|
||||
|
||||
if ('expiry' in data) {
|
||||
/**
|
||||
* When the poll ends
|
||||
* @type {?Date}
|
||||
*/
|
||||
this.expiry = new Date(data.expiry);
|
||||
} else {
|
||||
this.expiry ??= null;
|
||||
}
|
||||
|
||||
if ('duration' in data) {
|
||||
/**
|
||||
* Number of hours the poll should be open for (max 32 days, default 1)
|
||||
* @type {?Number}
|
||||
*/
|
||||
this.duration = data.duration;
|
||||
} else {
|
||||
this.duration ??= null;
|
||||
}
|
||||
|
||||
if ('results' in data) {
|
||||
/**
|
||||
* Poll Results Structure
|
||||
* @see {@link https://docs.discord.sex/resources/message#poll-results-structure}
|
||||
* @typedef {Object} MessagePollResult
|
||||
* @property {boolean} isFinalized Whether the votes have been precisely counted
|
||||
* @property {Collection<number, MessagePollResultAnswerCount>} answerCounts The counts for each answer
|
||||
*/
|
||||
/**
|
||||
* Poll Answer Count Structure
|
||||
* @see {@link https://docs.discord.sex/resources/message#poll-answer-count-structure}
|
||||
* @typedef {Object} MessagePollResultAnswerCount
|
||||
* @property {MessagePollMedia} answer answer
|
||||
* @property {number} count The number of votes for this answer
|
||||
* @property {boolean} selfVoted Whether the current user voted for this answer
|
||||
*/
|
||||
/**
|
||||
* In a nutshell, this contains the number of votes for each answer.
|
||||
* The `results` field may be not present in certain responses where, as an implementation detail,
|
||||
* Discord does not fetch the poll results in the backend.
|
||||
* This should be treated as "unknown results", as opposed to "no results".
|
||||
* You can keep using the results if you have previously received them through other means.
|
||||
* Due to the intricacies of counting at scale, while a poll is in progress the results may not
|
||||
* be perfectly accurate. They usually are accurate, and shouldn't deviate significantly — it's
|
||||
* just difficult to make guarantees. To compensate for this, after a poll is finished there is
|
||||
* a background job which performs a final, accurate tally of votes. This tally concludes once
|
||||
* `is_finalized` is `true`. Polls that have ended will also always contain results.
|
||||
* If `answer_counts` does not contain an entry for a particular answer, then there are no votes
|
||||
* for that answer.
|
||||
* @type {?MessagePollResult}
|
||||
*/
|
||||
this.results = {
|
||||
isFinalized: data.results.is_finalized,
|
||||
answerCounts: new Collection(),
|
||||
};
|
||||
data.results.answer_counts.forEach(obj => {
|
||||
this.results.answerCounts.set(obj.id, {
|
||||
count: obj.count,
|
||||
selfVoted: obj.me_voted,
|
||||
answer: this.answers.get(obj.id),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.results ??= null;
|
||||
}
|
||||
}
|
||||
|
||||
_resolvePollMedia(obj) {
|
||||
return {
|
||||
text: obj.text,
|
||||
emoji: Util.resolvePartialEmoji(obj.emoji),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
const data = {
|
||||
question: {
|
||||
text: this.question.text,
|
||||
emoji: this.question.emoji,
|
||||
},
|
||||
expiry: this.expiry?.toISOString(),
|
||||
allow_multiselect: this.allowMultiSelect,
|
||||
layout_type: typeof this.layoutType == 'number' ? this.layoutType : MessagePollLayoutTypes[this.layoutType],
|
||||
answers: Array.from(this.answers.entries()).map(([id, data]) => ({
|
||||
answer_id: id,
|
||||
poll_media: {
|
||||
text: data.text,
|
||||
emoji: data.emoji,
|
||||
},
|
||||
})),
|
||||
duration: this.duration ?? 1,
|
||||
};
|
||||
if (this.results) {
|
||||
data.results = {
|
||||
is_finalized: this.results.isFinalized,
|
||||
answer_counts: Array.from(this.results.answerCounts.entries()).map(([id, data]) => ({
|
||||
id: id,
|
||||
count: data.count,
|
||||
me_voted: data.selfVoted,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set question
|
||||
* @param {string} text question
|
||||
* @returns {MessagePoll}
|
||||
*/
|
||||
setQuestion(text) {
|
||||
this.question = {
|
||||
text,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set answers
|
||||
* @param {MessagePollMedia[]} answers answers
|
||||
* @returns {MessagePoll}
|
||||
*/
|
||||
setAnswers(answers) {
|
||||
this.answers.clear();
|
||||
answers.forEach((obj, index) => {
|
||||
this.answers.set(index + 1, this._resolvePollMedia(obj));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add answer
|
||||
* @param {MessagePollMedia} answer answer
|
||||
* @returns {MessagePoll}
|
||||
*/
|
||||
addAnswer(answer) {
|
||||
this.answers.set(this.answers.size + 1, answer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set allow multi select
|
||||
* @param {boolean} state state
|
||||
* @returns {MessagePoll}
|
||||
*/
|
||||
setAllowMultiSelect(state) {
|
||||
this.allowMultiSelect = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set duration
|
||||
* @param {number} duration duration (hours)
|
||||
* @returns {MessagePoll}
|
||||
*/
|
||||
setDuration(duration) {
|
||||
// [1, 4, 8, 24, 72, 168, 336];
|
||||
this.duration = duration;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessagePoll;
|
||||
108
src/structures/Poll.js
Normal file
108
src/structures/Poll.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const Base = require('./Base');
|
||||
const { PollAnswer } = require('./PollAnswer');
|
||||
const { Error } = require('../errors');
|
||||
const { PollLayoutTypes } = require('../util/Constants');
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
|
||||
/**
|
||||
* The media for a poll's question
|
||||
* @typedef {Object} PollQuestionMedia
|
||||
* @property {string} text The text of this question
|
||||
*/
|
||||
|
||||
/**
|
||||
* The media for this poll's question
|
||||
* @type {PollQuestionMedia}
|
||||
*/
|
||||
this.question = {
|
||||
text: data.question.text,
|
||||
};
|
||||
|
||||
/**
|
||||
* The answers of this poll
|
||||
* @type {Collection<number, PollAnswer>}
|
||||
*/
|
||||
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 = PollLayoutTypes[data.layout_type];
|
||||
|
||||
this._patch(data);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
if (data.results) {
|
||||
/**
|
||||
* Whether this poll's results have been precisely counted
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.resultsFinalized = data.results.is_finalized;
|
||||
|
||||
for (const answerResult of data.results.answer_counts) {
|
||||
const answer = this.answers.get(answerResult.id);
|
||||
answer?._patch(answerResult);
|
||||
}
|
||||
} else {
|
||||
this.resultsFinalized ??= false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The date when this poll expires
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get expiresAt() {
|
||||
return new Date(this.expiresTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends this poll.
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
end() {
|
||||
if (Date.now() > this.expiresTimestamp) {
|
||||
return Promise.reject(new Error('POLL_ALREADY_EXPIRED'));
|
||||
}
|
||||
return this.message.channel.messages.endPoll(this.message.id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Poll };
|
||||
88
src/structures/PollAnswer.js
Normal file
88
src/structures/PollAnswer.js
Normal file
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
_patch(data) {
|
||||
// This `count` field comes from `poll.results.answer_counts`
|
||||
if ('count' in data) {
|
||||
/**
|
||||
* The amount of votes this answer has
|
||||
* @type {number}
|
||||
*/
|
||||
this.voteCount = data.count;
|
||||
} else {
|
||||
this.voteCount ??= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Collection<Snowflake, User>>}
|
||||
*/
|
||||
fetchVoters({ after, limit } = {}) {
|
||||
return this.poll.message.channel.messages.fetchPollAnswerVoters({
|
||||
messageId: this.poll.message.id,
|
||||
answerId: this.id,
|
||||
after,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PollAnswer };
|
||||
@@ -111,7 +111,7 @@ class Webhook {
|
||||
|
||||
/**
|
||||
* Options that can be passed into send.
|
||||
* @typedef {BaseMessageOptions} WebhookMessageOptions
|
||||
* @typedef {BaseMessageOptionsWithPoll} WebhookMessageOptions
|
||||
* @property {string} [username=this.name] Username override for the message
|
||||
* @property {string} [avatarURL] Avatar URL override for the message
|
||||
* @property {Snowflake} [threadId] The id of the thread in the channel to send to.
|
||||
|
||||
@@ -27,7 +27,7 @@ class InteractionResponses {
|
||||
|
||||
/**
|
||||
* Options for a reply to an {@link Interaction}.
|
||||
* @typedef {BaseMessageOptions} InteractionReplyOptions
|
||||
* @typedef {BaseMessageOptionsWithPoll} InteractionReplyOptions
|
||||
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
|
||||
* @property {boolean} [fetchReply] Whether to fetch the reply
|
||||
* @property {MessageFlags} [flags] Which flags to set for the message.
|
||||
|
||||
@@ -60,6 +60,28 @@ class TextBasedChannel {
|
||||
return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the data for a poll answer.
|
||||
* @typedef {Object} PollAnswerData
|
||||
* @property {string} text The text for the poll answer
|
||||
* @property {EmojiIdentifierResolvable} [emoji] The emoji for the poll answer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the data for a poll.
|
||||
* @typedef {Object} PollData
|
||||
* @property {PollQuestionMedia} question The question for the poll
|
||||
* @property {PollAnswerData[]} answers The answers for the poll
|
||||
* @property {number} duration The duration in hours for the poll
|
||||
* @property {boolean} allowMultiselect Whether the poll allows multiple answers
|
||||
* @property {PollLayoutType} [layoutType] The layout type for the poll
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external PollLayoutType
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/PollLayoutType}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base options provided when sending.
|
||||
* @typedef {Object} BaseMessageOptions
|
||||
@@ -75,7 +97,12 @@ class TextBasedChannel {
|
||||
* @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components]
|
||||
* Action rows containing interactive components for the message (buttons, select menus)
|
||||
* @property {MessageAttachment[]} [attachments] Attachments to send in the message
|
||||
* @property {MessagePoll} [poll] A poll!
|
||||
*/
|
||||
|
||||
/**
|
||||
* The base message options for messages including a poll.
|
||||
* @typedef {BaseMessageOptions} BaseMessageOptionsWithPoll
|
||||
* @property {PollData} [poll] The poll to send with the message
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1768,10 +1768,10 @@ exports.ForumLayoutTypes = createEnum(['NOT_SET', 'LIST_VIEW', 'GALLERY_VIEW']);
|
||||
* Different layouts for {@link MessagePoll} will come in the future. For now though, this value will always be `DEFAULT`.
|
||||
* * DEFAULT
|
||||
* * IMAGE_ONLY_ANSWERS
|
||||
* @typedef {string} MessagePollLayoutType
|
||||
* @typedef {string} PollLayoutType
|
||||
* @see {@link https://docs.discord.sex/resources/message#poll-layout-type}
|
||||
*/
|
||||
exports.MessagePollLayoutTypes = createEnum([null, 'DEFAULT', 'IMAGE_ONLY_ANSWERS']);
|
||||
exports.PollLayoutTypes = createEnum([null, 'DEFAULT', 'IMAGE_ONLY_ANSWERS']);
|
||||
|
||||
/**
|
||||
* Relationship Enums:
|
||||
|
||||
Reference in New Issue
Block a user