Skip to content

Commit e3e3c21

Browse files
uhKevinMCalmeidxQjuh
authored
feat: polls overhaul (#10328)
* feat(Managers): add PollAnswerVoterManager * feat(Partials): make Polls partial-safe * types: add typings * chore: add tests * fix: use fetch method in manager instead * chore: add tests for manager * feat: add partial support to poll actions * style: formatting * fix: change all .users references to .voters * refactor: add additional logic for partials * fix: actually add the partials * fix: fixed issue where event does not emit on first event * fix: align property type with DAPI documentation * fix: resolve additional bugs with partials * typings: update typings to reflect property type change * fix: tests * fix: adjust tests * refactor: combine partials logic into one statement * docs: mark getter as readonly * refactor: apply suggestions Co-authored-by: Almeida <[email protected]> * refactor(Actions): apply suggestions * refactor(PollAnswerVoterManager): apply suggestions * refactor(Message): check for existing poll before creating a poll * refactor(Polls): apply suggestions * revert(types): remove unused method from Poll class * refactor(Actions): consolidate poll creation logic into action class * refactor(PollAnswerVoterManager): set default for fetch parameter * refactor(Message): apply suggestion * fix: remove partial setter * refactor(Polls): apply suggestions * types: apply suggestions * refactor: remove clones * docs: spacing * refactor: move setters from constructor to _patch * types: adjust partials for poll classes * test: add more tests for polls * refactor: move updates around, more correct partial types * fix: handle more cases * refactor: requested changes * fix: missing imports * fix: update imports * fix: require file extensions --------- Co-authored-by: Almeida <[email protected]> Co-authored-by: Qjuh <[email protected]>
1 parent 44b0f7d commit e3e3c21

File tree

11 files changed

+358
-73
lines changed

11 files changed

+358
-73
lines changed

packages/discord.js/src/client/actions/Action.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const { Poll } = require('../../structures/Poll.js');
4+
const { PollAnswer } = require('../../structures/PollAnswer.js');
35
const { Partials } = require('../../util/Partials.js');
46

57
/*
@@ -63,6 +65,23 @@ class Action {
6365
);
6466
}
6567

68+
getPoll(data, message, channel) {
69+
const includePollPartial = this.client.options.partials.includes(Partials.Poll);
70+
const includePollAnswerPartial = this.client.options.partials.includes(Partials.PollAnswer);
71+
if (message.partial && (!includePollPartial || !includePollAnswerPartial)) return null;
72+
73+
if (!message.poll && includePollPartial) {
74+
message.poll = new Poll(this.client, data, message, channel);
75+
}
76+
77+
if (message.poll && !message.poll.answers.has(data.answer_id) && includePollAnswerPartial) {
78+
const pollAnswer = new PollAnswer(this.client, data, message.poll);
79+
message.poll.answers.set(data.answer_id, pollAnswer);
80+
}
81+
82+
return message.poll;
83+
}
84+
6685
getReaction(data, message, user) {
6786
const id = data.emoji.id ?? decodeURIComponent(data.emoji.name);
6887
return this.getPayload(

packages/discord.js/src/client/actions/MessagePollVoteAdd.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ class MessagePollVoteAddAction extends Action {
1111
const message = this.getMessage(data, channel);
1212
if (!message) return false;
1313

14-
const { poll } = message;
14+
const poll = this.getPoll(data, message, channel);
15+
if (!poll) return false;
1516

16-
const answer = poll?.answers.get(data.answer_id);
17+
const answer = poll.answers.get(data.answer_id);
1718
if (!answer) return false;
1819

20+
const user = this.getUser(data);
21+
22+
if (user) {
23+
answer.voters._add(user);
24+
}
25+
1926
answer.voteCount++;
2027

2128
/**

packages/discord.js/src/client/actions/MessagePollVoteRemove.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ class MessagePollVoteRemoveAction extends Action {
1111
const message = this.getMessage(data, channel);
1212
if (!message) return false;
1313

14-
const { poll } = message;
14+
const poll = this.getPoll(data, message, channel);
15+
if (!poll) return false;
1516

16-
const answer = poll?.answers.get(data.answer_id);
17+
const answer = poll.answers.get(data.answer_id);
1718
if (!answer) return false;
1819

19-
answer.voteCount--;
20+
answer.voters.cache.delete(data.user_id);
21+
22+
if (answer.voteCount > 0) {
23+
answer.voteCount--;
24+
}
2025

2126
/**
2227
* Emitted whenever a user removes their vote in a poll.

packages/discord.js/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ exports.GuildStickerManager = require('./managers/GuildStickerManager.js').Guild
8181
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager.js').GuildTextThreadManager;
8282
exports.MessageManager = require('./managers/MessageManager.js').MessageManager;
8383
exports.PermissionOverwriteManager = require('./managers/PermissionOverwriteManager.js').PermissionOverwriteManager;
84+
exports.PollAnswerVoterManager = require('./managers/PollAnswerVoterManager.js').PollAnswerVoterManager;
8485
exports.PresenceManager = require('./managers/PresenceManager.js').PresenceManager;
8586
exports.ReactionManager = require('./managers/ReactionManager.js').ReactionManager;
8687
exports.ReactionUserManager = require('./managers/ReactionUserManager.js').ReactionUserManager;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const { Collection } = require('@discordjs/collection');
4+
const { makeURLSearchParams } = require('@discordjs/rest');
5+
const { Routes } = require('discord-api-types/v10');
6+
const { CachedManager } = require('./CachedManager.js');
7+
const { User } = require('../structures/User.js');
8+
9+
/**
10+
* Manages API methods for users who voted on a poll and stores their cache.
11+
* @extends {CachedManager}
12+
*/
13+
class PollAnswerVoterManager extends CachedManager {
14+
constructor(answer) {
15+
super(answer.client, User);
16+
17+
/**
18+
* The poll answer that this manager belongs to
19+
* @type {PollAnswer}
20+
*/
21+
this.answer = answer;
22+
}
23+
24+
/**
25+
* The cache of this manager
26+
* @type {Collection<Snowflake, User>}
27+
* @name PollAnswerVoterManager#cache
28+
*/
29+
30+
/**
31+
* Fetches the users that voted on this poll answer. Resolves with a collection of users, mapped by their ids.
32+
* @param {BaseFetchPollAnswerVotersOptions} [options={}] Options for fetching the users
33+
* @returns {Promise<Collection<Snowflake, User>>}
34+
*/
35+
async fetch({ after, limit } = {}) {
36+
const poll = this.answer.poll;
37+
const query = makeURLSearchParams({ limit, after });
38+
const data = await this.client.rest.get(Routes.pollAnswerVoters(poll.channelId, poll.messageId, this.answer.id), {
39+
query,
40+
});
41+
42+
return data.users.reduce((coll, rawUser) => {
43+
const user = this.client.users._add(rawUser);
44+
this.cache.set(user.id, user);
45+
return coll.set(user.id, user);
46+
}, new Collection());
47+
}
48+
}
49+
50+
exports.PollAnswerVoterManager = PollAnswerVoterManager;

packages/discord.js/src/structures/Message.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,15 @@ class Message extends Base {
414414
}
415415

416416
if (data.poll) {
417-
/**
418-
* The poll that was sent with the message
419-
* @type {?Poll}
420-
*/
421-
this.poll = new Poll(this.client, data.poll, this);
417+
if (this.poll) {
418+
this.poll._patch(data.poll);
419+
} else {
420+
/**
421+
* The poll that was sent with the message
422+
* @type {?Poll}
423+
*/
424+
this.poll = new Poll(this.client, data.poll, this, this.channel);
425+
}
422426
} else {
423427
this.poll ??= null;
424428
}

packages/discord.js/src/structures/Poll.js

Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,58 +10,38 @@ const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
1010
* @extends {Base}
1111
*/
1212
class Poll extends Base {
13-
constructor(client, data, message) {
13+
constructor(client, data, message, channel) {
1414
super(client);
1515

1616
/**
17-
* The message that started this poll
18-
* @name Poll#message
19-
* @type {Message}
20-
* @readonly
17+
* The id of the channel that this poll is in
18+
* @type {Snowflake}
2119
*/
22-
23-
Object.defineProperty(this, 'message', { value: message });
20+
this.channelId = data.channel_id ?? channel.id;
2421

2522
/**
26-
* The media for a poll's question
27-
* @typedef {Object} PollQuestionMedia
28-
* @property {string} text The text of this question
29-
*/
30-
31-
/**
32-
* The media for this poll's question
33-
* @type {PollQuestionMedia}
23+
* The channel that this poll is in
24+
* @name Poll#channel
25+
* @type {TextBasedChannel}
26+
* @readonly
3427
*/
35-
this.question = {
36-
text: data.question.text,
37-
};
3828

39-
/**
40-
* The answers of this poll
41-
* @type {Collection<number, PollAnswer>}
42-
*/
43-
this.answers = data.answers.reduce(
44-
(acc, answer) => acc.set(answer.answer_id, new PollAnswer(this.client, answer, this)),
45-
new Collection(),
46-
);
29+
Object.defineProperty(this, 'channel', { value: channel });
4730

4831
/**
49-
* The timestamp when this poll expires
50-
* @type {number}
32+
* The id of the message that started this poll
33+
* @type {Snowflake}
5134
*/
52-
this.expiresTimestamp = Date.parse(data.expiry);
35+
this.messageId = data.message_id ?? message.id;
5336

5437
/**
55-
* Whether this poll allows multiple answers
56-
* @type {boolean}
38+
* The message that started this poll
39+
* @name Poll#message
40+
* @type {Message}
41+
* @readonly
5742
*/
58-
this.allowMultiselect = data.allow_multiselect;
5943

60-
/**
61-
* The layout type of this poll
62-
* @type {PollLayoutType}
63-
*/
64-
this.layoutType = data.layout_type;
44+
Object.defineProperty(this, 'message', { value: message });
6545

6646
this._patch(data);
6747
}
@@ -81,15 +61,101 @@ class Poll extends Base {
8161
} else {
8262
this.resultsFinalized ??= false;
8363
}
64+
65+
if ('allow_multiselect' in data) {
66+
/**
67+
* Whether this poll allows multiple answers
68+
* @type {boolean}
69+
*/
70+
this.allowMultiselect = data.allow_multiselect;
71+
} else {
72+
this.allowMultiselect ??= null;
73+
}
74+
75+
if ('layout_type' in data) {
76+
/**
77+
* The layout type of this poll
78+
* @type {PollLayoutType}
79+
*/
80+
this.layoutType = data.layout_type;
81+
} else {
82+
this.layoutType ??= null;
83+
}
84+
85+
if ('expiry' in data) {
86+
/**
87+
* The timestamp when this poll expires
88+
* @type {?number}
89+
*/
90+
this.expiresTimestamp = data.expiry && Date.parse(data.expiry);
91+
} else {
92+
this.expiresTimestamp ??= null;
93+
}
94+
95+
if (data.question) {
96+
/**
97+
* The media for a poll's question
98+
* @typedef {Object} PollQuestionMedia
99+
* @property {?string} text The text of this question
100+
*/
101+
102+
/**
103+
* The media for this poll's question
104+
* @type {PollQuestionMedia}
105+
*/
106+
this.question = {
107+
text: data.question.text,
108+
};
109+
} else {
110+
this.question ??= {
111+
text: null,
112+
};
113+
}
114+
115+
/**
116+
* The answers of this poll
117+
* @type {Collection<number, PollAnswer|PartialPollAnswer>}
118+
*/
119+
this.answers ??= new Collection();
120+
121+
if (data.answers) {
122+
for (const answer of data.answers) {
123+
const existing = this.answers.get(answer.answer_id);
124+
if (existing) {
125+
existing._patch(answer);
126+
} else {
127+
this.answers.set(answer.answer_id, new PollAnswer(this.client, answer, this));
128+
}
129+
}
130+
}
84131
}
85132

86133
/**
87134
* The date when this poll expires
88-
* @type {Date}
135+
* @type {?Date}
89136
* @readonly
90137
*/
91138
get expiresAt() {
92-
return new Date(this.expiresTimestamp);
139+
return this.expiresTimestamp && new Date(this.expiresTimestamp);
140+
}
141+
142+
/**
143+
* Whether this poll is a partial
144+
* @type {boolean}
145+
* @readonly
146+
*/
147+
get partial() {
148+
return this.allowMultiselect === null;
149+
}
150+
151+
/**
152+
* Fetches the message that started this poll, then updates the poll from the fetched message.
153+
* @returns {Promise<Poll>}
154+
*/
155+
async fetch() {
156+
await this.channel.messages.fetch(this.messageId);
157+
158+
return this;
93159
}
94160

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

104-
return this.message.channel.messages.endPoll(this.message.id);
170+
return this.channel.messages.endPoll(this.messageId);
105171
}
106172
}
107173

0 commit comments

Comments
 (0)