Merge branch 'main' into reaction

This commit is contained in:
Helloyunho 2020-12-26 12:04:59 +09:00 committed by GitHub
commit 65b5c50ec3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 615 additions and 126 deletions

View File

@ -10,15 +10,12 @@
<br>
- 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!

13
deps.ts Normal file
View File

@ -0,0 +1,13 @@
export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts'
export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'
export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts'
export type {
Redis,
RedisConnectOptions
} from 'https://deno.land/x/redis@v0.14.1/mod.ts'
export {
Manager,
Player
} from 'https://raw.githubusercontent.com/Lavaclient/lavadeno/master/mod.ts'

3
mod.ts
View File

@ -1,5 +1,4 @@
export { GatewayIntents } from './src/types/gateway.ts'
export { default as EventEmitter } from 'https://deno.land/std@0.74.0/node/events.ts'
export { Base } from './src/structures/base.ts'
export { Gateway } from './src/gateway/index.ts'
export type { ClientEvents } from './src/gateway/handlers/index.ts'
@ -66,7 +65,7 @@ export {
ActivityTypes
} from './src/structures/presence.ts'
export { Role } from './src/structures/role.ts'
export { Snowflake } from './src/structures/snowflake.ts'
export { Snowflake } from './src/utils/snowflake.ts'
export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts'
export { MessageReaction } from './src/structures/messageReaction.ts'
export { User } from './src/structures/user.ts'

View File

@ -1,4 +1,4 @@
import { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
import { unzlib, EventEmitter } from '../../deps.ts'
import { Client } from '../models/client.ts'
import {
DISCORD_GATEWAY_URL,
@ -18,7 +18,6 @@ import { GatewayCache } from '../managers/gatewayCache.ts'
import { delay } from '../utils/delay.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import { Guild } from '../structures/guild.ts'
import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
export interface RequestMembersOptions {
limit?: number
@ -177,55 +176,61 @@ export class Gateway extends EventEmitter {
}
}
private async onclose(event: CloseEvent): Promise<void> {
if (event.reason === RECONNECT_REASON) return
this.emit('close', event.code, event.reason)
this.debug(`Connection Closed with code: ${event.code}`)
private async onclose({ reason, code }: CloseEvent): Promise<void> {
if (reason === RECONNECT_REASON) return
this.emit('close', code, reason)
this.debug(`Connection Closed with code: ${code}`)
if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) {
this.debug('API has encountered Unknown Error. Reconnecting...')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
} else if (event.code === GatewayCloseCodes.UNKNOWN_OPCODE) {
throw new Error("Unknown OP Code was sent. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.DECODE_ERROR) {
throw new Error("Invalid Payload was sent. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.NOT_AUTHENTICATED) {
throw new Error('Not Authorized: Payload was sent before Identifying.')
} else if (event.code === GatewayCloseCodes.AUTHENTICATION_FAILED) {
throw new Error('Invalid Token provided!')
} else if (event.code === GatewayCloseCodes.INVALID_SEQ) {
this.debug('Invalid Seq was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
} else if (event.code === GatewayCloseCodes.RATE_LIMITED) {
throw new Error("You're ratelimited. Calm down.")
} else if (event.code === GatewayCloseCodes.SESSION_TIMED_OUT) {
this.debug('Session Timeout. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect(true)
} else if (event.code === GatewayCloseCodes.INVALID_SHARD) {
this.debug('Invalid Shard was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
} else if (event.code === GatewayCloseCodes.SHARDING_REQUIRED) {
throw new Error("Couldn't connect. Sharding is required!")
} else if (event.code === GatewayCloseCodes.INVALID_API_VERSION) {
throw new Error("Invalid API Version was used. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.INVALID_INTENTS) {
throw new Error('Invalid Intents')
} else if (event.code === GatewayCloseCodes.DISALLOWED_INTENTS) {
throw new Error("Given Intents aren't allowed")
} else {
this.debug(
'Unknown Close code, probably connection error. Reconnecting in 5s.'
)
if (this.timedIdentify !== null) {
clearTimeout(this.timedIdentify)
this.debug('Timed Identify found. Cleared timeout.')
}
await delay(5000)
await this.reconnect(true)
switch (code) {
case GatewayCloseCodes.UNKNOWN_ERROR:
this.debug('API has encountered Unknown Error. Reconnecting...')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
break
case GatewayCloseCodes.UNKNOWN_OPCODE:
throw new Error("Unknown OP Code was sent. This shouldn't happen!")
case GatewayCloseCodes.DECODE_ERROR:
throw new Error("Invalid Payload was sent. This shouldn't happen!")
case GatewayCloseCodes.NOT_AUTHENTICATED:
throw new Error('Not Authorized: Payload was sent before Identifying.')
case GatewayCloseCodes.AUTHENTICATION_FAILED:
throw new Error('Invalid Token provided!')
case GatewayCloseCodes.INVALID_SEQ:
this.debug('Invalid Seq was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
break
case GatewayCloseCodes.RATE_LIMITED:
throw new Error("You're ratelimited. Calm down.")
case GatewayCloseCodes.SESSION_TIMED_OUT:
this.debug('Session Timeout. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect(true)
break
case GatewayCloseCodes.INVALID_SHARD:
this.debug('Invalid Shard was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
break
case GatewayCloseCodes.SHARDING_REQUIRED:
throw new Error("Couldn't connect. Sharding is required!")
case GatewayCloseCodes.INVALID_API_VERSION:
throw new Error("Invalid API Version was used. This shouldn't happen!")
case GatewayCloseCodes.INVALID_INTENTS:
throw new Error('Invalid Intents')
case GatewayCloseCodes.DISALLOWED_INTENTS:
throw new Error("Given Intents aren't allowed")
default:
this.debug(
'Unknown Close code, probably connection error. Reconnecting in 5s.'
)
if (this.timedIdentify !== null) {
clearTimeout(this.timedIdentify)
this.debug('Timed Identify found. Cleared timeout.')
}
await delay(5000)
await this.reconnect(true)
break
}
}

View File

@ -6,7 +6,7 @@ import { EmojiPayload } from '../types/emoji.ts'
import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts'
import { BaseChildManager } from './baseChild.ts'
import { EmojisManager } from './emojis.ts'
import { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
import { fetchAuto } from '../../deps.ts'
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
guild: Guild

View File

@ -3,7 +3,7 @@ import {
connect,
Redis,
RedisConnectOptions
} from 'https://denopkg.com/keroxp/deno-redis/mod.ts'
} from '../../deps.ts'
/**
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony.

View File

@ -2,7 +2,7 @@ import { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts'
import { RESTManager } from './rest.ts'
import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
import { EventEmitter } from '../../deps.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UsersManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts'
@ -247,6 +247,7 @@ export function event(name?: string) {
}
}
/** Decorator to create a Slash Command handler */
export function slash(name?: string, guild?: string) {
return function (client: Client | SlashModule, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
@ -262,6 +263,7 @@ export function slash(name?: string, guild?: string) {
}
}
/** Decorator to create a Sub-Slash Command handler */
export function subslash(parent: string, name?: string, guild?: string) {
return function (client: Client | SlashModule, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
@ -279,13 +281,14 @@ export function subslash(parent: string, name?: string, guild?: string) {
}
}
/** Decorator to create a Grouped Slash Command handler */
export function groupslash(
parent: string,
group: string,
name?: string,
guild?: string
) {
return function (client: Client | SlashModule, prop: string) {
return function (client: Client | SlashModule | SlashClient, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop]
if (typeof item !== 'function') {
@ -303,6 +306,7 @@ export function groupslash(
}
}
/** Decorator to add a Slash Module to Client */
export function slashModule() {
return function (client: Client, prop: string) {
if (client._decoratedSlashModules === undefined)

View File

@ -5,7 +5,7 @@ import { User } from '../structures/user.ts'
import { Collection } from '../utils/collection.ts'
import { CommandClient } from './commandClient.ts'
import { Extension } from './extensions.ts'
import { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'
import { parse } from '../../deps.ts'
export interface CommandContext {
/** The Client object */

View File

@ -97,6 +97,7 @@ export interface RESTOptions {
token?: string
headers?: { [name: string]: string | undefined }
canary?: boolean
version?: 6 | 7 | 8
}
export class RESTManager {
@ -111,6 +112,7 @@ export class RESTManager {
constructor(client?: RESTOptions) {
this.client = client
this.api = builder(this)
if (client?.version !== undefined) this.version = client.version
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.handleRateLimits()
}
@ -408,6 +410,7 @@ export class RESTManager {
const query =
method === 'get' && body !== undefined
? Object.entries(body as any)
.filter(([k, v]) => v !== undefined)
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(

View File

@ -1,6 +1,6 @@
import { Collection } from '../utils/collection.ts'
import { Client, ClientOptions } from './client.ts'
import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
import {EventEmitter} from '../../deps.ts'
import { RESTManager } from './rest.ts'
// import { GATEWAY_BOT } from '../types/endpoint.ts'
// import { GatewayBotPayload } from '../types/gatewayBot.ts'

View File

@ -1,20 +1,24 @@
import { Guild } from '../structures/guild.ts'
import { Interaction } from '../structures/slash.ts'
import {
APPLICATION_COMMAND,
APPLICATION_COMMANDS,
APPLICATION_GUILD_COMMAND,
APPLICATION_GUILD_COMMANDS
} from '../types/endpoint.ts'
import {
InteractionType,
SlashCommandChoice,
SlashCommandOption,
SlashCommandOptionType,
SlashCommandPartial,
SlashCommandPayload
} from '../types/slash.ts'
import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts'
import { RESTManager } from './rest.ts'
import { SlashModule } from './slashModule.ts'
import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts'
import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts'
import {
Request as ORequest,
Response as OResponse
} from 'https://deno.land/x/opine@1.0.0/src/types.ts'
import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts'
export class SlashCommand {
slash: SlashCommandsManager
@ -41,6 +45,158 @@ export class SlashCommand {
async edit(data: SlashCommandPartial): Promise<void> {
await this.slash.edit(this.id, data, this._guild)
}
/** Create a handler for this Slash Command */
handle(
func: SlashCommandHandlerCallback,
options?: { parent?: string; group?: string }
): SlashCommand {
this.slash.slash.handle({
name: this.name,
parent: options?.parent,
group: options?.group,
guild: this._guild,
handler: func
})
return this
}
}
export interface CreateOptions {
name: string
description?: string
options?: Array<SlashCommandOption | SlashOptionCallable>
choices?: Array<SlashCommandChoice | string>
}
function createSlashOption(
type: SlashCommandOptionType,
data: CreateOptions
): SlashCommandOption {
return {
name: data.name,
type,
description:
type === 0 || type === 1
? undefined
: data.description ?? 'No description.',
options: data.options?.map((e) =>
typeof e === 'function' ? e(SlashOption) : e
),
choices:
data.choices === undefined
? undefined
: data.choices.map((e) =>
typeof e === 'string' ? { name: e, value: e } : e
)
}
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class SlashOption {
static string(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.STRING, data)
}
static bool(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.BOOLEAN, data)
}
static subCommand(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data)
}
static subCommandGroup(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data)
}
static role(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.ROLE, data)
}
static channel(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.CHANNEL, data)
}
static user(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.USER, data)
}
static number(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.INTEGER, data)
}
}
export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption
export type SlashBuilderOptionsData =
| Array<SlashCommandOption | SlashOptionCallable>
| {
[name: string]:
| {
description: string
type: SlashCommandOptionType
options?: SlashCommandOption[]
choices?: SlashCommandChoice[]
}
| SlashOptionCallable
}
function buildOptionsArray(
options: SlashBuilderOptionsData
): SlashCommandOption[] {
return Array.isArray(options)
? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op))
: Object.entries(options).map((entry) =>
typeof entry[1] === 'function'
? entry[1](SlashOption)
: Object.assign(entry[1], { name: entry[0] })
)
}
export class SlashBuilder {
data: SlashCommandPartial
constructor(
name?: string,
description?: string,
options?: SlashBuilderOptionsData
) {
this.data = {
name: name ?? '',
description: description ?? 'No description.',
options: options === undefined ? [] : buildOptionsArray(options)
}
}
name(name: string): SlashBuilder {
this.data.name = name
return this
}
description(desc: string): SlashBuilder {
this.data.description = desc
return this
}
option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder {
if (this.data.options === undefined) this.data.options = []
this.data.options.push(
typeof option === 'function' ? option(SlashOption) : option
)
return this
}
options(options: SlashBuilderOptionsData): SlashBuilder {
this.data.options = buildOptionsArray(options)
return this
}
export(): SlashCommandPartial {
if (this.data.name === '')
throw new Error('Name was not provided in Slash Builder')
return this.data
}
}
export class SlashCommandsManager {
@ -58,9 +214,9 @@ export class SlashCommandsManager {
async all(): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
const res = (await this.rest.get(
APPLICATION_COMMANDS(this.slash.getID())
)) as SlashCommandPayload[]
const res = (await this.rest.api.applications[
this.slash.getID()
].commands.get()) as SlashCommandPayload[]
if (!Array.isArray(res)) return col
for (const raw of res) {
@ -77,12 +233,9 @@ export class SlashCommandsManager {
): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
const res = (await this.rest.get(
APPLICATION_GUILD_COMMANDS(
this.slash.getID(),
typeof guild === 'string' ? guild : guild.id
)
)) as SlashCommandPayload[]
const res = (await this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands.get()) as SlashCommandPayload[]
if (!Array.isArray(res)) return col
for (const raw of res) {
@ -99,15 +252,14 @@ export class SlashCommandsManager {
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommand> {
const payload = await this.rest.post(
const route =
guild === undefined
? APPLICATION_COMMANDS(this.slash.getID())
: APPLICATION_GUILD_COMMANDS(
this.slash.getID(),
? this.rest.api.applications[this.slash.getID()].commands
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
),
data
)
].commands
const payload = await route.post(data)
const cmd = new SlashCommand(this, payload)
cmd._guild =
@ -122,16 +274,14 @@ export class SlashCommandsManager {
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommandsManager> {
await this.rest.patch(
const route =
guild === undefined
? APPLICATION_COMMAND(this.slash.getID(), id)
: APPLICATION_GUILD_COMMAND(
this.slash.getID(),
typeof guild === 'string' ? guild : guild.id,
id
),
data
)
? this.rest.api.applications[this.slash.getID()].commands[id]
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands[id]
await route.patch(data)
return this
}
@ -140,29 +290,28 @@ export class SlashCommandsManager {
id: string,
guild?: Guild | string
): Promise<SlashCommandsManager> {
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<SlashCommand> {
const data = await this.rest.get(
const route =
guild === undefined
? APPLICATION_COMMAND(this.slash.getID(), id)
: APPLICATION_GUILD_COMMAND(
this.slash.getID(),
typeof guild === 'string' ? guild : guild.id,
id
)
)
? this.rest.api.applications[this.slash.getID()].commands[id]
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands[id]
const data = await route.get()
return new SlashCommand(this, data)
}
}
@ -182,6 +331,7 @@ export interface SlashOptions {
enabled?: boolean
token?: string
rest?: RESTManager
publicKey?: string
}
export class SlashClient {
@ -192,6 +342,18 @@ export class SlashClient {
commands: SlashCommandsManager
handlers: SlashCommandHandler[] = []
rest: RESTManager
modules: SlashModule[] = []
publicKey?: string
_decoratedSlash?: Array<{
name: string
guild?: string
parent?: string
group?: string
handler: (interaction: Interaction) => any
}>
_decoratedSlashModules?: SlashModule[]
constructor(options: SlashOptions) {
let id = options.id
@ -202,6 +364,7 @@ export class SlashClient {
this.client = options.client
this.token = options.token
this.commands = new SlashCommandsManager(this)
this.publicKey = options.publicKey
if (options !== undefined) {
this.enabled = options.enabled ?? true
@ -213,6 +376,24 @@ export class SlashClient {
})
}
if (this.client?._decoratedSlashModules !== undefined) {
this.client._decoratedSlashModules.forEach((e) => {
this.modules.push(e)
})
}
if (this._decoratedSlash !== undefined) {
this._decoratedSlash.forEach((e) => {
this.handlers.push(e)
})
}
if (this._decoratedSlashModules !== undefined) {
this._decoratedSlashModules.forEach((e) => {
this.modules.push(e)
})
}
this.rest =
options.client === undefined
? options.rest === undefined
@ -237,8 +418,28 @@ export class SlashClient {
return this
}
loadModule(module: SlashModule): SlashClient {
this.modules.push(module)
return this
}
getHandlers(): SlashCommandHandler[] {
let res = this.handlers
for (const mod of this.modules) {
if (mod === undefined) continue
res = [
...res,
...mod.commands.map((cmd) => {
cmd.handler = cmd.handler.bind(mod)
return cmd
})
]
}
return res
}
private _getCommand(i: Interaction): SlashCommandHandler | undefined {
return this.handlers.find((e) => {
return this.getHandlers().find((e) => {
const hasGroupOrParent = e.group !== undefined || e.parent !== undefined
const groupMatched =
e.group !== undefined && e.parent !== undefined
@ -271,4 +472,78 @@ export class SlashClient {
cmd.handler(interaction)
}
async verifyKey(
rawBody: string | Uint8Array | Buffer,
signature: string,
timestamp: string
): Promise<boolean> {
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<boolean> {
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<any> {
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<any> {
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
}
}

View File

@ -1,5 +1,6 @@
import { Client } from '../models/client.ts'
import { ChannelPayload } from '../types/channel.ts'
import { INVITE } from '../types/endpoint.ts'
import { GuildPayload } from '../types/guild.ts'
import { InvitePayload } from '../types/invite.ts'
import { UserPayload } from '../types/user.ts'
@ -31,6 +32,12 @@ export class Invite extends Base {
this.approximatePresenceCount = data.approximate_presence_count
}
/** Delete an invite. Requires the MANAGE_CHANNELS permission on the channel this invite belongs to, or MANAGE_GUILD to remove any invite across the guild. Returns an invite object on success. Fires a Invite Delete Gateway event. */
async delete(): Promise<Invite> {
const res = await this.client.rest.delete(INVITE(this.code))
return new Invite(this.client, res)
}
readFromData(data: InvitePayload): void {
this.code = data.code ?? this.code
this.guild = data.guild ?? this.guild

View File

@ -47,6 +47,10 @@ export class Message extends Base {
flags?: number
stickers?: MessageSticker[]
get createdAt(): Date {
return new Date(this.timestamp)
}
constructor(
client: Client,
data: MessagePayload,

View File

@ -0,0 +1,70 @@
import { Client } from '../models/client.ts'
import { TEMPLATE } from '../types/endpoint.ts'
import { TemplatePayload } from '../types/template.ts'
import { Base } from './base.ts'
import { Guild } from './guild.ts'
import { User } from './user.ts'
export class Template extends Base {
/** The template code (unique ID) */
code: string
/** The template name */
name: string
/** The description for the template */
description: string | null
/** Number of times this template has been used */
usageCount: number
/** The ID of the user who created the template */
creatorID: string
/** The user who created the template */
creator: User
/** When this template was created (in ms) */
createdAt: number
/** When this template was last synced to the source guild (in ms) */
updatedAt: number
/** The ID of the guild this template is based on */
sourceGuildID: string
/** The guild snapshot this template contains */
serializedSourceGuild: Guild
/** Whether the template has unsynced changes */
isDirty: boolean | null
constructor(client: Client, data: TemplatePayload) {
super(client, data)
this.code = data.code
this.name = data.name
this.description = data.description
this.usageCount = data.usage_count
this.creatorID = data.creator_id
this.creator = new User(client, data.creator)
this.createdAt = Date.parse(data.created_at)
this.updatedAt = Date.parse(data.updated_at)
this.sourceGuildID = data.source_guild_id
this.serializedSourceGuild = new Guild(client, data.serialized_source_guild)
this.isDirty = Boolean(data.is_dirty)
}
/** Modifies the template's metadata. Requires the MANAGE_GUILD permission. Returns the template object on success. */
async edit(data: ModifyGuildTemplateParams): Promise<Template> {
const res = await this.client.rest.patch(TEMPLATE(this.code), data)
return new Template(this.client, res)
}
/** Deletes the template. Requires the MANAGE_GUILD permission. Returns the deleted template object on success. */
async delete(): Promise<Template> {
const res = await this.client.rest.delete(TEMPLATE(this.code))
return new Template(this.client, res)
}
/** Syncs the template to the guild's current state. Requires the MANAGE_GUILD permission. Returns the template object on success. */
async sync(): Promise<Template> {
const res = await this.client.rest.put(TEMPLATE(this.code))
return new Template(this.client, res)
}
}
/** https://discord.com/developers/docs/resources/template#modify-guild-template-json-params */
export interface ModifyGuildTemplateParams {
name?: string
description?: string | null
}

View File

@ -3,6 +3,7 @@ import { Client } from '../models/client.ts'
import {
GuildTextChannelPayload,
MessageOption,
MessagePayload,
MessageReference,
ModifyGuildTextChannelOption,
ModifyGuildTextChannelPayload,
@ -16,6 +17,7 @@ import {
MESSAGE_REACTION_ME,
MESSAGE_REACTION_USER
} from '../types/endpoint.ts'
import { Collection } from '../utils/collection.ts'
import { Channel } from './channel.ts'
import { Embed } from './embed.ts'
import { Emoji } from './emoji.ts'
@ -177,6 +179,47 @@ export class TextChannel extends Channel {
MESSAGE_REACTION_USER(this.id, message, encodedEmoji, user)
)
}
/**
* 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<Collection<string, Message>> {
const res = new Collection<string, Message>()
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
}
}
@ -239,4 +282,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<Message | string> | number
): Promise<GuildTextChannel> {
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
}
}

View File

@ -11,7 +11,7 @@ import { Embed } from './embed.ts'
import { Message } from './message.ts'
import { TextChannel } from './textChannel.ts'
import { User } from './user.ts'
import { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
import { fetchAuto } from '../../deps.ts'
import { WEBHOOK_MESSAGE } from '../types/endpoint.ts'
export interface WebhookMessageOptions extends MessageOption {

View File

@ -7,13 +7,14 @@ import {
groupslash,
CommandContext,
Extension,
Collection
Collection,
GuildTextChannel
} from '../../mod.ts'
import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts'
import {
Manager,
Player
} from 'https://raw.githubusercontent.com/Lavaclient/lavadeno/master/mod.ts'
} from '../../deps.ts'
import { Interaction } from '../structures/slash.ts'
import { slash } from '../models/client.ts'
// import { SlashCommandOptionType } from '../types/slash.ts'
@ -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)
@ -205,6 +217,10 @@ class VCExtension extends Extension {
const client = new MyClient()
client.on('raw', (e, 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)

View File

@ -1,6 +1,16 @@
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[] = []
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}`)
})

View File

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

View File

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

View File

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