mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat: PollBuilder (#10324)
* Add PollBuilder * Add exports * Update typings * Update validations * Use correct enum validator method * Fix assertion, formatting * Add tests * Fix assertion * Add JSDoc, format * Make requested changes * Remove unnecessary blank import * Add support for PollBuilder in mainlib discord.js * Add types, fix formatting * Correct typings & assertions for poll answer emojis * Improve typings readability * Add JSDoc typings for overrides * Add types for using PollBuilder in message payload * Add tests, allow passing Emoji instance to emoji option * Fix formatting * Update max poll duration * refactor: implement builders v2 pattern
This commit is contained in:
216
packages/builders/__tests__/messages/poll.test.ts
Normal file
216
packages/builders/__tests__/messages/poll.test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { PollLayoutType } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { PollAnswerMediaBuilder, PollBuilder, PollQuestionBuilder } from '../../src/index.js';
|
||||
|
||||
const dummyData = {
|
||||
question: {
|
||||
text: '.',
|
||||
},
|
||||
answers: [],
|
||||
};
|
||||
|
||||
describe('Poll', () => {
|
||||
describe('Poll question', () => {
|
||||
test('GIVEN a poll with pre-defined question text THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder({ question: { text: 'foo' } });
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with question text THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder();
|
||||
|
||||
poll.setQuestion({ text: 'foo' });
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ ...dummyData, question: { text: 'foo' } });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with invalid question THEN throws error', () => {
|
||||
expect(() => new PollQuestionBuilder().setText('.'.repeat(301)).toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll duration', () => {
|
||||
test('GIVEN a poll with pre-defined duration THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder({ duration: 1, ...dummyData });
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with duration THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
poll.setDuration(1);
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ duration: 1, ...dummyData });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with invalid duration THEN throws error', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
expect(() => poll.setDuration(999).toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll layout type', () => {
|
||||
test('GIVEN a poll with pre-defined layout type THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder({ layout_type: PollLayoutType.Default, ...dummyData });
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with layout type THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
poll.setLayoutType(PollLayoutType.Default);
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ layout_type: PollLayoutType.Default, ...dummyData });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with invalid layout type THEN throws error', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
// @ts-expect-error Invalid layout type
|
||||
expect(() => poll.setLayoutType(-1).toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll multi select', () => {
|
||||
test('GIVEN a poll with pre-defined multi select enabled THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder({ allow_multiselect: true, ...dummyData });
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with multi select enabled THEN return valid toJSON data', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
poll.setMultiSelect();
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({ allow_multiselect: true, ...dummyData });
|
||||
});
|
||||
|
||||
test('GIVEN a poll with invalid multi select value THEN throws error', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
// @ts-expect-error Invalid multi-select value
|
||||
expect(() => poll.setMultiSelect('string').toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll answers', () => {
|
||||
test('GIVEN a poll with pre-defined answer THEN returns valid toJSON data', () => {
|
||||
const poll = new PollBuilder({
|
||||
...dummyData,
|
||||
answers: [{ poll_media: { text: 'foo' } }],
|
||||
});
|
||||
expect(poll.toJSON()).toStrictEqual({
|
||||
...dummyData,
|
||||
answers: [{ poll_media: { text: 'foo' } }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a poll using PollBuilder#addAnswers THEN returns valid toJSON data', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
poll.addAnswers({ poll_media: { text: 'foo' } });
|
||||
poll.addAnswers([{ poll_media: { text: 'foo' } }]);
|
||||
|
||||
expect(poll.toJSON()).toStrictEqual({
|
||||
...dummyData,
|
||||
answers: [{ poll_media: { text: 'foo' } }, { poll_media: { text: 'foo' } }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a poll using PollBuilder#spliceAnswers THEN returns valid toJSON data', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
poll.addAnswers({ poll_media: { text: 'foo' } }, { poll_media: { text: 'bar' } });
|
||||
|
||||
expect(poll.spliceAnswers(0, 1).toJSON()).toStrictEqual({
|
||||
...dummyData,
|
||||
answers: [{ poll_media: { text: 'bar' } }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a poll using PollBuilder#spliceAnswers THEN returns valid toJSON data 2', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
poll.addAnswers(...Array.from({ length: 8 }, () => ({ poll_media: { text: 'foo' } })));
|
||||
|
||||
expect(() =>
|
||||
poll.spliceAnswers(0, 3, ...Array.from({ length: 2 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a poll using PollBuilder#spliceAnswers that adds additional answers resulting in answers > 10 THEN throws error', () => {
|
||||
const poll = new PollBuilder();
|
||||
|
||||
poll.addAnswers(...Array.from({ length: 8 }, () => ({ poll_media: { text: 'foo' } })));
|
||||
|
||||
expect(() =>
|
||||
poll.spliceAnswers(0, 3, ...Array.from({ length: 8 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a poll using PollBuilder#setAnswers THEN returns valid toJSON data', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
expect(() =>
|
||||
poll.setAnswers(...Array.from({ length: 10 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).not.toThrowError();
|
||||
expect(() =>
|
||||
poll.setAnswers(Array.from({ length: 10 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a poll using PollBuilder#setAnswers that sets more than 10 answers THEN throws error', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
expect(() =>
|
||||
poll.setAnswers(...Array.from({ length: 11 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).toThrowError();
|
||||
expect(() =>
|
||||
poll.setAnswers(Array.from({ length: 11 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
describe('GIVEN invalid answer amount THEN throws error', () => {
|
||||
test('1', () => {
|
||||
const poll = new PollBuilder(dummyData);
|
||||
|
||||
expect(() =>
|
||||
poll.addAnswers(...Array.from({ length: 11 }, () => ({ poll_media: { text: 'foo' } }))).toJSON(),
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid answer THEN throws error', () => {
|
||||
test('2', () => {
|
||||
const poll = new PollBuilder().setQuestion({ text: '.' });
|
||||
|
||||
// @ts-expect-error Invalid answer
|
||||
expect(() => poll.addAnswers({}).toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid answer text length THEN throws error', () => {
|
||||
test('3', () => {
|
||||
expect(() => new PollAnswerMediaBuilder().setText('.'.repeat(56)).toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid answer text THEN throws error', () => {
|
||||
test('4', () => {
|
||||
expect(() => new PollAnswerMediaBuilder().setText('').toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid answer emoji THEN throws error', () => {
|
||||
test('5', () => {
|
||||
// @ts-expect-error Invalid emoji
|
||||
expect(() => new PollAnswerMediaBuilder().setEmoji('').toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,8 +60,16 @@ export * from './messages/embed/EmbedAuthor.js';
|
||||
export * from './messages/embed/EmbedField.js';
|
||||
export * from './messages/embed/EmbedFooter.js';
|
||||
|
||||
export * from './messages/poll/Assertions.js';
|
||||
export * from './messages/poll/Poll.js';
|
||||
export * from './messages/poll/PollAnswer.js';
|
||||
export * from './messages/poll/PollAnswerMedia.js';
|
||||
export * from './messages/poll/PollMedia.js';
|
||||
export * from './messages/poll/PollQuestion.js';
|
||||
|
||||
export * from './util/componentUtil.js';
|
||||
export * from './util/normalizeArray.js';
|
||||
export * from './util/resolveBuilder.js';
|
||||
export * from './util/validation.js';
|
||||
|
||||
export * from './Assertions.js';
|
||||
|
||||
20
packages/builders/src/messages/poll/Assertions.ts
Normal file
20
packages/builders/src/messages/poll/Assertions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { PollLayoutType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { emojiPredicate } from '../../components/Assertions';
|
||||
|
||||
export const pollQuestionPredicate = z.object({ text: z.string().min(1).max(300) });
|
||||
|
||||
export const pollAnswerMediaPredicate = z.object({
|
||||
text: z.string().min(1).max(55),
|
||||
emoji: emojiPredicate.nullish(),
|
||||
});
|
||||
|
||||
export const pollAnswerPredicate = z.object({ poll_media: pollAnswerMediaPredicate });
|
||||
|
||||
export const pollPredicate = z.object({
|
||||
question: pollQuestionPredicate,
|
||||
answers: z.array(pollAnswerPredicate).max(10),
|
||||
duration: z.number().min(1).max(768).optional(),
|
||||
allow_multiselect: z.boolean().optional(),
|
||||
layout_type: z.nativeEnum(PollLayoutType).optional(),
|
||||
});
|
||||
241
packages/builders/src/messages/poll/Poll.ts
Normal file
241
packages/builders/src/messages/poll/Poll.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { RESTAPIPoll, APIPollMedia, PollLayoutType, APIPollAnswer } from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||
import { validate } from '../../util/validation.js';
|
||||
import { pollPredicate } from './Assertions';
|
||||
import { PollAnswerBuilder } from './PollAnswer.js';
|
||||
import { PollQuestionBuilder } from './PollQuestion.js';
|
||||
|
||||
export interface PollData extends Omit<RESTAPIPoll, 'answers' | 'question'> {
|
||||
answers: PollAnswerBuilder[];
|
||||
question: PollQuestionBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for polls.
|
||||
*/
|
||||
export class PollBuilder implements JSONEncodable<RESTAPIPoll> {
|
||||
/**
|
||||
* The API data associated with this poll.
|
||||
*/
|
||||
private readonly data: PollData;
|
||||
|
||||
/**
|
||||
* Gets the answers of this poll.
|
||||
*/
|
||||
public get answers(): readonly PollAnswerBuilder[] {
|
||||
return this.data.answers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new poll from API data.
|
||||
*
|
||||
* @param data - The API data to create this poll with
|
||||
*/
|
||||
public constructor(data: Partial<RESTAPIPoll> = {}) {
|
||||
this.data = {
|
||||
...structuredClone(data),
|
||||
question: new PollQuestionBuilder(data.question),
|
||||
answers: data.answers?.map((answer) => new PollAnswerBuilder(answer)) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends answers to the poll.
|
||||
*
|
||||
* @remarks
|
||||
* This method accepts either an array of answers or a variable number of answer parameters.
|
||||
* The maximum amount of answers that can be added is 10.
|
||||
* @example
|
||||
* Using an array:
|
||||
* ```ts
|
||||
* const answers: APIPollMedia[] = ...;
|
||||
* const poll = new PollBuilder()
|
||||
* .addAnswers(answers);
|
||||
* ```
|
||||
* @example
|
||||
* Using rest parameters (variadic):
|
||||
* ```ts
|
||||
* const poll = new PollBuilder()
|
||||
* .addAnswers(
|
||||
* { text: 'Answer 1' },
|
||||
* { text: 'Answer 2' },
|
||||
* );
|
||||
* ```
|
||||
* @param answers - The answers to add
|
||||
*/
|
||||
public addAnswers(
|
||||
...answers: RestOrArray<
|
||||
Omit<APIPollAnswer, 'answer_id'> | PollAnswerBuilder | ((builder: PollAnswerBuilder) => PollAnswerBuilder)
|
||||
>
|
||||
): this {
|
||||
const normalizedAnswers = normalizeArray(answers);
|
||||
const resolved = normalizedAnswers.map((answer) => resolveBuilder(answer, PollAnswerBuilder));
|
||||
|
||||
this.data.answers.push(...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts answers for this poll.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
* The maximum amount of answers that can be added is 10.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing answers of a poll.
|
||||
* @example
|
||||
* Remove the first answer:
|
||||
* ```ts
|
||||
* poll.spliceAnswers(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n answers:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* poll.spliceAnswers(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last answer:
|
||||
* ```ts
|
||||
* poll.spliceAnswers(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of answers to remove
|
||||
* @param answers - The replacing answer objects
|
||||
*/
|
||||
public spliceAnswers(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...answers: (
|
||||
| Omit<APIPollAnswer, 'answer_id'>
|
||||
| PollAnswerBuilder
|
||||
| ((builder: PollAnswerBuilder) => PollAnswerBuilder)
|
||||
)[]
|
||||
): this {
|
||||
const normalizedAnswers = normalizeArray(answers);
|
||||
const resolved = normalizedAnswers.map((answer) => resolveBuilder(answer, PollAnswerBuilder));
|
||||
|
||||
this.data.answers.splice(index, deleteCount, ...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the answers for this poll.
|
||||
*
|
||||
* @remarks
|
||||
* This method is an alias for {@link PollBuilder.spliceAnswers}. More specifically,
|
||||
* it splices the entire array of answers, replacing them with the provided answers.
|
||||
*
|
||||
* You can set a maximum of 10 answers.
|
||||
* @param answers - The answers to set
|
||||
*/
|
||||
public setAnswers(
|
||||
...answers: RestOrArray<
|
||||
Omit<APIPollAnswer, 'answer_id'> | PollAnswerBuilder | ((builder: PollAnswerBuilder) => PollAnswerBuilder)
|
||||
>
|
||||
): this {
|
||||
return this.spliceAnswers(0, this.data.answers.length, ...normalizeArray(answers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the question for this poll.
|
||||
*
|
||||
* @param options - The data to use for this poll's question
|
||||
*/
|
||||
public setQuestion(
|
||||
options:
|
||||
| Omit<APIPollMedia, 'emoji'>
|
||||
| PollQuestionBuilder
|
||||
| ((builder: PollQuestionBuilder) => PollQuestionBuilder),
|
||||
): this {
|
||||
this.data.question = resolveBuilder(options, PollQuestionBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the question of this poll.
|
||||
*
|
||||
* @param updater - The function to update the question with
|
||||
*/
|
||||
public updateQuestion(updater: (builder: PollQuestionBuilder) => void): this {
|
||||
updater((this.data.question ??= new PollQuestionBuilder()));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layout type for this poll.
|
||||
*
|
||||
* @remarks
|
||||
* This method is redundant while only one type of poll layout exists (`PollLayoutType.Default`)
|
||||
* with Discord using that as the layout type if none is specified.
|
||||
* @param type - The type of poll layout to use
|
||||
*/
|
||||
public setLayoutType(type: PollLayoutType): this {
|
||||
this.data.layout_type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the layout type for this poll.
|
||||
*/
|
||||
public clearLayoutType(): this {
|
||||
this.data.layout_type = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether multi-select is enabled for this poll.
|
||||
*
|
||||
* @param multiSelect - Whether to allow multi-select
|
||||
*/
|
||||
public setMultiSelect(multiSelect = true): this {
|
||||
this.data.allow_multiselect = multiSelect;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets how long this poll will be open for in hours.
|
||||
*
|
||||
* @remarks
|
||||
* Minimum duration is `1`, with maximum duration being `768` (32 days).
|
||||
* Default if none specified is `24` (one day).
|
||||
* @param duration - The amount of hours this poll will be open for
|
||||
*/
|
||||
public setDuration(duration: number): this {
|
||||
this.data.duration = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the duration for this poll.
|
||||
*/
|
||||
public clearDuration(): this {
|
||||
this.data.duration = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): RESTAPIPoll {
|
||||
const { answers, question, ...rest } = this.data;
|
||||
|
||||
const data = {
|
||||
...structuredClone(rest),
|
||||
// Disable validation because the pollPredicate below will validate those as well
|
||||
answers: answers.map((answer) => answer.toJSON(false)),
|
||||
question: question.toJSON(false),
|
||||
};
|
||||
|
||||
validate(pollPredicate, data, validationOverride);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
60
packages/builders/src/messages/poll/PollAnswer.ts
Normal file
60
packages/builders/src/messages/poll/PollAnswer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { APIPollAnswer, APIPollMedia } from 'discord-api-types/v10';
|
||||
import { resolveBuilder } from '../../util/resolveBuilder';
|
||||
import { validate } from '../../util/validation';
|
||||
import { pollAnswerPredicate } from './Assertions';
|
||||
import { PollAnswerMediaBuilder } from './PollAnswerMedia';
|
||||
|
||||
export interface PollAnswerData extends Omit<APIPollAnswer, 'answer_id' | 'poll_media'> {
|
||||
poll_media: PollAnswerMediaBuilder;
|
||||
}
|
||||
|
||||
export class PollAnswerBuilder {
|
||||
protected readonly data: PollAnswerData;
|
||||
|
||||
public constructor(data: Partial<Omit<APIPollAnswer, 'answer_id'>> = {}) {
|
||||
this.data = {
|
||||
...structuredClone(data),
|
||||
poll_media: new PollAnswerMediaBuilder(data.poll_media),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media for this poll answer.
|
||||
*
|
||||
* @param options - The data to use for this poll answer's media
|
||||
*/
|
||||
public setMedia(
|
||||
options: APIPollMedia | PollAnswerMediaBuilder | ((builder: PollAnswerMediaBuilder) => PollAnswerMediaBuilder),
|
||||
): this {
|
||||
this.data.poll_media = resolveBuilder(options, PollAnswerMediaBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the media of this poll answer.
|
||||
*
|
||||
* @param updater - The function to update the media with
|
||||
*/
|
||||
public updateMedia(updater: (builder: PollAnswerMediaBuilder) => void) {
|
||||
updater((this.data.poll_media ??= new PollAnswerMediaBuilder()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): Omit<APIPollAnswer, 'answer_id'> {
|
||||
const data = {
|
||||
...structuredClone(this.data),
|
||||
// Disable validation because the pollAnswerPredicate below will validate this as well
|
||||
poll_media: this.data.poll_media?.toJSON(false),
|
||||
};
|
||||
|
||||
validate(pollAnswerPredicate, data, validationOverride);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
35
packages/builders/src/messages/poll/PollAnswerMedia.ts
Normal file
35
packages/builders/src/messages/poll/PollAnswerMedia.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { APIPartialEmoji, APIPollMedia } from 'discord-api-types/v10';
|
||||
import { validate } from '../../util/validation.js';
|
||||
import { pollAnswerMediaPredicate } from './Assertions.js';
|
||||
import { PollMediaBuilder } from './PollMedia.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for poll answers.
|
||||
*/
|
||||
export class PollAnswerMediaBuilder extends PollMediaBuilder {
|
||||
/**
|
||||
* Sets the emoji for this poll answer.
|
||||
*
|
||||
* @param emoji - The emoji to use
|
||||
*/
|
||||
public setEmoji(emoji: APIPartialEmoji): this {
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the emoji for this poll answer.
|
||||
*/
|
||||
public clearEmoji(): this {
|
||||
this.data.emoji = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override toJSON(validationOverride?: boolean): APIPollMedia {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
validate(pollAnswerMediaPredicate, clone, validationOverride);
|
||||
|
||||
return clone as APIPollMedia;
|
||||
}
|
||||
}
|
||||
33
packages/builders/src/messages/poll/PollMedia.ts
Normal file
33
packages/builders/src/messages/poll/PollMedia.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { APIPollMedia } from 'discord-api-types/v10';
|
||||
|
||||
export abstract class PollMediaBuilder {
|
||||
protected readonly data: Partial<APIPollMedia>;
|
||||
|
||||
/**
|
||||
* Creates new poll media from API data.
|
||||
*
|
||||
* @param data - The API data to use
|
||||
*/
|
||||
public constructor(data: Partial<APIPollMedia> = {}) {
|
||||
this.data = structuredClone(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text for this poll media.
|
||||
*
|
||||
* @param text - The text to use
|
||||
*/
|
||||
public setText(text: string): this {
|
||||
this.data.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public abstract toJSON(validationOverride?: boolean): APIPollMedia;
|
||||
}
|
||||
17
packages/builders/src/messages/poll/PollQuestion.ts
Normal file
17
packages/builders/src/messages/poll/PollQuestion.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { APIPollMedia } from 'discord-api-types/v10';
|
||||
import { validate } from '../../util/validation.js';
|
||||
import { pollQuestionPredicate } from './Assertions.js';
|
||||
import { PollMediaBuilder } from './PollMedia.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a poll question.
|
||||
*/
|
||||
export class PollQuestionBuilder extends PollMediaBuilder {
|
||||
public override toJSON(validationOverride?: boolean): Omit<APIPollMedia, 'emoji'> {
|
||||
const clone = structuredClone(this.data);
|
||||
|
||||
validate(pollQuestionPredicate, clone, validationOverride);
|
||||
|
||||
return clone as Omit<APIPollMedia, 'emoji'>;
|
||||
}
|
||||
}
|
||||
@@ -192,17 +192,19 @@ class MessagePayload {
|
||||
|
||||
let poll;
|
||||
if (this.options.poll) {
|
||||
poll = {
|
||||
question: {
|
||||
text: this.options.poll.question.text,
|
||||
},
|
||||
answers: this.options.poll.answers.map(answer => ({
|
||||
poll_media: { text: answer.text, emoji: resolvePartialEmoji(answer.emoji) },
|
||||
})),
|
||||
duration: this.options.poll.duration,
|
||||
allow_multiselect: this.options.poll.allowMultiselect,
|
||||
layout_type: this.options.poll.layoutType,
|
||||
};
|
||||
poll = isJSONEncodable(this.options.poll)
|
||||
? this.options.poll.toJSON()
|
||||
: {
|
||||
question: {
|
||||
text: this.options.poll.question.text,
|
||||
},
|
||||
answers: this.options.poll.answers.map(answer => ({
|
||||
poll_media: { text: answer.text, emoji: resolvePartialEmoji(answer.emoji) },
|
||||
})),
|
||||
duration: this.options.poll.duration,
|
||||
allow_multiselect: this.options.poll.allowMultiselect,
|
||||
layout_type: this.options.poll.layoutType,
|
||||
};
|
||||
}
|
||||
|
||||
this.body = {
|
||||
|
||||
3
packages/discord.js/typings/index.d.ts
vendored
3
packages/discord.js/typings/index.d.ts
vendored
@@ -176,6 +176,7 @@ import {
|
||||
RESTAPIInteractionCallbackActivityInstanceResource,
|
||||
VoiceChannelEffectSendAnimationType,
|
||||
GatewayVoiceChannelEffectSendDispatchData,
|
||||
RESTAPIPoll,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ChildProcess } from 'node:child_process';
|
||||
import { Stream } from 'node:stream';
|
||||
@@ -6352,7 +6353,7 @@ export interface BaseMessageOptions {
|
||||
}
|
||||
|
||||
export interface BaseMessageOptionsWithPoll extends BaseMessageOptions {
|
||||
poll?: PollData;
|
||||
poll?: JSONEncodable<RESTAPIPoll> | PollData;
|
||||
}
|
||||
|
||||
export interface MessageCreateOptions extends BaseMessageOptionsWithPoll {
|
||||
|
||||
Reference in New Issue
Block a user