lot of stuff

This commit is contained in:
DjDeveloperr 2021-02-10 17:59:21 +05:30
parent e0edb1a088
commit 086e4a95c3
8 changed files with 302 additions and 72 deletions

View file

@ -7,3 +7,5 @@ export type {
Redis, Redis,
RedisConnectOptions RedisConnectOptions
} from 'https://deno.land/x/redis@v0.14.1/mod.ts' } from 'https://deno.land/x/redis@v0.14.1/mod.ts'
export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts'
export { join } from 'https://deno.land/std@0.86.0/path/mod.ts'

View file

@ -8,13 +8,30 @@ export const interactionCreate: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,
d: InteractionPayload d: InteractionPayload
) => { ) => {
const guild = await gateway.client.guilds.get(d.guild_id) // NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction.
// This case can be seen in future proofing Interactions, and one he mentioned was
// that bots will be able to add custom context menus. In that case, Interaction will not have it.
// Ref: https://github.com/discord/discord-api-docs/pull/2568/files#r569025697
if (d.channel_id === undefined) return
const guild =
d.guild_id === undefined
? undefined
: await gateway.client.guilds.get(d.guild_id)
if (guild === undefined) return if (guild === undefined) return
if (d.member !== undefined)
await guild.members.set(d.member.user.id, d.member) await guild.members.set(d.member.user.id, d.member)
const member = ((await guild.members.get( const member =
d.member.user.id d.member !== undefined
)) as unknown) as Member ? (((await guild.members.get(d.member.user.id)) as unknown) as Member)
: undefined
if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user)
const dmUser =
d.user !== undefined ? await gateway.client.users.get(d.user.id) : undefined
const user = member !== undefined ? member.user : dmUser
if (user === undefined) return
const channel = const channel =
(await gateway.client.channels.get<GuildTextChannel>(d.channel_id)) ?? (await gateway.client.channels.get<GuildTextChannel>(d.channel_id)) ??
@ -23,7 +40,8 @@ export const interactionCreate: GatewayEventHandler = async (
const interaction = new Interaction(gateway.client, d, { const interaction = new Interaction(gateway.client, d, {
member, member,
guild, guild,
channel channel,
user
}) })
gateway.client.emit('interactionCreate', interaction) gateway.client.emit('interactionCreate', interaction)
} }

View file

@ -157,7 +157,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
const handler = gatewayHandlers[t] const handler = gatewayHandlers[t]
if (handler !== undefined) { if (handler !== undefined && d !== null) {
handler(this, d) handler(this, d)
} }
} }

View file

@ -5,7 +5,7 @@ import { User } from '../structures/user.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { CommandClient } from './commandClient.ts' import { CommandClient } from './commandClient.ts'
import { Extension } from './extensions.ts' import { Extension } from './extensions.ts'
import { parse } from '../../deps.ts' import { join, parse, walk } from '../../deps.ts'
export interface CommandContext { export interface CommandContext {
/** The Client object */ /** The Client object */
@ -284,13 +284,103 @@ export class CommandBuilder extends Command {
} }
} }
export class CommandsLoader {
client: CommandClient
#importSeq: { [name: string]: number } = {}
constructor(client: CommandClient) {
this.client = client
}
/**
* Load a Command from file.
*
* @param filePath Path of Command file.
* @param exportName Export name. Default is the "default" export.
*/
async load(
filePath: string,
exportName: string = 'default',
onlyRead?: boolean
): Promise<Command> {
const stat = await Deno.stat(filePath).catch(() => undefined)
if (stat === undefined || stat.isFile !== true)
throw new Error(`File not found on path ${filePath}`)
let seq: number | undefined
if (this.#importSeq[filePath] !== undefined) seq = this.#importSeq[filePath]
const mod = await import(
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
'file:///' +
join(Deno.cwd(), filePath) +
(seq === undefined ? '' : `#${seq}`)
)
if (this.#importSeq[filePath] === undefined) this.#importSeq[filePath] = 0
else this.#importSeq[filePath]++
const Cmd = mod[exportName]
if (Cmd === undefined)
throw new Error(`Command not exported as ${exportName} from ${filePath}`)
let cmd: Command
try {
if (Cmd instanceof Command) cmd = Cmd
else cmd = new Cmd()
if (!(cmd instanceof Command)) throw new Error('failed')
} catch (e) {
throw new Error(`Failed to load Command from ${filePath}`)
}
if (onlyRead !== true) this.client.commands.add(cmd)
return cmd
}
/**
* Load commands from a Directory.
*
* @param path Path of the directory.
* @param options Options to configure loading.
*/
async loadDirectory(
path: string,
options?: {
recursive?: boolean
exportName?: string
maxDepth?: number
exts?: string[]
onlyRead?: boolean
}
): Promise<Command[]> {
const commands: Command[] = []
for await (const entry of walk(path, {
maxDepth: options?.maxDepth,
exts: options?.exts,
includeDirs: false
})) {
if (entry.isFile !== true) continue
const cmd = await this.load(
entry.path,
options?.exportName,
options?.onlyRead
)
commands.push(cmd)
}
return commands
}
}
export class CommandsManager { export class CommandsManager {
client: CommandClient client: CommandClient
list: Collection<string, Command> = new Collection() list: Collection<string, Command> = new Collection()
disabled: Set<string> = new Set() disabled: Set<string> = new Set()
loader: CommandsLoader
constructor(client: CommandClient) { constructor(client: CommandClient) {
this.client = client this.client = client
this.loader = new CommandsLoader(client)
} }
/** Number of loaded Commands */ /** Number of loaded Commands */

View file

@ -155,6 +155,7 @@ function buildOptionsArray(
) )
} }
/** Slash Command Builder */
export class SlashBuilder { export class SlashBuilder {
data: SlashCommandPartial data: SlashCommandPartial
@ -200,6 +201,7 @@ export class SlashBuilder {
} }
} }
/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */
export class SlashCommandsManager { export class SlashCommandsManager {
slash: SlashClient slash: SlashClient
rest: RESTManager rest: RESTManager
@ -351,7 +353,7 @@ export class SlashCommandsManager {
} }
} }
export type SlashCommandHandlerCallback = (interaction: Interaction) => any export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown
export interface SlashCommandHandler { export interface SlashCommandHandler {
name: string name: string
guild?: string guild?: string
@ -360,6 +362,7 @@ export interface SlashCommandHandler {
handler: SlashCommandHandlerCallback handler: SlashCommandHandlerCallback
} }
/** Options for SlashClient */
export interface SlashOptions { export interface SlashOptions {
id?: string | (() => string) id?: string | (() => string)
client?: Client client?: Client
@ -369,6 +372,7 @@ export interface SlashOptions {
publicKey?: string publicKey?: string
} }
/** Slash Client represents an Interactions Client which can be used without Harmony Client. */
export class SlashClient { export class SlashClient {
id: string | (() => string) id: string | (() => string)
client?: Client client?: Client
@ -469,12 +473,20 @@ export class SlashClient {
const groupMatched = const groupMatched =
e.group !== undefined && e.parent !== undefined e.group !== undefined && e.parent !== undefined
? i.options ? i.options
.find((o) => o.name === e.group) .find(
(o) =>
o.name === e.group &&
o.type === SlashCommandOptionType.SUB_COMMAND_GROUP
)
?.options?.find((o) => o.name === e.name) !== undefined ?.options?.find((o) => o.name === e.name) !== undefined
: true : true
const subMatched = const subMatched =
e.group === undefined && e.parent !== undefined e.group === undefined && e.parent !== undefined
? i.options.find((o) => o.name === e.name) !== undefined ? i.options.find(
(o) =>
o.name === e.name &&
o.type === SlashCommandOptionType.SUB_COMMAND
) !== undefined
: true : true
const nameMatched1 = e.name === i.name const nameMatched1 = e.name === i.name
const parentMatched = hasGroupOrParent ? e.parent === i.name : true const parentMatched = hasGroupOrParent ? e.parent === i.name : true
@ -485,11 +497,15 @@ export class SlashClient {
}) })
} }
/** Process an incoming Slash Command (interaction) */ /** Process an incoming Interaction */
private _process(interaction: Interaction): void { private _process(interaction: Interaction): void {
if (!this.enabled) return if (!this.enabled) return
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return if (
interaction.type !== InteractionType.APPLICATION_COMMAND ||
interaction.data === undefined
)
return
const cmd = this._getCommand(interaction) const cmd = this._getCommand(interaction)
if (cmd?.group !== undefined) if (cmd?.group !== undefined)

View file

@ -1,12 +1,18 @@
import { Client } from '../models/client.ts' import { Client } from '../models/client.ts'
import { MessageOptions } from '../types/channel.ts' import {
AllowedMentionsPayload,
EmbedPayload,
MessageOptions
} from '../types/channel.ts'
import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts'
import { import {
InteractionData, InteractionApplicationCommandData,
InteractionOption, InteractionApplicationCommandOption,
InteractionPayload, InteractionPayload,
InteractionResponseFlags,
InteractionResponsePayload, InteractionResponsePayload,
InteractionResponseType InteractionResponseType,
InteractionType
} from '../types/slash.ts' } from '../types/slash.ts'
import { SnowflakeBase } from './base.ts' import { SnowflakeBase } from './base.ts'
import { Embed } from './embed.ts' import { Embed } from './embed.ts'
@ -15,7 +21,6 @@ import { Member } from './member.ts'
import { Message } from './message.ts' import { Message } from './message.ts'
import { GuildTextChannel, TextChannel } from './textChannel.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts'
import { User } from './user.ts' import { User } from './user.ts'
import { Webhook } from './webhook.ts'
interface WebhookMessageOptions extends MessageOptions { interface WebhookMessageOptions extends MessageOptions {
embeds?: Embed[] embeds?: Embed[]
@ -25,39 +30,57 @@ interface WebhookMessageOptions extends MessageOptions {
type AllWebhookMessageOptions = string | WebhookMessageOptions type AllWebhookMessageOptions = string | WebhookMessageOptions
export interface InteractionResponse { /** Interaction Message related Options */
type?: InteractionResponseType export interface InteractionMessageOptions {
content?: string content?: string
embeds?: Embed[] embeds?: EmbedPayload[]
tts?: boolean tts?: boolean
flags?: number flags?: number | InteractionResponseFlags[]
allowedMentions?: AllowedMentionsPayload
/** Whether to reply with Source or not. True by default */
withSource?: boolean
}
export interface InteractionResponse extends InteractionMessageOptions {
/** Type of Interaction Response */
type?: InteractionResponseType
/**
* DEPRECATED: Use `ephemeral` instead.
*
* @deprecated
*/
temp?: boolean temp?: boolean
allowedMentions?: { /** Whether the Message Response should be Ephemeral (only visible to User) or not */
parse?: string ephemeral?: boolean
roles?: string[]
users?: string[]
everyone?: boolean
}
} }
export class Interaction extends SnowflakeBase { export class Interaction extends SnowflakeBase {
client: Client client: Client
type: number /** Type of Interaction */
type: InteractionType
/** Interaction Token */
token: string token: string
/** Interaction ID */
id: string id: string
data: InteractionData /** Data sent with Interaction. Only applies to Application Command, type may change in future. */
channel: GuildTextChannel data?: InteractionApplicationCommandData
guild: Guild /** Channel in which Interaction was initiated */
member: Member channel?: TextChannel | GuildTextChannel
_savedHook?: Webhook guild?: Guild
/** Member object of who initiated the Interaction */
member?: Member
/** User object of who invoked Interaction */
user: User
responded: boolean = false
constructor( constructor(
client: Client, client: Client,
data: InteractionPayload, data: InteractionPayload,
others: { others: {
channel: GuildTextChannel channel?: TextChannel | GuildTextChannel
guild: Guild guild?: Guild
member: Member member?: Member
user: User
} }
) { ) {
super(client) super(client)
@ -66,31 +89,43 @@ export class Interaction extends SnowflakeBase {
this.token = data.token this.token = data.token
this.member = others.member this.member = others.member
this.id = data.id this.id = data.id
this.user = others.user
this.data = data.data this.data = data.data
this.guild = others.guild this.guild = others.guild
this.channel = others.channel this.channel = others.channel
} }
get user(): User { /** Name of the Command Used (may change with future additions to Interactions!) */
return this.member.user get name(): string | undefined {
return this.data?.name
} }
get name(): string { get options(): InteractionApplicationCommandOption[] {
return this.data.name return this.data?.options ?? []
}
get options(): InteractionOption[] {
return this.data.options ?? []
} }
/** Get an option by name */
option<T = any>(name: string): T { option<T = any>(name: string): T {
return this.options.find((e) => e.name === name)?.value return this.options.find((e) => e.name === name)?.value
} }
/** Respond to an Interaction */ /** Respond to an Interaction */
async respond(data: InteractionResponse): Promise<Interaction> { async respond(data: InteractionResponse): Promise<Interaction> {
if (this.responded) throw new Error('Already responded to Interaction')
let flags = 0
if (data.ephemeral === true || data.temp === true)
flags |= InteractionResponseFlags.EPHEMERAL
if (data.flags !== undefined) {
if (Array.isArray(data.flags))
flags = data.flags.reduce((p, a) => p | a, flags)
else if (typeof data.flags === 'number') flags |= data.flags
}
const payload: InteractionResponsePayload = { const payload: InteractionResponsePayload = {
type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type:
data.type ??
(data.withSource === false
? InteractionResponseType.CHANNEL_MESSAGE
: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE),
data: data:
data.type === undefined || data.type === undefined ||
data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE ||
@ -99,8 +134,8 @@ export class Interaction extends SnowflakeBase {
content: data.content ?? '', content: data.content ?? '',
embeds: data.embeds, embeds: data.embeds,
tts: data.tts ?? false, tts: data.tts ?? false,
flags: data.temp === true ? 64 : data.flags ?? undefined, flags,
allowed_mentions: (data.allowedMentions ?? undefined) as any allowed_mentions: data.allowedMentions ?? undefined
} }
: undefined : undefined
} }
@ -109,6 +144,52 @@ export class Interaction extends SnowflakeBase {
INTERACTION_CALLBACK(this.id, this.token), INTERACTION_CALLBACK(this.id, this.token),
payload payload
) )
this.responded = true
return this
}
/** Acknowledge the Interaction */
async acknowledge(withSource?: boolean): Promise<Interaction> {
await this.respond({
type:
withSource === true
? InteractionResponseType.ACK_WITH_SOURCE
: InteractionResponseType.ACKNOWLEDGE
})
return this
}
/** Reply with a Message to the Interaction */
async reply(content: string): Promise<Interaction>
async reply(options: InteractionMessageOptions): Promise<Interaction>
async reply(
content: string,
options: InteractionMessageOptions
): Promise<Interaction>
async reply(
content: string | InteractionMessageOptions,
messageOptions?: InteractionMessageOptions
): Promise<Interaction> {
let options: InteractionMessageOptions | undefined =
typeof content === 'object' ? content : messageOptions
if (
typeof content === 'object' &&
messageOptions !== undefined &&
options !== undefined
)
Object.assign(options, messageOptions)
if (options === undefined) options = {}
if (typeof content === 'string') Object.assign(options, { content })
await this.respond(
Object.assign(options, {
type:
options.withSource === false
? InteractionResponseType.CHANNEL_MESSAGE
: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE
})
)
return this return this
} }
@ -232,6 +313,7 @@ export class Interaction extends SnowflakeBase {
return this return this
} }
/** Delete a follow-up Message */
async deleteMessage(msg: Message | string): Promise<Interaction> { async deleteMessage(msg: Message | string): Promise<Interaction> {
await this.client.rest.delete( await this.client.rest.delete(
WEBHOOK_MESSAGE( WEBHOOK_MESSAGE(

View file

@ -156,16 +156,25 @@ export interface MessagePayload {
stickers?: MessageStickerPayload[] stickers?: MessageStickerPayload[]
} }
export enum AllowedMentionType {
Roles = 'roles',
Users = 'users',
Everyone = 'everyone'
}
export interface AllowedMentionsPayload {
parse?: AllowedMentionType[]
users?: string[]
roles?: string[]
replied_user?: boolean
}
export interface MessageOptions { export interface MessageOptions {
tts?: boolean tts?: boolean
embed?: Embed embed?: Embed
file?: MessageAttachment file?: MessageAttachment
files?: MessageAttachment[] files?: MessageAttachment[]
allowedMentions?: { allowedMentions?: AllowedMentionsPayload
parse?: 'everyone' | 'users' | 'roles'
roles?: string[]
users?: string[]
}
} }
export interface ChannelMention { export interface ChannelMention {

View file

@ -1,22 +1,25 @@
import { EmbedPayload } from './channel.ts' import { AllowedMentionsPayload, EmbedPayload } from './channel.ts'
import { MemberPayload } from './guild.ts' import { MemberPayload } from './guild.ts'
import { UserPayload } from './user.ts'
export interface InteractionOption { export interface InteractionApplicationCommandOption {
/** Option name */ /** Option name */
name: string name: string
/** Type of Option */
type: SlashCommandOptionType
/** Value of the option */ /** Value of the option */
value?: any value?: any
/** Sub options */ /** Sub options */
options?: any[] options?: InteractionApplicationCommandOption[]
} }
export interface InteractionData { export interface InteractionApplicationCommandData {
/** Name of the Slash Command */ /** Name of the Slash Command */
name: string name: string
/** Unique ID of the Slash Command */ /** Unique ID of the Slash Command */
id: string id: string
/** Options (arguments) sent with Interaction */ /** Options (arguments) sent with Interaction */
options: InteractionOption[] options: InteractionApplicationCommandOption[]
} }
export enum InteractionType { export enum InteractionType {
@ -26,27 +29,31 @@ export enum InteractionType {
APPLICATION_COMMAND = 2 APPLICATION_COMMAND = 2
} }
export interface InteractionMemberPayload extends MemberPayload {
permissions: string
}
export interface InteractionPayload { export interface InteractionPayload {
/** Type of the Interaction */ /** Type of the Interaction */
type: InteractionType type: InteractionType
/** Token of the Interaction to respond */ /** Token of the Interaction to respond */
token: string token: string
/** Member object of user who invoked */ /** Member object of user who invoked */
member: MemberPayload & { member?: InteractionMemberPayload
/** Total permissions of the member in the channel, including overrides */ /** User who initiated Interaction (only in DMs) */
permissions: string user?: UserPayload
}
/** ID of the Interaction */ /** ID of the Interaction */
id: string id: string
/** /**
* Data sent with the interaction * Data sent with the interaction
* **This can be undefined only when Interaction is not a Slash Command** *
* This can be undefined only when Interaction is not a Slash Command.
*/ */
data: InteractionData data?: InteractionApplicationCommandData
/** ID of the Guild in which Interaction was invoked */ /** ID of the Guild in which Interaction was invoked */
guild_id: string guild_id?: string
/** ID of the Channel in which Interaction was invoked */ /** ID of the Channel in which Interaction was invoked */
channel_id: string channel_id?: string
} }
export interface SlashCommandChoice { export interface SlashCommandChoice {
@ -68,13 +75,18 @@ export enum SlashCommandOptionType {
} }
export interface SlashCommandOption { export interface SlashCommandOption {
/** Name of the option. */
name: string name: string
/** Description not required in Sub-Command or Sub-Command-Group */ /** Description of the Option. Not required in Sub-Command-Group */
description?: string description?: string
/** Option type */
type: SlashCommandOptionType type: SlashCommandOptionType
/** Whether the option is required or not, false by default */
required?: boolean required?: boolean
default?: boolean default?: boolean
/** Optional choices out of which User can choose value */
choices?: SlashCommandChoice[] choices?: SlashCommandChoice[]
/** Nested options for Sub-Command or Sub-Command-Groups */
options?: SlashCommandOption[] options?: SlashCommandOption[]
} }
@ -103,19 +115,20 @@ export enum InteractionResponseType {
} }
export interface InteractionResponsePayload { export interface InteractionResponsePayload {
/** Type of the response */
type: InteractionResponseType type: InteractionResponseType
/** Data to be sent with response. Optional for types: Pong, Acknowledge, Ack with Source */
data?: InteractionResponseDataPayload data?: InteractionResponseDataPayload
} }
export interface InteractionResponseDataPayload { export interface InteractionResponseDataPayload {
tts?: boolean tts?: boolean
/** Text content of the Response (Message) */
content: string content: string
/** Upto 10 Embed Objects to send with Response */
embeds?: EmbedPayload[] embeds?: EmbedPayload[]
allowed_mentions?: { /** Allowed Mentions object */
parse?: 'everyone' | 'users' | 'roles' allowed_mentions?: AllowedMentionsPayload
roles?: string[]
users?: string[]
}
flags?: number flags?: number
} }