const {ApplicationCommandOptionTypes, MessageFlags} = require("@projectdysnomia/dysnomia").Constants; const InteractionCommand = require("../lib/interactionCommand.js"); const {getOption} = require("../lib/interactionDispatcher.js"); const events = require("../lib/events.js"); const {hasFlag} = require("../lib/guildSettings.js"); const REGEX_GITHUB = /(?:\s|^)(\|\|\s*)?https?:\/\/(www\.)?github\.com\/[a-z0-9-]+\/[a-z0-9-._]+\/blob\/([a-z0-9-_.?&=#/%]*)(\s*\|\|)?/gi; const REGEX_GITLAB = /(?:\s|^)(\|\|\s*)?https?:\/\/.+?\/[a-z0-9-]+\/[a-z0-9-._]+\/-\/blob\/([a-z0-9-_.?&=#/%]*)(\s*\|\|)?/gi; const REGEX_GITEA = /(?:\s|^)(\|\|\s*)?https?:\/\/.+?\/[a-z0-9-]+\/[a-z0-9-._]+\/src\/(branch|commit)\/([a-z0-9-_.?&=#/%]*)(\s*\|\|)?/gi; const REGEX_SPOILER = /(?:\s|^)\|\|([\s\S]+?)\|\|/; function unindent(str) { str = str.replace(/\t/g, " "); const minIndent = str .match(/^ *(?=\S)/gm) ?.reduce((prev, curr) => Math.min(prev, curr.length), Infinity) ?? 0; if (!minIndent) return str; return str.replace(new RegExp(`^ {${minIndent}}`, "gm"), ""); } const fileTypeAliases = { astro: "jsx", svelte: "jsx", vue: "jsx", mdx: "md", jsonc: "json", json5: "json", jsonld: "json", "sublime-build": "json", "sublime-settings": "json", "sublime-menu": "json", "sublime-commands": "json", "sublime-project": "json", "sublime-mousemap": "json", "sublime-keymap": "json", "sublime-macro": "json", "sublime-completions": "json", "code-workspace": "json", "code-snippets": "json", }; async function processFile( link, originalLink, spoiler = false, linkFile = false ) { link = link.replaceAll("||", "").trim(); const res = await fetch(link); if (!res.ok) return ""; if (!res.headers.get("Content-Type").startsWith("text/plain")) return ""; const file = await res.text(); const lines = file.replace(/\r/g, "").split("\n"); const urlObj = new URL(link); let fileName = decodeURIComponent(urlObj.pathname).substring( urlObj.pathname.lastIndexOf("/") + 1, urlObj.pathname.length ); const fileType = fileName.lastIndexOf(".") == -1 ? "" : fileName.substring(fileName.lastIndexOf(".") + 1); if (linkFile) { fileName = `[${fileName}](<${originalLink}>)`; } const lineStr = urlObj.hash.match(/#L\d+(-L?\d+)?/)?.[0]; let startLine, endLine; let entireFile = false; if (lineStr) { const [start, end] = lineStr.match(/\d+/g); if (!end) { startLine = endLine = start; } else { startLine = start; endLine = end; } } else { entireFile = true; startLine = 0; endLine = lines.length; } const whichLines = entireFile ? "" : startLine == endLine ? "Line " + startLine : "Lines " + startLine + "-" + endLine; if (entireFile) { if (fileType == "md") return ""; if (lines.length > 20) return ""; } let targetLines = ( entireFile ? lines : lines.slice(startLine - 1, endLine) ).join("\n"); let warning = ""; if (spoiler && targetLines.includes("||")) { targetLines = targetLines.replaceAll("||", "|\u200b|"); warning = " - :warning: Zero width spaces present"; } if (targetLines.includes("``")) { targetLines = targetLines.replaceAll("``", "`\u200b`"); warning = " - :warning: Zero width spaces present"; } return `**${fileName}:** ${whichLines}${warning}\n${ spoiler ? "||" : "" }\`\`\`${fileTypeAliases[fileType] ?? fileType}\n${unindent( targetLines )}\n\`\`\`${spoiler ? "||" : ""}`; } events.add("messageCreate", "codePreviews", async function (msg) { if (msg.author.id == hf.bot.user.id) return; if (!msg.guildID) return; if (!(await hasFlag(msg.guildID, "codePreviews"))) return; const files = []; const githubLinks = msg.content.match(REGEX_GITHUB); const gitlabLinks = msg.content.match(REGEX_GITLAB); const giteaLinks = msg.content.match(REGEX_GITEA); if (githubLinks?.length) { for (const link of githubLinks) { const spoiler = REGEX_SPOILER.test(link); files.push( await processFile(link.replace("/blob/", "/raw/"), link, spoiler) ); } } if (gitlabLinks?.length) { for (const link of gitlabLinks) { const spoiler = REGEX_SPOILER.test(link); files.push( await processFile(link.replace("/blob/", "/raw/"), link, spoiler) ); } } if (giteaLinks?.length) { for (const link of giteaLinks) { const spoiler = REGEX_SPOILER.test(link); files.push( await processFile(link.replace("/src/", "/raw/"), link, spoiler) ); } } let out = ""; const allFiles = files.join("\n").trim(); if (allFiles !== "" && allFiles.length <= 2000) { await msg.edit({flags: MessageFlags.SUPPRESS_EMBEDS}).catch(() => {}); } for (let i = 0; i < files.length; i++) { const file = files[i]; if (file === "") continue; if (out.length + file.length > 2000) { await msg.channel.createMessage({ content: out, allowedMentions: { repliedUser: false, }, messageReference: { messageID: msg.id, }, }); out = file; } else { out += "\n" + file; out = out.trim(); } if (i == files.length - 1 && out.length <= 2000) { await msg.channel.createMessage({ content: out, allowedMentions: { repliedUser: false, }, messageReference: { messageID: msg.id, }, }); } } }); const codepreviewsCommand = new InteractionCommand("codepreview"); codepreviewsCommand.helpText = "Post snippets of codes from files on GitHub, Gitlab and Gitea instances."; codepreviewsCommand.options.url = { name: "url", type: ApplicationCommandOptionTypes.STRING, description: "URL to attempt to parse", required: true, default: "", }; codepreviewsCommand.options.spoiler = { name: "spoiler", type: ApplicationCommandOptionTypes.BOOLEAN, description: "Send spoilered", required: false, default: false, }; codepreviewsCommand.callback = async function (interaction) { const url = getOption(interaction, codepreviewsCommand, "url"); const spoiler = getOption(interaction, codepreviewsCommand, "spoiler"); const githubOrGitlab = url.match(REGEX_GITHUB) ?? url.match(REGEX_GITLAB); const gitea = url.match(REGEX_GITEA); let out = ""; if (githubOrGitlab) { out = await processFile(url.replace("/blob/", "/raw/"), url, spoiler, true); } else if (gitea) { out = await processFile(url.replace("/src/", "/raw/"), url, spoiler, true); } else { return { content: "Provided link did not match any services.", flags: MessageFlags.EPHEMERAL, }; } if (out == "") { return { content: "No content was returned. Provided file is either too long, a markdown file, or not plaintext.", flags: MessageFlags.EPHEMERAL, }; } if (out.length > 2000) { return { content: "Provided file is too long.", flags: MessageFlags.EPHEMERAL, }; } return out; }; hf.registerCommand(codepreviewsCommand);