2021-01-07 05:34:14 -08:00
/ * T h e A r t i f i c e r w a s b u i l t i n m e m o r y o f B a b k a
* With love , Ean
*
* December 21 , 2020
* /
2021-01-07 11:00:46 -08:00
// DEVMODE is to prevent users from accessing parts of the bot that are currently broken
2021-01-07 05:34:14 -08:00
const DEVMODE = false ;
2021-01-07 11:00:46 -08:00
// DEBUG is used to toggle the cmdPrompt
const DEBUG = true ;
2021-01-07 05:34:14 -08:00
import {
startBot , editBotsStatus ,
Intents , StatusTypes , ActivityType ,
Message , Guild , sendMessage , sendDirectMessage ,
2021-01-26 23:35:46 -08:00
cache ,
MessageContent
2021-01-07 05:34:14 -08:00
} from "https://deno.land/x/discordeno@10.0.0/mod.ts" ;
2021-01-14 05:47:18 -08:00
2021-01-11 00:42:57 -08:00
import { serve } from "https://deno.land/std@0.83.0/http/server.ts" ;
import { Status , STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts" ;
2021-01-07 05:34:14 -08:00
2021-01-14 05:47:18 -08:00
import { Client } from "https://deno.land/x/mysql@v2.7.0/mod.ts" ;
2021-01-18 08:50:22 -08:00
import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts" ;
2021-01-14 05:47:18 -08:00
2021-01-07 05:34:14 -08:00
import utils from "./src/utils.ts" ;
import solver from "./src/solver.ts" ;
import config from "./config.ts" ;
2021-01-14 05:47:18 -08:00
const dbClient = await new Client ( ) . connect ( {
hostname : config.db.host ,
port : config.db.port ,
db : config.db.name ,
username : config.db.username ,
password : config.db.password ,
} ) ;
// Start up the Discord Bot
2021-01-07 05:34:14 -08:00
startBot ( {
token : config.token ,
intents : [ Intents . GUILD_MESSAGES , Intents . DIRECT_MESSAGES , Intents . GUILDS ] ,
eventHandlers : {
ready : ( ) = > {
console . log ( "Logged in!" ) ;
editBotsStatus ( StatusTypes . Online , ` ${ config . prefix } help for commands ` , ActivityType . Game ) ;
2021-01-07 11:00:46 -08:00
// setTimeout added to make sure the startup message does not error out
2021-01-07 05:34:14 -08:00
setTimeout ( ( ) = > {
sendMessage ( config . logChannel , ` ${ config . name } has started, running version ${ config . version } . ` ) . catch ( ( ) = > {
console . error ( "Failed to send message 00" ) ;
} ) ;
} , 1000 ) ;
} ,
guildCreate : ( guild : Guild ) = > {
sendMessage ( config . logChannel , ` New guild joined: ${ guild . name } (id: ${ guild . id } ). This guild has ${ guild . memberCount } members! ` ) . catch ( ( ) = > {
console . error ( "Failed to send message 01" ) ;
} ) ;
} ,
guildDelete : ( guild : Guild ) = > {
sendMessage ( config . logChannel , ` I have been removed from: ${ guild . name } (id: ${ guild . id } ) ` ) . catch ( ( ) = > {
console . error ( "Failed to send message 02" ) ;
} ) ;
} ,
debug : ( DEVMODE ? console . error : ( ) = > { } ) ,
messageCreate : async ( message : Message ) = > {
// Ignore all other bots
if ( message . author . bot ) return ;
// Ignore all messages that are not commands
if ( message . content . indexOf ( config . prefix ) !== 0 ) return ;
// Split into standard command + args format
const args = message . content . slice ( config . prefix . length ) . trim ( ) . split ( / +/g ) ;
const command = args . shift ( ) ? . toLowerCase ( ) ;
// All commands below here
// [[ping
// Its a ping test, what else do you want.
if ( command === "ping" ) {
// Calculates ping between sending a message and editing it, giving a nice round-trip latency.
try {
const m = await utils . sendIndirectMessage ( message , "Ping?" , sendMessage , sendDirectMessage ) ;
m . edit ( ` Pong! Latency is ${ m . timestamp - message . timestamp } ms. ` ) ;
} catch ( err ) {
console . error ( "Failed to send message 10" , message , err ) ;
}
}
2021-01-07 22:02:38 -08:00
// [[rip [[memory
// Displays a short message I wanted to include
else if ( command === "rip" || command === "memory" ) {
utils . sendIndirectMessage ( message , "The Artificer was built in memory of my Grandmother, Babka\nWith much love, Ean\n\nDecember 21, 2020" , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 11" , message , err ) ;
} ) ;
}
2021-01-26 23:35:46 -08:00
// [[rollhelp or [[rh or [[hr
// Help command specifically for the roll command
else if ( command === "rollhelp" || command === "rh" || command === "hr" || command === "??" ) {
utils . sendIndirectMessage ( message , config . rollhelp . join ( "\n" ) , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 21" , message , err ) ;
} ) ;
}
2021-01-07 05:34:14 -08:00
// [[help or [[h or [[?
// Help command, prints from help file
else if ( command === "help" || command === "h" || command === "?" ) {
utils . sendIndirectMessage ( message , config . help . join ( "\n" ) , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 20" , message , err ) ;
} ) ;
}
2021-01-07 11:00:46 -08:00
// [[version or [[v
2021-01-07 05:34:14 -08:00
// Returns version of the bot
else if ( command === "version" || command === "v" ) {
utils . sendIndirectMessage ( message , ` My current version is ${ config . version } . ` , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 30" , message , err ) ;
} ) ;
}
// [[report or [[r (command that failed)
// Manually report a failed roll
else if ( command === "report" || command === "r" ) {
2021-01-07 11:00:46 -08:00
sendMessage ( config . reportChannel , ( "USER REPORT:\n" + args . join ( " " ) ) ) . catch ( err = > {
2021-01-07 05:34:14 -08:00
console . error ( "Failed to send message 50" , message , err ) ;
} ) ;
utils . sendIndirectMessage ( message , "Failed command has been reported to my developer." , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 51" , message , err ) ;
} ) ;
}
// [[stats or [[s
// Displays stats on the bot
else if ( command === "stats" || command === "s" ) {
utils . sendIndirectMessage ( message , ` ${ config . name } is rolling dice for ${ cache . members . size } users, in ${ cache . channels . size } channels of ${ cache . guilds . size } servers. ` , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 60" , message , err ) ;
} ) ;
}
2021-01-18 08:50:22 -08:00
// [[roll]]
2021-01-07 05:34:14 -08:00
// Dice rolling commence!
2021-01-18 08:50:22 -08:00
else if ( ( command + args . join ( "" ) ) . indexOf ( config . postfix ) > - 1 ) {
2021-01-07 11:00:46 -08:00
// If DEVMODE is on, only allow this command to be used in the devServer
if ( DEVMODE && message . guildID !== config . devServer ) {
2021-01-07 05:34:14 -08:00
utils . sendIndirectMessage ( message , "Command is in development, please try again later." , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 70" , message , err ) ;
} ) ;
return ;
}
2021-01-07 11:00:46 -08:00
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
2021-01-07 05:34:14 -08:00
try {
2021-01-14 05:47:18 -08:00
const originalCommand = config . prefix + command + " " + args . join ( " " ) ;
2021-01-07 05:34:14 -08:00
const m = await utils . sendIndirectMessage ( message , "Rolling..." , sendMessage , sendDirectMessage ) ;
const modifiers = {
noDetails : false ,
spoiler : "" ,
maxRoll : false ,
nominalRoll : false ,
gmRoll : false ,
2021-01-26 23:35:46 -08:00
gms : < string [ ] > [ ] ,
order : ""
2021-01-07 05:34:14 -08:00
} ;
2021-01-07 11:00:46 -08:00
// Check if any of the args are command flags and pull those out into the modifiers object
2021-01-07 05:34:14 -08:00
for ( let i = 0 ; i < args . length ; i ++ ) {
2021-01-07 11:00:46 -08:00
switch ( args [ i ] . toLowerCase ( ) ) {
2021-01-07 05:34:14 -08:00
case "-nd" :
modifiers . noDetails = true ;
args . splice ( i , 1 ) ;
i -- ;
break ;
case "-s" :
modifiers . spoiler = "||" ;
args . splice ( i , 1 ) ;
i -- ;
break ;
case "-m" :
modifiers . maxRoll = true ;
args . splice ( i , 1 ) ;
i -- ;
break ;
case "-n" :
modifiers . nominalRoll = true ;
args . splice ( i , 1 ) ;
i -- ;
break ;
case "-gm" :
modifiers . gmRoll = true ;
2021-01-07 11:00:46 -08:00
// -gm is a little more complex, as we must get all of the GMs that need to be DMd
2021-01-07 22:02:38 -08:00
while ( ( ( i + 1 ) < args . length ) && args [ i + 1 ] . startsWith ( "<@" ) ) {
2021-01-07 11:00:46 -08:00
// Keep looping thru the rest of the args until one does not start with the discord mention code
2021-01-07 22:02:38 -08:00
modifiers . gms . push ( args [ i + 1 ] . replace ( /[!]/g , "" ) ) ;
2021-01-07 05:34:14 -08:00
args . splice ( ( i + 1 ) , 1 ) ;
}
if ( modifiers . gms . length < 1 ) {
2021-01-07 11:00:46 -08:00
// If -gm is on and none were found, throw an error
2021-01-07 05:34:14 -08:00
m . edit ( "Error: Must specifiy at least one GM by mentioning them" ) ;
2021-01-14 05:47:18 -08:00
if ( config . logRolls ) {
// If enabled, log rolls so we can verify the bots math
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)" , [ originalCommand , "NoGMsFound" , m . id ] ) . catch ( e = > {
console . log ( "Failed to insert into database 00" , e ) ;
} ) ;
}
2021-01-07 05:34:14 -08:00
return ;
}
2021-01-26 23:35:46 -08:00
args . splice ( i , 1 ) ;
i -- ;
break ;
case "-o" :
args . splice ( i , 1 ) ;
if ( args [ i ] . toLowerCase ( ) !== "d" && args [ i ] . toLowerCase ( ) !== "a" ) {
// If -o is on and asc or desc was not specified, error out
m . edit ( "Error: Must specifiy a or d to order the rolls ascending or descending" ) ;
if ( config . logRolls ) {
// If enabled, log rolls so we can verify the bots math
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)" , [ originalCommand , "NoOrderFound" , m . id ] ) . catch ( e = > {
console . log ( "Failed to insert into database 05" , e ) ;
} ) ;
}
return ;
}
modifiers . order = args [ i ] . toLowerCase ( ) ;
2021-01-07 05:34:14 -08:00
args . splice ( i , 1 ) ;
i -- ;
break ;
default :
break ;
}
}
2021-01-07 11:00:46 -08:00
// maxRoll and nominalRoll cannot both be on, throw an error
if ( modifiers . maxRoll && modifiers . nominalRoll ) {
m . edit ( "Error: Cannot maximise and nominise the roll at the same time" ) ;
2021-01-14 05:47:18 -08:00
if ( config . logRolls ) {
// If enabled, log rolls so we can verify the bots math
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)" , [ originalCommand , "MaxAndNominal" , m . id ] ) . catch ( e = > {
console . log ( "Failed to insert into database 01" , e ) ;
} ) ;
}
2021-01-07 11:00:46 -08:00
return ;
}
2021-01-07 05:34:14 -08:00
2021-01-07 11:00:46 -08:00
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in
const rollCmd = command + " " + args . join ( " " ) ;
2021-01-26 23:35:46 -08:00
const returnmsg = solver . parseRoll ( rollCmd , config . prefix , config . postfix , modifiers . maxRoll , modifiers . nominalRoll , modifiers . order ) || { error : true , errorCode : "EmptyMessage" , errorMsg : "Error: Empty message" , line1 : "" , line2 : "" , line3 : "" } ;
2021-01-07 11:00:46 -08:00
2021-01-07 05:34:14 -08:00
let returnText = "" ;
2021-01-07 11:00:46 -08:00
// If there was an error, report it to the user in hopes that they can determine what they did wrong
2021-01-07 05:34:14 -08:00
if ( returnmsg . error ) {
returnText = returnmsg . errorMsg ;
2021-01-07 11:00:46 -08:00
m . edit ( returnText ) ;
2021-01-14 05:47:18 -08:00
if ( config . logRolls ) {
// If enabled, log rolls so we can verify the bots math
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)" , [ originalCommand , returnmsg . errorCode , m . id ] ) . catch ( e = > {
console . log ( "Failed to insert into database 02" , e ) ;
} ) ;
}
2021-01-07 11:00:46 -08:00
return ;
2021-01-07 05:34:14 -08:00
} else {
2021-01-07 11:00:46 -08:00
// Else format the output using details from the solver
2021-01-07 05:34:14 -08:00
returnText = "<@" + message . author . id + ">" + returnmsg . line1 + "\n" + returnmsg . line2 ;
if ( modifiers . noDetails ) {
2021-01-11 00:42:57 -08:00
returnText += "\nDetails suppressed by -nd flag." ;
2021-01-07 05:34:14 -08:00
} else {
returnText += "\nDetails:\n" + modifiers . spoiler + returnmsg . line3 + modifiers . spoiler ;
}
}
2021-01-07 11:00:46 -08:00
// If the roll was a GM roll, send DMs to all the GMs
2021-01-07 05:34:14 -08:00
if ( modifiers . gmRoll ) {
2021-01-07 11:00:46 -08:00
// Make a new return line to be sent to the roller
2021-01-07 05:34:14 -08:00
const normalText = "<@" + message . author . id + ">" + returnmsg . line1 + "\nResults have been messaged to the following GMs: " + modifiers . gms . join ( " " ) ;
2021-01-07 11:00:46 -08:00
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
2021-01-07 05:34:14 -08:00
modifiers . gms . forEach ( async e = > {
2021-01-26 23:35:46 -08:00
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob ( [ returnText as BlobPart ] , { "type" : "text" } ) ;
// Update return text
returnText = "<@" + message . author . id + ">" + returnmsg . line1 + "\n" + returnmsg . line2 + "\nFull details have been attached to this messaged as a `.txt` file for verification purposes." ;
// Attempt to DM the GMs and send a warning if it could not DM a GM
await sendDirectMessage ( e . substr ( 2 , ( e . length - 3 ) ) , { "content" : returnText , "file" : { "blob" : b , "name" : "rollDetails.txt" } } ) . catch ( ( ) = > {
utils . sendIndirectMessage ( message , "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server." , sendMessage , sendDirectMessage ) ;
} ) ;
2021-01-07 05:34:14 -08:00
} ) ;
2021-01-11 00:42:57 -08:00
// Finally send the text
2021-01-07 05:34:14 -08:00
m . edit ( normalText ) ;
2021-01-14 05:47:18 -08:00
if ( config . logRolls ) {
// If enabled, log rolls so we can verify the bots math
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,0)" , [ originalCommand , returnText , m . id ] ) . catch ( e = > {
console . log ( "Failed to insert into database 03" , e ) ;
} ) ;
}
2021-01-07 05:34:14 -08:00
} else {
2021-01-07 11:00:46 -08:00
// When not a GM roll, make sure the message is not too big
2021-01-07 05:34:14 -08:00
if ( returnText . length > 2000 ) {
2021-01-26 23:35:46 -08:00
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob ( [ returnText as BlobPart ] , { "type" : "text" } ) ;
2021-01-11 00:42:57 -08:00
2021-01-26 23:35:46 -08:00
// Update return text
returnText = "<@" + message . author . id + ">" + returnmsg . line1 + "\n" + returnmsg . line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a `.txt` file for verification purposes." ;
2021-01-07 05:34:14 -08:00
2021-01-26 23:35:46 -08:00
// Remove the original message to send new one with attachment
m . delete ( ) ;
await utils . sendIndirectMessage ( message , { "content" : returnText , "file" : { "blob" : b , "name" : "rollDetails.txt" } } , sendMessage , sendDirectMessage ) ;
} else {
// Finally send the text
m . edit ( returnText ) ;
}
2021-01-14 05:47:18 -08:00
if ( config . logRolls ) {
// If enabled, log rolls so we can verify the bots math
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,0)" , [ originalCommand , returnText , m . id ] ) . catch ( e = > {
console . log ( "Failed to insert into database 04" , e ) ;
} ) ;
}
2021-01-07 05:34:14 -08:00
}
} catch ( err ) {
console . error ( "Something failed 71" ) ;
}
}
2021-01-18 08:50:22 -08:00
// [[emoji or [[emojialias
// Check if the unhandled command is an emoji request
else {
// Start looping thru the possible emojis
config . emojis . some ( e = > {
// If a match gets found
if ( e . aliases . indexOf ( command || "" ) > - 1 ) {
// Send the needed emoji
utils . sendIndirectMessage ( message , ` < ${ e . animated ? "a" : "" } : ${ e . name } : ${ e . id } > ` , sendMessage , sendDirectMessage ) . catch ( err = > {
console . error ( "Failed to send message 40" , message , err ) ;
} ) ;
// And attempt to delete if needed
if ( e . deleteSender ) {
message . delete ( ) . catch ( err = > {
console . error ( "Failed to delete message 41" , message , err ) ;
} ) ;
}
return true ;
}
} ) ;
}
2021-01-07 05:34:14 -08:00
}
}
} ) ;
2021-01-07 11:00:46 -08:00
// Start up the command prompt for debug usage
if ( DEBUG ) {
utils . cmdPrompt ( config . logChannel , config . name , sendMessage ) ;
}
2021-01-11 00:42:57 -08:00
// Start up the API for rolling from third party apps (like excel macros)
if ( config . api . enable ) {
const server = serve ( { hostname : "0.0.0.0" , port : config.api.port } ) ;
2021-01-18 08:50:22 -08:00
console . log ( ` HTTP api running at: http://localhost: ${ config . api . port } / ` ) ;
// rateLimitTime holds all users with the last time they started a rate limit timer
const rateLimitTime = new Map < string , number > ( ) ;
// rateLimitCnt holds the number of times the user has called the api in the current rate limit timer
const rateLimitCnt = new Map < string , number > ( ) ;
2021-01-11 00:42:57 -08:00
// Catching every request made to the server
for await ( const request of server ) {
2021-01-14 05:47:18 -08:00
// Check if user is authenticated to be using this API
let authenticated = false ;
2021-01-18 08:50:22 -08:00
let rateLimited = false ;
let updateRateLimitTime = false ;
let apiUserid = 0 n ;
let apiUseridStr = "" ;
2021-01-14 05:47:18 -08:00
// Check the requests API key
if ( request . headers . has ( "X-Api-Key" ) ) {
// Get the userid and flags for the specific key
2021-01-18 08:50:22 -08:00
const dbApiQuery = await dbClient . query ( "SELECT userid FROM all_keys WHERE apiKey = ? AND active = 1 AND banned = 0" , [ request . headers . get ( "X-Api-Key" ) ] ) ;
2021-01-14 05:47:18 -08:00
// If only one user returned, is not banned, and is currently active, mark as authenticated
2021-01-18 08:50:22 -08:00
if ( dbApiQuery . length === 1 ) {
2021-01-14 05:47:18 -08:00
apiUserid = dbApiQuery [ 0 ] . userid ;
authenticated = true ;
2021-01-18 08:50:22 -08:00
// Rate limiting inits
apiUseridStr = apiUserid . toString ( ) ;
const apiTimeNow = new Date ( ) . getTime ( ) ;
// Check if user has sent a request recently
if ( rateLimitTime . has ( apiUseridStr ) && ( ( ( rateLimitTime . get ( apiUseridStr ) || 0 ) + config . api . rateLimitTime ) > apiTimeNow ) ) {
// Get current count
const currentCnt = rateLimitCnt . get ( apiUseridStr ) || 0 ;
if ( currentCnt < config . api . rateLimitCnt ) {
// Limit not yet exceeded, update count
rateLimitCnt . set ( apiUseridStr , ( currentCnt + 1 ) ) ;
} else {
// Limit exceeded, prevent API use
rateLimited = true ;
}
} else {
// Update the maps
updateRateLimitTime = true ;
rateLimitCnt . set ( apiUseridStr , 1 ) ;
}
2021-01-14 05:47:18 -08:00
}
}
2021-01-11 00:42:57 -08:00
2021-01-18 08:50:22 -08:00
if ( authenticated && ! rateLimited ) {
2021-01-11 00:42:57 -08:00
// Get path and query as a string
const [ path , tempQ ] = request . url . split ( "?" ) ;
2021-01-18 08:50:22 -08:00
2021-01-11 00:42:57 -08:00
// Turn the query into a map (if it exists)
const query = new Map < string , string > ( ) ;
if ( tempQ !== undefined ) {
tempQ . split ( "&" ) . forEach ( e = > {
const [ option , params ] = e . split ( "=" ) ;
query . set ( option . toLowerCase ( ) , params ) ;
} ) ;
}
// Handle the request
switch ( request . method ) {
case "GET" :
2021-01-18 08:50:22 -08:00
switch ( path . toLowerCase ( ) ) {
case "/api/key" :
case "/api/key/" :
if ( ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) && ( query . has ( "a" ) && ( ( query . get ( "a" ) || "" ) . length > 0 ) ) ) {
if ( apiUserid === config . api . admin && apiUserid === BigInt ( query . get ( "a" ) ) ) {
// Generate new secure key
const newKey = await nanoid ( 25 ) ;
// Flag to see if there is an error inside the catch
let erroredOut = false ;
// Insert new key/user pair into the db
await dbClient . execute ( "INSERT INTO all_keys(userid,apiKey) values(?,?)" , [ BigInt ( query . get ( "user" ) ) , newKey ] ) . catch ( e = > {
console . log ( "Failed to insert into database 20" ) ;
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
erroredOut = true ;
} ) ;
// Exit this case now if catch errored
if ( erroredOut ) {
break ;
} else {
// Send API key as response
request . respond ( { status : Status.OK , body : JSON.stringify ( { "key" : newKey , "userid" : query . get ( "user" ) } ) } ) ;
break ;
}
} else {
// Only allow the db admin to use this API
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
case "/api/key/ban" :
case "/api/key/ban/" :
case "/api/key/unban" :
case "/api/key/unban/" :
case "/api/key/activate" :
case "/api/key/activate/" :
case "/api/key/deactivate" :
case "/api/key/deactivate/" :
if ( ( query . has ( "a" ) && ( ( query . get ( "a" ) || "" ) . length > 0 ) ) && ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) ) {
if ( apiUserid === config . api . admin && apiUserid === BigInt ( query . get ( "a" ) ) ) {
// Flag to see if there is an error inside the catch
let key , value , erroredOut = false ;
// Determine key to edit
if ( path . toLowerCase ( ) . indexOf ( "ban" ) > 0 ) {
key = "banned" ;
} else {
key = "active" ;
}
// Determine value to set
if ( path . toLowerCase ( ) . indexOf ( "de" ) > 0 || path . toLowerCase ( ) . indexOf ( "un" ) > 0 ) {
value = 0 ;
} else {
value = 1 ;
}
2021-01-11 00:42:57 -08:00
2021-01-18 08:50:22 -08:00
// Execute the DB modification
await dbClient . execute ( "UPDATE all_keys SET ?? = ? WHERE userid = ?" , [ key , value , BigInt ( query . get ( "user" ) ) ] ) . catch ( e = > {
console . log ( "Failed to update database 28" ) ;
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
erroredOut = true ;
} ) ;
// Exit this case now if catch errored
if ( erroredOut ) {
break ;
} else {
// Send API key as response
request . respond ( { status : Status.OK , body : STATUS_TEXT.get ( Status . OK ) } ) ;
break ;
}
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
case "/api/channel" :
case "/api/channel/" :
if ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) {
if ( apiUserid === BigInt ( query . get ( "user" ) ) ) {
// Flag to see if there is an error inside the catch
let erroredOut = false ;
// Get all channels userid has authorized
const dbAllowedChannelQuery = await dbClient . query ( "SELECT * FROM allowed_channels WHERE userid = ?" , [ BigInt ( query . get ( "user" ) ) ] ) . catch ( e = > {
console . log ( "Failed to query database 22" ) ;
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
erroredOut = true ;
} ) ;
if ( erroredOut ) {
break ;
} else {
// Customized strinification to handle BigInts correctly
const returnChannels = JSON . stringify ( dbAllowedChannelQuery , ( _key , value ) = > ( typeof value === 'bigint' ? value . toString ( ) : value ) ) ;
// Send API key as response
request . respond ( { status : Status.OK , body : returnChannels } ) ;
break ;
}
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
case "/api/channel/add" :
case "/api/channel/add/" :
if ( ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) && ( query . has ( "channel" ) && ( ( query . get ( "channel" ) || "" ) . length > 0 ) ) ) {
if ( apiUserid === BigInt ( query . get ( "user" ) ) ) {
// Flag to see if there is an error inside the catch
let erroredOut = false ;
// Insert new user/channel pair into the db
await dbClient . execute ( "INSERT INTO allowed_channels(userid,channelid) values(?,?)" , [ BigInt ( query . get ( "user" ) ) , BigInt ( query . get ( "channel" ) ) ] ) . catch ( e = > {
console . log ( "Failed to insert into database 21" ) ;
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
erroredOut = true ;
} ) ;
// Exit this case now if catch errored
if ( erroredOut ) {
break ;
} else {
// Send API key as response
request . respond ( { status : Status.OK , body : STATUS_TEXT.get ( Status . OK ) } ) ;
break ;
}
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
case "/api/channel/ban" :
case "/api/channel/ban/" :
case "/api/channel/unban" :
case "/api/channel/unban/" :
if ( ( query . has ( "a" ) && ( ( query . get ( "a" ) || "" ) . length > 0 ) ) && ( query . has ( "channel" ) && ( ( query . get ( "channel" ) || "" ) . length > 0 ) ) && ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) ) {
if ( apiUserid === config . api . admin && apiUserid === BigInt ( query . get ( "a" ) ) ) {
// Flag to see if there is an error inside the catch
let value , erroredOut = false ;
// Determine value to set
if ( path . toLowerCase ( ) . indexOf ( "un" ) > 0 ) {
value = 0 ;
} else {
value = 1 ;
}
// Execute the DB modification
await dbClient . execute ( "UPDATE allowed_channels SET banned = ? WHERE userid = ? AND channelid = ?" , [ value , BigInt ( query . get ( "user" ) ) , BigInt ( query . get ( "channel" ) ) ] ) . catch ( e = > {
console . log ( "Failed to update database 24" ) ;
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
erroredOut = true ;
} ) ;
// Exit this case now if catch errored
if ( erroredOut ) {
break ;
} else {
// Send API key as response
request . respond ( { status : Status.OK , body : STATUS_TEXT.get ( Status . OK ) } ) ;
break ;
}
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
case "/api/channel/activate" :
case "/api/channel/activate/" :
case "/api/channel/deactivate" :
case "/api/channel/deactivate/" :
if ( ( query . has ( "channel" ) && ( ( query . get ( "channel" ) || "" ) . length > 0 ) ) && ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) ) {
if ( apiUserid === BigInt ( query . get ( "user" ) ) ) {
// Flag to see if there is an error inside the catch
let value , erroredOut = false ;
// Determine value to set
if ( path . toLowerCase ( ) . indexOf ( "de" ) > 0 ) {
value = 0 ;
} else {
value = 1 ;
}
// Update the requested entry
await dbClient . execute ( "UPDATE allowed_channels SET active = ? WHERE userid = ? AND channelid = ?" , [ value , BigInt ( query . get ( "user" ) ) , BigInt ( query . get ( "channel" ) ) ] ) . catch ( e = > {
console . log ( "Failed to update database 26" ) ;
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
erroredOut = true ;
} ) ;
// Exit this case now if catch errored
if ( erroredOut ) {
break ;
} else {
// Send API key as response
request . respond ( { status : Status.OK , body : STATUS_TEXT.get ( Status . OK ) } ) ;
break ;
}
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
case "/api/roll" :
case "/api/roll/" :
// Make sure query contains all the needed parts
if ( ( query . has ( "rollstr" ) && ( ( query . get ( "rollstr" ) || "" ) . length > 0 ) ) && ( query . has ( "channel" ) && ( ( query . get ( "channel" ) || "" ) . length > 0 ) ) && ( query . has ( "user" ) && ( ( query . get ( "user" ) || "" ) . length > 0 ) ) ) {
2021-01-11 00:42:57 -08:00
if ( query . has ( "n" ) && query . has ( "m" ) ) {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
2021-01-11 00:46:34 -08:00
break ;
2021-01-11 00:42:57 -08:00
}
2021-01-14 05:47:18 -08:00
// Check if user is authenticated to use this endpoint
let authorized = false ;
// Check if the db has the requested userid/channelid combo, and that the requested userid matches the userid linked with the api key
2021-01-18 08:50:22 -08:00
const dbChannelQuery = await dbClient . query ( "SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?" , [ BigInt ( query . get ( "user" ) ) , BigInt ( query . get ( "channel" ) ) ] )
if ( dbChannelQuery . length === 1 && ( apiUserid === BigInt ( query . get ( "user" ) ) ) && dbChannelQuery [ 0 ] . active && ! dbChannelQuery [ 0 ] . banned ) {
2021-01-14 05:47:18 -08:00
authorized = true ;
}
2021-01-11 00:42:57 -08:00
if ( authorized ) {
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
try {
// Flag to tell if roll was completely successful
let errorOut = false ;
// Make sure rollCmd is not undefined
let rollCmd = query . get ( "rollstr" ) || "" ;
2021-01-18 08:50:22 -08:00
const originalCommand = query . get ( "rollstr" ) ;
2021-01-11 00:42:57 -08:00
if ( rollCmd . length === 0 ) {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
2021-01-14 05:47:18 -08:00
// Always log API rolls for abuse detection
2021-01-18 08:50:22 -08:00
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)" , [ originalCommand , "EmptyInput" , null ] ) . catch ( e = > {
2021-01-14 05:47:18 -08:00
console . log ( "Failed to insert into database 10" , e ) ;
} ) ;
2021-01-11 00:46:34 -08:00
break ;
2021-01-11 00:42:57 -08:00
}
2021-01-26 23:35:46 -08:00
if ( query . has ( "o" ) && ( query . get ( "o" ) ? . toLowerCase ( ) !== "d" && query . get ( "o" ) ? . toLowerCase ( ) !== "a" ) ) {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
// Always log API rolls for abuse detection
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)" , [ originalCommand , "BadOrder" , null ] ) . catch ( e = > {
console . log ( "Failed to insert into database 10" , e ) ;
} ) ;
break ;
}
2021-01-11 00:42:57 -08:00
// Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord
rollCmd = rollCmd . substr ( rollCmd . indexOf ( config . prefix ) + 2 ) . replace ( /%20/g , " " ) ;
// Parse the roll and get the return text
2021-01-26 23:35:46 -08:00
const returnmsg = solver . parseRoll ( rollCmd , config . prefix , config . postfix , query . has ( "m" ) , query . has ( "n" ) , query . has ( "o" ) ? ( query . get ( "o" ) ? . toLowerCase ( ) || "" ) : "" ) ;
2021-01-11 00:42:57 -08:00
// Alert users why this message just appeared and how they can report abues pf this feature
const apiPrefix = "The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <" + config . api . supportURL + ">\n\n" ;
2021-01-14 05:47:18 -08:00
let m , returnText = "" ;
2021-01-11 00:42:57 -08:00
// Handle sending the error message to whoever called the api
if ( returnmsg . error ) {
request . respond ( { status : Status.InternalServerError , body : returnmsg.errorMsg } ) ;
2021-01-14 05:47:18 -08:00
// Always log API rolls for abuse detection
2021-01-18 08:50:22 -08:00
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)" , [ originalCommand , returnmsg . errorCode , null ] ) . catch ( e = > {
2021-01-14 05:47:18 -08:00
console . log ( "Failed to insert into database 11" , e ) ;
} ) ;
2021-01-11 00:42:57 -08:00
break ;
} else {
returnText = apiPrefix + "<@" + query . get ( "user" ) + ">" + returnmsg . line1 + "\n" + returnmsg . line2 ;
let spoilerTxt = "" ;
// Determine if spoiler flag was on
if ( query . has ( "s" ) ) {
spoilerTxt = "||" ;
}
// Determine if no details flag was on
if ( query . has ( "nd" ) ) {
returnText += "\nDetails suppressed by nd query." ;
} else {
returnText += "\nDetails:\n" + spoilerTxt + returnmsg . line3 + spoilerTxt ;
}
}
// If the roll was a GM roll, send DMs to all the GMs
if ( query . has ( "gms" ) ) {
// Get all the GM user IDs from the query
const gms = ( query . get ( "gms" ) || "" ) . split ( "," ) ;
if ( gms . length === 0 ) {
// Alert API user that they messed up
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
2021-01-14 05:47:18 -08:00
// Always log API rolls for abuse detection
2021-01-18 08:50:22 -08:00
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)" , [ originalCommand , "NoGMsSent" , null ] ) . catch ( e = > {
2021-01-14 05:47:18 -08:00
console . log ( "Failed to insert into database 12" , e ) ;
} ) ;
2021-01-11 00:46:34 -08:00
break ;
2021-01-11 00:42:57 -08:00
}
// Make a new return line to be sent to the roller
let normalText = apiPrefix + "<@" + query . get ( "user" ) + ">" + returnmsg . line1 + "\nResults have been messaged to the following GMs: " ;
gms . forEach ( e = > {
normalText += "<@" + e + "> " ;
} ) ;
// Send the return message as a DM or normal message depening on if the channel is set
if ( ( query . get ( "channel" ) || "" ) . length > 0 ) {
2021-01-14 05:47:18 -08:00
m = await sendMessage ( query . get ( "channel" ) || "" , normalText ) . catch ( ( ) = > {
2021-01-11 00:42:57 -08:00
request . respond ( { status : Status.InternalServerError , body : "Message 00 failed to send." } ) ;
errorOut = true ;
} ) ;
} else {
2021-01-14 05:47:18 -08:00
m = await sendDirectMessage ( query . get ( "user" ) || "" , normalText ) . catch ( ( ) = > {
2021-01-11 00:42:57 -08:00
request . respond ( { status : Status.InternalServerError , body : "Message 01 failed to send." } ) ;
errorOut = true ;
} ) ;
}
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
gms . forEach ( async e = > {
2021-01-26 23:35:46 -08:00
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob ( [ returnText as BlobPart ] , { "type" : "text" } ) ;
// Update return text
returnText = apiPrefix + "<@" + query . get ( "user" ) + ">" + returnmsg . line1 + "\n" + returnmsg . line2 + "\nFull details have been attached to this messaged as a `.txt` file for verification purposes." ;
// Attempt to DM the GMs and send a warning if it could not DM a GM
await sendDirectMessage ( e , { "content" : returnText , "file" : { "blob" : b , "name" : "rollDetails.txt" } } ) . catch ( async ( ) = > {
const failedSend = "WARNING: <@" + e + "> could not be messaged. If this issue persists, make sure direct messages are allowed from this server."
// Send the return message as a DM or normal message depening on if the channel is set
if ( ( query . get ( "channel" ) || "" ) . length > 0 ) {
m = await sendMessage ( query . get ( "channel" ) || "" , failedSend ) . catch ( ( ) = > {
request . respond ( { status : Status.InternalServerError , body : "Message 10 failed to send." } ) ;
errorOut = true ;
} ) ;
} else {
m = await sendDirectMessage ( query . get ( "user" ) || "" , failedSend ) . catch ( ( ) = > {
request . respond ( { status : Status.InternalServerError , body : "Message 11 failed to send." } ) ;
errorOut = true ;
} ) ;
}
} ) ;
2021-01-11 00:42:57 -08:00
} ) ;
2021-01-14 05:47:18 -08:00
// Always log API rolls for abuse detection
2021-01-18 08:50:22 -08:00
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)" , [ originalCommand , returnText , ( ( typeof m === "object" ) ? m.id : null ) ] ) . catch ( e = > {
2021-01-14 05:47:18 -08:00
console . log ( "Failed to insert into database 13" , e ) ;
} ) ;
2021-01-11 00:42:57 -08:00
// Handle closing the request out
if ( errorOut ) {
break ;
} else {
request . respond ( { status : Status.OK , body : normalText } ) ;
break ;
}
} else {
2021-01-26 23:35:46 -08:00
const newMessage : MessageContent = { } ;
newMessage . content = returnText ;
2021-01-11 00:42:57 -08:00
// When not a GM roll, make sure the message is not too big
if ( returnText . length > 2000 ) {
2021-01-26 23:35:46 -08:00
// If its too big, collapse it into a .txt file and send that instead.
const b = await new Blob ( [ returnText as BlobPart ] , { "type" : "text" } ) ;
// Update return text
returnText = "<@" + query . get ( "user" ) + ">" + returnmsg . line1 + "\n" + returnmsg . line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a `.txt` file for verification purposes." ;
// Set info into the newMessage
newMessage . content = returnText ;
newMessage . file = { "blob" : b , "name" : "rollDetails.txt" } ;
2021-01-11 00:42:57 -08:00
}
// Send the return message as a DM or normal message depening on if the channel is set
if ( ( query . get ( "channel" ) || "" ) . length > 0 ) {
2021-01-26 23:35:46 -08:00
m = await sendMessage ( query . get ( "channel" ) || "" , newMessage ) . catch ( ( ) = > {
2021-01-11 00:42:57 -08:00
request . respond ( { status : Status.InternalServerError , body : "Message 20 failed to send." } ) ;
errorOut = true ;
} ) ;
} else {
2021-01-26 23:35:46 -08:00
m = await sendDirectMessage ( query . get ( "user" ) || "" , newMessage ) . catch ( ( ) = > {
2021-01-11 00:42:57 -08:00
request . respond ( { status : Status.InternalServerError , body : "Message 21 failed to send." } ) ;
errorOut = true ;
} ) ;
}
2021-01-14 05:47:18 -08:00
// If enabled, log rolls so we can verify the bots math
2021-01-18 08:50:22 -08:00
dbClient . execute ( "INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)" , [ originalCommand , returnText , ( ( typeof m === "object" ) ? m.id : null ) ] ) . catch ( e = > {
2021-01-14 05:47:18 -08:00
console . log ( "Failed to insert into database 14" , e ) ;
} ) ;
2021-01-11 00:42:57 -08:00
// Handle closing the request out
if ( errorOut ) {
break ;
} else {
request . respond ( { status : Status.OK , body : returnText } ) ;
break ;
}
}
} catch ( err ) {
// Handle any errors we missed
console . log ( err )
request . respond ( { status : Status.InternalServerError , body : STATUS_TEXT.get ( Status . InternalServerError ) } ) ;
}
} else {
// Alert API user that they messed up
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.BadRequest , body : STATUS_TEXT.get ( Status . BadRequest ) } ) ;
}
break ;
default :
// Alert API user that they messed up
request . respond ( { status : Status.NotFound , body : STATUS_TEXT.get ( Status . NotFound ) } ) ;
break ;
}
break ;
default :
// Alert API user that they messed up
request . respond ( { status : Status.MethodNotAllowed , body : STATUS_TEXT.get ( Status . MethodNotAllowed ) } ) ;
break ;
}
2021-01-18 08:50:22 -08:00
if ( updateRateLimitTime ) {
const apiTimeNow = new Date ( ) . getTime ( ) ;
rateLimitTime . set ( apiUseridStr , apiTimeNow ) ;
}
} else if ( authenticated && rateLimited ) {
// Alert API user that they are doing this too often
request . respond ( { status : Status.TooManyRequests , body : STATUS_TEXT.get ( Status . TooManyRequests ) } ) ;
2021-01-11 00:42:57 -08:00
} else {
// Alert API user that they shouldn't be doing this
request . respond ( { status : Status.Forbidden , body : STATUS_TEXT.get ( Status . Forbidden ) } ) ;
}
}
}