diff --git a/bun.lockb b/bun.lockb index fe39fbf..0e44ebc 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/core/package.json b/packages/core/package.json index a310ab7..58447ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "lilybird", - "version": "0.8.0", + "version": "0.9.0-beta.4", "description": "A bun-first discord api wrapper written in TS", "main": "./dist/index.js", "author": "DidaS", diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index df0d830..5e0b47e 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,582 +1,108 @@ import { CachingManager } from "./cache/manager.js"; import { DebugREST, REST } from "./http/rest.js"; +import { ListenerCompiler } from "./compiler.js"; import { WebSocketManager } from "#ws"; -import { - CachingDelegationType, - TransformerReturnType, - CacheExecutionPolicy, - CacheElementType, - DebugIdentifier, - GatewayEvent -} from "#enums"; - +import type { CompilerOptions } from "./compiler.js"; import type { DispatchFunction } from "#ws"; + import type { - UpdatePresenceStructure, CacheManagerStructure, - ParseCachingManager, - BaseClientOptions, - SelectiveCache, + CachingOptions, ClientOptions, DebugFunction, Transformers, Application, - Transformer + MockClient, + Listeners } from "./typings/index.js"; -type GetUserType = (T["userUpdate"] & {}) extends { handler: ((...args: infer U) => infer R) } +type GetUserType> = (T["userUpdate"] & {}) extends { handler: ((...args: infer U) => infer R) } ? unknown extends R ? U[1] : R : never ; -export interface Client { - readonly user: GetUserType; - readonly sessionId: string; - readonly application: Application.Structure; -} - -/* - I will probably end up making them props - but for now we use declaration merging - this is safe because as the library is meant to be used - the client will always have this properties defined - once the user can interact with it - - This however might not be true if the user - extends the class or tries to create its own instance -*/ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -export class Client { +export class Client implements MockClient { public readonly rest: REST; - public readonly cache: C; + public readonly cache: CacheManagerStructure; + public readonly ws: WebSocketManager; - readonly #ws: WebSocketManager; - readonly #debug: DebugFunction; + public readonly declare user: GetUserType>; + public readonly declare sessionId: string; + public readonly declare application: Application.Structure; + protected readonly declare ready: boolean; - protected readonly ready: boolean = false; + readonly #dispatch?: DispatchFunction; - public constructor(options: BaseClientOptions, debug?: DebugFunction) { + public constructor(options: ClientOptions, debug?: DebugFunction) { this.rest = options.useDebugRest === true ? new DebugREST(debug) : new REST(); - this.cache = typeof options.caching?.manager !== "undefined" ? options.caching.manager : new CachingManager(); - // eslint-disable-next-line @typescript-eslint/no-empty-function - this.#debug = debug ?? (() => {}); - this.#ws = new WebSocketManager( + this.cache = typeof options.cachingManager !== "undefined" ? options.cachingManager : new CachingManager(); + this.ws = new WebSocketManager( { intents: options.intents, presence: options.presence }, - this.#generateListeners(options), + undefined, debug ); + + this.#dispatch = options.dispatch; } - public async login(token: string): Promise { - this.#ws.options = { token }; + public async login(token: string, dispatch: DispatchFunction | undefined = this.#dispatch): Promise { + if (typeof dispatch === "undefined") throw new Error("the client doesn't have any 'dispatch' function defined."); + this.ws.init(token, dispatch); this.rest.setToken(token); - await this.#ws.connect(); + await this.ws.connect(); return token; } public close(): void { this.rest.setToken(undefined); - this.#ws.close(); - } - - public setPresence(presence: UpdatePresenceStructure): void { - this.#ws.updatePresence(presence); + this.ws.close(); } - /** Both numbers are represented in `ms` */ + /** + * Both numbers are represented in `ms`. + * + * This function requires Bun to work. + */ public async ping(): Promise<{ ws: number, rest: number }> { const start = performance.now(); await this.rest.getGateway(); const final = performance.now() - start; return { - ws: await this.#ws.ping(), + ws: await this.ws.ping(), rest: final }; } +} - /** - * DO NOT USE OUTSIDE OF INTERNAL CODE - * @internal - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - protected __updateResumeInfo(url: string, id: string): void { - Object.assign(this.#ws.resumeInfo, { - url, - id - }); - } - - #generateListeners(options: BaseClientOptions): DispatchFunction { - const builder = new Map(); - const functions = new Map any>(); - - const { listeners, caching } = options; - const transformers = (options.transformers ?? {}) as Record>; - - //#region Raw Listener - if (typeof listeners.raw !== "undefined") { - functions.set("raw", listeners.raw); - builder.set("RAW", "await raw(data);"); - } - //#endregion - //#region Ready Listener - const readyArr = []; - readyArr.push( - "if(data.t === \"READY\"){", - "Object.assign(client,{user:" - ); - - if (typeof transformers.userUpdate !== "undefined") { - if (transformers.userUpdate.return === TransformerReturnType.MULTIPLE) throw new Error("The transformer for 'userUpdate' should only return 1 value"); - functions.set("t_userUpdate", transformers.userUpdate.handler); - readyArr.push("await t_userUpdate(client, data.d.user)"); - } else readyArr.push("data.d.user"); - - readyArr.push( - ",sessionId:data.d.session_id,application:data.d.application});", - "client.__updateResumeInfo(data.d.resume_gateway_url, data.d.session_id);", - "if(!client.ready){client.ready=true;" - ); - - if (typeof options.setup !== "undefined") { - functions.set("setup", options.setup); - readyArr.push("await setup(client);"); - } - - readyArr.push("}"); - - if (typeof listeners.ready !== "undefined") { - functions.set("ready", listeners.ready); - - const transformer = transformers.ready; - - if (typeof transformer !== "undefined") { - functions.set("t_ready", transformer.handler); - switch (transformer.return) { - case TransformerReturnType.SINGLE: { - readyArr.push("await ready(t_ready(client, data.d));"); - break; - } - case TransformerReturnType.MULTIPLE: { - readyArr.push("await ready(...t_ready(client, data.d));"); - break; - } - } - } else readyArr.push("await ready(client, data.d);"); - } - - readyArr.push("}"); - builder.set("READY", readyArr.join("")); - //#endregion - //#region User defined listeners - for (let i = 0, listenerEntries = Object.entries(listeners), { length } = listenerEntries; i < length; i++) { - const [handlerName, handler] = listenerEntries[i] as [string, () => unknown]; - if (handlerName === "raw" || handlerName === "ready") continue; - if (typeof handler === "undefined") continue; - - const name = `${handlerName[0].toUpperCase()}${handlerName.slice(1)}`; - const event: GatewayEvent = GatewayEvent[name]; - const transformer = transformers[handlerName]; - - this.#createListener( - builder, - functions, - event, - name, - handler, - transformer - ); - } - //#endregion - //#region Cache handlers - if (typeof caching !== "undefined" && caching.delegate !== CachingDelegationType.TRANSFORMERS) { - if (caching.delegate === CachingDelegationType.EXTERNAL && caching.applyTransformers === true && !caching.safeToTransform) { - process.emitWarning("Transformers will be applied to external solution!", { - code: "EXTERNAL_TRANSFORMERS", - detail: "Some external caching managers might not support transformers, please read their documentation before proceeding." - }); - } - - if (Object.keys(caching.enabled).length === 0) throw new Error("Got unexpected empty object"); - - const defaults: SelectiveCache = { - create: CacheExecutionPolicy.FIRST, - update: CacheExecutionPolicy.LAST, - delete: CacheExecutionPolicy.LAST - }; - - // const enabledUser = caching.enabled.user === true ? defaults : typeof caching.enabled.user === "object" ? caching.enabled.user : undefined; - const enabledGuild = caching.enabled.guild === true ? defaults : typeof caching.enabled.guild === "object" ? caching.enabled.guild : undefined; - const enabledChannel = caching.enabled.channel === true ? defaults : typeof caching.enabled.channel === "object" ? caching.enabled.channel : undefined; - const enabledThreads = caching.enabled.channel === true - ? defaults - : typeof caching.enabled.channel === "object" - ? caching.enabled.channel.threads === true - ? defaults - : typeof caching.enabled.channel.threads === "object" - ? caching.enabled.channel.threads - : undefined - : undefined; - - if (typeof caching.enabled.voiceState !== "undefined" && typeof enabledGuild?.create === "undefined") throw new Error("The 'voiceState' cache needs the `guildCreate' cache to be enabled"); - - if (typeof enabledGuild?.create !== "undefined") { - const temp: Array = []; - if (typeof enabledChannel?.create !== "undefined") { - if (caching.applyTransformers) { - if (!functions.has("t_channelCreate")) { - if (typeof transformers.channelCreate === "undefined") throw Error("Missing 'channelCreate' transformer"); - functions.set("t_channelCreate", transformers.channelCreate.handler); - } - } - temp.push( - "for (let i = 0, {channels} = td, {length} = channels; i < length; i++){", - `const channel = ${caching.applyTransformers ? "t_channelCreate(client, channels[i])" : "channels[i]"};`, - `await client.cache.set(${CacheElementType.CHANNEL}, channel.id, channel);`, - "}" - ); - } - - if (typeof enabledThreads?.create !== "undefined") { - if (caching.applyTransformers) { - if (!functions.has("t_channelCreate")) { - if (typeof transformers.channelCreate === "undefined") throw Error("Missing 'channelCreate' transformer"); - functions.set("t_channelCreate", transformers.channelCreate.handler); - } - } - - temp.push( - "for (let i = 0, {threads} = td, {length} = threads; i < length; i++){", - `const channel = ${caching.applyTransformers ? "t_channelCreate(client, threads[i])" : "threads[i]"};`, - `await client.cache.set(${CacheElementType.CHANNEL}, channel.id, channel);`, - "}" - ); - } - - if (typeof caching.enabled.voiceState !== "undefined") { - if (caching.applyTransformers) { - if (!functions.has("t_voiceStateUpdate")) { - if (typeof transformers.voiceStateUpdate === "undefined") throw Error("Missing 'voiceStateUpdate' transformer"); - functions.set("t_voiceStateUpdate", transformers.voiceStateUpdate.handler); - } - } - const key = caching.customKeys?.guild_voice_states ?? "voice_states"; - const vCid = caching.customKeys?.voice_state_channel_id ?? "channel_id"; - const vUid = caching.customKeys?.voice_state_user_id ?? "user_id"; - - this.#createListener( - builder, - functions, - GatewayEvent.VoiceStateUpdate, - "voiceStateUpdate", - listeners.voiceStateUpdate, - transformers.voiceStateUpdate, - { - when: caching.enabled.voiceState, - content: `await client.cache.set(${CacheElementType.VOICE_STATE}, ${caching.applyTransformers - ? `\`\${td.${vUid}}:\${td.${vCid}}\`` - : `\`\${data.d.${vUid}}:\${data.d.${vCid}}\``}, data.d);` - } - ); - - temp.push( - `for (let i = 0, {${key}} = td, {length} = ${key}; i < length; i++){`, - `const voice = ${caching.applyTransformers ? `t_voiceStateUpdate(client, ${key}[i])` : `${key}[i]`};`, - `await client.cache.set(${CacheElementType.VOICE_STATE}, \`\${voice.${vUid}}:\${voice.${vCid}}\`, voice);`, - "}" - ); - } - - temp.push( - `await client.cache.set(${CacheElementType.GUILD},`, - caching.applyTransformers ? "td.id, td" : "data.d.id, {...data.d,channels: undefined,threads:undefined,voice_states:undefined}", - ");" - ); - - this.#createListener( - builder, - functions, - GatewayEvent.GuildCreate, - "guildCreate", - listeners.guildCreate, - transformers.guildCreate, - { when: enabledGuild.create, content: temp.join("") } - ); - } - if (typeof enabledGuild?.update !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.GuildUpdate, - "guildUpdate", - listeners.guildUpdate, - transformers.guildUpdate, - { - when: enabledGuild.update, - content: `await client.cache.set(${CacheElementType.GUILD}, ${caching.applyTransformers ? "td.id, td" : "data.d.id, data.d"});` - } - ); - } - if (typeof enabledGuild?.delete !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.GuildDelete, - "guildDelete", - listeners.guildDelete, - transformers.guildDelete, - { - when: enabledGuild.delete, - content: `await client.cache.delete(${CacheElementType.GUILD}, ${caching.applyTransformers ? "td.id" : "data.d.id"});` - } - ); - } - if (typeof enabledChannel?.create !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.ChannelCreate, - "channelCreate", - listeners.channelCreate, - transformers.channelCreate, - { - when: enabledChannel.create, - content: `await client.cache.set(${CacheElementType.CHANNEL}, ${caching.applyTransformers ? "td.id, td" : "data.d.id, data.d"});` - } - ); - } - if (typeof enabledChannel?.update !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.ChannelUpdate, - "channelUpdate", - listeners.channelUpdate, - transformers.channelUpdate, - { - when: enabledChannel.update, - content: `await client.cache.set(${CacheElementType.CHANNEL}, ${caching.applyTransformers ? "td.id, td" : "data.d.id, data.d"});` - } - ); - } - if (typeof enabledChannel?.delete !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.ChannelDelete, - "channelDelete", - listeners.channelDelete, - transformers.channelDelete, - { - when: enabledChannel.delete, - content: `await client.cache.delete(${CacheElementType.CHANNEL}, ${caching.applyTransformers ? "td.id" : "data.d.id"});` - } - ); - } - if (typeof enabledThreads?.create !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.ThreadCreate, - "threadCreate", - listeners.threadCreate, - transformers.threadCreate, - { - when: enabledThreads.create, - content: `await client.cache.set(${CacheElementType.CHANNEL}, ${caching.applyTransformers ? "td.id, td" : "data.d.id, data.d"});` - } - ); - } - if (typeof enabledThreads?.update !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.ThreadUpdate, - "threadUpdate", - listeners.threadUpdate, - transformers.threadUpdate, - { - when: enabledThreads.update, - content: `await client.cache.set(${CacheElementType.CHANNEL}, ${caching.applyTransformers ? "td.id, td" : "data.d.id, data.d"});` - } - ); - } - if (typeof enabledThreads?.delete !== "undefined") { - this.#createListener( - builder, - functions, - GatewayEvent.ThreadDelete, - "threadDelete", - listeners.threadDelete, - transformers.threadDelete, - { - when: enabledThreads.delete, - content: `await client.cache.delete(${CacheElementType.CHANNEL}, ${caching.applyTransformers ? "td.id" : "data.d.id"});` - } - ); - } - if (typeof caching.enabled.self !== "undefined") { - if (caching.applyTransformers) { - if (!functions.has("t_userUpdate")) { - if (typeof transformers.userUpdate === "undefined") throw new Error("Missing 'userUpdate' transformer"); - if (transformers.userUpdate.return === TransformerReturnType.MULTIPLE) throw new Error("The transformer for 'userUpdate' should only return 1 value"); - functions.set("t_userUpdate", transformers.userUpdate.handler); - } - } - - this.#createListener( - builder, - functions, - GatewayEvent.UserUpdate, - "userUpdate", - listeners.userUpdate, - transformers.userUpdate, - { - when: caching.enabled.self, - content: `client.user = ${caching.applyTransformers ? "td" : "data.d"}` - } - ); - } - } - //#endregion - - const names = functions.keys(); - const handlers = functions.values(); - const compiledListeners = [...builder.values()].join(""); - this.#debug(DebugIdentifier.CompiledListeners, compiledListeners); - // eslint-disable-next-line @typescript-eslint/no-implied-eval - return new Function("client", ...names, `return async (data) => { ${compiledListeners} }`)(this, ...handlers) as never; - } - - #createListener( - builder: Map, - functions: Map any>, - event: GatewayEvent, - name: string, - handler: ((...args: any) => any) | undefined, - transformer: Transformer | undefined, - extra: { when: CacheExecutionPolicy, content: string } | undefined = undefined - ): void { - const temp = [`else if(data.t === "${event}"){`]; - if (typeof handler !== "undefined") functions.set(name, handler); - - const transf = `t_${name}`; - if (typeof transformer !== "undefined") { - functions.set(transf, transformer.handler); - - switch (transformer.return) { - case TransformerReturnType.SINGLE: { - if (typeof extra !== "undefined") { - if (extra.when === CacheExecutionPolicy.FIRST) { - temp.push( - `const td = await ${transf}(client, data.d);`, - extra.content, - typeof handler !== "undefined" ? `await ${name}(td)` : "" - ); - } else { - temp.push( - `const td = await ${transf}(client, data.d);`, - typeof handler !== "undefined" ? `await ${name}(td)` : "", - extra.content - ); - } - } else if (typeof handler !== "undefined") temp.push(`await ${name}(await ${transf}(client, data.d));`); - break; - } - case TransformerReturnType.MULTIPLE: { - if (typeof extra !== "undefined") { - if (extra.when === CacheExecutionPolicy.FIRST) { - temp.push( - `const td = await ${transf}(client, data.d);`, - extra.content, - typeof handler !== "undefined" ? `await ${name}(...td);` : "" - ); - } else { - temp.push( - `const td = await ${transf}(client, data.d);`, - typeof handler !== "undefined" ? `await ${name}(...td);` : "", - extra.content - ); - } - } else if (typeof handler !== "undefined") temp.push(`await ${name}(...(await ${transf}(client, data.d)));`); - break; - } - } - } else if (typeof extra !== "undefined") { - if (extra.when === CacheExecutionPolicy.FIRST) - temp.push("const td = data.d;", extra.content, typeof handler !== "undefined" ? `await ${name}(client, td);` : ""); - else - temp.push("const td = data.d;", typeof handler !== "undefined" ? `await ${name}(client, td);` : "", extra.content); - } else if (typeof handler !== "undefined") temp.push(`await ${name}(client, data.d);`); - - if (temp.length === 1) return; - - // Dead code elimination - // transformed data sometimes might not be needed and its easier to do it this way - // than adding more complexity to all the branches that add listeners - if (temp.length === 4) { - if (temp[2] === "") { - // eslint-disable-next-line @typescript-eslint/prefer-destructuring - temp[2] = temp[3]; - temp.pop(); - } else if (temp[3] === "") - temp.pop(); - - if (!temp[1].startsWith("const td =")) - throw new Error("There was something wrong internally with the compiler"); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (temp.length === 4 && !temp[3].includes("td")) { - if (!temp[2].includes("td")) { - // eslint-disable-next-line @typescript-eslint/prefer-destructuring - temp[1] = temp[2]; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - temp[2] = temp.pop()!; - functions.delete(transf); - } - //@ts-expect-error We are rechecking because we modify the length inside this case - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } else if (temp.length === 3 && !temp[2].includes("td")) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - temp[1] = temp.pop()!; - functions.delete(transf); - } - } +export interface CreateClientOptions> extends Omit, CompilerOptions { + token: string; + listeners: Listeners; + caching?: CachingOptions; + debug?: DebugFunction; +} - temp.push("}"); +export async function createClient = Transformers>(options: CreateClientOptions): Promise { + const compiler = new ListenerCompiler({ + transformers: options.transformers, + transformClient: options.transformClient + }); - builder.set(event, temp.join("")); - } -} + compiler.addListenersFromObject(options.listeners); + if (typeof options.caching !== "undefined") compiler.appendCachingHandlers(options.caching); -export async function createClient = ClientOptions>(options: O): Promise>> { - if (typeof options.caching?.customKeys !== "undefined") options.caching.customKeys = { ...options.customCacheKeys, ...options.caching.customKeys }; - else if (typeof options.caching !== "undefined") options.caching.customKeys = options.customCacheKeys; + const client = new Client({ + intents: options.intents, + presence: options.presence, + useDebugRest: typeof options.debug !== "undefined", + cachingManager: options.cachingManager + }, options.debug); - return new Promise((res) => { - // This is a promise executer, it doesn't need to be async - // eslint-disable-next-line @typescript-eslint/no-floating-promises - new Client( - { - intents: Array.isArray(options.intents) ? options.intents.reduce((prev, curr) => prev | curr, 0) : options.intents, - listeners: options.listeners, - transformers: options.transformers, - presence: options.presence, - caching: options.caching, - useDebugRest: options.useDebugRest ?? options.attachDebugListener, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setup: typeof options.setup !== "undefined" ? async (client) => { await options.setup!(client); res(client); } : res - }, - options.attachDebugListener - ? options.debugListener ?? ((identifier, payload) => { - console.log(identifier, payload ?? ""); - }) - : undefined - ).login(options.token); - }); + await client.login(options.token, compiler.getDispatchFunction(client, client.ws.resumeInfo)); + return client; } diff --git a/packages/core/src/compiler.ts b/packages/core/src/compiler.ts new file mode 100644 index 0000000..aa4c82e --- /dev/null +++ b/packages/core/src/compiler.ts @@ -0,0 +1,415 @@ +import { + CachingDelegationType, + TransformerReturnType, + CacheExecutionPolicy, + CacheElementType, + GatewayEvent +} from "#enums"; + +import type { DispatchFunction } from "#ws"; +import type { + SelectiveCache, + CachingOptions, + Transformers, + Transformer, + MockClient, + Awaitable, + Listeners, + Ready +} from "./typings/index.js"; + +export interface CompilerOptions> { + transformers?: T; + transformClient?: boolean; +} + +export class ListenerCompiler> { + readonly #stack: Map; + readonly #callbacks: Map any>; + + readonly #transformers: T; + readonly #shouldTransformClientUser: boolean; + + public constructor(options: CompilerOptions = {}) { + this.#stack = new Map(); + this.#callbacks = new Map(); + this.#transformers = options.transformers ?? {}; + this.#shouldTransformClientUser = options.transformClient ?? false; + + this.#stack.set("ready", ""); + } + + #createListener( + event: GatewayEvent, + name: string, + extra: { when: CacheExecutionPolicy, content: string } | undefined = undefined + ): void { + const handler = this.#callbacks.get(name); + const transformer: Transformer = this.#transformers[name]; + const temp = [`else if(payload.t === "${event}"){`]; + const transf = `t_${name}`; + + if (typeof transformer !== "undefined") { + this.#callbacks.set(transf, transformer.handler); + const spread = transformer.return === TransformerReturnType.MULTIPLE ? "..." : ""; + + if (typeof extra !== "undefined") { + if (extra.content.includes("td.") || typeof handler !== "undefined") temp.push(`const td = await ${transf}(client_ptr, payload.d);`); + if (extra.when === CacheExecutionPolicy.FIRST) { + temp.push( + extra.content, + typeof handler !== "undefined" ? `await ${name}(${spread}td)` : "" + ); + } else { + temp.push( + typeof handler !== "undefined" ? `await ${name}(${spread}td)` : "", + extra.content + ); + } + } else if (typeof handler !== "undefined") temp.push(`await ${name}(${spread}(await ${transf}(client_ptr, payload.d)));`); + } else if (typeof extra !== "undefined") { + if (extra.content.includes("td.")) throw new Error(`No transformer found to apply to the caching handler '${event}'`); + if (extra.when === CacheExecutionPolicy.FIRST) + temp.push(extra.content, typeof handler !== "undefined" ? `await ${name}(client_ptr, payload.d);` : ""); + else + temp.push(typeof handler !== "undefined" ? `await ${name}(client_ptr, payload.d);` : "", extra.content); + } else if (typeof handler !== "undefined") temp.push(`await ${name}(client_ptr, payload.d);`); + + if (temp.length === 1) return; + temp.push("}"); + + this.#stack.set(event, temp.join("")); + } + + #addReadyListener(once: boolean = false, listener?: (client: C, payload: Ready["d"]) => Awaitable): void { + const onceListener = this.#callbacks.get("once_ready"); + const readyArr = []; + readyArr.push( + "if(payload.t === \"READY\"){", + "Object.assign(client_ptr,{user:" + ); + + if (this.#shouldTransformClientUser && typeof this.#transformers.userUpdate !== "undefined") { + if (this.#transformers.userUpdate.return === TransformerReturnType.MULTIPLE) throw new Error("The transformer for 'userUpdate' should only return 1 value"); + this.#callbacks.set("t_userUpdate", this.#transformers.userUpdate.handler); + readyArr.push("await t_userUpdate(client, payload.d.user)"); + } else readyArr.push("payload.d.user"); + + readyArr.push( + `,sessionId:payload.d.session_id,application:payload.d.application,ready:${once ? "false" : "true"}});`, + "Object.assign(resume_info_ptr,{url:payload.d.resume_gateway_url,id:payload.d.session_id});" + ); + + if (typeof listener !== "undefined") this.#callbacks.set("ready", listener); + if (once || onceListener) readyArr.push("if(!client_ptr.ready){client_ptr.ready=true;"); + + if (typeof this.#transformers.ready !== "undefined") { + this.#callbacks.set("t_ready", this.#transformers.ready.handler); + const spread = this.#transformers.ready.return === TransformerReturnType.MULTIPLE ? "..." : ""; + if (onceListener) readyArr.push(`await once_ready(${spread}t_ready(client_ptr, payload.d));`); + if (once && typeof listener !== "undefined") readyArr.push(`await ready(${spread}t_ready(client_ptr, payload.d));`); + } else { + if (onceListener) readyArr.push("await once_ready(client_ptr, payload.d);"); + if (once && typeof listener !== "undefined") readyArr.push("await ready(client_ptr, payload.d);"); + } + + if (once || onceListener) readyArr.push("}"); + + if (!once && typeof listener !== "undefined") { + if (typeof this.#transformers.ready !== "undefined") { + const spread = this.#transformers.ready.return === TransformerReturnType.MULTIPLE ? "..." : ""; + readyArr.push(`await ready(${spread}t_ready(client_ptr, payload.d));`); + } else readyArr.push("await ready(client_ptr, payload.d);"); + } + + readyArr.push("}"); + this.#stack.set("ready", readyArr.join("")); + } + + public appendCachingHandlers(options: CachingOptions): this { + if (typeof options !== "undefined" && options.delegate !== CachingDelegationType.TRANSFORMERS) { + if (options.delegate === CachingDelegationType.EXTERNAL && options.applyTransformers === true && !options.safeToTransform) { + process.emitWarning("Transformers will be applied to external solution!", { + code: "EXTERNAL_TRANSFORMERS", + detail: "Some external caching managers might not support transformers, please read their documentation before proceeding." + }); + } + + if (Object.keys(options.enabled).length === 0) throw new Error("Got unexpected empty object"); + + const defaults: SelectiveCache = { + create: CacheExecutionPolicy.FIRST, + update: CacheExecutionPolicy.LAST, + delete: CacheExecutionPolicy.LAST + }; + + // const enabledUser = caching.enabled.user === true ? defaults : typeof caching.enabled.user === "object" ? caching.enabled.user : undefined; + const enabledGuild = options.enabled.guild === true ? defaults : typeof options.enabled.guild === "object" ? options.enabled.guild : undefined; + const enabledChannel = options.enabled.channel === true ? defaults : typeof options.enabled.channel === "object" ? options.enabled.channel : undefined; + const enabledThreads = options.enabled.channel === true + ? defaults + : typeof options.enabled.channel === "object" + ? options.enabled.channel.threads === true + ? defaults + : typeof options.enabled.channel.threads === "object" + ? options.enabled.channel.threads + : undefined + : undefined; + + if (typeof options.enabled.voiceState !== "undefined" && typeof enabledGuild?.create === "undefined") throw new Error("The 'voiceState' cache needs the `guildCreate' cache to be enabled"); + + if (typeof enabledGuild?.create !== "undefined") { + const temp: Array = []; + if (typeof enabledChannel?.create !== "undefined") { + if (options.applyTransformers) { + if (!this.#callbacks.has("t_channelCreate")) { + if (typeof this.#transformers.channelCreate === "undefined") throw Error("Missing 'channelCreate' transformer"); + this.#callbacks.set("t_channelCreate", this.#transformers.channelCreate.handler); + } + } + temp.push( + "for (let i = 0, {channels} = td, {length} = channels; i < length; i++){", + `const channel = ${options.applyTransformers ? "t_channelCreate(client_ptr, channels[i])" : "channels[i]"};`, + `await client_ptr.cache.set(${CacheElementType.CHANNEL}, channel.id, channel);`, + "}" + ); + } + + if (typeof enabledThreads?.create !== "undefined") { + if (options.applyTransformers) { + if (!this.#callbacks.has("t_channelCreate")) { + if (typeof this.#transformers.channelCreate === "undefined") throw Error("Missing 'channelCreate' transformer"); + this.#callbacks.set("t_channelCreate", this.#transformers.channelCreate.handler); + } + } + + temp.push( + "for (let i = 0, {threads} = td, {length} = threads; i < length; i++){", + `const channel = ${options.applyTransformers ? "t_channelCreate(client_ptr, threads[i])" : "threads[i]"};`, + `await client_ptr.cache.set(${CacheElementType.CHANNEL}, channel.id, channel);`, + "}" + ); + } + + if (typeof options.enabled.voiceState !== "undefined") { + if (options.applyTransformers) { + if (!this.#callbacks.has("t_voiceStateUpdate")) { + if (typeof this.#transformers.voiceStateUpdate === "undefined") throw Error("Missing 'voiceStateUpdate' transformer"); + this.#callbacks.set("t_voiceStateUpdate", this.#transformers.voiceStateUpdate.handler); + } + } + const key = options.customKeys?.guild_voice_states ?? "voice_states"; + const vCid = options.customKeys?.voice_state_channel_id ?? "channel_id"; + const vUid = options.customKeys?.voice_state_user_id ?? "user_id"; + + this.#createListener( + GatewayEvent.VoiceStateUpdate, + "voiceStateUpdate", + { + when: options.enabled.voiceState, + content: `await client_ptr.cache.set(${CacheElementType.VOICE_STATE}, ${options.applyTransformers + ? `\`\${td.${vUid}}:\${td.${vCid}}\`` + : `\`\${payload.d.${vUid}}:\${payload.d.${vCid}}\``}, payload.d);` + } + ); + + temp.push( + `for (let i = 0, {${key}} = td, {length} = ${key}; i < length; i++){`, + `const voice = ${options.applyTransformers ? `t_voiceStateUpdate(client_ptr, ${key}[i])` : `${key}[i]`};`, + `await client_ptr.cache.set(${CacheElementType.VOICE_STATE}, \`\${voice.${vUid}}:\${voice.${vCid}}\`, voice);`, + "}" + ); + } + + temp.push( + `await client_ptr.cache.set(${CacheElementType.GUILD},`, + options.applyTransformers ? "td.id, td" : "payload.d.id, {...payload.d,channels: undefined,threads:undefined,voice_states:undefined}", + ");" + ); + + this.#createListener( + GatewayEvent.GuildCreate, + "guildCreate", + { when: enabledGuild.create, content: temp.join("") } + ); + } + if (typeof enabledGuild?.update !== "undefined") { + this.#createListener( + GatewayEvent.GuildUpdate, + "guildUpdate", + { + when: enabledGuild.update, + content: `await client_ptr.cache.set(${CacheElementType.GUILD}, ${options.applyTransformers ? "td.id, td" : "payload.d.id, payload.d"});` + } + ); + } + if (typeof enabledGuild?.delete !== "undefined") { + this.#createListener( + GatewayEvent.GuildDelete, + "guildDelete", + { + when: enabledGuild.delete, + content: `await client_ptr.cache.delete(${CacheElementType.GUILD}, ${options.applyTransformers ? "td.id" : "payload.d.id"});` + } + ); + } + if (typeof enabledChannel?.create !== "undefined") { + this.#createListener( + GatewayEvent.ChannelCreate, + "channelCreate", + { + when: enabledChannel.create, + content: `await client_ptr.cache.set(${CacheElementType.CHANNEL}, ${options.applyTransformers ? "td.id, td" : "payload.d.id, payload.d"});` + } + ); + } + if (typeof enabledChannel?.update !== "undefined") { + this.#createListener( + GatewayEvent.ChannelUpdate, + "channelUpdate", + { + when: enabledChannel.update, + content: `await client_ptr.cache.set(${CacheElementType.CHANNEL}, ${options.applyTransformers ? "td.id, td" : "payload.d.id, payload.d"});` + } + ); + } + if (typeof enabledChannel?.delete !== "undefined") { + this.#createListener( + GatewayEvent.ChannelDelete, + "channelDelete", + { + when: enabledChannel.delete, + content: `await client_ptr.cache.delete(${CacheElementType.CHANNEL}, ${options.applyTransformers ? "td.id" : "payload.d.id"});` + } + ); + } + if (typeof enabledThreads?.create !== "undefined") { + this.#createListener( + GatewayEvent.ThreadCreate, + "threadCreate", + { + when: enabledThreads.create, + content: `await client_ptr.cache.set(${CacheElementType.CHANNEL}, ${options.applyTransformers ? "td.id, td" : "payload.d.id, payload.d"});` + } + ); + } + if (typeof enabledThreads?.update !== "undefined") { + this.#createListener( + GatewayEvent.ThreadUpdate, + "threadUpdate", + { + when: enabledThreads.update, + content: `await client_ptr.cache.set(${CacheElementType.CHANNEL}, ${options.applyTransformers ? "td.id, td" : "payload.d.id, payload.d"});` + } + ); + } + if (typeof enabledThreads?.delete !== "undefined") { + this.#createListener( + GatewayEvent.ThreadDelete, + "threadDelete", + { + when: enabledThreads.delete, + content: `await client_ptr.cache.delete(${CacheElementType.CHANNEL}, ${options.applyTransformers ? "td.id" : "payload.d.id"});` + } + ); + } + if (typeof options.enabled.self !== "undefined") { + if (options.applyTransformers) { + if (!this.#callbacks.has("t_userUpdate")) { + if (typeof this.#transformers.userUpdate === "undefined") throw new Error("Missing 'userUpdate' transformer"); + if (this.#transformers.userUpdate.return === TransformerReturnType.MULTIPLE) throw new Error("The transformer for 'userUpdate' should only return 1 value"); + this.#callbacks.set("t_userUpdate", this.#transformers.userUpdate.handler); + } + } + + this.#createListener( + GatewayEvent.UserUpdate, + "userUpdate", + { + when: options.enabled.self, + content: `client_ptr.user = ${options.applyTransformers ? "td" : "payload.d"}` + } + ); + } + } + + return this; + } + + /** + * @param name - The name of the event you are listening to + * @param handler - The handler that will be executed when the event is received + * @param once - Wether the `ready` listener should only be called once + * @returns + */ + public addListener = Listeners, K extends (keyof L) & string = (keyof L) & string>( + name: K, + handler: L[K] & {}, + once: boolean = false + ): this { + if (name === "setup") { + this.#callbacks.set("once_ready", handler); + return this; + } else if (name === "ready") { + this.#addReadyListener(once, handler); + return this; + } + + const parsedName = `${name[0].toUpperCase()}${name.slice(1)}`; + const event: GatewayEvent = GatewayEvent[parsedName]; + + this.#callbacks.set(name, handler); + + this.#createListener( + event, + name + ); + + return this; + } + + public getCompilationOutput(): { + handlers: { + names: Array, + callbacks: Array<(...args: any) => any> + }, + stack: string + } { + const hasReadyListener = this.#stack.get("ready") !== ""; + if (!hasReadyListener) this.#addReadyListener(); + + const names = [...this.#callbacks.keys()]; + const handlers = [...this.#callbacks.values()]; + const compiledListeners = [...this.#stack.values()].join(""); + + return { + handlers: { + names, + callbacks: handlers + }, + stack: compiledListeners + }; + } + + public clearStack(): void { + this.#stack.clear(); + this.#callbacks.clear(); + this.#stack.set("ready", ""); + } + + public getDispatchFunction(clientPointer: C, resumeInfoPointer: { url: string, id: string }): DispatchFunction { + const { handlers, stack } = this.getCompilationOutput(); + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function("client_ptr", "resume_info_ptr", ...handlers.names, `return async (payload) => { ${stack} }`)(clientPointer, resumeInfoPointer, ...handlers.callbacks) as never; + } + + public addListenersFromObject(listeners: Listeners): this { + for (let i = 0, entries = Object.entries(listeners), { length } = entries; i < length; i++) { + const [event, handler] = <[string, (...args: any) => any]>entries[i]; + // There is no need for us to strongly type this + this.addListener(event, handler); + } + + return this; + } +} diff --git a/packages/core/src/enums/interaction.ts b/packages/core/src/enums/interaction.ts index 99e7ce0..44dc9d2 100644 --- a/packages/core/src/enums/interaction.ts +++ b/packages/core/src/enums/interaction.ts @@ -46,7 +46,14 @@ export const enum ComponentType { UserSelect, RoleSelect, MentionableSelect, - ChannelSelect + ChannelSelect, + Section, + TextDisplay, + Thumbnail, + MediaGallery, + File, + Separator, + Container = 17 } export const enum ButtonStyle { @@ -54,7 +61,8 @@ export const enum ButtonStyle { Secondary, Success, Danger, - Link + Link, + Premium } export const enum TextInputStyle { diff --git a/packages/core/src/enums/message.ts b/packages/core/src/enums/message.ts index f0345a9..9e89b0e 100644 --- a/packages/core/src/enums/message.ts +++ b/packages/core/src/enums/message.ts @@ -49,7 +49,9 @@ export const enum MessageFlags { LOADING = 128, FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 256, SUPPRESS_NOTIFICATIONS = 4096, - IS_VOICE_MESSAGE = 8192 + IS_VOICE_MESSAGE = 8192, + HAS_SNAPSHOT = 16384, + IS_COMPONENTS_V2 = 32768 } export const enum AttachmentFlags { diff --git a/packages/core/src/http/rest.ts b/packages/core/src/http/rest.ts index 1e205f9..6af7518 100644 --- a/packages/core/src/http/rest.ts +++ b/packages/core/src/http/rest.ts @@ -48,15 +48,18 @@ export class RestError extends Error { // I ran out of ideas for naming this thing type ExtractedData = ({ data: { attachments: Array | undefined } } | { attachments: Array | undefined }) & { reason?: string }; +export type TokenType = "Bearer" | "Bot"; export class REST { // eslint-disable-next-line @typescript-eslint/naming-convention public static readonly BaseURL = "https://discord.com/api/v10/"; #token?: string | undefined; + #tokenType: TokenType; - public constructor(token?: string) { + public constructor(token?: string, tokenType: TokenType = "Bot") { this.#token = token; + this.#tokenType = tokenType; } public async makeAPIRequest(method: "GET" | "POST" | "PATCH" | "DELETE" | "PUT", path: string, data: FormData, reason?: string): Promise; @@ -65,7 +68,7 @@ export class REST { const opts: RequestInit = { method, headers: { - Authorization: `Bot ${this.#token}`, + Authorization: `${this.#tokenType} ${this.#token}`, // eslint-disable-next-line @typescript-eslint/naming-convention "User-Agent": `DiscordBot/LilyBird/${(<{ version: string }>packageJson).version}` } @@ -128,8 +131,9 @@ export class REST { return await response.json(); } - public setToken(token: string | undefined): void { + public setToken(token: string | undefined, tokenType: TokenType = "Bot"): void { this.#token = token; + this.#tokenType = tokenType; } //#region Gateway @@ -517,10 +521,6 @@ export class REST { //#endregion Emoji //#region Guild - public async createGuild(body: Guild.Create.GuildJSONParams): Promise { - return this.makeAPIRequest("POST", "guilds", body); - } - public async getGuild(guildId: string, withCounts = false): Promise { return this.makeAPIRequest("GET", `guilds/${guildId}?with_counts=${withCounts}`); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ecc67ec..08f9907 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,5 +4,7 @@ export { REST } from "./http/rest.js"; export * as CDN from "./http/cdn.js"; export * from "./enums/index.js"; +export * from "./ws/manager.js"; +export * from "./compiler.js"; export * from "./client.js"; export * from "./utils.js"; diff --git a/packages/core/src/typings/client.ts b/packages/core/src/typings/client.ts index 62b52be..134f9dd 100644 --- a/packages/core/src/typings/client.ts +++ b/packages/core/src/typings/client.ts @@ -1,14 +1,14 @@ -import type { CachingManager } from "../cache/manager.js"; import type { CacheManagerStructure } from "./cache-manager.js"; +import type { CachingManager } from "../cache/manager.js"; +import type { DispatchFunction } from "../ws/manager.js"; +import type { Application } from "./application.js"; import type { Awaitable } from "./utils.js"; -import type { Client } from "../client.js"; import type { CachingDelegationType, TransformerReturnType, CacheExecutionPolicy, - DebugIdentifier, - Intents + DebugIdentifier } from "#enums"; import type { @@ -29,7 +29,6 @@ import type { GuildIntegrationsUpdate, MessageReactionRemove, MessagePollVoteRemove, - ReceiveDispatchEvent, GuildStickersUpdate, StageInstanceCreate, StageInstanceUpdate, @@ -77,7 +76,7 @@ import type { Ready } from "./gateway-events.js"; -export type ClientListeners = { +export type Listeners> = { [K in keyof T]?: T[K] extends { handler: unknown } ? (T[K] & {})["handler"] extends ((...args: any) => infer R) ? R extends [unknown, ...Array] ? ((...arg: R) => Awaitable) : ((arg: R) => Awaitable) @@ -87,79 +86,79 @@ export type ClientListeners = { : never }; -export type Transformer = { +export type Transformer = { return: TransformerReturnType, - handler: (...args: [client: Client, payload: T]) => unknown + handler: (...args: [client: C, payload: T]) => unknown }; -export interface Transformers { - raw?: { - return: TransformerReturnType, - handler: (data: ReceiveDispatchEvent) => unknown - }; - ready?: Transformer; - resumed?: Transformer; - applicationCommandPermissionsUpdate?: Transformer; - autoModerationRuleCreate?: Transformer; - autoModerationRuleUpdate?: Transformer; - autoModerationRuleDelete?: Transformer; - autoModerationActionExecution?: Transformer; - channelCreate?: Transformer; - channelUpdate?: Transformer; - channelDelete?: Transformer; - channelPinsUpdate?: Transformer; - threadCreate?: Transformer; - threadUpdate?: Transformer; - threadDelete?: Transformer; - threadListSync?: Transformer; - threadMemberUpdate?: Transformer; - threadMembersUpdate?: Transformer; - guildCreate?: Transformer; - guildUpdate?: Transformer; - guildDelete?: Transformer; - guildAuditLogEntryCreate?: Transformer; - guildBanAdd?: Transformer; - guildBanRemove?: Transformer; - guildEmojisUpdate?: Transformer; - guildStickersUpdate?: Transformer; - guildIntegrationsUpdate?: Transformer; - guildMemberAdd?: Transformer; - guildMemberRemove?: Transformer; - guildMemberUpdate?: Transformer; - guildMembersChunk?: Transformer; - guildRoleCreate?: Transformer; - guildRoleUpdate?: Transformer; - guildRoleDelete?: Transformer; - guildScheduledEventCreate?: Transformer; - guildScheduledEventUpdate?: Transformer; - guildScheduledEventDelete?: Transformer; - guildScheduledEventUserAdd?: Transformer; - guildScheduledEventUserRemove?: Transformer; - integrationCreate?: Transformer; - integrationUpdate?: Transformer; - integrationDelete?: Transformer; - interactionCreate?: Transformer; - inviteCreate?: Transformer; - inviteDelete?: Transformer; - messageCreate?: Transformer; - messageUpdate?: Transformer; - messageDelete?: Transformer; - messageDeleteBulk?: Transformer; - messageReactionAdd?: Transformer; - messageReactionRemove?: Transformer; - messageReactionRemoveAll?: Transformer; - messageReactionRemoveEmoji?: Transformer; - presenceUpdate?: Transformer; - stageInstanceCreate?: Transformer; - stageInstanceUpdate?: Transformer; - stageInstanceDelete?: Transformer; - typingStart?: Transformer; - userUpdate?: Transformer; - voiceStateUpdate?: Transformer; - voiceServerUpdate?: Transformer; - webhookUpdate?: Transformer; - messagePollVoteAdd?: Transformer; - messagePollVoteRemove?: Transformer; +export interface Transformers { + /** + * Special case, its a `ready` handler that only fires once + */ + setup?: Transformer; + ready?: Transformer; + resumed?: Transformer; + applicationCommandPermissionsUpdate?: Transformer; + autoModerationRuleCreate?: Transformer; + autoModerationRuleUpdate?: Transformer; + autoModerationRuleDelete?: Transformer; + autoModerationActionExecution?: Transformer; + channelCreate?: Transformer; + channelUpdate?: Transformer; + channelDelete?: Transformer; + channelPinsUpdate?: Transformer; + threadCreate?: Transformer; + threadUpdate?: Transformer; + threadDelete?: Transformer; + threadListSync?: Transformer; + threadMemberUpdate?: Transformer; + threadMembersUpdate?: Transformer; + guildCreate?: Transformer; + guildUpdate?: Transformer; + guildDelete?: Transformer; + guildAuditLogEntryCreate?: Transformer; + guildBanAdd?: Transformer; + guildBanRemove?: Transformer; + guildEmojisUpdate?: Transformer; + guildStickersUpdate?: Transformer; + guildIntegrationsUpdate?: Transformer; + guildMemberAdd?: Transformer; + guildMemberRemove?: Transformer; + guildMemberUpdate?: Transformer; + guildMembersChunk?: Transformer; + guildRoleCreate?: Transformer; + guildRoleUpdate?: Transformer; + guildRoleDelete?: Transformer; + guildScheduledEventCreate?: Transformer; + guildScheduledEventUpdate?: Transformer; + guildScheduledEventDelete?: Transformer; + guildScheduledEventUserAdd?: Transformer; + guildScheduledEventUserRemove?: Transformer; + integrationCreate?: Transformer; + integrationUpdate?: Transformer; + integrationDelete?: Transformer; + interactionCreate?: Transformer; + inviteCreate?: Transformer; + inviteDelete?: Transformer; + messageCreate?: Transformer; + messageUpdate?: Transformer; + messageDelete?: Transformer; + messageDeleteBulk?: Transformer; + messageReactionAdd?: Transformer; + messageReactionRemove?: Transformer; + messageReactionRemoveAll?: Transformer; + messageReactionRemoveEmoji?: Transformer; + presenceUpdate?: Transformer; + stageInstanceCreate?: Transformer; + stageInstanceUpdate?: Transformer; + stageInstanceDelete?: Transformer; + typingStart?: Transformer; + userUpdate?: Transformer; + voiceStateUpdate?: Transformer; + voiceServerUpdate?: Transformer; + webhookUpdate?: Transformer; + messagePollVoteAdd?: Transformer; + messagePollVoteRemove?: Transformer; } export interface SelectiveCache { @@ -216,31 +215,31 @@ export interface DefaultCache extends BaseCachingStructure { } // eslint-disable-next-line @typescript-eslint/no-empty-object-type -export type ParseCachingManager> = T["caching"] extends {} - ? T["caching"]["applyTransformers"] extends true - ? T["caching"] extends { transformerTypes: (infer U extends CacheWithTransformers["transformerTypes"]) } +export type ParseCachingManager = T extends {} + ? T["applyTransformers"] extends true + ? T extends { transformerTypes: (infer U extends CacheWithTransformers["transformerTypes"]) } ? CachingManager : never - : T["caching"]["delegate"] extends CachingDelegationType.EXTERNAL - ? T["caching"]["manager"] & {} + : T["delegate"] extends CachingDelegationType.EXTERNAL + ? T["manager"] & {} : CachingManager : never; export type DebugFunction = (identifier: DebugIdentifier, payload?: unknown) => any; -export interface BaseClientOptions { +export interface ClientOptions { intents: number; - listeners: ClientListeners; - transformers?: T; + dispatch?: DispatchFunction; presence?: UpdatePresenceStructure; - caching?: (DefaultCache | ExternalCache | TransformersCache) & ApplyTransformers; useDebugRest?: boolean; - setup?: (client: Client) => Awaitable; + cachingManager?: CacheManagerStructure; } -export interface ClientOptions extends Omit, "intents"> { - intents: Array | number; - token: string; - attachDebugListener?: boolean; - customCacheKeys?: BaseCachingStructure["customKeys"]; - debugListener?: (identifier: DebugIdentifier, payload: unknown) => void; +export type CachingOptions = (DefaultCache | ExternalCache | TransformersCache) & ApplyTransformers; + +export interface MockClient { + /** By default this is a UserStructure, but can change according to your transformers*/ + readonly user: any; + readonly sessionId: string; + readonly application: Application.Structure; + readonly cache: CacheManagerStructure; } diff --git a/packages/core/src/typings/guild.ts b/packages/core/src/typings/guild.ts index e23e6e8..20cbfd5 100644 --- a/packages/core/src/typings/guild.ts +++ b/packages/core/src/typings/guild.ts @@ -366,26 +366,6 @@ export declare namespace Guild { } export namespace Create { - /** - * @see {@link https://discord.com/developers/docs/resources/guild#create-guild-json-params} - */ - export interface GuildJSONParams { - name: string; - icon?: DiscordImageData; - verification_level?: VerificationLevel; - default_message_notifications?: DefaultMessageNotificationLevel; - explicit_content_filter?: ExplicitContentFilterLevel; - roles?: Array; - channels?: Partial; - afk_channel_id?: string; - afk_timeout?: number; - system_channel_id?: string; - /** - * Bitfield of {@link SystemChannelFlags} - */ - system_channel_flags?: number; - } - /** * @see {@link https://discord.com/developers/docs/resources/guild#create-guild-channel-json-params} */ diff --git a/packages/core/src/typings/message-components.ts b/packages/core/src/typings/message-components.ts index 903e289..e375624 100644 --- a/packages/core/src/typings/message-components.ts +++ b/packages/core/src/typings/message-components.ts @@ -2,14 +2,32 @@ import type { ButtonStyle, ChannelType, ComponentType, TextInputStyle } from "#e import type { Emoji } from "./emoji.js"; export declare namespace Component { - export type Structure = ActionRowStructure | ButtonStructure | SelectMenuStructure | TextInputStructure; + export type Structure = ActionRowStructure + | ButtonStructure + | StringSelectStructure + | TextInputStructure + | UserSelectStructure + | RoleSelectStructure + | MentionableSelectStructure + | ChannelSelectStructure + | SectionStructure + | TextDisplayStructure + | ThumbnailStructure + | MediaGalleryStructure + | FileStructure + | SeparatorStructure + | ContainerStructure; + /** + * @see {@link https://discord.com/developers/docs/components/reference#anatomy-of-a-component} + */ export interface Base { type: ComponentType; + id?: number; } /** - * @see {@link https://discord.com/developers/docs/interactions/message-components#action-rows} + * @see {@link https://discord.com/developers/docs/components/reference#action-row} */ export interface ActionRowStructure extends Base { type: ComponentType.ActionRow; @@ -17,7 +35,7 @@ export declare namespace Component { } /** - * @see {@link https://discord.com/developers/docs/interactions/message-components#button-object-button-structure} + * @see {@link https://discord.com/developers/docs/components/reference#button} */ export interface ButtonStructure extends Base { type: ComponentType.Button; @@ -25,27 +43,26 @@ export declare namespace Component { label?: string; emoji?: Pick; custom_id?: string; + sku_id?: string; url?: string; disabled?: boolean; } /** - * @see {@link https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure} + * @see {@link https://discord.com/developers/docs/components/reference#string-select} */ - export interface SelectMenuStructure extends Base { - type: ComponentType.StringSelect | ComponentType.UserSelect | ComponentType.RoleSelect | ComponentType.MentionableSelect | ComponentType.ChannelSelect; + export interface StringSelectStructure extends Base { + type: ComponentType.StringSelect; custom_id: string; options?: Array; - channel_types?: Array; placeholder?: string; - default_values?: Array; min_values?: number; max_values?: number; disabled?: boolean; } /** - * @see {@link https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure} + * @see {@link https://discord.com/developers/docs/components/reference#string-select-select-option-structure} */ export interface SelectOptionStructure { label: string; @@ -56,15 +73,7 @@ export declare namespace Component { } /** - * @see {@link https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-default-value-structure} - */ - export interface SelectDefaultValueStructure { - id: string; - type: "user" | "role" | "channel"; - } - - /** - * @see {@link https://discord.com/developers/docs/interactions/message-components#text-input-object-text-input-structure} + * @see {@link https://discord.com/developers/docs/components/reference#text-input} */ export interface TextInputStructure { type: ComponentType.TextInput; @@ -77,4 +86,148 @@ export declare namespace Component { value?: string; placeholder?: string; } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#user-select} + */ + export interface UserSelectStructure extends Base { + type: ComponentType.UserSelect; + custom_id: string; + placeholder?: string; + default_values?: Array; + min_values?: number; + max_values?: number; + disabled?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#user-select-select-default-value-structure} + */ + export interface SelectDefaultValueStructure { + id: string; + type: "user" | "role" | "channel"; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#role-select} + */ + export interface RoleSelectStructure extends Base { + type: ComponentType.RoleSelect; + custom_id: string; + placeholder?: string; + default_values?: Array; + min_values?: number; + max_values?: number; + disabled?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#mentionable-select} + */ + export interface MentionableSelectStructure extends Base { + type: ComponentType.MentionableSelect; + custom_id: string; + placeholder?: string; + default_values?: Array; + min_values?: number; + max_values?: number; + disabled?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#channel-select} + */ + export interface ChannelSelectStructure extends Base { + type: ComponentType.ChannelSelect; + custom_id: string; + channel_types?: Array; + placeholder?: string; + default_values?: Array; + min_values?: number; + max_values?: number; + disabled?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#section} + */ + export interface SectionStructure extends Base { + type: ComponentType.Section; + components: Array; + accessory: ThumbnailStructure | ButtonStructure; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#text-display} + */ + export interface TextDisplayStructure extends Base { + type: ComponentType.TextDisplay; + content: string; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#thumbnail} + */ + export interface ThumbnailStructure extends Base { + type: ComponentType.Thumbnail; + media: UnfurledMediaItemStructure; + description?: string; + spoiler?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#unfurled-media-item-structure} + */ + export interface UnfurledMediaItemStructure { + url: string; + proxy_url?: string; + height?: number | null; + width?: number | null; + content_type?: string; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#media-gallery} + */ + export interface MediaGalleryStructure extends Base { + type: ComponentType.MediaGallery; + items: Array; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#media-gallery-media-gallery-item-structure} + */ + export interface MediaGalleryItemStructure extends Base { + media: UnfurledMediaItemStructure; + description?: string; + spoiler?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#file} + */ + export interface FileStructure extends Base { + type: ComponentType.File; + file: UnfurledMediaItemStructure; + spoiler?: boolean; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#separator} + */ + export interface SeparatorStructure extends Base { + type: ComponentType.Separator; + divider?: boolean; + spacing?: number; + } + + /** + * @see {@link https://discord.com/developers/docs/components/reference#container} + */ + export interface ContainerStructure extends Base { + type: ComponentType.Container; + components: Array; + accent_color?: number | null; + spoiler?: boolean; + } } diff --git a/packages/core/src/typings/message.ts b/packages/core/src/typings/message.ts index c46c34b..cc8a1da 100644 --- a/packages/core/src/typings/message.ts +++ b/packages/core/src/typings/message.ts @@ -156,6 +156,9 @@ export declare namespace Message { files?: Array; payload_json?: string; attachments?: Array>; + /** + * Bitfield of {@link MessageFlags} + */ flags?: number; enforce_nonce?: boolean; poll?: Poll.CreateStructure; @@ -164,6 +167,9 @@ export declare namespace Message { export interface EditJSONParams { content?: string; embeds?: Array; + /** + * Bitfield of {@link MessageFlags} + */ flags?: number; allowed_mentions?: Channel.AllowedMentionsStructure; components?: Array; diff --git a/packages/core/src/ws/manager.ts b/packages/core/src/ws/manager.ts index bc29a67..27eea44 100644 --- a/packages/core/src/ws/manager.ts +++ b/packages/core/src/ws/manager.ts @@ -21,14 +21,14 @@ interface ManagerOptions { export type DispatchFunction = (data: ReceiveDispatchEvent) => any; export class WebSocketManager { - readonly #dispatch: DispatchFunction; readonly #debug: DebugFunction | undefined; + readonly #options: Required; + #dispatch: DispatchFunction | undefined; #sequenceNumber: number | null = null; #isResuming = false; #ws!: WebSocket; #gatewayInfo!: GetGatewayBotResponse; - #options: Required; #timer?: Timer; // eslint-disable-next-line @typescript-eslint/naming-convention #gotACK: boolean = true; @@ -39,7 +39,7 @@ export class WebSocketManager { id: string } = {}; - public constructor(options: ManagerOptions, dispatch: DispatchFunction, debug?: DebugFunction) { + public constructor(options: ManagerOptions, dispatch?: DispatchFunction, debug?: DebugFunction) { if (typeof options.intents !== "number" && Number.isNaN(options.intents)) throw new Error("Invalid intents"); this.#dispatch = dispatch; @@ -47,11 +47,17 @@ export class WebSocketManager { this.#options = options; } + public init(token: string, dispatch: DispatchFunction): void { + this.#options.token = token; + this.#dispatch = dispatch; + } + public close(): void { this.#ws.close(3000); } public async connect(url?: string): Promise { + if (typeof this.#dispatch === "undefined") throw new Error("You need to pass the dispatch function via 'init' before connecting."); if (typeof this.#gatewayInfo === "undefined") { const response = await fetch("https://discord.com/api/v10/gateway/bot", { headers: { @@ -95,7 +101,8 @@ export class WebSocketManager { switch (payload.op) { case GatewayOpCode.Dispatch: { - this.#dispatch(payload); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.#dispatch!(payload); break; } case GatewayOpCode.Hello: { @@ -251,10 +258,6 @@ export class WebSocketManager { this.#ws.send(JSON.stringify(options)); } - public set options(options: Partial) { - this.#options = { ...this.#options, ...options }; - } - public get options(): ManagerOptions { return this.#options; } diff --git a/packages/create/eslint.config.js b/packages/create/eslint.config.js index 1a5cf1c..09f3862 100644 --- a/packages/create/eslint.config.js +++ b/packages/create/eslint.config.js @@ -1,3 +1,3 @@ import config from "../../eslint.config.js"; -export default [{ ignores: ["src/templates/**/*", "dist/**/*"] }, ...config] \ No newline at end of file +export default [{ ignores: ["dist/**/*.ts"] }, ...config] \ No newline at end of file diff --git a/packages/create/globals.d.ts b/packages/create/globals.d.ts new file mode 100644 index 0000000..cc6f3d5 --- /dev/null +++ b/packages/create/globals.d.ts @@ -0,0 +1,5 @@ +declare module "bun" { + interface Env { + readonly TOKEN: string + } +} \ No newline at end of file diff --git a/packages/create/package.json b/packages/create/package.json index 3a2fa96..5defddb 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,6 +1,6 @@ { "name": "@lilybird/create", - "version": "0.4.0", + "version": "0.5.1", "description": "Create a new template bot using lilybird", "main": "./dist/index.js", "author": "DidaS", diff --git a/packages/create/src/templates.cts b/packages/create/src/templates.cts index d458748..402ffd0 100644 --- a/packages/create/src/templates.cts +++ b/packages/create/src/templates.cts @@ -58,12 +58,12 @@ export function generateTSConfig(type: string, pm: string): string { export function generateGlobalTypes(pm: string): string { return pm === "bun" ? `declare module "bun" { interface Env { - TOKEN: string + readonly TOKEN: string } }` : `declare namespace NodeJS { interface ProcessEnv { - TOKEN: string + readonly TOKEN: string } }`; } diff --git a/packages/create/src/templates/basic-template.ts b/packages/create/src/templates/basic-template.ts index 5d9114d..fa60d4f 100644 --- a/packages/create/src/templates/basic-template.ts +++ b/packages/create/src/templates/basic-template.ts @@ -2,10 +2,10 @@ import { createClient, Intents } from "lilybird"; await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], + intents: Intents.GUILDS, listeners: { - ready(client) { - console.log(`Logged in as ${client.user.username}`); + setup: (_, payload) => { + console.log(`Logged in as ${payload.user.username}`); } } }); diff --git a/packages/create/src/templates/handlers-template.ts b/packages/create/src/templates/handlers-template.ts index 503eee2..a7639d8 100644 --- a/packages/create/src/templates/handlers-template.ts +++ b/packages/create/src/templates/handlers-template.ts @@ -1,16 +1,16 @@ import { Intents, createClient } from "lilybird"; -import { handler } from "@lilybird/handlers/advanced"; +import { handler } from "@lilybird/handlers"; handler.cachePath = `${import.meta.dir}/lily-cache/handler`; -await handler.scanDir(`${import.meta.dir}/commands`); -await handler.scanDir(`${import.meta.dir}/listeners`); - await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - setup: async (client) => { - await handler.loadGlobalCommands(client); - }, - listeners: handler.getListenersObject() + intents: Intents.GUILDS, + listeners: { + // Order is important + setup: async (client) => { + await handler.loadGlobalCommands(client); + }, + ...handler.getListenersObject() + } }); diff --git a/packages/create/src/templates/listener-template.ts b/packages/create/src/templates/listener-template.ts index fb40fc0..c0a914a 100644 --- a/packages/create/src/templates/listener-template.ts +++ b/packages/create/src/templates/listener-template.ts @@ -1,4 +1,4 @@ -import { $listener } from "@lilybird/handlers/advanced"; +import { $listener } from "@lilybird/handlers"; $listener({ event: "ready", diff --git a/packages/create/src/templates/transformers-template.ts b/packages/create/src/templates/transformers-template.ts index 1ad3927..9cfd2c6 100644 --- a/packages/create/src/templates/transformers-template.ts +++ b/packages/create/src/templates/transformers-template.ts @@ -1,13 +1,18 @@ -import { defaultTransformers } from "@lilybird/transformers"; import { createClient, Intents } from "lilybird"; +import { makeTransformersObject } from "@lilybird/transformers"; -await createClient({ +import type { Client } from "lilybird"; +import type { MergeTransformers } from "@lilybird/transformers"; + +const transformers = makeTransformersObject(); + +await createClient>({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - transformers: defaultTransformers, + intents: Intents.GUILDS, + transformers, listeners: { - ready(client) { - console.log(`Logged in as ${client.user.username}`); + setup: (_, payload) => { + console.log(`Logged in as ${payload.user.username}`); } } }); diff --git a/packages/create/tsconfig.json b/packages/create/tsconfig.json index 9b23db4..2578140 100644 --- a/packages/create/tsconfig.json +++ b/packages/create/tsconfig.json @@ -2,12 +2,13 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "baseUrl": "." + "baseUrl": ".", + "types": [ + "bun-types", + "./globals" + ], }, "include": [ "src/**/*" - ], - "exclude": [ - "src/templates/**/*.ts" ] } \ No newline at end of file diff --git a/packages/docs/astro.config.ts b/packages/docs/astro.config.ts index e5633c5..900b523 100644 --- a/packages/docs/astro.config.ts +++ b/packages/docs/astro.config.ts @@ -1,18 +1,19 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections"; import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"; import { createStarlightTypeDocPlugin } from "starlight-typedoc"; import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [createCoreDocumentation, coreDocumentationSidebar] = createStarlightTypeDocPlugin(); +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [createTransformersDocumentation, transformersDocumentationSidebar] = createStarlightTypeDocPlugin(); export default defineConfig({ integrations: [ starlight({ title: "Lilybird", - description: "Lightweight and performant Discord API Wrapper built by the community for the community.", + description: "The most performant and lightweight Discord API Wrapper for JavaScript built by developers for developers.", customCss: ["./src/styles/index.css"], lastUpdated: true, social: { diff --git a/packages/docs/package.json b/packages/docs/package.json index 6c7559d..0f2fec4 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -15,9 +15,9 @@ "@expressive-code/plugin-collapsible-sections": "^0.36.1", "@astrojs/check": "^0.9.3", "@astrojs/starlight": "^0.26.3", - "starlight-typedoc": "^0.16.0", - "typedoc": "^0.26.6", - "typedoc-plugin-markdown": "^4.2.6", + "starlight-typedoc": "0.16.0", + "typedoc": "0.26.6", + "typedoc-plugin-markdown": "4.2.6", "astro": "^4.15.3", "sharp": "^0.33.5" } diff --git a/packages/docs/src/assets/diagrams/compiler.svg b/packages/docs/src/assets/diagrams/compiler.svg deleted file mode 100644 index 2c816f5..0000000 --- a/packages/docs/src/assets/diagrams/compiler.svg +++ /dev/null @@ -1,988 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -YOUUSEMAY USEMODULESCLIENTWS MANAGER

● DEFINITIONS

-
    -
  • RAW - DEFINE THE RAW LISTENER IF IT EXISTS, RAW IS ALWAYS FIRST LEVEL WITH 0 CONDITIONALS.
  • -
  • READY - DEFINE THE READY LISTENER, THIS IS DONE MANUALLY FOR A FEW REASONS. -
      -
    • THE SETUP API NEEDS TO ALSO BE ADDED IF IT EXISTS.
    • -
    • THE INNER USER OBJECT OF THE CLIENT MIGHT HAVE A TRANSFORMER ASSIGNED.
    • -
    -
  • -
  • USER DEFINED - THIS HANDLES CREATING THE LISTENERS YOU PASSED IN ON THE API. -
      -
    • THIS ALWAYS ADDS THE TRANSFORMER FOR THE EVENT IF IT EXISTS.
    • -
    -
  • -
  • CACHE - DEFINE THE CACHING LISTENERS.
  • -
  • DCE - DEAD CODE ELIMINATION HAPPENS FOR EACH LISTENER TO GET RID OF UNUSED TRANSFORMERS.
  • -
-
APICOMPILERLISTENERSRAWREADYUSER DEFINEDCACHEDCETRANSFORMERSLISTENERSCACHE - - - - - - - - - - - - - - - - - - - -
diff --git a/packages/docs/src/content/docs/api/cache.md b/packages/docs/src/content/docs/api/cache.md index c7d238a..fa8a9e8 100644 --- a/packages/docs/src/content/docs/api/cache.md +++ b/packages/docs/src/content/docs/api/cache.md @@ -44,7 +44,7 @@ import { await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], + intents: Intents.GUILDS, caching: { delegate: CachingDelegationType.DEFAULT, enabled: { diff --git a/packages/docs/src/content/docs/api/compiler.mdx b/packages/docs/src/content/docs/api/compiler.mdx index 4626cbd..ff4fc90 100644 --- a/packages/docs/src/content/docs/api/compiler.mdx +++ b/packages/docs/src/content/docs/api/compiler.mdx @@ -2,21 +2,18 @@ title: Listeners Compiler description: Lets try to break down how the compiler works. sidebar: - order: 4 - badge: - text: Internal - variant: note + order: 2 --- -import overviewDiagram from "../../../assets/diagrams/compiler.svg" import compilerDiagram from "../../../assets/diagrams/api-flow.svg" import { Image } from 'astro:assets'; + The compiler in lilybird is what makes our api possible, it builds all the listeners **once** at startup so you only get both, what you defined and with proper transformers (if present) without any runtime checks after the bot starts. The lack of this extra checks on every event is what makes lilybird so fast, the workflow is just
`event -> listener` with the only thing possibly in the middle being the transformers you define or in some cases the cache code. -overview +## Internal Workflow The great advantage of the compiler is that your listeners run smoothly without having to do any checks because it does all the checks when its compiling them. diff --git a/packages/docs/src/content/docs/api/internal-workflow.mdx b/packages/docs/src/content/docs/api/internal-workflow.mdx index 6058837..499a4b2 100644 --- a/packages/docs/src/content/docs/api/internal-workflow.mdx +++ b/packages/docs/src/content/docs/api/internal-workflow.mdx @@ -10,6 +10,12 @@ sidebar: import workflowDiagram from "../../../assets/diagrams/websocket-flow.svg" import { Image } from 'astro:assets'; +:::note +As of lilybird 0.9 the compilation step has been made completely optional and the bellow diagram will suffer some small changes in the future. + +Im currently trying to figure out how to get the free TALA license for open source projects. +::: + The diagram bellow illustrates all the steps taken by lilybird when your bot starts. internal flow diff --git a/packages/docs/src/content/docs/api/setup.md b/packages/docs/src/content/docs/api/setup.md deleted file mode 100644 index 9e9cb63..0000000 --- a/packages/docs/src/content/docs/api/setup.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Setup -description: Lets talk about the setup api. -sidebar: - order: 1 ---- - -The setup api in lilybird is what allows you to do actions to setup external resources like databases or even publish your commands. - -This api exists purely because unlike other frameworks lilybird does not ignore ready events sent after a failed resume attempt or reconnection, and since we don't have real events (we don't use `EventEmitter` or `EventTarget`), we added this api so you can execute certain actions only once at startup. - -To learn how you can make use of it we have an example in [one of the guides](/guides/handling-commands). diff --git a/packages/docs/src/content/docs/api/transformers.md b/packages/docs/src/content/docs/api/transformers.md index 4e049ae..e2ad6d8 100644 --- a/packages/docs/src/content/docs/api/transformers.md +++ b/packages/docs/src/content/docs/api/transformers.md @@ -39,7 +39,7 @@ import { createClient, Intents } from "lilybird"; await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], + intents: Intents.GUILDS, listeners: { // We get the client and payload, so we can do what we want with them ready: (client, payload) => { @@ -56,7 +56,7 @@ import { createClient, Intents, TransformerReturnType } from "lilybird"; await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], + intents: Intents.GUILDS, transformers: { ready: { return: TransformerReturnType.SINGLE, diff --git a/packages/docs/src/content/docs/guides/coloring.md b/packages/docs/src/content/docs/guides/coloring.md index 787ed9f..2b71153 100644 --- a/packages/docs/src/content/docs/guides/coloring.md +++ b/packages/docs/src/content/docs/guides/coloring.md @@ -47,7 +47,7 @@ import { await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], + intents: Intents.GUILDS, listeners: { messageCreate: async (client, message) => { await client.rest.createMessage(message.channel_id, { diff --git a/packages/docs/src/content/docs/guides/embeds-attachments.md b/packages/docs/src/content/docs/guides/embeds-attachments.md index 7bb5695..5269e3a 100644 --- a/packages/docs/src/content/docs/guides/embeds-attachments.md +++ b/packages/docs/src/content/docs/guides/embeds-attachments.md @@ -21,10 +21,10 @@ import { await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - // We pass the setup function we created above - setup, + intents: Intents.GUILDS, listeners: { + // We pass the setup function we created above + setup, interactionCreate: async (client, payload) => { // We only want to handle guild interactions if (!("guild_id" in payload)) return; @@ -63,10 +63,10 @@ import { await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - // We pass the setup function we created above - setup, + intents: Intents.GUILDS, listeners: { + // We pass the setup function we created above + setup, interactionCreate: async (client, payload) => { // We only want to handle guild interactions if (!("guild_id" in payload)) return; @@ -98,10 +98,10 @@ import { await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - // We pass the setup function we created above - setup, + intents: Intents.GUILDS, listeners: { + // We pass the setup function we created above + setup, interactionCreate: async (client, payload) => { // We only want to handle guild interactions if (!("guild_id" in payload)) return; diff --git a/packages/docs/src/content/docs/guides/getting-started.mdx b/packages/docs/src/content/docs/guides/getting-started.mdx index ac0b5da..ec8c2a6 100644 --- a/packages/docs/src/content/docs/guides/getting-started.mdx +++ b/packages/docs/src/content/docs/guides/getting-started.mdx @@ -1,21 +1,23 @@ --- title: Getting Started -description: Learn how to setup Lilybird. +description: Get started fast with Lilybird. sidebar: order: 0 --- import { Tabs, TabItem, FileTree } from "@astrojs/starlight/components"; -:::danger[Important] -Lilybird does not officially support **deno**. While the core should work, there's no guarantee that the modules or, for that matter the examples will. -::: +## Before you start + +Lilybird is a thing wrapper on top of the Discord API, if you are not using transformers the best documentation you can read is the official one that can be found [here](https://discord.com/developers/docs/). + +All the `interfaces`, `types` and `REST` methods in lilybird follow the naming scheme found in the official documentation, if you ever find something that doesn't match feel free to open a pull request. ## Installation The fastest way to get started with Lilybird is by using our template generator. - + ```bash frame="none" bun create @lilybird @@ -42,10 +44,6 @@ The fastest way to get started with Lilybird is by using our template generator. After running the command you should see a file structure somewhat like this: -:::note -Depending on the selected language for the template the structure may differ: The JavaScript template will not contain type or config files (`globals.d.ts` and `tsconfig.json`) -::: - - globals.d.ts @@ -57,38 +55,68 @@ Depending on the selected language for the template the structure may differ: Th -### Adding your token +Don't forget to add your token as mentioned above. -To add your bot token open your `.env` file and add the token after `TOKEN=`. +:::note +Depending on the selected language for the template the structure may differ: The JavaScript template will not contain type or config files (`globals.d.ts` and `tsconfig.json`) +::: ### Starting your bot -:::tip -If you are using Bun you can run `bun start` no matter what language you are using. -::: +To start your first bot all you need to do is run the `dev` script found in your `package.json`. - - + + + ```bash frame="none" + bun dev + ``` + + ```bash frame="none" npm run dev ``` - + ```bash frame="none" - npm run start + pnpm run dev + ``` + + + ```bash frame="none" + yarn run dev ``` ### Ready for production -If you are using TypeScript we highly recommend adding `NODE_ENV="production"` to your `.env` file and if you are using Node, do the following. +Running your bot is rather simple, however we have a few recommendations that can help improve performance in some cases: +- If you are using Bun or Node, add `NODE_ENV="production"` to your `.env` file. +- If you are using Bun, change or add a script to run production using `bun --smol`. -:::note -If you are using **Bun** we highly encourage changing or adding a script to run production using `bun --smol`. -::: - -```bash frame="none" -npm run build -npm run start -``` + + + ```bash frame="none" + bun run build + bun start + ``` + + + ```bash frame="none" + npm run build + npm run start + ``` + + + ```bash frame="none" + pnpm run build + pnpm run start + ``` + + + ```bash frame="none" + yarn run build + yarn run start + ``` + + diff --git a/packages/docs/src/content/docs/guides/handling-commands.md b/packages/docs/src/content/docs/guides/handling-commands.md index dcb5a53..0b64ea7 100644 --- a/packages/docs/src/content/docs/guides/handling-commands.md +++ b/packages/docs/src/content/docs/guides/handling-commands.md @@ -103,10 +103,10 @@ async function handleCommand( await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - // We pass the setup function we created above - setup, + intents: Intents.GUILDS, listeners: { + // We pass the setup function we created above + setup, interactionCreate: async (client, payload) => { // We only want to handle guild interactions if (!("guild_id" in payload)) return; diff --git a/packages/docs/src/content/docs/guides/manual-setup.mdx b/packages/docs/src/content/docs/guides/manual-setup.mdx index 1772f34..2f510b9 100644 --- a/packages/docs/src/content/docs/guides/manual-setup.mdx +++ b/packages/docs/src/content/docs/guides/manual-setup.mdx @@ -1,71 +1,87 @@ --- title: Manual Setup -description: Configure Lilybird however you would like. +description: Learn how to setup Lilybird all by yourself. sidebar: order: 1 --- -import { Tabs, TabItem } from "@astrojs/starlight/components"; +import { Tabs, TabItem, FileTree } from "@astrojs/starlight/components"; -## Creating the bot +## Initial setup -Creating the bot with Lilybird is a trivial task. +To start with lilybird you firstly need to create a new project, in this guide we will be using the template provided by `@lilybird/create` but you can always use another template generator or make it from scratch. -```ts title="index.ts" +### Installing lilybird + +Lilybird is available on the npm registry, therefor it can be downloaded with the package manager of your choice. + + + + ```bash frame="none" + bun add lilybird + ``` + + + ```bash frame="none" + npm i lilybird + ``` + + + ```bash frame="none" + pnpm i lilybird + ``` + + + ```bash frame="none" + yarn add lilybird + ``` + + + +## Fast Setup + +The `createClient` function is a helper provided by lilybird that allows you to spin up a client fast without having to worry about managing the compiler yourself. +If all you want is to spin up a bot and do your stuff this is the way to go, however, if you want full control over your bot skip this section. + +Using this helper function will still allow you to use transformers, handlers and other things but it requires every listener to be its own function. + +### Making your first bot + +Making your first bot is extremely simple, however it lacks functionality. All the bot functionality is up to you to implement using the tools provided, we have other guides that can help you with specific problems but we highly recommend that you read the official Discord documentation. + +```ts showLineNumbers {"The setup listener is a ready listener that is only ever called once":7-8} import { createClient, Intents } from "lilybird"; await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - listeners: {/* your listeners */} + intents: Intents.GUILDS, + listeners: { + + setup: (client) => { + console.log(`Logged in as: ${client.user.username} (${client.user.id})`) + } + } }) ``` -## Adding types to `process.env` +## In-depth setup -In case you are using TypeScript we highly encourage adding proper types to your process.env. +:::danger[Be aware] +This is a work in progress that hopes to give a more advanced guide that is intended for people who are writing frameworks for lilybird, if you are having trouble understanding it feel free to join our discord and ask questions. +::: -This can be done by creating a `globals.d.ts` file on the root of your project. +### The "client" - - - ```ts title="globals.d.ts" - declare module "bun" { - interface Env { - TOKEN: string; - // Other env variables - } - } - ``` - - - ```ts title="globals.d.ts" - declare global { - namespace NodeJS { - interface ProcessEnv { - TOKEN: string; - // Other env variables - } - } - } - ``` - - +In lilybird there is no strong concept of what the client is, the provided `Client` class is just a convenient wrapper that gets passed around each listener that includes an instance of the `REST` helpers and the active `ws` connection, however this is not the strict definition of a `Client`. -And then adding the following to your `tsconfig.json`. +#### Definition -:::caution -Setting `types` on the compiler options will make it so TypeScript won't search for `@types` packages. +The `Client` in lilybird is defined as an object that includes the fields returned by the `READY` event. -You can read more about it on the [TypeScript handbook](https://www.typescriptlang.org/tsconfig#types) -::: +The current interface all client representations should respect is called `MockClient` (name pending change). -```json title="tsconfig.json" -{ - "compilerOptions": { - "types": [ - "./globals" - ] - } -} -``` \ No newline at end of file +### The AOT Compiler + +The AOT Compiler in lilybird is one of the things that allows lilybird to provide apis like the transformers without having any impact on performance. Its job its to compile down your listeners and transformers into a simple linear dispatch function that has no useless/redundant checks. + +While this is an important piece, the AOT Compiler is **not** required for lilybird to work, you can always make your own dispatch functions manually. \ No newline at end of file diff --git a/packages/docs/src/content/docs/guides/receiving-messages.md b/packages/docs/src/content/docs/guides/receiving-messages.md index 9d91aec..25001b4 100644 --- a/packages/docs/src/content/docs/guides/receiving-messages.md +++ b/packages/docs/src/content/docs/guides/receiving-messages.md @@ -16,7 +16,7 @@ import { await createClient({ token: process.env.TOKEN, - intents: [Intents.GUILDS], + intents: Intents.GUILDS, listeners: { messageCreate: async (client, message) => { await client.rest.createMessage(message.channel_id, { diff --git a/packages/docs/src/content/docs/index.mdx b/packages/docs/src/content/docs/index.mdx index 9c9f5f6..68c1968 100644 --- a/packages/docs/src/content/docs/index.mdx +++ b/packages/docs/src/content/docs/index.mdx @@ -1,12 +1,8 @@ --- title: Lilybird template: splash -banner: - content: | - As of lilybird 0.4 the core has been converted to use - ESM! hero: - tagline: Built with modularity in mind, the only Discord API Wrapper you will ever need. + tagline: The most performant and lightweight Discord API Wrapper for JavaScript built by developers for developers. image: file: ../../assets/houston.webp actions: @@ -27,16 +23,16 @@ hero: import { Card, CardGrid } from "@astrojs/starlight/components"; - - With no dependencies and built with performance and DX in mind. + + Lilybird aims to provide full control to the developer while making it simple to modify anything you want. - - With lots of modules and an extensive public api. + + Lilybird provides an extensive API that allows you to modify every step. - - As a new framework we focus on new standards like ES modules. + + With no dependencies lilybird weighs less than 500kb uncompressed. - - With bun in mind we do not guarantee node compatibility **for our modules**. + + Lilybird has been crafted with performance and the developer in mind. diff --git a/packages/docs/src/content/docs/modules/handlers/default/application-commands.md b/packages/docs/src/content/docs/modules/handlers/default/application-commands.md deleted file mode 100644 index ccd4589..0000000 --- a/packages/docs/src/content/docs/modules/handlers/default/application-commands.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Handling Application Commands -description: How to use Lilybird's handlers for application commands. -sidebar: - order: 0 ---- - -## Creating a simple command - -Lets create a simple `ping` command to demonstrate how it works. - -```diff lang="ts" title="index.ts" -import { createClient, Intents } from "lilybird"; -+import { handler } from "@lilybird/handlers/advanced"; - -+handler.cachePath = `${import.meta.dir}/lily-cache/handler`; -+await handler.scanDir(`${import.meta.dir}/commands`); - -await createClient({ - token: process.env.TOKEN, - intents: [Intents.GUILDS], -- listeners: {/* your listeners */} -+ setup: async (client) => { -+ await handler.loadGlobalCommands(client); -+ }, -+ listeners: handler.getListenersObject() -}) -``` - -```ts title="commands/ping.ts" showLineNumbers -import { $applicationCommand } from "@lilybird/handlers/advanced"; - -$applicationCommand({ - name: "ping", - description: "pong", - handle: async (client, interaction) => { - const { ws, rest } = await client.ping(); - await client.rest.createInteractionResponse(interaction.id, interaction.token, { - type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` - } - }); - }, -}); -``` - -## Handling Sub Commands - -Handling sub commands with lilybird's handler is extremely simple, you write it just like you would for a normal command. - -```js showLineNumbers -import { $applicationCommand } from "@lilybird/handlers/advanced"; -import { ApplicationCommandOptionType } from "lilybird"; - -$applicationCommand({ - name: "wrapper", - description: "A wrapper group for basic commands", - options: [ - { - type: ApplicationCommandOptionType.SUB_COMMAND, - name: "ping", - description: "pong", - handle: async (client, interaction) => { - const { ws, rest } = await client.ping(); - await client.rest.createInteractionResponse(interaction.id, interaction.token, { - type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` - } - }); - }, - } - ] -}); -``` \ No newline at end of file diff --git a/packages/docs/src/content/docs/modules/handlers/default/events.md b/packages/docs/src/content/docs/modules/handlers/default/events.md deleted file mode 100644 index 6455326..0000000 --- a/packages/docs/src/content/docs/modules/handlers/default/events.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Handling Events -description: How to use lilybird's handlers for events. -sidebar: - order: 1 ---- - -## Creating a listener - -```diff lang="ts" title="index.ts" -import { createClient, Intents } from "lilybird"; -+import { handler } from "@lilybird/handlers/advanced"; - -+handler.cachePath = `${import.meta.dir}/lily-cache/handler`; -+await handler.scanDir(`${import.meta.dir}/events`); - -await createClient({ - token: process.env.TOKEN, - intents: [Intents.GUILDS], -- listeners: {/* your listeners */} -+ setup: async (client) => { -+ await handler.loadGlobalCommands(client); -+ }, -+ listeners: handler.getListenersObject() -}) -``` - -```ts title="events/ping.ts" showLineNumbers -import { $listener } from "@lilybird/handlers/advanced"; - -$listener({ - event: "ready", - handle: (client) => { - console.log("Connected as", client.user.username); - } -}); -``` \ No newline at end of file diff --git a/packages/docs/src/content/docs/modules/handlers/simple/application-commands.md b/packages/docs/src/content/docs/modules/handlers/simple/application-commands.md deleted file mode 100644 index 356829f..0000000 --- a/packages/docs/src/content/docs/modules/handlers/simple/application-commands.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Handling Application Commands -description: How to use Lilybird's handlers for application commands. -sidebar: - order: 0 ---- - -Currently the `@lilybird/handlers` package provides only one way of handling application commands, however, I can assure you there are more to come. - -To be completely honest, the current API is not the greatest but was the fastest one for a demo. - -## Creating a simple command - -Lets create a simple `ping` command to demonstrate how it works. - -```diff lang="ts" title="index.ts" -import { createClient, Intents } from "lilybird"; -+import { createHandler } from "@lilybird/handlers/simple"; - -+const listeners = await createHandler({ -+ dirs: { -+ slashCommands: `${import.meta.dir}/commands`, -+ } -+}) - -await createClient({ - token: process.env.TOKEN, - intents: [Intents.GUILDS], -- listeners: {/* your listeners */} -+ ...listeners -}) -``` - -```tsx title="commands/ping.tsx" -import { SlashCommand } from "@lilybird/handlers/simple"; - -export default { - post: "GLOBAL", - data: { - name: "ping", - description: "pong" - }, - run: async (interaction) => { - const { ws, rest } = await interaction.client.ping(); - - await interaction.reply({ - content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` - }); - }, -} satisfies SlashCommand -``` - -:::note -The above code was taken from the [Bun Discord bot](https://github.com/xHyroM/bun-discord-bot), join the Bun Discord server to see it in action. -::: \ No newline at end of file diff --git a/packages/docs/src/content/docs/modules/handlers/simple/events.md b/packages/docs/src/content/docs/modules/handlers/simple/events.md deleted file mode 100644 index b101bee..0000000 --- a/packages/docs/src/content/docs/modules/handlers/simple/events.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Handling Events -description: How to use lilybird's handlers for events. -sidebar: - order: 1 ---- - -Currently the `@lilybird/handlers` package provides only one way of handling events, however, I can assure you there are more to come. - -## Creating a listener - -```diff lang="ts" title="index.ts" -import { createClient, Intents } from "lilybird"; -+import { createHandler } from "@lilybird/handlers/simple"; - -+const listeners = await createHandler({ -+ dirs: { -+ events: `${import.meta.dir}/events`, -+ } -+}) - -await createClient({ - token: process.env.TOKEN, - intents: [Intents.GUILDS], -- listeners: {/* your listeners */} -+ ...listeners -}) -``` - -```ts title="events/ping.ts" -import { Event } from "@lilybird/handlers/simple"; - -export default { - event: "ready", - run: (client) => { - console.log(`Logged in as ${client.user.username}`); - }, -// This duplication is needed for TypeScript types to work properly -// This is also why this API isn't the best -} satisfies Event<"ready"> -``` \ No newline at end of file diff --git a/packages/docs/src/content/docs/modules/handlers/simple/message-commands.md b/packages/docs/src/content/docs/modules/handlers/simple/message-commands.md deleted file mode 100644 index 528fb05..0000000 --- a/packages/docs/src/content/docs/modules/handlers/simple/message-commands.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Handling Message Commands -description: How to use lilybird's handlers for application commands. -sidebar: - order: 2 ---- - -Currently the `@lilybird/handlers` package provides only one way of handling application commands, however, I can assure you there are more to come. - -## Creating a simple command - -Let's create a simple `ping` command to demonstrate how it works. - -```diff lang="ts" title="index.ts" -import { createClient, Intents } from "lilybird"; -+import { createHandler } from "@lilybird/handlers/simple"; - -+const listeners = await createHandler({ -+ dirs: { -+ messageCommands: `${import.meta.dir}/commands`, -+ } -+}) - -await createClient({ - token: process.env.TOKEN, - intents: [Intents.GUILDS], -- listeners: {/* your listeners */} -+ ...listeners -}) -``` - -:::note -The second argument, `args`, is the result of running the following code: -```ts -message.content.slice(this.prefix.length) - .trim() - .split(/\s+/g) - .shift() -``` -::: - -```ts title="commands/ping.ts" -import { MessageCommand } from "@lilybird/handlers/simple"; - -export default { - name: "ping", - run: async (message, args) => { - const { ws, rest } = await message.client.ping(); - - await message.reply({ - content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` - }); - }, -} satisfies MessageCommand -``` - -:::note -The above code was taken from the [Bun Discord bot](https://github.com/xHyroM/bun-discord-bot), join the Bun Discord server to see it in action. -::: \ No newline at end of file diff --git a/packages/handlers/package.json b/packages/handlers/package.json index ac6afc7..3cfc523 100644 --- a/packages/handlers/package.json +++ b/packages/handlers/package.json @@ -1,21 +1,16 @@ { "name": "@lilybird/handlers", - "version": "0.6.0", + "version": "0.7.0-beta.2", "description": "Command handlers and more for lilybird", "main": "./dist/index.js", "author": "DidaS", "license": "Apache-2.0", "type": "module", "exports": { - "./simple": { - "types": "./dist/simple/index.d.ts", - "require": "./dist/simple/index.js", - "default": "./dist/simple/index.js" - }, - "./advanced": { - "types": "./dist/advanced/index.d.ts", - "require": "./dist/advanced/index.js", - "default": "./dist/advanced/index.js" + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" }, "./package.json": "./package.json" }, @@ -52,7 +47,7 @@ "bun-first" ], "peerDependencies": { - "lilybird": "^0.8.0", - "@lilybird/transformers": "^0.4.0" + "lilybird": "^0.9.0-beta.2", + "@lilybird/transformers": "^0.5.0-beta.1" } } \ No newline at end of file diff --git a/packages/handlers/src/advanced/application-command-store.ts b/packages/handlers/src/application-command-store.ts similarity index 76% rename from packages/handlers/src/advanced/application-command-store.ts rename to packages/handlers/src/application-command-store.ts index 1cdaed8..311ad25 100644 --- a/packages/handlers/src/advanced/application-command-store.ts +++ b/packages/handlers/src/application-command-store.ts @@ -4,12 +4,6 @@ import { HandlerIdentifier } from "./shared.js"; import type { Client, ApplicationCommand, Awaitable, Interaction } from "lilybird"; import type { HandlerListener } from "./shared.js"; -import type { - Interaction as TransformedInteraction, - ApplicationCommandData, - AutocompleteData -} from "@lilybird/transformers"; - type Expand = T extends T ? { [K in keyof T]: T[K] } : never; type U2I = (U extends U ? (u: U) => 0 : never) extends (i: infer I) => 0 ? Extract : never; @@ -24,42 +18,47 @@ type ParseOptions> = U2I<{ type MapOptionType = T extends ApplicationCommandOptionType.INTEGER | ApplicationCommandOptionType.NUMBER ? number : T extends ApplicationCommandOptionType.BOOLEAN ? boolean : string; -export type ApplicationCommandHandler, U extends boolean> = U extends true - ? (interaction: TransformedInteraction) => Awaitable - : (client: Client, interaction: Interaction.ApplicationCommandInteractionStructure, options: Expand>) => Awaitable; +export type ApplicationCommandHandler, HO extends (...args: any) => any> = HO extends never + ? (client: Client, interaction: Interaction.ApplicationCommandInteractionStructure, options: Expand>) => Awaitable + : HO; -export type ApplicationAutocompleteHandler, U extends boolean> = U extends true - ? (interaction: TransformedInteraction) => Awaitable - : (client: Client, interaction: Interaction.ApplicationCommandInteractionStructure, options: Expand>) => Awaitable; +export type ApplicationCommandAutocompleteHandler, AO extends (...args: any) => any> = AO extends never + ? (client: Client, interaction: Interaction.ApplicationCommandInteractionStructure, options: Expand>) => Awaitable + : AO; interface CompiledCommand { body: { command: string, autocomplete: string | null }; - function: { names: Array, handlers: Array, boolean> | ApplicationAutocompleteHandler, boolean>> }; + // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents, @typescript-eslint/no-redundant-type-constituents + function: { names: Array, handlers: Array, any> | ApplicationCommandAutocompleteHandler, any>> }; // eslint-disable-next-line @typescript-eslint/naming-convention json: ApplicationCommand.Create.ApplicationCommandJSONParams & { __meta?: { ids?: string } }; } -export interface CommandStructure, U extends boolean> extends Omit { +export interface CommandStructure< + O extends Array, + HO extends (...args: any) => any, + AO extends (...args: any) => any +> extends Omit { options?: O; meta?: CommandMeta; - handle?: ApplicationCommandHandler; - autocomplete?: ApplicationAutocompleteHandler; + handle?: ApplicationCommandHandler; + autocomplete?: ApplicationCommandAutocompleteHandler; } -export type CommandOption = BaseCommandOption | SubCommandStructure, boolean> | SubCommandGroupOption; +export type CommandOption = BaseCommandOption | SubCommandStructure, never, never> | SubCommandGroupOption; export type BaseCommandOption = Exclude; -export interface SubCommandStructure, U extends boolean> extends ApplicationCommand.Option.Base { +export interface SubCommandStructure, HO extends (...args: any) => any, AO extends (...args: any) => any> extends ApplicationCommand.Option.Base { type: ApplicationCommandOptionType.SUB_COMMAND; options?: O; - handle: ApplicationCommandHandler; - autocomplete?: ApplicationAutocompleteHandler; + handle: ApplicationCommandHandler; + autocomplete?: ApplicationCommandAutocompleteHandler; } interface SubCommandGroupOption extends ApplicationCommand.Option.Base { type: ApplicationCommandOptionType.SUB_COMMAND_GROUP; - options: Array, boolean>>; + options: Array, never, never>>; } interface CommandMeta { @@ -67,20 +66,26 @@ interface CommandMeta { ids?: Array; } -export class ApplicationCommandStore { +export type ApplicationCommandStoreCustomKeys = { sub_command: string, sub_command_group: string, name: string }; +export type ApplicationCommandStoreOptions = { transformed: false, customKeys?: undefined } | { transformed: true, customKeys: ApplicationCommandStoreCustomKeys }; + +// TODO: Accept a transformer and do checks on the return type (Single or Multiple) +export class ApplicationCommandStore any, AO extends (...args: any) => any> { readonly #globalApplicationCommands = new Map(); readonly #guildApplicationCommands = new Map(); readonly #emit?: HandlerListener; - readonly #transformer: ((interaction: Interaction.Structure) => Awaitable) | undefined; + readonly #assumeTransformed: boolean; + readonly #customKeys?: ApplicationCommandStoreCustomKeys; - public constructor(handlerListener?: HandlerListener, transformer?: (interaction: Interaction.Structure) => Awaitable) { + public constructor(handlerListener?: HandlerListener, options: ApplicationCommandStoreOptions = { transformed: false }) { this.#emit = handlerListener; - this.#transformer = transformer; + this.#assumeTransformed = options.transformed; + this.#customKeys = options.customKeys; } - public storeCommand>(command: CommandStructure): void { + public storeCommand>(command: CommandStructure): void { const { meta, options, handle, autocomplete, ...actualCommand } = command; if (meta?.guild_command === true && !Array.isArray(meta.ids)) throw new Error("Invalid guild command. Lacking 'ids'"); @@ -108,6 +113,8 @@ export class ApplicationCommandStore { } #parseOptions(options: Array | undefined, appendSubCommandLogic: boolean = false): string | undefined { + // Parsing options should be the transformer responsibility + if (this.#assumeTransformed) return undefined; if (typeof options === "undefined") return undefined; const stack: Array = ["const _obj = {"]; @@ -138,13 +145,12 @@ export class ApplicationCommandStore { #makeCommandBase( command: ApplicationCommand.Create.ApplicationCommandJSONParams, - handler: { base_executor: ApplicationCommandHandler, U>, auto_executor?: ApplicationAutocompleteHandler, boolean> }, + handler: { base_executor: ApplicationCommandHandler, HO>, auto_executor?: ApplicationCommandAutocompleteHandler, AO> }, useElse: boolean, optionsBody: string | undefined = undefined, matchTo: "interaction_name" | "sub_command" = "interaction_name", name: string = command.name ): CompiledCommand { - const useTransformer = typeof this.#transformer !== "undefined"; const hasOptions = typeof optionsBody !== "undefined"; const names = [`handle_${name.replace("-", "_")}`]; const handlers: Array<(...args: any) => any> = [handler.base_executor]; @@ -154,18 +160,18 @@ export class ApplicationCommandStore { handlers.push(handler.auto_executor); } - let strArgs = useTransformer ? "transformer(client, interaction)" : "client, interaction"; - if (hasOptions && !useTransformer) strArgs += ", _obj"; + let strArgs = this.#assumeTransformed ? "interaction" : "client, interaction"; + if (hasOptions && !this.#assumeTransformed) strArgs += ", _obj"; return { body: { command: `${useElse ? "else " : ""}if (${matchTo} === "${command.name}") { - ${hasOptions && !useTransformer ? optionsBody : ""} + ${hasOptions && !this.#assumeTransformed ? optionsBody : ""} return handle_${name.replace("-", "_")}(${strArgs}); }`, autocomplete: typeof handler.auto_executor === "undefined" ? null : `${useElse ? "else " : ""}if (${matchTo} === "${command.name}") { - ${hasOptions && !useTransformer ? optionsBody : ""} + ${hasOptions && !this.#assumeTransformed ? optionsBody : ""} return auto_${name.replace("-", "_")}(${strArgs}); }` }, function: { @@ -178,24 +184,22 @@ export class ApplicationCommandStore { #compileCommand( command: ApplicationCommand.Create.ApplicationCommandJSONParams, - options: Required, U>>["options"], + options: Required, HO, AO>>["options"], useElse: boolean, optionsBody: string, matchTo: "interaction_name" | "sub_command" | "sub_command_group" = "interaction_name", name: string = command.name ): CompiledCommand { - const useTransformer = typeof this.#transformer !== "undefined"; - const fns = new Map, U> | ApplicationAutocompleteHandler, U>>(); + const fns = new Map, HO> | ApplicationCommandAutocompleteHandler, AO>>(); const cmdArr: Array = [`${useElse ? "else " : ""}if (${matchTo} === "${command.name}") {`]; const autoArr: Array = [`${useElse ? "else " : ""}if (${matchTo} === "${command.name}") {`]; const temp: Array = matchTo === "sub_command_group" ? [] - : useTransformer + : this.#assumeTransformed ? [ - "const int = transformer(client, interaction);", - "const sub_command = int.data.subCommand;", - "const sub_command_group = int.data.subCommandGroup;" + `const sub_command = interaction.${this.#customKeys?.sub_command};`, + `const sub_command_group = interaction.${this.#customKeys?.sub_command_group}` ] : [ "let sub_command = undefined;", @@ -217,9 +221,11 @@ export class ApplicationCommandStore { if (realOptions.body.autocomplete !== null) autoArr.push(realOptions.body.autocomplete); for (let j = 0, len = realOptions.function.names.length; j < len; j++) { const n = realOptions.function.names[j]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const h = realOptions.function.handlers[j]; - fns.set(n, h); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + fns.set(n, h); } if (!Array.isArray(command.options)) command.options = []; @@ -227,16 +233,18 @@ export class ApplicationCommandStore { } else if (subCommand.type === ApplicationCommandOptionType.SUB_COMMAND) { if (!("handle" in subCommand)) throw new Error("SubCommand requires 'handle' to exist"); - const { handle, autocomplete, ...realCommand } = , U>>subCommand; + const { handle, autocomplete, ...realCommand } = , HO, AO>>subCommand; const cmd = this.#makeCommandBase(realCommand, { base_executor: handle, auto_executor: autocomplete }, i > 0, "", "sub_command", `${name}_${subCommand.name}`); cmdArr.push(cmd.body.command); if (cmd.body.autocomplete !== null) autoArr.push(cmd.body.autocomplete); for (let j = 0, len = cmd.function.names.length; j < len; j++) { const n = cmd.function.names[j]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const h = cmd.function.handlers[j]; - fns.set(n, h); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + fns.set(n, h); } if (!Array.isArray(command.options)) command.options = []; @@ -253,7 +261,7 @@ export class ApplicationCommandStore { }; } - #isCommandWrapper(options: Required, U>>["options"]): boolean { + #isCommandWrapper(options: Required, HO, AO>>["options"]): boolean { for (let i = 0, { length } = options; i < length; i++) { const { type } = options[i]; if (type === ApplicationCommandOptionType.SUB_COMMAND_GROUP @@ -268,9 +276,9 @@ export class ApplicationCommandStore { handlers: IterableIterator<(...args: any) => any>, stack: string } | null { - const functions = new Map, U> | ApplicationAutocompleteHandler, U>>(); + const functions = new Map, HO> | ApplicationCommandAutocompleteHandler, AO>>(); const cmdArr: Array = [ - "const interaction_name = interaction.data.name;", + this.#assumeTransformed ? `const interaction_name = interaction.${this.#customKeys?.name};` : "const interaction_name = interaction.data.name;", `if (interaction.type === ${InteractionType.APPLICATION_COMMAND}) {` ]; @@ -283,9 +291,11 @@ export class ApplicationCommandStore { if (body.autocomplete !== null) autoArr.push(body.autocomplete); for (let j = 0, len = fn.names.length; j < len; j++) { const n = fn.names[j]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const h = fn.handlers[j]; - functions.set(n, h); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + functions.set(n, h); } } @@ -320,12 +330,10 @@ export class ApplicationCommandStore { // eslint-disable-next-line @typescript-eslint/no-implied-eval return new Function( - "transformer", "parseOpts", ...functionNames, - `return async (client, interaction) => { ${stack} }` + this.#assumeTransformed ? `return async (interaction) => { ${stack} }` : `return async (client, interaction) => { ${stack} }` )( - this.#transformer, innerOptionParser, ...handlers ) as never; diff --git a/packages/handlers/src/advanced/handler.ts b/packages/handlers/src/handler.ts similarity index 85% rename from packages/handlers/src/advanced/handler.ts rename to packages/handlers/src/handler.ts index 366818b..4a9ff82 100644 --- a/packages/handlers/src/advanced/handler.ts +++ b/packages/handlers/src/handler.ts @@ -4,69 +4,71 @@ import { ApplicationCommandOptionType } from "lilybird"; import { HandlerIdentifier } from "./shared.js"; import { join } from "node:path"; -import type { BaseCommandOption, CommandOption, CommandStructure, SubCommandStructure } from "./application-command-store.js"; import type { ComponentStructure, DynamicComponentStructure } from "./message-component-store.js"; import type { HandlerListener } from "./shared.js"; import type { - CacheManagerStructure, + ApplicationCommandStoreCustomKeys, + ApplicationCommandStoreOptions, + SubCommandStructure, + BaseCommandOption, + CommandStructure, + CommandOption +} from "./application-command-store.js"; + +import type { ApplicationCommand, - ClientListeners, Transformers, Interaction, Awaitable, + Listeners, Client } from "lilybird"; type ApplicationCommandJSONParams = ApplicationCommand.Create.ApplicationCommandJSONParams; -export class Handler { - readonly #acs = new ApplicationCommandStore(); +// TODO: Publish guild commands +export class Handler = Transformers, HO extends (...args: any) => any = never, AO extends (...args: any) => any = never> { + readonly #acs = new ApplicationCommandStore(); readonly #mcs = new MessageComponentStore(); readonly #listeners = new Map) => any>(); - readonly #globMatcher = new Bun.Glob("**/*.{!d,ts,js,tsx,jsx}"); + readonly #customKeys?: ApplicationCommandStoreCustomKeys; #emit?: HandlerListener; - readonly #transformer: unknown; #cachePath?: string; public constructor(options: { cachePath?: string, enableDynamicComponents?: boolean, - transformers?: Transformers, + acsOptions?: ApplicationCommandStoreOptions, handlerListener?: HandlerListener }) { this.#cachePath = options.cachePath; this.#emit = options.handlerListener; if (options.enableDynamicComponents) this.#mcs = new MessageComponentStore(options.handlerListener, options.enableDynamicComponents); - if (typeof options.transformers !== "undefined") { - this.#acs = new ApplicationCommandStore(options.handlerListener, options.transformers.interactionCreate?.handler); - this.#transformer = options.transformers.interactionCreate?.handler; + if (typeof options.acsOptions !== "undefined") { + this.#customKeys = options.acsOptions.customKeys; + this.#acs = new ApplicationCommandStore(options.handlerListener, options.acsOptions); } } - public async scanDir(path: string): Promise { - const files = this.#globMatcher.scan(path); - for await (const fileName of files) await import(join(path, fileName)); - } - public buttonCollector(component: DynamicComponentStructure): void { this.#mcs.addDynamicComponent(component); } - public storeCommand>(data: CommandStructure & { components?: Array }): void { + public storeCommand>(data: CommandStructure & { components?: Array }): void { const { components, ...command } = data; if (typeof components !== "undefined") for (let i = 0, { length } = components; i < length; i++) this.#mcs.storeComponent(components[i]); this.#acs.storeCommand(command); } - public subCommandMock>(data: SubCommandStructure): SubCommandStructure { return data; } + public subCommandMock>(data: SubCommandStructure): SubCommandStructure { return data; } public storeListener< - TR extends Transformers = T, + TR extends Transformers = T, K extends keyof TR = keyof TR - >(data: { event: K, handle: Required>[K] }): void { + >(data: { event: K, handle: Required>[K] }): void { this.#listeners.set(data.event, data.handle); } @@ -163,6 +165,7 @@ export class Handler = []; if (commandsBody.length > 0) realStack.push("const interaction_name = interaction.data.name;"); @@ -203,7 +206,7 @@ export class Handler Awaitable) | null { + public compileCommands(): ((...args: any) => Awaitable) | null { const compiledResult = this.getCompilationStack(); if (compiledResult === null) return null; @@ -212,18 +215,16 @@ export class Handler { ${stack} }` + typeof this.#customKeys !== "undefined" ? `return async (interaction) => { ${stack} }` : `return async (client, interaction) => { ${stack} }` )( - this.#transformer, innerOptionParser, ...handlers ) as never; } - public getListenersObject(includeCommands: boolean = true): ClientListeners { + public getListenersObject(includeCommands: boolean = true): Listeners { const obj: Record = {}; for (let i = 0, entries = [...this.#listeners.entries()], { length } = entries; i < length; i++) { @@ -235,11 +236,17 @@ export class Handler, interaction: Interaction.Structure) => { + obj.interactionCreate = typeof this.#customKeys !== "undefined" + ? (interaction: any) => { + // @ts-expect-error The obj constant is not typed + obj.interactionCreate(interaction); + listener(interaction); + } + : (client: Client, interaction: Interaction.Structure) => { // @ts-expect-error The obj constant is not typed - obj.interactionCreate(client, interaction); - listener(client, interaction); - }; + obj.interactionCreate(client, interaction); + listener(client, interaction); + }; } else obj.interactionCreate = listener; } @@ -297,8 +304,8 @@ export class Handler["getStoredGlobalCommands"]>, - guild: ReturnType["getStoredGuildCommands"]> + global: ReturnType["getStoredGlobalCommands"]>, + guild: ReturnType["getStoredGuildCommands"]> }, components: ReturnType, listeners: Array<[name: string, handle: (...args: Array) => any]> diff --git a/packages/handlers/src/advanced/index.ts b/packages/handlers/src/index.ts similarity index 100% rename from packages/handlers/src/advanced/index.ts rename to packages/handlers/src/index.ts diff --git a/packages/handlers/src/advanced/message-component-store.ts b/packages/handlers/src/message-component-store.ts similarity index 97% rename from packages/handlers/src/advanced/message-component-store.ts rename to packages/handlers/src/message-component-store.ts index 8551c40..5af7fbf 100644 --- a/packages/handlers/src/advanced/message-component-store.ts +++ b/packages/handlers/src/message-component-store.ts @@ -1,5 +1,5 @@ -import { defaultTransformers } from "@lilybird/transformers"; +import { makeTransformersObject } from "@lilybird/transformers"; import { ComponentType, InteractionType } from "lilybird"; import { HandlerIdentifier } from "./shared.js"; @@ -12,6 +12,9 @@ import type { MessageComponentData } from "@lilybird/transformers"; +//!TODO REWORK THIS ENTIRE THING +const defaultTransformers = makeTransformersObject(); + interface CompiledComponent { body: string; handler: [name: string, fn: MessageComponentHandler]; diff --git a/packages/handlers/src/advanced/shared.ts b/packages/handlers/src/shared.ts similarity index 100% rename from packages/handlers/src/advanced/shared.ts rename to packages/handlers/src/shared.ts diff --git a/packages/handlers/src/simple/application-command.ts b/packages/handlers/src/simple/application-command.ts deleted file mode 100644 index 48d596e..0000000 --- a/packages/handlers/src/simple/application-command.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ApplicationCommandData, AutocompleteData, GuildInteraction, Interaction } from "@lilybird/transformers"; -import type { ApplicationCommand, Awaitable } from "lilybird"; - -type Guild = `${number}` | Array<`${number}`>; - -export interface GlobalApplicationCommand { - data: ApplicationCommand.Create.ApplicationCommandJSONParams; - post: "GLOBAL"; - autocomplete?: (interaction: Interaction) => Awaitable; - run: (interaction: Interaction) => Awaitable; -} - -export interface GuildApplicationCommand { - data: ApplicationCommand.Create.ApplicationCommandJSONParams; - post: Guild; - autocomplete?: (interaction: GuildInteraction) => Awaitable; - run: (interaction: GuildInteraction) => Awaitable; -} - -export type ApplicationCommand = GuildApplicationCommand | GlobalApplicationCommand; - diff --git a/packages/handlers/src/simple/events.ts b/packages/handlers/src/simple/events.ts deleted file mode 100644 index 988e124..0000000 --- a/packages/handlers/src/simple/events.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { DefaultTransformers } from "@lilybird/transformers"; -import type { Awaitable, ClientListeners } from "lilybird"; - -export interface Event< - E extends keyof ClientListeners = keyof ClientListeners, - T extends Required> = Required> -> { - name?: string; - event: E; - run: (...args: Parameters) => Awaitable; -} diff --git a/packages/handlers/src/simple/handler.ts b/packages/handlers/src/simple/handler.ts deleted file mode 100644 index afbb97c..0000000 --- a/packages/handlers/src/simple/handler.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { defaultTransformers } from "@lilybird/transformers"; -import { join } from "node:path"; - -import type { GlobalApplicationCommand, GuildApplicationCommand, ApplicationCommand } from "./application-command.js"; -import type { DefaultTransformers, Interaction, Message } from "@lilybird/transformers"; -import type { MessageCommand } from "./message-commands.js"; -import type { Event } from "./events.js"; - -import type { - ClientListeners, - ClientOptions, - Client -} from "lilybird"; - -interface HandlerDirectories { - slashCommands?: string; - messageCommands?: string; - listeners?: string; -} - -export class Handler { - protected readonly guildApplicationCommands = new Map(); - protected readonly globalApplicationCommands = new Map(); - protected readonly messageCommands = new Map(); - protected readonly events = new Map(); - protected readonly messageCommandAliases = new Map(); - - protected readonly dirs: HandlerDirectories; - protected readonly prefix: string; - - readonly #globMatcher = new Bun.Glob("**/*.{ts,tsx,js,jsx}"); - - public constructor(dirs: HandlerDirectories, prefix?: string) { - this.dirs = dirs; - this.prefix = prefix ?? "!"; - } - - public async registerGlobalCommands(client: Client): Promise { - await client.rest.bulkOverwriteGlobalApplicationCommand(client.user.id, [...this.globalApplicationCommands.values()].map((e) => e.data)); - } - - public async registerGuildCommands(client: Client): Promise { - for await (const command of this.guildApplicationCommands.values()) { - if (Array.isArray(command.post)) { - const temp: Array> = []; - for (let i = 0; i < command.post.length; i++) temp.push(client.rest.createGuildApplicationCommand(client.user.id, command.post[i], command.data)); - await Promise.all(temp); - } else await client.rest.createGuildApplicationCommand(client.user.id, command.post, command.data); - } - } - - public async readSlashCommandDir(dir: string | undefined = this.dirs.slashCommands): Promise { - if (typeof dir === "undefined") return false; - - const files = this.#globMatcher.scan(dir); - - for await (const fileName of files) { - if (fileName.endsWith(".d.ts")) continue; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const command: ApplicationCommand = (await import(join(dir, fileName))).default; - if (typeof command === "undefined") continue; - - if (fileName.startsWith("/guild") || command.post !== "GLOBAL") this.guildApplicationCommands.set(command.data.name, command); - else this.globalApplicationCommands.set(command.data.name, command); - } - - return true; - } - - public async readEventDir(dir: string | undefined = this.dirs.listeners): Promise { - if (typeof dir === "undefined") return false; - - const files = this.#globMatcher.scan(dir); - - for await (const fileName of files) { - if (fileName.endsWith(".d.ts")) continue; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const event: Event = (await import(join(dir, fileName))).default; - if (typeof event === "undefined") continue; - - this.events.set(event.event, event); - } - - return true; - } - - public async readMessageCommandDir(dir: string | undefined = this.dirs.messageCommands): Promise { - if (typeof dir === "undefined") return false; - - const files = this.#globMatcher.scan(dir); - - for await (const fileName of files) { - if (fileName.endsWith(".d.ts")) continue; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const command: MessageCommand = (await import(join(dir, fileName))).default; - if (typeof command === "undefined") continue; - - if (typeof command.alias !== "undefined" && command.alias.length > 0) { - if (command.alias.length === 1) this.messageCommandAliases.set(command.alias[0], command.name); - else for (let i = 0, { length } = command.alias; i < length; i++) this.messageCommandAliases.set(command.alias[i], command.name); - } - - this.messageCommands.set(command.name, command); - } - - return true; - } - - private async onInteraction(interaction: Interaction): Promise { - if (interaction.isApplicationCommandInteraction()) { - await this.globalApplicationCommands.get(interaction.data.name)?.run(interaction); - if (interaction.inGuild()) await this.guildApplicationCommands.get(interaction.data.name)?.run(interaction); - } else if (interaction.isAutocompleteInteraction()) { - await this.globalApplicationCommands.get(interaction.data.name)?.autocomplete?.(interaction); - if (interaction.inGuild()) await this.guildApplicationCommands.get(interaction.data.name)?.autocomplete?.(interaction); - } - } - - private async onMessage(message: Message): Promise { - if (message.author.bot || (await message.fetchChannel()).isDM()) return; - - if (message.content?.startsWith(this.prefix)) { - const args = message.content.slice(this.prefix.length).trim().split(/\s+/g); - if (args.length === 0) return; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const alias = args.shift()!.toLowerCase(); - let command = this.messageCommands.get(alias); - let name: string | undefined = alias; - - if (typeof command === "undefined") { - name = this.messageCommandAliases.get(alias); - if (typeof name !== "string") return; - command = this.messageCommands.get(name); - if (typeof command === "undefined") return; - } - - if (command.enabled ?? true) await command.run(message, args, { name, alias }); - } - } - - public async buildListeners(): Promise> { - const slashCommandsExist = await this.readSlashCommandDir(); - const messageCommandsExist = await this.readMessageCommandDir(); - const eventsExist = await this.readEventDir(); - - let interactionCreateFn: Exclude["interactionCreate"], undefined> | undefined = undefined; - let messageCreateFn: Exclude["messageCreate"], undefined> | undefined = undefined; - - const listeners: ClientListeners & Record = {} as never; - - if (eventsExist) { - for (const [name, event] of this.events) { - if (name === "interactionCreate") { - interactionCreateFn = event.run; - continue; - } - - if (name === "messageCreate") { - messageCreateFn = event.run; - continue; - } - - listeners[name] = event.run; - } - } - - if (!slashCommandsExist) listeners.interactionCreate = interactionCreateFn; - else if (typeof interactionCreateFn !== "undefined") { - listeners.interactionCreate = async (interaction) => { - //@ts-expect-error It is being checked above... - await interactionCreateFn(interaction); - await this.onInteraction(interaction); - }; - } else { - listeners.interactionCreate = async (interaction) => { - await this.onInteraction(interaction); - }; - } - - if (!messageCommandsExist) listeners.messageCreate = messageCreateFn; - else if (typeof messageCreateFn !== "undefined") { - listeners.messageCreate = async (message) => { - //@ts-expect-error It is being checked above... - await messageCreateFn(message); - await this.onMessage(message); - }; - } else { - listeners.messageCreate = async (message) => { - await this.onMessage(message); - }; - } - - return listeners; - } -} - -type Expand = T extends (...args: Array) => any ? T : { [K in keyof T]: T[K] }; - -export async function createHandler({ - dirs, - prefix -}: { - dirs: HandlerDirectories, - prefix?: string | undefined -}): Promise>, "listeners" | "transformers" | "setup" | "customCacheKeys">>> { - const handler = new Handler(dirs, prefix); - - return { - transformers: defaultTransformers, - listeners: await handler.buildListeners(), - customCacheKeys: { - guild_voice_states: "voiceStates" - }, - setup: async (client) => { - await handler.registerGlobalCommands(client); - await handler.registerGuildCommands(client); - } - }; -} diff --git a/packages/handlers/src/simple/index.ts b/packages/handlers/src/simple/index.ts deleted file mode 100644 index becb671..0000000 --- a/packages/handlers/src/simple/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -process.emitWarning("Simple handlers are deprecated!", { - code: "HANDLERS_DEPRECATED", - detail: "Simple handlers have been deprecated, please use `@lilybird/handlers/advanced` instead." -}); - -export type * from "./application-command.js"; -export type * from "./message-commands.js"; -export type * from "./events.js"; - -export * from "./handler.js"; diff --git a/packages/handlers/src/simple/message-commands.ts b/packages/handlers/src/simple/message-commands.ts deleted file mode 100644 index e7c7870..0000000 --- a/packages/handlers/src/simple/message-commands.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Message } from "@lilybird/transformers"; -import type { Awaitable } from "lilybird"; - -export interface MessageCommand { - name: string; - alias?: Array; - description?: string; - enabled?: boolean; - run: (message: Message, args: Array, meta: { name: string, alias: string }) => Awaitable; -} diff --git a/packages/jsx/package.json b/packages/jsx/package.json index 504f1f5..7fdf148 100644 --- a/packages/jsx/package.json +++ b/packages/jsx/package.json @@ -1,6 +1,6 @@ { "name": "@lilybird/jsx", - "version": "0.3.0", + "version": "0.4.0", "description": "JSX support & builders for lilybird", "main": "./dist/index.js", "author": "DidaS", diff --git a/packages/jsx/src/components.ts b/packages/jsx/src/components.ts index 014782e..1b6d2f9 100644 --- a/packages/jsx/src/components.ts +++ b/packages/jsx/src/components.ts @@ -93,7 +93,7 @@ export function StringSelectMenu({ children }: BaseSelectMenuOptions & { children: Array | Message.Component.SelectOptionStructure -}): Message.Component.SelectMenuStructure { +}): Message.Component.StringSelectStructure { if (!Array.isArray(children)) children = [children]; return { @@ -116,7 +116,7 @@ export function UserSelectMenu({ children }: BaseSelectMenuOptions & { children?: Array | Message.Component.SelectDefaultValueStructure -}): Message.Component.SelectMenuStructure { +}): Message.Component.UserSelectStructure { if (children != null && !Array.isArray(children)) children = [children]; return { @@ -139,7 +139,7 @@ export function RoleSelectMenu({ children }: BaseSelectMenuOptions & { children?: Array | Message.Component.SelectDefaultValueStructure -}): Message.Component.SelectMenuStructure { +}): Message.Component.RoleSelectStructure { if (children != null && !Array.isArray(children)) children = [children]; @@ -163,7 +163,7 @@ export function MentionableSelectMenu({ children }: BaseSelectMenuOptions & { children?: Array | Message.Component.SelectDefaultValueStructure -}): Message.Component.SelectMenuStructure { +}): Message.Component.MentionableSelectStructure { if (children != null && !Array.isArray(children)) children = [children]; return { @@ -188,11 +188,11 @@ export function ChannelSelectMenu({ }: BaseSelectMenuOptions & { channel_types?: Array, children?: Array | Message.Component.SelectDefaultValueStructure -}): Message.Component.SelectMenuStructure { +}): Message.Component.ChannelSelectStructure { if (children != null && !Array.isArray(children)) children = [children]; return { - type: ComponentType.RoleSelect, + type: ComponentType.ChannelSelect, custom_id: id, placeholder, min_values, diff --git a/packages/test/globals.d.ts b/packages/test/globals.d.ts index 05deb9f..aec12fe 100644 --- a/packages/test/globals.d.ts +++ b/packages/test/globals.d.ts @@ -1,9 +1,9 @@ declare module "bun" { interface Env { - TOKEN: string; - TEST_CHANNEL_ID: string; - TEST_GUILD_ID: string; - SEARCH_KEY: string; - CX: string; + readonly TOKEN: string; + readonly TEST_CHANNEL_ID: string; + readonly TEST_GUILD_ID: string; + readonly SEARCH_KEY: string; + readonly CX: string; } } diff --git a/packages/test/src/barrel.ts b/packages/test/src/barrel.ts new file mode 100644 index 0000000..7934904 --- /dev/null +++ b/packages/test/src/barrel.ts @@ -0,0 +1,4 @@ +import "./events/ready.js"; +import "./commands/bananas-poll.js"; +import "./commands/search.js"; +import "./commands/ping.js"; diff --git a/packages/test/src/commands-adv/bananas-poll.ts b/packages/test/src/commands-adv/bananas-poll.ts deleted file mode 100644 index 1c4c03a..0000000 --- a/packages/test/src/commands-adv/bananas-poll.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { $applicationCommand } from "../handlers.js"; -import { Message } from "@lilybird/transformers"; - -import type { Interaction } from "@lilybird/transformers"; - -$applicationCommand({ - name: "bananas-poll", - description: "create bananas poll", - handle: async (interaction: Interaction): Promise => { - await interaction.reply({ - poll: { - question: { - text: "Do you like bananas?" - }, - answers: [ - { - answer_id: 0, - poll_media: { text: "Yes!" } - }, - { - answer_id: 1, - poll_media: { text: "No!" } - } - ], - allow_multiselect: false, - duration: 168 - } - }); - - setTimeout(async () => { - const msg = new Message( - interaction.client, - await interaction.client.rest.getOriginalInteractionResponse(interaction.client.application.id, interaction.token) - ); - - console.log(await msg.poll?.answers[0].fetchVoters({})); - }, 3000); - } -}); diff --git a/packages/test/src/commands-adv/ping.ts b/packages/test/src/commands-adv/ping.ts deleted file mode 100644 index b58ef58..0000000 --- a/packages/test/src/commands-adv/ping.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { $applicationCommand } from "../handlers.js"; - -$applicationCommand({ - name: "ping", - description: "pong", - handle: async (interaction) => { - await interaction.deferReply(); - const { ws, rest } = await interaction.client.ping(); - - await interaction.editReply({ - content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` - }); - } -}); diff --git a/packages/test/src/commands/bananas-poll.ts b/packages/test/src/commands/bananas-poll.ts index 2977f5f..1c4c03a 100644 --- a/packages/test/src/commands/bananas-poll.ts +++ b/packages/test/src/commands/bananas-poll.ts @@ -1,10 +1,12 @@ -import type { ApplicationCommand } from "@lilybird/handlers/simple"; +import { $applicationCommand } from "../handlers.js"; import { Message } from "@lilybird/transformers"; -export default { - post: "GLOBAL", - data: { name: "bananas-poll", description: "create bananas poll" }, - run: async (interaction) => { +import type { Interaction } from "@lilybird/transformers"; + +$applicationCommand({ + name: "bananas-poll", + description: "create bananas poll", + handle: async (interaction: Interaction): Promise => { await interaction.reply({ poll: { question: { @@ -34,4 +36,4 @@ export default { console.log(await msg.poll?.answers[0].fetchVoters({})); }, 3000); } -} satisfies ApplicationCommand; +}); diff --git a/packages/test/src/commands/ping.ts b/packages/test/src/commands/ping.ts index 4ae87c0..b58ef58 100644 --- a/packages/test/src/commands/ping.ts +++ b/packages/test/src/commands/ping.ts @@ -1,28 +1,14 @@ -import type { ApplicationCommand } from "@lilybird/handlers/simple"; -import { ApplicationIntegrationType, InteractionContextType } from "lilybird"; +import { $applicationCommand } from "../handlers.js"; -export default { - post: "GLOBAL", - data: { - name: "ping", - description: "pong", - integration_types: [ - ApplicationIntegrationType.GUILD_INSTALL, - ApplicationIntegrationType.USER_INSTALL - ], - contexts: [ - InteractionContextType.BOT_DM, - InteractionContextType.GUILD, - InteractionContextType.PRIVATE_CHANNEL - ] - }, - run: async (interaction) => { +$applicationCommand({ + name: "ping", + description: "pong", + handle: async (interaction) => { await interaction.deferReply(); - const { ws, rest } = await interaction.client.ping(); await interaction.editReply({ content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\`` }); } -} satisfies ApplicationCommand; +}); diff --git a/packages/test/src/commands-adv/search.ts b/packages/test/src/commands/search.ts similarity index 100% rename from packages/test/src/commands-adv/search.ts rename to packages/test/src/commands/search.ts diff --git a/packages/test/src/commands/search.tsx b/packages/test/src/commands/search.tsx deleted file mode 100644 index 7bfffc2..0000000 --- a/packages/test/src/commands/search.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { - ApplicationCommand as JSXApplicationCommand, - StringOption, - UserOption, - EmbedImage, - Embed, -} from "@lilybird/jsx"; - -import type { ApplicationCommand } from "@lilybird/handlers/simple"; - -interface GoogleAPIResponse { - kind: string; - url: Record; - queries: Record; - context: { - title: string - }; - searchInformation: Record; - items: Array; -} - -interface GoogleAPIItem { - kind: string; - title: string; - htmlTitle: string; - link: string; - displayLink: string; - snippet: string; - htmlSnippet: string; - cacheId: string; - formattedUrl: string; - htmlFormattedUrl: string; - pagemap: { - cse_thumbnail: Array<{ src: string, width: string, height: string }>, - xfn: Array>, - BreadcrumbList: Array>, - metatags: Array, - cse_image: Array<{ src: string }> - }; -} - -interface Metatag { - "og:image": string; - "theme-color": string; - "og:type": string; - "og:image:width": string; - "og:image:alt": string; - "twitter:card": string; - "og:site_name": string; - "og:title": string; - "og:image:height": string; - "og:image:type": string; - "og:description": string; - "twitter:creator": string; - viewport: string; - "og:locale": string; - position: string; - "og:url": string; -} - -// shit cache -const localCache = new Map(); - -export default { - data: ( - - - ) as never, - post: "GLOBAL", - run: async (interaction) => { - const cacheId = interaction.data.getString("query", true); - const tags = localCache.get(cacheId); - if (!tags) throw new Error("WTF"); - - const userId = interaction.data.getUser("user"); - - const embed = ( - - ) as never; - - await interaction.reply({ - content: userId ? `<@${userId}> learn how to fucking google` : "", - embeds: [embed] - }); - }, - autocomplete: async (interaction) => { - const query = interaction.data.getFocused().value; - if (query.length === 0) return; - - const url = `https://www.googleapis.com/customsearch/v1?key=${process.env.SEARCH_KEY}&cx=${process.env.CX}&q=${query}&num=10`; - - const response = await fetch(url); - const body: GoogleAPIResponse = await response.json() as never; - - populateCache(body.items); - - await interaction.showChoices(body.items.map((val) => ({ name: val.title, value: val.cacheId }))); - } -} satisfies ApplicationCommand; - -function populateCache(items: Array): void { - for (let i = 0, { length } = items; i < length; i++) { - const item = items[i]; - const meta = item.pagemap.metatags; - - if (localCache.has(item.cacheId)) continue; - - localCache.set(item.cacheId, meta[0]); - } -} diff --git a/packages/test/src/events-adv/ready.ts b/packages/test/src/events/ready.ts similarity index 68% rename from packages/test/src/events-adv/ready.ts rename to packages/test/src/events/ready.ts index fb40fc0..9d184e1 100644 --- a/packages/test/src/events-adv/ready.ts +++ b/packages/test/src/events/ready.ts @@ -1,4 +1,4 @@ -import { $listener } from "@lilybird/handlers/advanced"; +import { $listener } from "../handlers.js"; $listener({ event: "ready", diff --git a/packages/test/src/events/ready.tsx b/packages/test/src/events/ready.tsx deleted file mode 100644 index 3e98833..0000000 --- a/packages/test/src/events/ready.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { Event } from "@lilybird/handlers/simple"; - -export default { - event: "ready", - run(client) { - console.log("Connected as", client.user.username); - } -} satisfies Event<"ready">; diff --git a/packages/test/src/handlers.ts b/packages/test/src/handlers.ts index a8a3bed..005cc22 100644 --- a/packages/test/src/handlers.ts +++ b/packages/test/src/handlers.ts @@ -1,9 +1,17 @@ -import { defaultTransformers } from "@lilybird/transformers"; -import { Handler } from "@lilybird/handlers/advanced"; +import { makeTransformersObject, acsKeys } from "@lilybird/transformers"; +import { Handler } from "@lilybird/handlers"; -import type { DefaultTransformers } from "@lilybird/transformers"; +import type { MergeTransformers, ApplicationCommandStore } from "@lilybird/transformers"; +import type { Client } from "lilybird"; -export const handler = new Handler({ transformers: defaultTransformers }); +export const defaultTransformers = makeTransformersObject(); + +export const handler = new Handler, ApplicationCommandStore.HO, ApplicationCommandStore.AO>({ + acsOptions: { + transformed: true, + customKeys: acsKeys + } +}); export const $applicationCommand = handler.storeCommand.bind(handler); export const $listener = handler.storeListener.bind(handler); export const $component = handler.buttonCollector.bind(handler); diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index 3c0254e..6890be8 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -1,16 +1,19 @@ -import { Intents, createClient } from "lilybird"; -import { handler } from "./handlers.js"; +import { defaultTransformers, handler } from "./handlers.js"; +import { createClient, Intents } from "lilybird"; -handler.cachePath = `${import.meta.dir}/lily-cache/handler`; +import "./barrel.js"; -await handler.scanDir(`${import.meta.dir}/commands-adv`); -await handler.scanDir(`${import.meta.dir}/events-adv`); +import type { Client } from "lilybird"; +import type { MergeTransformers } from "@lilybird/transformers"; -await createClient({ +await createClient>({ token: process.env.TOKEN, - intents: [Intents.GUILDS], - setup: async (client) => { - await handler.loadGlobalCommands(client); - }, - listeners: handler.getListenersObject() + intents: Intents.GUILDS, + transformers: defaultTransformers, + listeners: { + setup: async (client) => { + await handler.loadGlobalCommands(client); + }, + ...handler.getListenersObject() + } }); diff --git a/packages/transformers/package.json b/packages/transformers/package.json index fffe173..8ebfe15 100644 --- a/packages/transformers/package.json +++ b/packages/transformers/package.json @@ -1,6 +1,6 @@ { "name": "@lilybird/transformers", - "version": "0.4.3", + "version": "0.5.0-beta.2", "description": "Event transformers and more for lilybird", "main": "./dist/index.js", "author": "DidaS", @@ -48,6 +48,6 @@ "transformers" ], "peerDependencies": { - "lilybird": "^0.7.0" + "lilybird": "^0.9.0-beta.2" } } \ No newline at end of file diff --git a/packages/transformers/src/factories/interaction.ts b/packages/transformers/src/factories/interaction.ts index c84ce7a..e766a67 100644 --- a/packages/transformers/src/factories/interaction.ts +++ b/packages/transformers/src/factories/interaction.ts @@ -59,6 +59,7 @@ export type InteractionData = ApplicationCommandData | AutocompleteData | Messag export interface AutocompleteData extends ApplicationCommandData {} export type InteractionReplyOptions = LilyInteraction.MessageCallbackDataStructure & { + componentsV2?: boolean, ephemeral?: boolean, suppressEmbeds?: boolean }; @@ -119,9 +120,10 @@ export class Interaction | undefined = Array> { - return this.type === ComponentType.StringSelect || this.type >= ComponentType.UserSelect; + // eslint-disable-next-line @stylistic/no-extra-parens + return this.type === ComponentType.StringSelect || (this.type >= ComponentType.UserSelect && this.type <= ComponentType.ChannelSelect); } } diff --git a/packages/transformers/src/factories/message.ts b/packages/transformers/src/factories/message.ts index 46a8c2f..aa09a4c 100644 --- a/packages/transformers/src/factories/message.ts +++ b/packages/transformers/src/factories/message.ts @@ -23,11 +23,13 @@ export type PartialMessage = Partial & { }; export interface MessageEditOptions extends LilyMessage.EditJSONParams { + componentsV2?: boolean; suppressEmbeds?: boolean; } export interface MessageReplyOptions extends LilyMessage.CreateJSONParams { tts?: boolean; + componentsV2?: boolean; suppressEmbeds?: boolean; suppressNotifications?: boolean; } @@ -127,8 +129,9 @@ export class Message { if (typeof content === "string") { if (typeof options !== "undefined") { - const { suppressEmbeds, suppressNotifications, flags: fl, files: f, ...obj } = options; + const { suppressEmbeds, suppressNotifications, componentsV2, flags: fl, files: f, ...obj } = options; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags |= MessageFlags.SUPPRESS_EMBEDS; if (suppressNotifications) flags |= MessageFlags.SUPPRESS_NOTIFICATIONS; flags |= fl ?? 0; @@ -140,9 +143,10 @@ export class Message { }; } else data = { content }; } else { - const { suppressEmbeds, suppressNotifications, flags: fl, files: f, ...obj } = content; + const { suppressEmbeds, suppressNotifications, componentsV2, flags: fl, files: f, ...obj } = content; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags |= MessageFlags.SUPPRESS_EMBEDS; if (suppressNotifications) flags |= MessageFlags.SUPPRESS_NOTIFICATIONS; @@ -171,9 +175,10 @@ export class Message { if (typeof content === "string") { if (typeof options !== "undefined") { - const { suppressEmbeds, suppressNotifications, flags: fl, files: f, ...obj } = options; + const { suppressEmbeds, suppressNotifications, componentsV2, flags: fl, files: f, ...obj } = options; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags |= MessageFlags.SUPPRESS_EMBEDS; if (suppressNotifications) flags |= MessageFlags.SUPPRESS_NOTIFICATIONS; @@ -184,9 +189,10 @@ export class Message { }; } else data = { content }; } else { - const { suppressEmbeds, suppressNotifications, flags: fl, files: f, ...obj } = content; + const { suppressEmbeds, suppressNotifications, componentsV2, flags: fl, files: f, ...obj } = content; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags |= MessageFlags.SUPPRESS_EMBEDS; if (suppressNotifications) flags |= MessageFlags.SUPPRESS_NOTIFICATIONS; @@ -211,9 +217,10 @@ export class Message { if (typeof content === "string") { if (typeof options !== "undefined") { - const { suppressEmbeds, suppressNotifications, flags: fl, files: f, ...obj } = options; + const { suppressEmbeds, suppressNotifications, componentsV2, flags: fl, files: f, ...obj } = options; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags |= MessageFlags.SUPPRESS_EMBEDS; if (suppressNotifications) flags |= MessageFlags.SUPPRESS_NOTIFICATIONS; @@ -224,9 +231,10 @@ export class Message { }; } else data = { content }; } else { - const { suppressEmbeds, suppressNotifications, flags: fl, files: f, ...obj } = content; + const { suppressEmbeds, suppressNotifications, componentsV2, flags: fl, files: f, ...obj } = content; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags |= MessageFlags.SUPPRESS_EMBEDS; if (suppressNotifications) flags |= MessageFlags.SUPPRESS_NOTIFICATIONS; @@ -256,9 +264,10 @@ export class Message { if (typeof content === "string") { if (typeof options !== "undefined") { - const { suppressEmbeds, flags: fl, files: f, ...obj } = options; + const { suppressEmbeds, componentsV2, flags: fl, files: f, ...obj } = options; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags = MessageFlags.SUPPRESS_EMBEDS; files = f; @@ -270,9 +279,10 @@ export class Message { } else data = { content, flags }; } else { - const { suppressEmbeds, flags: fl, files: f, ...obj } = content; + const { suppressEmbeds, componentsV2, flags: fl, files: f, ...obj } = content; flags |= fl ?? 0; + if (componentsV2) flags |= MessageFlags.IS_COMPONENTS_V2; if (suppressEmbeds) flags = MessageFlags.SUPPRESS_EMBEDS; files = f; diff --git a/packages/transformers/src/factories/user.ts b/packages/transformers/src/factories/user.ts index 1a3dbbd..be08207 100644 --- a/packages/transformers/src/factories/user.ts +++ b/packages/transformers/src/factories/user.ts @@ -1,4 +1,3 @@ -import { GuildMember } from "./guild-member.js"; import { PremiumType, CDN } from "lilybird"; import type { User as LilyUser, Client, CDNOptions } from "lilybird"; @@ -21,7 +20,6 @@ export class User { public readonly premiumType: PremiumType; public readonly publicFlags: number; public readonly avatarDecoration: string | undefined | null; - public readonly member: GuildMember | undefined; public readonly client: Client; @@ -45,8 +43,6 @@ export class User { this.premiumType = user.premium_type ?? PremiumType.None; this.publicFlags = user.public_flags ?? 0; this.avatarDecoration = user.avatar_decoration; - - if ("member" in user) this.member = new GuildMember(client, user.member); } public avatarURL(options?: CDNOptions): string { diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts index b09ef10..08bdafe 100644 --- a/packages/transformers/src/index.ts +++ b/packages/transformers/src/index.ts @@ -1,17 +1,27 @@ -import type { BaseCachingStructure } from "lilybird"; -import { transformers } from "./transformers.js"; +import type { ApplicationCommandData, AutocompleteData, Interaction } from "./factories/interaction.js"; +import type { Awaitable, BaseCachingStructure } from "lilybird"; import type { Transformers } from "lilybird"; -export type DefaultTransformers = MergeTransformers; -export const defaultTransformers: DefaultTransformers = transformers; +export { makeTransformersObject } from "./transformers.js"; export const cacheKeys: Required["customKeys"] = { guild_voice_states: "voiceStates" }; -export type MergeTransformers = T & { +export declare namespace ApplicationCommandStore { + export type HO = (interaction: Interaction) => Awaitable; + export type AO = (interaction: Interaction) => Awaitable; +} + +export const acsKeys = { + sub_command: "data.subCommand", + sub_command_group: "data.subCommandGroup", + name: "data.name" +}; + +export type MergeTransformers> = T & { // eslint-disable-next-line @typescript-eslint/no-empty-object-type - [K in keyof Transformers as T[K] extends {} ? never : K]: Transformers[K] + [K in keyof Transformers as T[K] extends {} ? never : K]: Transformers[K] }; export type { diff --git a/packages/transformers/src/transformers.ts b/packages/transformers/src/transformers.ts index 2006f0c..1eed25e 100644 --- a/packages/transformers/src/transformers.ts +++ b/packages/transformers/src/transformers.ts @@ -8,81 +8,84 @@ import { Message } from "./index.js"; import type { Channel, ExtendedThreadChannel } from "./factories/channel.js"; import type { GuildMemberWithGuildId } from "./factories/guild-member.js"; -import type { Transformers, Guild as LilyGuild } from "lilybird"; +import type { Transformers, Guild as LilyGuild, Client } from "lilybird"; import type { Interaction, PartialMessage } from "./index.js"; import type { Guild, NewGuild } from "./factories/guild.js"; -export const transformers = { - channelCreate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Channel => channelFactory(client, data) - }, - channelUpdate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Channel => channelFactory(client, data) - }, - channelDelete: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Channel => channelFactory(client, data) - }, - channelPinsUpdate: { - return: TransformerReturnType.MULTIPLE, - handler: (_, data): [guildId: string | undefined, channelId: string, lastPinTimestamp: Date | null] => [ - data.guild_id, - data.channel_id, - typeof data.last_pin_timestamp === "string" ? new Date(data.last_pin_timestamp) : null - ] - }, - threadCreate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): ExtendedThreadChannel => channelFactory(client, data) - }, - threadUpdate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Channel => channelFactory(client, data) - }, - threadDelete: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Pick => new ThreadChannel(client, data, false) - }, - guildCreate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): LilyGuild.UnavailableStructure | NewGuild => guildFactory(client, data) - }, - guildUpdate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Guild => guildFactory(client, data) - }, - guildMemberAdd: { - return: TransformerReturnType.SINGLE, - handler: (client, data): GuildMemberWithGuildId => new GuildMember(client, data) - }, - guildMemberRemove: { - return: TransformerReturnType.MULTIPLE, - handler: (client, data): [id: string, user: User] => [data.guild_id, new User(client, data.user)] - }, - guildMemberUpdate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): GuildMemberWithGuildId => new GuildMember(client, data) - }, - interactionCreate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Interaction => interactionFactory(client, data) - }, - messageCreate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): Message => new Message(client, data) - }, - messageUpdate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): PartialMessage => new Message(client, data) - }, - messageDelete: { - return: TransformerReturnType.SINGLE, - handler: (client, data): PartialMessage => new Message(client, data) - }, - userUpdate: { - return: TransformerReturnType.SINGLE, - handler: (client, data): User => new User(client, data) - } -} satisfies Transformers; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function makeTransformersObject() { + return { + channelCreate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Channel => channelFactory(client, data) + }, + channelUpdate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Channel => channelFactory(client, data) + }, + channelDelete: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Channel => channelFactory(client, data) + }, + channelPinsUpdate: { + return: TransformerReturnType.MULTIPLE, + handler: (_, data): [guildId: string | undefined, channelId: string, lastPinTimestamp: Date | null] => [ + data.guild_id, + data.channel_id, + typeof data.last_pin_timestamp === "string" ? new Date(data.last_pin_timestamp) : null + ] + }, + threadCreate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): ExtendedThreadChannel => channelFactory(client, data) + }, + threadUpdate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Channel => channelFactory(client, data) + }, + threadDelete: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Pick => new ThreadChannel(client, data, false) + }, + guildCreate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): LilyGuild.UnavailableStructure | NewGuild => guildFactory(client, data) + }, + guildUpdate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Guild => guildFactory(client, data) + }, + guildMemberAdd: { + return: TransformerReturnType.SINGLE, + handler: (client, data): GuildMemberWithGuildId => new GuildMember(client, data) + }, + guildMemberRemove: { + return: TransformerReturnType.MULTIPLE, + handler: (client, data): [id: string, user: User] => [data.guild_id, new User(client, data.user)] + }, + guildMemberUpdate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): GuildMemberWithGuildId => new GuildMember(client, data) + }, + interactionCreate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Interaction => interactionFactory(client, data) + }, + messageCreate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): Message => new Message(client, data) + }, + messageUpdate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): PartialMessage => new Message(client, data) + }, + messageDelete: { + return: TransformerReturnType.SINGLE, + handler: (client, data): PartialMessage => new Message(client, data) + }, + userUpdate: { + return: TransformerReturnType.SINGLE, + handler: (client, data): User => new User(client, data) + } + } satisfies Transformers; +}