diff --git a/README.md b/README.md index 767b21b..585d405 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,12 @@
- Lightweight and easy to use. -- Built-in Command Framework, - - Easily build Commands on the fly. - - Completely Customizable. - - Complete Object-Oriented approach. -- 100% Discord API Coverage. -- Customizable caching. - - Built in support for Redis. - - Write Custom Cache Adapters. -- Complete TypeScript support. +- Complete Object-Oriented approach. +- Slash Commands supported. +- Built-in Commands framework. +- Customizable Caching, with Redis support. +- Use `@decorators` to easily make things! +- Made with ❤️ TypeScript. ## Table of Contents @@ -102,13 +99,14 @@ client.connect('super secret token comes here', Intents.All) ``` Or with Decorators! + ```ts import { Client, event, Intents, command, - CommandContext, + CommandContext } from 'https://deno.land/x/harmony/mod.ts' class MyClient extends CommandClient { @@ -141,6 +139,7 @@ Documentation is available for `main` (branch) and `stable` (release). - [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmony-org/harmony/main/mod.ts) - [Stable](https://doc.deno.land/https/deno.land/x/harmony/mod.ts) +- [Guide](https://harmony-org.github.io) ## Found a bug or want support? Join our discord server! diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..3e1dd63 --- /dev/null +++ b/deps.ts @@ -0,0 +1,13 @@ +export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts' +export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts' +export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts' +export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts' +export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts' +export type { + Redis, + RedisConnectOptions +} from 'https://deno.land/x/redis@v0.14.1/mod.ts' +export { + Manager, + Player +} from 'https://raw.githubusercontent.com/Lavaclient/lavadeno/master/mod.ts' diff --git a/mod.ts b/mod.ts index 2c2e779..55bee3e 100644 --- a/mod.ts +++ b/mod.ts @@ -1,5 +1,4 @@ export { GatewayIntents } from './src/types/gateway.ts' -export { default as EventEmitter } from 'https://deno.land/std@0.74.0/node/events.ts' export { Base } from './src/structures/base.ts' export { Gateway } from './src/gateway/index.ts' export type { ClientEvents } from './src/gateway/handlers/index.ts' @@ -66,7 +65,7 @@ export { ActivityTypes } from './src/structures/presence.ts' export { Role } from './src/structures/role.ts' -export { Snowflake } from './src/structures/snowflake.ts' +export { Snowflake } from './src/utils/snowflake.ts' export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts' export { MessageReaction } from './src/structures/messageReaction.ts' export { User } from './src/structures/user.ts' diff --git a/src/gateway/index.ts b/src/gateway/index.ts index a8349a4..d678336 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -1,4 +1,4 @@ -import { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts' +import { unzlib, EventEmitter } from '../../deps.ts' import { Client } from '../models/client.ts' import { DISCORD_GATEWAY_URL, @@ -18,7 +18,6 @@ import { GatewayCache } from '../managers/gatewayCache.ts' import { delay } from '../utils/delay.ts' import { VoiceChannel } from '../structures/guildVoiceChannel.ts' import { Guild } from '../structures/guild.ts' -import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' export interface RequestMembersOptions { limit?: number @@ -177,55 +176,61 @@ export class Gateway extends EventEmitter { } } - private async onclose(event: CloseEvent): Promise { - if (event.reason === RECONNECT_REASON) return - this.emit('close', event.code, event.reason) - this.debug(`Connection Closed with code: ${event.code}`) + private async onclose({ reason, code }: CloseEvent): Promise { + if (reason === RECONNECT_REASON) return + this.emit('close', code, reason) + this.debug(`Connection Closed with code: ${code}`) - if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { - this.debug('API has encountered Unknown Error. Reconnecting...') - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect() - } else if (event.code === GatewayCloseCodes.UNKNOWN_OPCODE) { - throw new Error("Unknown OP Code was sent. This shouldn't happen!") - } else if (event.code === GatewayCloseCodes.DECODE_ERROR) { - throw new Error("Invalid Payload was sent. This shouldn't happen!") - } else if (event.code === GatewayCloseCodes.NOT_AUTHENTICATED) { - throw new Error('Not Authorized: Payload was sent before Identifying.') - } else if (event.code === GatewayCloseCodes.AUTHENTICATION_FAILED) { - throw new Error('Invalid Token provided!') - } else if (event.code === GatewayCloseCodes.INVALID_SEQ) { - this.debug('Invalid Seq was sent. Reconnecting.') - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect() - } else if (event.code === GatewayCloseCodes.RATE_LIMITED) { - throw new Error("You're ratelimited. Calm down.") - } else if (event.code === GatewayCloseCodes.SESSION_TIMED_OUT) { - this.debug('Session Timeout. Reconnecting.') - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect(true) - } else if (event.code === GatewayCloseCodes.INVALID_SHARD) { - this.debug('Invalid Shard was sent. Reconnecting.') - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect() - } else if (event.code === GatewayCloseCodes.SHARDING_REQUIRED) { - throw new Error("Couldn't connect. Sharding is required!") - } else if (event.code === GatewayCloseCodes.INVALID_API_VERSION) { - throw new Error("Invalid API Version was used. This shouldn't happen!") - } else if (event.code === GatewayCloseCodes.INVALID_INTENTS) { - throw new Error('Invalid Intents') - } else if (event.code === GatewayCloseCodes.DISALLOWED_INTENTS) { - throw new Error("Given Intents aren't allowed") - } else { - this.debug( - 'Unknown Close code, probably connection error. Reconnecting in 5s.' - ) - if (this.timedIdentify !== null) { - clearTimeout(this.timedIdentify) - this.debug('Timed Identify found. Cleared timeout.') - } - await delay(5000) - await this.reconnect(true) + switch (code) { + case GatewayCloseCodes.UNKNOWN_ERROR: + this.debug('API has encountered Unknown Error. Reconnecting...') + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.reconnect() + break + case GatewayCloseCodes.UNKNOWN_OPCODE: + throw new Error("Unknown OP Code was sent. This shouldn't happen!") + case GatewayCloseCodes.DECODE_ERROR: + throw new Error("Invalid Payload was sent. This shouldn't happen!") + case GatewayCloseCodes.NOT_AUTHENTICATED: + throw new Error('Not Authorized: Payload was sent before Identifying.') + case GatewayCloseCodes.AUTHENTICATION_FAILED: + throw new Error('Invalid Token provided!') + case GatewayCloseCodes.INVALID_SEQ: + this.debug('Invalid Seq was sent. Reconnecting.') + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.reconnect() + break + case GatewayCloseCodes.RATE_LIMITED: + throw new Error("You're ratelimited. Calm down.") + case GatewayCloseCodes.SESSION_TIMED_OUT: + this.debug('Session Timeout. Reconnecting.') + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.reconnect(true) + break + case GatewayCloseCodes.INVALID_SHARD: + this.debug('Invalid Shard was sent. Reconnecting.') + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.reconnect() + break + case GatewayCloseCodes.SHARDING_REQUIRED: + throw new Error("Couldn't connect. Sharding is required!") + case GatewayCloseCodes.INVALID_API_VERSION: + throw new Error("Invalid API Version was used. This shouldn't happen!") + case GatewayCloseCodes.INVALID_INTENTS: + throw new Error('Invalid Intents') + case GatewayCloseCodes.DISALLOWED_INTENTS: + throw new Error("Given Intents aren't allowed") + default: + this.debug( + 'Unknown Close code, probably connection error. Reconnecting in 5s.' + ) + if (this.timedIdentify !== null) { + clearTimeout(this.timedIdentify) + this.debug('Timed Identify found. Cleared timeout.') + } + await delay(5000) + await this.reconnect(true) + break } } diff --git a/src/managers/guildEmojis.ts b/src/managers/guildEmojis.ts index 2a3aeb2..dde2225 100644 --- a/src/managers/guildEmojis.ts +++ b/src/managers/guildEmojis.ts @@ -6,7 +6,7 @@ import { EmojiPayload } from '../types/emoji.ts' import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts' import { BaseChildManager } from './baseChild.ts' import { EmojisManager } from './emojis.ts' -import { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts' +import { fetchAuto } from '../../deps.ts' export class GuildEmojisManager extends BaseChildManager { guild: Guild diff --git a/src/models/cacheAdapter.ts b/src/models/cacheAdapter.ts index fe9df03..41b7503 100644 --- a/src/models/cacheAdapter.ts +++ b/src/models/cacheAdapter.ts @@ -3,7 +3,7 @@ import { connect, Redis, RedisConnectOptions -} from 'https://denopkg.com/keroxp/deno-redis/mod.ts' +} from '../../deps.ts' /** * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. diff --git a/src/models/client.ts b/src/models/client.ts index 07955d5..f0bcc73 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -2,7 +2,7 @@ import { User } from '../structures/user.ts' import { GatewayIntents } from '../types/gateway.ts' import { Gateway } from '../gateway/index.ts' import { RESTManager } from './rest.ts' -import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' +import { EventEmitter } from '../../deps.ts' import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' import { UsersManager } from '../managers/users.ts' import { GuildManager } from '../managers/guilds.ts' @@ -247,6 +247,7 @@ export function event(name?: string) { } } +/** Decorator to create a Slash Command handler */ export function slash(name?: string, guild?: string) { return function (client: Client | SlashModule, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] @@ -262,6 +263,7 @@ export function slash(name?: string, guild?: string) { } } +/** Decorator to create a Sub-Slash Command handler */ export function subslash(parent: string, name?: string, guild?: string) { return function (client: Client | SlashModule, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] @@ -279,13 +281,14 @@ export function subslash(parent: string, name?: string, guild?: string) { } } +/** Decorator to create a Grouped Slash Command handler */ export function groupslash( parent: string, group: string, name?: string, guild?: string ) { - return function (client: Client | SlashModule, prop: string) { + return function (client: Client | SlashModule | SlashClient, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] const item = (client as { [name: string]: any })[prop] if (typeof item !== 'function') { @@ -303,6 +306,7 @@ export function groupslash( } } +/** Decorator to add a Slash Module to Client */ export function slashModule() { return function (client: Client, prop: string) { if (client._decoratedSlashModules === undefined) diff --git a/src/models/command.ts b/src/models/command.ts index 2cbc033..3293229 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -5,7 +5,7 @@ import { User } from '../structures/user.ts' import { Collection } from '../utils/collection.ts' import { CommandClient } from './commandClient.ts' import { Extension } from './extensions.ts' -import { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts' +import { parse } from '../../deps.ts' export interface CommandContext { /** The Client object */ diff --git a/src/models/rest.ts b/src/models/rest.ts index 0e80c39..29b708c 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -97,6 +97,7 @@ export interface RESTOptions { token?: string headers?: { [name: string]: string | undefined } canary?: boolean + version?: 6 | 7 | 8 } export class RESTManager { @@ -111,6 +112,7 @@ export class RESTManager { constructor(client?: RESTOptions) { this.client = client this.api = builder(this) + if (client?.version !== undefined) this.version = client.version // eslint-disable-next-line @typescript-eslint/no-floating-promises this.handleRateLimits() } @@ -408,6 +410,7 @@ export class RESTManager { const query = method === 'get' && body !== undefined ? Object.entries(body as any) + .filter(([k, v]) => v !== undefined) .map( ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent( diff --git a/src/models/shard.ts b/src/models/shard.ts index 52927cb..436d01d 100644 --- a/src/models/shard.ts +++ b/src/models/shard.ts @@ -1,6 +1,6 @@ import { Collection } from '../utils/collection.ts' import { Client, ClientOptions } from './client.ts' -import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' +import {EventEmitter} from '../../deps.ts' import { RESTManager } from './rest.ts' // import { GATEWAY_BOT } from '../types/endpoint.ts' // import { GatewayBotPayload } from '../types/gatewayBot.ts' diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 5058660..e029e49 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,20 +1,24 @@ import { Guild } from '../structures/guild.ts' import { Interaction } from '../structures/slash.ts' -import { - APPLICATION_COMMAND, - APPLICATION_COMMANDS, - APPLICATION_GUILD_COMMAND, - APPLICATION_GUILD_COMMANDS -} from '../types/endpoint.ts' import { InteractionType, + SlashCommandChoice, SlashCommandOption, + SlashCommandOptionType, SlashCommandPartial, SlashCommandPayload } from '../types/slash.ts' import { Collection } from '../utils/collection.ts' import { Client } from './client.ts' import { RESTManager } from './rest.ts' +import { SlashModule } from './slashModule.ts' +import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' +import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' +import { + Request as ORequest, + Response as OResponse +} from 'https://deno.land/x/opine@1.0.0/src/types.ts' +import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts' export class SlashCommand { slash: SlashCommandsManager @@ -41,6 +45,158 @@ export class SlashCommand { async edit(data: SlashCommandPartial): Promise { await this.slash.edit(this.id, data, this._guild) } + + /** Create a handler for this Slash Command */ + handle( + func: SlashCommandHandlerCallback, + options?: { parent?: string; group?: string } + ): SlashCommand { + this.slash.slash.handle({ + name: this.name, + parent: options?.parent, + group: options?.group, + guild: this._guild, + handler: func + }) + return this + } +} + +export interface CreateOptions { + name: string + description?: string + options?: Array + choices?: Array +} + +function createSlashOption( + type: SlashCommandOptionType, + data: CreateOptions +): SlashCommandOption { + return { + name: data.name, + type, + description: + type === 0 || type === 1 + ? undefined + : data.description ?? 'No description.', + options: data.options?.map((e) => + typeof e === 'function' ? e(SlashOption) : e + ), + choices: + data.choices === undefined + ? undefined + : data.choices.map((e) => + typeof e === 'string' ? { name: e, value: e } : e + ) + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class SlashOption { + static string(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.STRING, data) + } + + static bool(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.BOOLEAN, data) + } + + static subCommand(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data) + } + + static subCommandGroup(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data) + } + + static role(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.ROLE, data) + } + + static channel(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.CHANNEL, data) + } + + static user(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.USER, data) + } + + static number(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.INTEGER, data) + } +} + +export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption + +export type SlashBuilderOptionsData = + | Array + | { + [name: string]: + | { + description: string + type: SlashCommandOptionType + options?: SlashCommandOption[] + choices?: SlashCommandChoice[] + } + | SlashOptionCallable + } + +function buildOptionsArray( + options: SlashBuilderOptionsData +): SlashCommandOption[] { + return Array.isArray(options) + ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op)) + : Object.entries(options).map((entry) => + typeof entry[1] === 'function' + ? entry[1](SlashOption) + : Object.assign(entry[1], { name: entry[0] }) + ) +} + +export class SlashBuilder { + data: SlashCommandPartial + + constructor( + name?: string, + description?: string, + options?: SlashBuilderOptionsData + ) { + this.data = { + name: name ?? '', + description: description ?? 'No description.', + options: options === undefined ? [] : buildOptionsArray(options) + } + } + + name(name: string): SlashBuilder { + this.data.name = name + return this + } + + description(desc: string): SlashBuilder { + this.data.description = desc + return this + } + + option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { + if (this.data.options === undefined) this.data.options = [] + this.data.options.push( + typeof option === 'function' ? option(SlashOption) : option + ) + return this + } + + options(options: SlashBuilderOptionsData): SlashBuilder { + this.data.options = buildOptionsArray(options) + return this + } + + export(): SlashCommandPartial { + if (this.data.name === '') + throw new Error('Name was not provided in Slash Builder') + return this.data + } } export class SlashCommandsManager { @@ -58,9 +214,9 @@ export class SlashCommandsManager { async all(): Promise> { const col = new Collection() - const res = (await this.rest.get( - APPLICATION_COMMANDS(this.slash.getID()) - )) as SlashCommandPayload[] + const res = (await this.rest.api.applications[ + this.slash.getID() + ].commands.get()) as SlashCommandPayload[] if (!Array.isArray(res)) return col for (const raw of res) { @@ -77,12 +233,9 @@ export class SlashCommandsManager { ): Promise> { const col = new Collection() - const res = (await this.rest.get( - APPLICATION_GUILD_COMMANDS( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id - ) - )) as SlashCommandPayload[] + const res = (await this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands.get()) as SlashCommandPayload[] if (!Array.isArray(res)) return col for (const raw of res) { @@ -99,15 +252,14 @@ export class SlashCommandsManager { data: SlashCommandPartial, guild?: Guild | string ): Promise { - const payload = await this.rest.post( + const route = guild === undefined - ? APPLICATION_COMMANDS(this.slash.getID()) - : APPLICATION_GUILD_COMMANDS( - this.slash.getID(), + ? this.rest.api.applications[this.slash.getID()].commands + : this.rest.api.applications[this.slash.getID()].guilds[ typeof guild === 'string' ? guild : guild.id - ), - data - ) + ].commands + + const payload = await route.post(data) const cmd = new SlashCommand(this, payload) cmd._guild = @@ -122,16 +274,14 @@ export class SlashCommandsManager { data: SlashCommandPartial, guild?: Guild | string ): Promise { - await this.rest.patch( + const route = guild === undefined - ? APPLICATION_COMMAND(this.slash.getID(), id) - : APPLICATION_GUILD_COMMAND( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id, - id - ), - data - ) + ? this.rest.api.applications[this.slash.getID()].commands[id] + : this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands[id] + + await route.patch(data) return this } @@ -140,29 +290,28 @@ export class SlashCommandsManager { id: string, guild?: Guild | string ): Promise { - await this.rest.delete( + const route = guild === undefined - ? APPLICATION_COMMAND(this.slash.getID(), id) - : APPLICATION_GUILD_COMMAND( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id, - id - ) - ) + ? this.rest.api.applications[this.slash.getID()].commands[id] + : this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands[id] + + await route.delete() return this } /** Get a Slash Command (global or Guild) */ async get(id: string, guild?: Guild | string): Promise { - const data = await this.rest.get( + const route = guild === undefined - ? APPLICATION_COMMAND(this.slash.getID(), id) - : APPLICATION_GUILD_COMMAND( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id, - id - ) - ) + ? this.rest.api.applications[this.slash.getID()].commands[id] + : this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands[id] + + const data = await route.get() + return new SlashCommand(this, data) } } @@ -182,6 +331,7 @@ export interface SlashOptions { enabled?: boolean token?: string rest?: RESTManager + publicKey?: string } export class SlashClient { @@ -192,6 +342,18 @@ export class SlashClient { commands: SlashCommandsManager handlers: SlashCommandHandler[] = [] rest: RESTManager + modules: SlashModule[] = [] + publicKey?: string + + _decoratedSlash?: Array<{ + name: string + guild?: string + parent?: string + group?: string + handler: (interaction: Interaction) => any + }> + + _decoratedSlashModules?: SlashModule[] constructor(options: SlashOptions) { let id = options.id @@ -202,6 +364,7 @@ export class SlashClient { this.client = options.client this.token = options.token this.commands = new SlashCommandsManager(this) + this.publicKey = options.publicKey if (options !== undefined) { this.enabled = options.enabled ?? true @@ -213,6 +376,24 @@ export class SlashClient { }) } + if (this.client?._decoratedSlashModules !== undefined) { + this.client._decoratedSlashModules.forEach((e) => { + this.modules.push(e) + }) + } + + if (this._decoratedSlash !== undefined) { + this._decoratedSlash.forEach((e) => { + this.handlers.push(e) + }) + } + + if (this._decoratedSlashModules !== undefined) { + this._decoratedSlashModules.forEach((e) => { + this.modules.push(e) + }) + } + this.rest = options.client === undefined ? options.rest === undefined @@ -237,8 +418,28 @@ export class SlashClient { return this } + loadModule(module: SlashModule): SlashClient { + this.modules.push(module) + return this + } + + getHandlers(): SlashCommandHandler[] { + let res = this.handlers + for (const mod of this.modules) { + if (mod === undefined) continue + res = [ + ...res, + ...mod.commands.map((cmd) => { + cmd.handler = cmd.handler.bind(mod) + return cmd + }) + ] + } + return res + } + private _getCommand(i: Interaction): SlashCommandHandler | undefined { - return this.handlers.find((e) => { + return this.getHandlers().find((e) => { const hasGroupOrParent = e.group !== undefined || e.parent !== undefined const groupMatched = e.group !== undefined && e.parent !== undefined @@ -271,4 +472,78 @@ export class SlashClient { cmd.handler(interaction) } + + async verifyKey( + rawBody: string | Uint8Array | Buffer, + signature: string, + timestamp: string + ): Promise { + if (this.publicKey === undefined) + throw new Error('Public Key is not present') + return edverify( + signature, + Buffer.concat([ + Buffer.from(timestamp, 'utf-8'), + Buffer.from( + rawBody instanceof Uint8Array + ? new TextDecoder().decode(rawBody) + : rawBody + ) + ]), + this.publicKey + ).catch(() => false) + } + + async verifyOpineRequest(req: ORequest): Promise { + const signature = req.headers.get('x-signature-ed25519') + const timestamp = req.headers.get('x-signature-timestamp') + const contentLength = req.headers.get('content-length') + + if (signature === null || timestamp === null || contentLength === null) + return false + + const body = new Uint8Array(parseInt(contentLength)) + await req.body.read(body) + + const verified = await this.verifyKey(body, signature, timestamp) + if (!verified) return false + + return true + } + + /** Middleware to verify request in Opine framework. */ + async verifyOpineMiddleware( + req: ORequest, + res: OResponse, + next: CallableFunction + ): Promise { + const verified = await this.verifyOpineRequest(req) + if (!verified) return res.setStatus(401).end() + + await next() + return true + } + + // TODO: create verifyOakMiddleware too + /** Method to verify Request from Oak server "Context". */ + async verifyOakRequest(ctx: Context): Promise { + const signature = ctx.request.headers.get('x-signature-ed25519') + const timestamp = ctx.request.headers.get('x-signature-timestamp') + const contentLength = ctx.request.headers.get('content-length') + + if ( + signature === null || + timestamp === null || + contentLength === null || + ctx.request.hasBody !== true + ) { + return false + } + + const body = await ctx.request.body().value + + const verified = await this.verifyKey(body as any, signature, timestamp) + if (!verified) return false + return true + } } diff --git a/src/structures/invite.ts b/src/structures/invite.ts index 59d172d..f27f841 100644 --- a/src/structures/invite.ts +++ b/src/structures/invite.ts @@ -1,5 +1,6 @@ import { Client } from '../models/client.ts' import { ChannelPayload } from '../types/channel.ts' +import { INVITE } from '../types/endpoint.ts' import { GuildPayload } from '../types/guild.ts' import { InvitePayload } from '../types/invite.ts' import { UserPayload } from '../types/user.ts' @@ -31,6 +32,12 @@ export class Invite extends Base { this.approximatePresenceCount = data.approximate_presence_count } + /** Delete an invite. Requires the MANAGE_CHANNELS permission on the channel this invite belongs to, or MANAGE_GUILD to remove any invite across the guild. Returns an invite object on success. Fires a Invite Delete Gateway event. */ + async delete(): Promise { + const res = await this.client.rest.delete(INVITE(this.code)) + return new Invite(this.client, res) + } + readFromData(data: InvitePayload): void { this.code = data.code ?? this.code this.guild = data.guild ?? this.guild diff --git a/src/structures/message.ts b/src/structures/message.ts index 553f91e..920994a 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -47,6 +47,10 @@ export class Message extends Base { flags?: number stickers?: MessageSticker[] + get createdAt(): Date { + return new Date(this.timestamp) + } + constructor( client: Client, data: MessagePayload, diff --git a/src/structures/template.ts b/src/structures/template.ts new file mode 100644 index 0000000..b137075 --- /dev/null +++ b/src/structures/template.ts @@ -0,0 +1,70 @@ +import { Client } from '../models/client.ts' +import { TEMPLATE } from '../types/endpoint.ts' +import { TemplatePayload } from '../types/template.ts' +import { Base } from './base.ts' +import { Guild } from './guild.ts' +import { User } from './user.ts' + +export class Template extends Base { + /** The template code (unique ID) */ + code: string + /** The template name */ + name: string + /** The description for the template */ + description: string | null + /** Number of times this template has been used */ + usageCount: number + /** The ID of the user who created the template */ + creatorID: string + /** The user who created the template */ + creator: User + /** When this template was created (in ms) */ + createdAt: number + /** When this template was last synced to the source guild (in ms) */ + updatedAt: number + /** The ID of the guild this template is based on */ + sourceGuildID: string + /** The guild snapshot this template contains */ + serializedSourceGuild: Guild + /** Whether the template has unsynced changes */ + isDirty: boolean | null + + constructor(client: Client, data: TemplatePayload) { + super(client, data) + this.code = data.code + this.name = data.name + this.description = data.description + this.usageCount = data.usage_count + this.creatorID = data.creator_id + this.creator = new User(client, data.creator) + this.createdAt = Date.parse(data.created_at) + this.updatedAt = Date.parse(data.updated_at) + this.sourceGuildID = data.source_guild_id + this.serializedSourceGuild = new Guild(client, data.serialized_source_guild) + this.isDirty = Boolean(data.is_dirty) + } + + /** Modifies the template's metadata. Requires the MANAGE_GUILD permission. Returns the template object on success. */ + async edit(data: ModifyGuildTemplateParams): Promise