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",
"dependencies": {
"chokidar": "^3.6.0",
"discord-api-types": "^0.37.76",
"discord-api-types": "^0.37.80",
"magic-bytes.js": "^1.10.0",
"ts-mixer": "^6.0.4",
"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,
RESTGetAPIChannelUsersThreadsArchivedResult,
RESTGetAPIGuildWebhooksResult,
RESTGetAPIPollAnswerVotersQuery,
RESTGetAPIPollAnswerVotersResult,
RESTPatchAPIChannelJSONBody,
RESTPatchAPIChannelMessageJSONBody,
RESTPatchAPIChannelMessageResult,
@ -45,6 +47,7 @@ import type {
RESTPostAPIChannelWebhookJSONBody,
RESTPostAPIChannelWebhookResult,
RESTPostAPIGuildForumThreadsJSONBody,
RESTPostAPIPollExpireResult,
RESTPutAPIChannelMessageReactionResult,
RESTPutAPIChannelPermissionJSONBody,
RESTPutAPIChannelPermissionResult,
@ -256,5 +259,17 @@ export interface ChannelRoutes {
'voice-status': {
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 './Modal';
export * from './SelectMenu';
export * from './Poll';
export * from './types';
export function fromComponent(

View File

@ -4,10 +4,11 @@ import type {
RESTPostAPIChannelMessagesThreadsJSONBody,
} from 'discord-api-types/v10';
import { resolveFiles } from '../../builders';
import { Message, MessagesMethods } from '../../structures';
import { Message, MessagesMethods, User } from '../../structures';
import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../types/write';
import { BaseShorter } from './base';
import type { ValidAnswerId } from '../../api/Routes/channels';
export class MessageShorter extends BaseShorter {
async write(channelId: string, { files, ...body }: MessageCreateBodyRequest) {
@ -88,4 +89,21 @@ export class MessageShorter extends BaseShorter {
) {
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,
APIMessageActionRowComponent,
APIModalInteractionResponse,
RESTAPIPollCreate,
RESTPatchAPIChannelMessageJSONBody,
RESTPatchAPIWebhookWithTokenMessageJSONBody,
RESTPostAPIChannelMessageJSONBody,
RESTPostAPIWebhookWithTokenJSONBody,
} from 'discord-api-types/v10';
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';
@ -21,10 +30,14 @@ export interface ResolverProps {
files?: AttachmentBuilder[] | Attachment[] | RawFile[] | undefined;
}
export interface SendResolverProps extends ResolverProps {
poll?: PollBuilder | RESTAPIPollCreate | undefined;
}
export type MessageCreateBodyRequest = OmitInsert<
RESTPostAPIChannelMessageJSONBody,
'components' | 'embeds',
ResolverProps
'components' | 'embeds' | 'poll',
SendResolverProps
>;
export type MessageUpdateBodyRequest = OmitInsert<
@ -35,8 +48,8 @@ export type MessageUpdateBodyRequest = OmitInsert<
export type MessageWebhookCreateBodyRequest = OmitInsert<
RESTPostAPIWebhookWithTokenJSONBody,
'components' | 'embeds',
ResolverProps
'components' | 'embeds' | 'poll',
SendResolverProps
>;
export type MessageWebhookUpdateBodyRequest = OmitInsert<

View File

@ -3,6 +3,7 @@ import type {
GatewayMessageCreateDispatchData,
GatewayMessageDeleteBulkDispatchData,
GatewayMessageDeleteDispatchData,
GatewayMessagePollVoteDispatchData,
GatewayMessageReactionAddDispatchData,
GatewayMessageReactionRemoveAllDispatchData,
GatewayMessageReactionRemoveDispatchData,
@ -53,3 +54,11 @@ export const MESSAGE_UPDATE = (
> => {
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 { DiscordBase } from './extra/DiscordBase';
import { messageLink } from './extra/functions';
import { Poll } from '..';
export type MessageData = APIMessage | GatewayMessageCreateDispatchData;
export interface BaseMessage
extends DiscordBase,
ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components'>> {}
ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll'>> {}
export class BaseMessage extends DiscordBase {
guildId: string | undefined;
timestamp?: number;
author!: User;
member?: GuildMember;
components: MessageActionRowComponent<ActionRowMessageComponents>[];
poll?: Poll;
mentions: {
roles: string[];
channels: APIChannelMention[];
@ -111,12 +113,16 @@ export class BaseMessage extends DiscordBase {
)
: 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
extends BaseMessage,
ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components'>> {}
ObjectToLower<Omit<MessageData, 'timestamp' | 'author' | 'mentions' | 'components' | 'poll'>> {}
export class Message extends BaseMessage {
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 './Webhook';
export * from './channels';
export * from './Poll';

View File

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