feat: message poll

#1150 #1147
This commit is contained in:
Elysia
2024-07-03 21:31:45 +07:00
parent e6e9febe8c
commit 485dab55bf
16 changed files with 545 additions and 87 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
const { Client, MessagePoll } = require('../src/index');
const client = new Client();
client.on('ready', async () => {
console.log(`${client.user.username} is ready!`);
const channel = client.channels.cache.get('channel id');
const poll = new MessagePoll();
poll
.setQuestion('Test question')
.setAnswers([
{
text: 'answer 1',
emoji: {
name: '🎈',
},
},
{
text: 'answer 2',
emoji: {
name: '🎃',
},
},
{
text: 'answer 3',
},
])
.setAllowMultiSelect(true)
.setDuration(4); // 4h
const msg = await channel.send({
poll,
});
// Multi select
await msg.vote(1,3);
// End poll
await msg.endPoll();
});
client.login('token');

View File

@@ -0,0 +1,22 @@
'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);
};

View File

@@ -0,0 +1,12 @@
'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);
};

View File

@@ -10,7 +10,7 @@ module.exports = (client, { d: data }) => {
* Emitted when a relationship is removed, relevant to the current user. * Emitted when a relationship is removed, relevant to the current user.
* @event Client#relationshipRemove * @event Client#relationshipRemove
* @param {Snowflake} user The userID that was updated * @param {Snowflake} user The userID that was updated
* @param {RelationshipTypes} type The type of the old relationship * @param {RelationshipType} type The type of the old relationship
* @param {string | null} nickname The nickname of the user in this relationship (1-32 characters) * @param {string | null} nickname The nickname of the user in this relationship (1-32 characters)
*/ */
client.emit(Events.RELATIONSHIP_REMOVE, data.id, data.type, data.nickname); client.emit(Events.RELATIONSHIP_REMOVE, data.id, data.type, data.nickname);

View File

@@ -5,7 +5,7 @@ const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => { module.exports = (client, { d: data }) => {
/** /**
* @typedef {Object} RelationshipUpdateObject * @typedef {Object} RelationshipUpdateObject
* @property {RelationshipTypes} type The type of relationship * @property {RelationshipType} type The type of relationship
* @property {Date} since When the user requested a relationship * @property {Date} since When the user requested a relationship
* @property {string | null} nickname The nickname of the user in this relationship (1-32 characters) * @property {string | null} nickname The nickname of the user in this relationship (1-32 characters)
*/ */

View File

@@ -76,6 +76,8 @@ const handlers = Object.fromEntries([
['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')], ['USER_SETTINGS_UPDATE', require('./USER_SETTINGS_UPDATE')],
['USER_GUILD_SETTINGS_UPDATE', require('./USER_GUILD_SETTINGS_UPDATE')], ['USER_GUILD_SETTINGS_UPDATE', require('./USER_GUILD_SETTINGS_UPDATE')],
['VOICE_CHANNEL_STATUS_UPDATE', require('./VOICE_CHANNEL_STATUS_UPDATE')], ['VOICE_CHANNEL_STATUS_UPDATE', require('./VOICE_CHANNEL_STATUS_UPDATE')],
['MESSAGE_POLL_VOTE_ADD', require('./MESSAGE_POLL_VOTE_ADD')],
['MESSAGE_POLL_VOTE_REMOVE', require('./MESSAGE_POLL_VOTE_REMOVE')],
]); ]);
module.exports = handlers; module.exports = handlers;

View File

@@ -156,3 +156,4 @@ exports.SpotifyRPC = require('./structures/Presence').SpotifyRPC;
exports.WebEmbed = require('./structures/WebEmbed'); exports.WebEmbed = require('./structures/WebEmbed');
exports.DiscordAuthWebsocket = require('./util/RemoteAuth'); exports.DiscordAuthWebsocket = require('./util/RemoteAuth');
exports.PurchasedFlags = require('./util/PurchasedFlags'); exports.PurchasedFlags = require('./util/PurchasedFlags');
exports.MessagePoll = require('./structures/MessagePoll');

View File

@@ -16,7 +16,7 @@ class RelationshipManager extends BaseManager {
super(client); super(client);
/** /**
* A collection of users this manager is caching. (Type: Number) * A collection of users this manager is caching. (Type: Number)
* @type {Collection<Snowflake, RelationshipTypes>} * @type {Collection<Snowflake, RelationshipType>}
*/ */
this.cache = new Collection(); this.cache = new Collection();
/** /**
@@ -81,7 +81,7 @@ class RelationshipManager extends BaseManager {
/** /**
* @typedef {Object} RelationshipJSONData * @typedef {Object} RelationshipJSONData
* @property {Snowflake} id The ID of the target user * @property {Snowflake} id The ID of the target user
* @property {RelationshipTypes} type The type of relationship * @property {RelationshipType} type The type of relationship
* @property {string | null} nickname The nickname of the user in this relationship (1-32 characters) * @property {string | null} nickname The nickname of the user in this relationship (1-32 characters)
* @property {string} since When the user requested a relationship (ISO8601 timestamp) * @property {string} since When the user requested a relationship (ISO8601 timestamp)
*/ */
@@ -130,7 +130,7 @@ class RelationshipManager extends BaseManager {
* Obtains a user from Discord, or the user cache if it's already available. * Obtains a user from Discord, or the user cache if it's already available.
* @param {UserResolvable} [user] The user to fetch * @param {UserResolvable} [user] The user to fetch
* @param {BaseFetchOptions} [options] Additional options for this fetch * @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<RelationshipTypes|RelationshipManager>} * @returns {Promise<RelationshipType|RelationshipManager>}
*/ */
async fetch(user, { force = false } = {}) { async fetch(user, { force = false } = {}) {
if (user) { if (user) {

View File

@@ -9,6 +9,7 @@ const MessageAttachment = require('./MessageAttachment');
const Embed = require('./MessageEmbed'); const Embed = require('./MessageEmbed');
const Mentions = require('./MessageMentions'); const Mentions = require('./MessageMentions');
const MessagePayload = require('./MessagePayload'); const MessagePayload = require('./MessagePayload');
const MessagePoll = require('./MessagePoll');
const ReactionCollector = require('./ReactionCollector'); const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker'); const { Sticker } = require('./Sticker');
const Application = require('./interfaces/Application'); const Application = require('./interfaces/Application');
@@ -257,6 +258,16 @@ class Message extends Base {
this.webhookId ??= null; this.webhookId ??= null;
} }
/**
* A poll!
* @type {?MessagePoll}
*/
if ('poll' in data) {
this.poll = new MessagePoll(data.poll, this.client);
} else {
this.poll = null;
}
if ('application' in data) { if ('application' in data) {
/** /**
* Supplemental application information for group activities * Supplemental application information for group activities
@@ -846,6 +857,55 @@ class Message extends Base {
return this.channel.threads.create({ ...options, startMessage: this }); return this.channel.threads.create({ ...options, startMessage: this });
} }
/**
* Submits a poll vote for the current user. Returns a 204 empty response on success.
* @param {...number[]} ids ID of the answer
* @returns {Promise<void>}
* @example
* // Vote multi choices
* message.vote(1,2);
* // Remove vote
* message.vote();
*/
vote(...ids) {
return this.client.api
.channels(this.channel.id)
.polls(this.id)
.answers['@me'].put({
data: {
answer_ids: ids.flat(1).map(value => value.toString()),
},
});
}
/**
* Immediately ends the poll. You cannot end polls from other users.
* @returns {Promise<RawMessage>}
*/
endPoll() {
return this.client.api.channels(this.channel.id).polls(this.id).expire.post();
}
/**
* 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<{ users: Partial<RawUser> }>}
*/
getAnswerVoter(answerId, afterUserId, limit = 25) {
return this.client.api
.channels(this.channel.id)
.polls(this.id)
.answers(answerId)
.get({
query: {
after: afterUserId,
limit,
},
});
}
/** /**
* Fetch this message. * Fetch this message.
* @param {boolean} [force=true] Whether to skip the cache check and request the API * @param {boolean} [force=true] Whether to skip the cache check and request the API

View File

@@ -3,6 +3,7 @@
const { Buffer } = require('node:buffer'); const { Buffer } = require('node:buffer');
const BaseMessageComponent = require('./BaseMessageComponent'); const BaseMessageComponent = require('./BaseMessageComponent');
const MessageEmbed = require('./MessageEmbed'); const MessageEmbed = require('./MessageEmbed');
const MessagePoll = require('./MessagePoll');
const { RangeError } = require('../errors'); const { RangeError } = require('../errors');
const ActivityFlags = require('../util/ActivityFlags'); const ActivityFlags = require('../util/ActivityFlags');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
@@ -230,6 +231,7 @@ class MessagePayload {
attachments: this.options.attachments, attachments: this.options.attachments,
sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker), sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
thread_name: threadName, thread_name: threadName,
poll: this.options.poll instanceof MessagePoll ? this.options.poll.toJSON() : this.options.poll,
}; };
return this; return this;
} }

View File

@@ -0,0 +1,238 @@
'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;

View File

@@ -75,6 +75,7 @@ class TextBasedChannel {
* @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components] * @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components]
* Action rows containing interactive components for the message (buttons, select menus) * Action rows containing interactive components for the message (buttons, select menus)
* @property {MessageAttachment[]} [attachments] Attachments to send in the message * @property {MessageAttachment[]} [attachments] Attachments to send in the message
* @property {MessagePoll} [poll] A poll!
*/ */
/** /**

View File

@@ -9,7 +9,7 @@ const { Error, RangeError, TypeError } = require('../errors');
exports.MaxBulkDeletableMessageAge = 1_209_600_000; exports.MaxBulkDeletableMessageAge = 1_209_600_000;
exports.UserAgent = exports.UserAgent =
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.215 Electron/22.3.26 Safari/537.36'; 'Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.215 Safari/537.36';
/** /**
* Google Chrome v108 TLS ciphers * Google Chrome v108 TLS ciphers
@@ -98,6 +98,7 @@ exports.Endpoints = {
}, },
AvatarDecoration: (userId, hash, format = 'png', size) => AvatarDecoration: (userId, hash, format = 'png', size) =>
makeImageUrl(`${root}/avatar-decorations/${userId}/${hash}`, { format, size }), makeImageUrl(`${root}/avatar-decorations/${userId}/${hash}`, { format, size }),
ClanBadge: (guildId, hash) => `${root}/clan-badges/${guildId}/${hash}.png`,
GuildMemberAvatar: (guildId, memberId, hash, format = 'webp', size, dynamic = false) => { GuildMemberAvatar: (guildId, memberId, hash, format = 'webp', size, dynamic = false) => {
if (dynamic && hash.startsWith('a_')) format = 'gif'; if (dynamic && hash.startsWith('a_')) format = 'gif';
return makeImageUrl(`${root}/guilds/${guildId}/users/${memberId}/avatars/${hash}`, { format, size }); return makeImageUrl(`${root}/guilds/${guildId}/users/${memberId}/avatars/${hash}`, { format, size });
@@ -430,6 +431,8 @@ exports.Events = {
CALL_CREATE: 'callCreate', CALL_CREATE: 'callCreate',
CALL_UPDATE: 'callUpdate', CALL_UPDATE: 'callUpdate',
CALL_DELETE: 'callDelete', CALL_DELETE: 'callDelete',
MESSAGE_POLL_VOTE_ADD: 'messagePollVoteAdd',
MESSAGE_POLL_VOTE_REMOVE: 'messagePollVoteRemove',
}; };
/** /**
@@ -1649,6 +1652,15 @@ exports.SortOrderTypes = createEnum([null, 'LATEST_ACTIVITY', 'CREATION_DATE']);
*/ */
exports.ForumLayoutTypes = createEnum(['NOT_SET', 'LIST_VIEW', 'GALLERY_VIEW']); 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
* @see {@link https://docs.discord.sex/resources/message#poll-layout-type}
*/
exports.MessagePollLayoutTypes = createEnum([null, 'DEFAULT', 'IMAGE_ONLY_ANSWERS']);
/** /**
* Relationship Enums: * Relationship Enums:
* * 0: NONE * * 0: NONE
@@ -1657,7 +1669,7 @@ exports.ForumLayoutTypes = createEnum(['NOT_SET', 'LIST_VIEW', 'GALLERY_VIEW']);
* * 3: PENDING_INCOMING * * 3: PENDING_INCOMING
* * 4: PENDING_OUTGOING * * 4: PENDING_OUTGOING
* * 5: IMPLICIT * * 5: IMPLICIT
* @typedef {string} RelationshipTypes * @typedef {string} RelationshipType
* @see {@link https://luna.gitlab.io/discord-unofficial-docs/relationships.html} * @see {@link https://luna.gitlab.io/discord-unofficial-docs/relationships.html}
*/ */
@@ -1728,7 +1740,7 @@ function createEnum(keys) {
* @property {Object<InteractionResponseType, number>} InteractionResponseTypes The type of an interaction response. * @property {Object<InteractionResponseType, number>} InteractionResponseTypes The type of an interaction response.
* @property {Object<InteractionType, number>} InteractionTypes The type of an {@link Interaction} object. * @property {Object<InteractionType, number>} InteractionTypes The type of an {@link Interaction} object.
* @property {InviteScope[]} InviteScopes The scopes of an invite. * @property {InviteScope[]} InviteScopes The scopes of an invite.
* @property {Object<RelationshipTypes, number>} RelationshipTypes Relationship Enums * @property {Object<RelationshipType, number>} RelationshipTypes Relationship Enums
* @property {Object<MembershipState, number>} MembershipStates The value set for a team members membership state. * @property {Object<MembershipState, number>} MembershipStates The value set for a team members membership state.
* @property {Object<MessageButtonStyle, number>} MessageButtonStyles The style of a message button. * @property {Object<MessageButtonStyle, number>} MessageButtonStyles The style of a message button.
* @property {Object<MessageComponentType, number>} MessageComponentTypes The type of a message component. * @property {Object<MessageComponentType, number>} MessageComponentTypes The type of a message component.

7
typings/enums.d.ts vendored
View File

@@ -87,6 +87,11 @@ export const enum ForumLayoutType {
GALLERY_VIEW = 2, GALLERY_VIEW = 2,
} }
export const enum MessagePollLayoutType {
DEFAULT = 1,
IMAGE_ONLY_ANSWERS,
}
export const enum MessageTypes { export const enum MessageTypes {
DEFAULT, DEFAULT,
RECIPIENT_ADD, RECIPIENT_ADD,
@@ -282,7 +287,7 @@ export enum ApplicationRoleConnectionMetadataTypes {
BOOLEAN_NOT_EQUAL, BOOLEAN_NOT_EQUAL,
} }
export const enum RelationshipTypes { export const enum RelationshipType {
NONE = 0, NONE = 0,
FRIEND = 1, FRIEND = 1,
BLOCKED = 2, BLOCKED = 2,

108
typings/index.d.ts vendored
View File

@@ -98,9 +98,10 @@ import {
SortOrderType, SortOrderType,
ForumLayoutType, ForumLayoutType,
ApplicationRoleConnectionMetadataTypes, ApplicationRoleConnectionMetadataTypes,
RelationshipTypes, RelationshipType,
SelectMenuComponentTypes, SelectMenuComponentTypes,
InviteType, InviteType,
MessagePollLayoutType,
} from './enums'; } from './enums';
import { import {
APIApplicationRoleConnectionMetadata, APIApplicationRoleConnectionMetadata,
@@ -1550,7 +1551,7 @@ export class HTTPError extends Error {
} }
// tslint:disable-next-line:no-empty-interface - Merge RateLimitData into RateLimitError to not have to type it again // tslint:disable-next-line:no-empty-interface - Merge RateLimitData into RateLimitError to not have to type it again
export interface RateLimitError extends RateLimitData {} export interface RateLimitError extends RateLimitData { }
export class RateLimitError extends Error { export class RateLimitError extends Error {
private constructor(data: RateLimitData); private constructor(data: RateLimitData);
public name: 'RateLimitError'; public name: 'RateLimitError';
@@ -1846,6 +1847,7 @@ export class Message<Cached extends boolean = boolean> extends Base {
public type: MessageType; public type: MessageType;
public readonly url: string; public readonly url: string;
public webhookId: Snowflake | null; public webhookId: Snowflake | null;
public poll: MessagePoll | null;
public flags: Readonly<MessageFlags>; public flags: Readonly<MessageFlags>;
public reference: MessageReference | null; public reference: MessageReference | null;
public position: number | null; public position: number | null;
@@ -1879,6 +1881,9 @@ export class Message<Cached extends boolean = boolean> extends Base {
public markUnread(): Promise<void>; public markUnread(): Promise<void>;
public markRead(): Promise<void>; public markRead(): Promise<void>;
public report(breadcrumbs: number[], elements?: object): Promise<{ report_id: Snowflake }>; public report(breadcrumbs: number[], elements?: object): Promise<{ report_id: Snowflake }>;
public vote(...ids: number[]): Promise<void>;
public endPoll(): Promise<RawMessageData>;
public getAnswerVoter(answerId: number, afterUserId?: Snowflake, limit?: number): Promise<{ users: Partial<RawUserData> }>;
} }
export class CallState extends Base { export class CallState extends Base {
@@ -1890,6 +1895,14 @@ export class CallState extends Base {
public setRTCRegion(): Promise<void>; public setRTCRegion(): Promise<void>;
} }
export interface MessagePollUserVote {
user_id: Snowflake;
message_id: Snowflake;
channel_id: Snowflake;
answer_id: number;
guild_id?: Snowflake;
}
export class MessageActionRow< export class MessageActionRow<
T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent, T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent,
U = T extends ModalActionRowComponent ? ModalActionRowComponentResolvable : MessageActionRowComponentResolvable, U = T extends ModalActionRowComponent ? ModalActionRowComponentResolvable : MessageActionRowComponentResolvable,
@@ -2263,6 +2276,39 @@ export class Modal {
public toJSON(): RawModalSubmitInteractionData; public toJSON(): RawModalSubmitInteractionData;
} }
export interface MessagePollMedia {
text?: string;
emoji?: RawEmojiData;
}
export interface MessagePollResultAnswerCount {
answer: MessagePollMedia;
count: number;
selfVoted: boolean;
}
export interface MessagePollResult {
isFinalized: boolean;
answerCounts: Collection<number, MessagePollResultAnswerCount>;
}
export class MessagePoll {
public constructor(data: MessagePoll | object);
public question: MessagePollMedia | null;
public answers: Collection<number, MessagePollMedia>;
public layoutType: MessagePollLayoutType | null;
public allowMultiSelect: boolean;
public expiry: Date | null;
public results: MessagePollResult | null;
public duration: number | null;
public toJSON(): object;
public setQuestion(text: string): this;
public setAnswers(answers: MessagePollMedia[]): this;
public addAnswer(answer: MessagePollMedia): this;
public setAllowMultiSelect(state: boolean): this;
public setDuration(duration: number): this;
}
export class ModalSubmitFieldsResolver { export class ModalSubmitFieldsResolver {
constructor(components: PartialModalActionRow[]); constructor(components: PartialModalActionRow[]);
private readonly _fields: PartialTextInputData[]; private readonly _fields: PartialTextInputData[];
@@ -3063,6 +3109,13 @@ export class Typing extends Base {
}; };
} }
export interface UserClan {
identityGuildId?: Snowflake;
identityEnabled?: boolean;
tag?: string;
badge?: string;
}
export class User extends PartialTextBasedChannel(Base) { export class User extends PartialTextBasedChannel(Base) {
protected constructor(client: Client, data: RawUserData); protected constructor(client: Client, data: RawUserData);
private _equals(user: APIUser): boolean; private _equals(user: APIUser): boolean;
@@ -3090,11 +3143,13 @@ export class User extends PartialTextBasedChannel(Base) {
public username: string; public username: string;
public readonly note: string | undefined; public readonly note: string | undefined;
public readonly voice?: VoiceState; public readonly voice?: VoiceState;
public readonly relationship: RelationshipTypes; public readonly relationship: RelationshipType;
public readonly friendNickname: string | null | undefined; public readonly friendNickname: string | null | undefined;
public clan: UserClan | null;
public avatarURL(options?: ImageURLOptions): string | null; public avatarURL(options?: ImageURLOptions): string | null;
public avatarDecorationURL(options?: StaticImageURLOptions): string | null; public avatarDecorationURL(options?: StaticImageURLOptions): string | null;
public bannerURL(options?: ImageURLOptions): string | null; public bannerURL(options?: ImageURLOptions): string | null;
public clanBadgeURL(): string | null;
public createDM(force?: boolean): Promise<DMChannel>; public createDM(force?: boolean): Promise<DMChannel>;
public deleteDM(): Promise<DMChannel>; public deleteDM(): Promise<DMChannel>;
public displayAvatarURL(options?: ImageURLOptions): string; public displayAvatarURL(options?: ImageURLOptions): string;
@@ -3484,6 +3539,7 @@ export const Constants: {
dynamic: boolean, dynamic: boolean,
): string; ): string;
AvatarDecoration(userId: Snowflake, hash: string, format: AllowedImageFormat, size: AllowedImageSize): string; AvatarDecoration(userId: Snowflake, hash: string, format: AllowedImageFormat, size: AllowedImageSize): string;
ClanBadge(guildId: Snowflake, hash: string): string;
Banner(id: Snowflake, hash: string, format: DynamicImageFormat, size: AllowedImageSize, dynamic: boolean): string; Banner(id: Snowflake, hash: string, format: DynamicImageFormat, size: AllowedImageSize, dynamic: boolean): string;
DefaultAvatar(index: number): string; DefaultAvatar(index: number): string;
DiscoverySplash(guildId: Snowflake, hash: string, format: AllowedImageFormat, size: AllowedImageSize): string; DiscoverySplash(guildId: Snowflake, hash: string, format: AllowedImageFormat, size: AllowedImageSize): string;
@@ -3525,7 +3581,7 @@ export const Constants: {
GuildScheduledEventStatuses: EnumHolder<typeof GuildScheduledEventStatuses>; GuildScheduledEventStatuses: EnumHolder<typeof GuildScheduledEventStatuses>;
IntegrationExpireBehaviors: IntegrationExpireBehaviors[]; IntegrationExpireBehaviors: IntegrationExpireBehaviors[];
SelectMenuComponentTypes: EnumHolder<typeof SelectMenuComponentTypes>; SelectMenuComponentTypes: EnumHolder<typeof SelectMenuComponentTypes>;
RelationshipTypes: EnumHolder<typeof RelationshipTypes>; RelationshipTypes: EnumHolder<typeof RelationshipType>;
MembershipStates: EnumHolder<typeof MembershipStates>; MembershipStates: EnumHolder<typeof MembershipStates>;
MessageButtonStyles: EnumHolder<typeof MessageButtonStyles>; MessageButtonStyles: EnumHolder<typeof MessageButtonStyles>;
MessageComponentTypes: EnumHolder<typeof MessageComponentTypes>; MessageComponentTypes: EnumHolder<typeof MessageComponentTypes>;
@@ -3725,22 +3781,22 @@ export class RelationshipManager extends BaseManager {
client: Client, client: Client,
data: { data: {
user: RawUserData; user: RawUserData;
type: RelationshipTypes; type: RelationshipType;
since?: string; since?: string;
nickname: string | null | undefined; nickname: string | null | undefined;
id: Snowflake; id: Snowflake;
}[], }[],
); );
public cache: Collection<Snowflake, RelationshipTypes>; public cache: Collection<Snowflake, RelationshipType>;
public friendNicknames: Collection<Snowflake, string | null>; public friendNicknames: Collection<Snowflake, string | null>;
public sinceCache: Collection<Snowflake, Date>; public sinceCache: Collection<Snowflake, Date>;
public readonly friendCache: Collection<Snowflake, User>; public readonly friendCache: Collection<Snowflake, User>;
public readonly blockedCache: Collection<Snowflake, User>; public readonly blockedCache: Collection<Snowflake, User>;
public readonly incomingCache: Collection<Snowflake, User>; public readonly incomingCache: Collection<Snowflake, User>;
public readonly outgoingCache: Collection<Snowflake, User>; public readonly outgoingCache: Collection<Snowflake, User>;
public toJSON(): { type: RelationshipTypes; since: string; nickname: string | null | undefined; id: Snowflake }[]; public toJSON(): { type: RelationshipType; since: string; nickname: string | null | undefined; id: Snowflake }[];
public resolveId(user: UserResolvable): Snowflake | undefined; public resolveId(user: UserResolvable): Snowflake | undefined;
public fetch(user?: UserResolvable, options?: BaseFetchOptions): Promise<RelationshipTypes | RelationshipManager>; public fetch(user?: UserResolvable, options?: BaseFetchOptions): Promise<RelationshipType | RelationshipManager>;
public deleteRelationship(user: UserResolvable): Promise<boolean>; public deleteRelationship(user: UserResolvable): Promise<boolean>;
public sendFriendRequest(options: FriendRequestOptions): Promise<boolean>; public sendFriendRequest(options: FriendRequestOptions): Promise<boolean>;
public addFriend(user: UserResolvable): Promise<boolean>; public addFriend(user: UserResolvable): Promise<boolean>;
@@ -4904,9 +4960,9 @@ export interface AutoModerationRuleCreateOptions {
reason?: string; reason?: string;
} }
export interface AutoModerationRuleEditOptions extends Partial<Omit<AutoModerationRuleCreateOptions, 'triggerType'>> {} export interface AutoModerationRuleEditOptions extends Partial<Omit<AutoModerationRuleCreateOptions, 'triggerType'>> { }
export interface AutoModerationTriggerMetadataOptions extends Partial<AutoModerationTriggerMetadata> {} export interface AutoModerationTriggerMetadataOptions extends Partial<AutoModerationTriggerMetadata> { }
export interface AutoModerationActionOptions { export interface AutoModerationActionOptions {
type: AutoModerationActionType | AutoModerationActionTypes; type: AutoModerationActionType | AutoModerationActionTypes;
@@ -5207,18 +5263,18 @@ export interface ClientEvents extends BaseClientEvents {
guildAuditLogEntryCreate: [auditLogEntry: GuildAuditLogsEntry, guild: Guild]; guildAuditLogEntryCreate: [auditLogEntry: GuildAuditLogsEntry, guild: Guild];
unhandledPacket: [packet: { t?: string; d: any }, shard: number]; unhandledPacket: [packet: { t?: string; d: any }, shard: number];
relationshipAdd: [userId: Snowflake, shouldNotify: boolean]; relationshipAdd: [userId: Snowflake, shouldNotify: boolean];
relationshipRemove: [userId: Snowflake, type: RelationshipTypes, nickname: string | null]; relationshipRemove: [userId: Snowflake, type: RelationshipType, nickname: string | null];
relationshipUpdate: [ relationshipUpdate: [
userId: Snowflake, userId: Snowflake,
oldData: { oldData: {
nickname: string | null; nickname: string | null;
since: Date; since: Date;
type: RelationshipTypes; type: RelationshipType;
}, },
newData: { newData: {
nickname: string | null; nickname: string | null;
since: Date; since: Date;
type: RelationshipTypes; type: RelationshipType;
}, },
]; ];
channelRecipientAdd: [channel: GroupDMChannel, user: User]; channelRecipientAdd: [channel: GroupDMChannel, user: User];
@@ -5227,6 +5283,8 @@ export interface ClientEvents extends BaseClientEvents {
callCreate: [call: CallState]; callCreate: [call: CallState];
callUpdate: [call: CallState]; callUpdate: [call: CallState];
callDelete: [call: CallState]; callDelete: [call: CallState];
messagePollVoteAdd: [data: MessagePollUserVote];
messagePollVoteRemove: [data: MessagePollUserVote];
} }
export interface ClientFetchInviteOptions { export interface ClientFetchInviteOptions {
@@ -5508,6 +5566,8 @@ export interface ConstantsEvents {
CALL_CREATE: 'callCreate'; CALL_CREATE: 'callCreate';
CALL_UPDATE: 'callUpdate'; CALL_UPDATE: 'callUpdate';
CALL_DELETE: 'callDelete'; CALL_DELETE: 'callDelete';
MESSAGE_POLL_VOTE_ADD: 'messagePollVoteAdd';
MESSAGE_POLL_VOTE_REMOVE: 'messagePollVoteRemove';
} }
export interface ConstantsOpcodes { export interface ConstantsOpcodes {
@@ -5975,8 +6035,8 @@ export interface GuildAuditLogsEntryTargetField<TActionType extends GuildAuditLo
INVITE: Invite; INVITE: Invite;
MESSAGE: TActionType extends 'MESSAGE_BULK_DELETE' ? Guild | { id: Snowflake } : User; MESSAGE: TActionType extends 'MESSAGE_BULK_DELETE' ? Guild | { id: Snowflake } : User;
INTEGRATION: Integration; INTEGRATION: Integration;
CHANNEL: NonThreadGuildBasedChannel | { id: Snowflake; [x: string]: unknown }; CHANNEL: NonThreadGuildBasedChannel | { id: Snowflake;[x: string]: unknown };
THREAD: ThreadChannel | { id: Snowflake; [x: string]: unknown }; THREAD: ThreadChannel | { id: Snowflake;[x: string]: unknown };
STAGE_INSTANCE: StageInstance; STAGE_INSTANCE: StageInstance;
STICKER: Sticker; STICKER: Sticker;
GUILD_SCHEDULED_EVENT: GuildScheduledEvent; GUILD_SCHEDULED_EVENT: GuildScheduledEvent;
@@ -6581,6 +6641,7 @@ export interface MessageOptions {
stickers?: StickerResolvable[]; stickers?: StickerResolvable[];
attachments?: MessageAttachment[]; attachments?: MessageAttachment[];
flags?: BitFieldResolvable<'SUPPRESS_EMBEDS' | 'SUPPRESS_NOTIFICATIONS' | 'IS_VOICE_MESSAGE', number>; flags?: BitFieldResolvable<'SUPPRESS_EMBEDS' | 'SUPPRESS_NOTIFICATIONS' | 'IS_VOICE_MESSAGE', number>;
poll?: MessagePoll;
} }
export type MessageReactionResolvable = MessageReaction | Snowflake | string; export type MessageReactionResolvable = MessageReaction | Snowflake | string;
@@ -6756,9 +6817,14 @@ export type PermissionString =
| 'MANAGE_EVENTS' | 'MANAGE_EVENTS'
| 'VIEW_CREATOR_MONETIZATION_ANALYTICS' | 'VIEW_CREATOR_MONETIZATION_ANALYTICS'
| 'USE_SOUNDBOARD' | 'USE_SOUNDBOARD'
| 'CREATE_GUILD_EXPRESSIONS'
| 'CREATE_EVENTS'
| 'USE_EXTERNAL_SOUNDS'
| 'SEND_VOICE_MESSAGES' | 'SEND_VOICE_MESSAGES'
| 'USE_CLYDE_AI' | 'USE_CLYDE_AI'
| 'SET_VOICE_CHANNEL_STATUS'; | 'SET_VOICE_CHANNEL_STATUS'
| 'SEND_POLLS'
| 'USE_EXTERNAL_APPS';
export type RecursiveArray<T> = ReadonlyArray<T | RecursiveArray<T>>; export type RecursiveArray<T> = ReadonlyArray<T | RecursiveArray<T>>;
@@ -6819,18 +6885,18 @@ export type Partialize<
partial: true; partial: true;
} & { } & {
[K in keyof Omit<T, 'client' | 'id' | 'partial' | E>]: K extends N ? null : K extends M ? T[K] | null : T[K]; [K in keyof Omit<T, 'client' | 'id' | 'partial' | E>]: K extends N ? null : K extends M ? T[K] | null : T[K];
}; };
export interface PartialDMChannel extends Partialize<DMChannel, null, null, 'lastMessageId'> { export interface PartialDMChannel extends Partialize<DMChannel, null, null, 'lastMessageId'> {
lastMessageId: undefined; lastMessageId: undefined;
} }
export interface PartialGuildMember extends Partialize<GuildMember, 'joinedAt' | 'joinedTimestamp'> {} export interface PartialGuildMember extends Partialize<GuildMember, 'joinedAt' | 'joinedTimestamp'> { }
export interface PartialMessage export interface PartialMessage
extends Partialize<Message, 'type' | 'system' | 'pinned' | 'tts', 'content' | 'cleanContent' | 'author'> {} extends Partialize<Message, 'type' | 'system' | 'pinned' | 'tts', 'content' | 'cleanContent' | 'author'> { }
export interface PartialMessageReaction extends Partialize<MessageReaction, 'count'> {} export interface PartialMessageReaction extends Partialize<MessageReaction, 'count'> { }
export interface PartialOverwriteData { export interface PartialOverwriteData {
id: Snowflake | number; id: Snowflake | number;
@@ -6845,7 +6911,7 @@ export interface PartialRoleData extends RoleData {
export type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' | 'MESSAGE' | 'REACTION' | 'GUILD_SCHEDULED_EVENT'; export type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' | 'MESSAGE' | 'REACTION' | 'GUILD_SCHEDULED_EVENT';
export interface PartialUser extends Partialize<User, 'username' | 'tag' | 'discriminator'> {} export interface PartialUser extends Partialize<User, 'username' | 'tag' | 'discriminator'> { }
export type PresenceStatusData = ClientPresenceStatus | 'invisible'; export type PresenceStatusData = ClientPresenceStatus | 'invisible';