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: support polls #139

Merged
merged 14 commits into from
Jul 19, 2024
Merged
49 changes: 49 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ declare namespace Dysnomia {
type MessageActivityTypes = Constants["MessageActivityTypes"][keyof Constants["MessageActivityTypes"]];
type MessageContent<T extends "hasNonce" | "" = ""> = string | AdvancedMessageContent<T>;
type MFALevel = Constants["MFALevels"][keyof Constants["MFALevels"]];
type PollLayoutTypes = Constants["PollLayoutTypes"][keyof Constants["PollLayoutTypes"]];
type PossiblyUncachedMessage = Message | { author?: User | Uncached; channel: TextableChannel | { id: string; guild?: Uncached }; guildID?: string; id: string };
type ReactionTypes = Constants["ReactionTypes"][keyof Constants["ReactionTypes"]];
type SelectMenu = BaseSelectMenu | ChannelSelectMenu | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu;
Expand Down Expand Up @@ -768,6 +769,8 @@ declare namespace Dysnomia {
messageCreate: [message: Message<PossiblyUncachedTextableChannel>];
messageDelete: [message: PossiblyUncachedMessage];
messageDeleteBulk: [messages: PossiblyUncachedMessage[]];
messagePollVoteAdd: [message: PossiblyUncachedMessage, answerID: number, user: User | Uncached];
messagePollVoteRemove: [message: PossiblyUncachedMessage, answerID: number, user: User | Uncached];
messageReactionAdd: [message: PossiblyUncachedMessage, emoji: PartialEmoji, reactor: Member | Uncached, isBurst: boolean];
messageReactionRemove: [message: PossiblyUncachedMessage, emoji: PartialEmoji, userID: string, isBurst: boolean];
messageReactionRemoveAll: [message: PossiblyUncachedMessage];
Expand Down Expand Up @@ -1362,6 +1365,7 @@ declare namespace Dysnomia {
flags?: number;
messageReference?: MessageReferenceReply;
nonce?: T extends "hasNonce" ? (string | number) : never;
poll?: NewPoll;
stickerIDs?: string[];
tts?: boolean;
}
Expand Down Expand Up @@ -1466,6 +1470,11 @@ declare namespace Dysnomia {
type?: ReactionTypes;
}

interface GetPollAnswerVotersOptions {
after?: string;
limit?: number;
}

interface InteractionButton extends ButtonBase {
custom_id: string;
style: ButtonStyleNormal;
Expand Down Expand Up @@ -1505,6 +1514,38 @@ declare namespace Dysnomia {
messageID: string;
failIfNotExists?: boolean;
}
interface NewPoll {
question: PollMedia;
answers: (Omit<PollAnswer, "answer_id">)[];
duration?: number;
allow_multiselect?: boolean;
layout_type?: PollLayoutTypes;
}
interface Poll {
question: PollMedia;
answers: PollAnswer[];
expiry: string | null;
allow_multiselect: boolean;
layout_type: PollLayoutTypes;
results?: PollResults;
}
interface PollAnswer {
answer_id: number;
poll_media: PollMedia;
}
interface PollAnswerCount {
id: number;
count: number;
me_voted: boolean;
}
interface PollMedia {
text?: string;
emoji?: PartialEmoji;
}
interface PollResults {
is_finalized: boolean;
answer_counts: PollAnswerCount[];
}
interface RoleSubscriptionData {
isRenewal: boolean;
roleSubscriptionListingID: string;
Expand Down Expand Up @@ -2324,6 +2365,9 @@ declare namespace Dysnomia {
allVoice: 40136803878673n;
all: 1829587348619263n;
};
PollLayoutTypes: {
DEFAULT: 1;
};
PremiumTiers: {
NONE: 0;
TIER_1: 1;
Expand Down Expand Up @@ -2770,6 +2814,7 @@ declare namespace Dysnomia {
): Promise<Message<GuildTextableChannel>>;
emit<K extends keyof ClientEvents>(event: K, ...args: ClientEvents[K]): boolean;
emit(event: string, ...args: any[]): boolean;
endPoll(channelID: string, messageID: string): Promise<Message>;
executeSlackWebhook(webhookID: string, token: string, options: Record<string, unknown> & { auth?: boolean; threadID?: string }): Promise<void>;
executeSlackWebhook(webhookID: string, token: string, options: Record<string, unknown> & { auth?: boolean; threadID?: string; wait: true }): Promise<Message<GuildTextableChannel>>;
executeWebhook(webhookID: string, token: string, options: WebhookPayload & { wait: true }): Promise<Message<GuildTextableChannel>>;
Expand Down Expand Up @@ -2818,6 +2863,7 @@ declare namespace Dysnomia {
getNitroStickerPacks(): Promise<{ sticker_packs: StickerPack[] }>;
getOAuthApplication(): Promise<OAuthApplicationInfo>;
getPins(channelID: string): Promise<Message[]>;
getPollAnswerVoters(channelID: string, messageID: string, answerID: number, options?: GetPollAnswerVotersOptions): Promise<User[]>;
getPruneCount(guildID: string, options?: GetPruneOptions): Promise<number>;
getRESTChannel(channelID: string): Promise<AnyChannel>;
getRESTGuild(guildID: string, withCounts?: boolean): Promise<Guild>;
Expand Down Expand Up @@ -3394,6 +3440,7 @@ declare namespace Dysnomia {
messageReference: MessageReference | null;
nonce?: string | number;
pinned: boolean;
poll?: Poll;
position?: number;
reactions: { [s: string]: { burstColors: string[]; count: number; countDetails: { burst: number; normal: number }; me: boolean; meBurst: boolean } };
referencedMessage?: Message | null;
Expand All @@ -3413,6 +3460,8 @@ declare namespace Dysnomia {
deleteWebhook(token: string): Promise<void>;
edit(content: MessageContent): Promise<Message<T>>;
editWebhook(token: string, options: MessageWebhookContent): Promise<Message<T>>;
endPoll(): Promise<Message<T>>;
getPollAnswerVoters(answerID: number, options?: GetPollAnswerVotersOptions): Promise<User[]>;
getReaction(reaction: string, options?: GetMessageReactionOptions): Promise<User[]>;
pin(): Promise<void>;
removeReaction(reaction: string, userID?: string): Promise<void>;
Expand Down
25 changes: 25 additions & 0 deletions lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ class Client extends EventEmitter {
* @arg {String} [content.messageReference.guildID] The guild ID of the referenced message
* @arg {String} content.messageReference.messageID The message ID of the referenced message. This cannot reference a system message
* @arg {String | Number} [content.nonce] A value that can be used to check if the message was sent
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Array<String>} [content.stickerIDs] An array of IDs corresponding to stickers to send
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise<Message>}
Expand Down Expand Up @@ -1958,6 +1959,16 @@ class Client extends EventEmitter {
return this.requestHandler.request("PATCH", Endpoints.WEBHOOK_MESSAGE(webhookID, token, messageID) + (qs ? "?" + qs : ""), false, options, files).then((response) => new Message(response, this));
}

/**
* Immediately ends a poll
* @param {String} channelID The ID of the channel the poll is in
* @param {String} messageID The ID of the message containing the poll
* @returns {Promise<Message>}
*/
endPoll(channelID, messageID) {
return this.requestHandler.request("POST", Endpoints.CHANNEL_POLL_EXPIRE(channelID, messageID), true).then((data) => new Message(data, this));
}

/**
* Execute a slack-style webhook
* @arg {String} webhookID The ID of the webhook
Expand Down Expand Up @@ -2615,6 +2626,20 @@ class Client extends EventEmitter {
return this.requestHandler.request("GET", Endpoints.CHANNEL_PINS(channelID), true).then((messages) => messages.map((message) => new Message(message, this)));
}

/**
* Gets a list of users that voted for an answer in a poll
* @param {String} channelID The ID of the channel the poll is in
* @param {String} messageID The ID of the message containing the poll
* @param {Number} answerID The ID of the answer
* @param {Object} [options] Options for fetching the answer list
* @param {String} [options.after] Get users after this user ID
* @param {Number} [options.limit=100] The maximum number of users to get
* @returns {Promise<Array<User>>}
*/
getPollAnswerVoters(channelID, messageID, answerID, options = {}) {
return this.requestHandler.request("GET", Endpoints.CHANNEL_POLL_ANSWERS(channelID, messageID, answerID), true, options).then((data) => data.users.map((user) => new User(user, this)));
}

/**
* Get the prune count for a guild
* @arg {String} guildID The ID of the guild
Expand Down
4 changes: 4 additions & 0 deletions lib/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,10 @@ Permissions.allVoice = Permissions.createInstantInvite
Permissions.all = Permissions.allGuild | Permissions.allText | Permissions.allVoice;
module.exports.Permissions = Permissions;

module.exports.PollLayoutTypes = {
DEFAULT: 1
};

module.exports.PremiumTiers = {
NONE: 0,
TIER_1: 1,
Expand Down
48 changes: 48 additions & 0 deletions lib/gateway/Shard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2312,6 +2312,54 @@ class Shard extends EventEmitter {
});
break;
}
case "MESSAGE_POLL_VOTE_ADD": {
const channel = this.client.getChannel(packet.d.channel_id);
let message = channel?.messages.get(packet.d.message_id);
const user = this.client.users.get(packet.d.user_id);
if(!message) {
message = {
id: packet.d.message_id,
channel: channel || {id: packet.d.channel_id}
};
if(packet.d.guild_id) {
message.guildID = packet.d.guild_id;
message.channel.guild ??= {id: packet.d.guild_id};
}
}
/**
* Fired when someone votes on a poll. If the poll allows multiple selection, this event will fire for each answer
* @event Client#messagePollVoteAdd
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with only an id. No other property is guaranteed
* @prop {Number} answerID The ID of the answer the user voted for
* @prop {User | Object} voter The user that voted. If the user is not cached, this will be an object with an `id` key. No other property is guaranteed
*/
this.emit("messagePollVoteAdd", message, packet.d.answer_id, user ?? {id: packet.d.user_id});
break;
}
case "MESSAGE_POLL_VOTE_REMOVE": {
const channel = this.client.getChannel(packet.d.channel_id);
let message = channel?.messages.get(packet.d.message_id);
const user = this.client.users.get(packet.d.user_id);
if(!message) {
message = {
id: packet.d.message_id,
channel: channel || {id: packet.d.channel_id}
};
if(packet.d.guild_id) {
message.guildID = packet.d.guild_id;
message.channel.guild ??= {id: packet.d.guild_id};
}
}
/**
* Fired when someone removes their vote on a poll. If the poll allows multiple selection, this event will fire for each answer
* @event Client#messagePollVoteRemove
* @prop {Message | Object} message The message object. If the message is not cached, this will be an object with `id` and `channel` keys. If the channel is not cached, channel key will be an object with only an id. No other property is guaranteed
* @prop {Number} answerID The ID of the answer the user voted for
* @prop {User | Object} voter The user that voted. If the user is not cached, this will be an object with an `id` key. No other property is guaranteed
*/
this.emit("messagePollVoteRemove", message, packet.d.answer_id, user ?? {id: packet.d.user_id});
break;
}
default: {
/**
* Fired when the shard encounters an unknown packet
Expand Down
2 changes: 2 additions & 0 deletions lib/rest/Endpoints.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/structures/CommandInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class CommandInteraction extends Interaction {
* @arg {String} [content.content] A content string
* @arg {Array<Object>} [options.embeds] An array of embed objects. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Number} [content.flags] A number representing the flags to apply. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for a list
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise<Message?>}
*/
Expand Down Expand Up @@ -148,6 +149,7 @@ class CommandInteraction extends Interaction {
* @arg {String} [content.content] A content string
* @arg {Array<Object>} [content.embeds] An array of embed objects. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Number} [content.flags] A number representing the flags to apply. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for a list
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise}
*/
Expand Down
2 changes: 2 additions & 0 deletions lib/structures/ComponentInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class ComponentInteraction extends Interaction {
* @arg {String} [content.content] A content string
* @arg {Array<Object>} [options.embeds] An array of embed objects. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Number} [content.flags] A number representing the flags to apply. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for a list
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise<Message?>}
*/
Expand Down Expand Up @@ -137,6 +138,7 @@ class ComponentInteraction extends Interaction {
* @arg {String} [content.content] A content string
* @arg {Array<Object>} [content.embeds] An array of embed objects. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Number} [content.flags] A number representing the flags to apply. See [Discord's Documentation](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for a list
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise}
*/
Expand Down
29 changes: 29 additions & 0 deletions lib/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,15 @@ class Message extends Base {
*/
this.position = data.position;
}

if(data.poll !== undefined) {
/**
* The poll data for the message. See [Discord's documentation](https://discord.com/developers/docs/resources/poll#poll-object-poll-object-structure)
* for more information about the structure.
* @type {Object?}
*/
this.poll = data.poll;
}
}

/**
Expand Down Expand Up @@ -655,6 +664,26 @@ class Message extends Base {
return this.#client.editWebhookMessage.call(this.#client, this.webhookID, token, this.id, options);
}

/**
* Immediately ends a poll associated with this message
* @returns {Promise<Message>}
*/
endPoll() {
return this.#client.endPoll.call(this.#client, this.channel.id, this.id);
}

/**
* Gets a list of users that voted for an answer in a poll
* @param {Number} answerID The ID of the answer
* @param {Object} [options] Options for fetching the answer list
* @param {String} [options.after] Get users after this user ID
* @param {Number} [options.limit=100] The maximum number of users to get
* @returns {Promise<Array<User>>}
*/
getPollAnswerVoters(answerID, options) {
return this.#client.getPollAnswerVoters.call(this.#client, this.channel.id, this.id, answerID, options);
}

/**
* Get a list of users who reacted with a specific reaction
* @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji)
Expand Down
2 changes: 2 additions & 0 deletions lib/structures/ModalSubmitInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ModalSubmitInteraction extends Interaction {
* @arg {String} [content.content] A content string
* @arg {Array<Object>} [options.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Number} [content.flags] 64 for Ephemeral
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise<Message?>}
*/
Expand Down Expand Up @@ -82,6 +83,7 @@ class ModalSubmitInteraction extends Interaction {
* @arg {String} [content.content] A content string
* @arg {Array<Object>} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Boolean} [content.flags] 64 for Ephemeral
* @arg {Object} [content.poll] A poll object. See [Discord's Documentation](https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure) for object structure
* @arg {Boolean} [content.tts] Set the message TTS flag
* @returns {Promise}
*/
Expand Down
Loading