From ff2e2ea58779560eca155176bea27dc920ea6d6c Mon Sep 17 00:00:00 2001
From: Ashley Graves <ashley@possum.city>
Date: Fri, 11 Oct 2024 14:07:01 +0200
Subject: [PATCH] revamp booru

---
 .gitignore                                    |  7 +-
 .../credentials.example.json                  |  0
 config/emojis.example.json                    | 16 +++
 src/commands/fun/blacklist.js                 |  2 +-
 src/commands/fun/gelbooru.js                  | 72 +++++++-------
 src/commands/fun/serverconfig.js              | 98 +++++++++++++++++++
 src/commands/fun/userconfig.js                | 62 ++++++++++++
 src/index.js                                  | 12 +++
 8 files changed, 231 insertions(+), 38 deletions(-)
 rename credentials.example.json => config/credentials.example.json (100%)
 create mode 100644 config/emojis.example.json
 create mode 100644 src/commands/fun/serverconfig.js
 create mode 100644 src/commands/fun/userconfig.js

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)) {