Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Slash Commands & Interactions [WIP?] #56

Merged
merged 11 commits into from
Dec 16, 2020
5 changes: 5 additions & 0 deletions src/gateway/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
EveryTextChannelTypes
} from '../../utils/getChannelByType.ts'
import { interactionCreate } from './interactionCreate.ts'
import { Interaction } from '../../structures/slash.ts'

export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
Expand Down Expand Up @@ -328,4 +329,8 @@ export interface ClientEvents extends EventTypes {
* @param channel Channel of which Webhooks were updated
*/
webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void
/**
* A Slash Command was triggered
*/
interactionCreate: (interaction: Interaction) => void
}
20 changes: 19 additions & 1 deletion src/gateway/handlers/interactionCreate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { Member } from '../../structures/member.ts'
import { Interaction } from '../../structures/slash.ts'
import { GuildTextChannel } from '../../structures/textChannel.ts'
import { InteractionPayload } from '../../types/slash.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'

export const interactionCreate: GatewayEventHandler = async (
gateway: Gateway,
d: InteractionPayload
) => {
const interaction = new Interaction(gateway.client, d)
const guild = await gateway.client.guilds.get(d.guild_id)
if (guild === undefined) return

await guild.members.set(d.member.user.id, d.member)
const member = ((await guild.members.get(
d.member.user.id
)) as unknown) as Member

const channel =
(await gateway.client.channels.get<GuildTextChannel>(d.channel_id)) ??
(await gateway.client.channels.fetch<GuildTextChannel>(d.channel_id))

const interaction = new Interaction(gateway.client, d, {
member,
guild,
channel
})
gateway.client.emit('interactionCreate', interaction)
}
5 changes: 5 additions & 0 deletions src/models/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EmojisManager } from '../managers/emojis.ts'
import { ActivityGame, ClientActivity } from '../types/presence.ts'
import { ClientEvents } from '../gateway/handlers/index.ts'
import { Extension } from './extensions.ts'
import { SlashClient } from './slashClient.ts'

/** OS related properties sent with Gateway Identify */
export interface ClientProperties {
Expand Down Expand Up @@ -72,6 +73,8 @@ export class Client extends EventEmitter {
fetchUncachedReactions: boolean = false
/** Client Properties */
clientProperties: ClientProperties
/** Slash-Commands Management client */
slash: SlashClient

users: UsersManager = new UsersManager(this)
guilds: GuildManager = new GuildManager(this)
Expand Down Expand Up @@ -133,6 +136,8 @@ export class Client extends EventEmitter {
device: 'harmony'
}
: options.clientProperties

this.slash = new SlashClient(this)
}

/**
Expand Down
144 changes: 144 additions & 0 deletions src/models/slashClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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 {
SlashCommandOption,
SlashCommandPartial,
SlashCommandPayload
} from '../types/slash.ts'
import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts'

export interface SlashOptions {
enabled?: boolean
}

export class SlashCommand {
id: string
applicationID: string
name: string
description: string
options: SlashCommandOption[]

constructor(data: SlashCommandPayload) {
this.id = data.id
this.applicationID = data.application_id
this.name = data.name
this.description = data.description
this.options = data.options
}
}

export class SlashCommands {
client: Client
slash: SlashClient

constructor(client: Client) {
this.client = client
this.slash = client.slash
}

/** Get all Global Slash Commands */
async all(): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()

const res = (await this.client.rest.get(
APPLICATION_COMMANDS(this.client.user?.id as string)
)) as SlashCommandPayload[]
if (!Array.isArray(res)) return col

for (const raw of res) {
col.set(raw.id, new SlashCommand(raw))
}

return col
}

/** Get a Guild's Slash Commands */
async guild(
guild: Guild | string
): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()

const res = (await this.client.rest.get(
APPLICATION_GUILD_COMMANDS(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id
)
)) as SlashCommandPayload[]
if (!Array.isArray(res)) return col

for (const raw of res) {
col.set(raw.id, new SlashCommand(raw))
}

return col
}

/** Create a Slash Command (global or Guild) */
async create(
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommand> {
const payload = await this.client.rest.post(
guild === undefined
? APPLICATION_COMMANDS(this.client.user?.id as string)
: APPLICATION_GUILD_COMMANDS(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id
),
data
)

return new SlashCommand(payload)
}

async edit(
id: string,
data: SlashCommandPayload,
guild?: Guild
): Promise<SlashCommands> {
await this.client.rest.patch(
guild === undefined
? APPLICATION_COMMAND(this.client.user?.id as string, id)
: APPLICATION_GUILD_COMMAND(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id,
id
),
data
)
return this
}
}
DjDeveloperr marked this conversation as resolved.
Show resolved Hide resolved

export class SlashClient {
client: Client
enabled: boolean = true
commands: SlashCommands

constructor(client: Client, options?: SlashOptions) {
this.client = client
this.commands = new SlashCommands(client)

if (options !== undefined) {
this.enabled = options.enabled ?? true
}

this.client.on('interactionCreate', (interaction) =>
this.process(interaction)
)
}

process(interaction: Interaction): any {}

handle(fn: (interaction: Interaction) => any): SlashClient {
this.process = fn
return this
}
}
31 changes: 27 additions & 4 deletions src/structures/slash.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Client } from '../models/client.ts'
import { INTERACTION_CALLBACK } from '../types/endpoint.ts'
import { MemberPayload } from '../types/guild.ts'
import {
InteractionData,
InteractionPayload,
InteractionResponsePayload,
InteractionResponseType
} from '../types/slash.ts'
import { Embed } from './embed.ts'
import { Guild } from './guild.ts'
import { Member } from './member.ts'
import { GuildTextChannel } from './textChannel.ts'
import { User } from './user.ts'

export interface InteractionResponse {
type?: InteractionResponseType
Expand All @@ -21,17 +24,37 @@ export class Interaction {
client: Client
type: number
token: string
member: MemberPayload
id: string
data: InteractionData
channel: GuildTextChannel
guild: Guild
member: Member

constructor(client: Client, data: InteractionPayload) {
constructor(
client: Client,
data: InteractionPayload,
others: {
channel: GuildTextChannel
guild: Guild
member: Member
}
) {
this.client = client
this.type = data.type
this.token = data.token
this.member = data.member
this.member = others.member
this.id = data.id
this.data = data.data
this.guild = others.guild
this.channel = others.channel
}

get user(): User {
return this.member.user
}

get name(): string {
return this.data.name
}

async respond(data: InteractionResponse): Promise<Interaction> {
Expand Down
49 changes: 48 additions & 1 deletion src/test/slash.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,62 @@
import { Client, Intents } from '../../mod.ts'
import { SlashCommandOptionType } from '../types/slash.ts'
import { TOKEN } from './config.ts'

const client = new Client()

client.on('ready', () => {
console.log('Logged in!')
client.slash.commands
.create(
{
name: 'eval',
description: 'Run some JS code!',
options: [
{
name: 'code',
description: 'Code to run',
type: SlashCommandOptionType.STRING,
required: true
}
]
},
'783319033205751809'
)
.then(console.log)
})

client.on('interactionCreate', async (d) => {
if (d.name === 'eval') {
if (d.user.id !== '422957901716652033') {
d.respond({
content: 'This command can only be used by owner!'
})
} else {
const code = d.data.options.find((e) => e.name === 'code')
?.value as string
try {
// eslint-disable-next-line no-eval
let evaled = eval(code)
if (evaled instanceof Promise) evaled = await evaled
if (typeof evaled === 'object') evaled = Deno.inspect(evaled)
let res = `${evaled}`.substring(0, 1990)
while (client.token !== undefined && res.includes(client.token)) {
res = res.replace(client.token, '[REMOVED]')
}
d.respond({
content: '```js\n' + `${res}` + '\n```'
})
} catch (e) {
d.respond({
content: '```js\n' + `${e.stack}` + '\n```'
})
}
}
return
}
await d.respond({
content: `Hi, ${d.member.user.username}!`
content: `Hi, ${d.member.user.username}!`,
flags: 64
})
})

Expand Down
9 changes: 8 additions & 1 deletion src/types/slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface InteractionPayload {
member: MemberPayload
id: string
data: InteractionData
guild_id: string
channel_id: string
}

export interface SlashCommandChoice {
Expand All @@ -50,12 +52,17 @@ export interface SlashCommandOption {
choices?: SlashCommandChoice[]
}

export interface SlashCommandPayload {
export interface SlashCommandPartial {
name: string
description: string
options: SlashCommandOption[]
}

export interface SlashCommandPayload extends SlashCommandPartial {
id: string
application_id: string
}

export enum InteractionResponseType {
PONG = 1,
ACKNOWLEDGE = 2,
Expand Down