diff --git a/.gitignore b/.gitignore index c08a6ec9..4f1ff5d6 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,5 @@ src/test/config.ts # macOS is shit xD **/.DS_Store + +src/test/music.mp3 \ No newline at end of file diff --git a/mod.ts b/mod.ts index a94898bf..91ddb18f 100644 --- a/mod.ts +++ b/mod.ts @@ -2,7 +2,9 @@ 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' export * from './src/models/client.ts' +export * from './src/models/slashClient.ts' export { RESTManager } from './src/models/rest.ts' export * from './src/models/cacheAdapter.ts' export { @@ -18,6 +20,7 @@ export { ExtensionCommands, ExtensionsManager } from './src/models/extensions.ts' +export { SlashModule } from './src/models/slashModule.ts' export { CommandClient, command } from './src/models/commandClient.ts' export type { CommandClientOptions } from './src/models/commandClient.ts' export { BaseManager } from './src/managers/base.ts' @@ -28,6 +31,8 @@ export { GatewayCache } from './src/managers/gatewayCache.ts' export { GuildChannelsManager } from './src/managers/guildChannels.ts' export type { GuildChannel } from './src/managers/guildChannels.ts' export { GuildManager } from './src/managers/guilds.ts' +export * from './src/structures/slash.ts' +export * from './src/types/slash.ts' export { GuildEmojisManager } from './src/managers/guildEmojis.ts' export { MembersManager } from './src/managers/members.ts' export { MessageReactionsManager } from './src/managers/messageReactions.ts' @@ -80,3 +85,41 @@ export type { StatusType } from './src/types/presence.ts' export { ChannelTypes } from './src/types/channel.ts' +export type { ApplicationPayload } from './src/types/application.ts' +export type { ImageFormats, ImageSize } from './src/types/cdn.ts' +export type { + ChannelMention, + ChannelPayload, + FollowedChannel, + GuildNewsChannelPayload, + GuildChannelCategoryPayload, + GuildChannelPayload, + GuildTextChannelPayload, + GuildVoiceChannelPayload, + GroupDMChannelPayload +} from './src/types/channel.ts' +export type { EmojiPayload } from './src/types/emoji.ts' +export type { + GuildBanPayload, + GuildFeatures, + GuildIntegrationPayload, + GuildPayload +} from './src/types/guild.ts' +export type { InvitePayload, PartialInvitePayload } from './src/types/invite.ts' +export { PermissionFlags } from './src/types/permissionFlags.ts' +export type { + ActivityAssets, + ActivityEmoji, + ActivityFlags, + ActivityParty, + ActivityPayload, + ActivitySecrets, + ActivityTimestamps, + ActivityType +} from './src/types/presence.ts' +export type { RolePayload } from './src/types/role.ts' +export type { TemplatePayload } from './src/types/template.ts' +export type { UserPayload } from './src/types/user.ts' +export { UserFlags } from './src/types/userFlags.ts' +export type { VoiceStatePayload } from './src/types/voice.ts' +export type { WebhookPayload } from './src/types/webhook.ts' diff --git a/src/gateway/handlers/index.ts b/src/gateway/handlers/index.ts index 89340682..af1b69d7 100644 --- a/src/gateway/handlers/index.ts +++ b/src/gateway/handlers/index.ts @@ -27,7 +27,7 @@ import { webhooksUpdate } from './webhooksUpdate.ts' import { messageDeleteBulk } from './messageDeleteBulk.ts' import { userUpdate } from './userUpdate.ts' import { typingStart } from './typingStart.ts' -import { GuildTextChannel } from '../../structures/textChannel.ts' +import { GuildTextChannel, TextChannel } from '../../structures/textChannel.ts' import { Guild } from '../../structures/guild.ts' import { User } from '../../structures/user.ts' import { Emoji } from '../../structures/emoji.ts' @@ -53,6 +53,8 @@ import { EveryChannelTypes, EveryTextChannelTypes } from '../../utils/getChannelByType.ts' +import { interactionCreate } from './interactionCreate.ts' +import { Interaction } from '../../structures/slash.ts' export const gatewayHandlers: { [eventCode in GatewayEvents]: GatewayEventHandler | undefined @@ -93,7 +95,8 @@ export const gatewayHandlers: { USER_UPDATE: userUpdate, VOICE_STATE_UPDATE: voiceStateUpdate, VOICE_SERVER_UPDATE: voiceServerUpdate, - WEBHOOKS_UPDATE: webhooksUpdate + WEBHOOKS_UPDATE: webhooksUpdate, + INTERACTION_CREATE: interactionCreate } export interface EventTypes { @@ -107,56 +110,227 @@ export interface VoiceServerUpdateData { } export interface ClientEvents extends EventTypes { + /** When Client has successfully connected to Discord */ ready: () => void + /** When a successful reconnect has been made */ reconnect: () => void + /** When a successful session resume has been done */ resumed: () => void + /** + * When a new Channel is created + * @param channel New Channel object + */ channelCreate: (channel: EveryChannelTypes) => void + /** + * When a Channel was deleted + * @param channel Channel object which was deleted + */ channelDelete: (channel: EveryChannelTypes) => void + /** + * Channel's Pinned Messages were updated + * @param before Channel object before update + * @param after Channel object after update + */ channelPinsUpdate: ( before: EveryTextChannelTypes, after: EveryTextChannelTypes ) => void + /** + * A Channel was updated + * @param before Channel object before update + * @param after Channel object after update + */ channelUpdate: (before: EveryChannelTypes, after: EveryChannelTypes) => void + /** + * A User was banned from a Guild + * @param guild The Guild from which User was banned + * @param user The User who was banned + */ guildBanAdd: (guild: Guild, user: User) => void + /** + * A ban from a User in Guild was elevated + * @param guild Guild from which ban was removed + * @param user User of which ban was elevated + */ guildBanRemove: (guild: Guild, user: User) => void + /** + * Client has joined a new Guild. + * @param guild The new Guild object + */ guildCreate: (guild: Guild) => void + /** + * A Guild in which Client was either deleted, or bot was kicked + * @param guild The Guild object + */ guildDelete: (guild: Guild) => void + /** + * A new Emoji was added to Guild + * @param guild Guild in which Emoji was added + * @param emoji The Emoji which was added + */ guildEmojiAdd: (guild: Guild, emoji: Emoji) => void + /** + * An Emoji was deleted from Guild + * @param guild Guild from which Emoji was deleted + * @param emoji Emoji which was deleted + */ guildEmojiDelete: (guild: Guild, emoji: Emoji) => void + /** + * An Emoji in a Guild was updated + * @param guild Guild in which Emoji was updated + * @param before Emoji object before update + * @param after Emoji object after update + */ guildEmojiUpdate: (guild: Guild, before: Emoji, after: Emoji) => void + /** + * Guild's Integrations were updated + * @param guild The Guild object + */ guildIntegrationsUpdate: (guild: Guild) => void + /** + * A new Member has joined a Guild + * @param member The Member object + */ guildMemberAdd: (member: Member) => void + /** + * A Guild Member has either left or was kicked from Guild + * @param member The Member object + */ guildMemberRemove: (member: Member) => void + /** + * A Guild Member was updated. Nickname changed, role assigned, etc. + * @param before Member object before update + * @param after Meber object after update + */ guildMemberUpdate: (before: Member, after: Member) => void + /** + * A new Role was created in Guild + * @param role The new Role object + */ guildRoleCreate: (role: Role) => void + /** + * A Role was deleted from the Guild + * @param role The Role object + */ guildRoleDelete: (role: Role) => void + /** + * A Role was updated in a Guild + * @param before Role object before update + * @param after Role object after updated + */ guildRoleUpdate: (before: Role, after: Role) => void + /** + * A Guild has been updated. For example name, icon, etc. + * @param before Guild object before update + * @param after Guild object after update + */ guildUpdate: (before: Guild, after: Guild) => void + /** + * A new Message was created (sent) + * @param message The new Message object + */ messageCreate: (message: Message) => void + /** + * A Message was deleted. + * @param message The Message object + */ messageDelete: (message: Message) => void + /** + * Messages were bulk deleted in a Guild Text Channel + * @param channel Channel in which Messages were deleted + * @param messages Collection of Messages deleted + * @param uncached Set of Messages deleted's IDs which were not cached + */ messageDeleteBulk: ( channel: GuildTextChannel, messages: Collection, uncached: Set ) => void + /** + * A Message was updated. For example content, embed, etc. + * @param before Message object before update + * @param after Message object after update + */ messageUpdate: (before: Message, after: Message) => void + /** + * Reaction was added to a Message + * @param reaction Reaction object + * @param user User who added the reaction + */ messageReactionAdd: (reaction: MessageReaction, user: User) => void + /** + * Reaction was removed fro a Message + * @param reaction Reaction object + * @param user User to who removed the reaction + */ messageReactionRemove: (reaction: MessageReaction, user: User) => void + /** + * All reactions were removed from a Message + * @param message Message from which reactions were removed + */ messageReactionRemoveAll: (message: Message) => void + /** + * All reactions of a single Emoji were removed + * @param message The Message object + * @param emoji The Emoji object + */ messageReactionRemoveEmoji: (message: Message, emoji: Emoji) => void + /** + * A User has started typing in a Text Channel + */ typingStart: ( user: User, - channel: EveryChannelTypes, + channel: TextChannel, at: Date, guildData?: TypingStartGuildData ) => void + /** + * A new Invite was created + * @param invite New Invite object + */ inviteCreate: (invite: Invite) => void + /** + * An Invite was deleted + * @param invite Invite object + */ inviteDelete: (invite: Invite) => void + /** + * A User was updated. For example username, avatar, etc. + * @param before The User object before update + * @param after The User object after update + */ userUpdate: (before: User, after: User) => void + /** + * Client has received credentials for establishing connection to Voice Server + */ voiceServerUpdate: (data: VoiceServerUpdateData) => void + /** + * A User has joined a Voice Channel + */ voiceStateAdd: (state: VoiceState) => void + /** + * A User has left a Voice Channel + */ voiceStateRemove: (state: VoiceState) => void + /** + * Voice State of a User has been updated + * @param before Voice State object before update + * @param after Voice State object after update + */ voiceStateUpdate: (state: VoiceState, after: VoiceState) => void + /** + * A User's presence has been updated + * @param presence New Presence + */ presenceUpdate: (presence: Presence) => void + /** + * Webhooks of a Channel in a Guild has been updated + * @param guild Guild in which Webhooks were updated + * @param channel Channel of which Webhooks were updated + */ webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void + /** + * A Slash Command was triggered + */ + interactionCreate: (interaction: Interaction) => void } diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts new file mode 100644 index 00000000..fac882ae --- /dev/null +++ b/src/gateway/handlers/interactionCreate.ts @@ -0,0 +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 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(d.channel_id)) ?? + (await gateway.client.channels.fetch(d.channel_id)) + + const interaction = new Interaction(gateway.client, d, { + member, + guild, + channel + }) + gateway.client.emit('interactionCreate', interaction) +} diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 6311ee9e..4f8dacbd 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -252,9 +252,9 @@ class Gateway { const payload: IdentityPayload = { token: this.token, properties: { - $os: Deno.build.os, - $browser: 'harmony', - $device: 'harmony' + $os: this.client.clientProperties.os ?? Deno.build.os, + $browser: this.client.clientProperties.browser ?? 'harmony', + $device: this.client.clientProperties.device ?? 'harmony' }, compress: true, shard: [0, 1], // TODO: Make sharding possible diff --git a/src/models/client.ts b/src/models/client.ts index 13646ebf..0b3d4da6 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -12,6 +12,16 @@ 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' +import { Interaction } from '../structures/slash.ts' +import { SlashModule } from './slashModule.ts' + +/** OS related properties sent with Gateway Identify */ +export interface ClientProperties { + os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string + browser?: 'harmony' | string + device?: 'harmony' | string +} /** Some Client Options to modify behaviour */ export interface ClientOptions { @@ -33,6 +43,10 @@ export interface ClientOptions { reactionCacheLifetime?: number /** Whether to fetch Uncached Message of Reaction or not? */ fetchUncachedReactions?: boolean + /** Client Properties */ + clientProperties?: ClientProperties + /** Enable/Disable Slash Commands Integration (enabled by default) */ + enableSlash?: boolean } /** @@ -61,6 +75,10 @@ export class Client extends EventEmitter { reactionCacheLifetime: number = 3600000 /** Whether to fetch Uncached Message of Reaction or not? */ fetchUncachedReactions: boolean = false + /** Client Properties */ + clientProperties: ClientProperties + /** Slash-Commands Management client */ + slash: SlashClient users: UsersManager = new UsersManager(this) guilds: GuildManager = new GuildManager(this) @@ -72,6 +90,13 @@ export class Client extends EventEmitter { /** Client's presence. Startup one if set before connecting */ presence: ClientPresence = new ClientPresence() _decoratedEvents?: { [name: string]: (...args: any[]) => any } + _decoratedSlash?: Array<{ + name: string + guild?: string + handler: (interaction: Interaction) => any + }> + + _decoratedSlashModules?: SlashModule[] private readonly _untypedOn = this.on @@ -113,6 +138,19 @@ export class Client extends EventEmitter { }) this._decoratedEvents = undefined } + + this.clientProperties = + options.clientProperties === undefined + ? { + os: Deno.build.os, + browser: 'harmony', + device: 'harmony' + } + : options.clientProperties + + this.slash = new SlashClient(this, { + enabled: options.enableSlash + }) } /** @@ -171,7 +209,34 @@ export function event(name?: string) { const listener = ((client as unknown) as { [name: string]: (...args: any[]) => any })[prop] + if (typeof listener !== 'function') + throw new Error('@event decorator requires a function') if (client._decoratedEvents === undefined) client._decoratedEvents = {} client._decoratedEvents[name === undefined ? prop : name] = listener } } + +export function slash(name?: string, guild?: string) { + return function (client: Client | SlashModule, prop: string) { + if (client._decoratedSlash === undefined) client._decoratedSlash = [] + const item = (client as { [name: string]: any })[prop] + if (typeof item !== 'function') { + client._decoratedSlash.push(item) + } else + client._decoratedSlash.push({ + name: name ?? prop, + guild, + handler: item + }) + } +} + +export function slashModule() { + return function (client: Client, prop: string) { + if (client._decoratedSlashModules === undefined) + client._decoratedSlashModules = [] + + const mod = ((client as unknown) as { [key: string]: any })[prop] + client._decoratedSlashModules.push(mod) + } +} diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 472e4f80..7fba315b 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -383,18 +383,26 @@ export class CommandClient extends Client implements CommandClientOptions { export function command(options?: CommandOptions) { return function (target: CommandClient | Extension, name: string) { - const command = new Command() + if (target._decoratedCommands === undefined) target._decoratedCommands = {} - command.name = name - command.execute = ((target as unknown) as { + const prop = ((target as unknown) as { [name: string]: (ctx: CommandContext) => any })[name] + if (prop instanceof Command) { + target._decoratedCommands[prop.name] = prop + return + } + + const command = new Command() + + command.name = name + command.execute = prop + if (options !== undefined) Object.assign(command, options) if (target instanceof Extension) command.extension = target - if (target._decoratedCommands === undefined) target._decoratedCommands = {} target._decoratedCommands[command.name] = command } } diff --git a/src/models/rest.ts b/src/models/rest.ts index bac7a56e..0d9ccd00 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -277,11 +277,11 @@ export class RESTManager { message: body?.message, errors: Object.fromEntries( Object.entries( - body?.errors as { + (body?.errors as { [name: string]: { _errors: Array<{ code: string; message: string }> } - } + }) ?? {} ).map((entry) => { return [entry[0], entry[1]._errors] }) diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts new file mode 100644 index 00000000..c397dde6 --- /dev/null +++ b/src/models/slashClient.ts @@ -0,0 +1,222 @@ +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, + 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 { + slash: SlashCommandsManager + id: string + applicationID: string + name: string + description: string + options: SlashCommandOption[] + _guild?: string + + constructor(manager: SlashCommandsManager, data: SlashCommandPayload) { + this.slash = manager + this.id = data.id + this.applicationID = data.application_id + this.name = data.name + this.description = data.description + this.options = data.options + } + + async delete(): Promise { + await this.slash.delete(this.id, this._guild) + } + + async edit(data: SlashCommandPartial): Promise { + await this.slash.edit(this.id, data, this._guild) + } +} + +export class SlashCommandsManager { + client: Client + slash: SlashClient + + constructor(client: Client) { + this.client = client + this.slash = client.slash + } + + /** Get all Global Slash Commands */ + async all(): Promise> { + const col = new Collection() + + 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) { + const cmd = new SlashCommand(this, raw) + col.set(raw.id, cmd) + } + + return col + } + + /** Get a Guild's Slash Commands */ + async guild( + guild: Guild | string + ): Promise> { + const col = new Collection() + + 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) { + const cmd = new SlashCommand(this, raw) + cmd._guild = typeof guild === 'string' ? guild : guild.id + col.set(raw.id, cmd) + } + + return col + } + + /** Create a Slash Command (global or Guild) */ + async create( + data: SlashCommandPartial, + guild?: Guild | string + ): Promise { + 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 + ) + + const cmd = new SlashCommand(this, payload) + cmd._guild = + typeof guild === 'string' || guild === undefined ? guild : guild.id + + return cmd + } + + /** Edit a Slash Command (global or Guild) */ + async edit( + id: string, + data: SlashCommandPartial, + guild?: Guild | string + ): Promise { + 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 + } + + /** Delete a Slash Command (global or Guild) */ + async delete( + id: string, + guild?: Guild | string + ): Promise { + await this.client.rest.delete( + 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 + ) + ) + return this + } +} + +export type SlashCommandHandlerCallback = (interaction: Interaction) => any +export interface SlashCommandHandler { + name: string + guild?: string + handler: SlashCommandHandlerCallback +} + +export class SlashClient { + client: Client + enabled: boolean = true + commands: SlashCommandsManager + handlers: SlashCommandHandler[] = [] + + constructor(client: Client, options?: SlashOptions) { + this.client = client + this.commands = new SlashCommandsManager(client) + + if (options !== undefined) { + this.enabled = options.enabled ?? true + } + + if (this.client._decoratedSlash !== undefined) { + this.client._decoratedSlash.forEach((e) => { + this.handlers.push(e) + }) + } + + this.client.on('interactionCreate', (interaction) => + this.process(interaction) + ) + } + + /** Adds a new Slash Command Handler */ + handle( + name: string, + handler: SlashCommandHandlerCallback, + guild?: string + ): SlashClient { + this.handlers.push({ + name, + guild, + handler + }) + return this + } + + /** Process an incoming Slash Command (interaction) */ + private process(interaction: Interaction): void { + if (!this.enabled) return + + if (interaction.type !== InteractionType.APPLICATION_COMMAND) return + + let cmd + + if (interaction.guild !== undefined) + cmd = + this.handlers.find( + (e) => e.guild !== undefined && e.name === interaction.name + ) ?? this.handlers.find((e) => e.name === interaction.name) + else cmd = this.handlers.find((e) => e.name === interaction.name) + + if (cmd === undefined) return + + cmd.handler(interaction) + } +} diff --git a/src/models/slashModule.ts b/src/models/slashModule.ts new file mode 100644 index 00000000..c44368e3 --- /dev/null +++ b/src/models/slashModule.ts @@ -0,0 +1,18 @@ +import { SlashCommandHandler } from './slashClient.ts' + +export class SlashModule { + name: string = '' + commands: SlashCommandHandler[] = [] + _decoratedSlash?: SlashCommandHandler[] + + constructor() { + if (this._decoratedSlash !== undefined) { + this.commands = this._decoratedSlash + } + } + + add(handler: SlashCommandHandler): SlashModule { + this.commands.push(handler) + return this + } +} diff --git a/src/structures/presence.ts b/src/structures/presence.ts index 0fdac0d5..a5c86af1 100644 --- a/src/structures/presence.ts +++ b/src/structures/presence.ts @@ -50,17 +50,23 @@ export class Presence extends Base { } } +interface StatusPayload extends StatusUpdatePayload { + client_status?: ClientStatus +} + export class ClientPresence { status: StatusType = 'online' activity?: ActivityGame | ActivityGame[] since?: number | null afk?: boolean + clientStatus?: ClientStatus - constructor(data?: ClientActivity | StatusUpdatePayload | ActivityGame) { + constructor(data?: ClientActivity | StatusPayload | ActivityGame) { if (data !== undefined) { if ((data as ClientActivity).activity !== undefined) { Object.assign(this, data) - } else if ((data as StatusUpdatePayload).activities !== undefined) { + } else if ((data as StatusPayload).activities !== undefined) { + this.parse(data as StatusPayload) } else if ((data as ActivityGame).name !== undefined) { if (this.activity === undefined) { this.activity = data as ActivityGame @@ -71,11 +77,12 @@ export class ClientPresence { } } - parse(payload: StatusUpdatePayload): ClientPresence { + parse(payload: StatusPayload): ClientPresence { this.afk = payload.afk this.activity = payload.activities ?? undefined this.since = payload.since this.status = payload.status + // this.clientStatus = payload.client_status return this } @@ -83,12 +90,13 @@ export class ClientPresence { return new ClientPresence().parse(payload) } - create(): StatusUpdatePayload { + create(): StatusPayload { return { afk: this.afk === undefined ? false : this.afk, activities: this.createActivity(), since: this.since === undefined ? null : this.since, status: this.status === undefined ? 'online' : this.status + // client_status: this.clientStatus } } @@ -144,4 +152,13 @@ export class ClientPresence { this.since = since return this } + + // setClientStatus( + // client: 'desktop' | 'web' | 'mobile', + // status: StatusType + // ): ClientPresence { + // if (this.clientStatus === undefined) this.clientStatus = {} + // this.clientStatus[client] = status + // return this + // } } diff --git a/src/structures/slash.ts b/src/structures/slash.ts new file mode 100644 index 00000000..81666ff6 --- /dev/null +++ b/src/structures/slash.ts @@ -0,0 +1,233 @@ +import { Client } from '../models/client.ts' +import { MessageOption } from '../types/channel.ts' +import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.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 { Message } from './message.ts' +import { GuildTextChannel, TextChannel } from './textChannel.ts' +import { User } from './user.ts' +import { Webhook } from './webhook.ts' + +interface WebhookMessageOptions extends MessageOption { + embeds?: Embed[] + name?: string + avatar?: string +} + +type AllWebhookMessageOptions = string | WebhookMessageOptions + +export interface InteractionResponse { + type?: InteractionResponseType + content?: string + embeds?: Embed[] + tts?: boolean + flags?: number + temp?: boolean + allowedMentions?: { + parse?: string + roles?: string[] + users?: string[] + everyone?: boolean + } +} + +export class Interaction { + client: Client + type: number + token: string + id: string + data: InteractionData + channel: GuildTextChannel + guild: Guild + member: Member + _savedHook?: Webhook + + 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 = 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 + } + + option(name: string): T { + return this.data.options.find((e) => e.name === name)?.value + } + + async respond(data: InteractionResponse): Promise { + const payload: InteractionResponsePayload = { + type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: + data.type === undefined || + data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || + data.type === InteractionResponseType.CHANNEL_MESSAGE + ? { + content: data.content ?? '', + embeds: data.embeds, + tts: data.tts ?? false, + flags: data.temp === true ? 64 : data.flags ?? undefined, + allowed_mentions: (data.allowedMentions ?? undefined) as any + } + : undefined + } + + await this.client.rest.post( + INTERACTION_CALLBACK(this.id, this.token), + payload + ) + + return this + } + + async editResponse(data: { + content?: string + embeds?: Embed[] + }): Promise { + const url = WEBHOOK_MESSAGE( + this.client.user?.id as string, + this.token, + '@original' + ) + await this.client.rest.patch(url, { + content: data.content ?? '', + embeds: data.embeds ?? [] + }) + return this + } + + async deleteResponse(): Promise { + const url = WEBHOOK_MESSAGE( + this.client.user?.id as string, + this.token, + '@original' + ) + await this.client.rest.delete(url) + return this + } + + get url(): string { + return `https://discord.com/api/v8/webhooks/${this.client.user?.id}/${this.token}` + } + + async send( + text?: string | AllWebhookMessageOptions, + option?: AllWebhookMessageOptions + ): Promise { + if (typeof text === 'object') { + option = text + text = undefined + } + + if (text === undefined && option === undefined) { + throw new Error('Either text or option is necessary.') + } + + if (option instanceof Embed) + option = { + embeds: [option] + } + + const payload: any = { + content: text, + embeds: + (option as WebhookMessageOptions)?.embed !== undefined + ? [(option as WebhookMessageOptions).embed] + : (option as WebhookMessageOptions)?.embeds !== undefined + ? (option as WebhookMessageOptions).embeds + : undefined, + file: (option as WebhookMessageOptions)?.file, + tts: (option as WebhookMessageOptions)?.tts, + allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions + } + + if ((option as WebhookMessageOptions)?.name !== undefined) { + payload.username = (option as WebhookMessageOptions)?.name + } + + if ((option as WebhookMessageOptions)?.avatar !== undefined) { + payload.avatar = (option as WebhookMessageOptions)?.avatar + } + + if ( + payload.embeds !== undefined && + payload.embeds instanceof Array && + payload.embeds.length > 10 + ) + throw new Error( + `Cannot send more than 10 embeds through Interaction Webhook` + ) + + const resp = await this.client.rest.post(`${this.url}?wait=true`, payload) + + const res = new Message( + this.client, + resp, + (this as unknown) as TextChannel, + (this as unknown) as User + ) + await res.mentions.fromPayload(resp) + return res + } + + async editMessage( + msg: Message | string, + data: { + content?: string + embeds?: Embed[] + file?: any + allowed_mentions?: { + parse?: string + roles?: string[] + users?: string[] + everyone?: boolean + } + } + ): Promise { + await this.client.rest.patch( + WEBHOOK_MESSAGE( + this.client.user?.id as string, + this.token ?? this.client.token, + typeof msg === 'string' ? msg : msg.id + ), + data + ) + return this + } + + async deleteMessage(msg: Message | string): Promise { + await this.client.rest.delete( + WEBHOOK_MESSAGE( + this.client.user?.id as string, + this.token ?? this.client.token, + typeof msg === 'string' ? msg : msg.id + ) + ) + return this + } +} diff --git a/src/structures/voiceState.ts b/src/structures/voiceState.ts index 92e58b50..cd3cc7f2 100644 --- a/src/structures/voiceState.ts +++ b/src/structures/voiceState.ts @@ -15,6 +15,8 @@ export class VoiceState extends Base { sessionID: string deaf: boolean mute: boolean + selfDeaf: boolean + selfMute: boolean stream?: boolean video: boolean suppress: boolean @@ -38,8 +40,8 @@ export class VoiceState extends Base { this.guild = _data.guild this.deaf = data.deaf this.mute = data.mute - this.deaf = data.self_deaf - this.mute = data.self_mute + this.selfDeaf = data.self_deaf + this.selfMute = data.self_mute this.stream = data.self_stream this.video = data.self_video this.suppress = data.suppress @@ -52,6 +54,8 @@ export class VoiceState extends Base { this.mute = data.mute ?? this.mute this.deaf = data.self_deaf ?? this.deaf this.mute = data.self_mute ?? this.mute + this.selfDeaf = data.self_deaf ?? this.selfDeaf + this.selfMute = data.self_mute ?? this.selfMute this.stream = data.self_stream ?? this.stream this.video = data.self_video ?? this.video this.suppress = data.suppress ?? this.suppress diff --git a/src/structures/webhook.ts b/src/structures/webhook.ts index ccc32f6e..dd5dc8c3 100644 --- a/src/structures/webhook.ts +++ b/src/structures/webhook.ts @@ -12,6 +12,7 @@ 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 { WEBHOOK_MESSAGE } from '../types/endpoint.ts' export interface WebhookMessageOptions extends MessageOption { embeds?: Embed[] @@ -191,4 +192,40 @@ export class Webhook { if (resp.response.status !== 204) return false else return true } + + async editMessage( + message: string | Message, + data: { + content?: string + embeds?: Embed[] + file?: any + allowed_mentions?: { + parse?: string + roles?: string[] + users?: string[] + everyone?: boolean + } + } + ): Promise { + await this.client?.rest.patch( + WEBHOOK_MESSAGE( + this.id, + (this.token ?? this.client.token) as string, + typeof message === 'string' ? message : message.id + ), + data + ) + return this + } + + async deleteMessage(message: string | Message): Promise { + await this.client?.rest.delete( + WEBHOOK_MESSAGE( + this.id, + (this.token ?? this.client.token) as string, + typeof message === 'string' ? message : message.id + ) + ) + return this + } } diff --git a/src/test/index.ts b/src/test/index.ts index 2da08456..3ae10f5d 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -2,7 +2,6 @@ import { Client, Intents, Message, - ClientPresence, Member, Role, GuildChannel, @@ -15,10 +14,9 @@ import { import { TOKEN } from './config.ts' const client = new Client({ - presence: new ClientPresence({ - name: 'Pokémon Sword', - type: 'COMPETING' - }) + clientProperties: { + browser: 'Discord iOS' + } // bot: false, // cache: new RedisCacheAdapter({ // hostname: '127.0.0.1', diff --git a/src/test/music.ts b/src/test/music.ts new file mode 100644 index 00000000..e43b2a46 --- /dev/null +++ b/src/test/music.ts @@ -0,0 +1,137 @@ +import { + CommandClient, + event, + Intents, + command, + CommandContext, + Extension, + Collection +} from '../../mod.ts' +import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' +import { + Manager, + Player +} from 'https://raw.githubusercontent.com/DjDeveloperr/lavaclient-deno/master/mod.ts' + +export const nodes = [ + { + id: 'main', + host: LL_IP, + port: LL_PORT, + password: LL_PASS + } +] + +class MyClient extends CommandClient { + manager: Manager + + constructor() { + super({ + prefix: ['.'], + caseSensitive: false + }) + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const client = this + + this.manager = new Manager(nodes, { + send(id, payload) { + // Sharding not added yet + client.gateway?.send(payload) + } + }) + + this.manager.on('socketError', ({ id }, error) => + console.error(`${id} ran into an error`, error) + ) + this.manager.on('socketReady', (node) => + console.log(`${node.id} connected.`) + ) + + this.on('raw', (evt: string, d: any) => { + if (evt === 'VOICE_SERVER_UPDATE') this.manager.serverUpdate(d) + else if (evt === 'VOICE_STATE_UPDATE') this.manager.stateUpdate(d) + }) + } + + @event() + ready(): void { + console.log(`Logged in as ${this.user?.tag}!`) + this.manager.init(this.user?.id as string) + } +} + +const players = new Collection() + +class VCExtension extends Extension { + name = 'VC' + subPrefix = 'vc' + + @command() + async join(ctx: CommandContext): Promise { + if (players.has(ctx.guild?.id as string) === true) + return ctx.message.reply(`Already playing in this server!`) + + ctx.argString = ctx.argString.slice(4).trim() + + if (ctx.argString === '') + return ctx.message.reply('You gave nothing to search.') + + const userVS = await ctx.guild?.voiceStates.get(ctx.author.id) + if (userVS === undefined) { + ctx.message.reply("You're not in VC.") + return + } + + const player = (ctx.client as MyClient).manager.create( + ctx.guild?.id as string + ) + + await player.connect(userVS.channel?.id as string, { selfDeaf: true }) + + ctx.message.reply(`Joined VC channel - ${userVS.channel?.name}!`) + + players.set(ctx.guild?.id as string, player) + + ctx.channel.send(`Loading...`) + + ctx.channel.send(`Searching for ${ctx.argString}...`) + + const { track, info } = await player.manager + .search(`ytsearch:${ctx.argString}`) + .then((e) => e.tracks[0]) + + await player.play(track) + + ctx.channel.send(`Now playing ${info.title}!`) + } + + @command() + async leave(ctx: CommandContext): Promise { + const userVS = await ctx.guild?.voiceStates.get( + (ctx.client.user?.id as unknown) as string + ) + if (userVS === undefined) { + ctx.message.reply("I'm not in VC.") + return + } + userVS.channel?.leave() + ctx.message.reply(`Left VC channel - ${userVS.channel?.name}!`) + + if (players.has(ctx.guild?.id as string) !== true) + return ctx.message.reply('Not playing anything in this server.') + + const player = (players.get(ctx.guild?.id as string) as unknown) as Player + await player.stop() + await player.destroy() + + players.delete(ctx.guild?.id as string) + ctx.message.reply('Stopped player') + } +} + +const client = new MyClient() + +client.extensions.load(VCExtension) + +client.connect(TOKEN, Intents.None) diff --git a/src/test/slash-cmd.ts b/src/test/slash-cmd.ts new file mode 100644 index 00000000..043b96d9 --- /dev/null +++ b/src/test/slash-cmd.ts @@ -0,0 +1,51 @@ +import { TOKEN } from './config.ts' + +export const CMD = { + name: 'blep', + description: 'Send a random adorable animal photo', + options: [ + { + name: 'animal', + description: 'The type of animal', + type: 3, + required: true, + choices: [ + { + name: 'Dog', + value: 'animal_dog' + }, + { + name: 'Cat', + value: 'animal_dog' + }, + { + name: 'Penguin', + value: 'animal_penguin' + } + ] + }, + { + name: 'only_smol', + description: 'Whether to show only baby animals', + type: 5, + required: false + } + ] +} + +// fetch('https://discord.com/api/v8/applications/783937840752099332/commands', { +fetch( + 'https://discord.com/api/v8/applications/783937840752099332/guilds/783319033205751809/commands', + { + method: 'POST', + body: JSON.stringify(CMD), + headers: { + 'Content-Type': 'application/json', + Authorization: + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + 'Bot ' + TOKEN + } + } +) + .then((r) => r.json()) + .then(console.log) diff --git a/src/test/slash.ts b/src/test/slash.ts new file mode 100644 index 00000000..5a1c3b90 --- /dev/null +++ b/src/test/slash.ts @@ -0,0 +1,96 @@ +import { Client, Intents, event, slash } from '../../mod.ts' +import { Embed } from '../structures/embed.ts' +import { Interaction } from '../structures/slash.ts' +import { TOKEN } from './config.ts' + +export class MyClient extends Client { + @event() + ready(): void { + console.log(`Logged in as ${this.user?.tag}!`) + } + + @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]') + } + d.respond({ + content: '```js\n' + `${res}` + '\n```' + }).catch(() => {}) + } catch (e) { + d.respond({ + content: '```js\n' + `${e.stack}` + '\n```' + }) + } + } + } + + @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() + 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!` + }) + } +} + +const client = new MyClient() +client.connect(TOKEN, Intents.None) diff --git a/src/types/endpoint.ts b/src/types/endpoint.ts index a34575a1..22713402 100644 --- a/src/types/endpoint.ts +++ b/src/types/endpoint.ts @@ -191,81 +191,35 @@ const VOICE_REGIONS = (guildID: string): string => const CLIENT_USER = (): string => `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/users/@me` -export default [ - GUILDS, - GUILD, - GUILD_AUDIT_LOGS, - GUILD_WIDGET, - GUILD_EMOJI, - GUILD_ROLE, - GUILD_ROLES, - GUILD_INTEGRATION, - GUILD_INTEGRATIONS, - GUILD_INTEGARTION_SYNC, - GUILD_WIDGET_IMAGE, - GUILD_BAN, - GUILD_BANS, - GUILD_CHANNEL, - GUILD_CHANNELS, - GUILD_MEMBER, - CLIENT_USER, - GUILD_MEMBERS, - GUILD_MEMBER_ROLE, - GUILD_INVITES, - GUILD_LEAVE, - GUILD_PRUNE, - GUILD_VANITY_URL, - GUILD_NICK, - GUILD_PREVIEW, - CHANNEL, - CHANNELS, - CHANNEL_MESSAGE, - CHANNEL_MESSAGES, - CHANNEL_CROSSPOST, - MESSAGE_REACTIONS, - MESSAGE_REACTION, - MESSAGE_REACTION_ME, - MESSAGE_REACTION_USER, - CHANNEL_BULK_DELETE, - CHANNEL_FOLLOW, - CHANNEL_INVITES, - CHANNEL_PIN, - CHANNEL_PINS, - CHANNEL_PERMISSION, - CHANNEL_TYPING, - GROUP_RECIPIENT, - CURRENT_USER, - CURRENT_USER_GUILDS, - USER_DM, - USER_CONNECTIONS, - LEAVE_GUILD, - USER, - CHANNEL_WEBHOOKS, - GUILD_WEBHOOK, - WEBHOOK, - WEBHOOK_WITH_TOKEN, - SLACK_WEBHOOK, - GITHUB_WEBHOOK, - GATEWAY, - GATEWAY_BOT, - CUSTOM_EMOJI, - GUILD_ICON, - GUILD_SPLASH, - GUILD_DISCOVERY_SPLASH, - GUILD_BANNER, - DEFAULT_USER_AVATAR, - USER_AVATAR, - APPLICATION_ASSET, - ACHIEVEMENT_ICON, - TEAM_ICON, - EMOJI, - GUILD_EMOJIS, - TEMPLATE, - INVITE, - VOICE_REGIONS -] +const APPLICATION_COMMANDS = (id: string): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/commands` + +const APPLICATION_COMMAND = (id: string, cmdID: string): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/commands/${cmdID}` + +const APPLICATION_GUILD_COMMANDS = (id: string, guildID: string): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/guilds/${guildID}/commands` + +const APPLICATION_GUILD_COMMAND = ( + id: string, + guildID: string, + cmdID: string +): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/guilds/${guildID}/commands/${cmdID}` + +const WEBHOOK_MESSAGE = (id: string, token: string, msgID: string): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/webhooks/${id}/${token}/messages/${msgID}` + +const INTERACTION_CALLBACK = (id: string, token: string): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/interactions/${id}/${token}/callback` export { + INTERACTION_CALLBACK, + APPLICATION_COMMAND, + APPLICATION_GUILD_COMMAND, + WEBHOOK_MESSAGE, + APPLICATION_COMMANDS, + APPLICATION_GUILD_COMMANDS, GUILDS, GUILD, GUILD_AUDIT_LOGS, diff --git a/src/types/gateway.ts b/src/types/gateway.ts index 9d9bc291..bbd2e48b 100644 --- a/src/types/gateway.ts +++ b/src/types/gateway.ts @@ -105,7 +105,8 @@ export enum GatewayEvents { User_Update = 'USER_UPDATE', Voice_Server_Update = 'VOICE_SERVER_UPDATE', Voice_State_Update = 'VOICE_STATE_UPDATE', - Webhooks_Update = 'WEBHOOKS_UPDATE' + Webhooks_Update = 'WEBHOOKS_UPDATE', + Interaction_Create = 'INTERACTION_CREATE' } export interface IdentityPayload { @@ -120,11 +121,11 @@ export interface IdentityPayload { } export interface IdentityConnection { - $os: 'darwin' | 'windows' | 'linux' | 'custom os' - $browser: 'harmony' | 'Firefox' - $device: 'harmony' | '' - $referrer?: '' - $referring_domain?: '' + $os: 'darwin' | 'windows' | 'linux' | 'custom os' | string + $browser: 'harmony' | 'Firefox' | string + $device: 'harmony' | string + $referrer?: '' | string + $referring_domain?: '' | string } export interface Resume { diff --git a/src/types/slash.ts b/src/types/slash.ts new file mode 100644 index 00000000..6e5290c7 --- /dev/null +++ b/src/types/slash.ts @@ -0,0 +1,90 @@ +import { EmbedPayload } from './channel.ts' +import { MemberPayload } from './guild.ts' + +export interface InteractionOption { + name: string + value?: any + options?: any[] +} + +export interface InteractionData { + name: string + id: string + options: InteractionOption[] +} + +export enum InteractionType { + PING = 1, + APPLICATION_COMMAND = 2 +} + +export interface InteractionPayload { + type: InteractionType + token: string + member: MemberPayload + id: string + data: InteractionData + guild_id: string + channel_id: string +} + +export interface SlashCommandChoice { + name: string + value: string +} + +export enum SlashCommandOptionType { + SUB_COMMAND = 1, + SUB_COMMAND_GROUP = 2, + STRING = 3, + INTEGER = 4, + BOOLEAN = 5, + USER = 6, + CHANNEL = 7, + ROLE = 8 +} + +export interface SlashCommandOption { + name: string + description: string + type: SlashCommandOptionType + required: boolean + choices?: SlashCommandChoice[] + options?: SlashCommandOption[] +} + +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, + CHANNEL_MESSAGE = 3, + CHANNEL_MESSAGE_WITH_SOURCE = 4, + ACK_WITH_SOURCE = 5 +} + +export interface InteractionResponsePayload { + type: InteractionResponseType + data?: InteractionResponseDataPayload +} + +export interface InteractionResponseDataPayload { + tts?: boolean + content: string + embeds?: EmbedPayload[] + allowed_mentions?: { + parse?: 'everyone' | 'users' | 'roles' + roles?: string[] + users?: string[] + } + flags?: number +} diff --git a/src/types/userFlags.ts b/src/types/userFlags.ts index a76dbe86..eebd1efc 100644 --- a/src/types/userFlags.ts +++ b/src/types/userFlags.ts @@ -1,7 +1,6 @@ export const UserFlags = { DISCORD_EMPLOYEE: 1 << 0, PARTNERED_SERVER_OWNER: 1 << 1, - DISCORD_PARTNER: 1 << 1, HYPESQUAD_EVENTS: 1 << 2, BUGHUNTER_LEVEL_1: 1 << 3, HOUSE_BRAVERY: 1 << 6, diff --git a/src/types/voice.ts b/src/types/voice.ts index 45f518c5..66ea4148 100644 --- a/src/types/voice.ts +++ b/src/types/voice.ts @@ -1,7 +1,6 @@ -// https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice import { MemberPayload } from './guild.ts' -export enum VoiceOpcodes { // add VoiceOpcodes - UnderC - +export enum VoiceOpcodes { IDENTIFY = 0, SELECT_PROTOCOL = 1, READY = 2,