From 4a34596beeee36542b98a7b52dead17b1da59421 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sun, 19 Jun 2022 03:55:33 -0400 Subject: [PATCH] Restructured API code to be more readable (like commands) --- src/api.ts | 813 +++---------------- src/endpoints/_index.ts | 29 + src/endpoints/deletes/apiKeyDelete.ts | 87 ++ src/endpoints/gets/apiChannel.ts | 41 + src/endpoints/gets/apiKey.ts | 56 ++ src/endpoints/gets/apiKeyAdmin.ts | 46 ++ src/endpoints/gets/apiRoll.ts | 286 +++++++ src/endpoints/posts/apiChannelAdd.ts | 40 + src/endpoints/puts/apiChannelManageActive.ts | 47 ++ src/endpoints/puts/apiChannelManageBan.ts | 51 ++ src/endpoints/puts/apiKeyManage.ts | 55 ++ 11 files changed, 849 insertions(+), 702 deletions(-) create mode 100644 src/endpoints/_index.ts create mode 100644 src/endpoints/deletes/apiKeyDelete.ts create mode 100644 src/endpoints/gets/apiChannel.ts create mode 100644 src/endpoints/gets/apiKey.ts create mode 100644 src/endpoints/gets/apiKeyAdmin.ts create mode 100644 src/endpoints/gets/apiRoll.ts create mode 100644 src/endpoints/posts/apiChannelAdd.ts create mode 100644 src/endpoints/puts/apiChannelManageActive.ts create mode 100644 src/endpoints/puts/apiChannelManageBan.ts create mode 100644 src/endpoints/puts/apiKeyManage.ts diff --git a/src/api.ts b/src/api.ts index aa41a5e..6cba381 100644 --- a/src/api.ts +++ b/src/api.ts @@ -4,29 +4,17 @@ * December 21, 2020 */ +import config from '../config.ts'; import { - // Discordeno deps - cache, - CreateMessage, // Log4Deno deps log, LT, - // nanoid deps - nanoid, - // Discordeno deps - sendDirectMessage, - sendMessage, // httpd deps Status, STATUS_TEXT, } from '../deps.ts'; - -import { RollModifiers } from './mod.d.ts'; -import { dbClient, queries } from './db.ts'; -import solver from './solver/_index.ts'; -import { generateApiDeleteEmail, generateApiKeyEmail, generateDMFailed } from './commandUtils.ts'; - -import config from '../config.ts'; +import { dbClient } from './db.ts'; +import endpoints from './endpoints/_index.ts'; // start(databaseClient) returns nothing // start initializes and runs the entire API for the bot @@ -90,639 +78,7 @@ const start = async (): Promise => { } } - if (authenticated && !rateLimited) { - // Get path and query as a string - const [path, tempQ] = request.url.split('?'); - - // Turn the query into a map (if it exists) - const query = new Map(); - if (tempQ !== undefined) { - tempQ.split('&').forEach((e: string) => { - log(LT.LOG, `Breaking down request query: ${request} ${e}`); - const [option, params] = e.split('='); - query.set(option.toLowerCase(), params); - }); - } - - // Handle the request - switch (request.method) { - case 'GET': - 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') || '0')) { - // 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(?,?)', [apiUserid, newKey]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-0`, { status: Status.InternalServerError })); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(JSON.stringify({ 'key': newKey, 'userid': query.get('user') }), { status: Status.OK })); - break; - } - } else { - // Only allow the db admin to use this API - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - } - break; - case '/api/channel': - case '/api/channel/': - if (query.has('user') && ((query.get('user') || '').length > 0)) { - if (apiUserid === BigInt(query.get('user') || '0')) { - // 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 = ?', [apiUserid]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-1`, { status: 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 - requestEvent.respondWith(new Response(returnChannels, { status: Status.OK })); - break; - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: 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)) - ) { - if (query.has('n') && query.has('m')) { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - break; - } - - // Check if user is authenticated to use this endpoint - let authorized = false; - let hideWarn = false; - - // Check if the db has the requested userid/channelid combo, and that the requested userid matches the userid linked with the api key - const dbChannelQuery = await dbClient.query('SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?', [apiUserid, BigInt(query.get('channel') || '0')]); - if (dbChannelQuery.length === 1 && (apiUserid === BigInt(query.get('user') || '0')) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) { - // Get the guild from the channel and make sure user is in said guild - const guild = cache.channels.get(BigInt(query.get('channel') || ''))?.guild; - if (guild && guild.members.get(BigInt(query.get('user') || ''))?.id) { - const dbGuildQuery = await dbClient.query('SELECT active, banned, hidewarn FROM allowed_guilds WHERE guildid = ? AND channelid = ?', [ - guild.id, - BigInt(query.get('channel') || '0'), - ]); - - // Make sure guild allows API rolls - if (dbGuildQuery.length === 1 && dbGuildQuery[0].active && !dbGuildQuery[0].banned) { - authorized = true; - hideWarn = dbGuildQuery[0].hidewarn; - } - } - } - - 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') || ''; - const originalCommand = query.get('rollstr'); - - if (rollCmd.length === 0) { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - - // Always log API rolls for abuse detection - dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'EmptyInput', null]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } - - if (query.has('o') && (query.get('o')?.toLowerCase() !== 'd' && query.get('o')?.toLowerCase() !== 'a')) { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - - // Always log API rolls for abuse detection - dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'BadOrder', null]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } - - // 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.substring(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, ' '); - - const modifiers: RollModifiers = { - noDetails: false, - superNoDetails: false, - spoiler: '', - maxRoll: query.has('m'), - nominalRoll: query.has('n'), - gmRoll: false, - gms: [], - order: query.has('o') ? (query.get('o')?.toLowerCase() || '') : '', - valid: true, - count: query.has('c'), - }; - - // Parse the roll and get the return text - const returnmsg = solver.parseRoll(rollCmd, modifiers); - - // Alert users why this message just appeared and how they can report abues pf this feature - const apiPrefix = hideWarn - ? '' - : `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`; - let m, returnText = ''; - - // Handle sending the error message to whoever called the api - if (returnmsg.error) { - requestEvent.respondWith(new Response(returnmsg.errorMsg, { status: Status.InternalServerError })); - - // Always log API rolls for abuse detection - dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, returnmsg.errorCode, null]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - 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('snd')) { - 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 - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - - // Always log API rolls for abuse detection - dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'NoGMsSent', null]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } - - // 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) => { - log(LT.LOG, `Appending GM ${e} to roll text`); - 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) { - // todo: embedify - m = await sendMessage(BigInt(query.get('channel') || ''), normalText).catch(() => { - requestEvent.respondWith(new Response('Message 00 failed to send.', { status: Status.InternalServerError })); - errorOut = true; - }); - } else { - // todo: embedify - m = await sendDirectMessage(BigInt(query.get('user') || ''), normalText).catch(() => { - requestEvent.respondWith(new Response('Message 01 failed to send.', { status: Status.InternalServerError })); - errorOut = true; - }); - } - - const newMessage: CreateMessage = {}; - // If its too big, collapse it into a .txt file and send that instead. - const b = await new Blob([returnText as BlobPart], { 'type': 'text' }); - - if (b.size > 8388290) { - // Update return text - newMessage.content = `${apiPrefix}<@${ - query.get('user') - }>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; - } else { - // Update return text - newMessage.content = `${apiPrefix}<@${ - query.get('user') - }>${returnmsg.line1}\n${returnmsg.line2}\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.`; - newMessage.file = { 'blob': b, 'name': 'rollDetails.txt' }; - } - - // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged - gms.forEach(async (e) => { - log(LT.LOG, `Messaging GM ${e} roll results`); - // Attempt to DM the GMs and send a warning if it could not DM a GM - await sendDirectMessage(BigInt(e), newMessage).catch(async () => { - const failedSend = generateDMFailed(e); - // 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(BigInt(query.get('channel') || ''), failedSend).catch(() => { - requestEvent.respondWith(new Response('Message failed to send.', { status: Status.InternalServerError })); - errorOut = true; - }); - } else { - m = await sendDirectMessage(BigInt(query.get('user') || ''), failedSend).catch(() => { - requestEvent.respondWith(new Response('Message failed to send.', { status: Status.InternalServerError })); - errorOut = true; - }); - } - }); - }); - - // Always log API rolls for abuse detection - dbClient.execute(queries.insertRollLogCmd(1, 0), [originalCommand, returnText, (typeof m === 'object') ? m.id : null]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - - // Handle closing the request out - if (errorOut) { - break; - } else { - requestEvent.respondWith(new Response(normalText, { status: Status.OK })); - break; - } - } else { - // todo: embedify - const newMessage: CreateMessage = {}; - newMessage.content = returnText; - - // When not a GM roll, make sure the message is not too big - if (returnText.length > 2000) { - // If its too big, collapse it into a .txt file and send that instead. - const b = await new Blob([returnText as BlobPart], { 'type': 'text' }); - - if (b.size > 8388290) { - // Update return text - newMessage.content = `${apiPrefix}<@${ - query.get('user') - }>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; - } else { - // Update return text - newMessage.content = `${apiPrefix}<@${ - 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.`; - newMessage.file = { 'blob': b, 'name': 'rollDetails.txt' }; - } - } - - // 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(BigInt(query.get('channel') || ''), newMessage).catch(() => { - requestEvent.respondWith(new Response('Message 20 failed to send.', { status: Status.InternalServerError })); - errorOut = true; - }); - } else { - m = await sendDirectMessage(BigInt(query.get('user') || ''), newMessage).catch(() => { - requestEvent.respondWith(new Response('Message 21 failed to send.', { status: Status.InternalServerError })); - errorOut = true; - }); - } - - // If enabled, log rolls so we can verify the bots math - dbClient.execute(queries.insertRollLogCmd(1, 0), [originalCommand, returnText, (typeof m === 'object') ? m.id : null]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - - // Handle closing the request out - if (errorOut) { - break; - } else { - requestEvent.respondWith(new Response(returnText, { status: Status.OK })); - break; - } - } - } catch (err) { - // Handle any errors we missed - log(LT.ERROR, `Unhandled Error: ${JSON.stringify(err)}`); - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); - break; - } - break; - case 'POST': - switch (path.toLowerCase()) { - 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') || '0')) { - // 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(?,?)', [apiUserid, BigInt(query.get('channel') || '0')]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-2`, { status: Status.InternalServerError })); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); - break; - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); - break; - } - break; - case 'PUT': - switch (path.toLowerCase()) { - 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') || '0')) { - // 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; - } - - // Execute the DB modification - await dbClient.execute('UPDATE all_keys SET ?? = ? WHERE userid = ?', [key, value, apiUserid]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-3`, { status: Status.InternalServerError })); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); - break; - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: 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') || '0')) { - // 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, apiUserid, BigInt(query.get('channel') || '0')]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-4`, { status: Status.InternalServerError })); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); - break; - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: 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') || '0')) { - // 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, apiUserid, BigInt(query.get('channel') || '0')]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-5`, { status: Status.InternalServerError })); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); - break; - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); - break; - } - break; - case 'DELETE': - switch (path.toLowerCase()) { - case '/api/key/delete': - case '/api/key/delete/': - if (query.has('user') && ((query.get('user') || '').length > 0) && query.has('email') && ((query.get('email') || '').length > 0)) { - if (apiUserid === BigInt(query.get('user') || '0') && apiUserEmail === query.get('email')) { - if (query.has('code') && ((query.get('code') || '').length > 0)) { - if ((query.get('code') || '') === apiUserDelCode) { - // User has recieved their delete code and we need to delete the account now - let erroredOut = false; - - await dbClient.execute('DELETE FROM allowed_channels WHERE userid = ?', [apiUserid]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-6`, { status: Status.InternalServerError })); - erroredOut = true; - }); - if (erroredOut) { - break; - } - - await dbClient.execute('DELETE FROM all_keys WHERE userid = ?', [apiUserid]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-7`, { status: Status.InternalServerError })); - erroredOut = true; - }); - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); - break; - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // User does not have their delete code yet, so we need to generate one and email it to them - const deleteCode = await nanoid(10); - - let erroredOut = false; - - // Execute the DB modification - await dbClient.execute('UPDATE all_keys SET deleteCode = ? WHERE userid = ?', [deleteCode, apiUserid]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-8`, { status: Status.InternalServerError })); - erroredOut = true; - }); - if (erroredOut) { - break; - } - - // "Send" the email - await sendMessage(config.api.email, generateApiDeleteEmail(apiUserEmail, deleteCode)).catch(() => { - requestEvent.respondWith(new Response('Message 30 failed to send.', { status: Status.InternalServerError })); - erroredOut = true; - }); - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.FailedDependency), { status: Status.FailedDependency })); - break; - } - } - } else { - // Alert API user that they shouldn't be doing this - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); - } - } else { - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); - break; - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.MethodNotAllowed), { status: Status.MethodNotAllowed })); - break; - } - - if (updateRateLimitTime) { - const apiTimeNow = new Date().getTime(); - rateLimitTime.set(apiUseridStr, apiTimeNow); - } - } else if (!authenticated && !rateLimited) { + if (!rateLimited) { // Get path and query as a string const [path, tempQ] = request.url.split('?'); @@ -736,61 +92,114 @@ const start = async (): Promise => { }); } - // Handle the request - switch (request.method) { - case 'GET': - switch (path.toLowerCase()) { - case '/api/key': - case '/api/key/': - if ((query.has('user') && ((query.get('user') || '').length > 0)) && (query.has('email') && ((query.get('email') || '').length > 0))) { - // 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,email) values(?,?,?)', [BigInt(query.get('user') || '0'), newKey, (query.get('email') || '').toLowerCase()]).catch( - (e) => { - log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-9`, { status: Status.InternalServerError })); - erroredOut = true; - }, - ); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } - - // "Send" the email - await sendMessage(config.api.email, generateApiKeyEmail(query.get('email') || 'no email', newKey)).catch(() => { - requestEvent.respondWith(new Response('Message 31 failed to send.', { status: Status.InternalServerError })); - erroredOut = true; - }); - - if (erroredOut) { - break; - } else { - // Send API key as response - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); - break; - } - } else { + if (authenticated) { + // Handle the authenticated request + switch (request.method) { + case 'GET': + switch (path.toLowerCase()) { + case '/api/key': + case '/api/key/': + endpoints.get.apiKeyAdmin(requestEvent, query, apiUserid); + break; + case '/api/channel': + case '/api/channel/': + endpoints.get.apiChannel(requestEvent, query, apiUserid); + break; + case '/api/roll': + case '/api/roll/': + endpoints.get.apiRoll(requestEvent, query, apiUserid); + break; + default: // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); - break; - } - break; - default: - // Alert API user that they messed up - requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.MethodNotAllowed), { status: Status.MethodNotAllowed })); - break; + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + case 'POST': + switch (path.toLowerCase()) { + case '/api/channel/add': + case '/api/channel/add/': + endpoints.post.apiChannelAdd(requestEvent, query, apiUserid); + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + case 'PUT': + switch (path.toLowerCase()) { + 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/': + endpoints.put.apiKeyManage(requestEvent, query, apiUserid, path); + break; + case '/api/channel/ban': + case '/api/channel/ban/': + case '/api/channel/unban': + case '/api/channel/unban/': + endpoints.put.apiChannelManageBan(requestEvent, query, apiUserid, path); + break; + case '/api/channel/activate': + case '/api/channel/activate/': + case '/api/channel/deactivate': + case '/api/channel/deactivate/': + endpoints.put.apiChannelManageActive(requestEvent, query, apiUserid, path); + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + case 'DELETE': + switch (path.toLowerCase()) { + case '/api/key/delete': + case '/api/key/delete/': + endpoints.delete.apiKeyDelete(requestEvent, query, apiUserid, apiUserEmail, apiUserDelCode); + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.MethodNotAllowed), { status: Status.MethodNotAllowed })); + break; + } + + // Update rate limit details + if (updateRateLimitTime) { + const apiTimeNow = new Date().getTime(); + rateLimitTime.set(apiUseridStr, apiTimeNow); + } + } else if (!authenticated) { + // Handle the unathenticated request + switch (request.method) { + case 'GET': + switch (path.toLowerCase()) { + case '/api/key': + case '/api/key/': + endpoints.get.apiKey(requestEvent, query); + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.MethodNotAllowed), { status: Status.MethodNotAllowed })); + break; + } } } else if (authenticated && rateLimited) { // Alert API user that they are doing this too often diff --git a/src/endpoints/_index.ts b/src/endpoints/_index.ts new file mode 100644 index 0000000..bbb7ab7 --- /dev/null +++ b/src/endpoints/_index.ts @@ -0,0 +1,29 @@ +import { apiKeyDelete } from './deletes/apiKeyDelete.ts'; +import { apiKey } from './gets/apiKey.ts'; +import { apiRoll } from './gets/apiRoll.ts'; +import { apiKeyAdmin } from './gets/apiKeyAdmin.ts'; +import { apiChannel } from './gets/apiChannel.ts'; +import { apiChannelAdd } from './posts/apiChannelAdd.ts'; +import { apiKeyManage } from './puts/apiKeyManage.ts'; +import { apiChannelManageBan } from './puts/apiChannelManageBan.ts'; +import { apiChannelManageActive } from './puts/apiChannelManageActive.ts'; + +export default { + delete: { + apiKeyDelete + }, + get: { + apiKey, + apiRoll, + apiKeyAdmin, + apiChannel, + }, + post: { + apiChannelAdd, + }, + put: { + apiKeyManage, + apiChannelManageBan, + apiChannelManageActive, + }, +}; diff --git a/src/endpoints/deletes/apiKeyDelete.ts b/src/endpoints/deletes/apiKeyDelete.ts new file mode 100644 index 0000000..f930ba6 --- /dev/null +++ b/src/endpoints/deletes/apiKeyDelete.ts @@ -0,0 +1,87 @@ +import config from '../../../config.ts'; +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // nanoid deps + nanoid, + // Discordeno deps + sendMessage, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; +import { generateApiDeleteEmail } from '../../commandUtils.ts'; + +export const apiKeyDelete = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt, apiUserEmail: string, apiUserDelCode: string) => { + if (query.has('user') && ((query.get('user') || '').length > 0) && query.has('email') && ((query.get('email') || '').length > 0)) { + if (apiUserid === BigInt(query.get('user') || '0') && apiUserEmail === query.get('email')) { + if (query.has('code') && ((query.get('code') || '').length > 0)) { + if ((query.get('code') || '') === apiUserDelCode) { + // User has recieved their delete code and we need to delete the account now + let erroredOut = false; + + await dbClient.execute('DELETE FROM allowed_channels WHERE userid = ?', [apiUserid]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-6`, { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + return; + } + + await dbClient.execute('DELETE FROM all_keys WHERE userid = ?', [apiUserid]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-7`, { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + return; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // User does not have their delete code yet, so we need to generate one and email it to them + const deleteCode = await nanoid(10); + + let erroredOut = false; + + // Execute the DB modification + await dbClient.execute('UPDATE all_keys SET deleteCode = ? WHERE userid = ?', [deleteCode, apiUserid]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-8`, { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + return; + } + + // "Send" the email + await sendMessage(config.api.email, generateApiDeleteEmail(apiUserEmail, deleteCode)).catch(() => { + requestEvent.respondWith(new Response('Message 30 failed to send.', { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.FailedDependency), { status: Status.FailedDependency })); + return; + } + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/gets/apiChannel.ts b/src/endpoints/gets/apiChannel.ts new file mode 100644 index 0000000..fd0b56b --- /dev/null +++ b/src/endpoints/gets/apiChannel.ts @@ -0,0 +1,41 @@ +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; + +export const apiChannel = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt) => { + if (query.has('user') && ((query.get('user') || '').length > 0)) { + if (apiUserid === BigInt(query.get('user') || '0')) { + // 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 = ?', [apiUserid]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-1`, { status: Status.InternalServerError })); + erroredOut = true; + }); + + if (erroredOut) { + return; + } 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 + requestEvent.respondWith(new Response(returnChannels, { status: Status.OK })); + return; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/gets/apiKey.ts b/src/endpoints/gets/apiKey.ts new file mode 100644 index 0000000..825fd53 --- /dev/null +++ b/src/endpoints/gets/apiKey.ts @@ -0,0 +1,56 @@ +import config from '../../../config.ts'; +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // nanoid deps + nanoid, + // Discordeno deps + sendMessage, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; +import { generateApiKeyEmail } from '../../commandUtils.ts'; + +export const apiKey = async (requestEvent: Deno.RequestEvent, query: Map) => { + if ((query.has('user') && ((query.get('user') || '').length > 0)) && (query.has('email') && ((query.get('email') || '').length > 0))) { + // 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,email) values(?,?,?)', [BigInt(query.get('user') || '0'), newKey, (query.get('email') || '').toLowerCase()]).catch( + (e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-9`, { status: Status.InternalServerError })); + erroredOut = true; + }, + ); + + // Exit this case now if catch errored + if (erroredOut) { + return; + } + + // "Send" the email + await sendMessage(config.api.email, generateApiKeyEmail(query.get('email') || 'no email', newKey)).catch(() => { + requestEvent.respondWith(new Response('Message 31 failed to send.', { status: Status.InternalServerError })); + erroredOut = true; + }); + + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + return; + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/gets/apiKeyAdmin.ts b/src/endpoints/gets/apiKeyAdmin.ts new file mode 100644 index 0000000..ffd3ca0 --- /dev/null +++ b/src/endpoints/gets/apiKeyAdmin.ts @@ -0,0 +1,46 @@ +import config from '../../../config.ts'; +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // nanoid deps + nanoid, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; + +export const apiKeyAdmin = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt) => { + 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') || '0')) { + // 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(?,?)', [apiUserid, newKey]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-0`, { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(JSON.stringify({ 'key': newKey, 'userid': query.get('user') }), { status: Status.OK })); + return; + } + } else { + // Only allow the db admin to use this API + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/gets/apiRoll.ts b/src/endpoints/gets/apiRoll.ts new file mode 100644 index 0000000..2aaac62 --- /dev/null +++ b/src/endpoints/gets/apiRoll.ts @@ -0,0 +1,286 @@ +import config from '../../../config.ts'; +import { dbClient, queries } from '../../db.ts'; +import { + // Discordeno deps + cache, + CreateMessage, + // Log4Deno deps + log, + LT, + // Discordeno deps + sendDirectMessage, + sendMessage, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; +import { RollModifiers } from '../../mod.d.ts'; +import solver from '../../solver/_index.ts'; +import { generateDMFailed } from '../../commandUtils.ts'; + +export const apiRoll = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt) => { + // 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)) + ) { + if (query.has('n') && query.has('m')) { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + return; + } + + // Check if user is authenticated to use this endpoint + let authorized = false; + let hideWarn = false; + + // Check if the db has the requested userid/channelid combo, and that the requested userid matches the userid linked with the api key + const dbChannelQuery = await dbClient.query('SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?', [apiUserid, BigInt(query.get('channel') || '0')]); + if (dbChannelQuery.length === 1 && (apiUserid === BigInt(query.get('user') || '0')) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) { + // Get the guild from the channel and make sure user is in said guild + const guild = cache.channels.get(BigInt(query.get('channel') || ''))?.guild; + if (guild && guild.members.get(BigInt(query.get('user') || ''))?.id) { + const dbGuildQuery = await dbClient.query('SELECT active, banned, hidewarn FROM allowed_guilds WHERE guildid = ? AND channelid = ?', [ + guild.id, + BigInt(query.get('channel') || '0'), + ]); + + // Make sure guild allows API rolls + if (dbGuildQuery.length === 1 && dbGuildQuery[0].active && !dbGuildQuery[0].banned) { + authorized = true; + hideWarn = dbGuildQuery[0].hidewarn; + } + } + } + + 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') || ''; + const originalCommand = query.get('rollstr'); + + if (rollCmd.length === 0) { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + + // Always log API rolls for abuse detection + dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'EmptyInput', null]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + return; + } + + if (query.has('o') && (query.get('o')?.toLowerCase() !== 'd' && query.get('o')?.toLowerCase() !== 'a')) { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + + // Always log API rolls for abuse detection + dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'BadOrder', null]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + return; + } + + // 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.substring(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, ' '); + + const modifiers: RollModifiers = { + noDetails: false, + superNoDetails: false, + spoiler: '', + maxRoll: query.has('m'), + nominalRoll: query.has('n'), + gmRoll: false, + gms: [], + order: query.has('o') ? (query.get('o')?.toLowerCase() || '') : '', + valid: true, + count: query.has('c'), + }; + + // Parse the roll and get the return text + const returnmsg = solver.parseRoll(rollCmd, modifiers); + + // Alert users why this message just appeared and how they can report abues pf this feature + const apiPrefix = hideWarn + ? '' + : `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`; + let m, returnText = ''; + + // Handle sending the error message to whoever called the api + if (returnmsg.error) { + requestEvent.respondWith(new Response(returnmsg.errorMsg, { status: Status.InternalServerError })); + + // Always log API rolls for abuse detection + dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, returnmsg.errorCode, null]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + return; + } 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('snd')) { + 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 + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + + // Always log API rolls for abuse detection + dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'NoGMsSent', null]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + return; + } + + // 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) => { + log(LT.LOG, `Appending GM ${e} to roll text`); + 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) { + // todo: embedify + m = await sendMessage(BigInt(query.get('channel') || ''), normalText).catch(() => { + requestEvent.respondWith(new Response('Message 00 failed to send.', { status: Status.InternalServerError })); + errorOut = true; + }); + } else { + // todo: embedify + m = await sendDirectMessage(BigInt(query.get('user') || ''), normalText).catch(() => { + requestEvent.respondWith(new Response('Message 01 failed to send.', { status: Status.InternalServerError })); + errorOut = true; + }); + } + + const newMessage: CreateMessage = {}; + // If its too big, collapse it into a .txt file and send that instead. + const b = await new Blob([returnText as BlobPart], { 'type': 'text' }); + + if (b.size > 8388290) { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get('user') + }>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; + } else { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get('user') + }>${returnmsg.line1}\n${returnmsg.line2}\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.`; + newMessage.file = { 'blob': b, 'name': 'rollDetails.txt' }; + } + + // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged + gms.forEach(async (e) => { + log(LT.LOG, `Messaging GM ${e} roll results`); + // Attempt to DM the GMs and send a warning if it could not DM a GM + await sendDirectMessage(BigInt(e), newMessage).catch(async () => { + const failedSend = generateDMFailed(e); + // 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(BigInt(query.get('channel') || ''), failedSend).catch(() => { + requestEvent.respondWith(new Response('Message failed to send.', { status: Status.InternalServerError })); + errorOut = true; + }); + } else { + m = await sendDirectMessage(BigInt(query.get('user') || ''), failedSend).catch(() => { + requestEvent.respondWith(new Response('Message failed to send.', { status: Status.InternalServerError })); + errorOut = true; + }); + } + }); + }); + + // Always log API rolls for abuse detection + dbClient.execute(queries.insertRollLogCmd(1, 0), [originalCommand, returnText, (typeof m === 'object') ? m.id : null]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + + // Handle closing the request out + if (errorOut) { + return; + } else { + requestEvent.respondWith(new Response(normalText, { status: Status.OK })); + return; + } + } else { + // todo: embedify + const newMessage: CreateMessage = {}; + newMessage.content = returnText; + + // When not a GM roll, make sure the message is not too big + if (returnText.length > 2000) { + // If its too big, collapse it into a .txt file and send that instead. + const b = await new Blob([returnText as BlobPart], { 'type': 'text' }); + + if (b.size > 8388290) { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get('user') + }>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; + } else { + // Update return text + newMessage.content = `${apiPrefix}<@${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.`; + newMessage.file = { 'blob': b, 'name': 'rollDetails.txt' }; + } + } + + // 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(BigInt(query.get('channel') || ''), newMessage).catch(() => { + requestEvent.respondWith(new Response('Message 20 failed to send.', { status: Status.InternalServerError })); + errorOut = true; + }); + } else { + m = await sendDirectMessage(BigInt(query.get('user') || ''), newMessage).catch(() => { + requestEvent.respondWith(new Response('Message 21 failed to send.', { status: Status.InternalServerError })); + errorOut = true; + }); + } + + // If enabled, log rolls so we can verify the bots math + dbClient.execute(queries.insertRollLogCmd(1, 0), [originalCommand, returnText, (typeof m === 'object') ? m.id : null]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + + // Handle closing the request out + if (errorOut) { + return; + } else { + requestEvent.respondWith(new Response(returnText, { status: Status.OK })); + return; + } + } + } catch (err) { + // Handle any errors we missed + log(LT.ERROR, `Unhandled Error: ${JSON.stringify(err)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/posts/apiChannelAdd.ts b/src/endpoints/posts/apiChannelAdd.ts new file mode 100644 index 0000000..654111d --- /dev/null +++ b/src/endpoints/posts/apiChannelAdd.ts @@ -0,0 +1,40 @@ +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; + +export const apiChannelAdd = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt) => { + if ((query.has('user') && ((query.get('user') || '').length > 0)) && (query.has('channel') && ((query.get('channel') || '').length > 0))) { + if (apiUserid === BigInt(query.get('user') || '0')) { + // 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(?,?)', [apiUserid, BigInt(query.get('channel') || '0')]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-2`, { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + return; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/puts/apiChannelManageActive.ts b/src/endpoints/puts/apiChannelManageActive.ts new file mode 100644 index 0000000..beed4bb --- /dev/null +++ b/src/endpoints/puts/apiChannelManageActive.ts @@ -0,0 +1,47 @@ +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; + +export const apiChannelManageActive = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt, path: string) => { + if ((query.has('channel') && ((query.get('channel') || '').length > 0)) && (query.has('user') && ((query.get('user') || '').length > 0))) { + if (apiUserid === BigInt(query.get('user') || '0')) { + // 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, apiUserid, BigInt(query.get('channel') || '0')]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-5`, { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + return; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/puts/apiChannelManageBan.ts b/src/endpoints/puts/apiChannelManageBan.ts new file mode 100644 index 0000000..eeb88bb --- /dev/null +++ b/src/endpoints/puts/apiChannelManageBan.ts @@ -0,0 +1,51 @@ +import config from '../../../config.ts'; +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; + +export const apiChannelManageBan = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt, path: string) => { + 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') || '0')) { + // 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, apiUserid, BigInt(query.get('channel') || '0')]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-4`, { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + return; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +}; diff --git a/src/endpoints/puts/apiKeyManage.ts b/src/endpoints/puts/apiKeyManage.ts new file mode 100644 index 0000000..f6c337d --- /dev/null +++ b/src/endpoints/puts/apiKeyManage.ts @@ -0,0 +1,55 @@ +import config from '../../../config.ts'; +import { dbClient } from '../../db.ts'; +import { + // Log4Deno deps + log, + LT, + // httpd deps + Status, + STATUS_TEXT, +} from '../../../deps.ts'; + +export const apiKeyManage = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt, path: string) => { + 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') || '0')) { + // 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; + } + + // Execute the DB modification + await dbClient.execute('UPDATE all_keys SET ?? = ? WHERE userid = ?', [key, value, apiUserid]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(`${STATUS_TEXT.get(Status.InternalServerError)}-3`, { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + return; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + return; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } +};