diff --git a/biome.json b/biome.json index ed8ba69..ab3a788 100644 --- a/biome.json +++ b/biome.json @@ -43,7 +43,8 @@ "noUselessConstructor": "off", "noThisInStatic": "off", "noExcessiveCognitiveComplexity": "off", - "noVoid": "off" + "noVoid": "off", + "noStaticOnlyClass": "off" }, "a11y": { "all": false diff --git a/src/api/Routes/guilds.ts b/src/api/Routes/guilds.ts index 28d8a4b..57c186b 100644 --- a/src/api/Routes/guilds.ts +++ b/src/api/Routes/guilds.ts @@ -84,6 +84,8 @@ import type { RESTPatchAPIGuildWidgetSettingsResult, RESTPostAPIAutoModerationRuleJSONBody, RESTPostAPIAutoModerationRuleResult, + RESTPostAPIGuildBulkBanJSONBody, + RESTPostAPIGuildBulkBanResult, RESTPostAPIGuildChannelJSONBody, RESTPostAPIGuildChannelResult, RESTPostAPIGuildEmojiJSONBody, @@ -233,6 +235,11 @@ export interface GuildRoutes { delete(args?: RestArguments): Promise; }; }; + 'bulk-bans': { + post( + args: RestArguments, + ): Promise; + }; mfa: { post( args: RestArguments, diff --git a/src/cache/index.ts b/src/cache/index.ts index a67fda5..69a43f0 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -14,6 +14,7 @@ import { StageInstances } from './resources/stage-instances'; import { Stickers } from './resources/stickers'; import { Threads } from './resources/threads'; import { VoiceStates } from './resources/voice-states'; +import { Bans } from './resources/bans'; import { ChannelType, GatewayIntentBits, type GatewayDispatchPayload } from 'discord-api-types/v10'; import type { InternalOptions, UsingClient } from '../commands'; @@ -36,7 +37,8 @@ export type GuildRelated = | 'presences' | 'stageInstances' | 'overwrites' - | 'messages'; + | 'messages' + | 'bans'; // ClientBased export type NonGuildBased = 'users' | 'guilds'; @@ -58,6 +60,8 @@ export type CachedEvents = | 'GUILD_ROLE_CREATE' | 'GUILD_ROLE_UPDATE' | 'GUILD_ROLE_DELETE' + | 'GUILD_BAN_ADD' + | 'GUILD_BAN_REMOVE' | 'GUILD_EMOJIS_UPDATE' | 'GUILD_STICKERS_UPDATE' | 'GUILD_MEMBER_ADD' @@ -93,6 +97,7 @@ export class Cache { presences?: Presences; stageInstances?: StageInstances; messages?: Messages; + bans?: Bans; constructor( public intents: number, @@ -144,6 +149,9 @@ export class Cache { if (!this.disabledCache.includes('messages')) { this.messages = new Messages(this, client); } + if (!this.disabledCache.includes('bans')) { + this.bans = new Bans(this, client); + } } /** @internal */ @@ -163,6 +171,7 @@ export class Cache { this.threads?.__setClient(client); this.stageInstances?.__setClient(client); this.messages?.__setClient(client); + this.bans?.__setClient(client); } flush(): ReturnCache { @@ -206,6 +215,10 @@ export class Cache { return this.hasIntent('DirectMessages'); } + get hasBansIntent() { + return this.hasIntent('GuildBans'); + } + async bulkGet( keys: ( | readonly [ @@ -246,6 +259,7 @@ export class Cache { case 'users': case 'guilds': case 'overwrites': + case 'bans': case 'messages': { if (!allData[type]) { @@ -313,6 +327,7 @@ export class Cache { case 'stageInstances': case 'emojis': case 'overwrites': + case 'bans': case 'messages': { if (!this[type]?.filter(data, id, guildId)) continue; @@ -404,6 +419,7 @@ export class Cache { case 'stageInstances': case 'emojis': case 'overwrites': + case 'bans': case 'messages': { if (!this[type]?.filter(data, id, guildId)) continue; @@ -500,6 +516,12 @@ export class Cache { case 'GUILD_ROLE_DELETE': await this.roles?.remove(event.d.role_id, event.d.guild_id); break; + case 'GUILD_BAN_ADD': + await this.bans?.set(event.d.user.id, event.d.guild_id, event.d); + break; + case 'GUILD_BAN_REMOVE': + await this.bans?.remove(event.d.user.id, event.d.guild_id); + break; case 'GUILD_EMOJIS_UPDATE': await this.emojis?.remove(await this.emojis?.keys(event.d.guild_id), event.d.guild_id); await this.emojis?.set( diff --git a/src/cache/resources/bans.ts b/src/cache/resources/bans.ts new file mode 100644 index 0000000..f788220 --- /dev/null +++ b/src/cache/resources/bans.ts @@ -0,0 +1,46 @@ +import type { APIBan } from 'discord-api-types/v10'; +import type { ReturnCache } from '../..'; +import { fakePromise } from '../../common'; +import { GuildBasedResource } from './default/guild-based'; +import { GuildBan } from '../../structures/GuildBan'; +export class Bans extends GuildBasedResource { + namespace = 'ban'; + + //@ts-expect-error + filter(data: APIBan, id: string, guild_id: string) { + return true; + } + + override parse(data: any, key: string, guild_id: string) { + const { user, ...rest } = super.parse(data, data.user?.id ?? key, guild_id); + return rest; + } + + override get(id: string, guild: string): ReturnCache { + return fakePromise(super.get(id, guild)).then(rawBan => + rawBan ? new GuildBan(this.client, rawBan, guild) : undefined, + ); + } + + override bulk(ids: string[], guild: string): ReturnCache { + return fakePromise(super.bulk(ids, guild)).then( + bans => + bans + .map(rawBan => { + return rawBan ? new GuildBan(this.client, rawBan, guild) : undefined; + }) + .filter(Boolean) as GuildBan[], + ); + } + + override values(guild: string): ReturnCache { + return fakePromise(super.values(guild)).then( + bans => + bans + .map(rawBan => { + return rawBan ? new GuildBan(this.client, rawBan, guild) : undefined; + }) + .filter(Boolean) as GuildBan[], + ); + } +} diff --git a/src/client/base.ts b/src/client/base.ts index fb45eb4..a87f826 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -1,482 +1,630 @@ -import { join } from 'node:path'; -import { ApiHandler, Router } from '../api'; -import type { Adapter } from '../cache'; -import { Cache, MemoryAdapter } from '../cache'; -import type { - Command, - CommandContext, - ExtraProps, - OnOptionsReturnObject, - RegisteredMiddlewares, - UsingClient, -} from '../commands'; -import { IgnoreCommand, type InferWithPrefix, type MiddlewareContext } from '../commands/applications/shared'; -import { CommandHandler } from '../commands/handler'; -import { - ChannelShorter, - EmojiShorter, - GuildShorter, - InteractionShorter, - LogLevels, - Logger, - MemberShorter, - MergeOptions, - MessageShorter, - ReactionShorter, - RoleShorter, - TemplateShorter, - ThreadShorter, - UsersShorter, - WebhookShorter, - filterSplit, - magicImport, - type MakeRequired, -} from '../common'; - -import type { LocaleString, RESTPostAPIChannelMessageJSONBody } from 'discord-api-types/rest/v10'; -import type { Awaitable, DeepPartial, IntentStrings, OmitInsert, PermissionStrings, When } from '../common/types/util'; -import { ComponentHandler } from '../components/handler'; -import { LangsHandler } from '../langs/handler'; -import type { - ChatInputCommandInteraction, - ComponentInteraction, - Message, - MessageCommandInteraction, - ModalSubmitInteraction, - UserCommandInteraction, -} from '../structures'; -import type { ComponentCommand, ComponentContext, ModalCommand, ModalContext } from '../components'; -import { promises } from 'node:fs'; - -export class BaseClient { - rest!: ApiHandler; - cache!: Cache; - - users = new UsersShorter(this); - channels = new ChannelShorter(this); - guilds = new GuildShorter(this); - messages = new MessageShorter(this); - members = new MemberShorter(this); - webhooks = new WebhookShorter(this); - templates = new TemplateShorter(this); - roles = new RoleShorter(this); - reactions = new ReactionShorter(this); - emojis = new EmojiShorter(this); - threads = new ThreadShorter(this); - interactions = new InteractionShorter(this); - - debugger?: Logger; - - logger = new Logger({ - name: '[Seyfert]', - }); - - langs? = new LangsHandler(this.logger); - commands? = new CommandHandler(this.logger, this); - components? = new ComponentHandler(this.logger, this); - - private _applicationId?: string; - private _botId?: string; - middlewares?: Record; - - protected static assertString(value: unknown, message?: string): asserts value is string { - if (!(typeof value === 'string' && value !== '')) { - throw new Error(message ?? 'Value is not a string'); - } - } - - protected static getBotIdFromToken(token: string): string { - return Buffer.from(token.split('.')[0], 'base64').toString('ascii'); - } - - options: BaseClientOptions; - - /**@internal */ - static _seyfertConfig?: InternalRuntimeConfigHTTP | InternalRuntimeConfig; - - constructor(options?: BaseClientOptions) { - this.options = MergeOptions( - { - commands: { - defaults: { - onRunError(context: CommandContext, error: unknown): any { - context.client.logger.fatal(`${context.command.name}.`, context.author.id, error); - }, - onOptionsError(context: CommandContext<{}, never>, metadata: OnOptionsReturnObject): any { - context.client.logger.fatal(`${context.command.name}.`, context.author.id, metadata); - }, - onMiddlewaresError(context: CommandContext<{}, never>, error: string): any { - context.client.logger.fatal(`${context.command.name}.`, context.author.id, error); - }, - onBotPermissionsFail(context: CommandContext<{}, never>, permissions: PermissionStrings): any { - context.client.logger.fatal( - `${context.command.name}.`, - context.author.id, - permissions, - ); - }, - onPermissionsFail(context: CommandContext<{}, never>, permissions: PermissionStrings): any { - context.client.logger.fatal( - `${context.command.name}.`, - context.author.id, - permissions, - ); - }, - onInternalError(client: UsingClient, command: Command, error?: unknown): any { - client.logger.fatal(`${command.name}.`, error); - }, - }, - }, - components: { - defaults: { - onRunError(context: ComponentContext, error: unknown): any { - context.client.logger.fatal('ComponentCommand.', context.author.id, error); - }, - onMiddlewaresError(context: ComponentContext, error: string): any { - context.client.logger.fatal('ComponentCommand.', context.author.id, error); - }, - onInternalError(client: UsingClient, error?: unknown): any { - client.logger.fatal(error); - }, - }, - }, - modals: { - defaults: { - onRunError(context: ModalContext, error: unknown): any { - context.client.logger.fatal('ComponentCommand.', context.author.id, error); - }, - onMiddlewaresError(context: ModalContext, error: string): any { - context.client.logger.fatal('ComponentCommand.', context.author.id, error); - }, - onInternalError(client: UsingClient, error?: unknown): any { - client.logger.fatal(error); - }, - }, - }, - } satisfies BaseClientOptions, - options, - ); - } - - set botId(id: string) { - this._botId = id; - } - - get botId() { - return this._botId ?? BaseClient.getBotIdFromToken(this.rest.options.token); - } - - set applicationId(id: string) { - this._applicationId = id; - } - - get applicationId() { - return this._applicationId ?? this.botId; - } - - get proxy() { - return new Router(this.rest).createProxy(); - } - - setServices({ rest, cache, langs, middlewares, handlers }: ServicesOptions) { - if (rest) { - this.rest = rest; - } - if (cache) { - this.cache = new Cache( - this.cache?.intents ?? 0, - cache?.adapter ?? this.cache?.adapter ?? new MemoryAdapter(), - cache.disabledCache ?? this.cache?.disabledCache ?? [], - this, - ); - } - if (middlewares) { - this.middlewares = middlewares; - } - if (handlers) { - if ('components' in handlers) { - if (!handlers.components) { - this.components = undefined; - } else if (typeof handlers.components === 'function') { - this.components ??= new ComponentHandler(this.logger, this); - this.components.setHandlers({ callback: handlers.components }); - } else { - this.components = handlers.components; - } - } - if ('commands' in handlers) { - if (!handlers.commands) { - this.commands = undefined; - } else if (typeof handlers.commands === 'object') { - this.commands ??= new CommandHandler(this.logger, this); - this.commands.setHandlers(handlers.commands); - } else { - this.commands = handlers.commands; - } - } - if ('langs' in handlers) { - if (!handlers.langs) { - this.langs = undefined; - } else if (typeof handlers.langs === 'function') { - this.langs ??= new LangsHandler(this.logger); - this.langs.setHandlers({ callback: handlers.langs }); - } else { - this.langs = handlers.langs; - } - } - } - if (langs) { - if (langs.default) this.langs!.defaultLang = langs.default; - if (langs.aliases) this.langs!.aliases = Object.entries(langs.aliases); - } - } - - protected async execute(..._options: unknown[]) { - if ((await this.getRC()).debug) { - this.debugger = new Logger({ - name: '[Debug]', - logLevel: LogLevels.Debug, - }); - } - } - - async start( - options: Pick, 'langsDir' | 'commandsDir' | 'connection' | 'token' | 'componentsDir'> = { - token: undefined, - langsDir: undefined, - commandsDir: undefined, - connection: undefined, - componentsDir: undefined, - }, - ) { - await this.loadLangs(options.langsDir); - await this.loadCommands(options.commandsDir); - await this.loadComponents(options.componentsDir); - - const { token: tokenRC } = await this.getRC(); - const token = options?.token ?? tokenRC; - - if (!this.rest) { - BaseClient.assertString(token, 'token is not a string'); - this.rest = new ApiHandler({ - token, - baseUrl: 'api/v10', - domain: 'https://discord.com', - debug: (await this.getRC()).debug, - }); - } - - if (this.cache) { - this.cache.__setClient(this); - } else { - this.cache = new Cache(0, new MemoryAdapter(), [], this); - } - } - - protected async onPacket(..._packet: unknown[]) { - throw new Error('Function not implemented'); - } - - shouldUploadCommands(cachePath: string) { - return this.commands!.shouldUpload(cachePath).then(async should => { - if (should) await promises.writeFile(cachePath, JSON.stringify(this.commands!.values.map(x => x.toJSON()))); - return should; - }); - } - - async uploadCommands(applicationId?: string) { - applicationId ??= await this.getRC().then(x => x.applicationId ?? this.applicationId); - BaseClient.assertString(applicationId, 'applicationId is not a string'); - - const commands = this.commands!.values; - const filter = filterSplit(commands, command => !command.guildId); - - await this.proxy.applications(applicationId).commands.put({ - body: filter.expect.filter(cmd => !('ignore' in cmd) || cmd.ignore !== IgnoreCommand.Slash).map(x => x.toJSON()), - }); - - const guilds = new Set(); - - for (const command of filter.never) { - for (const guild_id of command.guildId!) { - guilds.add(guild_id); - } - } - - for (const guild of guilds) { - await this.proxy - .applications(applicationId) - .guilds(guild) - .commands.put({ - body: filter.never - .filter(cmd => cmd.guildId?.includes(guild) && (!('ignore' in cmd) || cmd.ignore !== IgnoreCommand.Slash)) - .map(x => x.toJSON()), - }); - } - } - - async loadCommands(dir?: string) { - dir ??= await this.getRC().then(x => x.commands); - if (dir && this.commands) { - await this.commands.load(dir, this); - this.logger.info('CommandHandler loaded'); - } - } - - async loadComponents(dir?: string) { - dir ??= await this.getRC().then(x => x.components); - if (dir && this.components) { - await this.components.load(dir); - this.logger.info('ComponentHandler loaded'); - } - } - - async loadLangs(dir?: string) { - dir ??= await this.getRC().then(x => x.langs); - if (dir && this.langs) { - await this.langs.load(dir); - this.logger.info('LangsHandler loaded'); - } - } - - t(locale: string) { - return this.langs!.get(locale); - } - - async getRC< - T extends InternalRuntimeConfigHTTP | InternalRuntimeConfig = InternalRuntimeConfigHTTP | InternalRuntimeConfig, - >() { - const seyfertConfig = (BaseClient._seyfertConfig || - (await this.options.getRC?.()) || - (await magicImport(join(process.cwd(), 'seyfert.config.js')).then(x => x.default ?? x))) as T; - - const { locations, debug, ...env } = seyfertConfig; - - const obj = { - debug: !!debug, - ...env, - templates: locations.templates ? join(process.cwd(), locations.base, locations.templates) : undefined, - langs: locations.langs ? join(process.cwd(), locations.output, locations.langs) : undefined, - events: - 'events' in locations && locations.events ? join(process.cwd(), locations.output, locations.events) : undefined, - components: locations.components ? join(process.cwd(), locations.output, locations.components) : undefined, - commands: locations.commands ? join(process.cwd(), locations.output, locations.commands) : undefined, - base: join(process.cwd(), locations.base), - output: join(process.cwd(), locations.output), - }; - - BaseClient._seyfertConfig = seyfertConfig; - - return obj; - } -} - -export interface BaseClientOptions { - context?: ( - interaction: - | ChatInputCommandInteraction - | UserCommandInteraction - | MessageCommandInteraction - | ComponentInteraction - | ModalSubmitInteraction - | When, - ) => {}; - globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[]; - commands?: { - defaults?: { - onRunError?: Command['onRunError']; - onPermissionsFail?: Command['onPermissionsFail']; - onBotPermissionsFail?: Command['onBotPermissionsFail']; - onInternalError?: Command['onInternalError']; - onMiddlewaresError?: Command['onMiddlewaresError']; - onOptionsError?: Command['onOptionsError']; - onAfterRun?: Command['onAfterRun']; - props?: ExtraProps; - }; - }; - components?: { - defaults?: { - onRunError?: ComponentCommand['onRunError']; - onInternalError?: ComponentCommand['onInternalError']; - onMiddlewaresError?: ComponentCommand['onMiddlewaresError']; - onAfterRun?: ComponentCommand['onAfterRun']; - }; - }; - modals?: { - defaults?: { - onRunError?: ModalCommand['onRunError']; - onInternalError?: ModalCommand['onInternalError']; - onMiddlewaresError?: ModalCommand['onMiddlewaresError']; - onAfterRun?: ModalCommand['onAfterRun']; - }; - }; - allowedMentions?: Omit, 'parse'> & { - parse?: ('everyone' | 'roles' | 'users')[]; //nice types, d-api - }; - getRC?(): Awaitable; -} - -export interface StartOptions { - eventsDir: string; - langsDir: string; - commandsDir: string; - componentsDir: string; - connection: { intents: number }; - httpConnection: { - publicKey: string; - port: number; - useUWS: boolean; - }; - token: string; -} - -interface RC extends Variables { - debug?: boolean; - locations: { - base: string; - output: string; - commands?: string; - langs?: string; - templates?: string; - events?: string; - components?: string; - }; -} - -export interface Variables { - token: string; - intents?: number; - applicationId?: string; - port?: number; - publicKey?: string; -} - -export type InternalRuntimeConfigHTTP = Omit< - MakeRequired, - 'intents' | 'locations' -> & { locations: Omit }; -export type RuntimeConfigHTTP = Omit, 'intents' | 'locations'> & { - locations: Omit; -}; - -export type InternalRuntimeConfig = Omit, 'publicKey' | 'port'>; -export type RuntimeConfig = OmitInsert< - InternalRuntimeConfig, - 'intents', - { intents?: IntentStrings | number[] | number } ->; - -export interface ServicesOptions { - rest?: ApiHandler; - cache?: { adapter?: Adapter; disabledCache?: Cache['disabledCache'] }; - langs?: { - default?: string; - aliases?: Record; - }; - middlewares?: Record; - handlers?: { - components?: ComponentHandler | ComponentHandler['callback']; - commands?: CommandHandler | Parameters[0]; - langs?: LangsHandler | LangsHandler['callback']; - }; -} +import { join } from 'node:path'; +import { ApiHandler, Router } from '../api'; +import type { Adapter } from '../cache'; +import { Cache, MemoryAdapter } from '../cache'; +import type { + Command, + CommandContext, + ExtraProps, + OnOptionsReturnObject, + RegisteredMiddlewares, + UsingClient, +} from '../commands'; +import { + IgnoreCommand, + type InferWithPrefix, + type MiddlewareContext, +} from '../commands/applications/shared'; +import { CommandHandler } from '../commands/handler'; +import { + ChannelShorter, + EmojiShorter, + GuildShorter, + InteractionShorter, + LogLevels, + Logger, + MemberShorter, + MergeOptions, + MessageShorter, + ReactionShorter, + RoleShorter, + TemplateShorter, + ThreadShorter, + UsersShorter, + WebhookShorter, + filterSplit, + magicImport, + type MakeRequired, +} from '../common'; + +import type { + LocaleString, + RESTPostAPIChannelMessageJSONBody, +} from 'discord-api-types/rest/v10'; +import type { + Awaitable, + DeepPartial, + IntentStrings, + OmitInsert, + PermissionStrings, + When, +} from '../common/types/util'; +import { ComponentHandler } from '../components/handler'; +import { LangsHandler } from '../langs/handler'; +import type { + ChatInputCommandInteraction, + ComponentInteraction, + Message, + MessageCommandInteraction, + ModalSubmitInteraction, + UserCommandInteraction, +} from '../structures'; +import type { + ComponentCommand, + ComponentContext, + ModalCommand, + ModalContext, +} from '../components'; +import { promises } from 'node:fs'; +import { BanShorter } from '../common/shorters/bans'; + +export class BaseClient { + rest!: ApiHandler; + cache!: Cache; + + users = new UsersShorter(this); + channels = new ChannelShorter(this); + guilds = new GuildShorter(this); + messages = new MessageShorter(this); + members = new MemberShorter(this); + webhooks = new WebhookShorter(this); + templates = new TemplateShorter(this); + roles = new RoleShorter(this); + reactions = new ReactionShorter(this); + emojis = new EmojiShorter(this); + threads = new ThreadShorter(this); + bans = new BanShorter(this); + interactions = new InteractionShorter(this); + + debugger?: Logger; + + logger = new Logger({ + name: '[Seyfert]', + }); + + langs? = new LangsHandler(this.logger); + commands? = new CommandHandler(this.logger, this); + components? = new ComponentHandler(this.logger, this); + + private _applicationId?: string; + private _botId?: string; + middlewares?: Record; + + protected static assertString( + value: unknown, + message?: string + ): asserts value is string { + if (!(typeof value === 'string' && value !== '')) { + throw new Error(message ?? 'Value is not a string'); + } + } + + protected static getBotIdFromToken(token: string): string { + return Buffer.from(token.split('.')[0], 'base64').toString('ascii'); + } + + options: BaseClientOptions; + + /**@internal */ + static _seyfertConfig?: InternalRuntimeConfigHTTP | InternalRuntimeConfig; + + constructor(options?: BaseClientOptions) { + this.options = MergeOptions( + { + commands: { + defaults: { + onRunError( + context: CommandContext, + error: unknown + ): any { + context.client.logger.fatal( + `${context.command.name}.`, + context.author.id, + error + ); + }, + onOptionsError( + context: CommandContext<{}, never>, + metadata: OnOptionsReturnObject + ): any { + context.client.logger.fatal( + `${context.command.name}.`, + context.author.id, + metadata + ); + }, + onMiddlewaresError( + context: CommandContext<{}, never>, + error: string + ): any { + context.client.logger.fatal( + `${context.command.name}.`, + context.author.id, + error + ); + }, + onBotPermissionsFail( + context: CommandContext<{}, never>, + permissions: PermissionStrings + ): any { + context.client.logger.fatal( + `${context.command.name}.`, + context.author.id, + permissions + ); + }, + onPermissionsFail( + context: CommandContext<{}, never>, + permissions: PermissionStrings + ): any { + context.client.logger.fatal( + `${context.command.name}.`, + context.author.id, + permissions + ); + }, + onInternalError( + client: UsingClient, + command: Command, + error?: unknown + ): any { + client.logger.fatal( + `${command.name}.`, + error + ); + }, + }, + }, + components: { + defaults: { + onRunError( + context: ComponentContext, + error: unknown + ): any { + context.client.logger.fatal( + 'ComponentCommand.', + context.author.id, + error + ); + }, + onMiddlewaresError( + context: ComponentContext, + error: string + ): any { + context.client.logger.fatal( + 'ComponentCommand.', + context.author.id, + error + ); + }, + onInternalError( + client: UsingClient, + error?: unknown + ): any { + client.logger.fatal(error); + }, + }, + }, + modals: { + defaults: { + onRunError(context: ModalContext, error: unknown): any { + context.client.logger.fatal( + 'ComponentCommand.', + context.author.id, + error + ); + }, + onMiddlewaresError( + context: ModalContext, + error: string + ): any { + context.client.logger.fatal( + 'ComponentCommand.', + context.author.id, + error + ); + }, + onInternalError( + client: UsingClient, + error?: unknown + ): any { + client.logger.fatal(error); + }, + }, + }, + } satisfies BaseClientOptions, + options + ); + } + + set botId(id: string) { + this._botId = id; + } + + get botId() { + return ( + this._botId ?? BaseClient.getBotIdFromToken(this.rest.options.token) + ); + } + + set applicationId(id: string) { + this._applicationId = id; + } + + get applicationId() { + return this._applicationId ?? this.botId; + } + + get proxy() { + return new Router(this.rest).createProxy(); + } + + setServices({ + rest, + cache, + langs, + middlewares, + handlers, + }: ServicesOptions) { + if (rest) { + this.rest = rest; + } + if (cache) { + this.cache = new Cache( + this.cache?.intents ?? 0, + cache?.adapter ?? this.cache?.adapter ?? new MemoryAdapter(), + cache.disabledCache ?? this.cache?.disabledCache ?? [], + this + ); + } + if (middlewares) { + this.middlewares = middlewares; + } + if (handlers) { + if ('components' in handlers) { + if (!handlers.components) { + this.components = undefined; + } else if (typeof handlers.components === 'function') { + this.components ??= new ComponentHandler(this.logger, this); + this.components.setHandlers({ + callback: handlers.components, + }); + } else { + this.components = handlers.components; + } + } + if ('commands' in handlers) { + if (!handlers.commands) { + this.commands = undefined; + } else if (typeof handlers.commands === 'object') { + this.commands ??= new CommandHandler(this.logger, this); + this.commands.setHandlers(handlers.commands); + } else { + this.commands = handlers.commands; + } + } + if ('langs' in handlers) { + if (!handlers.langs) { + this.langs = undefined; + } else if (typeof handlers.langs === 'function') { + this.langs ??= new LangsHandler(this.logger); + this.langs.setHandlers({ callback: handlers.langs }); + } else { + this.langs = handlers.langs; + } + } + } + if (langs) { + if (langs.default) this.langs!.defaultLang = langs.default; + if (langs.aliases) + this.langs!.aliases = Object.entries(langs.aliases); + } + } + + protected async execute(..._options: unknown[]) { + if ((await this.getRC()).debug) { + this.debugger = new Logger({ + name: '[Debug]', + logLevel: LogLevels.Debug, + }); + } + } + + async start( + options: Pick< + DeepPartial, + | 'langsDir' + | 'commandsDir' + | 'connection' + | 'token' + | 'componentsDir' + > = { + token: undefined, + langsDir: undefined, + commandsDir: undefined, + connection: undefined, + componentsDir: undefined, + } + ) { + await this.loadLangs(options.langsDir); + await this.loadCommands(options.commandsDir); + await this.loadComponents(options.componentsDir); + + const { token: tokenRC } = await this.getRC(); + const token = options?.token ?? tokenRC; + + if (!this.rest) { + BaseClient.assertString(token, 'token is not a string'); + this.rest = new ApiHandler({ + token, + baseUrl: 'api/v10', + domain: 'https://discord.com', + debug: (await this.getRC()).debug, + }); + } + + if (this.cache) { + this.cache.__setClient(this); + } else { + this.cache = new Cache(0, new MemoryAdapter(), [], this); + } + } + + protected async onPacket(..._packet: unknown[]) { + throw new Error('Function not implemented'); + } + + shouldUploadCommands(cachePath: string) { + return this.commands!.shouldUpload(cachePath).then(async (should) => { + if (should) + await promises.writeFile( + cachePath, + JSON.stringify(this.commands!.values.map((x) => x.toJSON())) + ); + return should; + }); + } + + async uploadCommands(applicationId?: string) { + applicationId ??= await this.getRC().then( + (x) => x.applicationId ?? this.applicationId + ); + BaseClient.assertString(applicationId, 'applicationId is not a string'); + + const commands = this.commands!.values; + const filter = filterSplit(commands, (command) => !command.guildId); + + await this.proxy.applications(applicationId).commands.put({ + body: filter.expect + .filter( + (cmd) => + !('ignore' in cmd) || cmd.ignore !== IgnoreCommand.Slash + ) + .map((x) => x.toJSON()), + }); + + const guilds = new Set(); + + for (const command of filter.never) { + for (const guild_id of command.guildId!) { + guilds.add(guild_id); + } + } + + for (const guild of guilds) { + await this.proxy + .applications(applicationId) + .guilds(guild) + .commands.put({ + body: filter.never + .filter( + (cmd) => + cmd.guildId?.includes(guild) && + (!('ignore' in cmd) || + cmd.ignore !== IgnoreCommand.Slash) + ) + .map((x) => x.toJSON()), + }); + } + } + + async loadCommands(dir?: string) { + dir ??= await this.getRC().then((x) => x.commands); + if (dir && this.commands) { + await this.commands.load(dir, this); + this.logger.info('CommandHandler loaded'); + } + } + + async loadComponents(dir?: string) { + dir ??= await this.getRC().then((x) => x.components); + if (dir && this.components) { + await this.components.load(dir); + this.logger.info('ComponentHandler loaded'); + } + } + + async loadLangs(dir?: string) { + dir ??= await this.getRC().then((x) => x.langs); + if (dir && this.langs) { + await this.langs.load(dir); + this.logger.info('LangsHandler loaded'); + } + } + + t(locale: string) { + return this.langs!.get(locale); + } + + async getRC< + T extends InternalRuntimeConfigHTTP | InternalRuntimeConfig = + | InternalRuntimeConfigHTTP + | InternalRuntimeConfig + >() { + const seyfertConfig = (BaseClient._seyfertConfig || + (await this.options.getRC?.()) || + (await magicImport(join(process.cwd(), 'seyfert.config.js')).then( + (x) => x.default ?? x + ))) as T; + + const { locations, debug, ...env } = seyfertConfig; + + const obj = { + debug: !!debug, + ...env, + templates: locations.templates + ? join(process.cwd(), locations.base, locations.templates) + : undefined, + langs: locations.langs + ? join(process.cwd(), locations.output, locations.langs) + : undefined, + events: + 'events' in locations && locations.events + ? join(process.cwd(), locations.output, locations.events) + : undefined, + components: locations.components + ? join(process.cwd(), locations.output, locations.components) + : undefined, + commands: locations.commands + ? join(process.cwd(), locations.output, locations.commands) + : undefined, + base: join(process.cwd(), locations.base), + output: join(process.cwd(), locations.output), + }; + + BaseClient._seyfertConfig = seyfertConfig; + + return obj; + } +} + +export interface BaseClientOptions { + context?: ( + interaction: + | ChatInputCommandInteraction + | UserCommandInteraction + | MessageCommandInteraction + | ComponentInteraction + | ModalSubmitInteraction + | When + ) => {}; + globalMiddlewares?: readonly (keyof RegisteredMiddlewares)[]; + commands?: { + defaults?: { + onRunError?: Command['onRunError']; + onPermissionsFail?: Command['onPermissionsFail']; + onBotPermissionsFail?: Command['onBotPermissionsFail']; + onInternalError?: Command['onInternalError']; + onMiddlewaresError?: Command['onMiddlewaresError']; + onOptionsError?: Command['onOptionsError']; + onAfterRun?: Command['onAfterRun']; + props?: ExtraProps; + }; + }; + components?: { + defaults?: { + onRunError?: ComponentCommand['onRunError']; + onInternalError?: ComponentCommand['onInternalError']; + onMiddlewaresError?: ComponentCommand['onMiddlewaresError']; + onAfterRun?: ComponentCommand['onAfterRun']; + }; + }; + modals?: { + defaults?: { + onRunError?: ModalCommand['onRunError']; + onInternalError?: ModalCommand['onInternalError']; + onMiddlewaresError?: ModalCommand['onMiddlewaresError']; + onAfterRun?: ModalCommand['onAfterRun']; + }; + }; + allowedMentions?: Omit< + NonNullable, + 'parse' + > & { + parse?: ('everyone' | 'roles' | 'users')[]; //nice types, d-api + }; + getRC?(): Awaitable; +} + +export interface StartOptions { + eventsDir: string; + langsDir: string; + commandsDir: string; + componentsDir: string; + connection: { intents: number }; + httpConnection: { + publicKey: string; + port: number; + useUWS: boolean; + }; + token: string; +} + +interface RC extends Variables { + debug?: boolean; + locations: { + base: string; + output: string; + commands?: string; + langs?: string; + templates?: string; + events?: string; + components?: string; + }; +} + +export interface Variables { + token: string; + intents?: number; + applicationId?: string; + port?: number; + publicKey?: string; +} + +export type InternalRuntimeConfigHTTP = Omit< + MakeRequired, + 'intents' | 'locations' +> & { locations: Omit }; +export type RuntimeConfigHTTP = Omit< + MakeRequired, + 'intents' | 'locations' +> & { + locations: Omit; +}; + +export type InternalRuntimeConfig = Omit< + MakeRequired, + 'publicKey' | 'port' +>; +export type RuntimeConfig = OmitInsert< + InternalRuntimeConfig, + 'intents', + { intents?: IntentStrings | number[] | number } +>; + +export interface ServicesOptions { + rest?: ApiHandler; + cache?: { adapter?: Adapter; disabledCache?: Cache['disabledCache'] }; + langs?: { + default?: string; + aliases?: Record; + }; + middlewares?: Record; + handlers?: { + components?: ComponentHandler | ComponentHandler['callback']; + commands?: + | CommandHandler + | Parameters[0]; + langs?: LangsHandler | LangsHandler['callback']; + }; +} \ No newline at end of file diff --git a/src/client/client.ts b/src/client/client.ts index ffc46ec..01d445f 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -26,13 +26,26 @@ import { } from '../common'; import { EventHandler } from '../events'; import { ClientUser } from '../structures'; -import { ShardManager, properties, type ShardManagerOptions } from '../websocket'; +import { + ShardManager, + properties, + type ShardManagerOptions, +} from '../websocket'; import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate'; import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate'; -import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base'; +import type { + BaseClientOptions, + InternalRuntimeConfig, + ServicesOptions, + StartOptions, +} from './base'; import { BaseClient } from './base'; import { onInteractionCreate } from './oninteractioncreate'; -import { defaultArgsParser, defaultOptionsParser, onMessageCreate } from './onmessagecreate'; +import { + defaultArgsParser, + defaultOptionsParser, + onMessageCreate, +} from './onmessagecreate'; import { Collectors } from './collectors'; let parentPort: import('node:worker_threads').MessagePort; @@ -42,7 +55,10 @@ export class Client extends BaseClient { gateway!: ShardManager; me!: If; declare options: Omit & { - commands: MakeRequired, 'argsParser' | 'optionsParser'>; + commands: MakeRequired< + NonNullable, + 'argsParser' | 'optionsParser' + >; }; memberUpdateHandler = new MemberUpdateHandler(); presenceUpdateHandler = new PresenceUpdateHandler(); @@ -54,11 +70,14 @@ export class Client extends BaseClient { this.options = MergeOptions( { commands: { - argsParser: defaultArgsParser, - optionsParser: defaultOptionsParser, + argsParser: + options?.commands?.argsParser ?? defaultArgsParser, + optionsParser: + options?.commands?.optionsParser ?? + defaultOptionsParser, }, } satisfies ClientOptions, - this.options, + this.options ); } @@ -96,43 +115,60 @@ export class Client extends BaseClient { } async loadEvents(dir?: string) { - dir ??= await this.getRC().then(x => x.events); + dir ??= await this.getRC().then((x) => x.events); if (dir && this.events) { await this.events.load(dir); this.logger.info('EventHandler loaded'); } } - protected async execute(options: { token?: string; intents?: number } = {}) { + protected async execute( + options: { token?: string; intents?: number } = {} + ) { await super.execute(options); - const worker_threads = lazyLoadPackage('node:worker_threads'); + const worker_threads = lazyLoadPackage< + typeof import('node:worker_threads') + >('node:worker_threads'); if (worker_threads?.parentPort) { parentPort = worker_threads.parentPort; } if (worker_threads?.workerData?.__USING_WATCHER__) { - parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => { - switch (data.type) { - case 'PAYLOAD': - this.gateway.options.handlePayload(data.shardId, data.payload); - break; - case 'SEND_TO_SHARD': - this.gateway.send(data.shardId, data.payload); - break; + parentPort?.on( + 'message', + (data: WatcherPayload | WatcherSendToShard) => { + switch (data.type) { + case 'PAYLOAD': + this.gateway.options.handlePayload( + data.shardId, + data.payload + ); + break; + case 'SEND_TO_SHARD': + this.gateway.send(data.shardId, data.payload); + break; + } } - }); + ); } else { await this.gateway.spawnShards(); } } - async start(options: Omit, 'httpConnection'> = {}, execute = true) { + async start( + options: Omit, 'httpConnection'> = {}, + execute = true + ) { await super.start(options); await this.loadEvents(options.eventsDir); - const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC(); + const { + token: tokenRC, + intents: intentsRC, + debug: debugRC, + } = await this.getRC(); const token = options?.token ?? tokenRC; const intents = options?.connection?.intents ?? intentsRC; @@ -149,9 +185,14 @@ export class Client extends BaseClient { presence: this.options?.presence, debug: debugRC, shardStart: this.options?.shards?.start, - shardEnd: this.options?.shards?.end ?? this.options?.shards?.total, - totalShards: this.options?.shards?.total ?? this.options?.shards?.end, - properties: { ...properties, ...this.options?.gateway?.properties }, + shardEnd: + this.options?.shards?.end ?? this.options?.shards?.total, + totalShards: + this.options?.shards?.total ?? this.options?.shards?.end, + properties: { + ...properties, + ...this.options?.gateway?.properties, + }, compress: this.options?.gateway?.compress, }); } @@ -173,29 +214,59 @@ export class Client extends BaseClient { if (!this.memberUpdateHandler.check(packet.d)) { return; } - await this.events?.execute(packet.t, packet, this as Client, shardId); + await this.events?.execute( + packet.t, + packet, + this as Client, + shardId + ); break; case 'PRESENCE_UPDATE': if (!this.presenceUpdateHandler.check(packet.d as any)) { return; } - await this.events?.execute(packet.t, packet, this as Client, shardId); + await this.events?.execute( + packet.t, + packet, + this as Client, + shardId + ); break; case 'GUILD_CREATE': { if (this.__handleGuilds?.has(packet.d.id)) { this.__handleGuilds.delete(packet.d.id); - if (!this.__handleGuilds.size && [...this.gateway.values()].every(shard => shard.data.session_id)) { - await this.events?.runEvent('BOT_READY', this, this.me, -1); + if ( + !this.__handleGuilds.size && + [...this.gateway.values()].every( + (shard) => shard.data.session_id + ) + ) { + await this.events?.runEvent( + 'BOT_READY', + this, + this.me, + -1 + ); } if (!this.__handleGuilds.size) delete this.__handleGuilds; return this.cache.onPacket(packet); } - await this.events?.execute(packet.t, packet, this as Client, shardId); + await this.events?.execute( + packet.t, + packet, + this as Client, + shardId + ); break; } //rest of the events default: { - await this.events?.execute(packet.t, packet, this as Client, shardId); + await this.events?.execute( + packet.t, + packet, + this as Client, + shardId + ); switch (packet.t) { case 'INTERACTION_CREATE': await onInteractionCreate(this, packet.d, shardId); @@ -209,19 +280,36 @@ export class Client extends BaseClient { } this.botId = packet.d.user.id; this.applicationId = packet.d.application.id; - this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; + this.me = new ClientUser( + this, + packet.d.user, + packet.d.application + ) as never; if ( !( this.__handleGuilds?.size && - (this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds + (this.gateway.options.intents & + GatewayIntentBits.Guilds) === + GatewayIntentBits.Guilds ) ) { - if ([...this.gateway.values()].every(shard => shard.data.session_id)) { - await this.events?.runEvent('BOT_READY', this, this.me, -1); + if ( + [...this.gateway.values()].every( + (shard) => shard.data.session_id + ) + ) { + await this.events?.runEvent( + 'BOT_READY', + this, + this.me, + -1 + ); } delete this.__handleGuilds; } - this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`); + this.debugger?.debug( + `#${shardId}[${packet.d.user.username}](${this.botId}) is online...` + ); break; } break; @@ -243,15 +331,21 @@ export interface ClientOptions extends BaseClientOptions { }; commands?: BaseClientOptions['commands'] & { prefix?: (message: Message) => Promise | string[]; - deferReplyResponse?: (ctx: CommandContext) => Parameters[0]; + deferReplyResponse?: ( + ctx: CommandContext + ) => Parameters[0]; reply?: (ctx: CommandContext) => boolean; - argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record; + argsParser?: ( + content: string, + command: SubCommand | Command, + message: Message + ) => Record; optionsParser?: ( self: UsingClient, command: Command | SubCommand, message: GatewayMessageCreateDispatchData, args: Partial>, - resolved: MakeRequired, + resolved: MakeRequired ) => Awaitable<{ errors: { name: string; diff --git a/src/common/index.ts b/src/common/index.ts index 0eee05b..00637a4 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,8 +1,9 @@ export * from './it/constants'; export * from './it/utils'; -// export * from './it/colors'; export * from './it/logger'; +export * from './it/formatter'; +// export * from './shorters/channels'; export * from './shorters/emojis'; export * from './shorters/guilds'; @@ -15,6 +16,7 @@ export * from './shorters/users'; export * from './shorters/threads'; export * from './shorters/webhook'; export * from './shorters/interaction'; +// export * from './types/options'; export * from './types/resolvables'; export * from './types/util'; diff --git a/src/common/it/formatter.ts b/src/common/it/formatter.ts new file mode 100644 index 0000000..d24d1d9 --- /dev/null +++ b/src/common/it/formatter.ts @@ -0,0 +1,203 @@ +/** + * Represents heading levels. + */ +export enum HeadingLevel { + /** + * Represents a level 1 heading. (#) + */ + H1 = 1, + /** + * Represents a level 2 heading. (##) + */ + H2 = 2, + /** + * Represents a level 3 heading. (###) + */ + H3 = 3, +} + +/** + * Represents timestamp styles. + */ +export enum TimestampStyle { + /** + * Represents a short timestamp style. + */ + ShortTime = 't', + /** + * Represents a long timestamp style. + */ + LongTime = 'T', + /** + * Represents a short date style. + */ + ShortDate = 'd', + /** + * Represents a long date style. + */ + LongDate = 'D', + /** + * Represents a short time style. + */ + ShortDateTime = 'f', + /** + * Represents a long time style. + */ + LongDateTime = 'F', + /** + * Represents a relative time style. + */ + RelativeTime = 'R', +} + +/** + * Represents a message link. + */ +type MessageLink = `https://discord.com/channels/${string}/${string}/${string}`; + +/** + * Represents a timestamp. + */ +type Timestamp = ``; + +/** + * Represents a formatter utility for formatting content. + */ +export class Formatter { + /** + * Formats a code block. + * @param content The content of the code block. + * @param language The language of the code block. Defaults to 'txt'. + * @returns The formatted code block. + */ + static codeBlock(content: string, language = 'txt'): string { + return `\`\`\`${language}\n${content}\n\`\`\``; + } + + /** + * Formats content into inline code. + * @param content The content to format. + * @returns The formatted content. + */ + static inlineCode(content: string): `\`${string}\`` { + return `\`${content}\``; + } + + /** + * Formats content into bold text. + * @param content The content to format. + * @returns The formatted content. + */ + static bold(content: string): `**${string}**` { + return `**${content}**`; + } + + /** + * Formats content into italic text. + * @param content The content to format. + * @returns The formatted content. + */ + static italic(content: string): `*${string}*` { + return `*${content}*`; + } + + /** + * Formats content into underlined text. + * @param content The content to format. + * @returns The formatted content. + */ + static underline(content: string): `__${string}__` { + return `__${content}__`; + } + + /** + * Formats content into strikethrough text. + * @param content The content to format. + * @returns The formatted content. + */ + static strikeThrough(content: string): `~~${string}~~` { + return `~~${content}~~`; + } + + /** + * Formats content into a hyperlink. + * @param content The content to format. + * @param url The URL to hyperlink to. + * @returns The formatted content. + */ + static hyperlink(content: string, url: string): `[${string}](${string})` { + return `[${content}](${url})`; + } + + /** + * Formats content into a spoiler. + * @param content The content to format. + * @returns The formatted content. + */ + static spoiler(content: string): `||${string}||` { + return `||${content}||`; + } + + /** + * Formats content into a quote. + * @param content The content to format. + * @returns The formatted content. + */ + static blockQuote(content: string): string { + return `>>> ${content}`; + } + + /** + * Formats content into a quote. + * @param content The content to format. + * @returns The formatted content. + */ + static quote(content: string): string { + return `> ${content}`; + } + + /** + * Formats a message link. + * @param guildId The ID of the guild. + * @param channelId The ID of the channel. + * @param messageId The ID of the message. + * @returns The formatted message link. + */ + static messageLink(guildId: string, channelId: string, messageId: string): MessageLink { + return `https://discord.com/channels/${guildId}/${channelId}/${messageId}`; + } + + /** + * Formats a header. + * @param content The content of the header. + * @param level The level of the header. Defaults to 1. + * @returns The formatted header. + */ + static header(content: string, level: HeadingLevel = HeadingLevel.H1): string { + return `${'#'.repeat(level)} ${content}`; + } + + /** + * Formats a list. + * @param items The items of the list. + * @param ordered Whether the list is ordered. Defaults to false. + * @returns The formatted list. + */ + static list(items: string[], ordered = false): string { + return items + .map((item, index) => { + return (ordered ? `${index + 1}. ` : '- ') + item; + }) + .join('\n'); + } + + /** + * Formats the given timestamp into discord unix timestamp format. + * @param timestamp The timestamp to format. + * @param style The style of the timestamp. Defaults to 't'. + * @returns The formatted timestamp. + */ + static timestamp(timestamp: Date, style: TimestampStyle = TimestampStyle.RelativeTime): Timestamp { + return ``; + } +} diff --git a/src/common/shorters/bans.ts b/src/common/shorters/bans.ts new file mode 100644 index 0000000..2f079d8 --- /dev/null +++ b/src/common/shorters/bans.ts @@ -0,0 +1,86 @@ +import type { + APIBan, + RESTGetAPIGuildBansQuery, + RESTPostAPIGuildBulkBanJSONBody, + RESTPutAPIGuildBanJSONBody, +} from 'discord-api-types/v10'; +import { BaseShorter } from './base'; +import { GuildBan } from '../../structures/GuildBan'; + +export class BanShorter extends BaseShorter { + /** + * Bulk creates bans in the guild. + * @param guildId The ID of the guild. + * @param body The request body for bulk banning members. + * @param reason The reason for bulk banning members. + */ + async bulkCreate(guildId: string, body: RESTPostAPIGuildBulkBanJSONBody, reason?: string) { + const bans = await this.client.proxy.guilds(guildId)['bulk-bans'].post({ reason, body }); + for (const id of bans.banned_users) this.client.cache.members?.removeIfNI('GuildBans', id, guildId); + return bans; + } + + /** + * Unbans a member from the guild. + * @param guildId The ID of the guild. + * @param memberId The ID of the member to unban. + * @param reason The reason for unbanning the member. + */ + async remove(guildId: string, memberId: string, reason?: string) { + await this.client.proxy.guilds(guildId).bans(memberId).delete({ reason }); + } + + /** + * Bans a member from the guild. + * @param guildId The ID of the guild. + * @param memberId The ID of the member to ban. + * @param body The request body for banning the member. + * @param reason The reason for banning the member. + */ + async create(guildId: string, memberId: string, body?: RESTPutAPIGuildBanJSONBody, reason?: string) { + await this.client.proxy.guilds(guildId).bans(memberId).put({ reason, body }); + await this.client.cache.members?.removeIfNI('GuildBans', memberId, guildId); + } + + /** + * Fetches a ban from the guild. + * @param guildId The ID of the guild. + * @param userId The ID of the user to fetch. + * @param force Whether to force fetching the ban from the API even if it exists in the cache. + * @returns A Promise that resolves to the fetched ban. + */ + async fetch(guildId: string, userId: string, force = false) { + let ban; + if (!force) { + ban = await this.client.cache.bans?.get(userId, guildId); + if (ban) return ban; + } + + ban = await this.client.proxy.guilds(guildId).bans(userId).get(); + await this.client.cache.members?.set(ban.user!.id, guildId, ban); + return new GuildBan(this.client, ban, guildId); + } + + /** + * Lists bans in the guild based on the provided query. + * @param guildId The ID of the guild. + * @param query The query parameters for listing bans. + * @param force Whether to force listing bans from the API even if they exist in the cache. + * @returns A Promise that resolves to an array of listed bans. + */ + async list(guildId: string, query?: RESTGetAPIGuildBansQuery, force = false) { + let bans; + if (!force) { + bans = (await this.client.cache.bans?.values(guildId)) ?? []; + if (bans.length) return bans; + } + bans = await this.client.proxy.guilds(guildId).bans.get({ + query, + }); + await this.client.cache.bans?.set( + bans.map<[string, APIBan]>(x => [x.user!.id, x]), + guildId, + ); + return bans.map(m => new GuildBan(this.client, m, guildId)); + } +} diff --git a/src/index.ts b/src/index.ts index 790f7ee..2975771 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,112 +1,112 @@ -import { GatewayIntentBits } from 'discord-api-types/gateway/v10'; -import { - BaseClient, - type BaseClientOptions, - type InternalRuntimeConfig, - type InternalRuntimeConfigHTTP, - type RuntimeConfig, - type RuntimeConfigHTTP, -} from './client/base'; -import type { CustomEventsKeys, ClientNameEvents, EventContext } from './events'; -import { isCloudfareWorker } from './common'; -export { Logger, PermissionStrings, Watcher } from './common'; -// -export { Collection, LimitedCollection } from './collection'; -// -export * from './api'; -export * from './builders'; -export * from './cache'; -export * from './commands'; -export * from './components'; -export * from './events'; -export * from './langs'; -// -export { ShardManager, WorkerManager } from './websocket/discord'; -// -export * from './structures'; -// -export * from './client'; -// - -export function throwError(msg: string): never { - throw new Error(msg); -} - -/** - * Creates an event with the specified data and run function. - * - * @param data - The event data. - * @returns The created event. - * - * @example - * const myEvent = createEvent({ - * data: { name: 'ready', once: true }, - * run: (user, client, shard) => { - * client.logger.info(`Start ${user.username} on shard #${shard}`); - * } - * }); - */ -export function createEvent(data: { - data: { name: E; once?: boolean }; - run: (...args: EventContext<{ data: { name: E } }>) => any; -}) { - data.data.once ??= false; - return data; -} - -export const config = { - /** - * Configurations for the bot. - * - * @param data - The runtime configuration data for gateway connections. - * @returns The internal runtime configuration. - */ - bot(data: RuntimeConfig) { - return { - ...data, - intents: - 'intents' in data - ? typeof data.intents === 'number' - ? data.intents - : data.intents?.reduce( - (pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]), - 0, - ) ?? 0 - : 0, - } as InternalRuntimeConfig; - }, - /** - * Configurations for the HTTP server. - * - * @param data - The runtime configuration data for http server. - * @returns The internal runtime configuration for HTTP. - */ - http(data: RuntimeConfigHTTP) { - const obj = { - port: 8080, - ...data, - } as InternalRuntimeConfigHTTP; - if (isCloudfareWorker()) BaseClient._seyfertConfig = obj; - return obj; - }, -}; - -/** - * Extends the context of a command interaction. - * - * @param cb - The callback function to extend the context. - * @returns The extended context. - * - * @example - * const customContext = extendContext((interaction) => { - * return { - * owner: '123456789012345678', - * // Add your custom properties here - * }; - * }); - */ -export function extendContext( - cb: (interaction: Parameters>[0]) => T, -) { - return cb; -} +import { GatewayIntentBits } from 'discord-api-types/gateway/v10'; +import { + BaseClient, + type BaseClientOptions, + type InternalRuntimeConfig, + type InternalRuntimeConfigHTTP, + type RuntimeConfig, + type RuntimeConfigHTTP, +} from './client/base'; +import type { CustomEventsKeys, ClientNameEvents, EventContext } from './events'; +import { isCloudfareWorker } from './common'; +export { Logger, PermissionStrings, Watcher, Formatter } from './common'; +// +export { Collection, LimitedCollection } from './collection'; +// +export * from './api'; +export * from './builders'; +export * from './cache'; +export * from './commands'; +export * from './components'; +export * from './events'; +export * from './langs'; +// +export { ShardManager, WorkerManager } from './websocket/discord'; +// +export * from './structures'; +// +export * from './client'; +// + +export function throwError(msg: string): never { + throw new Error(msg); +} + +/** + * Creates an event with the specified data and run function. + * + * @param data - The event data. + * @returns The created event. + * + * @example + * const myEvent = createEvent({ + * data: { name: 'ready', once: true }, + * run: (user, client, shard) => { + * client.logger.info(`Start ${user.username} on shard #${shard}`); + * } + * }); + */ +export function createEvent(data: { + data: { name: E; once?: boolean }; + run: (...args: EventContext<{ data: { name: E } }>) => any; +}) { + data.data.once ??= false; + return data; +} + +export const config = { + /** + * Configurations for the bot. + * + * @param data - The runtime configuration data for gateway connections. + * @returns The internal runtime configuration. + */ + bot(data: RuntimeConfig) { + return { + ...data, + intents: + 'intents' in data + ? typeof data.intents === 'number' + ? data.intents + : data.intents?.reduce( + (pr, acc) => pr | (typeof acc === 'number' ? acc : GatewayIntentBits[acc]), + 0, + ) ?? 0 + : 0, + } as InternalRuntimeConfig; + }, + /** + * Configurations for the HTTP server. + * + * @param data - The runtime configuration data for http server. + * @returns The internal runtime configuration for HTTP. + */ + http(data: RuntimeConfigHTTP) { + const obj = { + port: 8080, + ...data, + } as InternalRuntimeConfigHTTP; + if (isCloudfareWorker()) BaseClient._seyfertConfig = obj; + return obj; + }, +}; + +/** + * Extends the context of a command interaction. + * + * @param cb - The callback function to extend the context. + * @returns The extended context. + * + * @example + * const customContext = extendContext((interaction) => { + * return { + * owner: '123456789012345678', + * // Add your custom properties here + * }; + * }); + */ +export function extendContext( + cb: (interaction: Parameters>[0]) => T, +) { + return cb; +} \ No newline at end of file diff --git a/src/structures/Guild.ts b/src/structures/Guild.ts index 0b707a9..21f4bc7 100644 --- a/src/structures/Guild.ts +++ b/src/structures/Guild.ts @@ -10,6 +10,7 @@ import { Sticker } from './Sticker'; import { BaseChannel, WebhookGuildMethods } from './channels'; import { BaseGuild } from './extra/BaseGuild'; import type { DiscordBase } from './extra/DiscordBase'; +import { GuildBan } from './GuildBan'; export interface Guild extends ObjectToLower>, DiscordBase {} export class Guild extends (BaseGuild as unknown as ToClass< @@ -75,6 +76,7 @@ export class Guild extends (BaseGuild as unk roles = GuildRole.methods({ client: this.client, guildId: this.id }); channels = BaseChannel.allMethods({ client: this.client, guildId: this.id }); emojis = GuildEmoji.methods({ client: this.client, guildId: this.id }); + bans = GuildBan.methods({ client: this.client, guildId: this.id }); } /** Maximun custom guild emojis per level */ diff --git a/src/structures/GuildBan.ts b/src/structures/GuildBan.ts new file mode 100644 index 0000000..993fe48 --- /dev/null +++ b/src/structures/GuildBan.ts @@ -0,0 +1,49 @@ +import type { APIBan, RESTGetAPIGuildBansQuery } from 'discord-api-types/v10'; +import type { UsingClient } from '../commands'; +import type { MethodContext, ObjectToLower } from '../common'; +import { DiscordBase } from './extra/DiscordBase'; +import type { BanShorter } from '../common/shorters/bans'; + +export interface GuildBan extends DiscordBase, ObjectToLower> {} + +export class GuildBan extends DiscordBase { + constructor( + client: UsingClient, + data: APIBan, + readonly guildId: string, + ) { + super(client, { ...data, id: data.user.id }); + } + + create(body?: Parameters[2], reason?: string) { + return this.client.bans.create(this.guildId, this.id, body, reason); + } + + remove(reason?: string) { + return this.client.bans.remove(this.guildId, this.id, reason); + } + + guild(force = false) { + return this.client.guilds.fetch(this.guildId, force); + } + + fetch(force = false) { + return this.client.bans.fetch(this.guildId, this.id, force); + } + + toString() { + return `<@${this.id}>`; + } + + static methods({ client, guildId }: MethodContext<{ guildId: string }>) { + return { + fetch: (userId: string, force = false) => client.bans.fetch(guildId, userId, force), + list: (query?: RESTGetAPIGuildBansQuery, force = false) => client.bans.list(guildId, query, force), + create: (memberId: string, body?: Parameters[2], reason?: string) => + client.bans.create(guildId, memberId, body, reason), + remove: (memberId: string, reason?: string) => client.bans.remove(guildId, memberId, reason), + bulkCreate: (body: Parameters[1], reason?: string) => + client.bans.bulkCreate(guildId, body, reason), + }; + } +} diff --git a/src/structures/channels.ts b/src/structures/channels.ts index 60ce415..40a20ab 100644 --- a/src/structures/channels.ts +++ b/src/structures/channels.ts @@ -41,7 +41,7 @@ import type { GuildMember } from './GuildMember'; import type { GuildRole } from './GuildRole'; import { DiscordBase } from './extra/DiscordBase'; import { channelLink } from './extra/functions'; -import type { RawFile } from '..'; +import { Collection, type RawFile } from '..'; export class BaseChannel extends DiscordBase> { declare type: T; @@ -216,9 +216,15 @@ export class MessagesMethods extends DiscordBase { return this.client.channels.typing(this.id); } - messages = MessagesMethods.messages({ client: this.client, channelId: this.id }); + messages = MessagesMethods.messages({ + client: this.client, + channelId: this.id, + }); pins = MessagesMethods.pins({ client: this.client, channelId: this.id }); - reactions = MessagesMethods.reactions({ client: this.client, channelId: this.id }); + reactions = MessagesMethods.reactions({ + client: this.client, + channelId: this.id, + }); static messages(ctx: MethodContext<{ channelId: string }>) { return { @@ -265,7 +271,10 @@ export class MessagesMethods extends DiscordBase { embeds: body.embeds?.map(x => (x instanceof Embed ? x.toJSON() : x)) ?? undefined, attachments: 'attachments' in body - ? body.attachments?.map((x, i) => ({ id: i, ...resolveAttachment(x) })) ?? undefined + ? body.attachments?.map((x, i) => ({ + id: i, + ...resolveAttachment(x), + })) ?? undefined : (files?.map((x, id) => ({ id, filename: x.name, @@ -378,10 +387,26 @@ export class VoiceChannelMethods extends DiscordBase { const filter = states.filter(state => state.channelId === this.id); return filter; } + + public async members(force?: boolean) { + const collection = new Collection(); + + const states = await this.states(); + + for (const state of states) { + const member = await state.member(force); + collection.set(member.id, member); + } + + return collection; + } } export class WebhookGuildMethods extends DiscordBase { - webhooks = WebhookGuildMethods.guild({ client: this.client, guildId: this.id }); + webhooks = WebhookGuildMethods.guild({ + client: this.client, + guildId: this.id, + }); static guild(ctx: MethodContext<{ guildId: string }>) { return { @@ -391,7 +416,10 @@ export class WebhookGuildMethods extends DiscordBase { } export class WebhookChannelMethods extends DiscordBase { - webhooks = WebhookChannelMethods.channel({ client: this.client, channelId: this.id }); + webhooks = WebhookChannelMethods.channel({ + client: this.client, + channelId: this.id, + }); static channel(ctx: MethodContext<{ channelId: string }>) { return { @@ -555,6 +583,7 @@ export type AllGuildChannels = export type AllTextableChannels = TextGuildChannel | VoiceChannel | DMChannel | NewsChannel | ThreadChannel; export type AllGuildTextableChannels = TextGuildChannel | VoiceChannel | NewsChannel | ThreadChannel; +export type AllGuildVoiceChannels = VoiceChannel | StageChannel; export type AllChannels = | BaseChannel