From 2eaadf2208cb155552c70ad9a1c660871a0182e8 Mon Sep 17 00:00:00 2001 From: Ashley Graves Date: Thu, 10 Oct 2024 08:34:00 +0200 Subject: [PATCH] booru --- package.json | 1 + pnpm-lock.yaml | 33 +++-- src/commands/accessibility/describe.js | 4 +- src/commands/accessibility/summarize.js | 18 +-- src/commands/ai/prompt.js | 4 +- src/commands/ai/query.js | 4 +- src/commands/fun/gelbooru.js | 175 ++++++++++++++++++++++++ src/commands/utility/message.js | 8 +- src/commands/utility/user.js | 8 +- 9 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 src/commands/fun/gelbooru.js diff --git a/package.json b/package.json index 6a59213..d0cec1a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "author": "", "license": "MIT", "dependencies": { + "booru": "^2.6.8", "discord.js": "^14.16.3", "dotenv": "^16.4.5", "groq-sdk": "^0.7.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f95256a..66393b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + booru: + specifier: ^2.6.8 + version: 2.6.8 discord.js: specifier: ^14.16.3 version: 14.16.3 @@ -17,9 +20,6 @@ importers: groq-sdk: specifier: ^0.7.0 version: 0.7.0 - uwuifier: - specifier: ^4.2.2 - version: 4.2.2 packages: @@ -90,6 +90,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + booru@2.6.8: + resolution: {integrity: sha512-/MN9Q5Yd3jBvOXgG05SLsdbGHFUWx6yoLYKU0xIoTWDsNlliUqM8Aj581pBujqFhg4E/Pi9cHdpMRN/FqustUA==} + engines: {node: '>=10.0.0'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -122,6 +126,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-xml-parser@4.5.0: + resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==} + hasBin: true + form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} @@ -172,6 +180,9 @@ packages: encoding: optional: true + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -191,9 +202,6 @@ packages: resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} engines: {node: '>=18.17'} - uwuifier@4.2.2: - resolution: {integrity: sha512-Fhtj3Yg3rrDTEs+L1fqkgM3VYGJrP4U9GY+4fPdw08T5kzOB8NSosZnZfa5Zhv1Hh7NCpH5G6t1DIZyM1wPK1w==} - web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -303,6 +311,11 @@ snapshots: asynckit@0.4.0: {} + booru@2.6.8: + dependencies: + fast-xml-parser: 4.5.0 + undici: 6.19.8 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -339,6 +352,10 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-xml-parser@4.5.0: + dependencies: + strnum: 1.0.5 + form-data-encoder@1.7.2: {} form-data@4.0.0: @@ -388,6 +405,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + strnum@1.0.5: {} + tr46@0.0.3: {} ts-mixer@6.0.4: {} @@ -400,8 +419,6 @@ snapshots: undici@6.19.8: {} - uwuifier@4.2.2: {} - web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {} diff --git a/src/commands/accessibility/describe.js b/src/commands/accessibility/describe.js index 89a1247..38b9348 100644 --- a/src/commands/accessibility/describe.js +++ b/src/commands/accessibility/describe.js @@ -1,7 +1,7 @@ -const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType, AttachmentBuilder, EmbedBuilder } = require('discord.js'); +const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType, AttachmentBuilder, EmbedBuilder } = require("discord.js"); const data = new ContextMenuCommandBuilder() - .setName('Describe Image(s)') + .setName("Describe Image(s)") .setType(ApplicationCommandType.Message) .setContexts([ InteractionContextType.Guild, diff --git a/src/commands/accessibility/summarize.js b/src/commands/accessibility/summarize.js index 815e75a..9e69a7c 100644 --- a/src/commands/accessibility/summarize.js +++ b/src/commands/accessibility/summarize.js @@ -1,7 +1,7 @@ -const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType } = require('discord.js'); +const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType } = require("discord.js"); const data = new ContextMenuCommandBuilder() - .setName('Summarize') + .setName("Summarize") .setType(ApplicationCommandType.Message) .setContexts([ InteractionContextType.Guild, @@ -23,13 +23,13 @@ module.exports = { const summary = await groq.chat.completions.create({ messages: [{ - role: "user", - content: interaction.client.prompts.summary - }, - { - role: "user", - content: message.content - } + role: "user", + content: interaction.client.prompts.summary + }, + { + role: "user", + content: message.content + } ], "model": interaction.defaultModel }); diff --git a/src/commands/ai/prompt.js b/src/commands/ai/prompt.js index 9c4f067..19f6183 100644 --- a/src/commands/ai/prompt.js +++ b/src/commands/ai/prompt.js @@ -1,7 +1,7 @@ -const { InteractionContextType, ApplicationIntegrationType, SlashCommandBuilder, EmbedBuilder } = require('discord.js'); +const { InteractionContextType, ApplicationIntegrationType, SlashCommandBuilder, EmbedBuilder } = require("discord.js"); const data = new SlashCommandBuilder() - .setName('prompt') + .setName("prompt") .setDescription("Prompt an AI model with data") .addStringOption(builder => builder // diff --git a/src/commands/ai/query.js b/src/commands/ai/query.js index c90e4f9..a00471e 100644 --- a/src/commands/ai/query.js +++ b/src/commands/ai/query.js @@ -1,7 +1,7 @@ -const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType } = require('discord.js'); +const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType } = require("discord.js"); const data = new ContextMenuCommandBuilder() - .setName('Query AI') + .setName("Query AI") .setType(ApplicationCommandType.Message) .setContexts([ InteractionContextType.Guild, diff --git a/src/commands/fun/gelbooru.js b/src/commands/fun/gelbooru.js new file mode 100644 index 0000000..73684a0 --- /dev/null +++ b/src/commands/fun/gelbooru.js @@ -0,0 +1,175 @@ +const { InteractionContextType, ApplicationIntegrationType, SlashCommandBuilder, EmbedBuilder, escapeMarkdown, bold } = require("discord.js"); +const { basename, extname } = require("node:path"); +const Booru = require('booru'); + +const boorus = []; +for (const site of Object.keys(Booru.sites)) { + boorus.push({ + name: site, + value: site + }) +} + +const defaultBooru = "gelbooru.com"; + +const data = new SlashCommandBuilder() + .setName("booru") + .setDescription("Select a random image from any booru") + .addStringOption(builder => + builder // + .setName("tags") + .setRequired(true) + .setDescription("Tags to search for") + ) + .addStringOption(builder => + builder // + .setName("booru") + .setRequired(false) + .setDescription("Booru board to search (default: gelbooru)") + .addChoices(boorus) + ) + .setContexts([ + InteractionContextType.Guild, + InteractionContextType.BotDM, + InteractionContextType.PrivateChannel + ]) + .setIntegrationTypes([ + ApplicationIntegrationType.GuildInstall, + ApplicationIntegrationType.UserInstall + ]); + +function isEmbeddableFileType(ext) { + return ['.jpg', '.jpeg', '.png', '.gif'].includes(ext) +} + +function notEmpty(str) { + return str.trim() !== '' +} + +const regexCutTags = /[\S\s]{1,75}[^,]{0,25}/; +function formatTags(tags) { + const tagString = tags.join(', ') + + if (tagString.length < 100) { + return tagString + } + + const tagCutMatch = tagString.match(regexCutTags) ?? [] + return `${escapeMarkdown(tagCutMatch[0] ?? '')}...` +} + +const ratingEmojis = { + s: '<:rating_safe:1293819920978804829>', + g: '<:rating_general:1293819929199513610>', + q: '<:rating_questionable:1293819907099725925>', + e: '<:rating_explicit:1293819893795389491>', + u: '<:rating_unknown:1293819936845594665>', +} + +function formatRating(rating) { + return ratingEmojis[rating] ?? rating.toUpperCase() +} + +function formatScore(score) { + if (score > 0) { + return `<:green_arrow_up:1293819944399667222> ${score}` + } else if (score < 0) { + return `<:red_arrow_down:1293819951764869181> ${score}` + } else { + return `<:yellow_tilde:1293819958643396608> ${score}` + } +} + +function isNSFWPost(post) { + return !['s', 'g'].includes(post.rating) +} + +function formatTime(time) { + return `${(Number(time) / 1e6).toFixed(2)}ms` +} + +module.exports = { + data, + async execute(interaction) { + const tags = interaction.options.getString("tags").split(" "); + const booru = interaction.options.getString("booru") ?? defaultBooru; + + await interaction.deferReply(); + console.log(booru, tags); + + const startTime = process.hrtime.bigint(); + + var post = null; + while (post == null) { + var newPost = (await Booru.search(booru, tags, { limit: 1, random: true }))[0]; + if (newPost == null) { + await interaction.followUp("Could not find any post matching tags."); + return; + } + if (isNSFWPost(newPost)) continue; + post = newPost; + } + + const endTime = process.hrtime.bigint(); + const timeTaken = endTime - startTime; + + const ext = extname((post['data']).file_name ?? post.fileUrl ?? '').toLowerCase(); + + const leadingDescription = [ + `**Score:** ${formatScore(post.score)}`, + `**Rating:** ${formatRating(post.rating)}`, + `[File URL](${post.fileUrl})`, + `\`${ext}\``, + ].join(' | ') + + const description = [leadingDescription, `**Tags:** ${formatTags(post.tags)}`] + .filter(notEmpty) + .join('\n') + + const footerText = [ + post.booru.domain, + post.id, + timeTaken ? formatTime(timeTaken) : '', + ].filter(notEmpty).join(' ยท ') + + + if (isEmbeddableFileType(ext)) { + const embed = new EmbedBuilder() + .setColor("#cba6f7") + .setTitle(`Post #${post.id}`) + .setURL(post.postView) + //.setDescription(description) + .setFields([{ + name: "Score", + value: formatScore(post.score), + inline: true + }, { + name: "Rating", + value: formatRating(post.rating), + inline: true + }, { + name: "File", + value: `[${basename(post.fileUrl)}](${post.fileUrl})`, + inline: true + }, { + name: "Tags", + value: formatTags(post.tags), + inline: false + }]) + .setImage(post.fileUrl) + .setFooter({ + text: footerText, + iconURL: `https://${post.booru.domain}/favicon.ico`, + }) + + await interaction.followUp({ embeds: [embed] }); + } else { + await interaction.followUp( + '>>> ' + bold(`[Post #${post.id}](<${post.postView}>)`) + "\n" + + description + "\n" + + footerText + ); + } + + }, +}; \ No newline at end of file diff --git a/src/commands/utility/message.js b/src/commands/utility/message.js index 9a174dc..b35cc0f 100644 --- a/src/commands/utility/message.js +++ b/src/commands/utility/message.js @@ -1,7 +1,7 @@ -const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType, AttachmentBuilder } = require('discord.js'); +const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType, AttachmentBuilder } = require("discord.js"); const data = new ContextMenuCommandBuilder() - .setName('Message Information') + .setName("Message Information") .setType(ApplicationCommandType.Message) .setContexts([ InteractionContextType.Guild, @@ -19,8 +19,8 @@ module.exports = { await interaction.reply({ files: [ new AttachmentBuilder() - .setName(interaction.targetMessage.id + ".json") - .setFile(Buffer.from(JSON.stringify(interaction.targetMessage.toJSON(), null, 4), "utf-8")) + .setName(interaction.targetMessage.id + ".json") + .setFile(Buffer.from(JSON.stringify(interaction.targetMessage.toJSON(), null, 4), "utf-8")) ], ephemeral: true }); diff --git a/src/commands/utility/user.js b/src/commands/utility/user.js index f3b71cf..6c07bec 100644 --- a/src/commands/utility/user.js +++ b/src/commands/utility/user.js @@ -1,7 +1,7 @@ -const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType } = require('discord.js'); +const { ContextMenuCommandBuilder, ApplicationCommandType, InteractionContextType, ApplicationIntegrationType } = require("discord.js"); const data = new ContextMenuCommandBuilder() - .setName('User Information') + .setName("User Information") .setType(ApplicationCommandType.User) .setContexts([ InteractionContextType.Guild, @@ -19,8 +19,8 @@ module.exports = { await interaction.reply({ files: [ new AttachmentBuilder() - .setName(interaction.targetUser.id + ".json") - .setFile(Buffer.from(JSON.stringify(interaction.targetUser.toJSON(), null, 4), "utf-8")) + .setName(interaction.targetUser.id + ".json") + .setFile(Buffer.from(JSON.stringify(interaction.targetUser.toJSON(), null, 4), "utf-8")) ], ephemeral: true });