From 006a9f390fa5685c88fcd77b82d95c4308ccdd48 Mon Sep 17 00:00:00 2001 From: MARCROCK22 <57925328+MARCROCK22@users.noreply.github.com> Date: Sat, 25 May 2024 13:18:43 -0400 Subject: [PATCH] feat: send attachments option by default --- src/api/api.ts | 1 - src/client/base.ts | 6 +- src/client/client.ts | 6 +- src/client/collectors.ts | 97 ++++++++++++++++++++++++++++++ src/client/workerclient.ts | 6 +- src/common/shorters/interaction.ts | 17 +++--- src/common/shorters/messages.ts | 8 ++- src/common/shorters/webhook.ts | 6 +- src/components/handler.ts | 34 ++++++----- src/events/handler.ts | 11 +++- src/index.ts | 2 +- src/structures/Interaction.ts | 37 +++++++++--- src/structures/channels.ts | 16 ++++- 13 files changed, 200 insertions(+), 47 deletions(-) create mode 100644 src/client/collectors.ts diff --git a/src/api/api.ts b/src/api/api.ts index f64eac4..ff120e6 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -386,7 +386,6 @@ export class ApiHandler { if (options.request.reason) { options.headers['X-Audit-Log-Reason'] = encodeURIComponent(options.request.reason); } - return { data, finalUrl } as { data: typeof data; finalUrl: `/${string}` }; } diff --git a/src/client/base.ts b/src/client/base.ts index e49aeae..befd4fd 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -85,7 +85,7 @@ export class BaseClient { options: BaseClientOptions; /**@internal */ - static _seyferConfig?: InternalRuntimeConfigHTTP | InternalRuntimeConfig; + static _seyfertConfig?: InternalRuntimeConfigHTTP | InternalRuntimeConfig; constructor(options?: BaseClientOptions) { this.options = MergeOptions( @@ -339,7 +339,7 @@ export class BaseClient { async getRC< T extends InternalRuntimeConfigHTTP | InternalRuntimeConfig = InternalRuntimeConfigHTTP | InternalRuntimeConfig, >() { - const seyfertConfig = (BaseClient._seyferConfig || + const seyfertConfig = (BaseClient._seyfertConfig || (await this.options.getRC?.()) || (await magicImport(join(process.cwd(), 'seyfert.config.js')).then(x => x.default ?? x))) as T; @@ -358,7 +358,7 @@ export class BaseClient { output: join(process.cwd(), locations.output), }; - BaseClient._seyferConfig = seyfertConfig; + BaseClient._seyfertConfig = seyfertConfig; return obj; } diff --git a/src/client/client.ts b/src/client/client.ts index ad96735..fa55451 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -10,17 +10,19 @@ import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOp import { BaseClient } from './base'; import { onInteractionCreate } from './oninteractioncreate'; import { onMessageCreate } from './onmessagecreate'; +import { Collectors } from './collectors'; let parentPort: import('node:worker_threads').MessagePort; export class Client extends BaseClient { private __handleGuilds?: Set = new Set(); gateway!: ShardManager; - events? = new EventHandler(this.logger); me!: If; declare options: ClientOptions; memberUpdateHandler = new MemberUpdateHandler(); presenceUpdateHandler = new PresenceUpdateHandler(); + collectors = new Collectors(); + events? = new EventHandler(this.logger, this.collectors); constructor(options?: ClientOptions) { super(options); @@ -49,7 +51,7 @@ export class Client extends BaseClient { if (!rest.handlers.events) { this.events = undefined; } else if (typeof rest.handlers.events === 'function') { - this.events = new EventHandler(this.logger); + this.events = new EventHandler(this.logger, this.collectors); this.events.setHandlers({ callback: rest.handlers.events, }); diff --git a/src/client/collectors.ts b/src/client/collectors.ts new file mode 100644 index 0000000..ba50291 --- /dev/null +++ b/src/client/collectors.ts @@ -0,0 +1,97 @@ +import { randomUUID } from 'node:crypto'; +import type { Awaitable, CamelCase, SnakeCase } from '../common'; +import type { ClientNameEvents, GatewayEvents } from '../events'; +import type { ClientEvents } from '../events/hooks'; + +type SnakeCaseClientNameEvents = Uppercase>; + +type RunData = { + options: { + event: T; + idle?: number; + timeout?: number; + onStop?: (reason: string) => unknown; + filter: (arg: Awaited>]>) => Awaitable; + run: (arg: Awaited>]>) => unknown; + }; + idle?: NodeJS.Timeout; + timeout?: NodeJS.Timeout; + nonce: string; +}; + +export class Collectors { + readonly values = new Map[]>(); + + private generateRandomUUID(name: SnakeCaseClientNameEvents) { + const collectors = this.values.get(name); + if (!collectors) return '*'; + + let nonce = randomUUID(); + + while (collectors.find(x => x.nonce === nonce)) { + nonce = randomUUID(); + } + + return nonce; + } + + create(options: RunData['options']) { + if (!this.values.has(options.event)) { + this.values.set(options.event, []); + } + + const nonce = this.generateRandomUUID(options.event); + + this.values.get(options.event)!.push({ + options: { + ...options, + name: options.event, + } as RunData['options'], + idle: + options.idle && options.idle > 0 + ? setTimeout(() => { + return this.delete(options.event, nonce, 'idle'); + }, options.idle) + : undefined, + timeout: + options.timeout && options.timeout > 0 + ? setTimeout(() => { + return this.delete(options.event, nonce, 'timeout'); + }, options.timeout) + : undefined, + nonce, + }); + return options; + } + + private delete(name: SnakeCaseClientNameEvents, nonce: string, reason = 'unknown') { + const collectors = this.values.get(name); + + if (!collectors?.length) { + if (collectors) this.values.delete(name); + return; + } + + const index = collectors.findIndex(x => x.nonce === nonce); + if (index === -1) return; + const collector = collectors[index]; + clearTimeout(collector.options.idle); + clearTimeout(collector.options.timeout); + collectors.splice(index, 1); + return collector.options.onStop?.(reason); + } + + /**@internal */ + async run(name: T, data: Awaited>]>) { + const collectors = this.values.get(name); + if (!collectors) return; + + for (const i of collectors) { + if (await i.options.filter(data)) { + i.idle?.refresh(); + await i.options.run(data); + break; + } + } + } +} diff --git a/src/client/workerclient.ts b/src/client/workerclient.ts index ffb4ab4..58cb8ba 100644 --- a/src/client/workerclient.ts +++ b/src/client/workerclient.ts @@ -25,6 +25,7 @@ import { BaseClient } from './base'; import type { Client } from './client'; import { onInteractionCreate } from './oninteractioncreate'; import { onMessageCreate } from './onmessagecreate'; +import { Collectors } from './collectors'; let workerData: WorkerData; let manager: import('node:worker_threads').MessagePort; @@ -46,7 +47,8 @@ export class WorkerClient extends BaseClient { name: `[Worker #${workerData.workerId}]`, }); - events? = new EventHandler(this.logger); + collectors = new Collectors(); + events? = new EventHandler(this.logger, this.collectors); me!: When; promises = new Map void; timeout: NodeJS.Timeout }>(); @@ -117,7 +119,7 @@ export class WorkerClient extends BaseClient { if (!rest.handlers.events) { this.events = undefined; } else if (typeof rest.handlers.events === 'function') { - this.events = new EventHandler(this.logger); + this.events = new EventHandler(this.logger, this.collectors); this.events.setHandlers({ callback: rest.handlers.events, }); diff --git a/src/common/shorters/interaction.ts b/src/common/shorters/interaction.ts index 024d198..72ab6ef 100644 --- a/src/common/shorters/interaction.ts +++ b/src/common/shorters/interaction.ts @@ -1,4 +1,4 @@ -import { BaseInteraction, type RawFile, WebhookMessage, resolveFiles, type ReplyInteractionBody, Modal } from '../..'; +import { BaseInteraction, WebhookMessage, resolveFiles, type ReplyInteractionBody, Modal } from '../..'; import type { InteractionMessageUpdateBodyRequest, MessageWebhookCreateBodyRequest } from '../types/write'; import { BaseShorter } from './base'; @@ -8,6 +8,7 @@ export class InteractionShorter extends BaseShorter { const { files, ...rest } = body.data ?? {}; //@ts-expect-error const data = body.data instanceof Modal ? body.data : rest; + const parsedFiles = files ? await resolveFiles(files) : undefined; return this.client.proxy .interactions(id)(token) .callback.post({ @@ -16,9 +17,10 @@ export class InteractionShorter extends BaseShorter { type: body.type, data, }, + parsedFiles, this.client, ), - files: files ? await resolveFiles(files) : undefined, + files: parsedFiles, }); } @@ -32,12 +34,13 @@ export class InteractionShorter extends BaseShorter { async editMessage(token: string, messageId: string, body: InteractionMessageUpdateBodyRequest) { const { files, ...data } = body; + const parsedFiles = files ? await resolveFiles(files) : undefined; const apiMessage = await this.client.proxy .webhooks(this.client.applicationId)(token) .messages(messageId) .patch({ - body: BaseInteraction.transformBody(data, this.client), - files: files ? await resolveFiles(files) : undefined, + body: BaseInteraction.transformBody(data, parsedFiles, this.client), + files: parsedFiles, }); return new WebhookMessage(this.client, apiMessage, this.client.applicationId, token); } @@ -59,12 +62,12 @@ export class InteractionShorter extends BaseShorter { } async followup(token: string, { files, ...body }: MessageWebhookCreateBodyRequest) { - files = files ? await resolveFiles(files) : undefined; + const parsedFiles = files ? await resolveFiles(files) : undefined; const apiMessage = await this.client.proxy .webhooks(this.client.applicationId)(token) .post({ - body: BaseInteraction.transformBody(body, this.client), - files: files as RawFile[] | undefined, + body: BaseInteraction.transformBody(body, parsedFiles, this.client), + files: parsedFiles, }); return new WebhookMessage(this.client, apiMessage, this.client.applicationId, token); } diff --git a/src/common/shorters/messages.ts b/src/common/shorters/messages.ts index 2ae1f9c..d69420f 100644 --- a/src/common/shorters/messages.ts +++ b/src/common/shorters/messages.ts @@ -14,7 +14,11 @@ export class MessageShorter extends BaseShorter { async write(channelId: string, { files, ...body }: MessageCreateBodyRequest) { const parsedFiles = files ? await resolveFiles(files) : []; - const transformedBody = MessagesMethods.transformMessageBody(body, this.client); + const transformedBody = MessagesMethods.transformMessageBody( + body, + parsedFiles, + this.client, + ); return this.client.proxy .channels(channelId) .messages.post({ @@ -33,7 +37,7 @@ export class MessageShorter extends BaseShorter { .channels(channelId) .messages(messageId) .patch({ - body: MessagesMethods.transformMessageBody(body, this.client), + body: MessagesMethods.transformMessageBody(body, parsedFiles, this.client), files: parsedFiles, }) .then(async message => { diff --git a/src/common/shorters/webhook.ts b/src/common/shorters/webhook.ts index 9016c6d..39fc2e8 100644 --- a/src/common/shorters/webhook.ts +++ b/src/common/shorters/webhook.ts @@ -83,11 +83,12 @@ export class WebhookShorter extends BaseShorter { */ async writeMessage(webhookId: string, token: string, { body: data, ...payload }: MessageWebhookMethodWriteParams) { const { files, ...body } = data; + const parsedFiles = files ? await resolveFiles(files) : []; const transformedBody = MessagesMethods.transformMessageBody( body, + parsedFiles, this.client, ); - const parsedFiles = files ? await resolveFiles(files) : []; return this.client.proxy .webhooks(webhookId)(token) .post({ ...payload, files: parsedFiles, body: transformedBody }) @@ -108,11 +109,12 @@ export class WebhookShorter extends BaseShorter { { messageId, body: data, ...json }: MessageWebhookMethodEditParams, ) { const { files, ...body } = data; + const parsedFiles = files ? await resolveFiles(files) : []; const transformedBody = MessagesMethods.transformMessageBody( body, + parsedFiles, this.client, ); - const parsedFiles = files ? await resolveFiles(files) : []; return this.client.proxy .webhooks(webhookId)(token) .messages(messageId) diff --git a/src/components/handler.ts b/src/components/handler.ts index 3be4f3c..6fc6f98 100644 --- a/src/components/handler.ts +++ b/src/components/handler.ts @@ -44,22 +44,24 @@ export class ComponentHandler extends BaseHandler { this.values.set(messageId, { components: [], options, - idle: options.idle - ? setTimeout(() => { - this.deleteValue(messageId); - options.onStop?.('idle', () => { - this.createComponentCollector(messageId, options); - }); - }, options.idle) - : undefined, - timeout: options.timeout - ? setTimeout(() => { - this.deleteValue(messageId); - options.onStop?.('timeout', () => { - this.createComponentCollector(messageId, options); - }); - }, options.timeout) - : undefined, + idle: + options.idle && options.idle > 0 + ? setTimeout(() => { + this.deleteValue(messageId); + options.onStop?.('idle', () => { + this.createComponentCollector(messageId, options); + }); + }, options.idle) + : undefined, + timeout: + options.timeout && options.timeout > 0 + ? setTimeout(() => { + this.deleteValue(messageId); + options.onStop?.('timeout', () => { + this.createComponentCollector(messageId, options); + }); + }, options.timeout) + : undefined, __run: (customId, callback) => { if (this.values.has(messageId)) { this.values.get(messageId)!.components.push({ diff --git a/src/events/handler.ts b/src/events/handler.ts index 4c29bbe..4451ffe 100644 --- a/src/events/handler.ts +++ b/src/events/handler.ts @@ -5,16 +5,24 @@ import type { GatewayMessageDeleteDispatch, } from 'discord-api-types/v10'; import type { Client, WorkerClient } from '../client'; -import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common'; +import { BaseHandler, type Logger, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common'; import type { ClientEvents } from '../events/hooks'; import * as RawEvents from '../events/hooks'; import type { ClientEvent, ClientNameEvents } from './event'; +import type { Collectors } from '../client/collectors'; export type EventValue = MakeRequired & { fired?: boolean }; export type GatewayEvents = Uppercase>; export class EventHandler extends BaseHandler { + constructor( + logger: Logger, + protected collectors: Collectors, + ) { + super(logger); + } + onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('.events.onFail', err, event); protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); @@ -69,6 +77,7 @@ export class EventHandler extends BaseHandler { break; } + await this.collectors.run(args[0].t, args[0].d); await this.runEvent(args[0].t, args[1], args[0].d, args[2]); } diff --git a/src/index.ts b/src/index.ts index f049eac..d0d479d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,7 +86,7 @@ export const config = { port: 8080, ...data, } as InternalRuntimeConfigHTTP; - if (isCloudfareWorker()) BaseClient._seyferConfig = obj; + if (isCloudfareWorker()) BaseClient._seyfertConfig = obj; return obj; }, }; diff --git a/src/structures/Interaction.ts b/src/structures/Interaction.ts index c0d38a3..1e18b4e 100644 --- a/src/structures/Interaction.ts +++ b/src/structures/Interaction.ts @@ -35,6 +35,7 @@ import { InteractionType, type MessageFlags, type RESTPostAPIInteractionCallbackJSONBody, + type RESTAPIAttachment, } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; import type { RawFile } from '../api'; @@ -114,7 +115,11 @@ export class BaseInteraction< this.user = this.member?.user ?? new User(client, interaction.user!); } - static transformBodyRequest(body: ReplyInteractionBody, self: UsingClient): APIInteractionResponse { + static transformBodyRequest( + body: ReplyInteractionBody, + files: RawFile[] | undefined, + self: UsingClient, + ): APIInteractionResponse { switch (body.type) { case InteractionResponseType.ApplicationCommandAutocompleteResult: case InteractionResponseType.DeferredMessageUpdate: @@ -131,9 +136,15 @@ export class BaseInteraction< allowed_mentions: self.options?.allowedMentions, ...(body.data ?? {}), //@ts-ignore - components: body.data?.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)) ?? undefined, - embeds: body.data?.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined, - attachments: body.data?.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) ?? undefined, + components: body.data?.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)), + embeds: body.data?.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)), + attachments: + body.data && 'attachments' in body.data + ? body.data.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) + : (files?.map((x, id) => ({ + id, + filename: x.name, + })) as RESTAPIAttachment[]), poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined, }, }; @@ -166,14 +177,23 @@ export class BaseInteraction< | MessageUpdateBodyRequest | MessageCreateBodyRequest | MessageWebhookCreateBodyRequest, + files: RawFile[] | undefined, self: UsingClient, ) { const poll = (body as MessageWebhookCreateBodyRequest).poll; + return { allowed_mentions: self.options?.allowedMentions, + attachments: + 'attachments' in body + ? body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) + : (files?.map((x, id) => ({ + id, + filename: x.name, + })) as RESTAPIAttachment[]), ...body, - components: body.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)) ?? undefined, - embeds: body?.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined, + components: body.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)), + embeds: body?.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)), poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined, } as T; } @@ -184,9 +204,10 @@ export class BaseInteraction< const { files, ...rest } = body.data ?? {}; //@ts-expect-error const data = body.data instanceof Modal ? body.data : rest; + const parsedFiles = files ? await resolveFiles(files) : undefined; return (this.replied = this.__reply({ - body: BaseInteraction.transformBodyRequest({ data, type: body.type }, this.client), - files: files ? await resolveFiles(files) : undefined, + body: BaseInteraction.transformBodyRequest({ data, type: body.type }, parsedFiles, this.client), + files: parsedFiles, }).then(() => (this.replied = true))); } return (this.replied = this.client.interactions.reply(this.id, this.token, body).then(() => (this.replied = true))); diff --git a/src/structures/channels.ts b/src/structures/channels.ts index 3d51bfd..3af5a9c 100644 --- a/src/structures/channels.ts +++ b/src/structures/channels.ts @@ -1,6 +1,7 @@ import { ChannelFlags, ChannelType, + type RESTAPIAttachment, VideoQualityMode, type APIChannelBase, type APIDMChannel, @@ -40,6 +41,7 @@ import type { GuildMember } from './GuildMember'; import type { GuildRole } from './GuildRole'; import { DiscordBase } from './extra/DiscordBase'; import { channelLink } from './extra/functions'; +import type { RawFile } from '..'; export class BaseChannel extends DiscordBase> { declare type: T; @@ -250,14 +252,24 @@ export class MessagesMethods extends DiscordBase { }; } - static transformMessageBody(body: MessageCreateBodyRequest | MessageUpdateBodyRequest, self: UsingClient) { + static transformMessageBody( + body: MessageCreateBodyRequest | MessageUpdateBodyRequest, + files: RawFile[] | undefined, + self: UsingClient, + ) { const poll = (body as MessageCreateBodyRequest).poll; return { allowed_mentions: self.options?.allowedMentions, ...body, components: body.components?.map(x => (x instanceof ActionRow ? x.toJSON() : x)) ?? undefined, embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined, - attachments: body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) ?? undefined, + attachments: + 'attachments' in body + ? body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) ?? undefined + : (files?.map((x, id) => ({ + id, + filename: x.name, + })) as RESTAPIAttachment[]), poll: poll ? (poll instanceof PollBuilder ? poll.toJSON() : poll) : undefined, } as T; }