Skip to content
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

feat: polls overhaul #10328

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
47 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
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
24 changes: 22 additions & 2 deletions packages/discord.js/src/client/actions/MessagePollVoteAdd.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use strict';

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

class MessagePollVoteAddAction extends Action {
handle(data) {
Expand All @@ -13,9 +16,26 @@ class MessagePollVoteAddAction extends Action {

const { poll } = message;

const answer = poll?.answers.get(data.answer_id);
if (!answer) return false;
const includePollPartial = this.client.options.partials.includes(Partials.Poll);
if (message.partial && !includePollPartial) return false;

if (!poll && includePollPartial) {
message.poll = new Poll(this.client, { ...data, partial: true }, message, channel);
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
}

const includePollAnswerPartial = this.client.options.partials.includes(Partials.PollAnswer);
if (message.partial && includePollPartial && !includePollAnswerPartial) return false;

const answer = poll?.answers?.get(data.answer_id);
if (!answer && poll) {
const pollAnswer = new PollAnswer(this.client, data, poll);

poll.answers.set(data.answer_id, pollAnswer);
} else {
return false;
}

answer.voters._add(data.user_id);
answer.voteCount++;

/**
Expand Down
24 changes: 22 additions & 2 deletions packages/discord.js/src/client/actions/MessagePollVoteRemove.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use strict';

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

class MessagePollVoteRemoveAction extends Action {
handle(data) {
Expand All @@ -13,9 +16,26 @@ class MessagePollVoteRemoveAction extends Action {

const { poll } = message;

const answer = poll?.answers.get(data.answer_id);
if (!answer) return false;
const includePollPartial = this.client.options.partials.includes(Partials.Poll);
if (message.partial && !includePollPartial) return false;

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

const includePollAnswerPartial = this.client.options.partials.includes(Partials.PollAnswer);
if (message.partial && includePollPartial && !includePollAnswerPartial) return false;

const answer = poll?.answers?.get(data.answer_id);
if (!answer && poll) {
const pollAnswer = new PollAnswer(this.client, data, poll);

poll.answers.set(data.answer_id, pollAnswer);
} else {
return false;
}

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

/**
Expand Down
62 changes: 62 additions & 0 deletions packages/discord.js/src/managers/PollAnswerVoterManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

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

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

/**
* 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
*/

/**
* Options used to fetch voters who voted for this poll answer.
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
* @typedef {Object} FetchPollAnswerVotersOptions
almeidx marked this conversation as resolved.
Show resolved Hide resolved
* @property {number} [limit] The maximum amount of users to fetch
* @property {Snowflake} [after] The user id to fetch voters after
*/

/**
* Fetches all the voters that voted on this poll answer. Resolves with a collection of users, mapped by their ids.
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
* @param {FetchPollAnswerVotersOptions} [options] Options for fetching the users
* @returns {Promise<Collection<Snowflake, User>>}
*/
async fetch({ after, limit }) {
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
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,
});

const users = new Collection();
for (const rawUser of data) {
const user = this.client.users._add(rawUser);
this.cache.set(user.id, user);
users.set(user.id, user);
}
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved

this.answer.voteCount = users.size;
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved

return users;
}
}

module.exports = PollAnswerVoterManager;
59 changes: 57 additions & 2 deletions packages/discord.js/src/structures/Poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,30 @@ const { ErrorCodes } = require('../errors/index');
* @extends {Base}
*/
class Poll extends Base {
constructor(client, data, message) {
constructor(client, data, message, channel) {
super(client);

/**
* The id of the channel that this poll is in
* @type {Snowflake}
*/
this.channelId = data.channel_id;

/**
* The channel that this poll is in
* @name Poll#channel
* @type {TextBasedChannel}
* @readonly
*/

Object.defineProperty(this, 'channel', { value: channel });
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved

/**
* The id of the message that started this poll
* @type {Snowflake}
*/
this.messageId = data.message_id;

/**
* The message that started this poll
* @name Poll#message
Expand All @@ -39,7 +60,7 @@ class Poll extends Base {

/**
* The answers of this poll
* @type {Collection<number, PollAnswer>}
* @type {Collection<number, PollAnswer | PartialPollAnswer>}
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
*/
this.answers = data.answers.reduce(
(acc, answer) => acc.set(answer.answer_id, new PollAnswer(this.client, answer, this)),
Expand Down Expand Up @@ -93,6 +114,40 @@ class Poll extends Base {
return new Date(this.expiresTimestamp);
}

/**
* Whether or not this poll is a partial
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
* @type {boolean}
* @readonly
*/
get partial() {
return (
typeof this.question.text !== 'string' ||
typeof this.layoutType !== 'number' ||
typeof this.allowMultiselect !== 'boolean'
);
}

/**
* Fetches the poll
* @returns {Promise<Poll>}
*/
async fetch() {
const message = await this.message.channel.messages.fetch(this.message.id);

const existing = message.poll;
this._patch(existing);

return this;
}

/**
* Fetches the message that started this poll
* @returns {Promise<Message>}
*/
fetchMessage() {
return this.message.channel.messages.fetch(this.message.id);
}

/**
* Ends this poll.
* @returns {Promise<Message>}
Expand Down
24 changes: 17 additions & 7 deletions packages/discord.js/src/structures/PollAnswer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const Base = require('./Base');
const { Emoji } = require('./Emoji');
const PollAnswerVoterManager = require('../managers/PollAnswerVoterManager');

/**
* Represents an answer to a {@link Poll}
Expand All @@ -14,7 +15,7 @@ class PollAnswer extends Base {
/**
* The {@link Poll} this answer is part of
* @name PollAnswer#poll
* @type {Poll}
* @type {Poll | PartialPoll}
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
* @readonly
*/
Object.defineProperty(this, 'poll', { value: poll });
Expand All @@ -31,6 +32,12 @@ class PollAnswer extends Base {
*/
this.text = data.poll_media.text ?? null;

/**
* The manager of the voters that voted for this answer
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
* @type {PollAnswerVoterManager}
*/
this.voters = new PollAnswerVoterManager(this, []);
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved

/**
* The raw emoji of this answer
* @name PollAnswer#_emoji
Expand Down Expand Up @@ -64,6 +71,14 @@ class PollAnswer extends Base {
return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
}

/**
* Whether or not this answer is a partial.
* @type {boolean}
*/
get partial() {
return this.poll.partial || (this.text === null && this.emoji === null);
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Options used for fetching voters of a poll answer.
* @typedef {Object} BaseFetchPollAnswerVotersOptions
Expand All @@ -77,12 +92,7 @@ class PollAnswer extends Base {
* @returns {Promise<Collection<Snowflake, User>>}
*/
fetchVoters({ after, limit } = {}) {
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
return this.poll.message.channel.messages.fetchPollAnswerVoters({
messageId: this.poll.message.id,
answerId: this.id,
after,
limit,
});
return this.users.fetch({ after, limit });
}
}

Expand Down
34 changes: 30 additions & 4 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2602,16 +2602,28 @@ export interface PollQuestionMedia {
text: string;
}

export class PollAnswerVoterManager extends CachedManager<Snowflake, User, UserResolvable> {
public constructor(answer: PollAnswer, iterable: Iterable<APIUser>);
public answer: PollAnswer;
public fetch(): Promise<Collection<Snowflake, User>>;
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
}

export class Poll extends Base {
private constructor(client: Client<true>, data: APIPoll, message: Message);
private constructor(client: Client<true>, data: APIPoll, message: Message, channel: TextBasedChannel);
public readonly channel: TextBasedChannel;
public channelId: Snowflake;
public readonly message: Message;
public messageId: Snowflake;
public question: PollQuestionMedia;
public answers: Collection<number, PollAnswer>;
public answers: Collection<number, PollAnswer | PartialPollAnswer>;
public expiresTimestamp: number;
public get expiresAt(): Date;
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
public allowMultiselect: boolean;
public layoutType: PollLayoutType;
public resultsFinalized: boolean;
public get partial(): false;
public fetch(): Promise<Poll>;
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
public fetchMessage(): Promise<Message>;
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
public end(): Promise<Message>;
}

Expand All @@ -2623,11 +2635,13 @@ export interface BaseFetchPollAnswerVotersOptions {
export class PollAnswer extends Base {
private constructor(client: Client<true>, data: APIPollAnswer & { count?: number }, poll: Poll);
private _emoji: APIPartialEmoji | null;
public readonly poll: Poll;
public readonly poll: Poll | PartialPoll;
public id: number;
public text: string | null;
public voteCount: number;
public users: PollAnswerVoterManager;
public get emoji(): GuildEmoji | Emoji | null;
public get partial(): false;
public fetchVoters(options?: BaseFetchPollAnswerVotersOptions): Promise<Collection<Snowflake, User>>;
uhKevinMC marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down Expand Up @@ -4678,7 +4692,9 @@ export type AllowedPartial =
| Message
| MessageReaction
| GuildScheduledEvent
| ThreadMember;
| ThreadMember
| Poll
| PollAnswer;

export type AllowedThreadTypeForNewsChannel = ChannelType.AnnouncementThread;

Expand Down Expand Up @@ -6535,6 +6551,14 @@ export interface PartialMessage

export interface PartialMessageReaction extends Partialize<MessageReaction, 'count'> {}

export interface PartialPoll
extends Partialize<
Poll,
'allowMultiselect' | 'answers' | 'expiresTimestamp' | 'layoutType' | 'question' | 'resultsFinalized' | 'message'
> {}

export interface PartialPollAnswer extends Partialize<PollAnswer, 'voteCount' | 'emoji' | 'poll' | 'text'> {}

export interface PartialGuildScheduledEvent
extends Partialize<GuildScheduledEvent, 'userCount', 'status' | 'privacyLevel' | 'name' | 'entityType'> {}

Expand All @@ -6559,6 +6583,8 @@ export enum Partials {
Reaction,
GuildScheduledEvent,
ThreadMember,
Poll,
PollAnswer,
}

export interface PartialUser extends Partialize<User, 'username' | 'tag' | 'discriminator'> {}
Expand Down
Loading
Loading