diff --git a/deps.ts b/deps.ts index 43fc90c..114c537 100644 --- a/deps.ts +++ b/deps.ts @@ -1,6 +1,6 @@ -export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' +export { EventEmitter } from 'https://deno.land/x/event@1.0.0/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 { 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 { walk } from 'https://deno.land/std@0.95.0/fs/walk.ts' +export { join } from 'https://deno.land/std@0.95.0/path/mod.ts' export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' diff --git a/mod.ts b/mod.ts index c7caa24..901d22a 100644 --- a/mod.ts +++ b/mod.ts @@ -194,3 +194,4 @@ export { default as getChannelByType } from './src/utils/channel.ts' export * from './src/utils/interactions.ts' +export * from "./src/utils/command.ts" diff --git a/src/cache/redis.ts b/src/cache/redis.ts index 8184f82..ce4ad70 100644 --- a/src/cache/redis.ts +++ b/src/cache/redis.ts @@ -4,7 +4,7 @@ import { connect, Redis, RedisConnectOptions -} from 'https://deno.land/x/redis@v0.14.1/mod.ts' +} from 'https://deno.land/x/redis@v0.22.0/mod.ts' /** Redis Cache Adapter for using Redis as a cache-provider. */ export class RedisCacheAdapter implements ICacheAdapter { diff --git a/src/commands/client.ts b/src/commands/client.ts index 22b0117..9442f4f 100644 --- a/src/commands/client.ts +++ b/src/commands/client.ts @@ -9,6 +9,7 @@ import { CommandsManager, parseCommand } from './command.ts' +import { parseArgs } from '../utils/command.ts' import { Extension, ExtensionsManager } from './extension.ts' type PrefixReturnType = string | string[] | Promise @@ -239,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions { client: this, name: parsed.name, prefix, - args: parsed.args, + args: parseArgs(command.args, parsed.args), argString: parsed.argString, message: msg, author: msg.author, diff --git a/src/commands/command.ts b/src/commands/command.ts index 987eaee..8601d70 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -6,7 +6,7 @@ import { Collection } from '../utils/collection.ts' import type { CommandClient } from './client.ts' import type { Extension } from './extension.ts' import { join, walk } from '../../deps.ts' - +import type { Args } from '../utils/command.ts' export interface CommandContext { /** The Client object */ client: CommandClient @@ -23,7 +23,7 @@ export interface CommandContext { /** Name of Command which was used */ name: string /** Array of Arguments used with Command */ - args: string[] + args: Record | null /** Complete Raw String of Arguments */ argString: string /** Guild which the command has called */ @@ -46,7 +46,7 @@ export interface CommandOptions { /** Usage Example of Command, only Arguments (without Prefix and Name) */ examples?: string | string[] /** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */ - args?: number | boolean | string[] + args?: Args[] /** Permissions(s) required by both User and Bot in order to use Command */ permissions?: string | string[] /** Permission(s) required for using Command */ @@ -81,7 +81,7 @@ export class Command implements CommandOptions { extension?: Extension usage?: string | string[] examples?: string | string[] - args?: number | boolean | string[] + args?: Args[] permissions?: string | string[] userPermissions?: string | string[] botPermissions?: string | string[] diff --git a/src/gateway/mod.ts b/src/gateway/mod.ts index 6764095..e31b527 100644 --- a/src/gateway/mod.ts +++ b/src/gateway/mod.ts @@ -399,7 +399,7 @@ export class Gateway extends HarmonyEventEmitter { await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`) } - this.close(1000, RECONNECT_REASON) + this.closeGateway(1000, RECONNECT_REASON) this.initWebsocket() } @@ -418,7 +418,7 @@ export class Gateway extends HarmonyEventEmitter { this.websocket.onerror = this.onerror.bind(this) as any } - close(code: number = 1000, reason?: string): void { + closeGateway(code: number = 1000, reason?: string): void { this.debug( `Closing with code ${code}${ reason !== undefined && reason !== '' ? ` and reason ${reason}` : '' diff --git a/src/managers/channels.ts b/src/managers/channels.ts index 69efe6c..8056de3 100644 --- a/src/managers/channels.ts +++ b/src/managers/channels.ts @@ -3,6 +3,7 @@ import { Channel } from '../structures/channel.ts' import { Embed } from '../structures/embed.ts' import { Message } from '../structures/message.ts' import type { TextChannel } from '../structures/textChannel.ts' +import type { User } from '../structures/user.ts' import type { ChannelPayload, GuildChannelPayload, @@ -19,6 +20,21 @@ export class ChannelsManager extends BaseManager { super(client, 'channels', Channel) } + async getUserDM(user: User | string): Promise { + return this.client.cache.get( + 'user_dms', + typeof user === 'string' ? user : user.id + ) + } + + async setUserDM(user: User | string, id: string): Promise { + await this.client.cache.set( + 'user_dms', + typeof user === 'string' ? user : user.id, + id + ) + } + // Override get method as Generic async get(key: string): Promise { const data = await this._get(key) @@ -97,7 +113,7 @@ export class ChannelsManager extends BaseManager { } const payload: any = { - content: content, + content: content ?? option?.content, embed: option?.embed, file: option?.file, files: option?.files, @@ -163,7 +179,7 @@ export class ChannelsManager extends BaseManager { const newMsg = await this.client.rest.api.channels[channelID].messages[ typeof message === 'string' ? message : message.id ].patch({ - content: text, + content: text ?? option?.content, embed: option?.embed !== undefined ? option.embed.toJSON() : undefined, // Cannot upload new files with Message // file: option?.file, diff --git a/src/rest/bucket.ts b/src/rest/bucket.ts index e8ec3a9..da9d3a3 100644 --- a/src/rest/bucket.ts +++ b/src/rest/bucket.ts @@ -10,11 +10,17 @@ import { RequestQueue } from './queue.ts' import { APIRequest } from './request.ts' function parseResponse(res: Response, raw: boolean): any { - if (raw) return res - if (res.status === 204) return undefined - if (res.headers.get('content-type')?.startsWith('application/json') === true) - return res.json() - return res.arrayBuffer().then((e) => new Uint8Array(e)) + let result + if (res.status === 204) result = Promise.resolve(undefined) + else if ( + res.headers.get('content-type')?.startsWith('application/json') === true + ) + result = res.json() + else result = res.arrayBuffer().then((e) => new Uint8Array(e)) + + if (raw) { + return { response: res, body: result } + } else return result } function getAPIOffset(serverDate: number | string): number { @@ -197,7 +203,7 @@ export class BucketHandler { let data try { - data = await parseResponse(res, request.options.rawResponse ?? false) + data = await parseResponse(res, false) } catch (err) { throw new HTTPError( err.message, diff --git a/src/structures/user.ts b/src/structures/user.ts index 4f1ede0..0a085be 100644 --- a/src/structures/user.ts +++ b/src/structures/user.ts @@ -6,6 +6,8 @@ import { ImageURL } from './cdn.ts' import type { ImageSize, ImageFormats } from '../types/cdn.ts' import { DEFAULT_USER_AVATAR, USER_AVATAR } from '../types/endpoint.ts' import type { DMChannel } from './dmChannel.ts' +import { AllMessageOptions } from './textChannel.ts' +import { Message } from './message.ts' export class User extends SnowflakeBase { id: string @@ -94,4 +96,25 @@ export class User extends SnowflakeBase { async createDM(): Promise { return this.client.createDM(this) } + + async resolveDM(): Promise { + const dmID = await this.client.channels.getUserDM(this.id) + const dm = + (dmID !== undefined + ? await this.client.channels.get(dmID) + : undefined) ?? + (await this.createDM().then((chan) => + this.client.channels.setUserDM(this.id, chan.id).then(() => chan) + )) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + return dm! + } + + async send( + content: string | AllMessageOptions, + options?: AllMessageOptions + ): Promise { + const dm = await this.resolveDM() + return dm.send(content, options) + } } diff --git a/src/types/channel.ts b/src/types/channel.ts index 25eb484..ee6e839 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -204,6 +204,7 @@ export interface AllowedMentionsPayload { } export interface MessageOptions { + content?: string tts?: boolean embed?: Embed file?: MessageAttachment diff --git a/src/utils/command.ts b/src/utils/command.ts new file mode 100644 index 0000000..4eda5bc --- /dev/null +++ b/src/utils/command.ts @@ -0,0 +1,107 @@ +interface MentionToRegex { + [key: string]: RegExp + mentionUser: RegExp + mentionRole: RegExp + mentionChannel: RegExp +} + +const mentionToRegex: MentionToRegex = { + mentionUser: /<@!?(\d{17,19})>/, + mentionRole: /<@&(\d{17,19})>/, + mentionChannel: /<#(\d{17,19})>/ +} + +export type CommandArgumentMatchTypes = + | 'flag' + | 'mentionUser' + | 'mentionRole' + | 'mentionChannel' + | 'content' + | 'rest' + +export interface Args { + name: string + match: CommandArgumentMatchTypes + defaultValue?: T + flag?: string +} + +export function parseArgs( + commandArgs: Args[] | undefined, + messageArgs: string[] +): Record | null { + if (commandArgs === undefined) return null + + const messageArgsNullableCopy: Array = [...messageArgs] + const args: Record = {} + + for (const entry of commandArgs) { + switch (entry.match) { + case 'flag': + parseFlags(args, entry, messageArgsNullableCopy) + break + case 'mentionUser': + case 'mentionRole': + case 'mentionChannel': + parseMention(args, entry, messageArgsNullableCopy) + break + case 'content': + parseContent(args, entry, messageArgs) + break + case 'rest': + parseRest(args, entry, messageArgsNullableCopy) + break + } + } + return args +} + +function parseFlags( + args: Record, + entry: Args, + argsNullable: Array +): void { + for (let i = 0; i < argsNullable.length; i++) { + if (entry.flag === argsNullable[i]) { + argsNullable[i] = null + args[entry.name] = true + break + } else args[entry.name] = entry.defaultValue ?? false + } +} + +function parseMention( + args: Record, + entry: Args, + argsNullable: Array +): void { + const regex = mentionToRegex[entry.match] + const index = argsNullable.findIndex( + (x) => typeof x === 'string' && regex.test(x) + ) + const regexMatches = regex.exec(argsNullable[index]!) + args[entry.name] = + regexMatches !== null + ? regexMatches[0].replace(regex, '$1') + : entry.defaultValue + argsNullable[index] = null +} + +function parseContent( + args: Record, + entry: Args, + argsNonNullable: Array +): void { + args[entry.name] = + argsNonNullable.length > 0 ? argsNonNullable : entry.defaultValue +} + +function parseRest( + args: Record, + entry: Args, + argsNullable: Array +): void { + const restValues = argsNullable.filter((x) => typeof x === 'string') + args[entry.name] = + restValues !== null ? restValues?.join(' ') : entry.defaultValue +} diff --git a/test/argsparser_test.ts b/test/argsparser_test.ts new file mode 100644 index 0000000..f428246 --- /dev/null +++ b/test/argsparser_test.ts @@ -0,0 +1,150 @@ +import { Args, parseArgs } from '../src/utils/command.ts' +import { + assertEquals, + assertNotEquals +} from 'https://deno.land/std@0.95.0/testing/asserts.ts' + +const commandArgs: Args[] = [ + { + name: 'originalMessage', + match: 'content' + }, + { + name: 'permaban', + match: 'flag', + flag: '--permanent', + defaultValue: true + }, + { + name: 'user', + match: 'mentionUser' + }, + { + name: 'reason', + match: 'rest', + defaultValue: 'ree' + } +] + +const messageArgs1: string[] = [ + '<@!708544768342229012>', + '--permanent', + 'bye', + 'bye', + 'Skyler' +] +const expectedResult1 = { + originalMessage: [ + '<@!708544768342229012>', + '--permanent', + 'bye', + 'bye', + 'Skyler' + ], + permaban: true, + user: '708544768342229012', + reason: 'bye bye Skyler' +} + +Deno.test({ + only: false, + name: 'parse command arguments 1 (assertEquals)', + fn: () => { + const result = parseArgs(commandArgs, messageArgs1) + assertEquals(result, expectedResult1) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const messageArgs2: string[] = [ + '<@!708544768342229012>', + 'bye', + 'bye', + 'Skyler' +] +const expectedResult2 = { + originalMessage: ['<@!708544768342229012>', 'bye', 'bye', 'Skyler'], + permaban: true, + user: '708544768342229012', + reason: 'bye bye Skyler' +} + +Deno.test({ + name: 'parse command arguments 2 (assertEquals)', + fn: () => { + const result = parseArgs(commandArgs, messageArgs2) + assertEquals(result, expectedResult2) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const messageArgs3: string[] = [ + '<@!708544768342229012>', + 'bye', + 'bye', + 'Skyler' +] +const expectedResult3 = { + permaban: false, + user: '708544768342229012', + reason: 'bye bye Skyler' +} + +Deno.test({ + name: 'parse command arguments default value (assertNotEquals)', + fn: () => { + const result = parseArgs(commandArgs, messageArgs3) + assertNotEquals(result, expectedResult3) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) + +const commandArgs2: Args[] = [ + { + name: 'user', + match: 'mentionUser' + }, + { + name: 'channel', + match: 'mentionChannel' + }, + { + name: 'role', + match: 'mentionRole' + }, + { + name: 'reason', + match: 'rest', + defaultValue: 'ree' + } +] + +const messageArgs4: string[] = [ + '<@!708544768342229012>', + 'bye', + '<#783319033730564098>', + '<@&836715188690092032>' +] +const expectedResult4 = { + channel: '783319033730564098', + role: '836715188690092032', + user: '708544768342229012', + reason: 'bye' +} + +Deno.test({ + name: 'parse command arguments mentions (assertEquals)', + fn: () => { + const result = parseArgs(commandArgs2, messageArgs4) + assertEquals(result, expectedResult4) + }, + sanitizeOps: true, + sanitizeResources: true, + sanitizeExit: true +}) diff --git a/test/deps.ts b/test/deps.ts new file mode 100644 index 0000000..960facd --- /dev/null +++ b/test/deps.ts @@ -0,0 +1,2 @@ +export * from 'https://deno.land/std@0.95.0/testing/asserts.ts' +export * from 'https://deno.land/std@0.95.0/http/server.ts' diff --git a/test/index.ts b/test/index.ts index 872f457..fa4d8ff 100644 --- a/test/index.ts +++ b/test/index.ts @@ -254,6 +254,13 @@ client.on('messageCreate', async (msg: Message) => { buf += `\n${role.name === '@everyone' ? 'everyone' : role.name}` } msg.reply(buf) + } else if (msg.content === '!addrole') { + msg.member?.roles.add('837255383759716362') + } else if (msg.content === '!dm') { + console.log('wtf') + msg.author.send('UwU').then((m) => { + msg.reply(`Done, ${m.id}`) + }) } else if (msg.content === '!timer') { msg.channel.send('3...').then((msg) => { setTimeout(() => { diff --git a/test/slash-http.ts b/test/slash-http.ts index 6ed7188..3ab4345 100644 --- a/test/slash-http.ts +++ b/test/slash-http.ts @@ -1,6 +1,6 @@ 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' +import { listenAndServe } from './deps.ts' const slash = new SlashClient({ id: SLASH_ID, diff --git a/test/unit.ts b/test/unit.ts index 5395f69..af98d46 100644 --- a/test/unit.ts +++ b/test/unit.ts @@ -5,7 +5,7 @@ import { TOKEN } from '../src/test/config.ts' import { assertEquals, assertExists -} from 'https://deno.land/std@0.84.0/testing/asserts.ts' +} from './deps.ts' //#region Lib Tests Deno.test({