From 192d1431c19e1fb49ee9f13103a29306f6d75cf4 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sun, 14 Sep 2025 16:24:18 -0600 Subject: [PATCH] Add hub settings and setup maybe? --- .env.example | 9 +++ commands.js | 10 +++ commands/link.js | 2 +- commands/settings.js | 47 ++++++++++++ commands/setup.js | 46 ++++++++++++ messageHandlers/hub_settings.js | 128 ++++++++++++++++++++++++++++++++ 6 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 commands/settings.js create mode 100644 commands/setup.js create mode 100644 messageHandlers/hub_settings.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..36ed2ab --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +SESSION_SECRET=superlongandsecretrandomstring +DB_HOST=localhost +DB_PORT=3306 +DB_USER=mystore +DB_PASS=mystore +DB_NAME=mystoredb +NODE_ENV=production +DISCORD_TOKEN=my discord bot token +BASE_URL=https://np.example.com \ No newline at end of file diff --git a/commands.js b/commands.js index 067b061..cb27ef7 100644 --- a/commands.js +++ b/commands.js @@ -220,6 +220,16 @@ module.exports = { { name: "hub", description: "Get the hub URL.", + }, + { + name: "setup", + description: "Setup the bot for this server", + default_member_permissions: 0 + }, + { + name: "settings", + description: "View and manage hub settings", + default_member_permissions: 0 } ], admin: [] diff --git a/commands/link.js b/commands/link.js index 7ea4e86..b9a398d 100644 --- a/commands/link.js +++ b/commands/link.js @@ -9,7 +9,7 @@ const execute = async (interaction) => { const [pairing] = await pool.query('SELECT * FROM users WHERE pairingCode = ?', [pairingCode]); if (!pairing) return interaction.reply({ content: "Invalid pairing code", ephemeral: true }); await pool.query('UPDATE users SET discordId = ?, discordDisplayName = ?, pairingCode = NULL WHERE pairingCode = ?', [discordID, interaction.user.displayName, pairingCode]); - return interaction.reply({ content: "Successfully linked your account", ephemeral: true }); + return interaction.reply({ content: "Successfully linked your account. Please relaunch the hub!", ephemeral: true }); } module.exports = { execute } \ No newline at end of file diff --git a/commands/settings.js b/commands/settings.js new file mode 100644 index 0000000..474befb --- /dev/null +++ b/commands/settings.js @@ -0,0 +1,47 @@ +const client = global.discord_client +const pool = global.db_pool; +const hubSettingsHandler = require('../messageHandlers/hub_settings.js'); + +if (!global.productCreationData) global.productCreationData = {}; + +const execute = async (interaction) => { + if (!interaction.guildId) return interaction.reply({ content: "This command can only be used in a server", ephemeral: true }); + if (global.hubSettingsHandlers[interaction.user.id]) return interaction.reply({ content: "See DMs!", ephemeral: true }); + try { + const guildId = interaction.guildId; + const hubResult = await pool.query(`SELECT * FROM hubs WHERE discordGuild = ?`, [guildId]); + if (hubResult.length === 0) { + delete global.hubSettingsHandlers[interaction.user.id]; + return interaction.reply({ content: "This guild does not have a hub set up!", ephemeral: true }); + } + // Proceed with settings menu + await interaction.reply({ ephemeral: true, content: "Getting things ready..." }); + await interaction.user.send({ embeds: [{ + title: `Hub Settings for ${hubResult[0].name}`, + description: `**Short Description:** ${hubResult[0].shortDescription}\n**Long Description:** ${hubResult[0].longDescription}\n**Allow Gift Purchase:** ${hubResult[0].allowGiftPurchase ? "Yes" : "No"}\n**Terms of Service:** ${hubResult[0].tos}\n\nHub Secret Key: ||${hubResult[0].secretKey}||`, + color: 0x00AE86, + fields: [ + { name: "Main Menu", value: "Type an option number, or `cancel` to close." }, + { name: "1. Edit Short Description", value: "Change the short description of the hub." }, + { name: "2. Edit Long Description", value: "Change the long description of the hub." }, + { name: "3. Toggle Allow Gift Purchase", value: "Enable or disable allowing users to buy gifts for others on this hub." }, + { name: "4. Edit Terms of Service", value: "Change the terms of service for this hub." }, + { name: "5. Regenerate Secret Key", value: "Generate a new secret key for this hub. (Old key will be invalidated)" }, + { name: "6. Parcel Auto-Import", value: "Set up automatic importing of Parcel purchases when linking." } + ] + }] }); + await interaction.user.send({ content: "Please provide a description for the product. Say `cancel` to exit." }); + interaction.editReply({ephemeral: true, content: "Check your DMs!"}); + global.hubSettingsHandlers[interaction.user.id] = { + step: 1, + hub: hubResult[0].id + }; + global.dmHandlers[interaction.user.id] = hubSettingsHandler; + } catch (err) { + console.error(err); + delete global.productCreationData[interaction.user.id]; + return interaction.editReply({ content: "An error occurred.", ephemeral: true }); + } +}; + +module.exports = { execute } \ No newline at end of file diff --git a/commands/setup.js b/commands/setup.js new file mode 100644 index 0000000..90e6827 --- /dev/null +++ b/commands/setup.js @@ -0,0 +1,46 @@ +const client = global.discord_client +const pool = global.db_pool; +// /setup command to initialize a hub in the server +/* Hub table schema +CREATE TABLE IF NOT EXISTS hubs ( + id CHAR(36) PRIMARY KEY DEFAULT UUID(), + ownerId BIGINT, -- Owner of the hub (Discord ID) + discordGuild BIGINT, -- Discord Guild ID + name VARCHAR(128), -- Name of the hub + shortDescription VARCHAR(256), -- Short description of the hub + longDescription TEXT, -- Long description of the hub + allowGiftPurchase BOOLEAN DEFAULT TRUE, -- Allow users to buy gifts for others on this hub. + tos TEXT DEFAULT 'This Hub does not have any Terms of Service yet. If you are the Hub owner, you can update this under settings.', -- Terms of Service + bgmId BIGINT DEFAULT NULL, -- Background Music ID + secretKey VARCHAR(128) UNIQUE NOT NULL -- Secret key for the hub to authenticate +) + +*/ + +const crypto = require('crypto'); +const log = require('../utils/logger.js'); + +const execute = async (interaction) => { + if (!interaction.guildId) return interaction.reply({ content: "This command can only be used in a server", ephemeral: true }); + try { + const guildId = interaction.guildId; + const [existingHub] = await pool.query('SELECT * FROM hubs WHERE discordGuild = ?', [guildId]); + if (existingHub) return interaction.reply({ content: "This server already has a hub set up.", ephemeral: true }); + const ownerId = interaction.user.id; + const name = interaction.guild.name; + const shortDescription = "Our super cool product hub!"; + const longDescription = "Welcome to our super cool product hub! We offer a variety of awesome products for you to explore and purchase. Enjoy your stay!"; + const [result] = await pool.query('INSERT INTO hubs (ownerId, discordGuild, name, shortDescription, longDescription, secretKey) VALUES (?, ?, ?, ?, ?, ?)', [ownerId, guildId, name, shortDescription, longDescription, crypto.randomBytes(64).toString('hex')]); + + if (result.affectedRows === 1) { + return interaction.reply({ content: `Hub successfully created with ID: \`${result.insertId}\`. Get details via the /settings command!`, ephemeral: true }); + } else { + return interaction.reply({ content: "An error occurred while creating the hub. Please try again later.", ephemeral: true }); + } + } catch (error) { + log.error(error); + return interaction.reply({ content: "An error occurred while creating the hub. Please try again later.", ephemeral: true }); + } +} + +module.exports = { execute } \ No newline at end of file diff --git a/messageHandlers/hub_settings.js b/messageHandlers/hub_settings.js new file mode 100644 index 0000000..e6cc8c6 --- /dev/null +++ b/messageHandlers/hub_settings.js @@ -0,0 +1,128 @@ +const client = global.discord_client +const pool = global.db_pool; +const crypto = require('crypto'); +const fs = require('fs'); + + +const { pipeline } = require('stream'); +const { promisify } = require('util'); + +const streamPipeline = promisify(pipeline); + +/** + * Downloads a file from a URL to a specified destination. + * @param {string} url - The URL to download the file from. + * @param {string} dest - The destination file path to save the file. + * @param {function} cb - Callback function called on completion or error. + */ +async function download(url, dest, cb) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.statusText}`); + } + await streamPipeline(response.body, fs.createWriteStream(dest)); + cb(null); // Signal success + } catch (error) { + cb(error); // Pass error to callback + } +} + +const cancel = (user) => { + delete global.hubSettingsHandlers[user.id]; + delete global.dmHandlers[user.id]; + user.send('Closing hub settings.'); +} + +const opts = { // Map of option number to step number + 1: {step: 21, exec: () => { + return 'Please provide the new short description. Say `cancel` to exit.'; + }}, + 2: {step: 22, exec: () => { + return 'Please provide the new long description. Say `cancel` to exit.'; + }}, + 3: {step: 23, exec: async (hubId) => { + const [hub] = await pool.query('SELECT allowGiftPurchase FROM hubs WHERE id = ?', [hubId]); + if (!hub) return 'Error fetching hub data.'; + const newValue = hub.allowGiftPurchase ? 0 : 1; + await pool.query('UPDATE hubs SET allowGiftPurchase = ? WHERE id = ?', [newValue, hubId]); + return `Allow Gift Purchase is now set to: ${newValue ? "Yes" : "No"}.\n\nType another option number, or \`cancel\` to exit.`; + }}, + 4: {step: 24, exec: () => { + return 'Please provide the new Terms of Service. Say `cancel` to exit.'; + }}, + 5: {step: 25, exec: async (hubId) => { + const newKey = crypto.randomBytes(16).toString('hex'); + await pool.query('UPDATE hubs SET secretKey = ? WHERE id = ?', [newKey, hubId]); + return `New secret key generated: ||${newKey}||\n\nType another option number, or \`cancel\` to exit.`; + }}, + 6: {step: 26, exec: () => { + return 'Parcel Auto-Import setup is not yet implemented. Type another option number, or `cancel` to exit.'; + }} +} + +const execute = async (message) => { + switch (global.hubSettingsHandlers[message.author.id].step) { + case 1: // Main Menu + if (message.content.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + const option = parseInt(message.content.trim()); + if (!opts[option]) { + message.channel.send('Invalid option. Please enter a valid option number or `cancel` to exit.'); + return; + } + global.hubSettingsHandlers[message.author.id].step = opts[option].step; + const response = await opts[option].exec(global.hubSettingsHandlers[message.author.id].hub); + message.channel.send(response); + break; + case 21: // Edit Short Description + const shortDesc = message.content.trim(); + if (shortDesc.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (shortDesc.length > 256) { + message.channel.send('Short description is too long. Please limit to 256 characters.'); + return; + } + await pool.query('UPDATE hubs SET shortDescription = ? WHERE id = ?', [shortDesc, global.hubSettingsHandlers[message.author.id].hub]); + global.hubSettingsHandlers[message.author.id].step = 1; + message.channel.send('Short description updated.\n\nType an option number, or `cancel` to exit.'); + break; + case 22: // Edit Long Description + const longDesc = message.content.trim(); + if (longDesc.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (longDesc.length > 5000) { + message.channel.send('Long description is too long. Please limit to 5000 characters.'); + return; + } + await pool.query('UPDATE hubs SET longDescription = ? WHERE id = ?', [longDesc, global.hubSettingsHandlers[message.author.id].hub]); + global.hubSettingsHandlers[message.author.id].step = 1; + message.channel.send('Long description updated.\n\nType an option number, or `cancel` to exit.'); + break; + case 24: // Edit Terms of Service + const tos = message.content.trim(); + if (tos.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (tos.length > 10000) { + message.channel.send('Terms of Service is too long. Please limit to 10000 characters.'); + return; + } + await pool.query('UPDATE hubs SET tos = ? WHERE id = ?', [tos, global.hubSettingsHandlers[message.author.id].hub]); + global.hubSettingsHandlers[message.author.id].step = 1; + message.channel.send('Terms of Service updated.\n\nType an option number, or `cancel` to exit.'); + break; + default: + message.channel.send('Invalid step.'); + cancel(message.author); + break; + } +} + module.exports = execute \ No newline at end of file