2021-03-14 10:16:44 +00:00
import {
Interaction ,
InteractionApplicationCommandResolved
} from '../structures/slash.ts'
2020-12-10 06:55:52 +00:00
import {
2021-03-14 09:42:05 +00:00
InteractionPayload ,
2021-03-14 10:16:44 +00:00
InteractionResponsePayload ,
2020-12-12 12:27:35 +00:00
InteractionType ,
2021-04-04 05:42:15 +00:00
SlashCommandOptionType
2020-12-10 06:55:52 +00:00
} from '../types/slash.ts'
2021-04-04 05:42:15 +00:00
import type { Client } from '../client/mod.ts'
import { RESTManager } from '../rest/mod.ts'
2020-12-22 06:58:45 +00:00
import { SlashModule } from './slashModule.ts'
2021-03-14 08:50:15 +00:00
import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts'
2021-03-14 10:16:44 +00:00
import { User } from '../structures/user.ts'
2021-04-04 04:51:39 +00:00
import { HarmonyEventEmitter } from '../utils/events.ts'
import { encodeText , decodeText } from '../utils/encoding.ts'
2021-04-04 05:42:15 +00:00
import { SlashCommandsManager } from './slashCommand.ts'
2020-12-10 09:10:00 +00:00
2021-02-10 12:29:21 +00:00
export type SlashCommandHandlerCallback = ( interaction : Interaction ) = > unknown
2020-12-10 09:10:00 +00:00
export interface SlashCommandHandler {
name : string
guild? : string
2020-12-16 10:00:13 +00:00
parent? : string
2020-12-16 13:05:26 +00:00
group? : string
2020-12-10 09:10:00 +00:00
handler : SlashCommandHandlerCallback
2020-12-10 06:55:52 +00:00
}
2021-02-10 12:29:21 +00:00
/** Options for SlashClient */
2020-12-20 09:45:49 +00:00
export interface SlashOptions {
id? : string | ( ( ) = > string )
client? : Client
enabled? : boolean
token? : string
rest? : RESTManager
2020-12-22 06:58:45 +00:00
publicKey? : string
2020-12-20 09:45:49 +00:00
}
2021-03-30 09:51:29 +00:00
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SlashClientEvents = {
interaction : [ Interaction ]
interactionError : [ Error ]
2021-03-30 10:07:13 +00:00
ping : [ ]
2021-03-30 09:51:29 +00:00
}
2021-02-10 12:29:21 +00:00
/** Slash Client represents an Interactions Client which can be used without Harmony Client. */
2021-03-30 09:51:29 +00:00
export class SlashClient extends HarmonyEventEmitter < SlashClientEvents > {
2020-12-20 09:45:49 +00:00
id : string | ( ( ) = > string )
client? : Client
token? : string
2020-12-10 06:55:52 +00:00
enabled : boolean = true
2020-12-10 09:10:00 +00:00
commands : SlashCommandsManager
handlers : SlashCommandHandler [ ] = [ ]
2020-12-20 09:45:49 +00:00
rest : RESTManager
2020-12-22 06:58:45 +00:00
modules : SlashModule [ ] = [ ]
publicKey? : string
_decoratedSlash? : Array < {
name : string
guild? : string
parent? : string
group? : string
handler : ( interaction : Interaction ) = > any
} >
2020-12-20 09:45:49 +00:00
constructor ( options : SlashOptions ) {
2021-03-30 09:51:29 +00:00
super ( )
2020-12-20 09:45:49 +00:00
let id = options . id
if ( options . token !== undefined ) id = atob ( options . token ? . split ( '.' ) [ 0 ] )
if ( id === undefined )
throw new Error ( 'ID could not be found. Pass at least client or token' )
this . id = id
this . client = options . client
this . token = options . token
2020-12-22 06:58:45 +00:00
this . publicKey = options . publicKey
2020-12-10 06:55:52 +00:00
2021-01-24 18:49:08 +00:00
this . enabled = options . enabled ? ? true
2020-12-10 06:55:52 +00:00
2020-12-20 09:45:49 +00:00
if ( this . client ? . _decoratedSlash !== undefined ) {
2020-12-10 09:10:00 +00:00
this . client . _decoratedSlash . forEach ( ( e ) = > {
2021-01-24 18:36:19 +00:00
e . handler = e . handler . bind ( this . client )
2020-12-10 09:10:00 +00:00
this . handlers . push ( e )
} )
}
2020-12-22 06:58:45 +00:00
if ( this . _decoratedSlash !== undefined ) {
this . _decoratedSlash . forEach ( ( e ) = > {
2021-01-24 18:36:19 +00:00
e . handler = e . handler . bind ( this . client )
2020-12-22 06:58:45 +00:00
this . handlers . push ( e )
} )
}
2020-12-20 09:45:49 +00:00
this . rest =
options . client === undefined
? options . rest === undefined
? new RESTManager ( {
2021-04-04 04:51:39 +00:00
token : this.token
} )
2020-12-20 09:45:49 +00:00
: options . rest
: options . client . rest
2021-04-04 04:51:39 +00:00
this . client ? . on (
'interactionCreate' ,
async ( interaction ) = > await this . _process ( interaction )
2020-12-10 06:55:52 +00:00
)
2021-01-24 18:36:19 +00:00
this . commands = new SlashCommandsManager ( this )
2020-12-10 06:55:52 +00:00
}
2020-12-20 09:45:49 +00:00
getID ( ) : string {
return typeof this . id === 'string' ? this . id : this.id ( )
}
2020-12-10 09:10:00 +00:00
/** Adds a new Slash Command Handler */
2021-04-04 08:21:08 +00:00
handle (
cmd : string | SlashCommandHandler ,
handler? : SlashCommandHandlerCallback
) : SlashClient {
const handle = {
name : typeof cmd === 'string' ? cmd : cmd.name ,
. . . ( handler !== undefined ? { handler } : { } ) ,
. . . ( typeof cmd === 'string' ? { } : cmd )
}
if ( handle . handler === undefined )
throw new Error ( 'Invalid usage. Handler function not provided' )
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
}
this . handlers . push ( handle as any )
2020-12-10 06:55:52 +00:00
return this
}
2020-12-10 09:10:00 +00:00
2021-01-01 05:55:23 +00:00
/** Load a Slash Module */
2020-12-23 09:56:02 +00:00
loadModule ( module : SlashModule ) : SlashClient {
this . modules . push ( module )
return this
}
2021-01-01 05:55:23 +00:00
/** Get all Handlers. Including Slash Modules */
2020-12-22 06:58:45 +00:00
getHandlers ( ) : SlashCommandHandler [ ] {
let res = this . handlers
for ( const mod of this . modules ) {
2020-12-23 09:56:02 +00:00
if ( mod === undefined ) continue
res = [
. . . res ,
. . . mod . commands . map ( ( cmd ) = > {
cmd . handler = cmd . handler . bind ( mod )
return cmd
} )
]
2020-12-22 06:58:45 +00:00
}
return res
}
2021-01-01 05:55:23 +00:00
/** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */
2020-12-16 13:05:26 +00:00
private _getCommand ( i : Interaction ) : SlashCommandHandler | undefined {
2020-12-22 06:58:45 +00:00
return this . getHandlers ( ) . find ( ( e ) = > {
2020-12-16 13:05:26 +00:00
const hasGroupOrParent = e . group !== undefined || e . parent !== undefined
const groupMatched =
e . group !== undefined && e . parent !== undefined
? i . options
2021-04-04 04:51:39 +00:00
. find (
( o ) = >
o . name === e . group &&
o . type === SlashCommandOptionType . SUB_COMMAND_GROUP
)
? . options ? . find ( ( o ) = > o . name === e . name ) !== undefined
2020-12-16 13:05:26 +00:00
: true
const subMatched =
e . group === undefined && e . parent !== undefined
2021-02-10 12:29:21 +00:00
? i . options . find (
2021-04-04 04:51:39 +00:00
( o ) = >
o . name === e . name &&
o . type === SlashCommandOptionType . SUB_COMMAND
) !== undefined
2020-12-16 13:05:26 +00:00
: true
const nameMatched1 = e . name === i . name
const parentMatched = hasGroupOrParent ? e . parent === i.name : true
const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1
const matched = groupMatched && subMatched && nameMatched
return matched
} )
}
2021-02-10 12:29:21 +00:00
/** Process an incoming Interaction */
2021-03-30 09:51:29 +00:00
private async _process ( interaction : Interaction ) : Promise < void > {
2020-12-10 09:10:00 +00:00
if ( ! this . enabled ) return
2021-02-10 12:29:21 +00:00
if (
interaction . type !== InteractionType . APPLICATION_COMMAND ||
interaction . data === undefined
)
return
2020-12-12 12:27:35 +00:00
2021-04-04 08:21:08 +00:00
const cmd =
this . _getCommand ( interaction ) ? ?
this . getHandlers ( ) . find ( ( e ) = > e . name === '*' )
2021-01-01 05:55:23 +00:00
if ( cmd ? . group !== undefined )
interaction . data . options = interaction . data . options [ 0 ] . options ? ? [ ]
if ( cmd ? . parent !== undefined )
interaction . data . options = interaction . data . options [ 0 ] . options ? ? [ ]
2020-12-10 09:10:00 +00:00
if ( cmd === undefined ) return
2021-03-30 09:51:29 +00:00
await this . emit ( 'interaction' , interaction )
2021-04-04 04:51:39 +00:00
try {
await cmd . handler ( interaction )
} catch ( e ) {
2021-03-30 09:51:29 +00:00
await this . emit ( 'interactionError' , e )
}
2020-12-10 09:10:00 +00:00
}
2020-12-22 06:58:45 +00:00
2021-03-14 09:42:05 +00:00
/** Verify HTTP based Interaction */
2020-12-22 06:58:45 +00:00
async verifyKey (
2021-03-14 08:50:15 +00:00
rawBody : string | Uint8Array ,
signature : string | Uint8Array ,
timestamp : string | Uint8Array
2020-12-22 06:58:45 +00:00
) : Promise < boolean > {
if ( this . publicKey === undefined )
throw new Error ( 'Public Key is not present' )
2021-03-14 08:50:15 +00:00
const fullBody = new Uint8Array ( [
2021-04-04 04:51:39 +00:00
. . . ( typeof timestamp === 'string' ? encodeText ( timestamp ) : timestamp ) ,
. . . ( typeof rawBody === 'string' ? encodeText ( rawBody ) : rawBody )
2021-03-14 08:50:15 +00:00
] )
return edverify ( signature , fullBody , this . publicKey ) . catch ( ( ) = > false )
2020-12-22 06:58:45 +00:00
}
2021-03-14 10:16:44 +00:00
/** 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.** */
2021-03-14 09:42:05 +00:00
async verifyServerRequest ( req : {
headers : Headers
method : string
2021-04-04 04:51:39 +00:00
body : Deno.Reader | Uint8Array
2021-03-14 09:42:05 +00:00
respond : ( options : {
status? : number
2021-03-14 10:16:44 +00:00
headers? : Headers
2021-03-29 12:30:40 +00:00
body? : string | Uint8Array | FormData
2021-03-14 09:42:05 +00:00
} ) = > Promise < void >
2021-03-14 10:16:44 +00:00
} ) : Promise < false | Interaction > {
2021-03-14 09:42:05 +00:00
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
2021-04-04 04:51:39 +00:00
const rawbody =
req . body instanceof Uint8Array ? req.body : await Deno . readAll ( req . body )
2021-03-14 09:42:05 +00:00
const verify = await this . verifyKey ( rawbody , signature , timestamp )
if ( ! verify ) return false
try {
2021-04-04 04:51:39 +00:00
const payload : InteractionPayload = JSON . parse ( decodeText ( rawbody ) )
2021-03-14 10:16:44 +00:00
// 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 : { }
}
} )
2021-03-29 12:30:40 +00:00
res . _httpRespond = async ( d : InteractionResponsePayload | FormData ) = >
2021-03-14 10:16:44 +00:00
await req . respond ( {
status : 200 ,
headers : new Headers ( {
2021-04-04 04:51:39 +00:00
'content-type' :
d instanceof FormData ? 'multipart/form-data' : 'application/json'
2021-03-14 10:16:44 +00:00
} ) ,
2021-03-29 12:30:40 +00:00
body : d instanceof FormData ? d : JSON.stringify ( d )
2021-03-14 10:16:44 +00:00
} )
2021-03-14 09:42:05 +00:00
return res
} catch ( e ) {
return false
}
}
2021-03-29 12:30:40 +00:00
/** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */
2021-04-04 04:51:39 +00:00
async verifyFetchEvent ( {
request : req ,
respondWith
} : {
respondWith : CallableFunction
request : Request
} ) : Promise < false | Interaction > {
2021-03-29 12:30:40 +00:00
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 ) = > {
2021-04-04 04:51:39 +00:00
await respondWith (
new Response ( options . body , {
headers : options.headers ,
status : options.status
} )
)
}
2021-03-29 12:30:40 +00:00
} )
}
2021-02-01 08:37:54 +00:00
async verifyOpineRequest ( req : any ) : Promise < boolean > {
2020-12-22 06:58:45 +00:00
const signature = req . headers . get ( 'x-signature-ed25519' )
const timestamp = req . headers . get ( 'x-signature-timestamp' )
const contentLength = req . headers . get ( 'content-length' )
if ( signature === null || timestamp === null || contentLength === null )
return false
const body = new Uint8Array ( parseInt ( contentLength ) )
await req . body . read ( body )
const verified = await this . verifyKey ( body , signature , timestamp )
if ( ! verified ) return false
return true
}
/** Middleware to verify request in Opine framework. */
async verifyOpineMiddleware (
2021-02-01 08:37:54 +00:00
req : any ,
res : any ,
2020-12-22 06:58:45 +00:00
next : CallableFunction
) : Promise < any > {
const verified = await this . verifyOpineRequest ( req )
if ( ! verified ) return res . setStatus ( 401 ) . end ( )
await next ( )
return true
}
// TODO: create verifyOakMiddleware too
/** Method to verify Request from Oak server "Context". */
2021-02-01 08:37:54 +00:00
async verifyOakRequest ( ctx : any ) : Promise < any > {
2020-12-22 06:58:45 +00:00
const signature = ctx . request . headers . get ( 'x-signature-ed25519' )
const timestamp = ctx . request . headers . get ( 'x-signature-timestamp' )
const contentLength = ctx . request . headers . get ( 'content-length' )
if (
signature === null ||
timestamp === null ||
contentLength === null ||
ctx . request . hasBody !== true
) {
return false
}
const body = await ctx . request . body ( ) . value
2021-02-01 08:37:54 +00:00
const verified = await this . verifyKey ( body , signature , timestamp )
2020-12-22 06:58:45 +00:00
if ( ! verified ) return false
return true
}
2020-12-10 06:55:52 +00:00
}
2021-03-30 09:51:29 +00:00
/** 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
} )
}
}