Merge pull request #106 from DjDeveloperr/slash
BREAKING: DM Slash Commands, new Interactions API changes
This commit is contained in:
		
						commit
						7dc316c76f
					
				
					 22 changed files with 973 additions and 426 deletions
				
			
		
							
								
								
									
										106
									
								
								deploy.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								deploy.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | ||||||
|  | import { | ||||||
|  |   SlashCommandsManager, | ||||||
|  |   SlashClient, | ||||||
|  |   SlashCommandHandlerCallback | ||||||
|  | } from './src/models/slashClient.ts' | ||||||
|  | import { InteractionResponseType, InteractionType } from './src/types/slash.ts' | ||||||
|  | 
 | ||||||
|  | export interface DeploySlashInitOptions { | ||||||
|  |   env?: boolean | ||||||
|  |   publicKey?: string | ||||||
|  |   token?: string | ||||||
|  |   id?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let client: SlashClient | ||||||
|  | let commands: SlashCommandsManager | ||||||
|  | 
 | ||||||
|  | export function init(options: DeploySlashInitOptions): void { | ||||||
|  |   if (client !== undefined) throw new Error('Already initialized') | ||||||
|  |   if (options.env === true) { | ||||||
|  |     options.publicKey = Deno.env.get('PUBLIC_KEY') | ||||||
|  |     options.token = Deno.env.get('TOKEN') | ||||||
|  |     options.id = Deno.env.get('ID') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (options.publicKey === undefined) | ||||||
|  |     throw new Error('Public Key not provided') | ||||||
|  | 
 | ||||||
|  |   client = new SlashClient({ | ||||||
|  |     id: options.id, | ||||||
|  |     token: options.token, | ||||||
|  |     publicKey: options.publicKey | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   commands = client.commands | ||||||
|  | 
 | ||||||
|  |   const cb = async (evt: { | ||||||
|  |     respondWith: CallableFunction | ||||||
|  |     request: Request | ||||||
|  |   }): Promise<void> => { | ||||||
|  |     try { | ||||||
|  |       const d = await client.verifyFetchEvent({ | ||||||
|  |         respondWith: (...args: any[]) => evt.respondWith(...args), | ||||||
|  |         request: evt.request, | ||||||
|  |       }) | ||||||
|  |       if (d === false) { | ||||||
|  |         await evt.respondWith( | ||||||
|  |           new Response('Not Authorized', { | ||||||
|  |             status: 400 | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (d.type === InteractionType.PING) { | ||||||
|  |         await d.respond({ type: InteractionResponseType.PONG }) | ||||||
|  |         client.emit('ping') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       await (client as any)._process(d) | ||||||
|  |     } catch (e) { | ||||||
|  |       console.log(e) | ||||||
|  |       await client.emit('interactionError', e) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   addEventListener('fetch', cb as any) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function handle( | ||||||
|  |   cmd: | ||||||
|  |     | string | ||||||
|  |     | { | ||||||
|  |       name: string | ||||||
|  |       parent?: string | ||||||
|  |       group?: string | ||||||
|  |       guild?: string | ||||||
|  |     }, | ||||||
|  |   handler: SlashCommandHandlerCallback | ||||||
|  | ): void { | ||||||
|  |   const handle = { | ||||||
|  |     name: typeof cmd === 'string' ? cmd : cmd.name, | ||||||
|  |     handler, | ||||||
|  |     ...(typeof cmd === 'string' ? {} : cmd) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (typeof handle.name === 'string' && handle.name.includes(' ') && handle.parent === undefined && handle.group === undefined) { | ||||||
|  |     const parts = handle.name.split(/ +/).filter(e => e !== '') | ||||||
|  |     if (parts.length > 3 || parts.length < 1) throw new Error('Invalid command name') | ||||||
|  |     const root = parts.shift() as string | ||||||
|  |     const group = parts.length === 2 ? parts.shift() : undefined | ||||||
|  |     const sub = parts.shift() | ||||||
|  | 
 | ||||||
|  |     handle.name = sub ?? root | ||||||
|  |     handle.group = group | ||||||
|  |     handle.parent = sub === undefined ? undefined : root | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   client.handle(handle) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { commands, client } | ||||||
|  | export * from './src/types/slash.ts' | ||||||
|  | export * from './src/structures/slash.ts' | ||||||
|  | export * from './src/models/slashClient.ts' | ||||||
							
								
								
									
										5
									
								
								deps.ts
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								deps.ts
									
										
									
									
									
								
							|  | @ -1,11 +1,6 @@ | ||||||
| export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' | export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' | ||||||
| export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' | export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' | ||||||
| export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts' | export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts' | ||||||
| export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts' |  | ||||||
| export type { |  | ||||||
|   Redis, |  | ||||||
|   RedisConnectOptions |  | ||||||
| } from 'https://deno.land/x/redis@v0.14.1/mod.ts' |  | ||||||
| export { walk } from 'https://deno.land/std@0.86.0/fs/walk.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' | export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' | ||||||
| export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' | export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								mod.ts
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								mod.ts
									
										
									
									
									
								
							|  | @ -5,7 +5,13 @@ export type { GatewayTypedEvents } from './src/gateway/index.ts' | ||||||
| export type { ClientEvents } from './src/gateway/handlers/index.ts' | export type { ClientEvents } from './src/gateway/handlers/index.ts' | ||||||
| export * from './src/models/client.ts' | export * from './src/models/client.ts' | ||||||
| export * from './src/models/slashClient.ts' | export * from './src/models/slashClient.ts' | ||||||
| export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts' | export { | ||||||
|  |   RESTManager, | ||||||
|  |   TokenType, | ||||||
|  |   HttpResponseCode, | ||||||
|  |   DiscordAPIError | ||||||
|  | } from './src/models/rest.ts' | ||||||
|  | export type { APIMap, DiscordAPIErrorPayload } from './src/models/rest.ts' | ||||||
| export type { RequestHeaders } from './src/models/rest.ts' | export type { RequestHeaders } from './src/models/rest.ts' | ||||||
| export type { RESTOptions } from './src/models/rest.ts' | export type { RESTOptions } from './src/models/rest.ts' | ||||||
| export * from './src/models/cacheAdapter.ts' | export * from './src/models/cacheAdapter.ts' | ||||||
|  | @ -63,7 +69,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts' | ||||||
| export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' | export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' | ||||||
| export { Invite } from './src/structures/invite.ts' | export { Invite } from './src/structures/invite.ts' | ||||||
| export * from './src/structures/member.ts' | export * from './src/structures/member.ts' | ||||||
| export { Message, MessageAttachment } from './src/structures/message.ts' | export { | ||||||
|  |   Message, | ||||||
|  |   MessageAttachment, | ||||||
|  |   MessageInteraction | ||||||
|  | } from './src/structures/message.ts' | ||||||
| export { MessageMentions } from './src/structures/messageMentions.ts' | export { MessageMentions } from './src/structures/messageMentions.ts' | ||||||
| export { | export { | ||||||
|   Presence, |   Presence, | ||||||
|  | @ -110,6 +120,16 @@ export type { | ||||||
|   GuildVoiceChannelPayload, |   GuildVoiceChannelPayload, | ||||||
|   GroupDMChannelPayload, |   GroupDMChannelPayload, | ||||||
|   MessageOptions, |   MessageOptions, | ||||||
|  |   MessagePayload, | ||||||
|  |   MessageInteractionPayload, | ||||||
|  |   MessageReference, | ||||||
|  |   MessageActivity, | ||||||
|  |   MessageActivityTypes, | ||||||
|  |   MessageApplication, | ||||||
|  |   MessageFlags, | ||||||
|  |   MessageStickerFormatTypes, | ||||||
|  |   MessageStickerPayload, | ||||||
|  |   MessageTypes, | ||||||
|   OverwriteAsArg, |   OverwriteAsArg, | ||||||
|   Overwrite, |   Overwrite, | ||||||
|   OverwriteAsOptions |   OverwriteAsOptions | ||||||
|  | @ -146,5 +166,7 @@ export { UserFlags } from './src/types/userFlags.ts' | ||||||
| export type { VoiceStatePayload } from './src/types/voice.ts' | export type { VoiceStatePayload } from './src/types/voice.ts' | ||||||
| export type { WebhookPayload } from './src/types/webhook.ts' | export type { WebhookPayload } from './src/types/webhook.ts' | ||||||
| export * from './src/models/collectors.ts' | export * from './src/models/collectors.ts' | ||||||
|  | export type { Dict } from './src/utils/dict.ts' | ||||||
|  | export * from './src/models/redisCache.ts' | ||||||
| export { ColorUtil } from './src/utils/colorutil.ts' | export { ColorUtil } from './src/utils/colorutil.ts' | ||||||
| export type { Colors } from './src/utils/colorutil.ts' | export type { Colors } from './src/utils/colorutil.ts' | ||||||
|  |  | ||||||
|  | @ -414,4 +414,5 @@ export type ClientEvents = { | ||||||
|   commandMissingArgs: [ctx: CommandContext] |   commandMissingArgs: [ctx: CommandContext] | ||||||
|   commandUsed: [ctx: CommandContext] |   commandUsed: [ctx: CommandContext] | ||||||
|   commandError: [ctx: CommandContext, err: Error] |   commandError: [ctx: CommandContext, err: Error] | ||||||
|  |   gatewayError: [err: ErrorEvent, shards: [number, number]] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,29 +1,110 @@ | ||||||
|  | import { Guild } from '../../structures/guild.ts' | ||||||
| import { Member } from '../../structures/member.ts' | import { Member } from '../../structures/member.ts' | ||||||
| import { Interaction } from '../../structures/slash.ts' | import { | ||||||
|  |   Interaction, | ||||||
|  |   InteractionApplicationCommandResolved, | ||||||
|  |   InteractionChannel | ||||||
|  | } from '../../structures/slash.ts' | ||||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||||
| import { InteractionPayload } from '../../types/slash.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' | import { Gateway, GatewayEventHandler } from '../index.ts' | ||||||
|  | import { User } from '../../structures/user.ts' | ||||||
|  | import { Role } from '../../structures/role.ts' | ||||||
| 
 | 
 | ||||||
| export const interactionCreate: GatewayEventHandler = async ( | 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.
 | ||||||
|   if (guild === undefined) return |   // 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 | ||||||
| 
 | 
 | ||||||
|   await guild.members.set(d.member.user.id, d.member) |   const guild = | ||||||
|   const member = ((await guild.members.get( |     d.guild_id === undefined | ||||||
|     d.member.user.id |       ? undefined | ||||||
|   )) as unknown) as Member |       : await gateway.client.guilds.get(d.guild_id) | ||||||
|  | 
 | ||||||
|  |   if (d.member !== undefined) | ||||||
|  |     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) | ||||||
|  |       : 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<GuildTextBasedChannel>(d.channel_id)) ?? |     (await gateway.client.channels.get<GuildTextBasedChannel>(d.channel_id)) ?? | ||||||
|     (await gateway.client.channels.fetch<GuildTextBasedChannel>(d.channel_id)) |     (await gateway.client.channels.fetch<GuildTextBasedChannel>(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, { |   const interaction = new Interaction(gateway.client, d, { | ||||||
|     member, |     member, | ||||||
|     guild, |     guild, | ||||||
|     channel |     channel, | ||||||
|  |     user, | ||||||
|  |     resolved | ||||||
|   }) |   }) | ||||||
|   gateway.client.emit('interactionCreate', interaction) |   gateway.client.emit('interactionCreate', interaction) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ import { | ||||||
| import { GatewayResponse } from '../types/gatewayResponse.ts' | import { GatewayResponse } from '../types/gatewayResponse.ts' | ||||||
| import { | import { | ||||||
|   GatewayOpcodes, |   GatewayOpcodes, | ||||||
|   GatewayIntents, |  | ||||||
|   GatewayCloseCodes, |   GatewayCloseCodes, | ||||||
|   IdentityPayload, |   IdentityPayload, | ||||||
|   StatusUpdatePayload, |   StatusUpdatePayload, | ||||||
|  | @ -19,6 +18,7 @@ import { delay } from '../utils/delay.ts' | ||||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import { Guild } from '../structures/guild.ts' | ||||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
|  | import { decodeText } from '../utils/encoding.ts' | ||||||
| 
 | 
 | ||||||
| export interface RequestMembersOptions { | export interface RequestMembersOptions { | ||||||
|   limit?: number |   limit?: number | ||||||
|  | @ -57,8 +57,6 @@ export type GatewayTypedEvents = { | ||||||
|  */ |  */ | ||||||
| export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   websocket?: WebSocket |   websocket?: WebSocket | ||||||
|   token?: string |  | ||||||
|   intents?: GatewayIntents[] |  | ||||||
|   connected = false |   connected = false | ||||||
|   initialized = false |   initialized = false | ||||||
|   heartbeatInterval = 0 |   heartbeatInterval = 0 | ||||||
|  | @ -92,7 +90,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     } |     } | ||||||
|     if (data instanceof Uint8Array) { |     if (data instanceof Uint8Array) { | ||||||
|       data = unzlib(data) |       data = unzlib(data) | ||||||
|       data = new TextDecoder('utf-8').decode(data) |       data = decodeText(data) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const { op, d, s, t }: GatewayResponse = JSON.parse(data) |     const { op, d, s, t }: GatewayResponse = JSON.parse(data) | ||||||
|  | @ -157,7 +155,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) | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  | @ -177,8 +175,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|       } |       } | ||||||
|       case GatewayOpcodes.RECONNECT: { |       case GatewayOpcodes.RECONNECT: { | ||||||
|         this.emit('reconnectRequired') |         this.emit('reconnectRequired') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         this.debug('Received OpCode RECONNECT') | ||||||
|         this.reconnect() |         await this.reconnect() | ||||||
|         break |         break | ||||||
|       } |       } | ||||||
|       default: |       default: | ||||||
|  | @ -194,8 +192,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     switch (code) { |     switch (code) { | ||||||
|       case GatewayCloseCodes.UNKNOWN_ERROR: |       case GatewayCloseCodes.UNKNOWN_ERROR: | ||||||
|         this.debug('API has encountered Unknown Error. Reconnecting...') |         this.debug('API has encountered Unknown Error. Reconnecting...') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect() | ||||||
|         this.reconnect() |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.UNKNOWN_OPCODE: |       case GatewayCloseCodes.UNKNOWN_OPCODE: | ||||||
|         throw new Error( |         throw new Error( | ||||||
|  | @ -209,20 +206,17 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|         throw new Error('Invalid Token provided!') |         throw new Error('Invalid Token provided!') | ||||||
|       case GatewayCloseCodes.INVALID_SEQ: |       case GatewayCloseCodes.INVALID_SEQ: | ||||||
|         this.debug('Invalid Seq was sent. Reconnecting.') |         this.debug('Invalid Seq was sent. Reconnecting.') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect() | ||||||
|         this.reconnect() |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.RATE_LIMITED: |       case GatewayCloseCodes.RATE_LIMITED: | ||||||
|         throw new Error("You're ratelimited. Calm down.") |         throw new Error("You're ratelimited. Calm down.") | ||||||
|       case GatewayCloseCodes.SESSION_TIMED_OUT: |       case GatewayCloseCodes.SESSION_TIMED_OUT: | ||||||
|         this.debug('Session Timeout. Reconnecting.') |         this.debug('Session Timeout. Reconnecting.') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect(true) | ||||||
|         this.reconnect(true) |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.INVALID_SHARD: |       case GatewayCloseCodes.INVALID_SHARD: | ||||||
|         this.debug('Invalid Shard was sent. Reconnecting.') |         this.debug('Invalid Shard was sent. Reconnecting.') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect() | ||||||
|         this.reconnect() |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.SHARDING_REQUIRED: |       case GatewayCloseCodes.SHARDING_REQUIRED: | ||||||
|         throw new Error("Couldn't connect. Sharding is required!") |         throw new Error("Couldn't connect. Sharding is required!") | ||||||
|  | @ -260,6 +254,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     error.name = 'ErrorEvent' |     error.name = 'ErrorEvent' | ||||||
|     console.log(error) |     console.log(error) | ||||||
|     this.emit('error', error, event) |     this.emit('error', error, event) | ||||||
|  |     this.client.emit('gatewayError', event, this.shards) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private enqueueIdentify(forceNew?: boolean): void { |   private enqueueIdentify(forceNew?: boolean): void { | ||||||
|  | @ -269,8 +264,9 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async sendIdentify(forceNewSession?: boolean): Promise<void> { |   private async sendIdentify(forceNewSession?: boolean): Promise<void> { | ||||||
|     if (typeof this.token !== 'string') throw new Error('Token not specified') |     if (typeof this.client.token !== 'string') | ||||||
|     if (typeof this.intents !== 'object') |       throw new Error('Token not specified') | ||||||
|  |     if (typeof this.client.intents !== 'object') | ||||||
|       throw new Error('Intents not specified') |       throw new Error('Intents not specified') | ||||||
| 
 | 
 | ||||||
|     if (this.client.fetchGatewayInfo === true) { |     if (this.client.fetchGatewayInfo === true) { | ||||||
|  | @ -300,7 +296,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const payload: IdentityPayload = { |     const payload: IdentityPayload = { | ||||||
|       token: this.token, |       token: this.client.token, | ||||||
|       properties: { |       properties: { | ||||||
|         $os: this.client.clientProperties.os ?? Deno.build.os, |         $os: this.client.clientProperties.os ?? Deno.build.os, | ||||||
|         $browser: this.client.clientProperties.browser ?? 'harmony', |         $browser: this.client.clientProperties.browser ?? 'harmony', | ||||||
|  | @ -311,7 +307,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|         this.shards === undefined |         this.shards === undefined | ||||||
|           ? [0, 1] |           ? [0, 1] | ||||||
|           : [this.shards[0] ?? 0, this.shards[1] ?? 1], |           : [this.shards[0] ?? 0, this.shards[1] ?? 1], | ||||||
|       intents: this.intents.reduce( |       intents: this.client.intents.reduce( | ||||||
|         (previous, current) => previous | current, |         (previous, current) => previous | current, | ||||||
|         0 |         0 | ||||||
|       ), |       ), | ||||||
|  | @ -327,9 +323,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async sendResume(): Promise<void> { |   private async sendResume(): Promise<void> { | ||||||
|     if (typeof this.token !== 'string') throw new Error('Token not specified') |     if (typeof this.client.token !== 'string') | ||||||
|     if (typeof this.intents !== 'object') |       throw new Error('Token not specified') | ||||||
|       throw new Error('Intents not specified') |  | ||||||
| 
 | 
 | ||||||
|     if (this.sessionID === undefined) { |     if (this.sessionID === undefined) { | ||||||
|       this.sessionID = await this.cache.get( |       this.sessionID = await this.cache.get( | ||||||
|  | @ -348,7 +343,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     const resumePayload = { |     const resumePayload = { | ||||||
|       op: GatewayOpcodes.RESUME, |       op: GatewayOpcodes.RESUME, | ||||||
|       d: { |       d: { | ||||||
|         token: this.token, |         token: this.client.token, | ||||||
|         session_id: this.sessionID, |         session_id: this.sessionID, | ||||||
|         seq: this.sequenceID ?? null |         seq: this.sequenceID ?? null | ||||||
|       } |       } | ||||||
|  | @ -405,6 +400,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
| 
 | 
 | ||||||
|   async reconnect(forceNew?: boolean): Promise<void> { |   async reconnect(forceNew?: boolean): Promise<void> { | ||||||
|     this.emit('reconnecting') |     this.emit('reconnecting') | ||||||
|  |     this.debug('Reconnecting... (force new: ' + String(forceNew) + ')') | ||||||
| 
 | 
 | ||||||
|     clearInterval(this.heartbeatIntervalID) |     clearInterval(this.heartbeatIntervalID) | ||||||
|     if (forceNew === true) { |     if (forceNew === true) { | ||||||
|  | @ -432,6 +428,11 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   close(code: number = 1000, reason?: string): void { |   close(code: number = 1000, reason?: string): void { | ||||||
|  |     this.debug( | ||||||
|  |       `Closing with code ${code}${ | ||||||
|  |         reason !== undefined && reason !== '' ? ` and reason ${reason}` : '' | ||||||
|  |       }` | ||||||
|  |     ) | ||||||
|     return this.websocket?.close(code, reason) |     return this.websocket?.close(code, reason) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { connect, Redis, RedisConnectOptions } from '../../deps.ts' |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. |  * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. | ||||||
|  | @ -71,106 +70,3 @@ export class DefaultCacheAdapter implements ICacheAdapter { | ||||||
|     return delete this.data[cacheName] |     return delete this.data[cacheName] | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /** Redis Cache Adapter for using Redis as a cache-provider. */ |  | ||||||
| export class RedisCacheAdapter implements ICacheAdapter { |  | ||||||
|   _redis: Promise<Redis> |  | ||||||
|   redis?: Redis |  | ||||||
|   ready: boolean = false |  | ||||||
|   readonly _expireIntervalTimer: number = 5000 |  | ||||||
|   private _expireInterval?: number |  | ||||||
| 
 |  | ||||||
|   constructor(options: RedisConnectOptions) { |  | ||||||
|     this._redis = connect(options) |  | ||||||
|     this._redis.then( |  | ||||||
|       (redis) => { |  | ||||||
|         this.redis = redis |  | ||||||
|         this.ready = true |  | ||||||
|         this._startExpireInterval() |  | ||||||
|       }, |  | ||||||
|       () => { |  | ||||||
|         // TODO: Make error for this
 |  | ||||||
|       } |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private _startExpireInterval(): void { |  | ||||||
|     this._expireInterval = setInterval(() => { |  | ||||||
|       this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { |  | ||||||
|         for (const name of names) { |  | ||||||
|           this.redis?.hvals(name).then((vals) => { |  | ||||||
|             for (const val of vals) { |  | ||||||
|               const expireVal: { |  | ||||||
|                 name: string |  | ||||||
|                 key: string |  | ||||||
|                 at: number |  | ||||||
|               } = JSON.parse(val) |  | ||||||
|               const expired = new Date().getTime() > expireVal.at |  | ||||||
|               if (expired) this.redis?.hdel(expireVal.name, expireVal.key) |  | ||||||
|             } |  | ||||||
|           }) |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     }, this._expireIntervalTimer) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async _checkReady(): Promise<void> { |  | ||||||
|     if (!this.ready) await this._redis |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async get(cacheName: string, key: string): Promise<string | undefined> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const cache = await this.redis?.hget(cacheName, key) |  | ||||||
|     if (cache === undefined) return |  | ||||||
|     try { |  | ||||||
|       return JSON.parse(cache) |  | ||||||
|     } catch (e) { |  | ||||||
|       return cache |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async set( |  | ||||||
|     cacheName: string, |  | ||||||
|     key: string, |  | ||||||
|     value: any, |  | ||||||
|     expire?: number |  | ||||||
|   ): Promise<number | undefined> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const result = await this.redis?.hset( |  | ||||||
|       cacheName, |  | ||||||
|       key, |  | ||||||
|       typeof value === 'object' ? JSON.stringify(value) : value |  | ||||||
|     ) |  | ||||||
|     if (expire !== undefined) { |  | ||||||
|       await this.redis?.hset( |  | ||||||
|         `${cacheName}:expires`, |  | ||||||
|         key, |  | ||||||
|         JSON.stringify({ |  | ||||||
|           name: cacheName, |  | ||||||
|           key, |  | ||||||
|           at: new Date().getTime() + expire |  | ||||||
|         }) |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|     return result |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async delete(cacheName: string, key: string): Promise<boolean> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const exists = await this.redis?.hexists(cacheName, key) |  | ||||||
|     if (exists === 0) return false |  | ||||||
|     await this.redis?.hdel(cacheName, key) |  | ||||||
|     return true |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async array(cacheName: string): Promise<any[] | undefined> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const data = await this.redis?.hvals(cacheName) |  | ||||||
|     return data?.map((e: string) => JSON.parse(e)) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async deleteCache(cacheName: string): Promise<boolean> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     return (await this.redis?.del(cacheName)) !== 0 |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -13,7 +13,6 @@ import { ActivityGame, ClientActivity } from '../types/presence.ts' | ||||||
| import { Extension } from './extensions.ts' | import { Extension } from './extensions.ts' | ||||||
| import { SlashClient } from './slashClient.ts' | import { SlashClient } from './slashClient.ts' | ||||||
| import { Interaction } from '../structures/slash.ts' | import { Interaction } from '../structures/slash.ts' | ||||||
| import { SlashModule } from './slashModule.ts' |  | ||||||
| import { ShardManager } from './shard.ts' | import { ShardManager } from './shard.ts' | ||||||
| import { Application } from '../structures/application.ts' | import { Application } from '../structures/application.ts' | ||||||
| import { Invite } from '../structures/invite.ts' | import { Invite } from '../structures/invite.ts' | ||||||
|  | @ -190,10 +189,10 @@ export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
|     this.clientProperties = |     this.clientProperties = | ||||||
|       options.clientProperties === undefined |       options.clientProperties === undefined | ||||||
|         ? { |         ? { | ||||||
|             os: Deno.build.os, |           os: Deno.build.os, | ||||||
|             browser: 'harmony', |           browser: 'harmony', | ||||||
|             device: 'harmony' |           device: 'harmony' | ||||||
|           } |         } | ||||||
|         : options.clientProperties |         : options.clientProperties | ||||||
| 
 | 
 | ||||||
|     if (options.shard !== undefined) this.shard = options.shard |     if (options.shard !== undefined) this.shard = options.shard | ||||||
|  | @ -208,7 +207,7 @@ export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
|           this.token = token |           this.token = token | ||||||
|           this.debug('Info', 'Found token in ENV') |           this.debug('Info', 'Found token in ENV') | ||||||
|         } |         } | ||||||
|       } catch (e) {} |       } catch (e) { } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const restOptions: RESTOptions = { |     const restOptions: RESTOptions = { | ||||||
|  | @ -436,59 +435,3 @@ export function event(name?: keyof ClientEvents) { | ||||||
|     client._decoratedEvents[key] = listener |     client._decoratedEvents[key] = listener | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /** Decorator to create a Slash Command handler */ |  | ||||||
| export function slash(name?: string, guild?: string) { |  | ||||||
|   return function (client: Client | SlashClient | SlashModule, prop: string) { |  | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |  | ||||||
|     const item = (client as { [name: string]: any })[prop] |  | ||||||
|     if (typeof item !== 'function') { |  | ||||||
|       throw new Error('@slash decorator requires a function') |  | ||||||
|     } else |  | ||||||
|       client._decoratedSlash.push({ |  | ||||||
|         name: name ?? prop, |  | ||||||
|         guild, |  | ||||||
|         handler: item |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** Decorator to create a Sub-Slash Command handler */ |  | ||||||
| export function subslash(parent: string, name?: string, guild?: string) { |  | ||||||
|   return function (client: Client | SlashModule | SlashClient, prop: string) { |  | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |  | ||||||
|     const item = (client as { [name: string]: any })[prop] |  | ||||||
|     if (typeof item !== 'function') { |  | ||||||
|       throw new Error('@subslash decorator requires a function') |  | ||||||
|     } else |  | ||||||
|       client._decoratedSlash.push({ |  | ||||||
|         parent, |  | ||||||
|         name: name ?? prop, |  | ||||||
|         guild, |  | ||||||
|         handler: item |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** Decorator to create a Grouped Slash Command handler */ |  | ||||||
| export function groupslash( |  | ||||||
|   parent: string, |  | ||||||
|   group: string, |  | ||||||
|   name?: string, |  | ||||||
|   guild?: string |  | ||||||
| ) { |  | ||||||
|   return function (client: Client | SlashModule | SlashClient, prop: string) { |  | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |  | ||||||
|     const item = (client as { [name: string]: any })[prop] |  | ||||||
|     if (typeof item !== 'function') { |  | ||||||
|       throw new Error('@groupslash decorator requires a function') |  | ||||||
|     } else |  | ||||||
|       client._decoratedSlash.push({ |  | ||||||
|         group, |  | ||||||
|         parent, |  | ||||||
|         name: name ?? prop, |  | ||||||
|         guild, |  | ||||||
|         handler: item |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										105
									
								
								src/models/redisCache.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/models/redisCache.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | ||||||
|  | import { ICacheAdapter } from './cacheAdapter.ts' | ||||||
|  | import { connect, Redis, RedisConnectOptions } from 'https://deno.land/x/redis@v0.14.1/mod.ts' | ||||||
|  | 
 | ||||||
|  | /** Redis Cache Adapter for using Redis as a cache-provider. */ | ||||||
|  | export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|  |   _redis: Promise<Redis> | ||||||
|  |   redis?: Redis | ||||||
|  |   ready: boolean = false | ||||||
|  |   readonly _expireIntervalTimer: number = 5000 | ||||||
|  |   private _expireInterval?: number | ||||||
|  | 
 | ||||||
|  |   constructor(options: RedisConnectOptions) { | ||||||
|  |     this._redis = connect(options) | ||||||
|  |     this._redis.then( | ||||||
|  |       (redis) => { | ||||||
|  |         this.redis = redis | ||||||
|  |         this.ready = true | ||||||
|  |         this._startExpireInterval() | ||||||
|  |       }, | ||||||
|  |       () => { | ||||||
|  |         // TODO: Make error for this
 | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private _startExpireInterval(): void { | ||||||
|  |     this._expireInterval = setInterval(() => { | ||||||
|  |       this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { | ||||||
|  |         for (const name of names) { | ||||||
|  |           this.redis?.hvals(name).then((vals) => { | ||||||
|  |             for (const val of vals) { | ||||||
|  |               const expireVal: { | ||||||
|  |                 name: string | ||||||
|  |                 key: string | ||||||
|  |                 at: number | ||||||
|  |               } = JSON.parse(val) | ||||||
|  |               const expired = new Date().getTime() > expireVal.at | ||||||
|  |               if (expired) this.redis?.hdel(expireVal.name, expireVal.key) | ||||||
|  |             } | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, this._expireIntervalTimer) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async _checkReady(): Promise<void> { | ||||||
|  |     if (!this.ready) await this._redis | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async get(cacheName: string, key: string): Promise<string | undefined> { | ||||||
|  |     await this._checkReady() | ||||||
|  |     const cache = await this.redis?.hget(cacheName, key) | ||||||
|  |     if (cache === undefined) return | ||||||
|  |     try { | ||||||
|  |       return JSON.parse(cache) | ||||||
|  |     } catch (e) { | ||||||
|  |       return cache | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async set( | ||||||
|  |     cacheName: string, | ||||||
|  |     key: string, | ||||||
|  |     value: any, | ||||||
|  |     expire?: number | ||||||
|  |   ): Promise<number | undefined> { | ||||||
|  |     await this._checkReady() | ||||||
|  |     const result = await this.redis?.hset( | ||||||
|  |       cacheName, | ||||||
|  |       key, | ||||||
|  |       typeof value === 'object' ? JSON.stringify(value) : value | ||||||
|  |     ) | ||||||
|  |     if (expire !== undefined) { | ||||||
|  |       await this.redis?.hset( | ||||||
|  |         `${cacheName}:expires`, | ||||||
|  |         key, | ||||||
|  |         JSON.stringify({ | ||||||
|  |           name: cacheName, | ||||||
|  |           key, | ||||||
|  |           at: new Date().getTime() + expire | ||||||
|  |         }) | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |     return result | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async delete(cacheName: string, key: string): Promise<boolean> { | ||||||
|  |     await this._checkReady() | ||||||
|  |     const exists = await this.redis?.hexists(cacheName, key) | ||||||
|  |     if (exists === 0) return false | ||||||
|  |     await this.redis?.hdel(cacheName, key) | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async array(cacheName: string): Promise<any[] | undefined> { | ||||||
|  |     await this._checkReady() | ||||||
|  |     const data = await this.redis?.hvals(cacheName) | ||||||
|  |     return data?.map((e: string) => JSON.parse(e)) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async deleteCache(cacheName: string): Promise<boolean> { | ||||||
|  |     await this._checkReady() | ||||||
|  |     return (await this.redis?.del(cacheName)) !== 0 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -79,8 +79,6 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> { | ||||||
|     const shardCount = await this.getShardCount() |     const shardCount = await this.getShardCount() | ||||||
| 
 | 
 | ||||||
|     const gw = new Gateway(this.client, [Number(id), shardCount]) |     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) |     this.list.set(id.toString(), gw) | ||||||
| 
 | 
 | ||||||
|     gw.initWebsocket() |     gw.initWebsocket() | ||||||
|  |  | ||||||
|  | @ -1,6 +1,11 @@ | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Interaction } from '../structures/slash.ts' |  | ||||||
| import { | import { | ||||||
|  |   Interaction, | ||||||
|  |   InteractionApplicationCommandResolved | ||||||
|  | } from '../structures/slash.ts' | ||||||
|  | import { | ||||||
|  |   InteractionPayload, | ||||||
|  |   InteractionResponsePayload, | ||||||
|   InteractionType, |   InteractionType, | ||||||
|   SlashCommandChoice, |   SlashCommandChoice, | ||||||
|   SlashCommandOption, |   SlashCommandOption, | ||||||
|  | @ -9,11 +14,13 @@ import { | ||||||
|   SlashCommandPayload |   SlashCommandPayload | ||||||
| } from '../types/slash.ts' | } from '../types/slash.ts' | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { Client } from './client.ts' | import type { Client } from './client.ts' | ||||||
| import { RESTManager } from './rest.ts' | import { RESTManager } from './rest.ts' | ||||||
| import { SlashModule } from './slashModule.ts' | import { SlashModule } from './slashModule.ts' | ||||||
| import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' | import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' | ||||||
| import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' | import { User } from '../structures/user.ts' | ||||||
|  | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
|  | import { encodeText, decodeText } from '../utils/encoding.ts' | ||||||
| 
 | 
 | ||||||
| export class SlashCommand { | export class SlashCommand { | ||||||
|   slash: SlashCommandsManager |   slash: SlashCommandsManager | ||||||
|  | @ -155,6 +162,7 @@ function buildOptionsArray( | ||||||
|       ) |       ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Slash Command Builder */ | ||||||
| export class SlashBuilder { | export class SlashBuilder { | ||||||
|   data: SlashCommandPartial |   data: SlashCommandPartial | ||||||
| 
 | 
 | ||||||
|  | @ -200,6 +208,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 +360,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 +369,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,7 +379,15 @@ export interface SlashOptions { | ||||||
|   publicKey?: string |   publicKey?: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class SlashClient { | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||||
|  | export type SlashClientEvents = { | ||||||
|  |   interaction: [Interaction] | ||||||
|  |   interactionError: [Error] | ||||||
|  |   ping: [] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Slash Client represents an Interactions Client which can be used without Harmony Client. */ | ||||||
|  | export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | ||||||
|   id: string | (() => string) |   id: string | (() => string) | ||||||
|   client?: Client |   client?: Client | ||||||
|   token?: string |   token?: string | ||||||
|  | @ -389,6 +407,7 @@ export class SlashClient { | ||||||
|   }> |   }> | ||||||
| 
 | 
 | ||||||
|   constructor(options: SlashOptions) { |   constructor(options: SlashOptions) { | ||||||
|  |     super() | ||||||
|     let id = options.id |     let id = options.id | ||||||
|     if (options.token !== undefined) id = atob(options.token?.split('.')[0]) |     if (options.token !== undefined) id = atob(options.token?.split('.')[0]) | ||||||
|     if (id === undefined) |     if (id === undefined) | ||||||
|  | @ -423,8 +442,9 @@ export class SlashClient { | ||||||
|           : options.rest |           : options.rest | ||||||
|         : options.client.rest |         : options.client.rest | ||||||
| 
 | 
 | ||||||
|     this.client?.on('interactionCreate', (interaction) => |     this.client?.on( | ||||||
|       this._process(interaction) |       'interactionCreate', | ||||||
|  |       async (interaction) => await this._process(interaction) | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     this.commands = new SlashCommandsManager(this) |     this.commands = new SlashCommandsManager(this) | ||||||
|  | @ -469,12 +489,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 +513,15 @@ export class SlashClient { | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Process an incoming Slash Command (interaction) */ |   /** Process an incoming Interaction */ | ||||||
|   private _process(interaction: Interaction): void { |   private async _process(interaction: Interaction): Promise<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) | ||||||
|  | @ -499,28 +531,113 @@ export class SlashClient { | ||||||
| 
 | 
 | ||||||
|     if (cmd === undefined) return |     if (cmd === undefined) return | ||||||
| 
 | 
 | ||||||
|     cmd.handler(interaction) |     await this.emit('interaction', interaction) | ||||||
|  |     try { | ||||||
|  |       await cmd.handler(interaction) | ||||||
|  |     } catch (e) { | ||||||
|  |       await this.emit('interactionError', e) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Verify HTTP based Interaction */ | ||||||
|   async verifyKey( |   async verifyKey( | ||||||
|     rawBody: string | Uint8Array | Buffer, |     rawBody: string | Uint8Array, | ||||||
|     signature: string, |     signature: string | Uint8Array, | ||||||
|     timestamp: string |     timestamp: string | Uint8Array | ||||||
|   ): Promise<boolean> { |   ): Promise<boolean> { | ||||||
|     if (this.publicKey === undefined) |     if (this.publicKey === undefined) | ||||||
|       throw new Error('Public Key is not present') |       throw new Error('Public Key is not present') | ||||||
|     return edverify( | 
 | ||||||
|       signature, |     const fullBody = new Uint8Array([ | ||||||
|       Buffer.concat([ |       ...(typeof timestamp === 'string' ? encodeText(timestamp) : timestamp), | ||||||
|         Buffer.from(timestamp, 'utf-8'), |       ...(typeof rawBody === 'string' ? encodeText(rawBody) : rawBody) | ||||||
|         Buffer.from( |     ]) | ||||||
|           rawBody instanceof Uint8Array | 
 | ||||||
|             ? new TextDecoder().decode(rawBody) |     return edverify(signature, fullBody, this.publicKey).catch(() => false) | ||||||
|             : rawBody |   } | ||||||
|  | 
 | ||||||
|  |   /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction. **Data present in Interaction returned by this method is very different from actual typings as there is no real `Client` behind the scenes to cache things.** */ | ||||||
|  |   async verifyServerRequest(req: { | ||||||
|  |     headers: Headers | ||||||
|  |     method: string | ||||||
|  |     body: Deno.Reader | Uint8Array | ||||||
|  |     respond: (options: { | ||||||
|  |       status?: number | ||||||
|  |       headers?: Headers | ||||||
|  |       body?: string | Uint8Array | FormData | ||||||
|  |     }) => Promise<void> | ||||||
|  |   }): Promise<false | Interaction> { | ||||||
|  |     if (req.method.toLowerCase() !== 'post') return false | ||||||
|  | 
 | ||||||
|  |     const signature = req.headers.get('x-signature-ed25519') | ||||||
|  |     const timestamp = req.headers.get('x-signature-timestamp') | ||||||
|  |     if (signature === null || timestamp === null) return false | ||||||
|  | 
 | ||||||
|  |     const rawbody = | ||||||
|  |       req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body) | ||||||
|  |     const verify = await this.verifyKey(rawbody, signature, timestamp) | ||||||
|  |     if (!verify) return false | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       const payload: InteractionPayload = JSON.parse(decodeText(rawbody)) | ||||||
|  | 
 | ||||||
|  |       // TODO: Maybe fix all this hackery going on here?
 | ||||||
|  |       const res = new Interaction(this as any, payload, { | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||||
|  |         user: new User(this as any, (payload.member?.user ?? payload.user)!), | ||||||
|  |         member: payload.member as any, | ||||||
|  |         guild: payload.guild_id as any, | ||||||
|  |         channel: payload.channel_id as any, | ||||||
|  |         resolved: ((payload.data | ||||||
|  |           ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { | ||||||
|  |           users: {}, | ||||||
|  |           members: {}, | ||||||
|  |           roles: {}, | ||||||
|  |           channels: {} | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       res._httpRespond = async (d: InteractionResponsePayload | FormData) => | ||||||
|  |         await req.respond({ | ||||||
|  |           status: 200, | ||||||
|  |           headers: new Headers({ | ||||||
|  |             'content-type': | ||||||
|  |               d instanceof FormData ? 'multipart/form-data' : 'application/json' | ||||||
|  |           }), | ||||||
|  |           body: d instanceof FormData ? d : JSON.stringify(d) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |       return res | ||||||
|  |     } catch (e) { | ||||||
|  |       return false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */ | ||||||
|  |   async verifyFetchEvent({ | ||||||
|  |     request: req, | ||||||
|  |     respondWith | ||||||
|  |   }: { | ||||||
|  |     respondWith: CallableFunction | ||||||
|  |     request: Request | ||||||
|  |   }): Promise<false | Interaction> { | ||||||
|  |     if (req.bodyUsed === true) throw new Error('Request Body already used') | ||||||
|  |     if (req.body === null) return false | ||||||
|  |     const body = (await req.body.getReader().read()).value | ||||||
|  |     if (body === undefined) return false | ||||||
|  | 
 | ||||||
|  |     return await this.verifyServerRequest({ | ||||||
|  |       headers: req.headers, | ||||||
|  |       body, | ||||||
|  |       method: req.method, | ||||||
|  |       respond: async (options) => { | ||||||
|  |         await respondWith( | ||||||
|  |           new Response(options.body, { | ||||||
|  |             headers: options.headers, | ||||||
|  |             status: options.status | ||||||
|  |           }) | ||||||
|         ) |         ) | ||||||
|       ]), |       } | ||||||
|       this.publicKey |     }) | ||||||
|     ).catch(() => false) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async verifyOpineRequest(req: any): Promise<boolean> { |   async verifyOpineRequest(req: any): Promise<boolean> { | ||||||
|  | @ -576,3 +693,59 @@ export class SlashClient { | ||||||
|     return true |     return true | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** Decorator to create a Slash Command handler */ | ||||||
|  | export function slash(name?: string, guild?: string) { | ||||||
|  |   return function (client: Client | SlashClient | SlashModule, prop: string) { | ||||||
|  |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|  |     const item = (client as { [name: string]: any })[prop] | ||||||
|  |     if (typeof item !== 'function') { | ||||||
|  |       throw new Error('@slash decorator requires a function') | ||||||
|  |     } else | ||||||
|  |       client._decoratedSlash.push({ | ||||||
|  |         name: name ?? prop, | ||||||
|  |         guild, | ||||||
|  |         handler: item | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Decorator to create a Sub-Slash Command handler */ | ||||||
|  | export function subslash(parent: string, name?: string, guild?: string) { | ||||||
|  |   return function (client: Client | SlashModule | SlashClient, prop: string) { | ||||||
|  |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|  |     const item = (client as { [name: string]: any })[prop] | ||||||
|  |     if (typeof item !== 'function') { | ||||||
|  |       throw new Error('@subslash decorator requires a function') | ||||||
|  |     } else | ||||||
|  |       client._decoratedSlash.push({ | ||||||
|  |         parent, | ||||||
|  |         name: name ?? prop, | ||||||
|  |         guild, | ||||||
|  |         handler: item | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Decorator to create a Grouped Slash Command handler */ | ||||||
|  | export function groupslash( | ||||||
|  |   parent: string, | ||||||
|  |   group: string, | ||||||
|  |   name?: string, | ||||||
|  |   guild?: string | ||||||
|  | ) { | ||||||
|  |   return function (client: Client | SlashModule | SlashClient, prop: string) { | ||||||
|  |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|  |     const item = (client as { [name: string]: any })[prop] | ||||||
|  |     if (typeof item !== 'function') { | ||||||
|  |       throw new Error('@groupslash decorator requires a function') | ||||||
|  |     } else | ||||||
|  |       client._decoratedSlash.push({ | ||||||
|  |         group, | ||||||
|  |         parent, | ||||||
|  |         name: name ?? prop, | ||||||
|  |         guild, | ||||||
|  |         handler: item | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ import { Client } from '../models/client.ts' | ||||||
| import { Snowflake } from '../utils/snowflake.ts' | import { Snowflake } from '../utils/snowflake.ts' | ||||||
| 
 | 
 | ||||||
| export class Base { | export class Base { | ||||||
|   client: Client |   client!: Client | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, _data?: any) { |   constructor(client: Client, _data?: any) { | ||||||
|     this.client = client |     Object.defineProperty(this, 'client', { value: client, enumerable: false }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { | ||||||
|   Attachment, |   Attachment, | ||||||
|   MessageActivity, |   MessageActivity, | ||||||
|   MessageApplication, |   MessageApplication, | ||||||
|  |   MessageInteractionPayload, | ||||||
|   MessageOptions, |   MessageOptions, | ||||||
|   MessagePayload, |   MessagePayload, | ||||||
|   MessageReference |   MessageReference | ||||||
|  | @ -19,9 +20,26 @@ import { Guild } from './guild.ts' | ||||||
| import { MessageReactionsManager } from '../managers/messageReactions.ts' | import { MessageReactionsManager } from '../managers/messageReactions.ts' | ||||||
| import { MessageSticker } from './messageSticker.ts' | import { MessageSticker } from './messageSticker.ts' | ||||||
| import { Emoji } from './emoji.ts' | import { Emoji } from './emoji.ts' | ||||||
|  | import { InteractionType } from '../types/slash.ts' | ||||||
|  | import { encodeText } from '../utils/encoding.ts' | ||||||
| 
 | 
 | ||||||
| type AllMessageOptions = MessageOptions | Embed | type AllMessageOptions = MessageOptions | Embed | ||||||
| 
 | 
 | ||||||
|  | export class MessageInteraction extends SnowflakeBase { | ||||||
|  |   id: string | ||||||
|  |   name: string | ||||||
|  |   type: InteractionType | ||||||
|  |   user: User | ||||||
|  | 
 | ||||||
|  |   constructor(client: Client, data: MessageInteractionPayload) { | ||||||
|  |     super(client) | ||||||
|  |     this.id = data.id | ||||||
|  |     this.name = data.name | ||||||
|  |     this.type = data.type | ||||||
|  |     this.user = new User(this.client, data.user) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class Message extends SnowflakeBase { | export class Message extends SnowflakeBase { | ||||||
|   id: string |   id: string | ||||||
|   channelID: string |   channelID: string | ||||||
|  | @ -46,6 +64,7 @@ export class Message extends SnowflakeBase { | ||||||
|   messageReference?: MessageReference |   messageReference?: MessageReference | ||||||
|   flags?: number |   flags?: number | ||||||
|   stickers?: MessageSticker[] |   stickers?: MessageSticker[] | ||||||
|  |   interaction?: MessageInteraction | ||||||
| 
 | 
 | ||||||
|   get createdAt(): Date { |   get createdAt(): Date { | ||||||
|     return new Date(this.timestamp) |     return new Date(this.timestamp) | ||||||
|  | @ -87,6 +106,10 @@ export class Message extends SnowflakeBase { | ||||||
|             (payload) => new MessageSticker(this.client, payload) |             (payload) => new MessageSticker(this.client, payload) | ||||||
|           ) |           ) | ||||||
|         : undefined |         : undefined | ||||||
|  |     this.interaction = | ||||||
|  |       data.interaction === undefined | ||||||
|  |         ? undefined | ||||||
|  |         : new MessageInteraction(this.client, data.interaction) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   readFromData(data: MessagePayload): void { |   readFromData(data: MessagePayload): void { | ||||||
|  | @ -195,8 +218,6 @@ export class Message extends SnowflakeBase { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const encoder = new TextEncoder() |  | ||||||
| 
 |  | ||||||
| /** Message Attachment that can be sent while Creating Message */ | /** Message Attachment that can be sent while Creating Message */ | ||||||
| export class MessageAttachment { | export class MessageAttachment { | ||||||
|   name: string |   name: string | ||||||
|  | @ -206,7 +227,7 @@ export class MessageAttachment { | ||||||
|     this.name = name |     this.name = name | ||||||
|     this.blob = |     this.blob = | ||||||
|       typeof blob === 'string' |       typeof blob === 'string' | ||||||
|         ? new Blob([encoder.encode(blob)]) |         ? new Blob([encodeText(blob)]) | ||||||
|         : blob instanceof Uint8Array |         : blob instanceof Uint8Array | ||||||
|         ? new Blob([blob]) |         ? new Blob([blob]) | ||||||
|         : blob |         : blob | ||||||
|  |  | ||||||
|  | @ -1,95 +1,178 @@ | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../models/client.ts' | ||||||
| import { MessageOptions } from '../types/channel.ts' | import { | ||||||
|  |   AllowedMentionsPayload, | ||||||
|  |   ChannelTypes, | ||||||
|  |   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, | ||||||
|  |   InteractionChannelPayload, | ||||||
|   InteractionPayload, |   InteractionPayload, | ||||||
|  |   InteractionResponseFlags, | ||||||
|   InteractionResponsePayload, |   InteractionResponsePayload, | ||||||
|   InteractionResponseType |   InteractionResponseType, | ||||||
|  |   InteractionType, | ||||||
|  |   SlashCommandOptionType | ||||||
| } from '../types/slash.ts' | } from '../types/slash.ts' | ||||||
|  | import { Dict } from '../utils/dict.ts' | ||||||
|  | import { Permissions } from '../utils/permissions.ts' | ||||||
| import { SnowflakeBase } from './base.ts' | import { SnowflakeBase } from './base.ts' | ||||||
|  | import { Channel } from './channel.ts' | ||||||
| import { Embed } from './embed.ts' | import { Embed } from './embed.ts' | ||||||
| import { Guild } from './guild.ts' | import { Guild } from './guild.ts' | ||||||
|  | import { GuildTextChannel } from './guildTextChannel.ts' | ||||||
| import { Member } from './member.ts' | import { Member } from './member.ts' | ||||||
| import { Message } from './message.ts' | import { Message } from './message.ts' | ||||||
|  | import { Role } from './role.ts' | ||||||
| import { TextChannel } from './textChannel.ts' | import { TextChannel } from './textChannel.ts' | ||||||
| import { GuildTextBasedChannel } from './guildTextChannel.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?: Array<Embed | EmbedPayload> | ||||||
|   name?: string |   name?: string | ||||||
|   avatar?: string |   avatar?: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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?: Array<Embed | EmbedPayload> | ||||||
|   tts?: boolean |   tts?: boolean | ||||||
|   flags?: number |   flags?: number | InteractionResponseFlags[] | ||||||
|   temp?: boolean |   allowedMentions?: AllowedMentionsPayload | ||||||
|   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 interface InteractionResponse extends InteractionMessageOptions { | ||||||
|  |   /** Type of Interaction Response */ | ||||||
|  |   type?: InteractionResponseType | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Represents a Channel Object for an Option in Slash Command */ | ||||||
|  | export class InteractionChannel extends SnowflakeBase { | ||||||
|  |   /** Name of the Channel */ | ||||||
|  |   name: string | ||||||
|  |   /** Channel Type */ | ||||||
|  |   type: ChannelTypes | ||||||
|  |   permissions: Permissions | ||||||
|  | 
 | ||||||
|  |   constructor(client: Client, data: InteractionChannelPayload) { | ||||||
|  |     super(client) | ||||||
|  |     this.id = data.id | ||||||
|  |     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<T = Channel>(): Promise<T | undefined> { | ||||||
|  |     return this.client.channels.get<T>(this.id) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface InteractionApplicationCommandResolved { | ||||||
|  |   users: Dict<InteractionUser> | ||||||
|  |   members: Dict<Member> | ||||||
|  |   channels: Dict<InteractionChannel> | ||||||
|  |   roles: Dict<Role> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class InteractionUser extends User { | ||||||
|  |   member?: Member | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class Interaction extends SnowflakeBase { | export class Interaction extends SnowflakeBase { | ||||||
|   client: Client |   /** Type of Interaction */ | ||||||
|   type: number |   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 */ | ||||||
|   channel: GuildTextBasedChannel |   data?: InteractionApplicationCommandData | ||||||
|   guild: Guild |   /** Channel in which Interaction was initiated */ | ||||||
|   member: Member |   channel?: TextChannel | GuildTextChannel | ||||||
|   _savedHook?: Webhook |   /** Guild in which Interaction was initiated */ | ||||||
|  |   guild?: Guild | ||||||
|  |   /** Member object of who initiated the Interaction */ | ||||||
|  |   member?: Member | ||||||
|  |   /** User object of who invoked Interaction */ | ||||||
|  |   user: User | ||||||
|  |   /** Whether we have responded to Interaction or not */ | ||||||
|  |   responded: boolean = false | ||||||
|  |   /** Resolved data for Snowflakes in Slash Command Arguments */ | ||||||
|  |   resolved: InteractionApplicationCommandResolved | ||||||
|  |   /** Whether response was deferred or not */ | ||||||
|  |   deferred: boolean = false | ||||||
|  |   _httpRespond?: (d: InteractionResponsePayload) => unknown | ||||||
|  |   _httpResponded?: boolean | ||||||
|  |   applicationID: string | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     client: Client, |     client: Client, | ||||||
|     data: InteractionPayload, |     data: InteractionPayload, | ||||||
|     others: { |     others: { | ||||||
|       channel: GuildTextBasedChannel |       channel?: TextChannel | GuildTextChannel | ||||||
|       guild: Guild |       guild?: Guild | ||||||
|       member: Member |       member?: Member | ||||||
|  |       user: User | ||||||
|  |       resolved: InteractionApplicationCommandResolved | ||||||
|     } |     } | ||||||
|   ) { |   ) { | ||||||
|     super(client) |     super(client) | ||||||
|     this.client = client |  | ||||||
|     this.type = data.type |     this.type = data.type | ||||||
|     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.applicationID = data.application_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 | ||||||
|  |     this.resolved = others.resolved | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   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[] { |   /** Get an option by name */ | ||||||
|     return this.data.options ?? [] |   option<T>(name: string): T { | ||||||
|   } |     const op = this.options.find((e) => e.name === name) | ||||||
| 
 |     if (op === undefined || op.value === undefined) return undefined as any | ||||||
|   option<T = any>(name: string): T { |     if (op.type === SlashCommandOptionType.USER) { | ||||||
|     return this.options.find((e) => e.name === name)?.value |       const u: InteractionUser = this.resolved.users[op.value] as any | ||||||
|  |       if (this.resolved.members[op.value] !== undefined) | ||||||
|  |         u.member = this.resolved.members[op.value] | ||||||
|  |       return u 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 */ |   /** 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) 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 ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, | ||||||
|       data: |       data: | ||||||
|  | @ -100,16 +183,70 @@ 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 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.client.rest.post( |     if (this._httpRespond !== undefined && this._httpResponded !== true) { | ||||||
|       INTERACTION_CALLBACK(this.id, this.token), |       this._httpResponded = true | ||||||
|       payload |       await this._httpRespond(payload) | ||||||
|  |     } else | ||||||
|  |       await this.client.rest.post( | ||||||
|  |         INTERACTION_CALLBACK(this.id, this.token), | ||||||
|  |         payload | ||||||
|  |       ) | ||||||
|  |     this.responded = true | ||||||
|  | 
 | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Defer the Interaction i.e. let the user know bot is processing and will respond later. You only have 15 minutes to edit the response! */ | ||||||
|  |   async defer(ephemeral = false): Promise<Interaction> { | ||||||
|  |     await this.respond({ | ||||||
|  |       type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE, | ||||||
|  |       flags: ephemeral ? 1 << 6 : 0 | ||||||
|  |     }) | ||||||
|  |     this.deferred = true | ||||||
|  |     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 }) | ||||||
|  | 
 | ||||||
|  |     if (this.deferred && this.responded) { | ||||||
|  |       await this.editResponse({ | ||||||
|  |         content: options.content, | ||||||
|  |         embeds: options.embeds, | ||||||
|  |         flags: options.flags, | ||||||
|  |         allowedMentions: options.allowedMentions | ||||||
|  |       }) | ||||||
|  |     } else | ||||||
|  |       await this.respond( | ||||||
|  |         Object.assign(options, { | ||||||
|  |           type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE | ||||||
|  |         }) | ||||||
|  |       ) | ||||||
| 
 | 
 | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
|  | @ -117,33 +254,32 @@ export class Interaction extends SnowflakeBase { | ||||||
|   /** Edit the original Interaction response */ |   /** Edit the original Interaction response */ | ||||||
|   async editResponse(data: { |   async editResponse(data: { | ||||||
|     content?: string |     content?: string | ||||||
|     embeds?: Embed[] |     embeds?: Array<Embed | EmbedPayload> | ||||||
|  |     flags?: number | number[] | ||||||
|  |     allowedMentions?: AllowedMentionsPayload | ||||||
|   }): Promise<Interaction> { |   }): Promise<Interaction> { | ||||||
|     const url = WEBHOOK_MESSAGE( |     const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') | ||||||
|       this.client.user?.id as string, |  | ||||||
|       this.token, |  | ||||||
|       '@original' |  | ||||||
|     ) |  | ||||||
|     await this.client.rest.patch(url, { |     await this.client.rest.patch(url, { | ||||||
|       content: data.content ?? '', |       content: data.content ?? '', | ||||||
|       embeds: data.embeds ?? [] |       embeds: data.embeds ?? [], | ||||||
|  |       flags: | ||||||
|  |         typeof data.flags === 'object' | ||||||
|  |           ? data.flags.reduce((p, a) => p | a, 0) | ||||||
|  |           : data.flags, | ||||||
|  |       allowed_mentions: data.allowedMentions | ||||||
|     }) |     }) | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Delete the original Interaction Response */ |   /** Delete the original Interaction Response */ | ||||||
|   async deleteResponse(): Promise<Interaction> { |   async deleteResponse(): Promise<Interaction> { | ||||||
|     const url = WEBHOOK_MESSAGE( |     const url = WEBHOOK_MESSAGE(this.applicationID, this.token, '@original') | ||||||
|       this.client.user?.id as string, |  | ||||||
|       this.token, |  | ||||||
|       '@original' |  | ||||||
|     ) |  | ||||||
|     await this.client.rest.delete(url) |     await this.client.rest.delete(url) | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get url(): string { |   get url(): string { | ||||||
|     return `https://discord.com/api/v8/webhooks/${this.client.user?.id}/${this.token}` |     return `https://discord.com/api/v8/webhooks/${this.applicationID}/${this.token}` | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Send a followup message */ |   /** Send a followup message */ | ||||||
|  | @ -174,6 +310,7 @@ export class Interaction extends SnowflakeBase { | ||||||
|           ? (option as WebhookMessageOptions).embeds |           ? (option as WebhookMessageOptions).embeds | ||||||
|           : undefined, |           : undefined, | ||||||
|       file: (option as WebhookMessageOptions)?.file, |       file: (option as WebhookMessageOptions)?.file, | ||||||
|  |       files: (option as WebhookMessageOptions)?.files, | ||||||
|       tts: (option as WebhookMessageOptions)?.tts, |       tts: (option as WebhookMessageOptions)?.tts, | ||||||
|       allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions |       allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions | ||||||
|     } |     } | ||||||
|  | @ -212,7 +349,7 @@ export class Interaction extends SnowflakeBase { | ||||||
|     msg: Message | string, |     msg: Message | string, | ||||||
|     data: { |     data: { | ||||||
|       content?: string |       content?: string | ||||||
|       embeds?: Embed[] |       embeds?: Array<Embed | EmbedPayload> | ||||||
|       file?: any |       file?: any | ||||||
|       allowed_mentions?: { |       allowed_mentions?: { | ||||||
|         parse?: string |         parse?: string | ||||||
|  | @ -224,7 +361,7 @@ export class Interaction extends SnowflakeBase { | ||||||
|   ): Promise<Interaction> { |   ): Promise<Interaction> { | ||||||
|     await this.client.rest.patch( |     await this.client.rest.patch( | ||||||
|       WEBHOOK_MESSAGE( |       WEBHOOK_MESSAGE( | ||||||
|         this.client.user?.id as string, |         this.applicationID, | ||||||
|         this.token ?? this.client.token, |         this.token ?? this.client.token, | ||||||
|         typeof msg === 'string' ? msg : msg.id |         typeof msg === 'string' ? msg : msg.id | ||||||
|       ), |       ), | ||||||
|  | @ -233,10 +370,11 @@ 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( | ||||||
|         this.client.user?.id as string, |         this.applicationID, | ||||||
|         this.token ?? this.client.token, |         this.token ?? this.client.token, | ||||||
|         typeof msg === 'string' ? msg : msg.id |         typeof msg === 'string' ? msg : msg.id | ||||||
|       ) |       ) | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								src/test/debug.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/test/debug.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | import { Client, event } from '../../mod.ts' | ||||||
|  | import { TOKEN } from './config.ts' | ||||||
|  | 
 | ||||||
|  | class MyClient extends Client { | ||||||
|  |   constructor() { | ||||||
|  |     super({ | ||||||
|  |       token: TOKEN, | ||||||
|  |       intents: [], | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @event() | ||||||
|  |   ready(): void { | ||||||
|  |     console.log('Connected!') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   debug(title: string, msg: string): void { | ||||||
|  |     console.log(`[${title}] ${msg}`) | ||||||
|  |     if (title === 'Gateway' && msg === 'Initializing WebSocket...') { | ||||||
|  |       try { throw new Error("Stack") } catch (e) { | ||||||
|  |         console.log(e.stack) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const client = new MyClient() | ||||||
|  | client.connect() | ||||||
|  | @ -119,16 +119,7 @@ client.on('messageCreate', async (msg: Message) => { | ||||||
|       msg.channel.send('Failed...') |       msg.channel.send('Failed...') | ||||||
|     } |     } | ||||||
|   } else if (msg.content === '!react') { |   } else if (msg.content === '!react') { | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 |     msg.addReaction('a:programming:785013658257195008') | ||||||
|     msg.addReaction('😂') |  | ||||||
|     msg.channel.send('x'.repeat(6969), { |  | ||||||
|       embed: new Embed() |  | ||||||
|         .setTitle('pepega'.repeat(6969)) |  | ||||||
|         .setDescription('pepega'.repeat(6969)) |  | ||||||
|         .addField('uwu', 'uwu'.repeat(6969)) |  | ||||||
|         .addField('uwu', 'uwu'.repeat(6969)) |  | ||||||
|         .setFooter('uwu'.repeat(6969)) |  | ||||||
|     }) |  | ||||||
|   } else if (msg.content === '!wait_for') { |   } else if (msg.content === '!wait_for') { | ||||||
|     msg.channel.send('Send anything!') |     msg.channel.send('Send anything!') | ||||||
|     const [receivedMsg] = await client.waitFor( |     const [receivedMsg] = await client.waitFor( | ||||||
|  | @ -211,13 +202,12 @@ client.on('messageCreate', async (msg: Message) => { | ||||||
|         ) |         ) | ||||||
|         .join('\n\n')}` |         .join('\n\n')}` | ||||||
|     ) |     ) | ||||||
|   } else if (msg.content === '!getPermissions') { |   } else if (msg.content === '!perms') { | ||||||
|     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
 |     if (msg.channel.type !== ChannelTypes.GUILD_TEXT) { | ||||||
|     if (!checkGuildTextBasedChannel(msg.channel)) { |  | ||||||
|       return msg.channel.send("This isn't a guild text channel!") |       return msg.channel.send("This isn't a guild text channel!") | ||||||
|     } |     } | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 |     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 | ||||||
|     const permissions = await (msg.channel as GuildTextChannel).permissionsFor( |     const permissions = await ((msg.channel as unknown) as GuildTextChannel).permissionsFor( | ||||||
|       // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 |       // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 | ||||||
|       msg.member as Member |       msg.member as Member | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								src/test/slash-http.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/test/slash-http.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | import { SlashClient } from '../../mod.ts' | ||||||
|  | import { SLASH_ID, SLASH_PUB_KEY, SLASH_TOKEN } from './config.ts' | ||||||
|  | import { listenAndServe } from 'https://deno.land/std@0.90.0/http/server.ts' | ||||||
|  | 
 | ||||||
|  | const slash = new SlashClient({ | ||||||
|  |   id: SLASH_ID, | ||||||
|  |   token: SLASH_TOKEN, | ||||||
|  |   publicKey: SLASH_PUB_KEY | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | await slash.commands.bulkEdit([ | ||||||
|  |   { | ||||||
|  |     name: 'ping', | ||||||
|  |     description: 'Just ping!' | ||||||
|  |   } | ||||||
|  | ]) | ||||||
|  | 
 | ||||||
|  | const options = { port: 8000 } | ||||||
|  | console.log('Listen on port: ' + options.port.toString()) | ||||||
|  | listenAndServe(options, async (req) => { | ||||||
|  |   const d = await slash.verifyServerRequest(req) | ||||||
|  |   if (d === false) return req.respond({ status: 401, body: 'not authorized' }) | ||||||
|  | 
 | ||||||
|  |   console.log(d) | ||||||
|  |   if (d.type === 1) return d.respond({ type: 1 }) | ||||||
|  |   d.reply('Pong!') | ||||||
|  | }) | ||||||
|  | @ -1,100 +1,56 @@ | ||||||
| import { Client, Intents, event, slash } from '../../mod.ts' | import { | ||||||
| import { Embed } from '../structures/embed.ts' |   Client, | ||||||
|  |   Intents, | ||||||
|  |   event, | ||||||
|  |   slash, | ||||||
|  |   SlashCommandOptionType as Type | ||||||
|  | } from '../../mod.ts' | ||||||
| import { Interaction } from '../structures/slash.ts' | import { Interaction } from '../structures/slash.ts' | ||||||
| import { TOKEN } from './config.ts' | import { TOKEN } from './config.ts' | ||||||
| 
 | 
 | ||||||
| export class MyClient extends Client { | export class MyClient extends Client { | ||||||
|   @event() |   @event() ready(): void { | ||||||
|   ready(): void { |  | ||||||
|     console.log(`Logged in as ${this.user?.tag}!`) |     console.log(`Logged in as ${this.user?.tag}!`) | ||||||
|     this.slash.commands.bulkEdit([{ name: 'send', description: 'idk' }]) |     this.slash.commands.bulkEdit( | ||||||
|   } |       [ | ||||||
| 
 |         { | ||||||
|   @event('debug') |           name: 'test', | ||||||
|   debugEvt(txt: string): void { |           description: 'Test command.', | ||||||
|     console.log(txt) |           options: [ | ||||||
|   } |             { | ||||||
| 
 |               name: 'user', | ||||||
|   @slash() |               type: Type.USER, | ||||||
|   send(d: Interaction): void { |               description: 'User' | ||||||
|     d.respond({ |             }, | ||||||
|       content: d.data.options?.find((e) => e.name === 'content')?.value |             { | ||||||
|     }) |               name: 'role', | ||||||
|   } |               type: Type.ROLE, | ||||||
| 
 |               description: 'Role' | ||||||
|   @slash() |             }, | ||||||
|   async eval(d: Interaction): Promise<void> { |             { | ||||||
|     if ( |               name: 'channel', | ||||||
|       d.user.id !== '422957901716652033' && |               type: Type.CHANNEL, | ||||||
|       d.user.id !== '682849186227552266' |               description: 'Channel' | ||||||
|     ) { |             }, | ||||||
|       d.respond({ |             { | ||||||
|         content: 'This command can only be used by owner!' |               name: 'string', | ||||||
|       }) |               type: Type.STRING, | ||||||
|     } else { |               description: 'String' | ||||||
|       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```' |       '807935370556866560' | ||||||
|         }).catch(() => {}) |     ) | ||||||
|       } catch (e) { |     this.slash.commands.bulkEdit([]) | ||||||
|         d.respond({ |  | ||||||
|           content: '```js\n' + `${e.stack}` + '\n```' |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @slash() |   @slash() test(d: Interaction): void { | ||||||
|   async hug(d: Interaction): Promise<void> { |     console.log(d.resolved) | ||||||
|     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() |   @event() raw(evt: string, d: any): void { | ||||||
|   async kiss(d: Interaction): Promise<void> { |     if (evt === 'INTERACTION_CREATE') console.log(evt, d?.data?.resolved) | ||||||
|     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!` |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import { Role } from '../structures/role.ts' | ||||||
| import { Permissions } from '../utils/permissions.ts' | import { Permissions } from '../utils/permissions.ts' | ||||||
| import { EmojiPayload } from './emoji.ts' | import { EmojiPayload } from './emoji.ts' | ||||||
| import { MemberPayload } from './guild.ts' | import { MemberPayload } from './guild.ts' | ||||||
|  | import { InteractionType } from './slash.ts' | ||||||
| import { UserPayload } from './user.ts' | import { UserPayload } from './user.ts' | ||||||
| 
 | 
 | ||||||
| export interface ChannelPayload { | export interface ChannelPayload { | ||||||
|  | @ -185,6 +186,7 @@ export interface MessagePayload { | ||||||
|   message_reference?: MessageReference |   message_reference?: MessageReference | ||||||
|   flags?: number |   flags?: number | ||||||
|   stickers?: MessageStickerPayload[] |   stickers?: MessageStickerPayload[] | ||||||
|  |   interaction?: MessageInteractionPayload | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum AllowedMentionType { | export enum AllowedMentionType { | ||||||
|  | @ -373,3 +375,10 @@ export interface MessageStickerPayload { | ||||||
|   preview_asset: string | null |   preview_asset: string | null | ||||||
|   format_type: MessageStickerFormatTypes |   format_type: MessageStickerFormatTypes | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export interface MessageInteractionPayload { | ||||||
|  |   id: string | ||||||
|  |   type: InteractionType | ||||||
|  |   name: string | ||||||
|  |   user: UserPayload | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,22 +1,47 @@ | ||||||
| import { EmbedPayload } from './channel.ts' | import { Dict } from '../utils/dict.ts' | ||||||
|  | import { | ||||||
|  |   AllowedMentionsPayload, | ||||||
|  |   ChannelTypes, | ||||||
|  |   EmbedPayload | ||||||
|  | } from './channel.ts' | ||||||
| import { MemberPayload } from './guild.ts' | import { MemberPayload } from './guild.ts' | ||||||
|  | import { RolePayload } from './role.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 InteractionChannelPayload { | ||||||
|  |   id: string | ||||||
|  |   name: string | ||||||
|  |   permissions: string | ||||||
|  |   type: ChannelTypes | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface InteractionApplicationCommandResolvedPayload { | ||||||
|  |   users?: Dict<UserPayload> | ||||||
|  |   members?: Dict<MemberPayload> | ||||||
|  |   channels?: Dict<InteractionChannelPayload> | ||||||
|  |   roles?: Dict<RolePayload> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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[] | ||||||
|  |   /** Resolved data for options in Slash Command */ | ||||||
|  |   resolved?: InteractionApplicationCommandResolvedPayload | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum InteractionType { | export enum InteractionType { | ||||||
|  | @ -26,27 +51,31 @@ export enum InteractionType { | ||||||
|   APPLICATION_COMMAND = 2 |   APPLICATION_COMMAND = 2 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface InteractionMemberPayload extends MemberPayload { | ||||||
|  |   /** Permissions of the Member who initiated Interaction (Guild-only) */ | ||||||
|  |   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. Undefined only when Interaction is not 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 | ||||||
|  |   application_id: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface SlashCommandChoice { | export interface SlashCommandChoice { | ||||||
|  | @ -57,7 +86,9 @@ export interface SlashCommandChoice { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum SlashCommandOptionType { | export enum SlashCommandOptionType { | ||||||
|  |   /** A sub command that is either a part of a root command or Sub Command Group */ | ||||||
|   SUB_COMMAND = 1, |   SUB_COMMAND = 1, | ||||||
|  |   /** A sub command group that is present in root command's options */ | ||||||
|   SUB_COMMAND_GROUP = 2, |   SUB_COMMAND_GROUP = 2, | ||||||
|   STRING = 3, |   STRING = 3, | ||||||
|   INTEGER = 4, |   INTEGER = 4, | ||||||
|  | @ -68,58 +99,71 @@ 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[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Represents the Slash Command (Application Command) payload sent for creating/bulk editing. */ | ||||||
| export interface SlashCommandPartial { | export interface SlashCommandPartial { | ||||||
|  |   /** Name of the Slash Command */ | ||||||
|   name: string |   name: string | ||||||
|  |   /** Description of the Slash Command */ | ||||||
|   description: string |   description: string | ||||||
|  |   /** Options (arguments, sub commands or group) of the Slash Command */ | ||||||
|   options?: SlashCommandOption[] |   options?: SlashCommandOption[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Represents a fully qualified Slash Command (Application Command) payload. */ | ||||||
| export interface SlashCommandPayload extends SlashCommandPartial { | export interface SlashCommandPayload extends SlashCommandPartial { | ||||||
|  |   /** ID of the Slash Command */ | ||||||
|   id: string |   id: string | ||||||
|  |   /** Application ID */ | ||||||
|   application_id: string |   application_id: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum InteractionResponseType { | export enum InteractionResponseType { | ||||||
|   /** Just ack a ping, Http-only. */ |   /** Just ack a ping, Http-only. */ | ||||||
|   PONG = 1, |   PONG = 1, | ||||||
|   /** Do nothing, just acknowledge the Interaction */ |   /** @deprecated **DEPRECATED:** Do nothing, just acknowledge the Interaction */ | ||||||
|   ACKNOWLEDGE = 2, |   ACKNOWLEDGE = 2, | ||||||
|   /** Send a channel message without "<User> used /<Command> with <Bot>" */ |   /** @deprecated **DEPRECATED:** Send a channel message without "<User> used /<Command> with <Bot>" */ | ||||||
|   CHANNEL_MESSAGE = 3, |   CHANNEL_MESSAGE = 3, | ||||||
|   /** Send a channel message with "<User> used /<Command> with <Bot>" */ |   /** Send a channel message as response. */ | ||||||
|   CHANNEL_MESSAGE_WITH_SOURCE = 4, |   CHANNEL_MESSAGE_WITH_SOURCE = 4, | ||||||
|   /** Send nothing further, but send "<User> used /<Command> with <Bot>" */ |   /** Let the user know bot is processing ("thinking") and you can edit the response later */ | ||||||
|   ACK_WITH_SOURCE = 5 |   DEFERRED_CHANNEL_MESSAGE = 5 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum InteractionResponseFlags { | export enum InteractionResponseFlags { | ||||||
|   /** A Message which is only visible to Interaction User, and is not saved on backend */ |   /** A Message which is only visible to Interaction User. */ | ||||||
|   EPHEMERAL = 1 << 6 |   EPHEMERAL = 1 << 6 | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								src/utils/dict.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utils/dict.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export interface Dict<T> { | ||||||
|  |   [name: string]: T | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								src/utils/encoding.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/utils/encoding.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | const encoder = new TextEncoder() | ||||||
|  | const decoder = new TextDecoder('utf-8') | ||||||
|  | 
 | ||||||
|  | export function encodeText(str: string): Uint8Array { | ||||||
|  |   return encoder.encode(str) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function decodeText(bytes: Uint8Array): string { | ||||||
|  |   return decoder.decode(bytes) | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue