feat: add polls (#188)

This commit is contained in:
Marcos Susaña 2024-04-22 15:53:38 -04:00 committed by GitHub
parent d55e904366
commit 9e54231d02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1224 additions and 881 deletions

View File

@ -21,7 +21,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"discord-api-types": "^0.37.76", "discord-api-types": "^0.37.80",
"magic-bytes.js": "^1.10.0", "magic-bytes.js": "^1.10.0",
"ts-mixer": "^6.0.4", "ts-mixer": "^6.0.4",
"ws": "^8.16.0" "ws": "^8.16.0"

1934
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,8 @@ import type {
RESTGetAPIChannelThreadsArchivedQuery, RESTGetAPIChannelThreadsArchivedQuery,
RESTGetAPIChannelUsersThreadsArchivedResult, RESTGetAPIChannelUsersThreadsArchivedResult,
RESTGetAPIGuildWebhooksResult, RESTGetAPIGuildWebhooksResult,
RESTGetAPIPollAnswerVotersQuery,
RESTGetAPIPollAnswerVotersResult,
RESTPatchAPIChannelJSONBody, RESTPatchAPIChannelJSONBody,
RESTPatchAPIChannelMessageJSONBody, RESTPatchAPIChannelMessageJSONBody,
RESTPatchAPIChannelMessageResult, RESTPatchAPIChannelMessageResult,
@ -45,6 +47,7 @@ import type {
RESTPostAPIChannelWebhookJSONBody, RESTPostAPIChannelWebhookJSONBody,
RESTPostAPIChannelWebhookResult, RESTPostAPIChannelWebhookResult,
RESTPostAPIGuildForumThreadsJSONBody, RESTPostAPIGuildForumThreadsJSONBody,
RESTPostAPIPollExpireResult,
RESTPutAPIChannelMessageReactionResult, RESTPutAPIChannelMessageReactionResult,
RESTPutAPIChannelPermissionJSONBody, RESTPutAPIChannelPermissionJSONBody,
RESTPutAPIChannelPermissionResult, RESTPutAPIChannelPermissionResult,
@ -256,5 +259,17 @@ export interface ChannelRoutes {
'voice-status': { 'voice-status': {
put(args: RestArguments<ProxyRequestMethod.Put, { status: string | null }>): Promise<never>; put(args: RestArguments<ProxyRequestMethod.Put, { status: string | null }>): Promise<never>;
}; };
polls(messageId: string): {
answers(id: ValidAnswerId): {
get(
args?: RestArguments<ProxyRequestMethod.Get, never, RESTGetAPIPollAnswerVotersQuery>,
): Promise<RESTGetAPIPollAnswerVotersResult>;
};
expire: {
post(args?: RestArguments<ProxyRequestMethod.Post>): Promise<RESTPostAPIPollExpireResult>;
};
};
}; };
} }
export type ValidAnswerId = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

51
src/builders/Poll.ts Normal file
View File

@ -0,0 +1,51 @@
import { type APIPollMedia, PollLayoutType, type RESTAPIPollCreate } from 'discord-api-types/v10';
import type { DeepPartial, EmojiResolvable, RestOrArray } from '../common';
import { throwError } from '..';
import { resolvePartialEmoji } from '../structures/extra/functions';
export class PollBuilder {
constructor(
public data: DeepPartial<Omit<RESTAPIPollCreate, 'answers'> & { answers: { media: APIPollMedia }[] }> = {},
) {
this.data.layout_type = PollLayoutType.Default;
}
addAnswers(...answers: RestOrArray<PollMedia>) {
this.data.answers = (this.data.answers ?? []).concat(
answers.flat().map(x => ({ media: this.resolvedPollMedia(x) })),
);
return this;
}
setAnswers(...answers: RestOrArray<PollMedia>) {
this.data.answers = answers.flat().map(x => ({ media: this.resolvedPollMedia(x) }));
return this;
}
setQuestion(data: PollMedia) {
this.data.question ??= {};
const { emoji, text } = this.resolvedPollMedia(data);
this.data.question.text = text;
this.data.question.emoji = emoji;
return this;
}
setDuration(hours: number) {
this.data.duration = hours;
return this;
}
allowMultiselect(value = true) {
this.data.allow_multiselect = value;
return this;
}
private resolvedPollMedia(data: PollMedia) {
if (!data.emoji) return { text: data.text };
const resolve = resolvePartialEmoji(data.emoji);
if (!resolve) return throwError('Invalid Emoji');
return { text: data.text, emoji: resolve };
}
}
export type PollMedia = { text?: string; emoji?: EmojiResolvable };

View File

@ -18,6 +18,7 @@ export * from './Button';
export * from './Embed'; export * from './Embed';
export * from './Modal'; export * from './Modal';
export * from './SelectMenu'; export * from './SelectMenu';
export * from './Poll';
export * from './types'; export * from './types';
export function fromComponent( export function fromComponent(

View File

@ -4,10 +4,11 @@ import type {
RESTPostAPIChannelMessagesThreadsJSONBody, RESTPostAPIChannelMessagesThreadsJSONBody,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import { resolveFiles } from '../../builders'; import { resolveFiles } from '../../builders';
import { Message, MessagesMethods } from '../../structures'; import { Message, MessagesMethods, User } from '../../structures';
import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../types/write'; import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../types/write';
import { BaseShorter } from './base'; import { BaseShorter } from './base';
import type { ValidAnswerId } from '../../api/Routes/channels';
export class MessageShorter extends BaseShorter { export class MessageShorter extends BaseShorter {
async write(channelId: string, { files, ...body }: MessageCreateBodyRequest) { async write(channelId: string, { files, ...body }: MessageCreateBodyRequest) {
@ -88,4 +89,21 @@ export class MessageShorter extends BaseShorter {
) { ) {
return this.client.threads.fromMessage(channelId, messageId, options); return this.client.threads.fromMessage(channelId, messageId, options);
} }
endPoll(channelId: string, messageId: string) {
return this.client.proxy
.channels(channelId)
.polls(messageId)
.expire.post()
.then(message => new Message(this.client, message));
}
getAnswerVoters(channelId: string, messageId: string, answerId: ValidAnswerId) {
return this.client.proxy
.channels(channelId)
.polls(messageId)
.answers(answerId)
.get()
.then(data => data.users.map(user => new User(this.client, user)));
}
} }

View File

@ -5,13 +5,22 @@ import type {
APIInteractionResponseChannelMessageWithSource, APIInteractionResponseChannelMessageWithSource,
APIMessageActionRowComponent, APIMessageActionRowComponent,
APIModalInteractionResponse, APIModalInteractionResponse,
RESTAPIPollCreate,
RESTPatchAPIChannelMessageJSONBody, RESTPatchAPIChannelMessageJSONBody,
RESTPatchAPIWebhookWithTokenMessageJSONBody, RESTPatchAPIWebhookWithTokenMessageJSONBody,
RESTPostAPIChannelMessageJSONBody, RESTPostAPIChannelMessageJSONBody,
RESTPostAPIWebhookWithTokenJSONBody, RESTPostAPIWebhookWithTokenJSONBody,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import type { RawFile } from '../../api'; import type { RawFile } from '../../api';
import type { ActionRow, Attachment, AttachmentBuilder, BuilderComponents, Embed, Modal } from '../../builders'; import type {
ActionRow,
Attachment,
AttachmentBuilder,
BuilderComponents,
Embed,
Modal,
PollBuilder,
} from '../../builders';
import type { OmitInsert } from './util'; import type { OmitInsert } from './util';
@ -21,10 +30,14 @@ export interface ResolverProps {
files?: AttachmentBuilder[] | Attachment[] | RawFile[] | undefined; files?: AttachmentBuilder[] | Attachment[] | RawFile[] | undefined;
} }
export interface SendResolverProps extends ResolverProps {
poll?: PollBuilder | RESTAPIPollCreate | undefined;
}
export type MessageCreateBodyRequest = OmitInsert< export type MessageCreateBodyRequest = OmitInsert<
RESTPostAPIChannelMessageJSONBody, RESTPostAPIChannelMessageJSONBody,
'components' | 'embeds', 'components' | 'embeds' | 'poll',
ResolverProps SendResolverProps
>; >;
export type MessageUpdateBodyRequest = OmitInsert< export type MessageUpdateBodyRequest = OmitInsert<
@ -35,8 +48,8 @@ export type MessageUpdateBodyRequest = OmitInsert<
export type MessageWebhookCreateBodyRequest = OmitInsert< export type MessageWebhookCreateBodyRequest = OmitInsert<
RESTPostAPIWebhookWithTokenJSONBody, RESTPostAPIWebhookWithTokenJSONBody,
'components' | 'embeds', 'components' | 'embeds' | 'poll',
ResolverProps SendResolverProps
>; >;
export type MessageWebhookUpdateBodyRequest = OmitInsert< export type MessageWebhookUpdateBodyRequest = OmitInsert<

View File

@ -3,6 +3,7 @@ import type {
GatewayMessageCreateDispatchData, GatewayMessageCreateDispatchData,
GatewayMessageDeleteBulkDispatchData, GatewayMessageDeleteBulkDispatchData,
GatewayMessageDeleteDispatchData, GatewayMessageDeleteDispatchData,
GatewayMessagePollVoteDispatchData,
GatewayMessageReactionAddDispatchData, GatewayMessageReactionAddDispatchData,
GatewayMessageReactionRemoveAllDispatchData, GatewayMessageReactionRemoveAllDispatchData,
GatewayMessageReactionRemoveDispatchData, GatewayMessageReactionRemoveDispatchData,
@ -53,3 +54,11 @@ export const MESSAGE_UPDATE = (
> => { > => {
return new Message(self, data as APIMessage); return new Message(self, data as APIMessage);
}; };
export const MESSAGE_POLL_VOTE_ADD = (_: BaseClient, data: GatewayMessagePollVoteDispatchData) => {
return toCamelCase(data);
};
export const MESSAGE_POLL_VOTE_REMOVE = (_: BaseClient, data: GatewayMessagePollVoteDispatchData) => {
return toCamelCase(data);
};

View File

@ -17,18 +17,20 @@ import { User } from './User';
import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook'; import type { MessageWebhookMethodEditParams, MessageWebhookMethodWriteParams } from './Webhook';
import { DiscordBase } from './extra/DiscordBase'; import { DiscordBase } from './extra/DiscordBase';
import { messageLink } from './extra/functions'; import { messageLink } from './extra/functions';
import { Poll } from '..';
export type MessageData = APIMessage | GatewayMessageCreateDispatchData; export type MessageData = APIMessage | GatewayMessageCreateDispatchData;
export interface BaseMessage export interface BaseMessage
extends DiscordBase, extends DiscordBase,
ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components'>> {} ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll'>> {}
export class BaseMessage extends DiscordBase { export class BaseMessage extends DiscordBase {
guildId: string | undefined; guildId: string | undefined;
timestamp?: number; timestamp?: number;
author!: User; author!: User;
member?: GuildMember; member?: GuildMember;
components: MessageActionRowComponent<ActionRowMessageComponents>[]; components: MessageActionRowComponent<ActionRowMessageComponents>[];
poll?: Poll;
mentions: { mentions: {
roles: string[]; roles: string[];
channels: APIChannelMention[]; channels: APIChannelMention[];
@ -111,12 +113,16 @@ export class BaseMessage extends DiscordBase {
) )
: data.mentions.map(u => new User(this.client, u)); : data.mentions.map(u => new User(this.client, u));
} }
if (data.poll) {
this.poll = new Poll(this.client, data.poll, this.channelId, this.id);
}
} }
} }
export interface Message export interface Message
extends BaseMessage, extends BaseMessage,
ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components'>> {} ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll'>> {}
export class Message extends BaseMessage { export class Message extends BaseMessage {
constructor(client: UsingClient, data: MessageData) { constructor(client: UsingClient, data: MessageData) {

29
src/structures/Poll.ts Normal file
View File

@ -0,0 +1,29 @@
import type { APIPoll } from 'discord-api-types/v10';
import { toCamelCase, type ObjectToLower } from '../common';
import { Base } from './extra/Base';
import type { UsingClient } from '../commands';
import type { ValidAnswerId } from '../api/Routes/channels';
export interface Poll extends ObjectToLower<Omit<APIPoll, 'expiry'>> {}
export class Poll extends Base {
expiry: number;
constructor(
client: UsingClient,
data: APIPoll,
readonly channelId: string,
readonly messageId: string,
) {
super(client);
this.expiry = Date.parse(data.expiry);
Object.assign(this, toCamelCase(data));
}
end() {
return this.client.messages.endPoll(this.channelId, this.messageId);
}
getAnswerVoters(id: ValidAnswerId) {
if (!this.answers.find(x => x.answerId === id)) throw new Error('Invalid answer id');
return this.client.messages.getAnswerVoters(this.channelId, this.messageId, id);
}
}

View File

@ -14,3 +14,4 @@ export * from './User';
export * from './VoiceState'; export * from './VoiceState';
export * from './Webhook'; export * from './Webhook';
export * from './channels'; export * from './channels';
export * from './Poll';

View File

@ -56,6 +56,7 @@ import {
type GatewayVoiceStateUpdateData, type GatewayVoiceStateUpdateData,
type GatewayWebhooksUpdateDispatchData, type GatewayWebhooksUpdateDispatchData,
type PresenceUpdateStatus, type PresenceUpdateStatus,
type GatewayMessagePollVoteDispatchData,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import type { RestToKeys } from '../common'; import type { RestToKeys } from '../common';
import type { GatewayGuildMemberAddDispatchDataFixed } from '../structures'; import type { GatewayGuildMemberAddDispatchDataFixed } from '../structures';
@ -155,6 +156,14 @@ export type StageSameEvents = RestToKeys<
] ]
>; >;
export type PollVoteSameEvents = RestToKeys<
[
GatewayMessagePollVoteDispatchData,
GatewayDispatchEvents.MessagePollVoteRemove,
GatewayDispatchEvents.MessagePollVoteAdd,
]
>;
export type IntegrationSameEvents = RestToKeys< export type IntegrationSameEvents = RestToKeys<
[ [
GatewayIntegrationCreateDispatchData, GatewayIntegrationCreateDispatchData,
@ -214,6 +223,7 @@ export type NormalizeEvents = Events &
GuildScheduledUserSameEvents & GuildScheduledUserSameEvents &
IntegrationSameEvents & IntegrationSameEvents &
EntitlementEvents & EntitlementEvents &
PollVoteSameEvents &
StageSameEvents & { RAW: GatewayDispatchEvents }; StageSameEvents & { RAW: GatewayDispatchEvents };
export type GatewayEvents = { [x in keyof NormalizeEvents]: NormalizeEvents[x] }; export type GatewayEvents = { [x in keyof NormalizeEvents]: NormalizeEvents[x] };