Skip to content

feat: polls overhaul #10328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 52 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e5a5966
feat(Managers): add PollAnswerVoterManager
uhKevinMC Jun 6, 2024
ea2cb46
feat(Partials): make Polls partial-safe
uhKevinMC Jun 6, 2024
9370828
types: add typings
uhKevinMC Jun 6, 2024
f311a41
chore: add tests
uhKevinMC Jun 6, 2024
e33342c
fix: use fetch method in manager instead
uhKevinMC Jun 6, 2024
c4c7806
chore: add tests for manager
uhKevinMC Jun 6, 2024
068d17a
feat: add partial support to poll actions
uhKevinMC Jun 8, 2024
ed3104a
style: formatting
uhKevinMC Jun 8, 2024
648f767
Merge branch 'discordjs:main' into main
uhKevinMC Jun 8, 2024
6db874e
fix: change all .users references to .voters
uhKevinMC Jun 9, 2024
43930c1
refactor: add additional logic for partials
uhKevinMC Jun 9, 2024
01fca07
fix: actually add the partials
uhKevinMC Jun 9, 2024
08061e6
fix: fixed issue where event does not emit on first event
uhKevinMC Jun 9, 2024
056fb3c
fix: align property type with DAPI documentation
uhKevinMC Jun 9, 2024
7113c56
fix: resolve additional bugs with partials
uhKevinMC Jun 9, 2024
c1c848a
typings: update typings to reflect property type change
uhKevinMC Jun 9, 2024
f4686f0
fix: tests
uhKevinMC Jun 9, 2024
2c4bc44
chore: rebase branch
uhKevinMC Jun 9, 2024
e241cea
fix: adjust tests
uhKevinMC Jun 10, 2024
316e7cc
refactor: combine partials logic into one statement
uhKevinMC Jun 11, 2024
f4b2911
docs: mark getter as readonly
uhKevinMC Jun 11, 2024
4203f09
refactor: apply suggestions
uhKevinMC Jun 30, 2024
e16401e
refactor(Actions): apply suggestions
uhKevinMC Jun 30, 2024
ec18c50
refactor(PollAnswerVoterManager): apply suggestions
uhKevinMC Jun 30, 2024
598fbfd
refactor(Message): check for existing poll before creating a poll
uhKevinMC Jun 30, 2024
8780adb
refactor(Polls): apply suggestions
uhKevinMC Jun 30, 2024
fa4dc3f
revert(types): remove unused method from Poll class
uhKevinMC Jun 30, 2024
bfbb377
refactor(Actions): consolidate poll creation logic into action class
uhKevinMC Jul 1, 2024
2eec293
refactor(PollAnswerVoterManager): set default for fetch parameter
uhKevinMC Jul 1, 2024
6e632ba
refactor(Message): apply suggestion
uhKevinMC Jul 1, 2024
4857a94
fix: remove partial setter
uhKevinMC Jul 1, 2024
bb2a2ad
refactor(Polls): apply suggestions
uhKevinMC Jul 1, 2024
418c23b
types: apply suggestions
uhKevinMC Jul 1, 2024
2e54eff
Merge branch 'discordjs:main' into main
uhKevinMC Jul 1, 2024
2f85ad8
refactor: remove clones
uhKevinMC Jul 3, 2024
93f2a36
docs: spacing
uhKevinMC Jul 3, 2024
c25c650
refactor: move setters from constructor to _patch
uhKevinMC Jul 3, 2024
060cc52
types: adjust partials for poll classes
uhKevinMC Jul 3, 2024
cea18b0
test: add more tests for polls
uhKevinMC Jul 3, 2024
46f55e4
refactor: move updates around, more correct partial types
almeidx Jul 4, 2024
4c65504
fix: handle more cases
almeidx Jul 4, 2024
a82f3a3
refactor: requested changes
almeidx Aug 24, 2024
1ddb5ab
Merge branch 'main' into main
almeidx Aug 24, 2024
71f1c65
Merge branch 'main' into main
almeidx Sep 22, 2024
f72c1f6
Merge branch 'main' into main
almeidx Dec 21, 2024
d103cd9
fix: missing imports
almeidx Dec 24, 2024
a8a4f29
Merge branch 'main' into main
almeidx Jan 12, 2025
ba19104
Merge branch 'main' into main
almeidx Jan 21, 2025
955cf6a
fix: update imports
almeidx Jan 21, 2025
6e73d7b
Merge branch 'main' into main
almeidx Feb 4, 2025
fa0244a
fix: require file extensions
almeidx Feb 4, 2025
908f2a3
Merge branch 'main' into main
Qjuh Feb 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/discord.js/src/client/actions/Action.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const { Poll } = require('../../structures/Poll.js');
const { PollAnswer } = require('../../structures/PollAnswer.js');
const { Partials } = require('../../util/Partials.js');

/*
Expand Down Expand Up @@ -63,6 +65,23 @@ class Action {
);
}

getPoll(data, message, channel) {
const includePollPartial = this.client.options.partials.includes(Partials.Poll);
const includePollAnswerPartial = this.client.options.partials.includes(Partials.PollAnswer);
if (message.partial && (!includePollPartial || !includePollAnswerPartial)) return null;

if (!message.poll && includePollPartial) {
message.poll = new Poll(this.client, data, message, channel);
}

if (message.poll && !message.poll.answers.has(data.answer_id) && includePollAnswerPartial) {
const pollAnswer = new PollAnswer(this.client, data, message.poll);
message.poll.answers.set(data.answer_id, pollAnswer);
}

return message.poll;
}

getReaction(data, message, user) {
const id = data.emoji.id ?? decodeURIComponent(data.emoji.name);
return this.getPayload(
Expand Down
11 changes: 9 additions & 2 deletions packages/discord.js/src/client/actions/MessagePollVoteAdd.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ class MessagePollVoteAddAction extends Action {
const message = this.getMessage(data, channel);
if (!message) return false;

const { poll } = message;
const poll = this.getPoll(data, message, channel);
if (!poll) return false;

const answer = poll?.answers.get(data.answer_id);
const answer = poll.answers.get(data.answer_id);
if (!answer) return false;

const user = this.getUser(data);

if (user) {
answer.voters._add(user);
}

answer.voteCount++;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ class MessagePollVoteRemoveAction extends Action {
const message = this.getMessage(data, channel);
if (!message) return false;

const { poll } = message;
const poll = this.getPoll(data, message, channel);
if (!poll) return false;

const answer = poll?.answers.get(data.answer_id);
const answer = poll.answers.get(data.answer_id);
if (!answer) return false;

answer.voteCount--;
answer.voters.cache.delete(data.user_id);

if (answer.voteCount > 0) {
answer.voteCount--;
}

/**
* Emitted whenever a user removes their vote in a poll.
Expand Down
1 change: 1 addition & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ exports.GuildStickerManager = require('./managers/GuildStickerManager.js').Guild
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager.js').GuildTextThreadManager;
exports.MessageManager = require('./managers/MessageManager.js').MessageManager;
exports.PermissionOverwriteManager = require('./managers/PermissionOverwriteManager.js').PermissionOverwriteManager;
exports.PollAnswerVoterManager = require('./managers/PollAnswerVoterManager.js').PollAnswerVoterManager;
exports.PresenceManager = require('./managers/PresenceManager.js').PresenceManager;
exports.ReactionManager = require('./managers/ReactionManager.js').ReactionManager;
exports.ReactionUserManager = require('./managers/ReactionUserManager.js').ReactionUserManager;
Expand Down
50 changes: 50 additions & 0 deletions packages/discord.js/src/managers/PollAnswerVoterManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const { CachedManager } = require('./CachedManager.js');
const { User } = require('../structures/User.js');

/**
* Manages API methods for users who voted on a poll and stores their cache.
* @extends {CachedManager}
*/
class PollAnswerVoterManager extends CachedManager {
constructor(answer) {
super(answer.client, User);

/**
* The poll answer that this manager belongs to
* @type {PollAnswer}
*/
this.answer = answer;
}

/**
* The cache of this manager
* @type {Collection<Snowflake, User>}
* @name PollAnswerVoterManager#cache
*/

/**
* Fetches the users that voted on this poll answer. Resolves with a collection of users, mapped by their ids.
* @param {BaseFetchPollAnswerVotersOptions} [options={}] Options for fetching the users
* @returns {Promise<Collection<Snowflake, User>>}
*/
async fetch({ after, limit } = {}) {
const poll = this.answer.poll;
const query = makeURLSearchParams({ limit, after });
const data = await this.client.rest.get(Routes.pollAnswerVoters(poll.channelId, poll.messageId, this.answer.id), {
query,
});

return data.users.reduce((coll, rawUser) => {
const user = this.client.users._add(rawUser);
this.cache.set(user.id, user);
return coll.set(user.id, user);
}, new Collection());
}
}

exports.PollAnswerVoterManager = PollAnswerVoterManager;
14 changes: 9 additions & 5 deletions packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,11 +414,15 @@ class Message extends Base {
}

if (data.poll) {
/**
* The poll that was sent with the message
* @type {?Poll}
*/
this.poll = new Poll(this.client, data.poll, this);
if (this.poll) {
this.poll._patch(data.poll);
} else {
/**
* The poll that was sent with the message
* @type {?Poll}
*/
this.poll = new Poll(this.client, data.poll, this, this.channel);
}
} else {
this.poll ??= null;
}
Expand Down
146 changes: 106 additions & 40 deletions packages/discord.js/src/structures/Poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,38 @@ const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
* @extends {Base}
*/
class Poll extends Base {
constructor(client, data, message) {
constructor(client, data, message, channel) {
super(client);

/**
* The message that started this poll
* @name Poll#message
* @type {Message}
* @readonly
* The id of the channel that this poll is in
* @type {Snowflake}
*/

Object.defineProperty(this, 'message', { value: message });
this.channelId = data.channel_id ?? channel.id;

/**
* 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}
* The channel that this poll is in
* @name Poll#channel
* @type {TextBasedChannel}
* @readonly
*/
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(),
);
Object.defineProperty(this, 'channel', { value: channel });

/**
* The timestamp when this poll expires
* @type {number}
* The id of the message that started this poll
* @type {Snowflake}
*/
this.expiresTimestamp = Date.parse(data.expiry);
this.messageId = data.message_id ?? message.id;

/**
* Whether this poll allows multiple answers
* @type {boolean}
* The message that started this poll
* @name Poll#message
* @type {Message}
* @readonly
*/
this.allowMultiselect = data.allow_multiselect;

/**
* The layout type of this poll
* @type {PollLayoutType}
*/
this.layoutType = data.layout_type;
Object.defineProperty(this, 'message', { value: message });

this._patch(data);
}
Expand All @@ -81,15 +61,101 @@ class Poll extends Base {
} else {
this.resultsFinalized ??= false;
}

if ('allow_multiselect' in data) {
/**
* Whether this poll allows multiple answers
* @type {boolean}
*/
this.allowMultiselect = data.allow_multiselect;
} else {
this.allowMultiselect ??= null;
}

if ('layout_type' in data) {
/**
* The layout type of this poll
* @type {PollLayoutType}
*/
this.layoutType = data.layout_type;
} else {
this.layoutType ??= null;
}

if ('expiry' in data) {
/**
* The timestamp when this poll expires
* @type {?number}
*/
this.expiresTimestamp = data.expiry && Date.parse(data.expiry);
} else {
this.expiresTimestamp ??= null;
}

if (data.question) {
/**
* 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,
};
} else {
this.question ??= {
text: null,
};
}

/**
* The answers of this poll
* @type {Collection<number, PollAnswer|PartialPollAnswer>}
*/
this.answers ??= new Collection();

if (data.answers) {
for (const answer of data.answers) {
const existing = this.answers.get(answer.answer_id);
if (existing) {
existing._patch(answer);
} else {
this.answers.set(answer.answer_id, new PollAnswer(this.client, answer, this));
}
}
}
}

/**
* The date when this poll expires
* @type {Date}
* @type {?Date}
* @readonly
*/
get expiresAt() {
return new Date(this.expiresTimestamp);
return this.expiresTimestamp && new Date(this.expiresTimestamp);
}

/**
* Whether this poll is a partial
* @type {boolean}
* @readonly
*/
get partial() {
return this.allowMultiselect === null;
}

/**
* Fetches the message that started this poll, then updates the poll from the fetched message.
* @returns {Promise<Poll>}
*/
async fetch() {
await this.channel.messages.fetch(this.messageId);

return this;
}

/**
Expand All @@ -101,7 +167,7 @@ class Poll extends Base {
throw new DiscordjsError(ErrorCodes.PollAlreadyExpired);
}

return this.message.channel.messages.endPoll(this.message.id);
return this.channel.messages.endPoll(this.messageId);
}
}

Expand Down
Loading