From 086e4a95c34c09b1cb7e60ff5ee2d764c0ef6f9a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 10 Feb 2021 17:59:21 +0530 Subject: [PATCH 01/72] lot of stuff --- deps.ts | 2 + src/gateway/handlers/interactionCreate.ts | 30 ++++- src/gateway/index.ts | 2 +- src/models/command.ts | 92 ++++++++++++- src/models/slashClient.ts | 26 +++- src/structures/slash.ts | 152 +++++++++++++++++----- src/types/channel.ts | 19 ++- src/types/slash.ts | 51 +++++--- 8 files changed, 302 insertions(+), 72 deletions(-) diff --git a/deps.ts b/deps.ts index 7a04e52..6656d75 100644 --- a/deps.ts +++ b/deps.ts @@ -7,3 +7,5 @@ export type { Redis, RedisConnectOptions } from 'https://deno.land/x/redis@v0.14.1/mod.ts' +export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts' +export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index fac882a..a27fa81 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -8,13 +8,30 @@ export const interactionCreate: GatewayEventHandler = async ( gateway: Gateway, d: InteractionPayload ) => { - const guild = await gateway.client.guilds.get(d.guild_id) + // NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction. + // This case can be seen in future proofing Interactions, and one he mentioned was + // that bots will be able to add custom context menus. In that case, Interaction will not have it. + // Ref: https://github.com/discord/discord-api-docs/pull/2568/files#r569025697 + if (d.channel_id === undefined) return + + const guild = + d.guild_id === undefined + ? undefined + : await gateway.client.guilds.get(d.guild_id) if (guild === undefined) return - await guild.members.set(d.member.user.id, d.member) - const member = ((await guild.members.get( - d.member.user.id - )) as unknown) as Member + if (d.member !== undefined) + await guild.members.set(d.member.user.id, d.member) + const member = + d.member !== undefined + ? (((await guild.members.get(d.member.user.id)) as unknown) as Member) + : undefined + if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user) + const dmUser = + d.user !== undefined ? await gateway.client.users.get(d.user.id) : undefined + + const user = member !== undefined ? member.user : dmUser + if (user === undefined) return const channel = (await gateway.client.channels.get(d.channel_id)) ?? @@ -23,7 +40,8 @@ export const interactionCreate: GatewayEventHandler = async ( const interaction = new Interaction(gateway.client, d, { member, guild, - channel + channel, + user }) gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 955b61d..c6dbc0a 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -157,7 +157,7 @@ export class Gateway extends HarmonyEventEmitter { const handler = gatewayHandlers[t] - if (handler !== undefined) { + if (handler !== undefined && d !== null) { handler(this, d) } } diff --git a/src/models/command.ts b/src/models/command.ts index cca21c1..05e59ff 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 '../../deps.ts' +import { join, parse, walk } from '../../deps.ts' export interface CommandContext { /** The Client object */ @@ -284,13 +284,103 @@ export class CommandBuilder extends Command { } } +export class CommandsLoader { + client: CommandClient + #importSeq: { [name: string]: number } = {} + + constructor(client: CommandClient) { + this.client = client + } + + /** + * Load a Command from file. + * + * @param filePath Path of Command file. + * @param exportName Export name. Default is the "default" export. + */ + async load( + filePath: string, + exportName: string = 'default', + onlyRead?: boolean + ): Promise { + const stat = await Deno.stat(filePath).catch(() => undefined) + if (stat === undefined || stat.isFile !== true) + throw new Error(`File not found on path ${filePath}`) + + let seq: number | undefined + + if (this.#importSeq[filePath] !== undefined) seq = this.#importSeq[filePath] + const mod = await import( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + 'file:///' + + join(Deno.cwd(), filePath) + + (seq === undefined ? '' : `#${seq}`) + ) + if (this.#importSeq[filePath] === undefined) this.#importSeq[filePath] = 0 + else this.#importSeq[filePath]++ + + const Cmd = mod[exportName] + if (Cmd === undefined) + throw new Error(`Command not exported as ${exportName} from ${filePath}`) + + let cmd: Command + try { + if (Cmd instanceof Command) cmd = Cmd + else cmd = new Cmd() + if (!(cmd instanceof Command)) throw new Error('failed') + } catch (e) { + throw new Error(`Failed to load Command from ${filePath}`) + } + + if (onlyRead !== true) this.client.commands.add(cmd) + return cmd + } + + /** + * Load commands from a Directory. + * + * @param path Path of the directory. + * @param options Options to configure loading. + */ + async loadDirectory( + path: string, + options?: { + recursive?: boolean + exportName?: string + maxDepth?: number + exts?: string[] + onlyRead?: boolean + } + ): Promise { + const commands: Command[] = [] + + for await (const entry of walk(path, { + maxDepth: options?.maxDepth, + exts: options?.exts, + includeDirs: false + })) { + if (entry.isFile !== true) continue + const cmd = await this.load( + entry.path, + options?.exportName, + options?.onlyRead + ) + commands.push(cmd) + } + + return commands + } +} + export class CommandsManager { client: CommandClient list: Collection = new Collection() disabled: Set = new Set() + loader: CommandsLoader constructor(client: CommandClient) { this.client = client + this.loader = new CommandsLoader(client) } /** Number of loaded Commands */ diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 3318e05..eac0486 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -155,6 +155,7 @@ function buildOptionsArray( ) } +/** Slash Command Builder */ export class SlashBuilder { data: SlashCommandPartial @@ -200,6 +201,7 @@ export class SlashBuilder { } } +/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ export class SlashCommandsManager { slash: SlashClient rest: RESTManager @@ -351,7 +353,7 @@ export class SlashCommandsManager { } } -export type SlashCommandHandlerCallback = (interaction: Interaction) => any +export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown export interface SlashCommandHandler { name: string guild?: string @@ -360,6 +362,7 @@ export interface SlashCommandHandler { handler: SlashCommandHandlerCallback } +/** Options for SlashClient */ export interface SlashOptions { id?: string | (() => string) client?: Client @@ -369,6 +372,7 @@ export interface SlashOptions { publicKey?: string } +/** Slash Client represents an Interactions Client which can be used without Harmony Client. */ export class SlashClient { id: string | (() => string) client?: Client @@ -469,12 +473,20 @@ export class SlashClient { const groupMatched = e.group !== undefined && e.parent !== undefined ? i.options - .find((o) => o.name === e.group) + .find( + (o) => + o.name === e.group && + o.type === SlashCommandOptionType.SUB_COMMAND_GROUP + ) ?.options?.find((o) => o.name === e.name) !== undefined : true const subMatched = e.group === undefined && e.parent !== undefined - ? i.options.find((o) => o.name === e.name) !== undefined + ? i.options.find( + (o) => + o.name === e.name && + o.type === SlashCommandOptionType.SUB_COMMAND + ) !== undefined : true const nameMatched1 = e.name === i.name const parentMatched = hasGroupOrParent ? e.parent === i.name : true @@ -485,11 +497,15 @@ export class SlashClient { }) } - /** Process an incoming Slash Command (interaction) */ + /** Process an incoming Interaction */ private _process(interaction: Interaction): void { if (!this.enabled) return - if (interaction.type !== InteractionType.APPLICATION_COMMAND) return + if ( + interaction.type !== InteractionType.APPLICATION_COMMAND || + interaction.data === undefined + ) + return const cmd = this._getCommand(interaction) if (cmd?.group !== undefined) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 388aa56..0df43af 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,12 +1,18 @@ import { Client } from '../models/client.ts' -import { MessageOptions } from '../types/channel.ts' +import { + AllowedMentionsPayload, + EmbedPayload, + MessageOptions +} from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { - InteractionData, - InteractionOption, + InteractionApplicationCommandData, + InteractionApplicationCommandOption, InteractionPayload, + InteractionResponseFlags, InteractionResponsePayload, - InteractionResponseType + InteractionResponseType, + InteractionType } from '../types/slash.ts' import { SnowflakeBase } from './base.ts' import { Embed } from './embed.ts' @@ -15,7 +21,6 @@ import { Member } from './member.ts' import { Message } from './message.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts' import { User } from './user.ts' -import { Webhook } from './webhook.ts' interface WebhookMessageOptions extends MessageOptions { embeds?: Embed[] @@ -25,39 +30,57 @@ interface WebhookMessageOptions extends MessageOptions { type AllWebhookMessageOptions = string | WebhookMessageOptions -export interface InteractionResponse { - type?: InteractionResponseType +/** Interaction Message related Options */ +export interface InteractionMessageOptions { content?: string - embeds?: Embed[] + embeds?: EmbedPayload[] tts?: boolean - flags?: number + flags?: number | InteractionResponseFlags[] + allowedMentions?: AllowedMentionsPayload + /** Whether to reply with Source or not. True by default */ + withSource?: boolean +} + +export interface InteractionResponse extends InteractionMessageOptions { + /** Type of Interaction Response */ + type?: InteractionResponseType + /** + * DEPRECATED: Use `ephemeral` instead. + * + * @deprecated + */ temp?: boolean - allowedMentions?: { - parse?: string - roles?: string[] - users?: string[] - everyone?: boolean - } + /** Whether the Message Response should be Ephemeral (only visible to User) or not */ + ephemeral?: boolean } export class Interaction extends SnowflakeBase { client: Client - type: number + /** Type of Interaction */ + type: InteractionType + /** Interaction Token */ token: string + /** Interaction ID */ id: string - data: InteractionData - channel: GuildTextChannel - guild: Guild - member: Member - _savedHook?: Webhook + /** Data sent with Interaction. Only applies to Application Command, type may change in future. */ + data?: InteractionApplicationCommandData + /** Channel in which Interaction was initiated */ + channel?: TextChannel | GuildTextChannel + guild?: Guild + /** Member object of who initiated the Interaction */ + member?: Member + /** User object of who invoked Interaction */ + user: User + responded: boolean = false constructor( client: Client, data: InteractionPayload, others: { - channel: GuildTextChannel - guild: Guild - member: Member + channel?: TextChannel | GuildTextChannel + guild?: Guild + member?: Member + user: User } ) { super(client) @@ -66,31 +89,43 @@ export class Interaction extends SnowflakeBase { this.token = data.token this.member = others.member this.id = data.id + this.user = others.user this.data = data.data this.guild = others.guild this.channel = others.channel } - get user(): User { - return this.member.user + /** Name of the Command Used (may change with future additions to Interactions!) */ + get name(): string | undefined { + return this.data?.name } - get name(): string { - return this.data.name - } - - get options(): InteractionOption[] { - return this.data.options ?? [] + get options(): InteractionApplicationCommandOption[] { + return this.data?.options ?? [] } + /** Get an option by name */ option(name: string): T { return this.options.find((e) => e.name === name)?.value } /** Respond to an Interaction */ async respond(data: InteractionResponse): Promise { + if (this.responded) throw new Error('Already responded to Interaction') + let flags = 0 + if (data.ephemeral === true || data.temp === true) + flags |= InteractionResponseFlags.EPHEMERAL + if (data.flags !== undefined) { + if (Array.isArray(data.flags)) + flags = data.flags.reduce((p, a) => p | a, flags) + else if (typeof data.flags === 'number') flags |= data.flags + } const payload: InteractionResponsePayload = { - type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + type: + data.type ?? + (data.withSource === false + ? InteractionResponseType.CHANNEL_MESSAGE + : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE), data: data.type === undefined || data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || @@ -99,8 +134,8 @@ export class Interaction extends SnowflakeBase { content: data.content ?? '', embeds: data.embeds, tts: data.tts ?? false, - flags: data.temp === true ? 64 : data.flags ?? undefined, - allowed_mentions: (data.allowedMentions ?? undefined) as any + flags, + allowed_mentions: data.allowedMentions ?? undefined } : undefined } @@ -109,6 +144,52 @@ export class Interaction extends SnowflakeBase { INTERACTION_CALLBACK(this.id, this.token), payload ) + this.responded = true + + return this + } + + /** Acknowledge the Interaction */ + async acknowledge(withSource?: boolean): Promise { + await this.respond({ + type: + withSource === true + ? InteractionResponseType.ACK_WITH_SOURCE + : InteractionResponseType.ACKNOWLEDGE + }) + return this + } + + /** Reply with a Message to the Interaction */ + async reply(content: string): Promise + async reply(options: InteractionMessageOptions): Promise + async reply( + content: string, + options: InteractionMessageOptions + ): Promise + async reply( + content: string | InteractionMessageOptions, + messageOptions?: InteractionMessageOptions + ): Promise { + let options: InteractionMessageOptions | undefined = + typeof content === 'object' ? content : messageOptions + if ( + typeof content === 'object' && + messageOptions !== undefined && + options !== undefined + ) + Object.assign(options, messageOptions) + if (options === undefined) options = {} + if (typeof content === 'string') Object.assign(options, { content }) + + await this.respond( + Object.assign(options, { + type: + options.withSource === false + ? InteractionResponseType.CHANNEL_MESSAGE + : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + }) + ) return this } @@ -232,6 +313,7 @@ export class Interaction extends SnowflakeBase { return this } + /** Delete a follow-up Message */ async deleteMessage(msg: Message | string): Promise { await this.client.rest.delete( WEBHOOK_MESSAGE( diff --git a/src/types/channel.ts b/src/types/channel.ts index e1d44e0..10c5ea2 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -156,16 +156,25 @@ export interface MessagePayload { stickers?: MessageStickerPayload[] } +export enum AllowedMentionType { + Roles = 'roles', + Users = 'users', + Everyone = 'everyone' +} + +export interface AllowedMentionsPayload { + parse?: AllowedMentionType[] + users?: string[] + roles?: string[] + replied_user?: boolean +} + export interface MessageOptions { tts?: boolean embed?: Embed file?: MessageAttachment files?: MessageAttachment[] - allowedMentions?: { - parse?: 'everyone' | 'users' | 'roles' - roles?: string[] - users?: string[] - } + allowedMentions?: AllowedMentionsPayload } export interface ChannelMention { diff --git a/src/types/slash.ts b/src/types/slash.ts index 2dac0cd..692611c 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,22 +1,25 @@ -import { EmbedPayload } from './channel.ts' +import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' import { MemberPayload } from './guild.ts' +import { UserPayload } from './user.ts' -export interface InteractionOption { +export interface InteractionApplicationCommandOption { /** Option name */ name: string + /** Type of Option */ + type: SlashCommandOptionType /** Value of the option */ value?: any /** Sub options */ - options?: any[] + options?: InteractionApplicationCommandOption[] } -export interface InteractionData { +export interface InteractionApplicationCommandData { /** Name of the Slash Command */ name: string /** Unique ID of the Slash Command */ id: string /** Options (arguments) sent with Interaction */ - options: InteractionOption[] + options: InteractionApplicationCommandOption[] } export enum InteractionType { @@ -26,27 +29,31 @@ export enum InteractionType { APPLICATION_COMMAND = 2 } +export interface InteractionMemberPayload extends MemberPayload { + permissions: string +} + export interface InteractionPayload { /** Type of the Interaction */ type: InteractionType /** Token of the Interaction to respond */ token: string /** Member object of user who invoked */ - member: MemberPayload & { - /** Total permissions of the member in the channel, including overrides */ - permissions: string - } + member?: InteractionMemberPayload + /** User who initiated Interaction (only in DMs) */ + user?: UserPayload /** ID of the Interaction */ id: string /** * Data sent with the interaction - * **This can be undefined only when Interaction is not a Slash Command** + * + * This can be undefined only when Interaction is not a Slash Command. */ - data: InteractionData + data?: InteractionApplicationCommandData /** ID of the Guild in which Interaction was invoked */ - guild_id: string + guild_id?: string /** ID of the Channel in which Interaction was invoked */ - channel_id: string + channel_id?: string } export interface SlashCommandChoice { @@ -68,13 +75,18 @@ export enum SlashCommandOptionType { } export interface SlashCommandOption { + /** Name of the option. */ name: string - /** Description not required in Sub-Command or Sub-Command-Group */ + /** Description of the Option. Not required in Sub-Command-Group */ description?: string + /** Option type */ type: SlashCommandOptionType + /** Whether the option is required or not, false by default */ required?: boolean default?: boolean + /** Optional choices out of which User can choose value */ choices?: SlashCommandChoice[] + /** Nested options for Sub-Command or Sub-Command-Groups */ options?: SlashCommandOption[] } @@ -103,19 +115,20 @@ export enum InteractionResponseType { } export interface InteractionResponsePayload { + /** Type of the response */ type: InteractionResponseType + /** Data to be sent with response. Optional for types: Pong, Acknowledge, Ack with Source */ data?: InteractionResponseDataPayload } export interface InteractionResponseDataPayload { tts?: boolean + /** Text content of the Response (Message) */ content: string + /** Upto 10 Embed Objects to send with Response */ embeds?: EmbedPayload[] - allowed_mentions?: { - parse?: 'everyone' | 'users' | 'roles' - roles?: string[] - users?: string[] - } + /** Allowed Mentions object */ + allowed_mentions?: AllowedMentionsPayload flags?: number } From a800f394ac5e5dc848be0d28502b9191e8e75d17 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 12 Feb 2021 17:07:38 +0530 Subject: [PATCH 02/72] new things --- src/gateway/handlers/interactionCreate.ts | 73 +++++++++++- src/gateway/index.ts | 19 ++-- src/models/shard.ts | 2 - src/structures/base.ts | 4 +- src/structures/slash.ts | 56 +++++++++- src/test/slash.ts | 130 +++++++--------------- src/types/slash.ts | 27 ++++- src/utils/bitfield.ts | 34 +++--- src/utils/permissions.ts | 4 +- 9 files changed, 219 insertions(+), 130 deletions(-) diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index a27fa81..4c8eb00 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -1,7 +1,16 @@ +import { Guild } from '../../structures/guild.ts' import { Member } from '../../structures/member.ts' -import { Interaction } from '../../structures/slash.ts' +import { Role } from '../../structures/role.ts' +import { + Interaction, + InteractionApplicationCommandResolved, + InteractionChannel +} from '../../structures/slash.ts' import { GuildTextChannel } from '../../structures/textChannel.ts' +import { User } from '../../structures/user.ts' import { InteractionPayload } from '../../types/slash.ts' +import { UserPayload } from '../../types/user.ts' +import { Permissions } from '../../utils/permissions.ts' import { Gateway, GatewayEventHandler } from '../index.ts' export const interactionCreate: GatewayEventHandler = async ( @@ -18,13 +27,12 @@ export const interactionCreate: GatewayEventHandler = async ( d.guild_id === undefined ? undefined : await gateway.client.guilds.get(d.guild_id) - if (guild === undefined) return if (d.member !== undefined) - await guild.members.set(d.member.user.id, d.member) + await guild?.members.set(d.member.user.id, d.member) const member = d.member !== undefined - ? (((await guild.members.get(d.member.user.id)) as unknown) as Member) + ? (((await guild?.members.get(d.member.user.id)) as unknown) as Member) : undefined if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user) const dmUser = @@ -37,11 +45,66 @@ export const interactionCreate: GatewayEventHandler = async ( (await gateway.client.channels.get(d.channel_id)) ?? (await gateway.client.channels.fetch(d.channel_id)) + const resolved: InteractionApplicationCommandResolved = { + users: {}, + channels: {}, + members: {}, + roles: {} + } + + if (d.data?.resolved !== undefined) { + for (const [id, data] of Object.entries(d.data.resolved.users ?? {})) { + await gateway.client.users.set(id, data) + resolved.users[id] = ((await gateway.client.users.get( + id + )) as unknown) as User + if (resolved.members[id] !== undefined) + resolved.users[id].member = resolved.members[id] + } + + for (const [id, data] of Object.entries(d.data.resolved.members ?? {})) { + const roles = await guild?.roles.array() + let permissions = new Permissions(Permissions.DEFAULT) + if (roles !== undefined) { + const mRoles = roles.filter( + (r) => (data?.roles?.includes(r.id) as boolean) || r.id === guild?.id + ) + permissions = new Permissions(mRoles.map((r) => r.permissions)) + } + data.user = (d.data.resolved.users?.[id] as unknown) as UserPayload + resolved.members[id] = new Member( + gateway.client, + data, + resolved.users[id], + guild as Guild, + permissions + ) + } + + for (const [id, data] of Object.entries(d.data.resolved.roles ?? {})) { + if (guild !== undefined) { + await guild.roles.set(id, data) + resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role + } else { + resolved.roles[id] = new Role( + gateway.client, + data, + (guild as unknown) as Guild + ) + } + } + + for (const [id, data] of Object.entries(d.data.resolved.channels ?? {})) { + resolved.channels[id] = new InteractionChannel(gateway.client, data) + } + } + const interaction = new Interaction(gateway.client, d, { member, guild, channel, - user + user, + resolved }) gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index c6dbc0a..d890457 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -7,7 +7,6 @@ import { import { GatewayResponse } from '../types/gatewayResponse.ts' import { GatewayOpcodes, - GatewayIntents, GatewayCloseCodes, IdentityPayload, StatusUpdatePayload, @@ -57,8 +56,6 @@ export type GatewayTypedEvents = { */ export class Gateway extends HarmonyEventEmitter { websocket?: WebSocket - token?: string - intents?: GatewayIntents[] connected = false initialized = false heartbeatInterval = 0 @@ -269,8 +266,9 @@ export class Gateway extends HarmonyEventEmitter { } private async sendIdentify(forceNewSession?: boolean): Promise { - if (typeof this.token !== 'string') throw new Error('Token not specified') - if (typeof this.intents !== 'object') + if (typeof this.client.token !== 'string') + throw new Error('Token not specified') + if (typeof this.client.intents !== 'object') throw new Error('Intents not specified') if (this.client.fetchGatewayInfo === true) { @@ -300,7 +298,7 @@ export class Gateway extends HarmonyEventEmitter { } const payload: IdentityPayload = { - token: this.token, + token: this.client.token, properties: { $os: this.client.clientProperties.os ?? Deno.build.os, $browser: this.client.clientProperties.browser ?? 'harmony', @@ -311,7 +309,7 @@ export class Gateway extends HarmonyEventEmitter { this.shards === undefined ? [0, 1] : [this.shards[0] ?? 0, this.shards[1] ?? 1], - intents: this.intents.reduce( + intents: this.client.intents.reduce( (previous, current) => previous | current, 0 ), @@ -327,9 +325,8 @@ export class Gateway extends HarmonyEventEmitter { } private async sendResume(): Promise { - if (typeof this.token !== 'string') throw new Error('Token not specified') - if (typeof this.intents !== 'object') - throw new Error('Intents not specified') + if (typeof this.client.token !== 'string') + throw new Error('Token not specified') if (this.sessionID === undefined) { this.sessionID = await this.cache.get( @@ -348,7 +345,7 @@ export class Gateway extends HarmonyEventEmitter { const resumePayload = { op: GatewayOpcodes.RESUME, d: { - token: this.token, + token: this.client.token, session_id: this.sessionID, seq: this.sequenceID ?? null } diff --git a/src/models/shard.ts b/src/models/shard.ts index 80cd649..762ce40 100644 --- a/src/models/shard.ts +++ b/src/models/shard.ts @@ -79,8 +79,6 @@ export class ShardManager extends HarmonyEventEmitter { const shardCount = await this.getShardCount() const gw = new Gateway(this.client, [Number(id), shardCount]) - gw.token = this.client.token - gw.intents = this.client.intents this.list.set(id.toString(), gw) gw.initWebsocket() diff --git a/src/structures/base.ts b/src/structures/base.ts index e5806e5..0898cfe 100644 --- a/src/structures/base.ts +++ b/src/structures/base.ts @@ -2,10 +2,10 @@ import { Client } from '../models/client.ts' import { Snowflake } from '../utils/snowflake.ts' export class Base { - client: Client + client!: Client constructor(client: Client, _data?: any) { - this.client = client + Object.defineProperty(this, 'client', { value: client, enumerable: false }) } } diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 0df43af..99a1308 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,24 +1,31 @@ import { Client } from '../models/client.ts' import { AllowedMentionsPayload, + ChannelTypes, EmbedPayload, MessageOptions } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { + Dict, InteractionApplicationCommandData, InteractionApplicationCommandOption, + InteractionChannelPayload, InteractionPayload, InteractionResponseFlags, InteractionResponsePayload, InteractionResponseType, - InteractionType + InteractionType, + SlashCommandOptionType } from '../types/slash.ts' +import { Permissions } from '../utils/permissions.ts' import { SnowflakeBase } from './base.ts' +import { Channel } from './channel.ts' import { Embed } from './embed.ts' import { Guild } from './guild.ts' import { Member } from './member.ts' import { Message } from './message.ts' +import { Role } from './role.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts' import { User } from './user.ts' @@ -54,8 +61,37 @@ export interface InteractionResponse extends InteractionMessageOptions { ephemeral?: boolean } +/** Represents a Channel Object for an Option in Slash Command */ +export class InteractionChannel extends SnowflakeBase { + name: string + type: ChannelTypes + permissions: Permissions + + constructor(client: Client, data: InteractionChannelPayload) { + super(client) + this.name = data.name + this.type = data.type + this.permissions = new Permissions(data.permissions) + } + + /** Resolve to actual Channel object if present in Cache */ + async resolve(): Promise { + return this.client.channels.get(this.id) + } +} + +export interface InteractionApplicationCommandResolved { + users: Dict + members: Dict + channels: Dict + roles: Dict +} + +export class InteractionUser extends User { + member?: Member +} + export class Interaction extends SnowflakeBase { - client: Client /** Type of Interaction */ type: InteractionType /** Interaction Token */ @@ -72,6 +108,7 @@ export class Interaction extends SnowflakeBase { /** User object of who invoked Interaction */ user: User responded: boolean = false + resolved: InteractionApplicationCommandResolved constructor( client: Client, @@ -81,10 +118,10 @@ export class Interaction extends SnowflakeBase { guild?: Guild member?: Member user: User + resolved: InteractionApplicationCommandResolved } ) { super(client) - this.client = client this.type = data.type this.token = data.token this.member = others.member @@ -93,6 +130,7 @@ export class Interaction extends SnowflakeBase { this.data = data.data this.guild = others.guild this.channel = others.channel + this.resolved = others.resolved } /** Name of the Command Used (may change with future additions to Interactions!) */ @@ -105,8 +143,16 @@ export class Interaction extends SnowflakeBase { } /** Get an option by name */ - option(name: string): T { - return this.options.find((e) => e.name === name)?.value + option(name: string): T { + const op = this.options.find((e) => e.name === name) + if (op === undefined || op.value === undefined) return undefined as any + if (op.type === SlashCommandOptionType.USER) + return this.resolved.users[op.value] as any + else if (op.type === SlashCommandOptionType.ROLE) + return this.resolved.roles[op.value] as any + else if (op.type === SlashCommandOptionType.CHANNEL) + return this.resolved.channels[op.value] as any + else return op.value } /** Respond to an Interaction */ diff --git a/src/test/slash.ts b/src/test/slash.ts index 155a745..215cad0 100644 --- a/src/test/slash.ts +++ b/src/test/slash.ts @@ -1,100 +1,56 @@ -import { Client, Intents, event, slash } from '../../mod.ts' -import { Embed } from '../structures/embed.ts' +import { + Client, + Intents, + event, + slash, + SlashCommandOptionType as Type +} from '../../mod.ts' import { Interaction } from '../structures/slash.ts' import { TOKEN } from './config.ts' export class MyClient extends Client { - @event() - ready(): void { + @event() ready(): void { console.log(`Logged in as ${this.user?.tag}!`) - this.slash.commands.bulkEdit([{ name: 'send', description: 'idk' }]) - } - - @event('debug') - debugEvt(txt: string): void { - console.log(txt) - } - - @slash() - send(d: Interaction): void { - d.respond({ - content: d.data.options?.find((e) => e.name === 'content')?.value - }) - } - - @slash() - async eval(d: Interaction): Promise { - if ( - d.user.id !== '422957901716652033' && - d.user.id !== '682849186227552266' - ) { - d.respond({ - content: 'This command can only be used by owner!' - }) - } else { - const code = d.data.options?.find((e) => e.name === 'code') - ?.value as string - try { - // eslint-disable-next-line no-eval - let evaled = eval(code) - if (evaled instanceof Promise) evaled = await evaled - if (typeof evaled === 'object') evaled = Deno.inspect(evaled) - let res = `${evaled}`.substring(0, 1990) - while (client.token !== undefined && res.includes(client.token)) { - res = res.replace(client.token, '[REMOVED]') + this.slash.commands.bulkEdit( + [ + { + name: 'test', + description: 'Test command.', + options: [ + { + name: 'user', + type: Type.USER, + description: 'User' + }, + { + name: 'role', + type: Type.ROLE, + description: 'Role' + }, + { + name: 'channel', + type: Type.CHANNEL, + description: 'Channel' + }, + { + name: 'string', + type: Type.STRING, + description: 'String' + } + ] } - d.respond({ - content: '```js\n' + `${res}` + '\n```' - }).catch(() => {}) - } catch (e) { - d.respond({ - content: '```js\n' + `${e.stack}` + '\n```' - }) - } - } + ], + '807935370556866560' + ) + this.slash.commands.bulkEdit([]) } - @slash() - async hug(d: Interaction): Promise { - const id = d.data.options?.find((e) => e.name === 'user')?.value as string - const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) - const url = await fetch('https://nekos.life/api/v2/img/hug') - .then((r) => r.json()) - .then((e) => e.url) - - d.respond({ - embeds: [ - new Embed() - .setTitle(`${d.user.username} hugged ${user?.username}!`) - .setImage({ url }) - .setColor(0x2f3136) - ] - }) + @slash() test(d: Interaction): void { + console.log(d.resolved) } - @slash() - async kiss(d: Interaction): Promise { - const id = d.data.options?.find((e) => e.name === 'user')?.value as string - const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) - const url = await fetch('https://nekos.life/api/v2/img/kiss') - .then((r) => r.json()) - .then((e) => e.url) - - d.respond({ - embeds: [ - new Embed() - .setTitle(`${d.user.username} kissed ${user?.username}!`) - .setImage({ url }) - .setColor(0x2f3136) - ] - }) - } - - @slash('ping') - pingCmd(d: Interaction): void { - d.respond({ - content: `Pong!` - }) + @event() raw(evt: string, d: any): void { + if (evt === 'INTERACTION_CREATE') console.log(evt, d?.data?.resolved) } } diff --git a/src/types/slash.ts b/src/types/slash.ts index 692611c..69eea51 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,5 +1,10 @@ -import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' +import { + AllowedMentionsPayload, + ChannelTypes, + EmbedPayload +} from './channel.ts' import { MemberPayload } from './guild.ts' +import { RolePayload } from './role.ts' import { UserPayload } from './user.ts' export interface InteractionApplicationCommandOption { @@ -13,6 +18,24 @@ export interface InteractionApplicationCommandOption { options?: InteractionApplicationCommandOption[] } +export interface InteractionChannelPayload { + id: string + name: string + permissions: string + type: ChannelTypes +} + +export interface Dict { + [name: string]: T +} + +export interface InteractionApplicationCommandResolvedPayload { + users?: Dict + members?: Dict + channels?: Dict + roles?: Dict +} + export interface InteractionApplicationCommandData { /** Name of the Slash Command */ name: string @@ -20,6 +43,8 @@ export interface InteractionApplicationCommandData { id: string /** Options (arguments) sent with Interaction */ options: InteractionApplicationCommandOption[] + /** Resolved data for options in Slash Command */ + resolved?: InteractionApplicationCommandResolvedPayload } export enum InteractionType { diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts index eb7a67c..d2f5f03 100644 --- a/src/utils/bitfield.ts +++ b/src/utils/bitfield.ts @@ -9,31 +9,31 @@ export type BitFieldResolvable = /** Bit Field utility to work with Bits and Flags */ export class BitField { - flags: { [name: string]: number } = {} + #flags: { [name: string]: number } = {} bitfield: number constructor(flags: { [name: string]: number }, bits: any) { - this.flags = flags - this.bitfield = BitField.resolve(this.flags, bits) + this.#flags = flags + this.bitfield = BitField.resolve(this.#flags, bits) } any(bit: BitFieldResolvable): boolean { - return (this.bitfield & BitField.resolve(this.flags, bit)) !== 0 + return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0 } equals(bit: BitFieldResolvable): boolean { - return this.bitfield === BitField.resolve(this.flags, bit) + return this.bitfield === BitField.resolve(this.#flags, bit) } has(bit: BitFieldResolvable, ...args: any[]): boolean { if (Array.isArray(bit)) return (bit.every as any)((p: any) => this.has(p)) - bit = BitField.resolve(this.flags, bit) + bit = BitField.resolve(this.#flags, bit) return (this.bitfield & bit) === bit } missing(bits: any, ...hasParams: any[]): string[] { if (!Array.isArray(bits)) - bits = new BitField(this.flags, bits).toArray(false) + bits = new BitField(this.#flags, bits).toArray(false) return bits.filter((p: any) => !this.has(p, ...hasParams)) } @@ -44,10 +44,10 @@ export class BitField { add(...bits: BitFieldResolvable[]): BitField { let total = 0 for (const bit of bits) { - total |= BitField.resolve(this.flags, bit) + total |= BitField.resolve(this.#flags, bit) } if (Object.isFrozen(this)) - return new BitField(this.flags, this.bitfield | total) + return new BitField(this.#flags, this.bitfield | total) this.bitfield |= total return this } @@ -55,27 +55,31 @@ export class BitField { remove(...bits: BitFieldResolvable[]): BitField { let total = 0 for (const bit of bits) { - total |= BitField.resolve(this.flags, bit) + total |= BitField.resolve(this.#flags, bit) } if (Object.isFrozen(this)) - return new BitField(this.flags, this.bitfield & ~total) + return new BitField(this.#flags, this.bitfield & ~total) this.bitfield &= ~total return this } + flags(): { [name: string]: number } { + return this.#flags + } + serialize(...hasParams: any[]): { [key: string]: any } { const serialized: { [key: string]: any } = {} - for (const [flag, bit] of Object.entries(this.flags)) + for (const [flag, bit] of Object.entries(this.#flags)) serialized[flag] = this.has( - BitField.resolve(this.flags, bit), + BitField.resolve(this.#flags, bit), ...hasParams ) return serialized } toArray(...hasParams: any[]): string[] { - return Object.keys(this.flags).filter((bit) => - this.has(BitField.resolve(this.flags, bit), ...hasParams) + return Object.keys(this.#flags).filter((bit) => + this.has(BitField.resolve(this.#flags, bit), ...hasParams) ) } diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 03b4b82..99b258e 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -21,14 +21,14 @@ export class Permissions extends BitField { any(permission: PermissionResolvable, checkAdmin = true): boolean { return ( - (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || + (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.any(permission as any) ) } has(permission: PermissionResolvable, checkAdmin = true): boolean { return ( - (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || + (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.has(permission as any) ) } From 98874dd7e75f29ef3b94b9e286b06bbd7c211154 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 15 Feb 2021 13:38:07 +0530 Subject: [PATCH 03/72] fix --- mod.ts | 1 + src/structures/slash.ts | 2 +- src/types/slash.ts | 5 +---- src/utils/dict.ts | 3 +++ 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 src/utils/dict.ts diff --git a/mod.ts b/mod.ts index bc096bb..bdfbde3 100644 --- a/mod.ts +++ b/mod.ts @@ -134,3 +134,4 @@ export { UserFlags } from './src/types/userFlags.ts' export type { VoiceStatePayload } from './src/types/voice.ts' export type { WebhookPayload } from './src/types/webhook.ts' export * from './src/models/collectors.ts' +export type { Dict } from './src/utils/dict.ts' diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 99a1308..1e4003c 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -7,7 +7,6 @@ import { } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { - Dict, InteractionApplicationCommandData, InteractionApplicationCommandOption, InteractionChannelPayload, @@ -18,6 +17,7 @@ import { InteractionType, SlashCommandOptionType } from '../types/slash.ts' +import { Dict } from '../utils/dict.ts' import { Permissions } from '../utils/permissions.ts' import { SnowflakeBase } from './base.ts' import { Channel } from './channel.ts' diff --git a/src/types/slash.ts b/src/types/slash.ts index 69eea51..f8a8ecd 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,3 +1,4 @@ +import { Dict } from '../utils/dict.ts' import { AllowedMentionsPayload, ChannelTypes, @@ -25,10 +26,6 @@ export interface InteractionChannelPayload { type: ChannelTypes } -export interface Dict { - [name: string]: T -} - export interface InteractionApplicationCommandResolvedPayload { users?: Dict members?: Dict diff --git a/src/utils/dict.ts b/src/utils/dict.ts new file mode 100644 index 0000000..6ffaaf3 --- /dev/null +++ b/src/utils/dict.ts @@ -0,0 +1,3 @@ +export interface Dict { + [name: string]: T +} From 7bdbb165f4898c355617370a7785b211c22c5fb0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 17 Feb 2021 14:00:07 +0530 Subject: [PATCH 04/72] up --- src/models/commandClient.ts | 27 ++++++--------- src/test/index.ts | 8 +++-- src/types/permissionFlags.ts | 65 ++++++++++++++++++------------------ src/utils/bitfield.ts | 33 +++++++++--------- src/utils/mixedPromise.ts | 3 -- src/utils/permissions.ts | 15 ++++----- 6 files changed, 73 insertions(+), 78 deletions(-) delete mode 100644 src/utils/mixedPromise.ts diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 94d01ba..67d121d 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -1,6 +1,5 @@ import { Message } from '../structures/message.ts' import { GuildTextChannel } from '../structures/textChannel.ts' -import { awaitSync } from '../utils/mixedPromise.ts' import { Client, ClientOptions } from './client.ts' import { CategoriesManager, @@ -129,35 +128,29 @@ export class CommandClient extends Client implements CommandClientOptions { async processMessage(msg: Message): Promise { if (!this.allowBots && msg.author.bot === true) return - const isUserBlacklisted = await awaitSync( - this.isUserBlacklisted(msg.author.id) - ) - if (isUserBlacklisted === true) return + const isUserBlacklisted = await this.isUserBlacklisted(msg.author.id) + if (isUserBlacklisted) return - const isChannelBlacklisted = await awaitSync( - this.isChannelBlacklisted(msg.channel.id) - ) - if (isChannelBlacklisted === true) return + const isChannelBlacklisted = await this.isChannelBlacklisted(msg.channel.id) + if (isChannelBlacklisted) return if (msg.guild !== undefined) { - const isGuildBlacklisted = await awaitSync( - this.isGuildBlacklisted(msg.guild.id) - ) - if (isGuildBlacklisted === true) return + const isGuildBlacklisted = await this.isGuildBlacklisted(msg.guild.id) + if (isGuildBlacklisted) return } let prefix: string | string[] = [] if (typeof this.prefix === 'string') prefix = [...prefix, this.prefix] else prefix = [...prefix, ...this.prefix] - const userPrefix = await awaitSync(this.getUserPrefix(msg.author.id)) + const userPrefix = await this.getUserPrefix(msg.author.id) if (userPrefix !== undefined) { if (typeof userPrefix === 'string') prefix = [...prefix, userPrefix] else prefix = [...prefix, ...userPrefix] } if (msg.guild !== undefined) { - const guildPrefix = await awaitSync(this.getGuildPrefix(msg.guild.id)) + const guildPrefix = await this.getGuildPrefix(msg.guild.id) if (guildPrefix !== undefined) { if (typeof guildPrefix === 'string') prefix = [...prefix, guildPrefix] else prefix = [...prefix, ...guildPrefix] @@ -361,10 +354,10 @@ export class CommandClient extends Client implements CommandClientOptions { try { this.emit('commandUsed', ctx) - const beforeExecute = await awaitSync(command.beforeExecute(ctx)) + const beforeExecute = await command.beforeExecute(ctx) if (beforeExecute === false) return - const result = await awaitSync(command.execute(ctx)) + const result = await command.execute(ctx) command.afterExecute(ctx, result) } catch (e) { this.emit('commandError', ctx, e) diff --git a/src/test/index.ts b/src/test/index.ts index 1d3d7d9..ba93b2a 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -201,7 +201,7 @@ client.on('messageCreate', async (msg: Message) => { ) .join('\n\n')}` ) - } else if (msg.content === '!getPermissions') { + } else if (msg.content === '!perms') { if (msg.channel.type !== ChannelTypes.GUILD_TEXT) { return msg.channel.send("This isn't a guild text channel!") } @@ -210,7 +210,11 @@ client.on('messageCreate', async (msg: Message) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion msg.member as Member ) - msg.channel.send(`Your permissions:\n${permissions.toArray().join('\n')}`) + msg.channel.send( + Object.entries(permissions.serialize()) + .map((e) => `${e[0]}: ${e[1] === true ? '`✅`' : '`❌`'}`) + .join('\n') + ) } }) diff --git a/src/types/permissionFlags.ts b/src/types/permissionFlags.ts index 9cb3040..928a16f 100644 --- a/src/types/permissionFlags.ts +++ b/src/types/permissionFlags.ts @@ -1,35 +1,36 @@ // https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags -export const PermissionFlags: { [key: string]: number } = { - CREATE_INSTANT_INVITE: 1 << 0, - KICK_MEMBERS: 1 << 1, - BAN_MEMBERS: 1 << 2, - ADMINISTRATOR: 1 << 3, - MANAGE_CHANNELS: 1 << 4, - MANAGE_GUILD: 1 << 5, - ADD_REACTIONS: 1 << 6, - VIEW_AUDIT_LOG: 1 << 7, - PRIORITY_SPEAKER: 1 << 8, - STREAM: 1 << 9, - VIEW_CHANNEL: 1 << 10, - SEND_MESSAGES: 1 << 11, - SEND_TTS_MESSAGES: 1 << 12, - MANAGE_MESSAGES: 1 << 13, - EMBED_LINKS: 1 << 14, - ATTACH_FILES: 1 << 15, - READ_MESSAGE_HISTORY: 1 << 16, - MENTION_EVERYONE: 1 << 17, - USE_EXTERNAL_EMOJIS: 1 << 18, - VIEW_GUILD_INSIGHTS: 1 << 19, - CONNECT: 1 << 20, - SPEAK: 1 << 21, - MUTE_MEMBERS: 1 << 22, - DEAFEN_MEMBERS: 1 << 23, - MOVE_MEMBERS: 1 << 24, - USE_VAD: 1 << 25, - CHANGE_NICKNAME: 1 << 26, - MANAGE_NICKNAMES: 1 << 27, - MANAGE_ROLES: 1 << 28, - MANAGE_WEBHOOKS: 1 << 29, - MANAGE_EMOJIS: 1 << 30 +export const PermissionFlags: { [key: string]: bigint } = { + CREATE_INSTANT_INVITE: 1n << 0n, + KICK_MEMBERS: 1n << 1n, + BAN_MEMBERS: 1n << 2n, + ADMINISTRATOR: 1n << 3n, + MANAGE_CHANNELS: 1n << 4n, + MANAGE_GUILD: 1n << 5n, + ADD_REACTIONS: 1n << 6n, + VIEW_AUDIT_LOG: 1n << 7n, + PRIORITY_SPEAKER: 1n << 8n, + STREAM: 1n << 9n, + VIEW_CHANNEL: 1n << 10n, + SEND_MESSAGES: 1n << 11n, + SEND_TTS_MESSAGES: 1n << 12n, + MANAGE_MESSAGES: 1n << 13n, + EMBED_LINKS: 1n << 14n, + ATTACH_FILES: 1n << 15n, + READ_MESSAGE_HISTORY: 1n << 16n, + MENTION_EVERYONE: 1n << 17n, + USE_EXTERNAL_EMOJIS: 1n << 18n, + VIEW_GUILD_INSIGHTS: 1n << 19n, + CONNECT: 1n << 20n, + SPEAK: 1n << 21n, + MUTE_MEMBERS: 1n << 22n, + DEAFEN_MEMBERS: 1n << 23n, + MOVE_MEMBERS: 1n << 24n, + USE_VAD: 1n << 25n, + CHANGE_NICKNAME: 1n << 26n, + MANAGE_NICKNAMES: 1n << 27n, + MANAGE_ROLES: 1n << 28n, + MANAGE_WEBHOOKS: 1n << 29n, + MANAGE_EMOJIS: 1n << 30n, + USE_SLASH_COMMANDS: 1n << 31n } diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts index d2f5f03..8631f1f 100644 --- a/src/utils/bitfield.ts +++ b/src/utils/bitfield.ts @@ -6,19 +6,21 @@ export type BitFieldResolvable = | string | string[] | BitField[] + | bigint + | Array /** Bit Field utility to work with Bits and Flags */ export class BitField { - #flags: { [name: string]: number } = {} - bitfield: number + #flags: { [name: string]: number | bigint } = {} + bitfield: bigint - constructor(flags: { [name: string]: number }, bits: any) { + constructor(flags: { [name: string]: number | bigint }, bits: any) { this.#flags = flags this.bitfield = BitField.resolve(this.#flags, bits) } any(bit: BitFieldResolvable): boolean { - return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0 + return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0n } equals(bit: BitFieldResolvable): boolean { @@ -42,7 +44,7 @@ export class BitField { } add(...bits: BitFieldResolvable[]): BitField { - let total = 0 + let total = 0n for (const bit of bits) { total |= BitField.resolve(this.#flags, bit) } @@ -53,7 +55,7 @@ export class BitField { } remove(...bits: BitFieldResolvable[]): BitField { - let total = 0 + let total = 0n for (const bit of bits) { total |= BitField.resolve(this.#flags, bit) } @@ -63,12 +65,12 @@ export class BitField { return this } - flags(): { [name: string]: number } { + flags(): { [name: string]: bigint | number } { return this.#flags } - serialize(...hasParams: any[]): { [key: string]: any } { - const serialized: { [key: string]: any } = {} + serialize(...hasParams: any[]): { [key: string]: boolean } { + const serialized: { [key: string]: boolean } = {} for (const [flag, bit] of Object.entries(this.#flags)) serialized[flag] = this.has( BitField.resolve(this.#flags, bit), @@ -83,11 +85,11 @@ export class BitField { ) } - toJSON(): number { - return this.bitfield + toJSON(): string { + return this.bitfield.toString() } - valueOf(): number { + valueOf(): bigint { return this.bitfield } @@ -95,9 +97,10 @@ export class BitField { yield* this.toArray() } - static resolve(flags: any, bit: BitFieldResolvable = 0): number { - if (typeof bit === 'string' && !isNaN(parseInt(bit))) return parseInt(bit) - if (typeof bit === 'number' && bit >= 0) return bit + static resolve(flags: any, bit: BitFieldResolvable = 0n): bigint { + if (typeof bit === 'bigint') return bit + if (typeof bit === 'string' && !isNaN(parseInt(bit))) return BigInt(bit) + if (typeof bit === 'number' && bit >= 0) return BigInt(bit) if (bit instanceof BitField) return this.resolve(flags, bit.bitfield) if (Array.isArray(bit)) return (bit.map as any)((p: any) => this.resolve(flags, p)).reduce( diff --git a/src/utils/mixedPromise.ts b/src/utils/mixedPromise.ts deleted file mode 100644 index 4f1b297..0000000 --- a/src/utils/mixedPromise.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const awaitSync = async (val: any | Promise): Promise => { - return val instanceof Promise ? await val : val -} diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 99b258e..8d7a488 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -2,18 +2,15 @@ import { PermissionFlags } from '../types/permissionFlags.ts' import { BitField, BitFieldResolvable } from './bitfield.ts' -export type PermissionResolvable = - | string - | string[] - | number - | number[] - | Permissions - | PermissionResolvable[] +export type PermissionResolvable = BitFieldResolvable /** Manages Discord's Bit-based Permissions */ export class Permissions extends BitField { - static DEFAULT = 104324673 - static ALL = Object.values(PermissionFlags).reduce((all, p) => all | p, 0) + static DEFAULT = 104324673n + static ALL = Object.values(PermissionFlags).reduce( + (all, p) => BigInt(all) | BigInt(p), + 0n + ) constructor(bits: BitFieldResolvable) { super(PermissionFlags, bits) From dffce5bd0bc0ed1bf88dedfbb4fb3149b9f537d4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 17 Feb 2021 14:25:04 +0530 Subject: [PATCH 05/72] new --- src/structures/slash.ts | 66 ++++++++++++++++++++++------------------- src/types/slash.ts | 26 ++++++++++------ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 1e4003c..9b8cc10 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -44,26 +44,20 @@ export interface InteractionMessageOptions { tts?: boolean flags?: number | InteractionResponseFlags[] allowedMentions?: AllowedMentionsPayload - /** Whether to reply with Source or not. True by default */ - withSource?: boolean } export interface InteractionResponse extends InteractionMessageOptions { /** Type of Interaction Response */ type?: InteractionResponseType - /** - * DEPRECATED: Use `ephemeral` instead. - * - * @deprecated - */ - temp?: boolean /** Whether the Message Response should be Ephemeral (only visible to User) or not */ ephemeral?: boolean } /** Represents a Channel Object for an Option in Slash Command */ export class InteractionChannel extends SnowflakeBase { + /** Name of the Channel */ name: string + /** Channel Type */ type: ChannelTypes permissions: Permissions @@ -98,17 +92,22 @@ export class Interaction extends SnowflakeBase { token: string /** Interaction ID */ id: string - /** Data sent with Interaction. Only applies to Application Command, type may change in future. */ + /** Data sent with Interaction. Only applies to Application Command */ data?: InteractionApplicationCommandData /** Channel in which Interaction was initiated */ channel?: TextChannel | GuildTextChannel + /** Guild in which Interaction was initiated */ guild?: Guild /** Member object of who initiated the Interaction */ member?: Member /** User object of who invoked Interaction */ user: User + /** Whether we have responded to Interaction or not */ responded: boolean = false + /** Resolved data for Snowflakes in Slash Command Arguments */ resolved: InteractionApplicationCommandResolved + /** Whether response was deferred or not */ + deferred: boolean = false constructor( client: Client, @@ -159,19 +158,14 @@ export class Interaction extends SnowflakeBase { async respond(data: InteractionResponse): Promise { if (this.responded) throw new Error('Already responded to Interaction') let flags = 0 - if (data.ephemeral === true || data.temp === true) - flags |= InteractionResponseFlags.EPHEMERAL + if (data.ephemeral === true) flags |= InteractionResponseFlags.EPHEMERAL if (data.flags !== undefined) { if (Array.isArray(data.flags)) flags = data.flags.reduce((p, a) => p | a, flags) else if (typeof data.flags === 'number') flags |= data.flags } const payload: InteractionResponsePayload = { - type: - data.type ?? - (data.withSource === false - ? InteractionResponseType.CHANNEL_MESSAGE - : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE), + type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: data.type === undefined || data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || @@ -195,14 +189,12 @@ export class Interaction extends SnowflakeBase { return this } - /** Acknowledge the Interaction */ - async acknowledge(withSource?: boolean): Promise { + /** Defer the Interaction i.e. let the user know bot is processing and will respond later. You only have 15 minutes to edit the response! */ + async defer(): Promise { await this.respond({ - type: - withSource === true - ? InteractionResponseType.ACK_WITH_SOURCE - : InteractionResponseType.ACKNOWLEDGE + type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE }) + this.deferred = true return this } @@ -228,14 +220,19 @@ export class Interaction extends SnowflakeBase { if (options === undefined) options = {} if (typeof content === 'string') Object.assign(options, { content }) - await this.respond( - Object.assign(options, { - type: - options.withSource === false - ? InteractionResponseType.CHANNEL_MESSAGE - : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + if (this.deferred && this.responded) { + await this.editResponse({ + content: options.content, + embeds: options.embeds, + flags: options.flags, + allowedMentions: options.allowedMentions }) - ) + } else + await this.respond( + Object.assign(options, { + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + }) + ) return this } @@ -243,7 +240,9 @@ export class Interaction extends SnowflakeBase { /** Edit the original Interaction response */ async editResponse(data: { content?: string - embeds?: Embed[] + embeds?: EmbedPayload[] + flags?: number | number[] + allowedMentions?: AllowedMentionsPayload }): Promise { const url = WEBHOOK_MESSAGE( this.client.user?.id as string, @@ -252,7 +251,12 @@ export class Interaction extends SnowflakeBase { ) await this.client.rest.patch(url, { content: data.content ?? '', - embeds: data.embeds ?? [] + embeds: data.embeds ?? [], + flags: + typeof data.flags === 'object' + ? data.flags.reduce((p, a) => p | a, 0) + : data.flags, + allowed_mentions: data.allowedMentions }) return this } diff --git a/src/types/slash.ts b/src/types/slash.ts index f8a8ecd..c700fbd 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -52,6 +52,7 @@ export enum InteractionType { } export interface InteractionMemberPayload extends MemberPayload { + /** Permissions of the Member who initiated Interaction (Guild-only) */ permissions: string } @@ -67,9 +68,7 @@ export interface InteractionPayload { /** ID of the Interaction */ id: string /** - * Data sent with the interaction - * - * This can be undefined only when Interaction is not a Slash Command. + * Data sent with the interaction. Undefined only when Interaction is not Slash Command.* */ data?: InteractionApplicationCommandData /** ID of the Guild in which Interaction was invoked */ @@ -86,7 +85,9 @@ export interface SlashCommandChoice { } export enum SlashCommandOptionType { + /** A sub command that is either a part of a root command or Sub Command Group */ SUB_COMMAND = 1, + /** A sub command group that is present in root command's options */ SUB_COMMAND_GROUP = 2, STRING = 3, INTEGER = 4, @@ -112,28 +113,35 @@ export interface SlashCommandOption { options?: SlashCommandOption[] } +/** Represents the Slash Command (Application Command) payload sent for creating/bulk editing. */ export interface SlashCommandPartial { + /** Name of the Slash Command */ name: string + /** Description of the Slash Command */ description: string + /** Options (arguments, sub commands or group) of the Slash Command */ options?: SlashCommandOption[] } +/** Represents a fully qualified Slash Command (Application Command) payload. */ export interface SlashCommandPayload extends SlashCommandPartial { + /** ID of the Slash Command */ id: string + /** Application ID */ application_id: string } export enum InteractionResponseType { /** Just ack a ping, Http-only. */ PONG = 1, - /** Do nothing, just acknowledge the Interaction */ + /** @deprecated **DEPRECATED:** Do nothing, just acknowledge the Interaction */ ACKNOWLEDGE = 2, - /** Send a channel message without " used / with " */ + /** @deprecated **DEPRECATED:** Send a channel message without " used / with " */ CHANNEL_MESSAGE = 3, - /** Send a channel message with " used / with " */ + /** Send a channel message as response. */ CHANNEL_MESSAGE_WITH_SOURCE = 4, - /** Send nothing further, but send " used / with " */ - ACK_WITH_SOURCE = 5 + /** Let the user know bot is processing ("thinking") and you can edit the response later */ + DEFERRED_CHANNEL_MESSAGE = 5 } export interface InteractionResponsePayload { @@ -155,6 +163,6 @@ export interface InteractionResponseDataPayload { } export enum InteractionResponseFlags { - /** A Message which is only visible to Interaction User, and is not saved on backend */ + /** A Message which is only visible to Interaction User. */ EPHEMERAL = 1 << 6 } From 90159fa2dad9c1ab0a716a6af47adcc57ba8e948 Mon Sep 17 00:00:00 2001 From: DjDeveloper <43033058+DjDeveloperr@users.noreply.github.com> Date: Fri, 12 Mar 2021 13:31:48 +0530 Subject: [PATCH 06/72] fix interaction channel --- src/structures/slash.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 9b8cc10..688c024 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -63,6 +63,7 @@ export class InteractionChannel extends SnowflakeBase { constructor(client: Client, data: InteractionChannelPayload) { super(client) + this.id = data.id this.name = data.name this.type = data.type this.permissions = new Permissions(data.permissions) From 5b015c6c080761e3b71a0fbbd5b23ef7369a4670 Mon Sep 17 00:00:00 2001 From: DjDeveloper <43033058+DjDeveloperr@users.noreply.github.com> Date: Fri, 12 Mar 2021 15:01:14 +0530 Subject: [PATCH 07/72] fix interaction user --- src/structures/slash.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 688c024..552a54d 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -147,7 +147,9 @@ export class Interaction extends SnowflakeBase { const op = this.options.find((e) => e.name === name) if (op === undefined || op.value === undefined) return undefined as any if (op.type === SlashCommandOptionType.USER) - return this.resolved.users[op.value] as any + const u: InteractionUser = this.resolved.users[op.value] as any + if (this.resolved.members[op.value] !== undefined) u.member = this.resolved.members[op.value] as any + return u else if (op.type === SlashCommandOptionType.ROLE) return this.resolved.roles[op.value] as any else if (op.type === SlashCommandOptionType.CHANNEL) From c836ca8f429b055fbfbc95a51011d1b8ecadbcdd Mon Sep 17 00:00:00 2001 From: DjDeveloper <43033058+DjDeveloperr@users.noreply.github.com> Date: Fri, 12 Mar 2021 15:05:43 +0530 Subject: [PATCH 08/72] fix --- src/structures/slash.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 552a54d..ffcff03 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -146,11 +146,11 @@ export class Interaction extends SnowflakeBase { option(name: string): T { const op = this.options.find((e) => e.name === name) if (op === undefined || op.value === undefined) return undefined as any - if (op.type === SlashCommandOptionType.USER) + if (op.type === SlashCommandOptionType.USER) { const u: InteractionUser = this.resolved.users[op.value] as any if (this.resolved.members[op.value] !== undefined) u.member = this.resolved.members[op.value] as any return u - else if (op.type === SlashCommandOptionType.ROLE) + } else if (op.type === SlashCommandOptionType.ROLE) return this.resolved.roles[op.value] as any else if (op.type === SlashCommandOptionType.CHANNEL) return this.resolved.channels[op.value] as any From 5f75fc3e71603918c166b04872d2cf7235ce41d1 Mon Sep 17 00:00:00 2001 From: DjDeveloper <43033058+DjDeveloperr@users.noreply.github.com> Date: Fri, 12 Mar 2021 15:12:38 +0530 Subject: [PATCH 09/72] fix ffs --- src/structures/slash.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index ffcff03..ef793af 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -148,8 +148,8 @@ export class Interaction extends SnowflakeBase { if (op === undefined || op.value === undefined) return undefined as any if (op.type === SlashCommandOptionType.USER) { const u: InteractionUser = this.resolved.users[op.value] as any - if (this.resolved.members[op.value] !== undefined) u.member = this.resolved.members[op.value] as any - return u + if (this.resolved.members[op.value] !== undefined) u.member = this.resolved.members[op.value] + return u as any } else if (op.type === SlashCommandOptionType.ROLE) return this.resolved.roles[op.value] as any else if (op.type === SlashCommandOptionType.CHANNEL) From e65fa8fded2985864ffe34cf7e48d8fa4083b6a1 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 14 Mar 2021 14:20:15 +0530 Subject: [PATCH 10/72] try fix verifyKey --- src/models/slashClient.ts | 32 +++++++++++++++----------------- src/test/index.ts | 11 +---------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 3318e05..6917ce8 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -12,8 +12,7 @@ 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 { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' export class SlashCommand { slash: SlashCommandsManager @@ -369,6 +368,8 @@ export interface SlashOptions { publicKey?: string } +const encoder = new TextEncoder() + export class SlashClient { id: string | (() => string) client?: Client @@ -503,24 +504,21 @@ export class SlashClient { } async verifyKey( - rawBody: string | Uint8Array | Buffer, - signature: string, - timestamp: string + rawBody: string | Uint8Array, + signature: string | Uint8Array, + timestamp: string | Uint8Array ): 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) + + const fullBody = new Uint8Array([ + ...(typeof timestamp === 'string' + ? encoder.encode(timestamp) + : timestamp), + ...(typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody) + ]) + + return edverify(signature, fullBody, this.publicKey).catch(() => false) } async verifyOpineRequest(req: any): Promise { diff --git a/src/test/index.ts b/src/test/index.ts index da0d0bb..3923b5e 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -117,16 +117,7 @@ client.on('messageCreate', async (msg: Message) => { msg.channel.send('Failed...') } } else if (msg.content === '!react') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - msg.addReaction('😂') - msg.channel.send('x'.repeat(6969), { - embed: new Embed() - .setTitle('pepega'.repeat(6969)) - .setDescription('pepega'.repeat(6969)) - .addField('uwu', 'uwu'.repeat(6969)) - .addField('uwu', 'uwu'.repeat(6969)) - .setFooter('uwu'.repeat(6969)) - }) + msg.addReaction('a:programming:785013658257195008') } else if (msg.content === '!wait_for') { msg.channel.send('Send anything!') const [receivedMsg] = await client.waitFor( From dcb07a9aaab4cc769c09df6214a06b574b539c4e Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 14 Mar 2021 15:12:05 +0530 Subject: [PATCH 11/72] x --- src/models/slashClient.ts | 32 ++++++++++++++++++++++++++++ src/structures/slash.ts | 2 ++ src/test/slash-http.ts | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 src/test/slash-http.ts diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 6917ce8..7e9b581 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,6 +1,7 @@ import { Guild } from '../structures/guild.ts' import { Interaction } from '../structures/slash.ts' import { + InteractionPayload, InteractionType, SlashCommandChoice, SlashCommandOption, @@ -369,6 +370,7 @@ export interface SlashOptions { } const encoder = new TextEncoder() +const decoder = new TextDecoder('utf-8') export class SlashClient { id: string | (() => string) @@ -503,6 +505,7 @@ export class SlashClient { cmd.handler(interaction) } + /** Verify HTTP based Interaction */ async verifyKey( rawBody: string | Uint8Array, signature: string | Uint8Array, @@ -521,6 +524,35 @@ export class SlashClient { return edverify(signature, fullBody, this.publicKey).catch(() => false) } + /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction */ + async verifyServerRequest(req: { + headers: Headers + method: string + body: Deno.Reader + respond: (options: { + status?: number + body?: string | Uint8Array + }) => Promise + }): Promise { + if (req.method.toLowerCase() !== 'post') return false + + const signature = req.headers.get('x-signature-ed25519') + const timestamp = req.headers.get('x-signature-timestamp') + if (signature === null || timestamp === null) return false + + const rawbody = await Deno.readAll(req.body) + const verify = await this.verifyKey(rawbody, signature, timestamp) + if (!verify) return false + + try { + const payload: InteractionPayload = JSON.parse(decoder.decode(rawbody)) + const res = new Interaction(this as any, payload, {}) + return res + } catch (e) { + return false + } + } + async verifyOpineRequest(req: any): Promise { const signature = req.headers.get('x-signature-ed25519') const timestamp = req.headers.get('x-signature-timestamp') diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 388aa56..a564a30 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -41,6 +41,7 @@ export interface InteractionResponse { } export class Interaction extends SnowflakeBase { + /** This will be `SlashClient` in case of `SlashClient#verifyServerRequest` */ client: Client type: number token: string @@ -50,6 +51,7 @@ export class Interaction extends SnowflakeBase { guild: Guild member: Member _savedHook?: Webhook + _respond?: (data: InteractionResponsePayload) => unknown constructor( client: Client, diff --git a/src/test/slash-http.ts b/src/test/slash-http.ts new file mode 100644 index 0000000..02f9836 --- /dev/null +++ b/src/test/slash-http.ts @@ -0,0 +1,44 @@ +import { SlashClient } from '../../mod.ts' +import { SLASH_ID, SLASH_PUB_KEY, SLASH_TOKEN } from './config.ts' +import { listenAndServe } from 'https://deno.land/std@0.90.0/http/server.ts' + +const slash = new SlashClient({ + id: SLASH_ID, + token: SLASH_TOKEN, + publicKey: SLASH_PUB_KEY +}) + +await slash.commands.bulkEdit([ + { + name: 'ping', + description: 'Just ping!' + } +]) + +const options = { port: 8000 } +console.log('Listen on port: ' + options.port.toString()) +listenAndServe(options, async (req) => { + const verify = await slash.verifyServerRequest(req) + if (verify === false) + return req.respond({ status: 401, body: 'not authorized' }) + + const respond = async (d: any): Promise => + req.respond({ + status: 200, + body: JSON.stringify(d), + headers: new Headers({ + 'content-type': 'application/json' + }) + }) + + const body = JSON.parse( + new TextDecoder('utf-8').decode(await Deno.readAll(req.body)) + ) + if (body.type === 1) return await respond({ type: 1 }) + await respond({ + type: 4, + data: { + content: 'Pong!' + } + }) +}) From b02b16ffcca409aa37a2de88008d2bd1edb57d6f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 14 Mar 2021 15:17:19 +0530 Subject: [PATCH 12/72] i give up --- src/structures/slash.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 3714697..626ae97 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -7,14 +7,12 @@ import { } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { - InteractionApplicationCommandData, InteractionApplicationCommandOption, InteractionChannelPayload, InteractionPayload, InteractionResponseFlags, InteractionResponsePayload, InteractionResponseType, - InteractionType, SlashCommandOptionType } from '../types/slash.ts' import { Dict } from '../utils/dict.ts' @@ -28,6 +26,7 @@ import { Message } from './message.ts' import { Role } from './role.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts' import { User } from './user.ts' +import { Webhook } from './webhook.ts' interface WebhookMessageOptions extends MessageOptions { embeds?: Embed[] @@ -87,15 +86,15 @@ export class InteractionUser extends User { export class Interaction extends SnowflakeBase { /** This will be `SlashClient` in case of `SlashClient#verifyServerRequest` */ - client: Client + client!: Client type: number token: string /** Interaction ID */ id: string data: InteractionData channel: GuildTextChannel - guild: Guild - member: Member + guild?: Guild + member?: Member _savedHook?: Webhook _respond?: (data: InteractionResponsePayload) => unknown From c1a14fdac1e0eb3079e26861fcd822665df9559d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 14 Mar 2021 15:46:44 +0530 Subject: [PATCH 13/72] http slash commands --- src/models/slashClient.ts | 41 +++++++++++++++++++++++++++++++++++---- src/structures/slash.ts | 14 +++++++++---- src/test/slash-http.ts | 27 +++++--------------------- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 5f8955d..33b3aca 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,7 +1,11 @@ import { Guild } from '../structures/guild.ts' -import { Interaction } from '../structures/slash.ts' +import { + Interaction, + InteractionApplicationCommandResolved +} from '../structures/slash.ts' import { InteractionPayload, + InteractionResponsePayload, InteractionType, SlashCommandChoice, SlashCommandOption, @@ -14,6 +18,7 @@ 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@1.0.1/mod.ts' +import { User } from '../structures/user.ts' export class SlashCommand { slash: SlashCommandsManager @@ -372,6 +377,9 @@ export interface SlashOptions { publicKey?: string } +const encoder = new TextEncoder() +const decoder = new TextDecoder('utf-8') + /** Slash Client represents an Interactions Client which can be used without Harmony Client. */ export class SlashClient { id: string | (() => string) @@ -537,16 +545,17 @@ export class SlashClient { return edverify(signature, fullBody, this.publicKey).catch(() => false) } - /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction */ + /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction. **Data present in Interaction returned by this method is very different from actual typings as there is no real `Client` behind the scenes to cache things.** */ async verifyServerRequest(req: { headers: Headers method: string body: Deno.Reader respond: (options: { status?: number + headers?: Headers body?: string | Uint8Array }) => Promise - }): Promise { + }): Promise { if (req.method.toLowerCase() !== 'post') return false const signature = req.headers.get('x-signature-ed25519') @@ -559,7 +568,31 @@ export class SlashClient { try { const payload: InteractionPayload = JSON.parse(decoder.decode(rawbody)) - const res = new Interaction(this as any, payload, {}) + + // TODO: Maybe fix all this hackery going on here? + const res = new Interaction(this as any, payload, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + user: new User(this as any, (payload.member?.user ?? payload.user)!), + member: payload.member as any, + guild: payload.guild_id as any, + channel: payload.channel_id as any, + resolved: ((payload.data + ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { + users: {}, + members: {}, + roles: {}, + channels: {} + } + }) + res._httpRespond = async (d: InteractionResponsePayload) => + await req.respond({ + status: 200, + headers: new Headers({ + 'content-type': 'application/json' + }), + body: JSON.stringify(d) + }) + return res } catch (e) { return false diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 9b8cc10..259151b 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -108,6 +108,8 @@ export class Interaction extends SnowflakeBase { resolved: InteractionApplicationCommandResolved /** Whether response was deferred or not */ deferred: boolean = false + _httpRespond?: (d: InteractionResponsePayload) => unknown + _httpResponded?: boolean constructor( client: Client, @@ -180,10 +182,14 @@ export class Interaction extends SnowflakeBase { : undefined } - await this.client.rest.post( - INTERACTION_CALLBACK(this.id, this.token), - payload - ) + if (this._httpRespond !== undefined && this._httpResponded !== true) { + this._httpResponded = true + await this._httpRespond(payload) + } else + await this.client.rest.post( + INTERACTION_CALLBACK(this.id, this.token), + payload + ) this.responded = true return this diff --git a/src/test/slash-http.ts b/src/test/slash-http.ts index 02f9836..893feaf 100644 --- a/src/test/slash-http.ts +++ b/src/test/slash-http.ts @@ -18,27 +18,10 @@ await slash.commands.bulkEdit([ const options = { port: 8000 } console.log('Listen on port: ' + options.port.toString()) listenAndServe(options, async (req) => { - const verify = await slash.verifyServerRequest(req) - if (verify === false) - return req.respond({ status: 401, body: 'not authorized' }) + const d = await slash.verifyServerRequest(req) + if (d === false) return req.respond({ status: 401, body: 'not authorized' }) - const respond = async (d: any): Promise => - req.respond({ - status: 200, - body: JSON.stringify(d), - headers: new Headers({ - 'content-type': 'application/json' - }) - }) - - const body = JSON.parse( - new TextDecoder('utf-8').decode(await Deno.readAll(req.body)) - ) - if (body.type === 1) return await respond({ type: 1 }) - await respond({ - type: 4, - data: { - content: 'Pong!' - } - }) + console.log(d) + if (d.type === 1) return d.respond({ type: 1 }) + d.reply('Pong!') }) From 3f7372d6a77f758dc43e625c61708b8841a168e4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 26 Mar 2021 12:47:11 +0530 Subject: [PATCH 14/72] finna --- mod.ts | 24 ++++++++++++++++++++++-- src/structures/message.ts | 22 ++++++++++++++++++++++ src/types/channel.ts | 9 +++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/mod.ts b/mod.ts index d02a3b6..cdbf8ec 100644 --- a/mod.ts +++ b/mod.ts @@ -5,7 +5,13 @@ export type { GatewayTypedEvents } from './src/gateway/index.ts' export type { ClientEvents } from './src/gateway/handlers/index.ts' export * from './src/models/client.ts' export * from './src/models/slashClient.ts' -export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts' +export { + RESTManager, + TokenType, + HttpResponseCode, + DiscordAPIError +} from './src/models/rest.ts' +export type { APIMap, DiscordAPIErrorPayload } from './src/models/rest.ts' export type { RequestHeaders } from './src/models/rest.ts' export type { RESTOptions } from './src/models/rest.ts' export * from './src/models/cacheAdapter.ts' @@ -62,7 +68,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts' export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' export { Invite } from './src/structures/invite.ts' export * from './src/structures/member.ts' -export { Message, MessageAttachment } from './src/structures/message.ts' +export { + Message, + MessageAttachment, + MessageInteraction +} from './src/structures/message.ts' export { MessageMentions } from './src/structures/messageMentions.ts' export { Presence, @@ -109,6 +119,16 @@ export type { GuildVoiceChannelPayload, GroupDMChannelPayload, MessageOptions, + MessagePayload, + MessageInteractionPayload, + MessageReference, + MessageActivity, + MessageActivityTypes, + MessageApplication, + MessageFlags, + MessageStickerFormatTypes, + MessageStickerPayload, + MessageTypes, OverwriteAsArg, Overwrite, OverwriteAsOptions diff --git a/src/structures/message.ts b/src/structures/message.ts index 8a9a360..c0cbb9e 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -3,6 +3,7 @@ import { Attachment, MessageActivity, MessageApplication, + MessageInteractionPayload, MessageOptions, MessagePayload, MessageReference @@ -19,9 +20,25 @@ import { Guild } from './guild.ts' import { MessageReactionsManager } from '../managers/messageReactions.ts' import { MessageSticker } from './messageSticker.ts' import { Emoji } from './emoji.ts' +import { InteractionType } from '../types/slash.ts' type AllMessageOptions = MessageOptions | Embed +export class MessageInteraction extends SnowflakeBase { + id: string + name: string + type: InteractionType + user: User + + constructor(client: Client, data: MessageInteractionPayload) { + super(client) + this.id = data.id + this.name = data.name + this.type = data.type + this.user = new User(this.client, data.user) + } +} + export class Message extends SnowflakeBase { id: string channelID: string @@ -46,6 +63,7 @@ export class Message extends SnowflakeBase { messageReference?: MessageReference flags?: number stickers?: MessageSticker[] + interaction?: MessageInteraction get createdAt(): Date { return new Date(this.timestamp) @@ -87,6 +105,10 @@ export class Message extends SnowflakeBase { (payload) => new MessageSticker(this.client, payload) ) : undefined + this.interaction = + data.interaction === undefined + ? undefined + : new MessageInteraction(this.client, data.interaction) } readFromData(data: MessagePayload): void { diff --git a/src/types/channel.ts b/src/types/channel.ts index 80bf065..40f4748 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -5,6 +5,7 @@ import { Role } from '../structures/role.ts' import { Permissions } from '../utils/permissions.ts' import { EmojiPayload } from './emoji.ts' import { MemberPayload } from './guild.ts' +import { InteractionType } from './slash.ts' import { UserPayload } from './user.ts' export interface ChannelPayload { @@ -185,6 +186,7 @@ export interface MessagePayload { message_reference?: MessageReference flags?: number stickers?: MessageStickerPayload[] + interaction?: MessageInteractionPayload } export enum AllowedMentionType { @@ -373,3 +375,10 @@ export interface MessageStickerPayload { preview_asset: string | null format_type: MessageStickerFormatTypes } + +export interface MessageInteractionPayload { + id: string + type: InteractionType + name: string + user: UserPayload +} From 010a48c7f028fa808dad423f81e7aee2f4afe246 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 29 Mar 2021 09:39:37 +0530 Subject: [PATCH 15/72] debug logs --- src/gateway/handlers/index.ts | 1 + src/gateway/index.ts | 23 +++++++++++------------ src/structures/slash.ts | 21 +++++++++++---------- src/test/debug.ts | 28 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 src/test/debug.ts diff --git a/src/gateway/handlers/index.ts b/src/gateway/handlers/index.ts index 9775575..ba5dfa9 100644 --- a/src/gateway/handlers/index.ts +++ b/src/gateway/handlers/index.ts @@ -414,4 +414,5 @@ export type ClientEvents = { commandMissingArgs: [ctx: CommandContext] commandUsed: [ctx: CommandContext] commandError: [ctx: CommandContext, err: Error] + gatewayError: [err: ErrorEvent, shards: [number, number]] } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index d890457..1d672af 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -174,8 +174,8 @@ export class Gateway extends HarmonyEventEmitter { } case GatewayOpcodes.RECONNECT: { this.emit('reconnectRequired') - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect() + this.debug('Received OpCode RECONNECT') + await this.reconnect() break } default: @@ -191,8 +191,7 @@ export class Gateway extends HarmonyEventEmitter { 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() + await this.reconnect() break case GatewayCloseCodes.UNKNOWN_OPCODE: throw new Error( @@ -206,20 +205,17 @@ export class Gateway extends HarmonyEventEmitter { 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() + await 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) + await 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() + await this.reconnect() break case GatewayCloseCodes.SHARDING_REQUIRED: throw new Error("Couldn't connect. Sharding is required!") @@ -257,6 +253,7 @@ export class Gateway extends HarmonyEventEmitter { error.name = 'ErrorEvent' console.log(error) this.emit('error', error, event) + this.client.emit('gatewayError', event, this.shards) } private enqueueIdentify(forceNew?: boolean): void { @@ -388,8 +385,8 @@ export class Gateway extends HarmonyEventEmitter { channel === undefined ? null : typeof channel === 'string' - ? channel - : channel?.id, + ? channel + : channel?.id, self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute, self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf } @@ -402,6 +399,7 @@ export class Gateway extends HarmonyEventEmitter { async reconnect(forceNew?: boolean): Promise { this.emit('reconnecting') + this.debug('Reconnecting... (force new: ' + String(forceNew) + ')') clearInterval(this.heartbeatIntervalID) if (forceNew === true) { @@ -429,6 +427,7 @@ export class Gateway extends HarmonyEventEmitter { } close(code: number = 1000, reason?: string): void { + this.debug(`Closing with code ${code}${reason !== undefined && reason !== '' ? ` and reason ${reason}` : ''}`) return this.websocket?.close(code, reason) } diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 86c48dc..d7681e5 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -175,15 +175,15 @@ export class Interaction extends SnowflakeBase { type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: data.type === undefined || - data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || - data.type === InteractionResponseType.CHANNEL_MESSAGE + data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || + data.type === InteractionResponseType.CHANNEL_MESSAGE ? { - content: data.content ?? '', - embeds: data.embeds, - tts: data.tts ?? false, - flags, - allowed_mentions: data.allowedMentions ?? undefined - } + content: data.content ?? '', + embeds: data.embeds, + tts: data.tts ?? false, + flags, + allowed_mentions: data.allowedMentions ?? undefined + } : undefined } @@ -313,9 +313,10 @@ export class Interaction extends SnowflakeBase { (option as WebhookMessageOptions)?.embed !== undefined ? [(option as WebhookMessageOptions).embed] : (option as WebhookMessageOptions)?.embeds !== undefined - ? (option as WebhookMessageOptions).embeds - : undefined, + ? (option as WebhookMessageOptions).embeds + : undefined, file: (option as WebhookMessageOptions)?.file, + files: (option as WebhookMessageOptions)?.files, tts: (option as WebhookMessageOptions)?.tts, allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions } diff --git a/src/test/debug.ts b/src/test/debug.ts new file mode 100644 index 0000000..48bc55e --- /dev/null +++ b/src/test/debug.ts @@ -0,0 +1,28 @@ +import { Client, event } from '../../mod.ts' +import { TOKEN } from './config.ts' + +class MyClient extends Client { + constructor() { + super({ + token: TOKEN, + intents: [], + }) + } + + @event() + ready(): void { + console.log('Connected!') + } + + debug(title: string, msg: string): void { + console.log(`[${title}] ${msg}`) + if (title === 'Gateway' && msg === 'Initializing WebSocket...') { + try { throw new Error("Stack") } catch (e) { + console.log(e.stack) + } + } + } +} + +const client = new MyClient() +client.connect() \ No newline at end of file From 5fc58bd901fbf8a1ee23db21b29d75c1b4700439 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 29 Mar 2021 05:30:40 -0700 Subject: [PATCH 16/72] service worker --- src/models/slashClient.ts | 112 ++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 33b3aca..0dac52c 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -93,8 +93,8 @@ function createSlashOption( data.choices === undefined ? undefined : data.choices.map((e) => - typeof e === 'string' ? { name: e, value: e } : e - ) + typeof e === 'string' ? { name: e, value: e } : e + ) } } @@ -138,15 +138,15 @@ export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption export type SlashBuilderOptionsData = | Array | { - [name: string]: - | { - description: string - type: SlashCommandOptionType - options?: SlashCommandOption[] - choices?: SlashCommandChoice[] - } - | SlashOptionCallable + [name: string]: + | { + description: string + type: SlashCommandOptionType + options?: SlashCommandOption[] + choices?: SlashCommandChoice[] } + | SlashOptionCallable + } function buildOptionsArray( options: SlashBuilderOptionsData @@ -154,10 +154,10 @@ function buildOptionsArray( 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] }) - ) + typeof entry[1] === 'function' + ? entry[1](SlashOption) + : Object.assign(entry[1], { name: entry[0] }) + ) } /** Slash Command Builder */ @@ -267,8 +267,8 @@ export class SlashCommandsManager { guild === undefined ? this.rest.api.applications[this.slash.getID()].commands : this.rest.api.applications[this.slash.getID()].guilds[ - typeof guild === 'string' ? guild : guild.id - ].commands + typeof guild === 'string' ? guild : guild.id + ].commands const payload = await route.post(data) @@ -276,8 +276,8 @@ export class SlashCommandsManager { typeof guild === 'object' ? guild : guild === undefined - ? undefined - : await this.slash.client?.guilds.get(guild) + ? undefined + : await this.slash.client?.guilds.get(guild) const cmd = new SlashCommand(this, payload, _guild) cmd._guild = @@ -296,8 +296,8 @@ export class SlashCommandsManager { guild === undefined ? 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] + typeof guild === 'string' ? guild : guild.id + ].commands[id] await route.patch(data) return this @@ -312,8 +312,8 @@ export class SlashCommandsManager { guild === undefined ? 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] + typeof guild === 'string' ? guild : guild.id + ].commands[id] await route.delete() return this @@ -325,8 +325,8 @@ export class SlashCommandsManager { guild === undefined ? 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] + typeof guild === 'string' ? guild : guild.id + ].commands[id] const data = await route.get() @@ -334,8 +334,8 @@ export class SlashCommandsManager { typeof guild === 'object' ? guild : guild === undefined - ? undefined - : await this.slash.client?.guilds.get(guild) + ? undefined + : await this.slash.client?.guilds.get(guild) return new SlashCommand(this, data, _guild) } @@ -349,8 +349,8 @@ export class SlashCommandsManager { guild === undefined ? this.rest.api.applications[this.slash.getID()].commands : this.rest.api.applications[this.slash.getID()].guilds[ - typeof guild === 'string' ? guild : guild.id - ].commands + typeof guild === 'string' ? guild : guild.id + ].commands await route.put(cmds) @@ -430,8 +430,8 @@ export class SlashClient { options.client === undefined ? options.rest === undefined ? new RESTManager({ - token: this.token - }) + token: this.token + }) : options.rest : options.client.rest @@ -481,20 +481,20 @@ export class SlashClient { const groupMatched = e.group !== undefined && e.parent !== undefined ? i.options - .find( - (o) => - o.name === e.group && - o.type === SlashCommandOptionType.SUB_COMMAND_GROUP - ) - ?.options?.find((o) => o.name === e.name) !== undefined + .find( + (o) => + o.name === e.group && + o.type === SlashCommandOptionType.SUB_COMMAND_GROUP + ) + ?.options?.find((o) => o.name === e.name) !== undefined : true const subMatched = e.group === undefined && e.parent !== undefined ? i.options.find( - (o) => - o.name === e.name && - o.type === SlashCommandOptionType.SUB_COMMAND - ) !== undefined + (o) => + o.name === e.name && + o.type === SlashCommandOptionType.SUB_COMMAND + ) !== undefined : true const nameMatched1 = e.name === i.name const parentMatched = hasGroupOrParent ? e.parent === i.name : true @@ -549,11 +549,11 @@ export class SlashClient { async verifyServerRequest(req: { headers: Headers method: string - body: Deno.Reader + body: Deno.Reader | Uint8Array, respond: (options: { status?: number headers?: Headers - body?: string | Uint8Array + body?: string | Uint8Array | FormData }) => Promise }): Promise { if (req.method.toLowerCase() !== 'post') return false @@ -562,7 +562,7 @@ export class SlashClient { const timestamp = req.headers.get('x-signature-timestamp') if (signature === null || timestamp === null) return false - const rawbody = await Deno.readAll(req.body) + const rawbody = req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body) const verify = await this.verifyKey(rawbody, signature, timestamp) if (!verify) return false @@ -584,13 +584,13 @@ export class SlashClient { channels: {} } }) - res._httpRespond = async (d: InteractionResponsePayload) => + res._httpRespond = async (d: InteractionResponsePayload | FormData) => await req.respond({ status: 200, headers: new Headers({ - 'content-type': 'application/json' + 'content-type': d instanceof FormData ? 'multipart/form-data' : 'application/json' }), - body: JSON.stringify(d) + body: d instanceof FormData ? d : JSON.stringify(d) }) return res @@ -599,6 +599,26 @@ export class SlashClient { } } + /** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */ + async verifyFetchEvent({ request: req, respondWith }: { respondWith: CallableFunction, request: Request }): Promise { + if (req.bodyUsed === true) throw new Error('Request Body already used') + if (req.body === null) return false + const body = (await req.body.getReader().read()).value + if (body === undefined) return false + + return await this.verifyServerRequest({ + headers: req.headers, + body, + method: req.method, + respond: async (options) => { + await respondWith(new Response(options.body, { + headers: options.headers, + status: options.status, + })) + }, + }) + } + async verifyOpineRequest(req: any): Promise { const signature = req.headers.get('x-signature-ed25519') const timestamp = req.headers.get('x-signature-timestamp') From b775d3e323032bfeceb0e7ed48d5a535bd237c2d Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 29 Mar 2021 05:41:21 -0700 Subject: [PATCH 17/72] x --- src/structures/slash.ts | 12 +++++++----- src/types/slash.ts | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index d7681e5..70c5c96 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -112,6 +112,7 @@ export class Interaction extends SnowflakeBase { deferred: boolean = false _httpRespond?: (d: InteractionResponsePayload) => unknown _httpResponded?: boolean + applicationID: string constructor( client: Client, @@ -129,6 +130,7 @@ export class Interaction extends SnowflakeBase { this.token = data.token this.member = others.member this.id = data.id + this.applicationID = data.application_id this.user = others.user this.data = data.data this.guild = others.guild @@ -257,7 +259,7 @@ export class Interaction extends SnowflakeBase { allowedMentions?: AllowedMentionsPayload }): Promise { const url = WEBHOOK_MESSAGE( - this.client.user?.id as string, + this.applicationID, this.token, '@original' ) @@ -276,7 +278,7 @@ export class Interaction extends SnowflakeBase { /** Delete the original Interaction Response */ async deleteResponse(): Promise { const url = WEBHOOK_MESSAGE( - this.client.user?.id as string, + this.applicationID, this.token, '@original' ) @@ -285,7 +287,7 @@ export class Interaction extends SnowflakeBase { } get url(): string { - return `https://discord.com/api/v8/webhooks/${this.client.user?.id}/${this.token}` + return `https://discord.com/api/v8/webhooks/${this.applicationID}/${this.token}` } /** Send a followup message */ @@ -367,7 +369,7 @@ export class Interaction extends SnowflakeBase { ): Promise { await this.client.rest.patch( WEBHOOK_MESSAGE( - this.client.user?.id as string, + this.applicationID, this.token ?? this.client.token, typeof msg === 'string' ? msg : msg.id ), @@ -380,7 +382,7 @@ export class Interaction extends SnowflakeBase { async deleteMessage(msg: Message | string): Promise { await this.client.rest.delete( WEBHOOK_MESSAGE( - this.client.user?.id as string, + this.applicationID, this.token ?? this.client.token, typeof msg === 'string' ? msg : msg.id ) diff --git a/src/types/slash.ts b/src/types/slash.ts index c700fbd..faf6221 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -75,6 +75,7 @@ export interface InteractionPayload { guild_id?: string /** ID of the Channel in which Interaction was invoked */ channel_id?: string + application_id: string } export interface SlashCommandChoice { From 4462d7d8324a9a4a17236527d582196633a1858f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 29 Mar 2021 05:43:50 -0700 Subject: [PATCH 18/72] x --- src/structures/slash.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 70c5c96..0891f4a 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -45,13 +45,13 @@ export interface InteractionMessageOptions { tts?: boolean flags?: number | InteractionResponseFlags[] allowedMentions?: AllowedMentionsPayload + /** Whether the Message Response should be Ephemeral (only visible to User) or not */ + ephemeral?: boolean } export interface InteractionResponse extends InteractionMessageOptions { /** Type of Interaction Response */ type?: InteractionResponseType - /** Whether the Message Response should be Ephemeral (only visible to User) or not */ - ephemeral?: boolean } /** Represents a Channel Object for an Option in Slash Command */ @@ -239,7 +239,7 @@ export class Interaction extends SnowflakeBase { content: options.content, embeds: options.embeds, flags: options.flags, - allowedMentions: options.allowedMentions + allowedMentions: options.allowedMentions, }) } else await this.respond( From 62b2aa07dee0668dc318b3729446622830912acf Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 29 Mar 2021 19:41:50 +0530 Subject: [PATCH 19/72] remove redis dep from deps.ts and move to redisCache.ts --- deps.ts | 5 -- mod.ts | 1 + src/models/cacheAdapter.ts | 104 ------------------------------------ src/models/redisCache.ts | 105 +++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 109 deletions(-) create mode 100644 src/models/redisCache.ts diff --git a/deps.ts b/deps.ts index 91097fc..4b47aa2 100644 --- a/deps.ts +++ b/deps.ts @@ -2,11 +2,6 @@ export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/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 { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts' export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' diff --git a/mod.ts b/mod.ts index cdbf8ec..b99a120 100644 --- a/mod.ts +++ b/mod.ts @@ -166,3 +166,4 @@ export type { VoiceStatePayload } from './src/types/voice.ts' export type { WebhookPayload } from './src/types/webhook.ts' export * from './src/models/collectors.ts' export type { Dict } from './src/utils/dict.ts' +export * from './src/models/redisCache.ts' diff --git a/src/models/cacheAdapter.ts b/src/models/cacheAdapter.ts index b9f5193..a04c149 100644 --- a/src/models/cacheAdapter.ts +++ b/src/models/cacheAdapter.ts @@ -1,5 +1,4 @@ import { Collection } from '../utils/collection.ts' -import { connect, Redis, RedisConnectOptions } from '../../deps.ts' /** * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. @@ -71,106 +70,3 @@ export class DefaultCacheAdapter implements ICacheAdapter { return delete this.data[cacheName] } } - -/** Redis Cache Adapter for using Redis as a cache-provider. */ -export class RedisCacheAdapter implements ICacheAdapter { - _redis: Promise - redis?: Redis - ready: boolean = false - readonly _expireIntervalTimer: number = 5000 - private _expireInterval?: number - - constructor(options: RedisConnectOptions) { - this._redis = connect(options) - this._redis.then( - (redis) => { - this.redis = redis - this.ready = true - this._startExpireInterval() - }, - () => { - // TODO: Make error for this - } - ) - } - - private _startExpireInterval(): void { - this._expireInterval = setInterval(() => { - this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { - for (const name of names) { - this.redis?.hvals(name).then((vals) => { - for (const val of vals) { - const expireVal: { - name: string - key: string - at: number - } = JSON.parse(val) - const expired = new Date().getTime() > expireVal.at - if (expired) this.redis?.hdel(expireVal.name, expireVal.key) - } - }) - } - }) - }, this._expireIntervalTimer) - } - - async _checkReady(): Promise { - if (!this.ready) await this._redis - } - - async get(cacheName: string, key: string): Promise { - await this._checkReady() - const cache = await this.redis?.hget(cacheName, key) - if (cache === undefined) return - try { - return JSON.parse(cache) - } catch (e) { - return cache - } - } - - async set( - cacheName: string, - key: string, - value: any, - expire?: number - ): Promise { - await this._checkReady() - const result = await this.redis?.hset( - cacheName, - key, - typeof value === 'object' ? JSON.stringify(value) : value - ) - if (expire !== undefined) { - await this.redis?.hset( - `${cacheName}:expires`, - key, - JSON.stringify({ - name: cacheName, - key, - at: new Date().getTime() + expire - }) - ) - } - return result - } - - async delete(cacheName: string, key: string): Promise { - await this._checkReady() - const exists = await this.redis?.hexists(cacheName, key) - if (exists === 0) return false - await this.redis?.hdel(cacheName, key) - return true - } - - async array(cacheName: string): Promise { - await this._checkReady() - const data = await this.redis?.hvals(cacheName) - return data?.map((e: string) => JSON.parse(e)) - } - - async deleteCache(cacheName: string): Promise { - await this._checkReady() - return (await this.redis?.del(cacheName)) !== 0 - } -} diff --git a/src/models/redisCache.ts b/src/models/redisCache.ts new file mode 100644 index 0000000..0820213 --- /dev/null +++ b/src/models/redisCache.ts @@ -0,0 +1,105 @@ +import { ICacheAdapter } from './cacheAdapter.ts' +import { connect, Redis, RedisConnectOptions } from 'https://deno.land/x/redis@v0.14.1/mod.ts' + +/** Redis Cache Adapter for using Redis as a cache-provider. */ +export class RedisCacheAdapter implements ICacheAdapter { + _redis: Promise + redis?: Redis + ready: boolean = false + readonly _expireIntervalTimer: number = 5000 + private _expireInterval?: number + + constructor(options: RedisConnectOptions) { + this._redis = connect(options) + this._redis.then( + (redis) => { + this.redis = redis + this.ready = true + this._startExpireInterval() + }, + () => { + // TODO: Make error for this + } + ) + } + + private _startExpireInterval(): void { + this._expireInterval = setInterval(() => { + this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { + for (const name of names) { + this.redis?.hvals(name).then((vals) => { + for (const val of vals) { + const expireVal: { + name: string + key: string + at: number + } = JSON.parse(val) + const expired = new Date().getTime() > expireVal.at + if (expired) this.redis?.hdel(expireVal.name, expireVal.key) + } + }) + } + }) + }, this._expireIntervalTimer) + } + + async _checkReady(): Promise { + if (!this.ready) await this._redis + } + + async get(cacheName: string, key: string): Promise { + await this._checkReady() + const cache = await this.redis?.hget(cacheName, key) + if (cache === undefined) return + try { + return JSON.parse(cache) + } catch (e) { + return cache + } + } + + async set( + cacheName: string, + key: string, + value: any, + expire?: number + ): Promise { + await this._checkReady() + const result = await this.redis?.hset( + cacheName, + key, + typeof value === 'object' ? JSON.stringify(value) : value + ) + if (expire !== undefined) { + await this.redis?.hset( + `${cacheName}:expires`, + key, + JSON.stringify({ + name: cacheName, + key, + at: new Date().getTime() + expire + }) + ) + } + return result + } + + async delete(cacheName: string, key: string): Promise { + await this._checkReady() + const exists = await this.redis?.hexists(cacheName, key) + if (exists === 0) return false + await this.redis?.hdel(cacheName, key) + return true + } + + async array(cacheName: string): Promise { + await this._checkReady() + const data = await this.redis?.hvals(cacheName) + return data?.map((e: string) => JSON.parse(e)) + } + + async deleteCache(cacheName: string): Promise { + await this._checkReady() + return (await this.redis?.del(cacheName)) !== 0 + } +} \ No newline at end of file From 768fec7195de630d9331e2bfdc2203b6d8717071 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 15:21:29 +0530 Subject: [PATCH 20/72] deploy built in support --- deploy.ts | 87 +++++++++++++++++++++++++++++++++++++++ src/models/client.ts | 67 +++--------------------------- src/models/slashClient.ts | 81 ++++++++++++++++++++++++++++++++---- 3 files changed, 166 insertions(+), 69 deletions(-) create mode 100644 deploy.ts diff --git a/deploy.ts b/deploy.ts new file mode 100644 index 0000000..536e17b --- /dev/null +++ b/deploy.ts @@ -0,0 +1,87 @@ +import { + SlashCommandsManager, + SlashClient, + SlashCommandHandlerCallback +} from './src/models/slashClient.ts' +import { InteractionResponseType, InteractionType } from './src/types/slash.ts' + +export interface DeploySlashInitOptions { + env?: boolean + publicKey?: string + token?: string + id?: string +} + +let client: SlashClient +let commands: SlashCommandsManager + +export function init(options: DeploySlashInitOptions): void { + if (client !== undefined) throw new Error('Already initialized') + if (options.env === true) { + options.publicKey = Deno.env.get('PUBLIC_KEY') + options.token = Deno.env.get('TOKEN') + options.id = Deno.env.get('ID') + } + + if (options.publicKey === undefined) + throw new Error('Public Key not provided') + + client = new SlashClient({ + id: options.id, + token: options.token, + publicKey: options.publicKey + }) + + commands = client.commands + + const cb = async (evt: { + respondWith: CallableFunction + request: Request + }): Promise => { + try { + const d = await client.verifyFetchEvent(evt) + if (d === false) { + await evt.respondWith( + new Response(null, { + status: 400 + }) + ) + return + } + + if (d.type === InteractionType.PING) { + await d.respond({ type: InteractionResponseType.PONG }) + return + } + + await (client as any)._process(d) + } catch (e) { + await client.emit('interactionError', e) + } + } + + addEventListener('fetch', cb as any) +} + +export function handle( + cmd: + | string + | { + name: string + parent?: string + group?: string + guild?: string + }, + handler: SlashCommandHandlerCallback +): void { + client.handle({ + name: typeof cmd === 'string' ? cmd : cmd.name, + handler, + ...(typeof cmd === 'string' ? {} : cmd) + }) +} + +export { commands, client } +export * from './src/types/slash.ts' +export * from './src/structures/slash.ts' +export * from './src/models/slashClient.ts' diff --git a/src/models/client.ts b/src/models/client.ts index 83846cd..dc40e24 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -13,7 +13,6 @@ import { ActivityGame, ClientActivity } from '../types/presence.ts' import { Extension } from './extensions.ts' import { SlashClient } from './slashClient.ts' import { Interaction } from '../structures/slash.ts' -import { SlashModule } from './slashModule.ts' import { ShardManager } from './shard.ts' import { Application } from '../structures/application.ts' import { Invite } from '../structures/invite.ts' @@ -190,10 +189,10 @@ export class Client extends HarmonyEventEmitter { this.clientProperties = options.clientProperties === undefined ? { - os: Deno.build.os, - browser: 'harmony', - device: 'harmony' - } + os: Deno.build.os, + browser: 'harmony', + device: 'harmony' + } : options.clientProperties if (options.shard !== undefined) this.shard = options.shard @@ -208,7 +207,7 @@ export class Client extends HarmonyEventEmitter { this.token = token this.debug('Info', 'Found token in ENV') } - } catch (e) {} + } catch (e) { } } const restOptions: RESTOptions = { @@ -436,59 +435,3 @@ export function event(name?: keyof ClientEvents) { client._decoratedEvents[key] = listener } } - -/** Decorator to create a Slash Command handler */ -export function slash(name?: string, guild?: string) { - return function (client: Client | SlashClient | SlashModule, prop: string) { - if (client._decoratedSlash === undefined) client._decoratedSlash = [] - const item = (client as { [name: string]: any })[prop] - if (typeof item !== 'function') { - throw new Error('@slash decorator requires a function') - } else - client._decoratedSlash.push({ - name: name ?? prop, - guild, - handler: item - }) - } -} - -/** Decorator to create a Sub-Slash Command handler */ -export function subslash(parent: string, name?: string, guild?: 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') { - throw new Error('@subslash decorator requires a function') - } else - client._decoratedSlash.push({ - parent, - name: name ?? prop, - guild, - handler: item - }) - } -} - -/** Decorator to create a Grouped Slash Command handler */ -export function groupslash( - parent: string, - group: string, - name?: string, - guild?: 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') { - throw new Error('@groupslash decorator requires a function') - } else - client._decoratedSlash.push({ - group, - parent, - name: name ?? prop, - guild, - handler: item - }) - } -} diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 0dac52c..f4dbec1 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,4 +1,4 @@ -import { Guild } from '../structures/guild.ts' +import type { Guild } from '../structures/guild.ts' import { Interaction, InteractionApplicationCommandResolved @@ -14,11 +14,12 @@ import { SlashCommandPayload } from '../types/slash.ts' import { Collection } from '../utils/collection.ts' -import { Client } from './client.ts' +import type { Client } from './client.ts' import { RESTManager } from './rest.ts' import { SlashModule } from './slashModule.ts' import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' import { User } from '../structures/user.ts' +import { HarmonyEventEmitter } from "../utils/events.ts" export class SlashCommand { slash: SlashCommandsManager @@ -380,8 +381,14 @@ export interface SlashOptions { const encoder = new TextEncoder() const decoder = new TextDecoder('utf-8') +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SlashClientEvents = { + interaction: [Interaction] + interactionError: [Error] +} + /** Slash Client represents an Interactions Client which can be used without Harmony Client. */ -export class SlashClient { +export class SlashClient extends HarmonyEventEmitter { id: string | (() => string) client?: Client token?: string @@ -401,6 +408,7 @@ export class SlashClient { }> constructor(options: SlashOptions) { + super() let id = options.id if (options.token !== undefined) id = atob(options.token?.split('.')[0]) if (id === undefined) @@ -435,8 +443,8 @@ export class SlashClient { : options.rest : options.client.rest - this.client?.on('interactionCreate', (interaction) => - this._process(interaction) + this.client?.on('interactionCreate', async (interaction) => + await this._process(interaction) ) this.commands = new SlashCommandsManager(this) @@ -506,7 +514,7 @@ export class SlashClient { } /** Process an incoming Interaction */ - private _process(interaction: Interaction): void { + private async _process(interaction: Interaction): Promise { if (!this.enabled) return if ( @@ -523,7 +531,10 @@ export class SlashClient { if (cmd === undefined) return - cmd.handler(interaction) + await this.emit('interaction', interaction) + try { await cmd.handler(interaction) } catch (e) { + await this.emit('interactionError', e) + } } /** Verify HTTP based Interaction */ @@ -672,3 +683,59 @@ export class SlashClient { return true } } + +/** Decorator to create a Slash Command handler */ +export function slash(name?: string, guild?: string) { + return function (client: Client | SlashClient | SlashModule, prop: string) { + if (client._decoratedSlash === undefined) client._decoratedSlash = [] + const item = (client as { [name: string]: any })[prop] + if (typeof item !== 'function') { + throw new Error('@slash decorator requires a function') + } else + client._decoratedSlash.push({ + name: name ?? prop, + guild, + handler: item + }) + } +} + +/** Decorator to create a Sub-Slash Command handler */ +export function subslash(parent: string, name?: string, guild?: 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') { + throw new Error('@subslash decorator requires a function') + } else + client._decoratedSlash.push({ + parent, + name: name ?? prop, + guild, + handler: item + }) + } +} + +/** Decorator to create a Grouped Slash Command handler */ +export function groupslash( + parent: string, + group: string, + name?: string, + guild?: 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') { + throw new Error('@groupslash decorator requires a function') + } else + client._decoratedSlash.push({ + group, + parent, + name: name ?? prop, + guild, + handler: item + }) + } +} From a89318c3c3951cd612381490a687bb7827d47cef Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 15:31:50 +0530 Subject: [PATCH 21/72] x --- deploy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy.ts b/deploy.ts index 536e17b..68f9d2d 100644 --- a/deploy.ts +++ b/deploy.ts @@ -41,6 +41,7 @@ export function init(options: DeploySlashInitOptions): void { try { const d = await client.verifyFetchEvent(evt) if (d === false) { + console.log('not authorized') await evt.respondWith( new Response(null, { status: 400 @@ -50,12 +51,15 @@ export function init(options: DeploySlashInitOptions): void { } if (d.type === InteractionType.PING) { + console.log('ping pong') await d.respond({ type: InteractionResponseType.PONG }) return } + console.log('slash command', d.name) await (client as any)._process(d) } catch (e) { + console.log(e) await client.emit('interactionError', e) } } From 218e3e7ddf16b95735283c98d99634b9f097f7a5 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 15:33:43 +0530 Subject: [PATCH 22/72] x --- deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.ts b/deploy.ts index 68f9d2d..2310096 100644 --- a/deploy.ts +++ b/deploy.ts @@ -43,7 +43,7 @@ export function init(options: DeploySlashInitOptions): void { if (d === false) { console.log('not authorized') await evt.respondWith( - new Response(null, { + new Response('not authorized', { status: 400 }) ) From 68cf1105c1ec6541faf3e12121d8edee97f029c7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 15:35:52 +0530 Subject: [PATCH 23/72] x --- deploy.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/deploy.ts b/deploy.ts index 2310096..f5cd9e2 100644 --- a/deploy.ts +++ b/deploy.ts @@ -41,9 +41,8 @@ export function init(options: DeploySlashInitOptions): void { try { const d = await client.verifyFetchEvent(evt) if (d === false) { - console.log('not authorized') await evt.respondWith( - new Response('not authorized', { + new Response('Not Authorized', { status: 400 }) ) @@ -51,12 +50,10 @@ export function init(options: DeploySlashInitOptions): void { } if (d.type === InteractionType.PING) { - console.log('ping pong') await d.respond({ type: InteractionResponseType.PONG }) return } - console.log('slash command', d.name) await (client as any)._process(d) } catch (e) { console.log(e) From 75620ee7ea88f8e4ca6ceece7f29c8f35e420722 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 15:37:13 +0530 Subject: [PATCH 24/72] x --- deploy.ts | 6 +++++- src/models/slashClient.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy.ts b/deploy.ts index f5cd9e2..a38026a 100644 --- a/deploy.ts +++ b/deploy.ts @@ -39,7 +39,10 @@ export function init(options: DeploySlashInitOptions): void { request: Request }): Promise => { try { - const d = await client.verifyFetchEvent(evt) + const d = await client.verifyFetchEvent({ + respondWith: (...args: any[]) => evt.respondWith(...args), + request: evt.request, + }) if (d === false) { await evt.respondWith( new Response('Not Authorized', { @@ -51,6 +54,7 @@ export function init(options: DeploySlashInitOptions): void { if (d.type === InteractionType.PING) { await d.respond({ type: InteractionResponseType.PONG }) + client.emit('ping') return } diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index f4dbec1..acc38c7 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -385,6 +385,7 @@ const decoder = new TextDecoder('utf-8') export type SlashClientEvents = { interaction: [Interaction] interactionError: [Error] + ping: [] } /** Slash Client represents an Interactions Client which can be used without Harmony Client. */ From 95145c1bc21e2fd1ebe93a81fb70f130beb31f27 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 17:22:13 +0530 Subject: [PATCH 25/72] support for root, group and sub command parsing from name --- deploy.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/deploy.ts b/deploy.ts index a38026a..a831b83 100644 --- a/deploy.ts +++ b/deploy.ts @@ -79,11 +79,25 @@ export function handle( }, handler: SlashCommandHandlerCallback ): void { - client.handle({ + const handle = { name: typeof cmd === 'string' ? cmd : cmd.name, handler, ...(typeof cmd === 'string' ? {} : cmd) - }) + } + + if (typeof handle.name === 'string' && handle.name.includes(' ') && handle.parent === undefined && handle.group === undefined) { + const parts = handle.name.split(/ +/).filter(e => e !== '') + if (parts.length > 3 || parts.length < 1) throw new Error('Invalid command name') + const root = parts.shift() as string + const group = parts.length === 3 ? parts.shift() : undefined + const sub = parts.shift() + + handle.name = sub ?? root + handle.group = group + handle.parent = sub === undefined ? undefined : root + } + + client.handle(handle) } export { commands, client } From 82431168d319fa4e406f05ec097bf408e840ce9b Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 30 Mar 2021 17:45:38 +0530 Subject: [PATCH 26/72] fix --- deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.ts b/deploy.ts index a831b83..e5d149b 100644 --- a/deploy.ts +++ b/deploy.ts @@ -89,7 +89,7 @@ export function handle( const parts = handle.name.split(/ +/).filter(e => e !== '') if (parts.length > 3 || parts.length < 1) throw new Error('Invalid command name') const root = parts.shift() as string - const group = parts.length === 3 ? parts.shift() : undefined + const group = parts.length === 2 ? parts.shift() : undefined const sub = parts.shift() handle.name = sub ?? root From 8bf2c1e99d245c68998b3969a9a82f9a65592812 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 4 Apr 2021 10:11:00 +0530 Subject: [PATCH 27/72] required changes --- src/structures/slash.ts | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 0891f4a..0f61cc2 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -177,15 +177,15 @@ export class Interaction extends SnowflakeBase { type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: data.type === undefined || - data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || - data.type === InteractionResponseType.CHANNEL_MESSAGE + data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || + data.type === InteractionResponseType.CHANNEL_MESSAGE ? { - content: data.content ?? '', - embeds: data.embeds, - tts: data.tts ?? false, - flags, - allowed_mentions: data.allowedMentions ?? undefined - } + content: data.content ?? '', + embeds: data.embeds, + tts: data.tts ?? false, + flags, + allowed_mentions: data.allowedMentions ?? undefined + } : undefined } @@ -239,7 +239,7 @@ export class Interaction extends SnowflakeBase { content: options.content, embeds: options.embeds, flags: options.flags, - allowedMentions: options.allowedMentions, + allowedMentions: options.allowedMentions }) } else await this.respond( @@ -258,11 +258,7 @@ export class Interaction extends SnowflakeBase { flags?: number | number[] allowedMentions?: AllowedMentionsPayload }): Promise { - const url = WEBHOOK_MESSAGE( - this.applicationID, - this.token, - '@original' - ) + const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') await this.client.rest.patch(url, { content: data.content ?? '', embeds: data.embeds ?? [], @@ -277,11 +273,7 @@ export class Interaction extends SnowflakeBase { /** Delete the original Interaction Response */ async deleteResponse(): Promise { - const url = WEBHOOK_MESSAGE( - this.applicationID, - this.token, - '@original' - ) + const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') await this.client.rest.delete(url) return this } @@ -315,8 +307,8 @@ export class Interaction extends SnowflakeBase { (option as WebhookMessageOptions)?.embed !== undefined ? [(option as WebhookMessageOptions).embed] : (option as WebhookMessageOptions)?.embeds !== undefined - ? (option as WebhookMessageOptions).embeds - : undefined, + ? (option as WebhookMessageOptions).embeds + : undefined, file: (option as WebhookMessageOptions)?.file, files: (option as WebhookMessageOptions)?.files, tts: (option as WebhookMessageOptions)?.tts, @@ -357,7 +349,7 @@ export class Interaction extends SnowflakeBase { msg: Message | string, data: { content?: string - embeds?: Embed[] + embeds?: Array file?: any allowed_mentions?: { parse?: string From 38b11f40764dd793f9f979450dfa5cd4420421e9 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 4 Apr 2021 10:14:40 +0530 Subject: [PATCH 28/72] fix --- src/structures/slash.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 0f61cc2..42d1e64 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -31,7 +31,7 @@ import { TextChannel } from './textChannel.ts' import { User } from './user.ts' interface WebhookMessageOptions extends MessageOptions { - embeds?: Embed[] + embeds?: Array name?: string avatar?: string } @@ -41,7 +41,7 @@ type AllWebhookMessageOptions = string | WebhookMessageOptions /** Interaction Message related Options */ export interface InteractionMessageOptions { content?: string - embeds?: EmbedPayload[] + embeds?: Array tts?: boolean flags?: number | InteractionResponseFlags[] allowedMentions?: AllowedMentionsPayload @@ -254,7 +254,7 @@ export class Interaction extends SnowflakeBase { /** Edit the original Interaction response */ async editResponse(data: { content?: string - embeds?: EmbedPayload[] + embeds?: Array flags?: number | number[] allowedMentions?: AllowedMentionsPayload }): Promise { From 2b46b389085748f4d9fb443dcda8d38cdab43530 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 4 Apr 2021 10:21:39 +0530 Subject: [PATCH 29/72] encoding util --- src/gateway/index.ts | 13 ++-- src/models/slashClient.ts | 131 ++++++++++++++++++++------------------ src/structures/message.ts | 5 +- src/utils/encoding.ts | 10 +++ 4 files changed, 91 insertions(+), 68 deletions(-) create mode 100644 src/utils/encoding.ts diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 1d672af..f80dfa5 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -18,6 +18,7 @@ import { delay } from '../utils/delay.ts' import { VoiceChannel } from '../structures/guildVoiceChannel.ts' import { Guild } from '../structures/guild.ts' import { HarmonyEventEmitter } from '../utils/events.ts' +import { decodeText } from '../utils/encoding.ts' export interface RequestMembersOptions { limit?: number @@ -89,7 +90,7 @@ export class Gateway extends HarmonyEventEmitter { } if (data instanceof Uint8Array) { data = unzlib(data) - data = new TextDecoder('utf-8').decode(data) + data = decodeText(data) } const { op, d, s, t }: GatewayResponse = JSON.parse(data) @@ -385,8 +386,8 @@ export class Gateway extends HarmonyEventEmitter { channel === undefined ? null : typeof channel === 'string' - ? channel - : channel?.id, + ? channel + : channel?.id, self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute, self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf } @@ -427,7 +428,11 @@ export class Gateway extends HarmonyEventEmitter { } close(code: number = 1000, reason?: string): void { - this.debug(`Closing with code ${code}${reason !== undefined && reason !== '' ? ` and reason ${reason}` : ''}`) + this.debug( + `Closing with code ${code}${ + reason !== undefined && reason !== '' ? ` and reason ${reason}` : '' + }` + ) return this.websocket?.close(code, reason) } diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index acc38c7..0ff8c42 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -19,7 +19,8 @@ import { RESTManager } from './rest.ts' import { SlashModule } from './slashModule.ts' import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' import { User } from '../structures/user.ts' -import { HarmonyEventEmitter } from "../utils/events.ts" +import { HarmonyEventEmitter } from '../utils/events.ts' +import { encodeText, decodeText } from '../utils/encoding.ts' export class SlashCommand { slash: SlashCommandsManager @@ -94,8 +95,8 @@ function createSlashOption( data.choices === undefined ? undefined : data.choices.map((e) => - typeof e === 'string' ? { name: e, value: e } : e - ) + typeof e === 'string' ? { name: e, value: e } : e + ) } } @@ -139,15 +140,15 @@ export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption export type SlashBuilderOptionsData = | Array | { - [name: string]: - | { - description: string - type: SlashCommandOptionType - options?: SlashCommandOption[] - choices?: SlashCommandChoice[] + [name: string]: + | { + description: string + type: SlashCommandOptionType + options?: SlashCommandOption[] + choices?: SlashCommandChoice[] + } + | SlashOptionCallable } - | SlashOptionCallable - } function buildOptionsArray( options: SlashBuilderOptionsData @@ -155,10 +156,10 @@ function buildOptionsArray( 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] }) - ) + typeof entry[1] === 'function' + ? entry[1](SlashOption) + : Object.assign(entry[1], { name: entry[0] }) + ) } /** Slash Command Builder */ @@ -268,8 +269,8 @@ export class SlashCommandsManager { guild === undefined ? this.rest.api.applications[this.slash.getID()].commands : this.rest.api.applications[this.slash.getID()].guilds[ - typeof guild === 'string' ? guild : guild.id - ].commands + typeof guild === 'string' ? guild : guild.id + ].commands const payload = await route.post(data) @@ -277,8 +278,8 @@ export class SlashCommandsManager { typeof guild === 'object' ? guild : guild === undefined - ? undefined - : await this.slash.client?.guilds.get(guild) + ? undefined + : await this.slash.client?.guilds.get(guild) const cmd = new SlashCommand(this, payload, _guild) cmd._guild = @@ -297,8 +298,8 @@ export class SlashCommandsManager { guild === undefined ? 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] + typeof guild === 'string' ? guild : guild.id + ].commands[id] await route.patch(data) return this @@ -313,8 +314,8 @@ export class SlashCommandsManager { guild === undefined ? 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] + typeof guild === 'string' ? guild : guild.id + ].commands[id] await route.delete() return this @@ -326,8 +327,8 @@ export class SlashCommandsManager { guild === undefined ? 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] + typeof guild === 'string' ? guild : guild.id + ].commands[id] const data = await route.get() @@ -335,8 +336,8 @@ export class SlashCommandsManager { typeof guild === 'object' ? guild : guild === undefined - ? undefined - : await this.slash.client?.guilds.get(guild) + ? undefined + : await this.slash.client?.guilds.get(guild) return new SlashCommand(this, data, _guild) } @@ -350,8 +351,8 @@ export class SlashCommandsManager { guild === undefined ? this.rest.api.applications[this.slash.getID()].commands : this.rest.api.applications[this.slash.getID()].guilds[ - typeof guild === 'string' ? guild : guild.id - ].commands + typeof guild === 'string' ? guild : guild.id + ].commands await route.put(cmds) @@ -378,9 +379,6 @@ export interface SlashOptions { publicKey?: string } -const encoder = new TextEncoder() -const decoder = new TextDecoder('utf-8') - // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type SlashClientEvents = { interaction: [Interaction] @@ -439,13 +437,14 @@ export class SlashClient extends HarmonyEventEmitter { options.client === undefined ? options.rest === undefined ? new RESTManager({ - token: this.token - }) + token: this.token + }) : options.rest : options.client.rest - this.client?.on('interactionCreate', async (interaction) => - await this._process(interaction) + this.client?.on( + 'interactionCreate', + async (interaction) => await this._process(interaction) ) this.commands = new SlashCommandsManager(this) @@ -490,20 +489,20 @@ export class SlashClient extends HarmonyEventEmitter { const groupMatched = e.group !== undefined && e.parent !== undefined ? i.options - .find( - (o) => - o.name === e.group && - o.type === SlashCommandOptionType.SUB_COMMAND_GROUP - ) - ?.options?.find((o) => o.name === e.name) !== undefined + .find( + (o) => + o.name === e.group && + o.type === SlashCommandOptionType.SUB_COMMAND_GROUP + ) + ?.options?.find((o) => o.name === e.name) !== undefined : true const subMatched = e.group === undefined && e.parent !== undefined ? i.options.find( - (o) => - o.name === e.name && - o.type === SlashCommandOptionType.SUB_COMMAND - ) !== undefined + (o) => + o.name === e.name && + o.type === SlashCommandOptionType.SUB_COMMAND + ) !== undefined : true const nameMatched1 = e.name === i.name const parentMatched = hasGroupOrParent ? e.parent === i.name : true @@ -533,7 +532,9 @@ export class SlashClient extends HarmonyEventEmitter { if (cmd === undefined) return await this.emit('interaction', interaction) - try { await cmd.handler(interaction) } catch (e) { + try { + await cmd.handler(interaction) + } catch (e) { await this.emit('interactionError', e) } } @@ -548,10 +549,8 @@ export class SlashClient extends HarmonyEventEmitter { throw new Error('Public Key is not present') const fullBody = new Uint8Array([ - ...(typeof timestamp === 'string' - ? encoder.encode(timestamp) - : timestamp), - ...(typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody) + ...(typeof timestamp === 'string' ? encodeText(timestamp) : timestamp), + ...(typeof rawBody === 'string' ? encodeText(rawBody) : rawBody) ]) return edverify(signature, fullBody, this.publicKey).catch(() => false) @@ -561,7 +560,7 @@ export class SlashClient extends HarmonyEventEmitter { async verifyServerRequest(req: { headers: Headers method: string - body: Deno.Reader | Uint8Array, + body: Deno.Reader | Uint8Array respond: (options: { status?: number headers?: Headers @@ -574,12 +573,13 @@ export class SlashClient extends HarmonyEventEmitter { const timestamp = req.headers.get('x-signature-timestamp') if (signature === null || timestamp === null) return false - const rawbody = req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body) + const rawbody = + req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body) const verify = await this.verifyKey(rawbody, signature, timestamp) if (!verify) return false try { - const payload: InteractionPayload = JSON.parse(decoder.decode(rawbody)) + const payload: InteractionPayload = JSON.parse(decodeText(rawbody)) // TODO: Maybe fix all this hackery going on here? const res = new Interaction(this as any, payload, { @@ -600,7 +600,8 @@ export class SlashClient extends HarmonyEventEmitter { await req.respond({ status: 200, headers: new Headers({ - 'content-type': d instanceof FormData ? 'multipart/form-data' : 'application/json' + 'content-type': + d instanceof FormData ? 'multipart/form-data' : 'application/json' }), body: d instanceof FormData ? d : JSON.stringify(d) }) @@ -612,7 +613,13 @@ export class SlashClient extends HarmonyEventEmitter { } /** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */ - async verifyFetchEvent({ request: req, respondWith }: { respondWith: CallableFunction, request: Request }): Promise { + async verifyFetchEvent({ + request: req, + respondWith + }: { + respondWith: CallableFunction + request: Request + }): Promise { if (req.bodyUsed === true) throw new Error('Request Body already used') if (req.body === null) return false const body = (await req.body.getReader().read()).value @@ -623,11 +630,13 @@ export class SlashClient extends HarmonyEventEmitter { body, method: req.method, respond: async (options) => { - await respondWith(new Response(options.body, { - headers: options.headers, - status: options.status, - })) - }, + await respondWith( + new Response(options.body, { + headers: options.headers, + status: options.status + }) + ) + } }) } diff --git a/src/structures/message.ts b/src/structures/message.ts index c0cbb9e..92b4bbe 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -21,6 +21,7 @@ import { MessageReactionsManager } from '../managers/messageReactions.ts' import { MessageSticker } from './messageSticker.ts' import { Emoji } from './emoji.ts' import { InteractionType } from '../types/slash.ts' +import { encodeText } from '../utils/encoding.ts' type AllMessageOptions = MessageOptions | Embed @@ -217,8 +218,6 @@ export class Message extends SnowflakeBase { } } -const encoder = new TextEncoder() - /** Message Attachment that can be sent while Creating Message */ export class MessageAttachment { name: string @@ -228,7 +227,7 @@ export class MessageAttachment { this.name = name this.blob = typeof blob === 'string' - ? new Blob([encoder.encode(blob)]) + ? new Blob([encodeText(blob)]) : blob instanceof Uint8Array ? new Blob([blob]) : blob diff --git a/src/utils/encoding.ts b/src/utils/encoding.ts new file mode 100644 index 0000000..04ce231 --- /dev/null +++ b/src/utils/encoding.ts @@ -0,0 +1,10 @@ +const encoder = new TextEncoder() +const decoder = new TextDecoder('utf-8') + +export function encodeText(str: string): Uint8Array { + return encoder.encode(str) +} + +export function decodeText(bytes: Uint8Array): string { + return decoder.decode(bytes) +} From 22e041f4402d8f20f2732e5a61711d30120c56db Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 4 Apr 2021 11:12:15 +0530 Subject: [PATCH 30/72] refactor --- deploy.ts | 28 +- mod.ts | 34 +- .../cacheAdapter.ts => cache/adapter.ts} | 0 src/{models/redisCache.ts => cache/redis.ts} | 10 +- src/{models => client}/client.ts | 874 +++++------ src/{models => client}/collectors.ts | 2 +- src/client/mod.ts | 3 + src/{models => client}/shard.ts | 4 +- .../commandClient.ts => commands/client.ts} | 4 +- src/{models => commands}/command.ts | 4 +- .../extensions.ts => commands/extension.ts} | 2 +- src/commands/mod.ts | 3 + src/consts/urlsAndVersions.ts | 9 - .../handlers/applicationCommandCreate.ts | 4 +- .../handlers/applicationCommandDelete.ts | 4 +- .../handlers/applicationCommandUpdate.ts | 4 +- src/gateway/handlers/channelCreate.ts | 2 +- src/gateway/handlers/channelDelete.ts | 2 +- src/gateway/handlers/channelPinsUpdate.ts | 2 +- src/gateway/handlers/channelUpdate.ts | 2 +- src/gateway/handlers/guildBanAdd.ts | 2 +- src/gateway/handlers/guildBanRemove.ts | 2 +- src/gateway/handlers/guildCreate.ts | 2 +- src/gateway/handlers/guildDelete.ts | 2 +- src/gateway/handlers/guildEmojiUpdate.ts | 2 +- .../handlers/guildIntegrationsUpdate.ts | 2 +- src/gateway/handlers/guildMemberAdd.ts | 2 +- src/gateway/handlers/guildMemberRemove.ts | 2 +- src/gateway/handlers/guildMemberUpdate.ts | 2 +- src/gateway/handlers/guildMembersChunk.ts | 2 +- src/gateway/handlers/guildRoleCreate.ts | 2 +- src/gateway/handlers/guildRoleDelete.ts | 2 +- src/gateway/handlers/guildRoleUpdate.ts | 2 +- src/gateway/handlers/guildUpdate.ts | 2 +- src/gateway/handlers/interactionCreate.ts | 2 +- src/gateway/handlers/inviteCreate.ts | 2 +- src/gateway/handlers/inviteDelete.ts | 2 +- src/gateway/handlers/messageCreate.ts | 2 +- src/gateway/handlers/messageDelete.ts | 2 +- src/gateway/handlers/messageDeleteBulk.ts | 2 +- src/gateway/handlers/messageReactionAdd.ts | 2 +- src/gateway/handlers/messageReactionRemove.ts | 2 +- .../handlers/messageReactionRemoveAll.ts | 2 +- .../handlers/messageReactionRemoveEmoji.ts | 2 +- src/gateway/handlers/messageUpdate.ts | 2 +- src/gateway/handlers/{index.ts => mod.ts} | 8 +- src/gateway/handlers/presenceUpdate.ts | 2 +- src/gateway/handlers/ready.ts | 2 +- src/gateway/handlers/reconnect.ts | 2 +- src/gateway/handlers/resume.ts | 2 +- src/gateway/handlers/typingStart.ts | 2 +- src/gateway/handlers/userUpdate.ts | 2 +- src/gateway/handlers/voiceServerUpdate.ts | 2 +- src/gateway/handlers/voiceStateUpdate.ts | 2 +- src/gateway/handlers/webhooksUpdate.ts | 2 +- src/gateway/{index.ts => mod.ts} | 11 +- src/interactions/mod.ts | 3 + src/{models => interactions}/slashClient.ts | 351 +---- src/interactions/slashCommand.ts | 349 +++++ src/{models => interactions}/slashModule.ts | 0 src/managers/base.ts | 2 +- src/managers/baseChild.ts | 2 +- src/managers/channels.ts | 2 +- src/managers/emojis.ts | 2 +- src/managers/gatewayCache.ts | 2 +- src/managers/guildChannelVoiceStates.ts | 2 +- src/managers/guildChannels.ts | 2 +- src/managers/guildEmojis.ts | 2 +- src/managers/guildVoiceStates.ts | 2 +- src/managers/guilds.ts | 2 +- src/managers/invites.ts | 2 +- src/managers/memberRoles.ts | 2 +- src/managers/members.ts | 2 +- src/managers/messageReactions.ts | 2 +- src/managers/messages.ts | 2 +- src/managers/presences.ts | 2 +- src/managers/reactionUsers.ts | 2 +- src/managers/roles.ts | 2 +- src/managers/users.ts | 2 +- src/{models/rest.ts => rest/manager.ts} | 1321 ++++++++--------- src/rest/mod.ts | 2 + src/rest/types.ts | 37 + src/structures/application.ts | 2 +- src/structures/base.ts | 2 +- src/structures/channel.ts | 23 +- src/structures/dmChannel.ts | 2 +- src/structures/emoji.ts | 2 +- src/structures/groupChannel.ts | 2 +- src/structures/guild.ts | 6 +- src/structures/guildTextChannel.ts | 2 +- src/structures/guildVoiceChannel.ts | 6 +- src/structures/invite.ts | 2 +- src/structures/member.ts | 2 +- src/structures/message.ts | 2 +- src/structures/messageMentions.ts | 2 +- src/structures/messageReaction.ts | 2 +- src/structures/messageSticker.ts | 2 +- src/structures/presence.ts | 2 +- src/structures/role.ts | 2 +- src/structures/slash.ts | 2 +- src/structures/template.ts | 2 +- src/structures/textChannel.ts | 2 +- src/structures/user.ts | 2 +- src/structures/voiceState.ts | 2 +- src/structures/webhook.ts | 13 +- src/test/music.ts | 2 +- src/types/constants.ts | 7 + src/types/endpoint.ts | 161 +- src/types/mod.ts | 20 + src/utils/getChannelByType.ts | 2 +- src/utils/index.ts | 3 - 111 files changed, 1753 insertions(+), 1713 deletions(-) rename src/{models/cacheAdapter.ts => cache/adapter.ts} (100%) rename src/{models/redisCache.ts => cache/redis.ts} (91%) rename src/{models => client}/client.ts (93%) rename src/{models => client}/collectors.ts (95%) create mode 100644 src/client/mod.ts rename src/{models => client}/shard.ts (97%) rename src/{models/commandClient.ts => commands/client.ts} (99%) rename src/{models => commands}/command.ts (99%) rename src/{models/extensions.ts => commands/extension.ts} (98%) create mode 100644 src/commands/mod.ts delete mode 100644 src/consts/urlsAndVersions.ts rename src/gateway/handlers/{index.ts => mod.ts} (98%) rename src/gateway/{index.ts => mod.ts} (98%) create mode 100644 src/interactions/mod.ts rename src/{models => interactions}/slashClient.ts (54%) create mode 100644 src/interactions/slashCommand.ts rename src/{models => interactions}/slashModule.ts (100%) rename src/{models/rest.ts => rest/manager.ts} (94%) create mode 100644 src/rest/mod.ts create mode 100644 src/rest/types.ts create mode 100644 src/types/constants.ts create mode 100644 src/types/mod.ts delete mode 100644 src/utils/index.ts diff --git a/deploy.ts b/deploy.ts index e5d149b..ed5ab84 100644 --- a/deploy.ts +++ b/deploy.ts @@ -2,7 +2,7 @@ import { SlashCommandsManager, SlashClient, SlashCommandHandlerCallback -} from './src/models/slashClient.ts' +} from './src/interactions/mod.ts' import { InteractionResponseType, InteractionType } from './src/types/slash.ts' export interface DeploySlashInitOptions { @@ -41,7 +41,7 @@ export function init(options: DeploySlashInitOptions): void { try { const d = await client.verifyFetchEvent({ respondWith: (...args: any[]) => evt.respondWith(...args), - request: evt.request, + request: evt.request }) if (d === false) { await evt.respondWith( @@ -72,11 +72,11 @@ export function handle( cmd: | string | { - name: string - parent?: string - group?: string - guild?: string - }, + name: string + parent?: string + group?: string + guild?: string + }, handler: SlashCommandHandlerCallback ): void { const handle = { @@ -85,9 +85,15 @@ export function handle( ...(typeof cmd === 'string' ? {} : cmd) } - if (typeof handle.name === 'string' && handle.name.includes(' ') && handle.parent === undefined && handle.group === undefined) { - const parts = handle.name.split(/ +/).filter(e => e !== '') - if (parts.length > 3 || parts.length < 1) throw new Error('Invalid command name') + if ( + typeof handle.name === 'string' && + handle.name.includes(' ') && + handle.parent === undefined && + handle.group === undefined + ) { + const parts = handle.name.split(/ +/).filter((e) => e !== '') + if (parts.length > 3 || parts.length < 1) + throw new Error('Invalid command name') const root = parts.shift() as string const group = parts.length === 2 ? parts.shift() : undefined const sub = parts.shift() @@ -103,4 +109,4 @@ export function handle( export { commands, client } export * from './src/types/slash.ts' export * from './src/structures/slash.ts' -export * from './src/models/slashClient.ts' +export * from './src/interactions/mod.ts' diff --git a/mod.ts b/mod.ts index 70faacb..4ebaf42 100644 --- a/mod.ts +++ b/mod.ts @@ -1,20 +1,18 @@ export { GatewayIntents } from './src/types/gateway.ts' export { Base } from './src/structures/base.ts' -export { Gateway } from './src/gateway/index.ts' -export type { GatewayTypedEvents } from './src/gateway/index.ts' -export type { ClientEvents } from './src/gateway/handlers/index.ts' -export * from './src/models/client.ts' -export * from './src/models/slashClient.ts' +export { Gateway } from './src/gateway/mod.ts' +export type { GatewayTypedEvents } from './src/gateway/mod.ts' +export type { ClientEvents } from './src/gateway/handlers/mod.ts' +export * from './src/client/mod.ts' +export * from './src/interactions/mod.ts' export { RESTManager, TokenType, HttpResponseCode, DiscordAPIError -} from './src/models/rest.ts' -export type { APIMap, DiscordAPIErrorPayload } from './src/models/rest.ts' -export type { RequestHeaders } from './src/models/rest.ts' -export type { RESTOptions } from './src/models/rest.ts' -export * from './src/models/cacheAdapter.ts' +} from './src/rest/mod.ts' +export * from './src/rest/mod.ts' +export * from './src/cache/adapter.ts' export { Command, CommandBuilder, @@ -22,16 +20,16 @@ export { CommandsManager, CategoriesManager, CommandsLoader -} from './src/models/command.ts' -export type { CommandContext, CommandOptions } from './src/models/command.ts' +} from './src/commands/command.ts' +export type { CommandContext, CommandOptions } from './src/commands/command.ts' export { Extension, ExtensionCommands, ExtensionsManager -} from './src/models/extensions.ts' -export { SlashModule } from './src/models/slashModule.ts' -export { CommandClient, command } from './src/models/commandClient.ts' -export type { CommandClientOptions } from './src/models/commandClient.ts' +} from './src/commands/extension.ts' +export { SlashModule } from './src/interactions/slashModule.ts' +export { CommandClient, command } from './src/commands/client.ts' +export type { CommandClientOptions } from './src/commands/client.ts' export { BaseManager } from './src/managers/base.ts' export { BaseChildManager } from './src/managers/baseChild.ts' export { ChannelsManager } from './src/managers/channels.ts' @@ -165,8 +163,8 @@ export type { UserPayload } from './src/types/user.ts' export { UserFlags } from './src/types/userFlags.ts' export type { VoiceStatePayload } from './src/types/voice.ts' export type { WebhookPayload } from './src/types/webhook.ts' -export * from './src/models/collectors.ts' +export * from './src/client/collectors.ts' export type { Dict } from './src/utils/dict.ts' -export * from './src/models/redisCache.ts' +export * from './src/cache/redis.ts' export { ColorUtil } from './src/utils/colorutil.ts' export type { Colors } from './src/utils/colorutil.ts' diff --git a/src/models/cacheAdapter.ts b/src/cache/adapter.ts similarity index 100% rename from src/models/cacheAdapter.ts rename to src/cache/adapter.ts diff --git a/src/models/redisCache.ts b/src/cache/redis.ts similarity index 91% rename from src/models/redisCache.ts rename to src/cache/redis.ts index 0820213..7ce9182 100644 --- a/src/models/redisCache.ts +++ b/src/cache/redis.ts @@ -1,5 +1,9 @@ -import { ICacheAdapter } from './cacheAdapter.ts' -import { connect, Redis, RedisConnectOptions } from 'https://deno.land/x/redis@v0.14.1/mod.ts' +import { ICacheAdapter } from './adapter.ts' +import { + connect, + Redis, + RedisConnectOptions +} from 'https://deno.land/x/redis@v0.14.1/mod.ts' /** Redis Cache Adapter for using Redis as a cache-provider. */ export class RedisCacheAdapter implements ICacheAdapter { @@ -102,4 +106,4 @@ export class RedisCacheAdapter implements ICacheAdapter { await this._checkReady() return (await this.redis?.del(cacheName)) !== 0 } -} \ No newline at end of file +} diff --git a/src/models/client.ts b/src/client/client.ts similarity index 93% rename from src/models/client.ts rename to src/client/client.ts index dc40e24..a8d3709 100644 --- a/src/models/client.ts +++ b/src/client/client.ts @@ -1,437 +1,437 @@ -/* eslint-disable @typescript-eslint/method-signature-style */ -import { User } from '../structures/user.ts' -import { GatewayIntents } from '../types/gateway.ts' -import { Gateway } from '../gateway/index.ts' -import { RESTManager, RESTOptions, TokenType } from './rest.ts' -import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' -import { UsersManager } from '../managers/users.ts' -import { GuildManager } from '../managers/guilds.ts' -import { ChannelsManager } from '../managers/channels.ts' -import { ClientPresence } from '../structures/presence.ts' -import { EmojisManager } from '../managers/emojis.ts' -import { ActivityGame, ClientActivity } from '../types/presence.ts' -import { Extension } from './extensions.ts' -import { SlashClient } from './slashClient.ts' -import { Interaction } from '../structures/slash.ts' -import { ShardManager } from './shard.ts' -import { Application } from '../structures/application.ts' -import { Invite } from '../structures/invite.ts' -import { INVITE } from '../types/endpoint.ts' -import { ClientEvents } from '../gateway/handlers/index.ts' -import type { Collector } from './collectors.ts' -import { HarmonyEventEmitter } from '../utils/events.ts' -import { VoiceRegion } from '../types/voice.ts' -import { fetchAuto } from '../../deps.ts' -import { DMChannel } from '../structures/dmChannel.ts' -import { Template } from '../structures/template.ts' - -/** OS related properties sent with Gateway Identify */ -export interface ClientProperties { - os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string - browser?: 'harmony' | string - device?: 'harmony' | string -} - -/** Some Client Options to modify behaviour */ -export interface ClientOptions { - /** ID of the Client/Application to initialize Slash Client REST */ - id?: string - /** Token of the Bot/User */ - token?: string - /** Gateway Intents */ - intents?: GatewayIntents[] - /** Cache Adapter to use, defaults to Collections one */ - cache?: ICacheAdapter - /** Force New Session and don't use cached Session (by persistent caching) */ - forceNewSession?: boolean - /** Startup presence of client */ - presence?: ClientPresence | ClientActivity | ActivityGame - /** Force all requests to Canary API */ - canary?: boolean - /** Time till which Messages are to be cached, in MS. Default is 3600000 */ - messageCacheLifetime?: number - /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ - reactionCacheLifetime?: number - /** Whether to fetch Uncached Message of Reaction or not? */ - fetchUncachedReactions?: boolean - /** Client Properties */ - clientProperties?: ClientProperties - /** Enable/Disable Slash Commands Integration (enabled by default) */ - enableSlash?: boolean - /** Disable taking token from env if not provided (token is taken from env if present by default) */ - disableEnvToken?: boolean - /** Override REST Options */ - restOptions?: RESTOptions - /** Whether to fetch Gateway info or not */ - fetchGatewayInfo?: boolean - /** ADVANCED: Shard ID to launch on */ - shard?: number - /** ADVACNED: Shard count. */ - shardCount?: number | 'auto' -} - -/** - * Discord Client. - */ -export class Client extends HarmonyEventEmitter { - /** REST Manager - used to make all requests */ - rest: RESTManager - /** User which Client logs in to, undefined until logs in */ - user?: User - /** WebSocket ping of Client */ - ping = 0 - /** Token of the Bot/User */ - token?: string - /** Cache Adapter */ - cache: ICacheAdapter = new DefaultCacheAdapter() - /** Gateway Intents */ - intents?: GatewayIntents[] - /** Whether to force new session or not */ - forceNewSession?: boolean - /** Time till messages to stay cached, in MS. */ - messageCacheLifetime: number = 3600000 - /** Time till messages to stay cached, in MS. */ - reactionCacheLifetime: number = 3600000 - /** Whether to fetch Uncached Message of Reaction or not? */ - fetchUncachedReactions: boolean = false - /** Client Properties */ - clientProperties: ClientProperties - /** Slash-Commands Management client */ - slash: SlashClient - /** Whether to fetch Gateway info or not */ - fetchGatewayInfo: boolean = true - - /** Users Manager, containing all Users cached */ - users: UsersManager = new UsersManager(this) - /** Guilds Manager, providing cache & API interface to Guilds */ - guilds: GuildManager = new GuildManager(this) - /** Channels Manager, providing cache interface to Channels */ - channels: ChannelsManager = new ChannelsManager(this) - /** Channels Manager, providing cache interface to Channels */ - emojis: EmojisManager = new EmojisManager(this) - - /** Last READY timestamp */ - upSince?: Date - - /** Client's presence. Startup one if set before connecting */ - presence: ClientPresence = new ClientPresence() - _decoratedEvents?: { - [name: string]: (...args: any[]) => void - } - - _decoratedSlash?: Array<{ - name: string - guild?: string - parent?: string - group?: string - handler: (interaction: Interaction) => any - }> - - _id?: string - - /** Shard on which this Client is */ - shard?: number - /** Shard Count */ - shardCount: number | 'auto' = 'auto' - /** Shard Manager of this Client if Sharded */ - shards: ShardManager - /** Collectors set */ - collectors: Set = new Set() - - /** Since when is Client online (ready). */ - get uptime(): number { - if (this.upSince === undefined) return 0 - else { - const dif = Date.now() - this.upSince.getTime() - if (dif < 0) return 0 - else return dif - } - } - - get gateway(): Gateway { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return this.shards.list.get('0') as Gateway - } - - applicationID?: string - applicationFlags?: number - - constructor(options: ClientOptions = {}) { - super() - this._id = options.id - this.token = options.token - this.intents = options.intents - this.shards = new ShardManager(this) - this.forceNewSession = options.forceNewSession - if (options.cache !== undefined) this.cache = options.cache - if (options.presence !== undefined) - this.presence = - options.presence instanceof ClientPresence - ? options.presence - : new ClientPresence(options.presence) - if (options.messageCacheLifetime !== undefined) - this.messageCacheLifetime = options.messageCacheLifetime - if (options.reactionCacheLifetime !== undefined) - this.reactionCacheLifetime = options.reactionCacheLifetime - if (options.fetchUncachedReactions === true) - this.fetchUncachedReactions = true - - if ( - this._decoratedEvents !== undefined && - Object.keys(this._decoratedEvents).length !== 0 - ) { - Object.entries(this._decoratedEvents).forEach((entry) => { - this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) - }) - this._decoratedEvents = undefined - } - - this.clientProperties = - options.clientProperties === undefined - ? { - os: Deno.build.os, - browser: 'harmony', - device: 'harmony' - } - : options.clientProperties - - if (options.shard !== undefined) this.shard = options.shard - if (options.shardCount !== undefined) this.shardCount = options.shardCount - - this.fetchGatewayInfo = options.fetchGatewayInfo ?? true - - if (this.token === undefined) { - try { - const token = Deno.env.get('DISCORD_TOKEN') - if (token !== undefined) { - this.token = token - this.debug('Info', 'Found token in ENV') - } - } catch (e) { } - } - - const restOptions: RESTOptions = { - token: () => this.token, - tokenType: TokenType.Bot, - canary: options.canary, - client: this - } - - if (options.restOptions !== undefined) - Object.assign(restOptions, options.restOptions) - this.rest = new RESTManager(restOptions) - - this.slash = new SlashClient({ - id: () => this.getEstimatedID(), - client: this, - enabled: options.enableSlash - }) - } - - /** - * Sets Cache Adapter - * - * Should NOT be set after bot is already logged in or using current cache. - * Please look into using `cache` option. - */ - setAdapter(adapter: ICacheAdapter): Client { - this.cache = adapter - return this - } - - /** Changes Presence of Client */ - setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { - if (presence instanceof ClientPresence) { - this.presence = presence - } else this.presence = new ClientPresence(presence) - this.gateway?.sendPresence(this.presence.create()) - } - - /** Emits debug event */ - debug(tag: string, msg: string): void { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.emit('debug', `[${tag}] ${msg}`) - } - - getEstimatedID(): string { - if (this.user !== undefined) return this.user.id - else if (this.token !== undefined) { - try { - return atob(this.token.split('.')[0]) - } catch (e) { - return this._id ?? 'unknown' - } - } else { - return this._id ?? 'unknown' - } - } - - /** Fetch Application of the Client */ - async fetchApplication(): Promise { - const app = await this.rest.api.oauth2.applications['@me'].get() - return new Application(this, app) - } - - /** Fetch an Invite */ - async fetchInvite(id: string): Promise { - return await new Promise((resolve, reject) => { - this.rest - .get(INVITE(id)) - .then((data) => { - resolve(new Invite(this, data)) - }) - .catch((e) => reject(e)) - }) - } - - /** - * This function is used for connecting to discord. - * @param token Your token. This is required if not given in ClientOptions. - * @param intents Gateway intents in array. This is required if not given in ClientOptions. - */ - async connect(token?: string, intents?: GatewayIntents[]): Promise { - token ??= this.token - if (token === undefined) throw new Error('No Token Provided') - this.token = token - if (intents !== undefined && this.intents !== undefined) { - this.debug( - 'client', - 'Intents were set in both client and connect function. Using the one in the connect function...' - ) - } else if (intents === undefined && this.intents !== undefined) { - intents = this.intents - } else if (intents !== undefined && this.intents === undefined) { - this.intents = intents - } else throw new Error('No Gateway Intents were provided') - - this.rest.token = token - if (this.shard !== undefined) { - if (typeof this.shardCount === 'number') - this.shards.cachedShardCount = this.shardCount - await this.shards.launch(this.shard) - } else await this.shards.connect() - return this.waitFor('ready', () => true).then(() => this) - } - - /** Destroy the Gateway connection */ - async destroy(): Promise { - this.gateway.initialized = false - this.gateway.sequenceID = undefined - this.gateway.sessionID = undefined - await this.gateway.cache.delete('seq') - await this.gateway.cache.delete('session_id') - this.gateway.close() - this.user = undefined - this.upSince = undefined - return this - } - - /** Attempt to Close current Gateway connection and Resume */ - async reconnect(): Promise { - this.gateway.close() - this.gateway.initWebsocket() - return this.waitFor('ready', () => true).then(() => this) - } - - /** Add a new Collector */ - addCollector(collector: Collector): boolean { - if (this.collectors.has(collector)) return false - else { - this.collectors.add(collector) - return true - } - } - - /** Remove a Collector */ - removeCollector(collector: Collector): boolean { - if (!this.collectors.has(collector)) return false - else { - this.collectors.delete(collector) - return true - } - } - - async emit(event: keyof ClientEvents, ...args: any[]): Promise { - const collectors: Collector[] = [] - for (const collector of this.collectors.values()) { - if (collector.event === event) collectors.push(collector) - } - if (collectors.length !== 0) { - this.collectors.forEach((collector) => collector._fire(...args)) - } - // TODO(DjDeveloperr): Fix this ts-ignore - // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error - // @ts-ignore - return super.emit(event, ...args) - } - - /** Returns an array of voice region objects that can be used when creating servers. */ - async fetchVoiceRegions(): Promise { - return this.rest.api.voice.regions.get() - } - - /** Modify current (Client) User. */ - async editUser(data: { - username?: string - avatar?: string - }): Promise { - if (data.username === undefined && data.avatar === undefined) - throw new Error( - 'Either username or avatar or both must be specified to edit' - ) - - if (data.avatar?.startsWith('http') === true) { - data.avatar = await fetchAuto(data.avatar) - } - - await this.rest.api.users['@me'].patch({ - username: data.username, - avatar: data.avatar - }) - return this - } - - /** Change Username of the Client User */ - async setUsername(username: string): Promise { - return await this.editUser({ username }) - } - - /** Change Avatar of the Client User */ - async setAvatar(avatar: string): Promise { - return await this.editUser({ avatar }) - } - - /** Create a DM Channel with a User */ - async createDM(user: User | string): Promise { - const id = typeof user === 'object' ? user.id : user - const dmPayload = await this.rest.api.users['@me'].channels.post({ - recipient_id: id - }) - await this.channels.set(dmPayload.id, dmPayload) - return (this.channels.get(dmPayload.id) as unknown) as DMChannel - } - - /** Returns a template object for the given code. */ - async fetchTemplate(code: string): Promise