diff --git a/src/api.ts b/src/api.ts index ac74fe6..de427c5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -33,7 +33,7 @@ const start = async (): Promise => { const httpConn = Deno.serveHttp(conn); for await (const requestEvent of httpConn) { const request = requestEvent.request; - log(LT.LOG, `Handling request: ${JSON.stringify(request)}`); + log(LT.LOG, `Handling request: ${JSON.stringify(request.headers)} | ${JSON.stringify(request.method)} | ${JSON.stringify(request.url)}`); // Check if user is authenticated to be using this API let authenticated = false; let rateLimited = false; @@ -80,13 +80,14 @@ const start = async (): Promise => { if (!rateLimited) { // Get path and query as a string - const [path, tempQ] = request.url.split('?'); + const [urlPath, tempQ] = request.url.split('?'); + const path = urlPath.split('api')[1]; // Turn the query into a map (if it exists) const query = new Map(); if (tempQ !== undefined) { tempQ.split('&').forEach((e: string) => { - log(LT.LOG, `Parsing request query #2 ${request} ${e}`); + log(LT.LOG, `Parsing request query ${request} ${e}`); const [option, params] = e.split('='); query.set(option.toLowerCase(), params); }); @@ -97,16 +98,16 @@ const start = async (): Promise => { switch (request.method) { case 'GET': switch (path.toLowerCase()) { - case '/api/key': - case '/api/key/': + case '/key': + case '/key/': endpoints.get.apiKeyAdmin(requestEvent, query, apiUserid); break; - case '/api/channel': - case '/api/channel/': + case '/channel': + case '/channel/': endpoints.get.apiChannel(requestEvent, query, apiUserid); break; - case '/api/roll': - case '/api/roll/': + case '/roll': + case '/roll/': endpoints.get.apiRoll(requestEvent, query, apiUserid); break; default: @@ -117,8 +118,8 @@ const start = async (): Promise => { break; case 'POST': switch (path.toLowerCase()) { - case '/api/channel/add': - case '/api/channel/add/': + case '/channel/add': + case '/channel/add/': endpoints.post.apiChannelAdd(requestEvent, query, apiUserid); break; default: @@ -129,26 +130,26 @@ const start = async (): Promise => { 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/': + case '/key/ban': + case '/key/ban/': + case '/key/unban': + case '/key/unban/': + case '/key/activate': + case '/key/activate/': + case '/key/deactivate': + case '/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/': + case '/channel/ban': + case '/channel/ban/': + case '/channel/unban': + case '/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/': + case '/channel/activate': + case '/channel/activate/': + case '/channel/deactivate': + case '/channel/deactivate/': endpoints.put.apiChannelManageActive(requestEvent, query, apiUserid, path); break; default: @@ -159,8 +160,8 @@ const start = async (): Promise => { break; case 'DELETE': switch (path.toLowerCase()) { - case '/api/key/delete': - case '/api/key/delete/': + case '/key/delete': + case '/key/delete/': endpoints.delete.apiKeyDelete(requestEvent, query, apiUserid, apiUserEmail, apiUserDelCode); break; default: @@ -185,8 +186,8 @@ const start = async (): Promise => { switch (request.method) { case 'GET': switch (path.toLowerCase()) { - case '/api/key': - case '/api/key/': + case '/key': + case '/key/': endpoints.get.apiKey(requestEvent, query); break; default: diff --git a/src/commands/roll.ts b/src/commands/roll.ts index 49a8615..dddfb63 100644 --- a/src/commands/roll.ts +++ b/src/commands/roll.ts @@ -52,9 +52,10 @@ export const roll = async (message: DiscordenoMessage, args: string[], command: queueRoll( { apiRoll: false, - dd: { m, message, originalCommand }, + dd: { m, message }, rollCmd, modifiers, + originalCommand, }, ); } catch (e) { diff --git a/src/commands/roll/getModifiers.ts b/src/commands/roll/getModifiers.ts index 5596a4f..db165ae 100644 --- a/src/commands/roll/getModifiers.ts +++ b/src/commands/roll/getModifiers.ts @@ -24,6 +24,7 @@ export const getModifiers = (m: DiscordenoMessage, args: string[], command: stri order: '', valid: false, count: false, + apiWarn: '', }; // Check if any of the args are command flags and pull those out into the modifiers object diff --git a/src/endpoints/gets/apiRoll.ts b/src/endpoints/gets/apiRoll.ts index 0e219c7..37658ea 100644 --- a/src/endpoints/gets/apiRoll.ts +++ b/src/endpoints/gets/apiRoll.ts @@ -3,20 +3,17 @@ 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'; +import { RollModifiers, QueuedRoll } from '../../mod.d.ts'; +import { queueRoll } from '../../solver/rollQueue.ts'; + +const apiWarning = `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}>`; export const apiRoll = async (requestEvent: Deno.RequestEvent, query: Map, apiUserid: BigInt) => { // Make sure query contains all the needed parts @@ -56,8 +53,6 @@ export const apiRoll = async (requestEvent: Deno.RequestEvent, query: Map\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; - } - } + await queueRoll( { + apiRoll: true, + api: { requestEvent, channelId: BigInt(query.get('channel') || '0'), userId: BigInt(query.get('user') || '')}, + rollCmd, + modifiers, + originalCommand, + }); } catch (err) { // Handle any errors we missed log(LT.ERROR, `Unhandled Error: ${JSON.stringify(err)}`); @@ -281,7 +111,10 @@ export const apiRoll = async (requestEvent: Deno.RequestEvent, query: Map { const workerTimeout = setTimeout(async () => { rollWorker.terminate(); currentWorkers--; - rq.dd.m.edit({ - embeds: [ - (await generateRollEmbed( - rq.dd.message.authorId, - { - error: true, - errorCode: 'TooComplex', - errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts', - }, - {}, - )).embed, - ], - }); + if (rq.apiRoll) { + rq.api.requestEvent.respondWith(new Response( + 'Roll took too long to process, try breaking roll down into simpler parts', + { status: Status.RequestTimeout, statusText: STATUS_TEXT.get(Status.RequestTimeout) } + )); + } else { + rq.dd.m.edit({ + embeds: [ + (await generateRollEmbed( + rq.dd.message.authorId, + { + error: true, + errorCode: 'TooComplex', + errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts', + }, + {}, + )).embed, + ], + }); + } }, config.limits.workerTimeout); rollWorker.postMessage({ @@ -51,64 +62,125 @@ const handleRollWorker = async (rq: QueuedRoll) => { }); rollWorker.addEventListener('message', async (workerMessage) => { + let apiErroredOut = false; try { currentWorkers--; clearTimeout(workerTimeout); const returnmsg = workerMessage.data; - const pubEmbedDetails = await generateRollEmbed(rq.dd.message.authorId, returnmsg, rq.modifiers); - const gmEmbedDetails = await generateRollEmbed(rq.dd.message.authorId, returnmsg, gmModifiers); + const pubEmbedDetails = await generateRollEmbed(rq.apiRoll ? rq.api.userId : rq.dd.message.authorId, returnmsg, rq.modifiers); + const gmEmbedDetails = await generateRollEmbed(rq.apiRoll ? rq.api.userId : rq.dd.message.authorId, returnmsg, gmModifiers); const countEmbed = generateCountDetailsEmbed(returnmsg.counts); // If there was an error, report it to the user in hopes that they can determine what they did wrong if (returnmsg.error) { - rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] }); + if (rq.apiRoll) { + rq.api.requestEvent.respondWith(new Response( + returnmsg.errorMsg, + { status: Status.InternalServerError, statusText: STATUS_TEXT.get(Status.InternalServerError) } + )); + } else { + rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] }); + } - if (DEVMODE && config.logRolls) { + if (rq.apiRoll || DEVMODE && config.logRolls) { // If enabled, log rolls so we can see what went wrong - dbClient.execute(queries.insertRollLogCmd(0, 1), [rq.dd.originalCommand, returnmsg.errorCode, rq.dd.m.id]).catch((e) => { + dbClient.execute(queries.insertRollLogCmd(rq.apiRoll ? 1 : 0, 1), [rq.originalCommand, returnmsg.errorCode, rq.apiRoll ? null : rq.dd.m.id]).catch((e) => { log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e)}`); }); } } else { + let n: DiscordenoMessage | void; // Determine if we are to send a GM roll or a normal roll if (rq.modifiers.gmRoll) { - // Send the public embed to correct channel - rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] }); - - // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged - rq.modifiers.gms.forEach(async (gm) => { - log(LT.LOG, `Messaging GM ${gm}`); - // Attempt to DM the GM and send a warning if it could not DM a GM - await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), { - embeds: rq.modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed], - }).then(async () => { - // Check if we need to attach a file and send it after the initial details sent - if (gmEmbedDetails.hasAttachment) { - await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), { - file: gmEmbedDetails.attachment, - }).catch(() => { - rq.dd.message.reply(generateDMFailed(gm)); - }); - } + if (rq.apiRoll) { + n = await sendMessage(rq.api.channelId, { + content: rq.modifiers.apiWarn, + embeds: [pubEmbedDetails.embed] }).catch(() => { - rq.dd.message.reply(generateDMFailed(gm)); + apiErroredOut = true; + rq.api.requestEvent.respondWith(new Response( + 'Message failed to send.', + { status: Status.InternalServerError, statusText: STATUS_TEXT.get(Status.InternalServerError) } + )); }); - }); + } else { + // Send the public embed to correct channel + rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] }); + } + + if (!apiErroredOut) { + // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged + rq.modifiers.gms.forEach(async (gm) => { + log(LT.LOG, `Messaging GM ${gm}`); + // Attempt to DM the GM and send a warning if it could not DM a GM + await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), { + embeds: rq.modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed], + }).then(async () => { + // Check if we need to attach a file and send it after the initial details sent + if (gmEmbedDetails.hasAttachment) { + await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), { + file: gmEmbedDetails.attachment, + }).catch(() => { + if (rq.apiRoll && n) { + n.reply(generateDMFailed(gm)); + } else { + rq.dd.message.reply(generateDMFailed(gm)); + } + }); + } + }).catch(() => { + if (rq.apiRoll && n) { + n.reply(generateDMFailed(gm)); + } else { + rq.dd.message.reply(generateDMFailed(gm)); + } + }); + }); + } } else { // Not a gm roll, so just send normal embed to correct channel - const n = await rq.dd.m.edit({ - embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed], - }); - if (pubEmbedDetails.hasAttachment) { + if (rq.apiRoll) { + n = await sendMessage(rq.api.channelId, { + content: rq.modifiers.apiWarn, + embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed] + }).catch(() => { + apiErroredOut = true; + rq.api.requestEvent.respondWith(new Response( + 'Message failed to send.', + { status: Status.InternalServerError, statusText: STATUS_TEXT.get(Status.InternalServerError) } + )); + }); + } else { + n = await rq.dd.m.edit({ + embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed], + }); + } + + if (pubEmbedDetails.hasAttachment && n) { // Attachment requires you to send a new message n.reply({ file: pubEmbedDetails.attachment, }); } } + + if (!apiErroredOut) { + rq.api.requestEvent.respondWith(new Response( + JSON.stringify( + rq.modifiers.count ? { counts: countEmbed, details: pubEmbedDetails } : { details: pubEmbedDetails } + ), + { status: Status.OK, statusText: STATUS_TEXT.get(Status.OK) } + )); + } } } catch (e) { log(LT.ERROR, `Unddandled Error: ${JSON.stringify(e)}`); + if (rq.apiRoll && !apiErroredOut) { + rq.api.requestEvent.respondWith(new Response( + JSON.stringify(e), + { status: Status.InternalServerError, statusText: STATUS_TEXT.get(Status.InternalServerError) } + )); + } } }); }; diff --git a/start.command b/start.command index 33ed8a0..6ac3400 100644 --- a/start.command +++ b/start.command @@ -1 +1 @@ -deno run --allow-write=./logs --allow-read=./src/solver/rollWorker.ts --allow-net .\mod.ts \ No newline at end of file +deno run --allow-write=./logs/ --allow-read=./src/solver/ --allow-net .\mod.ts \ No newline at end of file