From da0bfc12c71cf71a75952e3097480e31169af59c Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 21 Dec 2020 19:18:46 +0530 Subject: [PATCH 01/13] feat: slash builde --- src/models/client.ts | 4 + src/models/rest.ts | 2 + src/models/slashClient.ts | 224 ++++++++++++++++++++++++++++++-------- src/test/music.ts | 4 + src/test/slash-only.ts | 32 +++++- src/types/slash.ts | 3 +- 6 files changed, 220 insertions(+), 49 deletions(-) diff --git a/src/models/client.ts b/src/models/client.ts index 07955d5..547009a 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -247,6 +247,7 @@ export function event(name?: string) { } } +/** Decorator to create a Slash Command handler */ export function slash(name?: string, guild?: string) { return function (client: Client | SlashModule, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] @@ -262,6 +263,7 @@ export function slash(name?: string, guild?: string) { } } +/** Decorator to create a Sub-Slash Command handler */ export function subslash(parent: string, name?: string, guild?: string) { return function (client: Client | SlashModule, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] @@ -279,6 +281,7 @@ export function subslash(parent: string, name?: string, guild?: string) { } } +/** Decorator to create a Grouped Slash Command handler */ export function groupslash( parent: string, group: string, @@ -303,6 +306,7 @@ export function groupslash( } } +/** Decorator to add a Slash Module to Client */ export function slashModule() { return function (client: Client, prop: string) { if (client._decoratedSlashModules === undefined) diff --git a/src/models/rest.ts b/src/models/rest.ts index 0e80c39..95aa94b 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -97,6 +97,7 @@ export interface RESTOptions { token?: string headers?: { [name: string]: string | undefined } canary?: boolean + version?: 6 | 7 | 8 } export class RESTManager { @@ -111,6 +112,7 @@ export class RESTManager { constructor(client?: RESTOptions) { this.client = client this.api = builder(this) + if (client?.version !== undefined) this.version = client.version // eslint-disable-next-line @typescript-eslint/no-floating-promises this.handleRateLimits() } diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 5058660..29effff 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,14 +1,10 @@ import { Guild } from '../structures/guild.ts' import { Interaction } from '../structures/slash.ts' -import { - APPLICATION_COMMAND, - APPLICATION_COMMANDS, - APPLICATION_GUILD_COMMAND, - APPLICATION_GUILD_COMMANDS -} from '../types/endpoint.ts' import { InteractionType, + SlashCommandChoice, SlashCommandOption, + SlashCommandOptionType, SlashCommandPartial, SlashCommandPayload } from '../types/slash.ts' @@ -43,6 +39,147 @@ export class SlashCommand { } } +export interface CreateOptions { + name: string + description?: string + options?: Array + choices?: Array +} + +function createSlashOption( + type: SlashCommandOptionType, + data: CreateOptions +): SlashCommandOption { + return { + name: data.name, + type, + description: + type === 0 || type === 1 + ? undefined + : data.description ?? 'No description.', + options: data.options?.map((e) => + typeof e === 'function' ? e(SlashOptionCallableBuilder) : e + ), + choices: + data.choices === undefined + ? undefined + : data.choices.map((e) => + typeof e === 'string' ? { name: e, value: e } : e + ) + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class SlashOptionCallableBuilder { + static string(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.STRING, data) + } + + static bool(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.BOOLEAN, data) + } + + static subCommand(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data) + } + + static subCommandGroup(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data) + } + + static role(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.ROLE, data) + } + + static channel(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.CHANNEL, data) + } + + static user(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.USER, data) + } + + static number(data: CreateOptions): SlashCommandOption { + return createSlashOption(SlashCommandOptionType.INTEGER, data) + } +} + +export type SlashOptionCallable = ( + o: typeof SlashOptionCallableBuilder +) => SlashCommandOption + +export type SlashBuilderOptionsData = + | Array + | { + [name: string]: + | { + description: string + type: SlashCommandOptionType + options?: SlashCommandOption[] + choices?: SlashCommandChoice[] + } + | SlashOptionCallable + } + +function buildOptionsArray( + options: SlashBuilderOptionsData +): SlashCommandOption[] { + return Array.isArray(options) + ? options.map((op) => + typeof op === 'function' ? op(SlashOptionCallableBuilder) : op + ) + : Object.entries(options).map((entry) => + typeof entry[1] === 'function' + ? entry[1](SlashOptionCallableBuilder) + : Object.assign(entry[1], { name: entry[0] }) + ) +} + +export class SlashBuilder { + data: SlashCommandPartial + + constructor( + name?: string, + description?: string, + options?: SlashBuilderOptionsData + ) { + this.data = { + name: name ?? '', + description: description ?? 'No description.', + options: options === undefined ? [] : buildOptionsArray(options) + } + } + + name(name: string): SlashBuilder { + this.data.name = name + return this + } + + description(desc: string): SlashBuilder { + this.data.description = desc + return this + } + + option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { + if (this.data.options === undefined) this.data.options = [] + this.data.options.push( + typeof option === 'function' ? option(SlashOptionCallableBuilder) : option + ) + return this + } + + options(options: SlashBuilderOptionsData): SlashBuilder { + this.data.options = buildOptionsArray(options) + return this + } + + export(): SlashCommandPartial { + if (this.data.name === '') + throw new Error('Name was not provided in Slash Builder') + return this.data + } +} + export class SlashCommandsManager { slash: SlashClient @@ -58,9 +195,9 @@ export class SlashCommandsManager { async all(): Promise> { const col = new Collection() - const res = (await this.rest.get( - APPLICATION_COMMANDS(this.slash.getID()) - )) as SlashCommandPayload[] + const res = (await this.rest.api.applications[ + this.slash.getID() + ].commands.get()) as SlashCommandPayload[] if (!Array.isArray(res)) return col for (const raw of res) { @@ -77,12 +214,9 @@ export class SlashCommandsManager { ): Promise> { const col = new Collection() - const res = (await this.rest.get( - APPLICATION_GUILD_COMMANDS( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id - ) - )) as SlashCommandPayload[] + const res = (await this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands.get()) as SlashCommandPayload[] if (!Array.isArray(res)) return col for (const raw of res) { @@ -99,15 +233,14 @@ export class SlashCommandsManager { data: SlashCommandPartial, guild?: Guild | string ): Promise { - const payload = await this.rest.post( + const route = guild === undefined - ? APPLICATION_COMMANDS(this.slash.getID()) - : APPLICATION_GUILD_COMMANDS( - this.slash.getID(), + ? this.rest.api.applications[this.slash.getID()].commands + : this.rest.api.applications[this.slash.getID()].guilds[ typeof guild === 'string' ? guild : guild.id - ), - data - ) + ].commands + + const payload = await route.post(data) const cmd = new SlashCommand(this, payload) cmd._guild = @@ -122,16 +255,14 @@ export class SlashCommandsManager { data: SlashCommandPartial, guild?: Guild | string ): Promise { - await this.rest.patch( + const route = guild === undefined - ? APPLICATION_COMMAND(this.slash.getID(), id) - : APPLICATION_GUILD_COMMAND( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id, - id - ), - data - ) + ? this.rest.api.applications[this.slash.getID()].commands[id] + : this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands[id] + + await route.patch(data) return this } @@ -140,29 +271,28 @@ export class SlashCommandsManager { id: string, guild?: Guild | string ): Promise { - await this.rest.delete( + const route = guild === undefined - ? APPLICATION_COMMAND(this.slash.getID(), id) - : APPLICATION_GUILD_COMMAND( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id, - id - ) - ) + ? this.rest.api.applications[this.slash.getID()].commands[id] + : this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands[id] + + await route.delete() return this } /** Get a Slash Command (global or Guild) */ async get(id: string, guild?: Guild | string): Promise { - const data = await this.rest.get( + const route = guild === undefined - ? APPLICATION_COMMAND(this.slash.getID(), id) - : APPLICATION_GUILD_COMMAND( - this.slash.getID(), - typeof guild === 'string' ? guild : guild.id, - id - ) - ) + ? this.rest.api.applications[this.slash.getID()].commands[id] + : this.rest.api.applications[this.slash.getID()].guilds[ + typeof guild === 'string' ? guild : guild.id + ].commands[id] + + const data = await route.get() + return new SlashCommand(this, data) } } diff --git a/src/test/music.ts b/src/test/music.ts index 07f65b6..854aa77 100644 --- a/src/test/music.ts +++ b/src/test/music.ts @@ -205,6 +205,10 @@ class VCExtension extends Extension { const client = new MyClient() +client.on('raw', (e, d) => { + if (e === 'READY') console.log(d) +}) + client.extensions.load(VCExtension) client.connect(TOKEN, Intents.None) diff --git a/src/test/slash-only.ts b/src/test/slash-only.ts index cd326bf..00bb12b 100644 --- a/src/test/slash-only.ts +++ b/src/test/slash-only.ts @@ -1,6 +1,36 @@ -import { SlashClient } from '../models/slashClient.ts' +import { SlashClient, SlashBuilder } from '../models/slashClient.ts' import { TOKEN } from './config.ts' const slash = new SlashClient({ token: TOKEN }) slash.commands.all().then(console.log) + +const cmd = new SlashBuilder() + .name('searchmusic') + .description('Search for music.') + .option((o) => + o.string({ name: 'query', description: 'Query to search with.' }) + ) + .option((o) => + o.string({ + name: 'engine', + description: 'Search engine to use.', + choices: [{ name: 'YouTube', value: 'youtube' }, 'Spotify'] + }) + ) + .options({ + query: { + description: 'Query UWU', + type: 3 + }, + engine: { + description: 'Engine UWU', + type: 3, + choices: [ + { name: 'YouTube', value: 'youtube' }, + { name: 'Spotify', value: 'spotify' } + ] + } + }) + +console.log(JSON.stringify(cmd.export())) diff --git a/src/types/slash.ts b/src/types/slash.ts index d48f363..b56f100 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -66,7 +66,8 @@ export enum SlashCommandOptionType { export interface SlashCommandOption { name: string - description: string + /** Description not required in Sub-Command or Sub-Command-Group */ + description?: string type: SlashCommandOptionType required?: boolean default?: boolean From 03ea5df55189ca5783037b671e27e3412f74cd67 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 22 Dec 2020 12:28:45 +0530 Subject: [PATCH 02/13] feat: middlewares and http-based slash util --- README.md | 19 +++-- src/models/client.ts | 2 +- src/models/slashClient.ts | 155 +++++++++++++++++++++++++++++++++++--- 3 files changed, 154 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 767b21b..585d405 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,12 @@
- Lightweight and easy to use. -- Built-in Command Framework, - - Easily build Commands on the fly. - - Completely Customizable. - - Complete Object-Oriented approach. -- 100% Discord API Coverage. -- Customizable caching. - - Built in support for Redis. - - Write Custom Cache Adapters. -- Complete TypeScript support. +- Complete Object-Oriented approach. +- Slash Commands supported. +- Built-in Commands framework. +- Customizable Caching, with Redis support. +- Use `@decorators` to easily make things! +- Made with ❤️ TypeScript. ## Table of Contents @@ -102,13 +99,14 @@ client.connect('super secret token comes here', Intents.All) ``` Or with Decorators! + ```ts import { Client, event, Intents, command, - CommandContext, + CommandContext } from 'https://deno.land/x/harmony/mod.ts' class MyClient extends CommandClient { @@ -141,6 +139,7 @@ Documentation is available for `main` (branch) and `stable` (release). - [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmony-org/harmony/main/mod.ts) - [Stable](https://doc.deno.land/https/deno.land/x/harmony/mod.ts) +- [Guide](https://harmony-org.github.io) ## Found a bug or want support? Join our discord server! diff --git a/src/models/client.ts b/src/models/client.ts index 547009a..2fc4f03 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -288,7 +288,7 @@ export function groupslash( name?: string, guild?: string ) { - return function (client: Client | SlashModule, prop: string) { + return function (client: Client | SlashModule | SlashClient, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] const item = (client as { [name: string]: any })[prop] if (typeof item !== 'function') { diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 29effff..c652d58 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -11,6 +11,14 @@ import { import { Collection } from '../utils/collection.ts' import { Client } from './client.ts' import { RESTManager } from './rest.ts' +import { SlashModule } from './slashModule.ts' +import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' +import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' +import { + Request as ORequest, + Response as OResponse +} from 'https://deno.land/x/opine@1.0.0/src/types.ts' +import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts' export class SlashCommand { slash: SlashCommandsManager @@ -37,6 +45,21 @@ export class SlashCommand { async edit(data: SlashCommandPartial): Promise { await this.slash.edit(this.id, data, this._guild) } + + /** Create a handler for this Slash Command */ + handle( + func: SlashCommandHandlerCallback, + options?: { parent?: string; group?: string } + ): SlashCommand { + this.slash.slash.handle({ + name: this.name, + parent: options?.parent, + group: options?.group, + guild: this._guild, + handler: func + }) + return this + } } export interface CreateOptions { @@ -58,7 +81,7 @@ function createSlashOption( ? undefined : data.description ?? 'No description.', options: data.options?.map((e) => - typeof e === 'function' ? e(SlashOptionCallableBuilder) : e + typeof e === 'function' ? e(SlashOption) : e ), choices: data.choices === undefined @@ -70,7 +93,7 @@ function createSlashOption( } // eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class SlashOptionCallableBuilder { +export class SlashOption { static string(data: CreateOptions): SlashCommandOption { return createSlashOption(SlashCommandOptionType.STRING, data) } @@ -104,9 +127,7 @@ export class SlashOptionCallableBuilder { } } -export type SlashOptionCallable = ( - o: typeof SlashOptionCallableBuilder -) => SlashCommandOption +export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption export type SlashBuilderOptionsData = | Array @@ -125,12 +146,10 @@ function buildOptionsArray( options: SlashBuilderOptionsData ): SlashCommandOption[] { return Array.isArray(options) - ? options.map((op) => - typeof op === 'function' ? op(SlashOptionCallableBuilder) : op - ) + ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op)) : Object.entries(options).map((entry) => typeof entry[1] === 'function' - ? entry[1](SlashOptionCallableBuilder) + ? entry[1](SlashOption) : Object.assign(entry[1], { name: entry[0] }) ) } @@ -163,7 +182,7 @@ export class SlashBuilder { option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { if (this.data.options === undefined) this.data.options = [] this.data.options.push( - typeof option === 'function' ? option(SlashOptionCallableBuilder) : option + typeof option === 'function' ? option(SlashOption) : option ) return this } @@ -312,6 +331,7 @@ export interface SlashOptions { enabled?: boolean token?: string rest?: RESTManager + publicKey?: string } export class SlashClient { @@ -322,6 +342,18 @@ export class SlashClient { commands: SlashCommandsManager handlers: SlashCommandHandler[] = [] rest: RESTManager + modules: SlashModule[] = [] + publicKey?: string + + _decoratedSlash?: Array<{ + name: string + guild?: string + parent?: string + group?: string + handler: (interaction: Interaction) => any + }> + + _decoratedSlashModules?: SlashModule[] constructor(options: SlashOptions) { let id = options.id @@ -332,6 +364,7 @@ export class SlashClient { this.client = options.client this.token = options.token this.commands = new SlashCommandsManager(this) + this.publicKey = options.publicKey if (options !== undefined) { this.enabled = options.enabled ?? true @@ -343,6 +376,24 @@ export class SlashClient { }) } + if (this.client?._decoratedSlashModules !== undefined) { + this.client._decoratedSlashModules.forEach((e) => { + this.modules.push(e) + }) + } + + if (this._decoratedSlash !== undefined) { + this._decoratedSlash.forEach((e) => { + this.handlers.push(e) + }) + } + + if (this._decoratedSlashModules !== undefined) { + this._decoratedSlashModules.forEach((e) => { + this.modules.push(e) + }) + } + this.rest = options.client === undefined ? options.rest === undefined @@ -367,8 +418,16 @@ export class SlashClient { return this } + getHandlers(): SlashCommandHandler[] { + let res = this.handlers + for (const mod of this.modules) { + res = [...res, ...mod.commands] + } + return res + } + private _getCommand(i: Interaction): SlashCommandHandler | undefined { - return this.handlers.find((e) => { + return this.getHandlers().find((e) => { const hasGroupOrParent = e.group !== undefined || e.parent !== undefined const groupMatched = e.group !== undefined && e.parent !== undefined @@ -401,4 +460,78 @@ export class SlashClient { cmd.handler(interaction) } + + async verifyKey( + rawBody: string | Uint8Array | Buffer, + signature: string, + timestamp: string + ): Promise { + if (this.publicKey === undefined) + throw new Error('Public Key is not present') + return edverify( + signature, + Buffer.concat([ + Buffer.from(timestamp, 'utf-8'), + Buffer.from( + rawBody instanceof Uint8Array + ? new TextDecoder().decode(rawBody) + : rawBody + ) + ]), + this.publicKey + ).catch(() => false) + } + + async verifyOpineRequest(req: ORequest): Promise { + const signature = req.headers.get('x-signature-ed25519') + const timestamp = req.headers.get('x-signature-timestamp') + const contentLength = req.headers.get('content-length') + + if (signature === null || timestamp === null || contentLength === null) + return false + + const body = new Uint8Array(parseInt(contentLength)) + await req.body.read(body) + + const verified = await this.verifyKey(body, signature, timestamp) + if (!verified) return false + + return true + } + + /** Middleware to verify request in Opine framework. */ + async verifyOpineMiddleware( + req: ORequest, + res: OResponse, + next: CallableFunction + ): Promise { + const verified = await this.verifyOpineRequest(req) + if (!verified) return res.setStatus(401).end() + + await next() + return true + } + + // TODO: create verifyOakMiddleware too + /** Method to verify Request from Oak server "Context". */ + async verifyOakRequest(ctx: Context): Promise { + const signature = ctx.request.headers.get('x-signature-ed25519') + const timestamp = ctx.request.headers.get('x-signature-timestamp') + const contentLength = ctx.request.headers.get('content-length') + + if ( + signature === null || + timestamp === null || + contentLength === null || + ctx.request.hasBody !== true + ) { + return false + } + + const body = await ctx.request.body().value + + const verified = await this.verifyKey(body as any, signature, timestamp) + if (!verified) return false + return true + } } From 94a447921ddfb1a00904d5ab62cd8e4972c438bb Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 22 Dec 2020 15:40:19 +0530 Subject: [PATCH 03/13] feat: member screening types update --- src/models/rest.ts | 1 + src/structures/message.ts | 4 ++ src/structures/textChannel.ts | 80 +++++++++++++++++++++++++++++++++++ src/test/music.ts | 14 +++++- src/test/slash-only.ts | 44 ++++++------------- src/types/guild.ts | 4 ++ src/types/slash.ts | 2 +- 7 files changed, 115 insertions(+), 34 deletions(-) diff --git a/src/models/rest.ts b/src/models/rest.ts index 95aa94b..29b708c 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -410,6 +410,7 @@ export class RESTManager { const query = method === 'get' && body !== undefined ? Object.entries(body as any) + .filter(([k, v]) => v !== undefined) .map( ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent( diff --git a/src/structures/message.ts b/src/structures/message.ts index d8e6325..3c6501e 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -46,6 +46,10 @@ export class Message extends Base { flags?: number stickers?: MessageSticker[] + get createdAt(): Date { + return new Date(this.timestamp) + } + constructor( client: Client, data: MessagePayload, diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index 699b4ad..f63763a 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -3,6 +3,7 @@ import { Client } from '../models/client.ts' import { GuildTextChannelPayload, MessageOption, + MessagePayload, MessageReference, ModifyGuildTextChannelOption, ModifyGuildTextChannelPayload, @@ -14,6 +15,7 @@ import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' +import { Collection } from '../utils/collection.ts' import { Channel } from './channel.ts' import { Embed } from './embed.ts' import { Guild } from './guild.ts' @@ -124,6 +126,48 @@ export class TextChannel extends Channel { await res.mentions.fromPayload(newMsg) return res } + + /** + * Fetch Messages of a Channel + * @param options Options to configure fetching Messages + */ + async fetchMessages(options?: { + limit?: number + around?: Message | string + before?: Message | string + after?: Message | string + }): Promise> { + const res = new Collection() + const raws = (await this.client.rest.api.channels[this.id].messages.get({ + limit: options?.limit ?? 50, + around: + options?.around === undefined + ? undefined + : typeof options.around === 'string' + ? options.around + : options.around.id, + before: + options?.before === undefined + ? undefined + : typeof options.before === 'string' + ? options.before + : options.before.id, + after: + options?.after === undefined + ? undefined + : typeof options.after === 'string' + ? options.after + : options.after.id + })) as MessagePayload[] + + for (const raw of raws) { + await this.messages.set(raw.id, raw) + const msg = ((await this.messages.get(raw.id)) as unknown) as Message + res.set(msg.id, msg) + } + + return res + } } export class GuildTextChannel extends TextChannel { @@ -185,4 +229,40 @@ export class GuildTextChannel extends TextChannel { return new GuildTextChannel(this.client, resp, this.guild) } + + /** + * Bulk Delete Messages in a Guild Text Channel + * @param messages Messages to delete. Can be a number, or Array of Message or IDs + */ + async bulkDelete( + messages: Array | number + ): Promise { + let ids: string[] = [] + + if (Array.isArray(messages)) + ids = messages.map((e) => (typeof e === 'string' ? e : e.id)) + else { + let list = await this.messages.array() + if (list.length < messages) list = (await this.fetchMessages()).array() + ids = list + .sort((b, a) => a.createdAt.getTime() - b.createdAt.getTime()) + .filter((e, i) => i < messages) + .filter( + (e) => + new Date().getTime() - e.createdAt.getTime() <= + 1000 * 60 * 60 * 24 * 14 + ) + .map((e) => e.id) + } + + ids = [...new Set(ids)] + if (ids.length < 2 || ids.length > 100) + throw new Error('bulkDelete can only delete messages in range 2-100') + + await this.client.rest.api.channels[this.id].messages['bulk-delete'].post({ + messages: ids + }) + + return this + } } diff --git a/src/test/music.ts b/src/test/music.ts index 854aa77..bf654af 100644 --- a/src/test/music.ts +++ b/src/test/music.ts @@ -7,7 +7,8 @@ import { groupslash, CommandContext, Extension, - Collection + Collection, + GuildTextChannel } from '../../mod.ts' import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' import { @@ -69,6 +70,17 @@ class MyClient extends CommandClient { d.respond({ content: 'sub-cmd-group worked' }) } + @command() + rmrf(ctx: CommandContext): any { + if (ctx.author.id !== '422957901716652033') return + ;((ctx.channel as any) as GuildTextChannel) + .bulkDelete(3) + .then((chan) => { + ctx.channel.send(`Bulk deleted 2 in ${chan}`) + }) + .catch((e) => ctx.channel.send(`${e.message}`)) + } + @slash() run(d: Interaction): void { console.log(d.name) diff --git a/src/test/slash-only.ts b/src/test/slash-only.ts index 00bb12b..1798eee 100644 --- a/src/test/slash-only.ts +++ b/src/test/slash-only.ts @@ -1,36 +1,16 @@ -import { SlashClient, SlashBuilder } from '../models/slashClient.ts' +import { SlashClient } from '../models/slashClient.ts' +import { SlashCommandPartial } from '../types/slash.ts' import { TOKEN } from './config.ts' -const slash = new SlashClient({ token: TOKEN }) +export const slash = new SlashClient({ token: TOKEN }) -slash.commands.all().then(console.log) +// Cmd objects come here +const commands: SlashCommandPartial[] = [] -const cmd = new SlashBuilder() - .name('searchmusic') - .description('Search for music.') - .option((o) => - o.string({ name: 'query', description: 'Query to search with.' }) - ) - .option((o) => - o.string({ - name: 'engine', - description: 'Search engine to use.', - choices: [{ name: 'YouTube', value: 'youtube' }, 'Spotify'] - }) - ) - .options({ - query: { - description: 'Query UWU', - type: 3 - }, - engine: { - description: 'Engine UWU', - type: 3, - choices: [ - { name: 'YouTube', value: 'youtube' }, - { name: 'Spotify', value: 'spotify' } - ] - } - }) - -console.log(JSON.stringify(cmd.export())) +console.log('Creating...') +commands.forEach((cmd) => { + slash.commands + .create(cmd, '!! Your testing guild ID comes here !!') + .then((c) => console.log(`Created command ${c.name}!`)) + .catch((e) => `Failed to create ${cmd.name} - ${e.message}`) +}) diff --git a/src/types/guild.ts b/src/types/guild.ts index b2b7b68..6825398 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -62,6 +62,7 @@ export interface MemberPayload { premium_since?: string deaf: boolean mute: boolean + pending?: boolean } export enum MessageNotification { @@ -113,6 +114,9 @@ export type GuildFeatures = | 'FEATURABLE' | 'ANIMATED_ICON' | 'BANNER' + | 'WELCOME_SCREEN_ENABLED' + | 'MEMBER_VERIFICATION_GATE_ENABLED' + | 'PREVIEW_ENABLED' export enum IntegrationExpireBehavior { REMOVE_ROLE = 0, diff --git a/src/types/slash.ts b/src/types/slash.ts index b56f100..1bd83ba 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -50,7 +50,7 @@ export interface SlashCommandChoice { /** (Display) name of the Choice */ name: string /** Actual value to be sent in Interaction */ - value: string + value: any } export enum SlashCommandOptionType { From 015b7951d8e854796a80fb6f47f54895cb37d7e6 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 23 Dec 2020 15:26:02 +0530 Subject: [PATCH 04/13] fix slash modules --- src/models/slashClient.ts | 14 +++++++++++++- src/test/music.ts | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index c652d58..e029e49 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -418,10 +418,22 @@ export class SlashClient { return this } + loadModule(module: SlashModule): SlashClient { + this.modules.push(module) + return this + } + getHandlers(): SlashCommandHandler[] { let res = this.handlers for (const mod of this.modules) { - res = [...res, ...mod.commands] + if (mod === undefined) continue + res = [ + ...res, + ...mod.commands.map((cmd) => { + cmd.handler = cmd.handler.bind(mod) + return cmd + }) + ] } return res } diff --git a/src/test/music.ts b/src/test/music.ts index bf654af..ba2ee26 100644 --- a/src/test/music.ts +++ b/src/test/music.ts @@ -218,9 +218,9 @@ class VCExtension extends Extension { const client = new MyClient() client.on('raw', (e, d) => { - if (e === 'READY') console.log(d) + if (e === 'GUILD_MEMBER_ADD' || e === 'GUILD_MEMBER_UPDATE') console.log(e, d) }) client.extensions.load(VCExtension) -client.connect(TOKEN, Intents.None) +client.connect(TOKEN, Intents.All) From d1fcce7b839246fba89484538cf187dc72f25812 Mon Sep 17 00:00:00 2001 From: ayntee Date: Fri, 25 Dec 2020 14:21:01 +0400 Subject: [PATCH 05/13] feat: add template structure and methods --- src/structures/template.ts | 70 ++++++++++++++++++++++++++++++++++++++ src/types/template.ts | 4 +-- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/structures/template.ts diff --git a/src/structures/template.ts b/src/structures/template.ts new file mode 100644 index 0000000..22224b0 --- /dev/null +++ b/src/structures/template.ts @@ -0,0 +1,70 @@ +import { Client } from '../models/client.ts' +import { TEMPLATE } from '../types/endpoint.ts' +import { TemplatePayload } from '../types/template.ts' +import { Base } from './base.ts' +import { Guild } from './guild.ts' +import { User } from './user.ts' + +export class Template extends Base { + /** the template code (unique ID) */ + code: string + /** template name */ + name: string + /** the description for the template */ + description: string | null + /** number of times this template has been used */ + usageCount: number + /** the ID of the user who created the template */ + creatorID: string + /** the user who created the template */ + creator: User + /** when this template was created (in ms) */ + createdAt: number + /** when this template was last synced to the source guild (in ms) */ + updatedAt: number + /** the ID of the guild this template is based on */ + sourceGuildID: string + /** the guild snapshot this template contains */ + serializedSourceGuild: Guild + /** whether the template has unsynced changes */ + isDirty: boolean | null + + constructor(client: Client, data: TemplatePayload) { + super(client, data) + this.code = data.code + this.name = data.name + this.description = data.description + this.usageCount = data.usage_count + this.creatorID = data.creator_id + this.creator = new User(client, data.creator) + this.createdAt = Date.parse(data.created_at) + this.updatedAt = Date.parse(data.updated_at) + this.sourceGuildID = data.source_guild_id + this.serializedSourceGuild = new Guild(client, data.serialized_source_guild) + this.isDirty = Boolean(data.is_dirty) + } + + /** Modifies the template's metadata. Requires the MANAGE_GUILD permission. Returns the template object on success. */ + async edit(data: ModifyGuildTemplateParams) { + const res = await this.client.rest.patch(TEMPLATE(this.code), data) + return res + } + + /** Deletes the template. Requires the MANAGE_GUILD permission. Returns the deleted template object on success. */ + async delete() { + const res = await this.client.rest.delete(TEMPLATE(this.code)) + return res + } + + /** Syncs the template to the guild's current state. Requires the MANAGE_GUILD permission. Returns the template object on success. */ + async sync() { + const res = await this.client.rest.put(TEMPLATE(this.code)) + return res + } +} + +/** https://discord.com/developers/docs/resources/template#modify-guild-template-json-params */ +export interface ModifyGuildTemplateParams { + name?: string + description?: string | null +} diff --git a/src/types/template.ts b/src/types/template.ts index 8128515..05fb0a8 100644 --- a/src/types/template.ts +++ b/src/types/template.ts @@ -4,7 +4,7 @@ import { UserPayload } from './user.ts' export interface TemplatePayload { code: string name: string - description: string | undefined + description: string | null usage_count: number creator_id: string creator: UserPayload @@ -12,5 +12,5 @@ export interface TemplatePayload { updated_at: string source_guild_id: string serialized_source_guild: GuildPayload - is_dirty: boolean | undefined + is_dirty: boolean | null } From be662a230b98ad0ae56308f32ebdcf5f2e833665 Mon Sep 17 00:00:00 2001 From: ayntee Date: Fri, 25 Dec 2020 14:59:24 +0400 Subject: [PATCH 06/13] chore: add return type and handle raw API payload --- src/structures/template.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/structures/template.ts b/src/structures/template.ts index 22224b0..b59d7c9 100644 --- a/src/structures/template.ts +++ b/src/structures/template.ts @@ -45,21 +45,21 @@ export class Template extends Base { } /** Modifies the template's metadata. Requires the MANAGE_GUILD permission. Returns the template object on success. */ - async edit(data: ModifyGuildTemplateParams) { + async edit(data: ModifyGuildTemplateParams): Promise