From 794265d93685a5b1f7fa6439e7979f46695b71ab Mon Sep 17 00:00:00 2001 From: Mampfinator <46071499+Mampfinator@users.noreply.github.com> Date: Wed, 22 May 2024 20:41:03 +0200 Subject: [PATCH 1/3] basic implementation --- package.json | 1 + src/amiami/index.js | 1 + src/client.js | 21 ++++ src/deploy.js | 31 ++++++ src/index.js | 41 ++++--- src/settings.js | 251 +++++++++++++++++++++++++++++++++++++++++++ src/youtube/index.js | 1 + 7 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 src/client.js create mode 100644 src/deploy.js create mode 100644 src/settings.js diff --git a/package.json b/package.json index 48d329d..a60cf6a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "deploy": "node src/deploy.js", "start": "node src/index.js" }, "author": "", diff --git a/src/amiami/index.js b/src/amiami/index.js index 8bed75c..9b4be34 100644 --- a/src/amiami/index.js +++ b/src/amiami/index.js @@ -122,6 +122,7 @@ class AmiAmiFallbackPreview { * @see {@link AmiAmiFallbackPreview} */ const AmiAmiPreview = { + name: "AmiAmi", /** * @param {string} content * @returns {string[]} matches diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..019fdce --- /dev/null +++ b/src/client.js @@ -0,0 +1,21 @@ +const { Client, IntentsBitField: {Flags: IntentsFlags} } = require("discord.js"); +const { AmiAmiPreview } = require("./amiami"); +const { YouTubePreview } = require("./youtube"); +const sqlite = require("sqlite3"); + +const client = new Client({ + intents: [IntentsFlags.Guilds, IntentsFlags.GuildMessages, IntentsFlags.MessageContent], +}); + +client.db = new sqlite.Database(process.env.DB_PATH ?? "./data.db"); + +client.previews = [ + AmiAmiPreview, + YouTubePreview, +]; + +function getClient() { + return client; +} + +module.exports = { getClient }; \ No newline at end of file diff --git a/src/deploy.js b/src/deploy.js new file mode 100644 index 0000000..e935613 --- /dev/null +++ b/src/deploy.js @@ -0,0 +1,31 @@ +require("dotenv").config(); +const { getSettingsCommand } = require("./settings"); +const { getClient } = require("./client"); + + +const client = getClient(); + + +client.on("ready", async () => { + console.log(`Deploying commands for ${client.user.tag}.`); + + const debugGuild = process.env.DEBUG_GUILD; + + if (debugGuild) { + console.log(`Deploying commands to Guild ${debugGuild}`); + } else { + console.log("Deploying commands globally."); + } + + await client.application.commands.set([ + getSettingsCommand(client).builder, + ], debugGuild); + + await client.destroy(); + + console.log("Done."); + + process.exit(0); +}); + +client.login(process.env.DISCORD_TOKEN); \ No newline at end of file diff --git a/src/index.js b/src/index.js index e081d33..e2dc5b2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,22 +1,20 @@ require("dotenv").config(); -const { Client, IntentsBitField: {Flags: IntentsFlags} } = require("discord.js"); -const { AmiAmiPreview } = require("./amiami"); -const { YouTubePreview } = require("./youtube"); - -const client = new Client({ - intents: [IntentsFlags.Guilds, IntentsFlags.GuildMessages, IntentsFlags.MessageContent], -}); +const { getClient } = require("./client"); +const { Settings, getSettingsCommand } = require("./settings"); - -client.previews = [ - AmiAmiPreview, - YouTubePreview, -]; +const client = getClient(); client.on("messageCreate", async message => { if (message.author.bot) return; + if (!message.inGuild()) return; + + const settings = await Settings.forGuild(client.db, message.guildId); - for (const group of client.previews) { + const disabled = settings.disabled; + + const enabledPreviews = client.previews.filter(preview => !disabled.has(preview.name)); + + for (const group of enabledPreviews) { const matches = group.match(message.content); matches: for (const match of matches) { for (const generator of group.generators) { @@ -35,13 +33,30 @@ client.on("messageCreate", async message => { } }) +const settingsHandler = getSettingsCommand(client).handler; + +client.on("interactionCreate", async interaction => { + if (!interaction.isChatInputCommand()) return; + if (interaction.commandName === "settings") { + await settingsHandler(interaction); + } +}); + + + async function main() { for (const matcher of client.previews) { + console.log(`Initializing ${matcher.generators.length} generators for "${matcher.name}".`); + await matcher.init?.(); + for (const generator of matcher.generators) { await generator.init?.(); } } + console.log(`Initializing settings.`); + await Settings.init(client.db); + await client.login(process.env.DISCORD_TOKEN); console.log(`Logged into Discord as ${client.user.tag}.`); } diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000..c90752a --- /dev/null +++ b/src/settings.js @@ -0,0 +1,251 @@ +const { Colors, PermissionFlagsBits, SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +/** + * Per-guild settings. + * + * Construct with `Settings.forGuild(db, guildId)`. + */ +class Settings { + /** + * @type {Map} + */ + static instances = new Map(); + + /** + * Whether the settings have changed since the last save. + */ + #changed = false; + + /** + * @type {{guild: string, disabled: string | null, preferredStyle: string | null}} + */ + #row; + /** + * @type {sqlite.Database} + */ + #db; + + constructor(db, row) { + this.#db = db; + this.#row = row; + } + + async save() { + if (!this.#changed) return; + + return new Promise((res, rej) => { + this.#db.run("UPDATE settings SET disabled = (?), preferredStyle = (?) WHERE guild = (?)", [this.#row.disabled, this.#row.preferredStyle, this.#row.guild], (err) => { + if (err) rej(err); + this.#changed = false; + + res(); + }); + }); + } + + /** + * @type {string} + */ + get guildId() { + return this.#row.guild; + } + + /** + * @type {Set} + */ + get disabled() { + const raw = this.#row.disabled; + + return raw ? new Set(raw.split(",")) : new Set(); + } + + /** + * Disable a preview provider. + */ + disable(previewName) { + const disabled = this.disabled; + + if (disabled.has(previewName)) this.#changed = true; + + disabled.add(previewName); + + this.#row.disabled = Array.from(disabled).join(","); + + return this; + } + + /** + * Enable a preview provider. + */ + enable(previewName) { + const disabled = this.disabled; + + if (disabled.has(previewName)) this.#changed = true; + + disabled.delete(previewName); + + this.#row[2] = Array.from(disabled).join(","); + + return this; + } + + get preferredStyle() { + return this.#row.preferredStyle ?? "full"; + } + + set preferredStyle(value) { + if (value != "full" && value != "compact") throw new TypeError("Prefered style must be 'full' or 'compact'"); + + this.#changed = true; + this.#row.preferredStyle = value; + } + + /** + * @returns {Promise} + */ + static async forGuild(db, id) { + if (Settings.instances.has(id)) return Settings.instances.get(id); + + let row = await new Promise((res, rej) => db.get("SELECT * FROM settings WHERE guild = (?)", [id], (err, row) => { + if (err) rej(err); + res(row); + })); + + if (!row) { + await new Promise((res, rej) => db.run("INSERT INTO settings VALUES (?, ?, ?)", [id, null, null], (err) => { + if (err) rej(err); + res(); + })); + + row = await new Promise((res, rej) => db.get("SELECT * FROM settings WHERE guild = (?)", [id], (err, row) => { + if (err) rej(err); + res(row); + })); + } + + this.instances.set(id, new Settings(db, row)); + + return new Settings(db, row); + } + + + static async init(db) { + return new Promise((res, rej) => db.run("CREATE TABLE IF NOT EXISTS settings (guild TEXT UNIQUE NOT NULL, disabled TEXT, preferredStyle TEXT)", (err) => { + if (err) rej(err); + res(); + })); + } +} + +function getSettingsCommand(client) { + /** + * @type {[string, string][]} + */ + const previewNames = client.previews.map(preview => preview.name); + + return { + builder: new SlashCommandBuilder() + .setName("settings") + .setDescription("View or modify your settings.") + .setDMPermission(false) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand(view => view + .setName("view") + .setDescription("View your settings.") + ) + .addSubcommand(disable => disable + .setName("disable") + .setDescription("Disable a preview provider.") + .addStringOption(option => option + .setName("provider") + .setDescription("The provider to disable.") + .setChoices(...previewNames.map(name => ({ name, value: name }))) + .setRequired(true) + ) + ) + .addSubcommand(enable => enable + .setName("enable") + .setDescription("Enable a preview provider.") + .addStringOption(option => option + .setName("provider") + .setDescription("The provider to enable.") + .setChoices(...previewNames.map(name => ({ name, value: name }))) + .setRequired(true) + ) + ) + .addSubcommand(style => style + .setName("style") + .setDescription("Set the preferred preview style.") + .addStringOption(option => option + .setName("style") + .setDescription("The preferred preview style.") + .setChoices({ name: "Full", value: "full" }, { name: "Compact", value: "compact" }) + .setRequired(true) + ) + ), + handler: async interaction => { + if (!interaction.inGuild()) throw new Error("Must be in a server to use this command."); + + const subcommand = interaction.options.getSubcommand(); + + const settings = await Settings.forGuild(client.db, interaction.guildId); + switch (subcommand) { + case "view": { + const disabled = settings.disabled; + const enabled = interaction.client.previews.filter(preview => !disabled.has(preview.name)).map(preview => preview.name); + const style = settings.preferredStyle; + + const embed = new EmbedBuilder() + .setAuthor({ + name: interaction.guild.name, + iconURL: interaction.guild.iconURL(), + }) + .setTitle("Settings") + .addFields( + { name: "Disabled", value: disabled.size ? Array.from(disabled).join(", ") : "None", inline: true }, + { name: "Enabled", value: enabled.length ? Array.from(enabled).join(", ") : "None", inline: true }, + { name: "Preferred style", value: style, inline: false }, + ) + .setColor(Colors.Aqua); + + return interaction.reply({ embeds: [embed] }); + } + + case "disable": { + const provider = interaction.options.getString("provider"); + settings.disable(provider); + await settings.save(); + return interaction.reply({ embeds: [ + new EmbedBuilder() + .setDescription(`Disabled ${provider}.`) + .setColor(Colors.Green) + ]}); + } + + case "enable": { + const provider = interaction.options.getString("provider"); + settings.enable(provider); + await settings.save(); + return interaction.reply({ embeds: [ + new EmbedBuilder() + .setDescription(`Enabled ${provider}.`) + .setColor(Colors.Green) + ]}); + } + + case "style": { + const style = interaction.options.getString("style"); + settings.setPreferredStyle(style); + await settings.save(); + return interaction.reply({ embeds: [ + new EmbedBuilder() + .setDescription(`Set preferred style to ${style}.`) + .setColor(Colors.Green) + ]}); + } + } + } + } +} + +module.exports = { Settings, getSettingsCommand }; \ No newline at end of file diff --git a/src/youtube/index.js b/src/youtube/index.js index dfccedc..d01aa0c 100644 --- a/src/youtube/index.js +++ b/src/youtube/index.js @@ -100,6 +100,7 @@ function postToEmbed(post, channel) { } const YouTubePreview = { + name: "YouTube (Community Posts)", match(content) { return [...content.matchAll(postIdRegex)].map(match => typeof match == "string" ? match : match[0]); }, From 2c9d7c68553e995671141d664419c663766ab68f Mon Sep 17 00:00:00 2001 From: Mampfinator <46071499+Mampfinator@users.noreply.github.com> Date: Sun, 26 May 2024 17:30:48 +0200 Subject: [PATCH 2/3] separate `/settings` builder and handler --- src/deploy.js | 2 +- src/index.js | 4 +- src/settings.js | 197 ++++++++++++++++++++++++------------------------ 3 files changed, 100 insertions(+), 103 deletions(-) diff --git a/src/deploy.js b/src/deploy.js index e935613..aacae2b 100644 --- a/src/deploy.js +++ b/src/deploy.js @@ -18,7 +18,7 @@ client.on("ready", async () => { } await client.application.commands.set([ - getSettingsCommand(client).builder, + getSettingsCommand(client), ], debugGuild); await client.destroy(); diff --git a/src/index.js b/src/index.js index e2dc5b2..d1dec50 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ require("dotenv").config(); const { getClient } = require("./client"); -const { Settings, getSettingsCommand } = require("./settings"); +const { Settings, settingsHandler } = require("./settings"); const client = getClient(); @@ -33,8 +33,6 @@ client.on("messageCreate", async message => { } }) -const settingsHandler = getSettingsCommand(client).handler; - client.on("interactionCreate", async interaction => { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === "settings") { diff --git a/src/settings.js b/src/settings.js index c90752a..41c7c25 100644 --- a/src/settings.js +++ b/src/settings.js @@ -139,113 +139,112 @@ class Settings { function getSettingsCommand(client) { /** - * @type {[string, string][]} + * @type {string[]} */ const previewNames = client.previews.map(preview => preview.name); - return { - builder: new SlashCommandBuilder() - .setName("settings") - .setDescription("View or modify your settings.") - .setDMPermission(false) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) - .addSubcommand(view => view - .setName("view") - .setDescription("View your settings.") + return new SlashCommandBuilder() + .setName("settings") + .setDescription("View or modify your settings.") + .setDMPermission(false) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand(view => view + .setName("view") + .setDescription("View your settings.") + ) + .addSubcommand(disable => disable + .setName("disable") + .setDescription("Disable a preview provider.") + .addStringOption(option => option + .setName("provider") + .setDescription("The provider to disable.") + .setChoices(...previewNames.map(name => ({ name, value: name }))) + .setRequired(true) ) - .addSubcommand(disable => disable - .setName("disable") - .setDescription("Disable a preview provider.") - .addStringOption(option => option - .setName("provider") - .setDescription("The provider to disable.") - .setChoices(...previewNames.map(name => ({ name, value: name }))) - .setRequired(true) - ) - ) - .addSubcommand(enable => enable - .setName("enable") - .setDescription("Enable a preview provider.") - .addStringOption(option => option - .setName("provider") - .setDescription("The provider to enable.") - .setChoices(...previewNames.map(name => ({ name, value: name }))) - .setRequired(true) - ) + ) + .addSubcommand(enable => enable + .setName("enable") + .setDescription("Enable a preview provider.") + .addStringOption(option => option + .setName("provider") + .setDescription("The provider to enable.") + .setChoices(...previewNames.map(name => ({ name, value: name }))) + .setRequired(true) ) - .addSubcommand(style => style + ) + .addSubcommand(style => style + .setName("style") + .setDescription("Set the preferred preview style.") + .addStringOption(option => option .setName("style") - .setDescription("Set the preferred preview style.") - .addStringOption(option => option - .setName("style") - .setDescription("The preferred preview style.") - .setChoices({ name: "Full", value: "full" }, { name: "Compact", value: "compact" }) - .setRequired(true) + .setDescription("The preferred preview style.") + .setChoices({ name: "Full", value: "full" }, { name: "Compact", value: "compact" }) + .setRequired(true) + ) + ); +} + +async function settingsHandler(interaction) { + if (!interaction.inGuild()) throw new Error("Must be in a server to use this command."); + + const subcommand = interaction.options.getSubcommand(); + + const settings = await Settings.forGuild(client.db, interaction.guildId); + switch (subcommand) { + case "view": { + const disabled = settings.disabled; + const enabled = interaction.client.previews.filter(preview => !disabled.has(preview.name)).map(preview => preview.name); + const style = settings.preferredStyle; + + const embed = new EmbedBuilder() + .setAuthor({ + name: interaction.guild.name, + iconURL: interaction.guild.iconURL(), + }) + .setTitle("Settings") + .addFields( + { name: "Disabled", value: disabled.size ? Array.from(disabled).join(", ") : "None", inline: true }, + { name: "Enabled", value: enabled.length ? Array.from(enabled).join(", ") : "None", inline: true }, + { name: "Preferred style", value: style, inline: false }, ) - ), - handler: async interaction => { - if (!interaction.inGuild()) throw new Error("Must be in a server to use this command."); - - const subcommand = interaction.options.getSubcommand(); - - const settings = await Settings.forGuild(client.db, interaction.guildId); - switch (subcommand) { - case "view": { - const disabled = settings.disabled; - const enabled = interaction.client.previews.filter(preview => !disabled.has(preview.name)).map(preview => preview.name); - const style = settings.preferredStyle; - - const embed = new EmbedBuilder() - .setAuthor({ - name: interaction.guild.name, - iconURL: interaction.guild.iconURL(), - }) - .setTitle("Settings") - .addFields( - { name: "Disabled", value: disabled.size ? Array.from(disabled).join(", ") : "None", inline: true }, - { name: "Enabled", value: enabled.length ? Array.from(enabled).join(", ") : "None", inline: true }, - { name: "Preferred style", value: style, inline: false }, - ) - .setColor(Colors.Aqua); - - return interaction.reply({ embeds: [embed] }); - } - - case "disable": { - const provider = interaction.options.getString("provider"); - settings.disable(provider); - await settings.save(); - return interaction.reply({ embeds: [ - new EmbedBuilder() - .setDescription(`Disabled ${provider}.`) - .setColor(Colors.Green) - ]}); - } - - case "enable": { - const provider = interaction.options.getString("provider"); - settings.enable(provider); - await settings.save(); - return interaction.reply({ embeds: [ - new EmbedBuilder() - .setDescription(`Enabled ${provider}.`) - .setColor(Colors.Green) - ]}); - } - - case "style": { - const style = interaction.options.getString("style"); - settings.setPreferredStyle(style); - await settings.save(); - return interaction.reply({ embeds: [ - new EmbedBuilder() - .setDescription(`Set preferred style to ${style}.`) - .setColor(Colors.Green) - ]}); - } - } + .setColor(Colors.Aqua); + + return interaction.reply({ embeds: [embed] }); + } + + case "disable": { + const provider = interaction.options.getString("provider"); + settings.disable(provider); + await settings.save(); + return interaction.reply({ embeds: [ + new EmbedBuilder() + .setDescription(`Disabled ${provider}.`) + .setColor(Colors.Green) + ]}); + } + + case "enable": { + const provider = interaction.options.getString("provider"); + settings.enable(provider); + await settings.save(); + return interaction.reply({ embeds: [ + new EmbedBuilder() + .setDescription(`Enabled ${provider}.`) + .setColor(Colors.Green) + ]}); + } + + case "style": { + const style = interaction.options.getString("style"); + settings.preferredStyle = style; + await settings.save(); + return interaction.reply({ embeds: [ + new EmbedBuilder() + .setDescription(`Set preferred style to ${style}.`) + .setColor(Colors.Green) + ]}); } } } -module.exports = { Settings, getSettingsCommand }; \ No newline at end of file +module.exports = { Settings, getSettingsCommand, settingsHandler }; \ No newline at end of file From 95a9e234bcbd5da01c3a786bcbce52b710b693ed Mon Sep 17 00:00:00 2001 From: Mampfinator <46071499+Mampfinator@users.noreply.github.com> Date: Fri, 9 Aug 2024 13:11:08 +0200 Subject: [PATCH 3/3] fix settings instance caching --- src/settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/settings.js b/src/settings.js index 41c7c25..6ebdb18 100644 --- a/src/settings.js +++ b/src/settings.js @@ -123,9 +123,9 @@ class Settings { })); } - this.instances.set(id, new Settings(db, row)); - - return new Settings(db, row); + const settings = new Settings(db, row); + Settings.instances.set(id, settings); + return settings; }