diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index a27fa81..4c8eb00 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -1,7 +1,16 @@ +import { Guild } from '../../structures/guild.ts' import { Member } from '../../structures/member.ts' -import { Interaction } from '../../structures/slash.ts' +import { Role } from '../../structures/role.ts' +import { + Interaction, + InteractionApplicationCommandResolved, + InteractionChannel +} from '../../structures/slash.ts' import { GuildTextChannel } from '../../structures/textChannel.ts' +import { User } from '../../structures/user.ts' import { InteractionPayload } from '../../types/slash.ts' +import { UserPayload } from '../../types/user.ts' +import { Permissions } from '../../utils/permissions.ts' import { Gateway, GatewayEventHandler } from '../index.ts' export const interactionCreate: GatewayEventHandler = async ( @@ -18,13 +27,12 @@ export const interactionCreate: GatewayEventHandler = async ( d.guild_id === undefined ? undefined : await gateway.client.guilds.get(d.guild_id) - 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 = d.member !== undefined - ? (((await guild.members.get(d.member.user.id)) 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 = @@ -37,11 +45,66 @@ export const interactionCreate: GatewayEventHandler = async ( (await gateway.client.channels.get(d.channel_id)) ?? (await gateway.client.channels.fetch(d.channel_id)) + const resolved: InteractionApplicationCommandResolved = { + users: {}, + channels: {}, + members: {}, + roles: {} + } + + if (d.data?.resolved !== undefined) { + for (const [id, data] of Object.entries(d.data.resolved.users ?? {})) { + await gateway.client.users.set(id, data) + resolved.users[id] = ((await gateway.client.users.get( + id + )) as unknown) as User + if (resolved.members[id] !== undefined) + resolved.users[id].member = resolved.members[id] + } + + for (const [id, data] of Object.entries(d.data.resolved.members ?? {})) { + const roles = await guild?.roles.array() + let permissions = new Permissions(Permissions.DEFAULT) + if (roles !== undefined) { + const mRoles = roles.filter( + (r) => (data?.roles?.includes(r.id) as boolean) || r.id === guild?.id + ) + permissions = new Permissions(mRoles.map((r) => r.permissions)) + } + data.user = (d.data.resolved.users?.[id] as unknown) as UserPayload + resolved.members[id] = new Member( + gateway.client, + data, + resolved.users[id], + guild as Guild, + permissions + ) + } + + for (const [id, data] of Object.entries(d.data.resolved.roles ?? {})) { + if (guild !== undefined) { + await guild.roles.set(id, data) + resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role + } else { + resolved.roles[id] = new Role( + gateway.client, + data, + (guild as unknown) as Guild + ) + } + } + + for (const [id, data] of Object.entries(d.data.resolved.channels ?? {})) { + resolved.channels[id] = new InteractionChannel(gateway.client, data) + } + } + const interaction = new Interaction(gateway.client, d, { member, guild, channel, - user + user, + resolved }) gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index c6dbc0a..d890457 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -7,7 +7,6 @@ import { import { GatewayResponse } from '../types/gatewayResponse.ts' import { GatewayOpcodes, - GatewayIntents, GatewayCloseCodes, IdentityPayload, StatusUpdatePayload, @@ -57,8 +56,6 @@ export type GatewayTypedEvents = { */ export class Gateway extends HarmonyEventEmitter { websocket?: WebSocket - token?: string - intents?: GatewayIntents[] connected = false initialized = false heartbeatInterval = 0 @@ -269,8 +266,9 @@ export class Gateway extends HarmonyEventEmitter { } private async sendIdentify(forceNewSession?: boolean): Promise { - if (typeof this.token !== 'string') throw new Error('Token not specified') - if (typeof this.intents !== 'object') + if (typeof this.client.token !== 'string') + throw new Error('Token not specified') + if (typeof this.client.intents !== 'object') throw new Error('Intents not specified') if (this.client.fetchGatewayInfo === true) { @@ -300,7 +298,7 @@ export class Gateway extends HarmonyEventEmitter { } const payload: IdentityPayload = { - token: this.token, + token: this.client.token, properties: { $os: this.client.clientProperties.os ?? Deno.build.os, $browser: this.client.clientProperties.browser ?? 'harmony', @@ -311,7 +309,7 @@ export class Gateway extends HarmonyEventEmitter { this.shards === undefined ? [0, 1] : [this.shards[0] ?? 0, this.shards[1] ?? 1], - intents: this.intents.reduce( + intents: this.client.intents.reduce( (previous, current) => previous | current, 0 ), @@ -327,9 +325,8 @@ export class Gateway extends HarmonyEventEmitter { } private async sendResume(): Promise { - if (typeof this.token !== 'string') throw new Error('Token not specified') - if (typeof this.intents !== 'object') - throw new Error('Intents not specified') + if (typeof this.client.token !== 'string') + throw new Error('Token not specified') if (this.sessionID === undefined) { this.sessionID = await this.cache.get( @@ -348,7 +345,7 @@ export class Gateway extends HarmonyEventEmitter { const resumePayload = { op: GatewayOpcodes.RESUME, d: { - token: this.token, + token: this.client.token, session_id: this.sessionID, seq: this.sequenceID ?? null } diff --git a/src/models/shard.ts b/src/models/shard.ts index 80cd649..762ce40 100644 --- a/src/models/shard.ts +++ b/src/models/shard.ts @@ -79,8 +79,6 @@ export class ShardManager extends HarmonyEventEmitter { const shardCount = await this.getShardCount() const gw = new Gateway(this.client, [Number(id), shardCount]) - gw.token = this.client.token - gw.intents = this.client.intents this.list.set(id.toString(), gw) gw.initWebsocket() diff --git a/src/structures/base.ts b/src/structures/base.ts index e5806e5..0898cfe 100644 --- a/src/structures/base.ts +++ b/src/structures/base.ts @@ -2,10 +2,10 @@ import { Client } from '../models/client.ts' import { Snowflake } from '../utils/snowflake.ts' export class Base { - client: Client + client!: Client constructor(client: Client, _data?: any) { - this.client = client + Object.defineProperty(this, 'client', { value: client, enumerable: false }) } } diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 0df43af..99a1308 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,24 +1,31 @@ import { Client } from '../models/client.ts' import { AllowedMentionsPayload, + ChannelTypes, EmbedPayload, MessageOptions } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { + Dict, InteractionApplicationCommandData, InteractionApplicationCommandOption, + InteractionChannelPayload, InteractionPayload, InteractionResponseFlags, InteractionResponsePayload, InteractionResponseType, - InteractionType + InteractionType, + SlashCommandOptionType } from '../types/slash.ts' +import { Permissions } from '../utils/permissions.ts' import { SnowflakeBase } from './base.ts' +import { Channel } from './channel.ts' import { Embed } from './embed.ts' import { Guild } from './guild.ts' import { Member } from './member.ts' import { Message } from './message.ts' +import { Role } from './role.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts' import { User } from './user.ts' @@ -54,8 +61,37 @@ export interface InteractionResponse extends InteractionMessageOptions { ephemeral?: boolean } +/** Represents a Channel Object for an Option in Slash Command */ +export class InteractionChannel extends SnowflakeBase { + name: string + type: ChannelTypes + permissions: Permissions + + constructor(client: Client, data: InteractionChannelPayload) { + super(client) + this.name = data.name + this.type = data.type + this.permissions = new Permissions(data.permissions) + } + + /** Resolve to actual Channel object if present in Cache */ + async resolve(): Promise { + return this.client.channels.get(this.id) + } +} + +export interface InteractionApplicationCommandResolved { + users: Dict + members: Dict + channels: Dict + roles: Dict +} + +export class InteractionUser extends User { + member?: Member +} + export class Interaction extends SnowflakeBase { - client: Client /** Type of Interaction */ type: InteractionType /** Interaction Token */ @@ -72,6 +108,7 @@ export class Interaction extends SnowflakeBase { /** User object of who invoked Interaction */ user: User responded: boolean = false + resolved: InteractionApplicationCommandResolved constructor( client: Client, @@ -81,10 +118,10 @@ export class Interaction extends SnowflakeBase { guild?: Guild member?: Member user: User + resolved: InteractionApplicationCommandResolved } ) { super(client) - this.client = client this.type = data.type this.token = data.token this.member = others.member @@ -93,6 +130,7 @@ export class Interaction extends SnowflakeBase { this.data = data.data this.guild = others.guild this.channel = others.channel + this.resolved = others.resolved } /** Name of the Command Used (may change with future additions to Interactions!) */ @@ -105,8 +143,16 @@ export class Interaction extends SnowflakeBase { } /** Get an option by name */ - option(name: string): T { - return this.options.find((e) => e.name === name)?.value + option(name: string): T { + const op = this.options.find((e) => e.name === name) + if (op === undefined || op.value === undefined) return undefined as any + if (op.type === SlashCommandOptionType.USER) + return this.resolved.users[op.value] as any + else if (op.type === SlashCommandOptionType.ROLE) + return this.resolved.roles[op.value] as any + else if (op.type === SlashCommandOptionType.CHANNEL) + return this.resolved.channels[op.value] as any + else return op.value } /** Respond to an Interaction */ diff --git a/src/test/slash.ts b/src/test/slash.ts index 155a745..215cad0 100644 --- a/src/test/slash.ts +++ b/src/test/slash.ts @@ -1,100 +1,56 @@ -import { Client, Intents, event, slash } from '../../mod.ts' -import { Embed } from '../structures/embed.ts' +import { + Client, + Intents, + event, + slash, + SlashCommandOptionType as Type +} from '../../mod.ts' import { Interaction } from '../structures/slash.ts' import { TOKEN } from './config.ts' export class MyClient extends Client { - @event() - ready(): void { + @event() ready(): void { console.log(`Logged in as ${this.user?.tag}!`) - this.slash.commands.bulkEdit([{ name: 'send', description: 'idk' }]) - } - - @event('debug') - debugEvt(txt: string): void { - console.log(txt) - } - - @slash() - send(d: Interaction): void { - d.respond({ - content: d.data.options?.find((e) => e.name === 'content')?.value - }) - } - - @slash() - async eval(d: Interaction): Promise { - if ( - d.user.id !== '422957901716652033' && - d.user.id !== '682849186227552266' - ) { - 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]') + this.slash.commands.bulkEdit( + [ + { + name: 'test', + description: 'Test command.', + options: [ + { + name: 'user', + type: Type.USER, + description: 'User' + }, + { + name: 'role', + type: Type.ROLE, + description: 'Role' + }, + { + name: 'channel', + type: Type.CHANNEL, + description: 'Channel' + }, + { + name: 'string', + type: Type.STRING, + description: 'String' + } + ] } - d.respond({ - content: '```js\n' + `${res}` + '\n```' - }).catch(() => {}) - } catch (e) { - d.respond({ - content: '```js\n' + `${e.stack}` + '\n```' - }) - } - } + ], + '807935370556866560' + ) + this.slash.commands.bulkEdit([]) } - @slash() - async hug(d: Interaction): Promise { - const id = d.data.options?.find((e) => e.name === 'user')?.value as string - const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) - const url = await fetch('https://nekos.life/api/v2/img/hug') - .then((r) => r.json()) - .then((e) => e.url) - - d.respond({ - embeds: [ - new Embed() - .setTitle(`${d.user.username} hugged ${user?.username}!`) - .setImage({ url }) - .setColor(0x2f3136) - ] - }) + @slash() test(d: Interaction): void { + console.log(d.resolved) } - @slash() - async kiss(d: Interaction): Promise { - const id = d.data.options?.find((e) => e.name === 'user')?.value as string - const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) - const url = await fetch('https://nekos.life/api/v2/img/kiss') - .then((r) => r.json()) - .then((e) => e.url) - - d.respond({ - embeds: [ - new Embed() - .setTitle(`${d.user.username} kissed ${user?.username}!`) - .setImage({ url }) - .setColor(0x2f3136) - ] - }) - } - - @slash('ping') - pingCmd(d: Interaction): void { - d.respond({ - content: `Pong!` - }) + @event() raw(evt: string, d: any): void { + if (evt === 'INTERACTION_CREATE') console.log(evt, d?.data?.resolved) } } diff --git a/src/types/slash.ts b/src/types/slash.ts index 692611c..69eea51 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,5 +1,10 @@ -import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' +import { + AllowedMentionsPayload, + ChannelTypes, + EmbedPayload +} from './channel.ts' import { MemberPayload } from './guild.ts' +import { RolePayload } from './role.ts' import { UserPayload } from './user.ts' export interface InteractionApplicationCommandOption { @@ -13,6 +18,24 @@ export interface InteractionApplicationCommandOption { options?: InteractionApplicationCommandOption[] } +export interface InteractionChannelPayload { + id: string + name: string + permissions: string + type: ChannelTypes +} + +export interface Dict { + [name: string]: T +} + +export interface InteractionApplicationCommandResolvedPayload { + users?: Dict + members?: Dict + channels?: Dict + roles?: Dict +} + export interface InteractionApplicationCommandData { /** Name of the Slash Command */ name: string @@ -20,6 +43,8 @@ export interface InteractionApplicationCommandData { id: string /** Options (arguments) sent with Interaction */ options: InteractionApplicationCommandOption[] + /** Resolved data for options in Slash Command */ + resolved?: InteractionApplicationCommandResolvedPayload } export enum InteractionType { diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts index eb7a67c..d2f5f03 100644 --- a/src/utils/bitfield.ts +++ b/src/utils/bitfield.ts @@ -9,31 +9,31 @@ export type BitFieldResolvable = /** Bit Field utility to work with Bits and Flags */ export class BitField { - flags: { [name: string]: number } = {} + #flags: { [name: string]: number } = {} bitfield: number constructor(flags: { [name: string]: number }, bits: any) { - this.flags = flags - this.bitfield = BitField.resolve(this.flags, bits) + this.#flags = flags + this.bitfield = BitField.resolve(this.#flags, bits) } any(bit: BitFieldResolvable): boolean { - return (this.bitfield & BitField.resolve(this.flags, bit)) !== 0 + return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0 } equals(bit: BitFieldResolvable): boolean { - return this.bitfield === BitField.resolve(this.flags, bit) + return this.bitfield === BitField.resolve(this.#flags, bit) } has(bit: BitFieldResolvable, ...args: any[]): boolean { if (Array.isArray(bit)) return (bit.every as any)((p: any) => this.has(p)) - bit = BitField.resolve(this.flags, bit) + bit = BitField.resolve(this.#flags, bit) return (this.bitfield & bit) === bit } missing(bits: any, ...hasParams: any[]): string[] { if (!Array.isArray(bits)) - bits = new BitField(this.flags, bits).toArray(false) + bits = new BitField(this.#flags, bits).toArray(false) return bits.filter((p: any) => !this.has(p, ...hasParams)) } @@ -44,10 +44,10 @@ export class BitField { add(...bits: BitFieldResolvable[]): BitField { let total = 0 for (const bit of bits) { - total |= BitField.resolve(this.flags, bit) + total |= BitField.resolve(this.#flags, bit) } if (Object.isFrozen(this)) - return new BitField(this.flags, this.bitfield | total) + return new BitField(this.#flags, this.bitfield | total) this.bitfield |= total return this } @@ -55,27 +55,31 @@ export class BitField { remove(...bits: BitFieldResolvable[]): BitField { let total = 0 for (const bit of bits) { - total |= BitField.resolve(this.flags, bit) + total |= BitField.resolve(this.#flags, bit) } if (Object.isFrozen(this)) - return new BitField(this.flags, this.bitfield & ~total) + return new BitField(this.#flags, this.bitfield & ~total) this.bitfield &= ~total return this } + flags(): { [name: string]: number } { + return this.#flags + } + serialize(...hasParams: any[]): { [key: string]: any } { const serialized: { [key: string]: any } = {} - for (const [flag, bit] of Object.entries(this.flags)) + for (const [flag, bit] of Object.entries(this.#flags)) serialized[flag] = this.has( - BitField.resolve(this.flags, bit), + BitField.resolve(this.#flags, bit), ...hasParams ) return serialized } toArray(...hasParams: any[]): string[] { - return Object.keys(this.flags).filter((bit) => - this.has(BitField.resolve(this.flags, bit), ...hasParams) + return Object.keys(this.#flags).filter((bit) => + this.has(BitField.resolve(this.#flags, bit), ...hasParams) ) } diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 03b4b82..99b258e 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -21,14 +21,14 @@ export class Permissions extends BitField { any(permission: PermissionResolvable, checkAdmin = true): boolean { return ( - (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || + (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.any(permission as any) ) } has(permission: PermissionResolvable, checkAdmin = true): boolean { return ( - (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || + (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.has(permission as any) ) }