diff --git a/.vscode/settings.json b/.vscode/settings.json index 6dde31d..a8e3c2e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,6 +32,7 @@ "Mult", "nojs", "noodp", + "noopener", "noydir", "oldcnt", "oper", diff --git a/deno.json b/deno.json index db76459..3ea71b2 100644 --- a/deno.json +++ b/deno.json @@ -20,13 +20,15 @@ "singleQuote": true, "proseWrap": "preserve" }, + "nodeModulesDir": "none", "imports": { "@discordeno": "https://deno.land/x/discordeno@12.0.1/mod.ts", + "@imagescript": "https://deno.land/x/imagescript@1.3.0/mod.ts", "@Log4Deno": "https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.1.1/mod.ts", "@mysql": "https://deno.land/x/mysql@v2.12.1/mod.ts", - "@std/http": "jsr:@std/http@1.0.15", "@nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts", - "@imagescript": "https://deno.land/x/imagescript@1.3.0/mod.ts", + "@showdown": "npm:showdown@2.1.0", + "@std/http": "jsr:@std/http@1.0.15", "~config": "./config.ts", "~flags": "./flags.ts", "artigen/": "./src/artigen/", @@ -39,4 +41,4 @@ "src/api.ts": "./src/api.ts", "src/events.ts": "./src/events.ts" } -} \ No newline at end of file +} diff --git a/deno.lock b/deno.lock index b23d81a..9404071 100644 --- a/deno.lock +++ b/deno.lock @@ -10,7 +10,8 @@ "jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/path@^1.0.9": "1.0.9", - "jsr:@std/streams@^1.0.9": "1.0.9" + "jsr:@std/streams@^1.0.9": "1.0.9", + "npm:showdown@2.1.0": "2.1.0" }, "jsr": { "@std/cli@1.0.17": { @@ -54,6 +55,17 @@ "integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035" } }, + "npm": { + "commander@9.5.0": { + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" + }, + "showdown@2.1.0": { + "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==", + "dependencies": [ + "commander" + ] + } + }, "redirects": { "https://deno.land/std/hash/mod.ts": "https://deno.land/std@0.224.0/hash/mod.ts" }, @@ -837,7 +849,8 @@ }, "workspace": { "dependencies": [ - "jsr:@std/http@1.0.15" + "jsr:@std/http@1.0.15", + "npm:showdown@2.1.0" ] } } diff --git a/src/api.ts b/src/api.ts index 74c7e60..711a876 100644 --- a/src/api.ts +++ b/src/api.ts @@ -172,6 +172,8 @@ const start = () => { return endpoints.get.apiKey(query); case '/heatmap.png': return endpoints.get.heatmapPng(); + case '/webview': + return endpoints.get.generateWebView(query); default: // Alert API user that they messed up return stdResp.NotFound('NoAuth Get'); diff --git a/src/artigen/managers/handler/workerComplete.ts b/src/artigen/managers/handler/workerComplete.ts index b1cd424..3e3a0f8 100644 --- a/src/artigen/managers/handler/workerComplete.ts +++ b/src/artigen/managers/handler/workerComplete.ts @@ -11,7 +11,7 @@ import { removeWorker } from 'artigen/managers/countManager.ts'; import { QueuedRoll } from 'artigen/managers/manager.d.ts'; import { ApiResolveMap, TestResolveMap } from 'artigen/managers/resolveManager.ts'; -import { generateCountDetailsEmbed, generateDMFailed, generateRollDistsEmbed, generateRollEmbed } from 'artigen/utils/embeds.ts'; +import { generateCountDetailsEmbed, generateDMFailed, generateRollDistsEmbed, generateRollEmbed, toggleWebView } from 'artigen/utils/embeds.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; import dbClient from 'db/client.ts'; @@ -188,23 +188,29 @@ export const onWorkerComplete = async (workerMessage: MessageEvent, const respMessage: Embed[] = [ { color: infoColor1, - description: `This message contains information for a previous roll.\nPlease click on "<@${botId}> *Click to see attachment*" above this message to see the previous roll.`, + description: `This message contains information for a previous roll.\nPlease click on "<@${botId}> *Click to see attachment*" above this message to see the previous roll. + +As anyone with the Web View link can view the roll, Web View is disabled by default for privacy. Click the button below to enable Web View and generate a link for this roll.`, }, ]; if (pubAttachments.map((file) => file.blob.size).reduce(basicReducer, 0) < config.maxFileSize) { // All attachments will fit in one message - newMsg.reply({ - embeds: respMessage, - file: pubAttachments, - }); + newMsg + .reply({ + embeds: respMessage, + file: pubAttachments, + }) + .then((attachmentMsg) => toggleWebView(attachmentMsg, getUserIdForEmbed(rollRequest).toString(), false)); } else { pubAttachments.forEach((file) => { newMsg && - newMsg.reply({ - embeds: respMessage, - file, - }); + newMsg + .reply({ + embeds: respMessage, + file, + }) + .then((attachmentMsg) => toggleWebView(attachmentMsg, getUserIdForEmbed(rollRequest).toString(), false)); }); } } diff --git a/src/artigen/utils/embeds.ts b/src/artigen/utils/embeds.ts index f0fbea6..058005f 100644 --- a/src/artigen/utils/embeds.ts +++ b/src/artigen/utils/embeds.ts @@ -1,4 +1,4 @@ -import { CreateMessage, EmbedField } from '@discordeno'; +import { ButtonStyles, CreateMessage, DiscordenoMessage, EmbedField, MessageComponentTypes } from '@discordeno'; import { log, LogTypes as LT } from '@Log4Deno'; import config from '~config'; @@ -8,9 +8,14 @@ import { ArtigenEmbedNoAttachment, ArtigenEmbedWithAttachment, SolvedRoll } from import { CountDetails, RollDistributionMap, RollModifiers } from 'artigen/dice/dice.d.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; -import { failColor, infoColor1, infoColor2 } from 'embeds/colors.ts'; import { basicReducer } from 'artigen/utils/reducers.ts'; +import { failColor, infoColor1, infoColor2 } from 'embeds/colors.ts'; + +import { InteractionValueSeparator } from 'events/interactionCreate.ts'; + +import utils from 'utils/utils.ts'; + export const rollingEmbed: CreateMessage = { embeds: [ { @@ -130,9 +135,10 @@ export const generateRollDistsEmbed = (rollDists: RollDistributionMap): ArtigenE const totalSize = fields.map((field) => field.name.length + field.value.length).reduce(basicReducer, 0); if (totalSize > 4_000 || fields.length > 25 || fields.some((field) => field.name.length > 256 || field.value.length > 1024)) { const rollDistBlob = new Blob([fields.map((field) => `# ${field.name}\n${field.value}`).join('\n\n') as BlobPart], { type: 'text' }); + let rollDistErrDesc = 'The roll distribution was omitted from this message as it was over 4,000 characters, '; if (rollDistBlob.size > config.maxFileSize) { - const rollDistErrDesc = - 'The roll distribution was too large to be included and could not be attached below. If you would like to see the roll distribution details, please send the rolls in multiple messages.'; + rollDistErrDesc += + 'and was too large to be attached as the file would be too large for Discord to handle. If you would like to see the roll distribution details, please simplify or send the rolls in multiple messages.'; return { charCount: rollDistTitle.length + rollDistErrDesc.length, embed: { @@ -143,7 +149,7 @@ export const generateRollDistsEmbed = (rollDists: RollDistributionMap): ArtigenE hasAttachment: false, }; } else { - const rollDistErrDesc = 'The roll distribution was too large to be included and has been attached below.'; + rollDistErrDesc += 'and has been attached to a followup message as a formatted `.md` file.'; return { charCount: rollDistTitle.length + rollDistErrDesc.length, embed: { @@ -225,28 +231,31 @@ export const generateRollEmbed = ( } const baseDesc = `${line1Details}**${line2Details.shift()}:**\n${line2Details.join(': ')}`; + const fullDesc = `${baseDesc}\n\n${details}`; + + const formattingCount = (fullDesc.match(/(\*\*)|(__)|(~~)|(`)/g) ?? []).length / 2 + (fullDesc.match(/(<@)|(<#)/g) ?? []).length; // Embed desc limit is 4096 - if (baseDesc.length + details.length < 4_000) { + // Discord only formats 200 items per message + if (fullDesc.length < 4_000 && formattingCount <= 200) { // Response is valid size - const desc = `${baseDesc}\n\n${details}`; return { - charCount: desc.length, + charCount: fullDesc.length, embed: { color: infoColor2, - description: desc, + description: fullDesc, }, hasAttachment: false, }; } - // Response is too big, collapse it into a .txt file and send that instead. - const b = new Blob([`${baseDesc}\n\n${details}` as BlobPart], { type: 'text' }); - details = `${baseDesc}\n\nDetails have been omitted from this message for being over 4000 characters.`; + // Response is too big, collapse it into a .md file and send that instead. + const b = new Blob([fullDesc as BlobPart], { type: 'text' }); + details = `${baseDesc}\n\nDetails have been omitted from this message for ${fullDesc.length < 4_000 ? 'being over 4,000 characters' : 'having over 200 formatted items'}.`; if (b.size > config.maxFileSize) { // blob is too big, don't attach it details += - '\n\nFull 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.'; + '\n\nFull details could not be attached as the file would be too large for Discord to handle. If you would like to see the details of rolls, please simplify or send the rolls in multiple messages.'; return { charCount: details.length, embed: { @@ -258,7 +267,7 @@ export const generateRollEmbed = ( } // blob is small enough, attach it - details += '\n\nFull details have been attached to this messaged as a `.txt` file for verification purposes.'; + details += '\n\nFull details have been attached to a followup message as a formatted `.md` file for verification purposes.'; return { charCount: details.length, embed: { @@ -268,7 +277,40 @@ export const generateRollEmbed = ( hasAttachment: true, attachment: { blob: b, - name: 'rollDetails.txt', + name: 'rollDetails.md', }, }; }; + +export const webViewCustomId = 'webview'; +export const disabledStr = 'disabled'; +export const toggleWebView = (attachmentMessage: DiscordenoMessage, ownerId: string, enableWebView: boolean) => { + attachmentMessage + .edit({ + embeds: [ + { + ...attachmentMessage.embeds[0], + fields: [ + { + name: 'Web View:', + value: enableWebView ? `[Open Web View](${config.api.publicDomain}api/webview?c=${attachmentMessage.channelId}&m=${attachmentMessage.id})` : `Web View is ${disabledStr}.`, + }, + ], + }, + ], + components: [ + { + type: MessageComponentTypes.ActionRow, + components: [ + { + type: MessageComponentTypes.Button, + label: enableWebView ? 'Disable Web View' : 'Enable Web View', + customId: `${webViewCustomId}${InteractionValueSeparator}${ownerId}${InteractionValueSeparator}${enableWebView ? 'disable' : 'enable'}`, + style: ButtonStyles.Secondary, + }, + ], + }, + ], + }) + .catch((e) => utils.commonLoggers.messageEditError('embeds.ts:304', attachmentMessage, e)); +}; diff --git a/src/endpoints/_index.ts b/src/endpoints/_index.ts index 5a42630..717780f 100644 --- a/src/endpoints/_index.ts +++ b/src/endpoints/_index.ts @@ -4,6 +4,7 @@ import { apiChannel } from 'endpoints/gets/apiChannel.ts'; import { apiKey } from 'endpoints/gets/apiKey.ts'; import { apiKeyAdmin } from 'endpoints/gets/apiKeyAdmin.ts'; import { apiRoll } from 'endpoints/gets/apiRoll.ts'; +import { generateWebView } from 'endpoints/gets/webView.ts'; import { heatmapPng } from 'endpoints/gets/heatmapPng.ts'; import { apiChannelAdd } from 'endpoints/posts/apiChannelAdd.ts'; @@ -21,6 +22,7 @@ export default { apiRoll, apiKeyAdmin, apiChannel, + generateWebView, heatmapPng, }, post: { diff --git a/src/endpoints/gets/heatmapPng.ts b/src/endpoints/gets/heatmapPng.ts index cae81c7..110ca8d 100644 --- a/src/endpoints/gets/heatmapPng.ts +++ b/src/endpoints/gets/heatmapPng.ts @@ -2,12 +2,12 @@ import { STATUS_CODE, STATUS_TEXT } from '@std/http'; export const heatmapPng = (): Response => { const file = Deno.readFileSync('./src/endpoints/gets/heatmap.png'); - const imageHeaders = new Headers(); - imageHeaders.append('Content-Type', 'image/png'); + const headers = new Headers(); + headers.append('Content-Type', 'image/png'); // Send basic OK to indicate key has been sent return new Response(file, { status: STATUS_CODE.OK, statusText: STATUS_TEXT[STATUS_CODE.OK], - headers: imageHeaders, + headers, }); }; diff --git a/src/endpoints/gets/webView.ts b/src/endpoints/gets/webView.ts new file mode 100644 index 0000000..31c6811 --- /dev/null +++ b/src/endpoints/gets/webView.ts @@ -0,0 +1,222 @@ +import { DiscordenoMember, getChannel, getMember, getMessage, getRoles } from '@discordeno'; +import { log, LogTypes as LT } from '@Log4Deno'; +import showdown from '@showdown'; +import { STATUS_CODE, STATUS_TEXT } from '@std/http/status'; + +import config from '~config'; + +import { disabledStr } from 'artigen/utils/embeds.ts'; + +import utils from 'utils/utils.ts'; + +// globalName is added with discord's new username system +interface ModernMemberHOTFIX extends DiscordenoMember { + globalName: string; +} + +const converter = new showdown.Converter({ + emoji: true, + underline: true, +}); + +// Utilize the pre-existing stylesheets, do a little tweaking to make it ours +const wrapBasic = (str: string) => + ` + + + +The Artificer Roll Web View + + + + + + + + + + + + + + + + +${str} + +`; +const centerHTML = (str: string) => `
${str}
`; + +const badRequestMD = '# Invalid URL for Web View!'; +const badRequestHTML = wrapBasic(centerHTML(converter.makeHtml(badRequestMD))); + +const notAuthorizedMD = '# Web View is Disabled for this roll!'; +const notAuthorizedHTML = wrapBasic(centerHTML(converter.makeHtml(notAuthorizedMD))); + +const failedToGetAttachmentMD = '# Failed to get attachment from Discord!'; +const failedToGetAttachmentHTML = wrapBasic(centerHTML(converter.makeHtml(failedToGetAttachmentMD))); + +interface HtmlResp { + name: string; + html: string; +} + +const headerHeight = '3rem'; +const generatePage = (files: HtmlResp[]): string => + wrapBasic(`
+${config.name} Roll Web View +Available Files: +${ + files + .map( + (f, idx) => + ``, + ) + .join('') + } + +
+
+${files.map((f, idx) => `
${f.html}
`).join('')} +
`); + +const colorShade = (col: string, amt: number) => { + col = col.replace(/^#/, ''); + if (col.length === 3) col = col[0] + col[0] + col[1] + col[1] + col[2] + col[2]; + + const parts = col.match(/.{2}/g) ?? []; + let r = parts.shift() ?? '00'; + let g = parts.shift() ?? '00'; + let b = parts.shift() ?? '00'; + const [rInt, gInt, bInt] = [parseInt(r, 16) + amt, parseInt(g, 16) + amt, parseInt(b, 16) + amt]; + + r = Math.max(Math.min(255, rInt), 0).toString(16); + g = Math.max(Math.min(255, gInt), 0).toString(16); + b = Math.max(Math.min(255, bInt), 0).toString(16); + + const rr = (r.length < 2 ? '0' : '') + r; + const gg = (g.length < 2 ? '0' : '') + g; + const bb = (b.length < 2 ? '0' : '') + b; + + return `#${rr}${gg}${bb}`; +}; + +const makeMention = (mentionType: string, name: string, backgroundColor: string, color = 'var(--page-font-color)') => + `${mentionType}${name}`; + +export const generateWebView = async (query: Map): Promise => { + const headers = new Headers(); + headers.append('Content-Type', 'text/html'); + + const messageId = BigInt(query.get('m') ?? '0'); + const channelId = BigInt(query.get('c') ?? '0'); + + if (!messageId || !channelId) { + return new Response(badRequestHTML, { + status: STATUS_CODE.BadRequest, + statusText: STATUS_TEXT[STATUS_CODE.BadRequest], + headers, + }); + } + + const attachmentMessage = await getMessage(channelId, messageId).catch((e) => utils.commonLoggers.messageGetError('webView.ts:23', channelId, messageId, e)); + const discordAttachments = attachmentMessage?.attachments ?? []; + const embed = attachmentMessage?.embeds.shift(); + const webViewField = embed?.fields?.shift(); + + if (!attachmentMessage || discordAttachments.length === 0 || !embed || !webViewField) { + return new Response(badRequestHTML, { + status: STATUS_CODE.BadRequest, + statusText: STATUS_TEXT[STATUS_CODE.BadRequest], + headers, + }); + } + + if (webViewField.value.includes(disabledStr)) { + return new Response(notAuthorizedHTML, { + status: STATUS_CODE.Forbidden, + statusText: STATUS_TEXT[STATUS_CODE.Forbidden], + headers, + }); + } + + const htmlArr: HtmlResp[] = []; + for (const discordAttachment of discordAttachments) { + const attachment = await fetch(discordAttachment.url).catch((e) => log(LT.LOG, `Failed to get attachment: ${discordAttachment}`, e)); + const bodyText = (await attachment?.text()) ?? ''; + + htmlArr.push({ + name: discordAttachment.filename, + html: bodyText ? converter.makeHtml(bodyText) : failedToGetAttachmentHTML, + }); + } + + let fullPage = generatePage(htmlArr); + + if (fullPage.indexOf('<@&')) { + const guildRoles = (await getRoles(attachmentMessage.guildId).catch((e) => log(LT.LOG, `Failed to get Guild Roles: ${attachmentMessage.guildId}`, e))) ?? []; + const rolesToReplace = fullPage.matchAll(/<@&(\d+)>/g); + for (const roleToReplace of rolesToReplace) { + const role = guildRoles.filter((r) => r.id === BigInt(roleToReplace[1] ?? '-1')).shift() ?? { name: 'unknown-role', color: 4211819 }; + fullPage = fullPage.replaceAll( + roleToReplace[0], + makeMention('@', role.name, colorShade(`#${role.color.toString(16)}`, -100), colorShade(`#${role.color.toString(16)}`, 50)), + ); + } + } + + if (fullPage.indexOf('<#')) { + const channelsToReplace = fullPage.matchAll(/<#(\d+)>/g); + for (const channelToReplace of channelsToReplace) { + const channel = (await getChannel(BigInt(channelToReplace[1] ?? '-1')).catch((e) => log(LT.LOG, `Failed to get Channel: ${channelToReplace[1]}`, e))) ?? { + name: 'unknown', + }; + fullPage = fullPage.replaceAll(channelToReplace[0], makeMention('#', channel.name ?? 'unknown', '#40446b')); + } + } + + if (fullPage.indexOf('<@')) { + const usersToReplace = fullPage.matchAll(/<@(\d+)>/g); + for (const userToReplace of usersToReplace) { + const rawUser = await getMember(attachmentMessage.guildId, BigInt(userToReplace[1] ?? '-1')).catch((e) => log(LT.LOG, `Failed to get Channel: ${userToReplace[1]}`, e)); + const user = rawUser ? (rawUser as ModernMemberHOTFIX) : { + name: (_gId: bigint) => 'unknown-user', + username: 'unknown-user', + globalName: 'unknown-user', + }; + const nickName = user.name(attachmentMessage.guildId); + const name = nickName === user.username ? user.globalName : nickName ?? user.globalName; + fullPage = fullPage.replaceAll(userToReplace[0], makeMention('@', name ?? user.username, '#40446b')); + } + } + + return new Response(fullPage, { + status: STATUS_CODE.OK, + statusText: STATUS_TEXT[STATUS_CODE.OK], + headers, + }); +}; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 1500d02..d5635d0 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,31 +1,76 @@ -import { ButtonData, DiscordMessageComponentTypes, editMessage, Interaction, InteractionResponseTypes, SelectMenuData, sendInteractionResponse } from '@discordeno'; +import { + ButtonData, + DiscordenoMessage, + DiscordMessageComponentTypes, + editMessage, + Interaction, + InteractionResponseTypes, + MessageFlags, + SelectMenuData, + sendInteractionResponse, + structures, +} from '@discordeno'; import { log, LogTypes as LT } from '@Log4Deno'; +import { toggleWebView, webViewCustomId } from 'artigen/utils/embeds.ts'; + import { generateHelpMessage, helpCustomId } from 'commands/helpLibrary/generateHelpMessage.ts'; +import { failColor } from 'embeds/colors.ts'; + import utils from 'utils/utils.ts'; export const InteractionValueSeparator = '\u205a'; -export const interactionCreateHandler = (interaction: Interaction) => { +const ackInteraction = (interaction: Interaction) => + sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.DeferredUpdateMessage, + }).catch((e: Error) => utils.commonLoggers.messageSendError('interactionCreate.ts:26', interaction, e)); + +export const interactionCreateHandler = async (interaction: Interaction) => { try { if (interaction.data) { const parsedData = JSON.parse(JSON.stringify(interaction.data)) as SelectMenuData | ButtonData; if (parsedData.customId.startsWith(helpCustomId) && parsedData.componentType === DiscordMessageComponentTypes.SelectMenu) { // Acknowledge the request since we're editing the original message - sendInteractionResponse(interaction.id, interaction.token, { - type: InteractionResponseTypes.DeferredUpdateMessage, - }).catch((e: Error) => utils.commonLoggers.messageEditError('interactionCreate.ts:26', interaction, e)); + ackInteraction(interaction); // Edit original message - editMessage(BigInt(interaction.channelId ?? '0'), BigInt(interaction.message?.id ?? '0'), generateHelpMessage(parsedData.values[0])).catch((e: Error) => + editMessage(BigInt(interaction.channelId ?? '0'), BigInt(interaction.message?.id ?? '0'), generateHelpMessage(parsedData.values[0])).catch((e) => utils.commonLoggers.messageEditError('interactionCreate.ts:30', interaction, e) ); return; } - log(LT.WARN, `UNHANDLED INTERACTION!!! data: ${JSON.stringify(interaction.data)}`); + if (parsedData.customId.startsWith(webViewCustomId) && parsedData.componentType === DiscordMessageComponentTypes.Button && interaction.message) { + const ownerId = parsedData.customId.split(InteractionValueSeparator)[1] ?? 'missingOwnerId'; + const userInteractingId = interaction.member?.user.id ?? interaction.user?.id ?? 'missingUserId'; + if (ownerId === userInteractingId) { + ackInteraction(interaction); + const enableWebView = parsedData.customId.split(InteractionValueSeparator)[2] === 'enable'; + const ddMsg: DiscordenoMessage = await structures.createDiscordenoMessage(interaction.message); + + toggleWebView(ddMsg, ownerId, enableWebView); + } else { + sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: MessageFlags.Empheral, + embeds: [ + { + color: failColor, + title: 'Not Allowed!', + description: 'Only the original user that requested this roll can disable/enable Web View.', + }, + ], + }, + }).catch((e) => utils.commonLoggers.messageSendError('interactionCreate.ts:57', interaction, e)); + } + return; + } + + log(LT.WARN, `UNHANDLED INTERACTION!!! data: ${JSON.stringify(interaction.data)} | Full Interaction: ${JSON.stringify(interaction)}`); } else { log(LT.WARN, `UNHANDLED INTERACTION!!! Missing data! ${JSON.stringify(interaction)}`); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index bc8c8ad..3089e02 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -7,9 +7,11 @@ import { log, LogTypes as LT } from '@Log4Deno'; import { DiscordenoMessage, Interaction } from '@discordeno'; const genericLogger = (level: LT, message: string) => log(level, message); +const messageGetError = (location: string, channelId: bigint | string, messageId: bigint | string, err: Error) => + genericLogger(LT.ERROR, `${location} | Failed to edit message: ${channelId}-${messageId} | Error: ${err.name} - ${err.message}`); const messageEditError = (location: string, message: DiscordenoMessage | Interaction | string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to edit message: ${JSON.stringify(message)} | Error: ${err.name} - ${err.message}`); -const messageSendError = (location: string, message: DiscordenoMessage | string, err: Error) => +const messageSendError = (location: string, message: DiscordenoMessage | Interaction | string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to send message: ${JSON.stringify(message)} | Error: ${err.name} - ${err.message}`); const messageDeleteError = (location: string, message: DiscordenoMessage | string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to delete message: ${JSON.stringify(message)} | Error: ${err.name} - ${err.message}`); @@ -18,8 +20,9 @@ const dbError = (location: string, type: string, err: Error) => genericLogger(LT export default { commonLoggers: { dbError, - messageEditError, - messageSendError, messageDeleteError, + messageEditError, + messageGetError, + messageSendError, }, }; diff --git a/www/home/main.css b/www/home/main.css index 9ac2b92..50d465c 100644 --- a/www/home/main.css +++ b/www/home/main.css @@ -1,195 +1,196 @@ body { - font-family: "Play", sans-serif; + font-family: 'Play', sans-serif; - padding: 0; - margin: 0; - overflow: hidden; + padding: 0; + margin: 0; + overflow: hidden; } #page { - height: 100vh; - display: grid; - grid-template-columns: auto; - grid-template-rows: 3rem calc(100vh - 5rem) 2rem; - grid-template-areas: "header" "page-contents" "footer"; - - color: var(--page-font-color); - background-color: var(--page-bg-color); + height: 100vh; + display: grid; + grid-template-columns: auto; + grid-template-rows: 3rem calc(100vh - 5rem) 2rem; + grid-template-areas: 'header' 'page-contents' 'footer'; + + color: var(--page-font-color); + background-color: var(--page-bg-color); } +header, #header { - grid-area: header; + grid-area: header; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto; - grid-template-areas: "header-left header-right"; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-template-areas: 'header-left header-right'; - font-family: "Cinzel", serif; - font-size: 2rem; - line-height: 3rem; - font-weight: 500; - padding: 0 10px; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - background-color: var(--header-bg-color); - color: var(--header-font-color); - border-bottom: 1px solid var(--header-font-color); + font-family: 'Cinzel', serif; + font-size: 2rem; + line-height: 3rem; + font-weight: 500; + padding: 0 10px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + background-color: var(--header-bg-color); + color: var(--header-font-color); + border-bottom: 1px solid var(--header-font-color); } #header-left { - grid-area: header-left; + grid-area: header-left; } #header-right { - grid-area: header-right; - justify-self: end; + grid-area: header-right; + justify-self: end; - font-size: 1.75rem; + font-size: 1.75rem; } #footer { - grid-area: footer; + grid-area: footer; - display: grid; - grid-template-columns: 1fr 1.5fr 1.5fr 1fr; - grid-template-rows: auto; - grid-template-areas: ". footer-left footer-right ."; + display: grid; + grid-template-columns: 1fr 1.5fr 1.5fr 1fr; + grid-template-rows: auto; + grid-template-areas: '. footer-left footer-right .'; - line-height: 2rem; - height: 2rem; + line-height: 2rem; + height: 2rem; - background-color: var(--footer-bg-color); + background-color: var(--footer-bg-color); } #footer-left { - grid-area: footer-left; + grid-area: footer-left; } #footer-right { - grid-area: footer-right; - justify-self: end; + grid-area: footer-right; + justify-self: end; } #page-contents { - grid-area: page-contents; - padding: 0 20rem; - height: calc(100vh - 5rem); + grid-area: page-contents; + padding: 0 20rem; + height: calc(100vh - 5rem); - display: grid; - grid-template-columns: auto; - grid-template-rows: fit-content(5rem) fit-content(10rem) fit-content(37rem) auto 1rem; - grid-template-areas: "slogan" "logo-desc" "examples" "api" "final"; + display: grid; + grid-template-columns: auto; + grid-template-rows: fit-content(5rem) fit-content(10rem) fit-content(37rem) auto 1rem; + grid-template-areas: 'slogan' 'logo-desc' 'examples' 'api' 'final'; - overflow-y: auto; + overflow-y: auto; } #slogan { - grid-area: slogan; + grid-area: slogan; } #slogan h1 { - line-height: 2.5rem; - font-size: 2.5rem; + line-height: 2.5rem; + font-size: 2.5rem; } #logo-desc { - grid-area: logo-desc; + grid-area: logo-desc; - margin-bottom: 0.5rem; + margin-bottom: 0.5rem; - display: grid; - grid-template-columns: 11rem auto; - grid-template-rows: auto; - grid-template-areas: "logo description"; + display: grid; + grid-template-columns: 11rem auto; + grid-template-rows: auto; + grid-template-areas: 'logo description'; } #logo { - grid-area: logo; - margin: auto; + grid-area: logo; + margin: auto; } #logo img { - height: 10rem; + height: 10rem; } #description { - grid-area: description; + grid-area: description; } #examples { - grid-area: examples; + grid-area: examples; } h4.example { - line-height: 0.25rem; + line-height: 0.25rem; } p.example { - margin: 0; - margin-bottom: 0.1rem; - padding: 0; + margin: 0; + margin-bottom: 0.1rem; + padding: 0; } .slug h3 { - margin-top: 0; + margin-top: 0; } #api { - grid-area: api; + grid-area: api; } #final { - grid-area: final; + grid-area: final; } @media screen and (max-width: 1900px) { - #page-contents { - padding: 0 10rem; - } + #page-contents { + padding: 0 10rem; + } } @media screen and (max-width: 1400px) { - #page-contents { - padding: 0 5rem; - } + #page-contents { + padding: 0 5rem; + } } @media screen and (max-width: 1000px) { - #page-contents { - padding: 0 1rem; - } + #page-contents { + padding: 0 1rem; + } } @media screen and (max-width: 630px) { - #page { - grid-template-rows: 6rem calc(100vh - 8rem) 2rem; - } + #page { + grid-template-rows: 6rem calc(100vh - 8rem) 2rem; + } - #page-contents { - height: calc(100vh - 8rem); - } + #page-contents { + height: calc(100vh - 8rem); + } } @media screen and (max-width: 330px) { - #page { - grid-template-rows: 9rem calc(100vh - 11rem) 2rem; - } + #page { + grid-template-rows: 9rem calc(100vh - 11rem) 2rem; + } - #page-contents { - height: calc(100vh - 11rem); - } + #page-contents { + height: calc(100vh - 11rem); + } } @media screen and (max-width: 292px) { - #page { - grid-template-rows: 12rem calc(100vh - 14rem) 2rem; - } + #page { + grid-template-rows: 12rem calc(100vh - 14rem) 2rem; + } - #page-contents { - height: calc(100vh - 14rem); - } + #page-contents { + height: calc(100vh - 14rem); + } } diff --git a/www/home/theme.css b/www/home/theme.css index 9d21e11..ae7fcd4 100644 --- a/www/home/theme.css +++ b/www/home/theme.css @@ -2,50 +2,55 @@ * https://paletton.com/#uid=5000p0kw8dLmVn9rgiuHN93Sj4E */ :root { - --header-font-color: rgb(238, 237, 226); - --header-bg-color: rgb(110, 0, 0); + --header-font-color: rgb(238, 237, 226); + --header-bg-color: rgb(110, 0, 0); - --page-font-color: white; - --page-bg-color: rgb(32, 34, 37); + --page-font-color: white; + --page-bg-color: rgb(32, 34, 37); - --footer-bg-color: rgb(54, 57, 63); + --footer-bg-color: rgb(54, 57, 63); - --link-new-color: rgb(147, 22, 22); - --link-hover-color: rgb(255, 94, 94); - --link-visited-color: rgb(184, 52, 52); + --link-new-color: rgb(147, 22, 22); + --link-hover-color: rgb(255, 94, 94); + --link-visited-color: rgb(184, 52, 52); - --code-bg: rgb(47, 49, 54); + --code-bg: rgb(47, 49, 54); - --slug-bg: rgb(15, 16, 17); - --slug-border: rgb(0, 0, 0); + --slug-bg: rgb(15, 16, 17); + --slug-border: rgb(0, 0, 0); } -#header a { - color: var(--header-font-color); - text-decoration: none; +header a, +header a:visited, +#header a, +#header a:visited { + color: var(--header-font-color); + text-decoration: none; } a { - color: var(--link-new-color); + color: var(--link-new-color); } -a:active, a:visited { - color: var(--link-visited-color); +a:active, +a:visited { + color: var(--link-visited-color); } a:hover, +header a:hover, #header a:hover { - cursor: pointer; - color: var(--link-hover-color); + cursor: pointer; + color: var(--link-hover-color); } code { - background-color: var(--code-bg); + background-color: var(--code-bg); } .slug { - padding: 0.5rem; - background-color: var(--slug-bg); - border: 4px solid var(--slug-border); - border-radius: 1.5rem; + padding: 0.5rem; + background-color: var(--slug-bg); + border: 4px solid var(--slug-border); + border-radius: 1.5rem; }