Merge branch 'main' into design-fix

This commit is contained in:
Helloyunho 2021-04-30 23:47:13 +09:00 committed by GitHub
commit 461e1557c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 335 additions and 21 deletions

View File

@ -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'

1
mod.ts
View File

@ -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"

2
src/cache/redis.ts vendored
View File

@ -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 {

View File

@ -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<string | string[]>
@ -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,

View File

@ -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<string, unknown> | 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[]

View File

@ -399,7 +399,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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<GatewayTypedEvents> {
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}` : ''

View File

@ -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<ChannelPayload, Channel> {
super(client, 'channels', Channel)
}
async getUserDM(user: User | string): Promise<string | undefined> {
return this.client.cache.get(
'user_dms',
typeof user === 'string' ? user : user.id
)
}
async setUserDM(user: User | string, id: string): Promise<void> {
await this.client.cache.set(
'user_dms',
typeof user === 'string' ? user : user.id,
id
)
}
// Override get method as Generic
async get<T = Channel>(key: string): Promise<T | undefined> {
const data = await this._get(key)
@ -97,7 +113,7 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
}
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<ChannelPayload, Channel> {
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,

View File

@ -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,

View File

@ -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<DMChannel> {
return this.client.createDM(this)
}
async resolveDM(): Promise<DMChannel> {
const dmID = await this.client.channels.getUserDM(this.id)
const dm =
(dmID !== undefined
? await this.client.channels.get<DMChannel>(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<Message> {
const dm = await this.resolveDM()
return dm.send(content, options)
}
}

View File

@ -204,6 +204,7 @@ export interface AllowedMentionsPayload {
}
export interface MessageOptions {
content?: string
tts?: boolean
embed?: Embed
file?: MessageAttachment

107
src/utils/command.ts Normal file
View File

@ -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<T = unknown> {
name: string
match: CommandArgumentMatchTypes
defaultValue?: T
flag?: string
}
export function parseArgs(
commandArgs: Args[] | undefined,
messageArgs: string[]
): Record<string, unknown> | null {
if (commandArgs === undefined) return null
const messageArgsNullableCopy: Array<string | null> = [...messageArgs]
const args: Record<string, unknown> = {}
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<string, unknown>,
entry: Args,
argsNullable: Array<string | null>
): 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<string, unknown>,
entry: Args,
argsNullable: Array<string | null>
): 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<string, unknown>,
entry: Args,
argsNonNullable: Array<string | null>
): void {
args[entry.name] =
argsNonNullable.length > 0 ? argsNonNullable : entry.defaultValue
}
function parseRest(
args: Record<string, unknown>,
entry: Args,
argsNullable: Array<string | null>
): void {
const restValues = argsNullable.filter((x) => typeof x === 'string')
args[entry.name] =
restValues !== null ? restValues?.join(' ') : entry.defaultValue
}

150
test/argsparser_test.ts Normal file
View File

@ -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
})

2
test/deps.ts Normal file
View File

@ -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'

View File

@ -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(() => {

View File

@ -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,

View File

@ -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({