diff --git a/.gitignore b/.gitignore index 065fd69..153b006 100644 --- a/.gitignore +++ b/.gitignore @@ -72,8 +72,7 @@ web_modules/ # Yarn Integrity file .yarn-integrity -# dotenv environment variable files and credentials -credentials.json +# dotenv environment variable files .env .env.development.local .env.test.local @@ -130,5 +129,9 @@ dist .yarn/install-state.gz .pnp.* +# configs +credentials.json +emojis.json + # database data.db \ No newline at end of file diff --git a/credentials.example.json b/config/credentials.example.json similarity index 100% rename from credentials.example.json rename to config/credentials.example.json diff --git a/config/emojis.example.json b/config/emojis.example.json new file mode 100644 index 0000000..49592e7 --- /dev/null +++ b/config/emojis.example.json @@ -0,0 +1,16 @@ +{ + "booru": { + "rating": { + "safe": "<:rating_safe:1293819920978804829>", + "general": "<:rating_general:1293819929199513610>", + "questionable": "<:rating_questionable:1293819907099725925>", + "explicit": "<:rating_explicit:1293819893795389491>", + "unknown": "<:rating_unknown:1293819936845594665>" + }, + "score": { + "green_arrow_up": "<:green_arrow_up:1293819944399667222>", + "red_arrow_down": "<:red_arrow_down:1293819951764869181>", + "yellow_tilde": "<:yellow_tilde:1293819958643396608>" + } + } +} \ No newline at end of file diff --git a/src/commands/fun/blacklist.js b/src/commands/fun/blacklist.js index acfcad3..812292b 100644 --- a/src/commands/fun/blacklist.js +++ b/src/commands/fun/blacklist.js @@ -102,4 +102,4 @@ module.exports = { await interaction.respond(choices.map(choice => ({ name: choice, value: choice }))) } }, -}; +}; \ No newline at end of file diff --git a/src/commands/fun/gelbooru.js b/src/commands/fun/gelbooru.js index d44eab2..09e0114 100644 --- a/src/commands/fun/gelbooru.js +++ b/src/commands/fun/gelbooru.js @@ -3,7 +3,7 @@ const { generateImageUrl } = require('@imgproxy/imgproxy-node'); const { stringify } = require("node:querystring"); const { readFileSync } = require("node:fs"); const { decode } = require("html-entities"); -const { extname } = require("node:path"); +const { extname, basename } = require("node:path"); const { knex } = require("../../db.js"); const Booru = require("@himeka/booru"); @@ -63,11 +63,11 @@ function notEmpty(str) { return str.trim() !== '' } -const regexCutTags = /[\S\s]{1,75}[^,]{0,25}/; +const regexCutTags = /[\S\s]{1,75}[^,]{0,125}/; function formatTags(tags) { const tagString = decode(tags.join(', ')); - if (tagString.length < 100) { + if (tagString.length < 500) { return tagString; } @@ -75,12 +75,15 @@ function formatTags(tags) { return `${escapeMarkdown(tagCutMatch[0] ?? '')}, ...`; } +var credentials = JSON.parse(readFileSync("config/credentials.json")); +var emojis = JSON.parse(readFileSync("config/emojis.json")); + const ratingEmojis = { - s: '<:rating_safe:1293819920978804829>', - g: '<:rating_general:1293819929199513610>', - q: '<:rating_questionable:1293819907099725925>', - e: '<:rating_explicit:1293819893795389491>', - u: '<:rating_unknown:1293819936845594665>', + s: emojis.booru.rating.safe, + g: emojis.booru.rating.general, + q: emojis.booru.rating.questionable, + e: emojis.booru.rating.explicit, + u: emojis.booru.rating.unknown } function formatRating(rating) { @@ -89,11 +92,11 @@ function formatRating(rating) { function formatScore(score) { if (score > 0) { - return `<:green_arrow_up:1293819944399667222> ${score}` + return `${emojis.booru.score.green_arrow_up} ${score}` } else if (score < 0) { - return `<:red_arrow_down:1293819951764869181> ${score}` + return `${emojis.booru.score.red_arrow_down} ${score}` } else { - return `<:yellow_tilde:1293819958643396608> ${score}` + return `${emojis.booru.score.yellow_tilde} ${score}` } } @@ -106,8 +109,6 @@ const blacklist = [ "ai_art" ]; -var credentials = JSON.parse(readFileSync("credentials.json")); - function proxy(url) { if (!process.env.IMGPROXY_HOST) return url; @@ -142,10 +143,12 @@ module.exports = { if (!result) result = { blacklist: '' }; - const userBlacklist = (result.blacklist ?? "").trim().split(" "); + const userBlacklist = (result.blacklist ?? "").trim().split(" ").filter(notEmpty); const searchTags = [rating, ...tags, ...[...blacklist, ...userBlacklist].map(i => "-" + i)]; const startTime = process.hrtime.bigint(); + console.log(searchTags); + var post = (await Booru.search(booru, searchTags, { limit: 1, random: true, credentials: credentials[booru] ?? null }))[0]; if (post == null) { await interaction.followUp("<:warning:1293874152150667315> Could not find any post matching tags."); @@ -155,12 +158,13 @@ module.exports = { const endTime = process.hrtime.bigint(); const timeTaken = endTime - startTime; - const ext = extname((post['data']).file_name ?? post.fileUrl ?? '').toLowerCase(); + const fileName = (post.rating != "g" ? "SPOILER_" : "") + basename(post.fileUrl); + const ext = extname(fileName).toLowerCase(); const leadingDescription = [ `**Score:** ${formatScore(post.score ?? 0)}`, `**Rating:** ${formatRating(post.rating)}`, - `[File URL](${post.fileUrl})`, + `[File URL](<${post.fileUrl}>)`, `\`${ext}\``, ].join(' | ') @@ -174,26 +178,24 @@ module.exports = { timeTaken ? formatTime(timeTaken) : '', ].filter(notEmpty).join(' ยท ') - if (isEmbeddableFileType(ext)) { - const embed = new EmbedBuilder() - .setColor("#cba6f7") - .setTitle(`Post #${post.id}`) - .setURL(post.postView) - .setDescription(description) - .setImage(post.fileUrl) - .setFooter({ - text: footerText, - iconURL: proxy(`https://${post.booru.domain}/favicon.ico`), - }) + const embed = new EmbedBuilder() + .setColor("#cba6f7") + .setTitle(`Post #${post.id}`) + .setURL(post.postView) + .setDescription(description) + .setFooter({ + text: footerText, + iconURL: proxy(`https://${post.booru.domain}/favicon.ico`), + }) - await interaction.followUp({ content: "", embeds: [embed.data] }); - } else { - await interaction.followUp( - '>>> ' + bold(`[Post #${post.id}](<${post.postView}>)`) + "\n" + - description + "\n" + - footerText - ); - } + await interaction.followUp({ + content: "", + embeds: [embed.data], + files: [{ + attachment: post.fileUrl, + name: fileName + }] + }); }, async autocomplete(interaction) { const focusedValue = interaction.options.getFocused(); diff --git a/src/commands/fun/serverconfig.js b/src/commands/fun/serverconfig.js new file mode 100644 index 0000000..2123fa4 --- /dev/null +++ b/src/commands/fun/serverconfig.js @@ -0,0 +1,98 @@ +const { InteractionContextType, ApplicationIntegrationType, SlashCommandBuilder } = require("discord.js"); +const { format } = require("node:util"); +const { knex } = require("../../db.js"); + +const configData = { + "yuri": { + "description": "Automated yuri posting", + "options": [{ + "name": "enable", + "required": true, + "type": "Boolean", + "description": "Should the bot post yuri?" + }, { + "name": "channel", + "type": "Channel", + "description": "Where to post yuri", + }] + } +} + +const data = new SlashCommandBuilder() + .setName("serverconfig") + .setDescription("Manage your server settings") + .setContexts([ + InteractionContextType.Guild + ]) + .setIntegrationTypes([ + ApplicationIntegrationType.GuildInstall + ]); + +for (const cfg in configData) { + const config = configData[cfg]; + + data.addSubcommand((builder) => { + builder + .setName(cfg) + .setDescription(config.description); + + for (const opt in config.options) { + const option = config.options[opt]; + + builder[`add${option.type}Option`](optionBuilder => + optionBuilder + .setName(option.name) + .setRequired(option.required ?? false) + .setDescription(option.description) + ); + } + return builder; + }); +} + +module.exports = { + data, + async execute(interaction) { + var result = await knex.select("data").from("serverconfigs").where("id", interaction.guildId).first(); + if (!result) + result = { data: '{}' }; + + const config = JSON.parse(result.data); + const cfg = interaction.options.getSubcommand(true); + config[cfg] = config[cfg] ?? {}; + + for (const option of configData[cfg].options) { + config[cfg][option.name] = interaction.options[`get${option.type}`](option.name); + } + + const data = { + id: interaction.guildId, + data: JSON.stringify(config) + } + + await knex.raw(format('%s ON CONFLICT (id) DO UPDATE SET %s', + knex("serverconfigs").insert(data).toString().toString(), + knex("serverconfigs").update(data).whereRaw(`'serverconfigs'.id = '${interaction.guildId}'`).toString().replace(/^update\s.*\sset\s/i, '') + )); + + interaction.reply({ content: "Settings updated!", ephemeral: true }); + }, + async autocomplete(interaction) { + const focusedOption = interaction.options.getFocused(true); + const command = interaction.options.getSubcommand(true); + + console.log(command, focusedOption); + + const id = ""; + + const choices = []; + for (const option in configData) { + if (focusedOption.name == "name" && option.startsWith(focusedOption.value)) + choices.push(option); + else if (focusedOption.name == "value" && (option == interaction.options.getString("name") ?? "")) + choices.push(...buildChoices(option, interaction)); + } + + await interaction.respond(choices.map(choice => ({ name: choice, value: choice }))) + }, +}; \ No newline at end of file diff --git a/src/commands/fun/userconfig.js b/src/commands/fun/userconfig.js new file mode 100644 index 0000000..83cc8f2 --- /dev/null +++ b/src/commands/fun/userconfig.js @@ -0,0 +1,62 @@ +const { InteractionContextType, ApplicationIntegrationType, SlashCommandBuilder } = require("discord.js"); +const { knex } = require("../../db.js"); + +const configData = {} + +const data = new SlashCommandBuilder() + .setName("userconfig") + .setDescription("Manage your user settings") + .setContexts([ + InteractionContextType.Guild, + InteractionContextType.BotDM, + InteractionContextType.PrivateChannel + ]) + .setIntegrationTypes([ + ApplicationIntegrationType.UserInstall + ]); + +for (const option in configData) { + const config = configData[option]; + + data.addSubcommand((builder) => { + builder + .setName(option) + .setDescription(config.description) + switch (config.type) { + case "bool": + builder.addBooleanOption(builder => + builder.setName("value") + ); + case "channel": + builder.addChannelOption(builder => + builder.setName("channel") + ); + default: + } + }) +} + +module.exports = { + data, + async execute(interaction) { + interaction.reply("Not implemented yet, sorry!"); + }, + async autocomplete(interaction) { + const focusedOption = interaction.options.getFocused(true); + const command = interaction.options.getSubcommand(true); + + console.log(command, focusedOption); + + const id = ""; + + const choices = []; + for (const option in configData) { + if (focusedOption.name == "name" && option.startsWith(focusedOption.value)) + choices.push(option); + else if (focusedOption.name == "value" && (option == interaction.options.getString("name") ?? "")) + choices.push(...buildChoices(option, interaction)); + } + + await interaction.respond(choices.map(choice => ({ name: choice, value: choice }))) + }, +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 83b2ccf..3177851 100644 --- a/src/index.js +++ b/src/index.js @@ -80,6 +80,18 @@ client.once(Events.ClientReady, async () => { table.string("blacklist"); }); + if (!(await knex.schema.hasTable("serverconfigs"))) + await knex.schema.createTable("serverconfigs", function (table) { + table.string("id").primary(); + table.string("data"); + }); + + if (!(await knex.schema.hasTable("userconfigs"))) + await knex.schema.createTable("userconfigs", function (table) { + table.string("id").primary(); + table.string("data"); + }); + var user = client.user.toJSON(); for (const prompt of fs.readdirSync(promptsDir)) {