From 700ba6b1c92ef322dfec170ab4f446259f6e691a Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Thu, 16 Jan 2025 21:09:47 -0700 Subject: [PATCH] Force Push, guh --- commands.js | 113 +++++++++++++++++ commands/createproduct.js | 47 +++++++ commands/forcelink.js | 34 +++++ commands/give.js | 32 ++++- commands/retrieve.js | 9 +- commands/revoke.js | 24 +++- commands/unlink.js | 4 +- commands/update.js | 61 +++++++++ index.js | 11 +- messageHandlers/create_prod.js | 136 ++++++++++++++++++++ messageHandlers/update_prod.js | 143 ++++++++++++++++++++++ migrations/006_update_hubs_add_logChannel | 3 + routes/hub.js | 41 ++++++- routes/payments.js | 66 +++++++++- 14 files changed, 710 insertions(+), 14 deletions(-) create mode 100644 commands/createproduct.js create mode 100644 commands/forcelink.js create mode 100644 commands/update.js create mode 100644 messageHandlers/create_prod.js create mode 100644 messageHandlers/update_prod.js create mode 100644 migrations/006_update_hubs_add_logChannel diff --git a/commands.js b/commands.js index 281ec57..d2d9570 100644 --- a/commands.js +++ b/commands.js @@ -103,6 +103,119 @@ module.exports = { required: true } ] + }, + { + "name": "createproduct", + "description": "Create a product", + "default_member_permissions": 0, + "options": [ + { + "name": "name", + "description": "The name of the product", + "type": 3, + "required": true + } + ] + }, + { + name: "deleteproduct", + description: "Delete a product", + default_member_permissions: 0, + options: [ + { + name: "name", + description: "The name of the product", + type: Discord.ApplicationCommandOptionType.String, + required: true + } + ] + }, + { + name: "update", + description: "Update a product", + default_member_permissions: 0, + options: [ + { + name: "name", + description: "The name of the product", + type: Discord.ApplicationCommandOptionType.String, + required: true + }, + { + name: "field", + description: "The field to update", + type: Discord.ApplicationCommandOptionType.String, + required: true, + choices: [ + { + name: "Name", + value: "name" + }, + { + name: "Description", + value: "description" + }, + { + name: "Dev Product ID", + value: "devProductId" + }, + { + name: "Image ID", + value: "imageId" + }, + { + name: "File", + value: "file" + }, + { + name: "Stocking Info", + value: "stock" + }, + { + name: "Category", + value: "category" + } + ] + } + ] + }, + { + name: "forcelink", + description: "Force link a Roblox account to a Discord user", + default_member_permissions: 0, + options: [ + { + name: "roblox-id", + description: "The Roblox ID of the user", + type: Discord.ApplicationCommandOptionType.Number, + required: true + }, + { + name: "discord-id", + description: "The Discord ID of the user", + type: Discord.ApplicationCommandOptionType.User, + required: true + } + ] + }, + { + name: "forceunlink", + description: "Force unlink a Roblox account from a Discord user", + default_member_permissions: 0, + options: [ + { + name: "roblox-id", + description: "The Roblox ID of the user", + type: Discord.ApplicationCommandOptionType.Number, + required: false + }, + { + name: "discord-id", + description: "The Discord ID of the user", + type: Discord.ApplicationCommandOptionType.User, + required: false + } + ] } ], admin: [] diff --git a/commands/createproduct.js b/commands/createproduct.js new file mode 100644 index 0000000..6fc8d4d --- /dev/null +++ b/commands/createproduct.js @@ -0,0 +1,47 @@ +const client = global.discord_client +const pool = global.db_pool; +const createProdHandler = require('../messageHandlers/create_prod.js'); + +if (!global.productCreationData) global.productCreationData = {}; + +const execute = async (interaction) => { + console.log("Checking if user is already creating a product"); + if (global.productCreationData[interaction.user.id]) return interaction.reply({ content: "You are already creating a product!", ephemeral: true }); + global.productCreationData[interaction.user.id] = { + name: interaction.options.getString("name") + }; + try { + const productName = global.productCreationData[interaction.user.id].name; + const productResult = await pool.query(`SELECT * FROM products WHERE UPPER(name) = UPPER(?)`, [productName]); + if (productResult.length > 0) { + delete global.productCreationData[interaction.user.id]; + return interaction.reply({ content: "A product with this name already exists!", ephemeral: true }); + } + console.log("Checking guild hub"); + const guildId = interaction.guildId; + const hubResult = await pool.query(`SELECT * FROM hubs WHERE discordGuild = ?`, [guildId]); + if (hubResult.length === 0) { + console.log("No hub found"); + delete global.productCreationData[interaction.user.id]; + return interaction.reply({ content: "This guild does not have a hub set up!", ephemeral: true }); + } + console.log("Hub found"); + // Proceed with creation + await interaction.reply({ ephemeral: true, content: "Getting things ready..." }); + await interaction.user.send({ content: `Creating product: \`${productName}\`` }); + 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.productCreationData[interaction.user.id] = { + name: productName, + step: 1, + hub: hubResult[0].id + }; + global.dmHandlers[interaction.user.id] = createProdHandler; + } catch (err) { + console.error(err); + delete global.productCreationData[interaction.user.id]; + return interaction.editReply({ content: "An error occurred during the product creation process.", ephemeral: true }); + } +}; + +module.exports = { execute } \ No newline at end of file diff --git a/commands/forcelink.js b/commands/forcelink.js new file mode 100644 index 0000000..1bd820d --- /dev/null +++ b/commands/forcelink.js @@ -0,0 +1,34 @@ +const client = global.discord_client +const pool = global.db_pool; + +const execute = async (interaction) => { + const robloxId = interaction.options.getNumber("roblox-id"); + const discordID = interaction.options.getUser("discord-id")?.id; + if (!discordID) return interaction.reply({ content: "You must provide a Discord User", ephemeral: true }); + if (!robloxId) return interaction.reply({ content: "You must provide a Roblox ID", ephemeral: true }); + try { + const connection = await pool.getConnection(); + const [rows] = await connection.query("SELECT * FROM users WHERE robloxId = ?", [robloxId]); + + if (rows.length === 0) { + await connection.query("INSERT INTO users (robloxId, discordId) VALUES (?, ?)", [robloxId, discordID]); + } else { + const user = rows[0]; + if (!user.discordId) { + await connection.query("UPDATE users SET discordId = ? WHERE robloxId = ?", [discordID, robloxId]); + } else if (user.discordId !== discordID) { + await connection.query("UPDATE users SET discordId = NULL WHERE discordId = ?", [discordID]); + await connection.query("UPDATE users SET discordId = ? WHERE robloxId = ?", [discordID, robloxId]); + } + } + + connection.release(); + } catch (error) { + console.error(error); + return interaction.reply({ content: "An error occurred while linking your account", ephemeral: true }); + } + + return interaction.reply({ content: "Successfully linked your account", ephemeral: true }); +} + +module.exports = { execute } \ No newline at end of file diff --git a/commands/give.js b/commands/give.js index 01f7c6f..98b1397 100644 --- a/commands/give.js +++ b/commands/give.js @@ -15,6 +15,11 @@ const execute = async (interaction) => { if (!row) return interaction.reply({ content: "User not found", ephemeral: true }); robloxID = row.robloxId; } + + // Get the hub for the guild + const guildID = interaction.guild.id; + const [hub] = await pool.query('SELECT * FROM hubs WHERE discordGuild = ?', [guildID]); + if (!hub) return interaction.reply({ content: "Hub not found for this guild", ephemeral: true }); // Check if the user exists const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]); @@ -22,14 +27,39 @@ const execute = async (interaction) => { const productName = interaction.options.getString("product-name"); // try catch try and find the product based on partial product name, parse everything in uppercase to make things easier - const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]); + const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ? AND hubId = ?', [`%${productName.toUpperCase()}%`, hub.id]); if (!product) return interaction.reply({ content: "Product not found", ephemeral: true }); + // Check if the user already owns the product const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]); if (purchase) return interaction.reply({ content: "User already owns this product", ephemeral: true }); // Insert purchase into database await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [robloxID, product.id, product.hubId]); + try { + // Assuming you have a function to send a message to the user + dscUser = await client.users.fetch(user.discordId); + dscUser.send(`You have been givena copy of ${product.name}!\nUse \`/retrive ${product.name}\` in the Discord server to download it!`); + } catch (error) { + // Do nothing, user has privacy settings enabled + } + + if (hub.logChannel != null) { + try { + chan = await client.channels.fetch(hub.logChannel); + chan.send({ + embeds: [ + { + title: `Product Given`, + color: 0x00ff00, + description: `**Roblox ID:** ${user.robloxId}\n**Discord User:** <@${user.discordId}>\n**Product:** ${product.name}\n**Type:** Give` + } + ] + }) + } catch (error) { + // Do nothing, channel was deleted + } + } return interaction.reply({ content: `Gave \`${product.name}\` to ${robloxID}`, ephemeral: true }); }; diff --git a/commands/retrieve.js b/commands/retrieve.js index 8cd1e6c..3d7f551 100644 --- a/commands/retrieve.js +++ b/commands/retrieve.js @@ -11,9 +11,12 @@ const execute = async (interaction) => { // Check if the user exists if (!robloxID) return interaction.reply({ content: "User not found", ephemeral: true }); + const [hub] = await pool.query('SELECT * FROM hubs WHERE discordGuild = ?', [interaction.guild.id]); + if (!hub) return interaction.reply({ content: "Hub not found for this guild", ephemeral: true }); + const productName = interaction.options.getString("product-name"); // try catch try and find the product based on partial product name, parse everything in uppercase to make things easier - const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]); + const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ? AND hubId = ?', [`%${productName.toUpperCase()}%`, hub.id]); if (!product) return interaction.reply({ content: "Product not found", ephemeral: true }); // Check if the user already owns the product const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]); @@ -28,7 +31,7 @@ const execute = async (interaction) => { type: 2, label: 'Download', style: 5, - url: `https://cdn.example.com/${product.file}/${authToken}` + url: `${process.env.BASE_URL}/cdn/${product.file}/${authToken}` } ] } @@ -47,7 +50,7 @@ const execute = async (interaction) => { type: 2, label: 'Download', style: 5, - url: `https://cdn.example.com/${product.file}/${authToken}` + url: `${process.env.BASE_URL}/cdn/${product.file}/${authToken}` } ] } diff --git a/commands/revoke.js b/commands/revoke.js index b1371ee..d2c0a5a 100644 --- a/commands/revoke.js +++ b/commands/revoke.js @@ -20,9 +20,14 @@ const execute = async (interaction) => { const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]); if (!user) return interaction.reply({ content: "User not found", ephemeral: true }); + // Get the hub for the guild + const guildID = interaction.guild.id; + const [hub] = await pool.query('SELECT * FROM hubs WHERE discordGuild = ?', [guildID]); + if (!hub) return interaction.reply({ content: "Hub not found for this guild", ephemeral: true }); + const productName = interaction.options.getString("product-name"); // try catch try and find the product based on partial product name, parse everything in uppercase to make things easier - const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]); + const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ? AND hubId = ?', [`%${productName.toUpperCase()}%`, hub.id]); if (!product) return interaction.reply({ content: "Product not found", ephemeral: true }); // Check if the user already owns the product const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]); @@ -30,6 +35,23 @@ const execute = async (interaction) => { // Remove purchase from database await pool.query('DELETE FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]); + + if (hub.logChannel != null) { + try { + chan = await client.channels.fetch(hub.logChannel); + chan.send({ + embeds: [ + { + title: `Product Revoked`, + color: 0xff0000, + description: `**Roblox ID:** ${user.robloxId}\n**Discord User:** <@${user.discordId}>\n**Product:** ${product.name}` + } + ] + }) + } catch (error) { + // Do nothing, channel was deleted + } + } return interaction.reply({ content: `Removed \`${product.name}\` from ${robloxID}`, ephemeral: true }); }; diff --git a/commands/unlink.js b/commands/unlink.js index b1f28ae..68750b7 100644 --- a/commands/unlink.js +++ b/commands/unlink.js @@ -9,4 +9,6 @@ const execute = async (interaction) => { return interaction.reply({ content: "Successfully unlinked your account. Please use the hub game to get a new pairing code!", ephemeral: true }); } -module.exports = { execute } \ No newline at end of file +const noop = () => { }; + +module.exports = { execute: noop } \ No newline at end of file diff --git a/commands/update.js b/commands/update.js new file mode 100644 index 0000000..7b10811 --- /dev/null +++ b/commands/update.js @@ -0,0 +1,61 @@ +const client = global.discord_client +const pool = global.db_pool; +const updateProductHandler = require('../messageHandlers/update_prod.js'); + +if (!global.productUpdateData) global.productUpdateData = {}; + +const execute = async (interaction) => { + if (!interaction.guild) return interaction.reply({ content: "This command must be used in a server!", ephemeral: true }); + if (global.productUpdateData[interaction.user.id]) return interaction.reply({ content: "You are already updating a product!", ephemeral: true }); + try { + + // Get the hub for the guild + const guildID = interaction.guild.id; + const [hub] = await pool.query('SELECT * FROM hubs WHERE discordGuild = ?', [guildID]); + if (!hub) return interaction.reply({ content: "Hub not found for this guild", ephemeral: true }); + + const productName = interaction.options.getString("name"); + const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ? AND hubId = ?', [`%${productName.toUpperCase()}%`, hub.id]); + if (!product) return interaction.reply({ content: "Product not found", ephemeral: true }); + // Proceed with creation + await interaction.reply({ ephemeral: true, content: "Getting things ready..." }); + await interaction.user.send({ content: `Updating product: \`${product.name}\`` }); + switch (interaction.options.getString("field")) { + case "name": + await interaction.user.send({ content: "Please provide a new name for the product. Say `cancel` to exit." }); + break; + case "description": + await interaction.user.send({ content: "Please provide a new description for the product. Say `cancel` to exit." }); + break; + case "devProductId": + await interaction.user.send({ content: "Please provide a new developer product ID for the product. Say `cancel` to exit." }); + break; + case "imageId": + await interaction.user.send({ content: "Please provide a new image ID for the product. Say `cancel` to exit." }); + break; + case "file": + await interaction.user.send({ content: "Please provide a new file for the product. Say `cancel` to exit." }); + break; + case "stock": + await interaction.user.send({ content: "Please provide a new stock quantity for the product. Say `cancel` to exit. Set `-1` to disable." }); + break; + case "category": + await interaction.user.send({ content: "Please provide a new category for the product. Say `cancel` to exit. Set to `~none` to remove." }); + break; + default: + return interaction.editReply({ content: "Invalid field provided.", ephemeral: true }); + } + interaction.editReply({ ephemeral: true, content: "Check your DMs!" }); + global.productUpdateData[interaction.user.id] = { + id: product.id, + type: interaction.options.getString("field") + }; + global.dmHandlers[interaction.user.id] = updateProductHandler; + } catch (err) { + console.error(err); + delete global.productCreationData[interaction.user.id]; + return interaction.editReply({ content: "An error occurred during the product creation process.", ephemeral: true }); + } +}; + +module.exports = { execute } \ No newline at end of file diff --git a/index.js b/index.js index 2a87bd0..dfd8e0d 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const app = express(); global.log = log; const Discord = require("discord.js") -const client = new Discord.Client({intents: ["Guilds", "DirectMessages"]}) +const client = new Discord.Client({intents: ["Guilds", "DirectMessages", "MessageContent", "GuildMembers"]}) const pool = MariaDB.createPool({ host: process.env.DB_HOST, @@ -86,6 +86,15 @@ client.on("interactionCreate", async (interaction) => { await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); } }); +global.dmHandlers = {} +client.on("messageCreate", async (message) => { + if (message.author.bot) return; + if (!message.channel.isDMBased()) return; + if (!global.dmHandlers[message.author.id]) { + return; + } + global.dmHandlers[message.author.id](message); +}); const port = process.env.SERVER_PORT || 3000; diff --git a/messageHandlers/create_prod.js b/messageHandlers/create_prod.js new file mode 100644 index 0000000..d954cd3 --- /dev/null +++ b/messageHandlers/create_prod.js @@ -0,0 +1,136 @@ +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.productCreationData[user.id]; + delete global.dmHandlers[user.id]; + user.send('Product creation cancelled.'); +} + +const execute = async (message) => { + if (!interaction.guild) return interaction.reply({ content: "This command must be used in a server!", ephemeral: true }); + switch (global.productCreationData[message.author.id].step) { + case 1: // Description + const description = message.content.trim(); + if (description.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + global.productCreationData[message.author.id].description = description; + global.productCreationData[message.author.id].step = 2; + message.channel.send('Description set. Please provide the dev product ID. Say `cancel` to cancel creation.'); + break; + case 2: // Dev product ID + const devProductId = message.content.trim(); + + if (devProductId.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (!/^\d{1,15}$/.test(newDevId)) { + message.channel.send('Invalid Dev Product ID. It must be a number between 1 and 15 digits.'); + return; + } + global.productCreationData[message.author.id].devProductId = devProductId; + global.productCreationData[message.author.id].step = 3; + message.channel.send('Dev product ID set. Please provide the decal ID. Say `skip` to not set one, or `cancel` to cancel creation.'); + break; + case 3: // Image ID + const imageId = message.content.trim(); + if (imageId.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + + if (imageId.toLowerCase() === "skip") { + global.productCreationData[message.author.id].imageId = null; + global.productCreationData[message.author.id].step = 4; + message.channel.send('Image ID skipped. Please upload the product file. Say `cancel` to cancel creation.'); + return; + } + if (!/^\d{1,15}$/.test(newImageId)) { + message.channel.send('Invalid Image ID. It must be a number between 1 and 15 digits.'); + return; + } + global.productCreationData[message.author.id].imageId = imageId; + global.productCreationData[message.author.id].step = 4; + message.channel.send('Image ID set. Please upload the product file.'); + break; + case 4: // File + if (message.content.trim().toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + const validExtensions = ['zip', 'rbxm', 'rbxmx']; + const fileExtension = attachment.name.split('.').pop().toLowerCase(); + + if (!validExtensions.includes(fileExtension)) { + message.channel.send('Invalid file type. Only zip, rbxm, and rbxmx files are allowed.'); + cancel(message.author); + return; + } + const file = fs.createWriteStream(`./productFiles/${crypto.randomBytes(8).toString('hex')}`); + const prodId = crypto.randomUUID(); + download(attachment.url, file.path, (err) => { + if (err) { + message.channel.send('Failed to download the file.'); + cancel(message.author); + return; + } + global.productCreationData[message.author.id].filePath = file.path.split('/').pop(); + let fileType = attachment.name.split('.').pop(); + global.productCreationData[message.author.id].step = 5; + message.channel.send('File uploaded successfully. Product creation complete.'); + pool.query(`INSERT INTO products (id, name, description, devProductId, decalId, file, fileType, hubId) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, [prodId, global.productCreationData[message.author.id].name, global.productCreationData[message.author.id].description, global.productCreationData[message.author.id].devProductId, global.productCreationData[message.author.id].imageId, global.productCreationData[message.author.id].filePath, fileType, global.productCreationData[message.author.id].hub], (err) => { + if (err) { + console.error(err); + message.channel.send('An error occurred while creating the product.'); + cancel(message.author); + return; + } + message.author.send('Product created successfully.'); + delete global.productCreationData[message.author.id]; + delete global.dmHandlers[message.author.id]; + }); + }); + } else { + message.channel.send('No file attached. Please upload the product file.'); + } + break; + default: + message.channel.send('Invalid step.'); + cancel(message.author); + break; + } +} + module.exports = execute \ No newline at end of file diff --git a/messageHandlers/update_prod.js b/messageHandlers/update_prod.js new file mode 100644 index 0000000..01991a4 --- /dev/null +++ b/messageHandlers/update_prod.js @@ -0,0 +1,143 @@ +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.productUpdateData[user.id]; + delete global.dmHandlers[user.id]; + user.send('Product update cancelled.'); +} + +const execute = async (message) => { + switch (global.productUpdateData[message.author.id].type) { + case "name": // Name + const newName = message.content.trim(); + if (newName.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + await pool.query('UPDATE products SET name = ? WHERE id = ?', [newName, global.productUpdateData[message.author.id].id]); + message.channel.send('Product name updated!'); + break; + case "description": // Description + const newDesc = message.content.trim(); + if (newDesc.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + await pool.query('UPDATE products SET description = ? WHERE id = ?', [newDesc, global.productUpdateData[message.author.id].id]); + message.channel.send('Product description updated!'); + break; + case "devProductId": // Dev Product ID + const newDevId = message.content.trim(); + if (newDevId.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (!/^\d{1,15}$/.test(newDevId)) { + message.channel.send('Invalid Dev Product ID. It must be a number between 1 and 15 digits.'); + return; + } + await pool.query('UPDATE products SET devProductID = ? WHERE id = ?', [newDevId, global.productUpdateData[message.author.id].id]); + message.channel.send('Dev Product ID updated!'); + break; + case "imageId": // Image ID + const newImageId = message.content.trim(); + if (newImageId.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (!/^\d{1,15}$/.test(newImageId)) { + message.channel.send('Invalid Image ID. It must be a number between 1 and 15 digits.'); + return; + } + await pool.query('UPDATE products SET imageId = ? WHERE id = ?', [newImageId, global.productUpdateData[message.author.id].id]); + message.channel.send('Image ID updated!'); + break; + case "file": // File + if (message.content.trim().toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + const validExtensions = ['zip', 'rbxm', 'rbxmx']; + const fileExtension = attachment.name.split('.').pop().toLowerCase(); + + if (!validExtensions.includes(fileExtension)) { + message.channel.send('Invalid file type. Only zip, rbxm, and rbxmx files are allowed.'); + cancel(message.author); + return; + } + const file = fs.createWriteStream(`./productFiles/${crypto.randomBytes(8).toString('hex')}`); + download(attachment.url, file.path, async (err) => { + if (err) { + message.channel.send('Failed to download the file.'); + cancel(message.author); + return; + } + const fileType = attachment.name.split('.').pop(); + await pool.query('UPDATE products SET file = ?, fileType = ? WHERE id = ?', [file.path.split('/').pop(), fileType, global.productUpdateData[message.author.id].id]); + message.channel.send('File updated successfully!'); + }); + } else { + message.channel.send('No file attached. Please upload the product file.'); + } + break; + case "stock": // Stocking info + const newStock = parseInt(message.content.trim()); + if (isNaN(newStock)) { + message.channel.send('Invalid stock quantity. It must be a number.'); + return; + } + await pool.query('UPDATE products SET stock = ? WHERE id = ?', [newStock, global.productUpdateData[message.author.id].id]); + message.channel.send('Stock quantity updated!'); + break; + case "category": // Category + const newCategory = message.content.trim(); + if (newCategory.toLowerCase() === 'cancel') { + cancel(message.author); + return; + } + if (newCategory.toLowerCase() === '~none') { + newCategory = null; + } + await pool.query('UPDATE products SET category = ? WHERE id = ?', [newCategory, global.productUpdateData[message.author.id].id]); + message.channel.send('Category updated!'); + break; + default: + message.channel.send('Invalid type. Somehow?'); + cancel(message.author); + break; + } + delete global.productUpdateData[message.author.id]; + delete global.dmHandlers[message.author.id]; +} + module.exports = execute \ No newline at end of file diff --git a/migrations/006_update_hubs_add_logChannel b/migrations/006_update_hubs_add_logChannel new file mode 100644 index 0000000..9b3f9c3 --- /dev/null +++ b/migrations/006_update_hubs_add_logChannel @@ -0,0 +1,3 @@ +ALTER TABLE hubs +ADD COLUMN logChannel VARCHAR(255); +-- Uhh \ No newline at end of file diff --git a/routes/hub.js b/routes/hub.js index af30cb8..33cace4 100644 --- a/routes/hub.js +++ b/routes/hub.js @@ -25,6 +25,37 @@ router.get('/getSession', async (req, res) => { try { const products = await pool.query('SELECT * FROM products WHERE hubId = ?', [hub.id]); const purchases = await pool.query('SELECT * FROM purchases WHERE hubId = ?', [hub.id]); + // Generate bestSeller object from purchases. Find what product id has the most sales, then make the object + const purchaseCounts = purchases.reduce((acc, purchase) => { + console.log(acc) + console.log(purchase) + acc[purchase.productId] = (acc[purchase.productId] || 0) + 1; + return acc; + }, {}); + + const bestSellerProductId = Object.keys(purchaseCounts).reduce((a, b) => purchaseCounts[a] > purchaseCounts[b] ? a : b, null); + const bestSellerProduct = products.find(product => product.id == bestSellerProductId); + const bestSeller = bestSellerProduct ? { + productID: bestSellerProduct.id, + name: bestSellerProduct.name, + description: bestSellerProduct.description, + devproduct_id: (bestSellerProduct.stock == 0) ? 0 : bestSellerProduct.devProductID, + decalID: bestSellerProduct.decalId || "0", + stock: bestSellerProduct.stock == -1 ? false : bestSellerProduct.stock, + onsale: bestSellerProduct.stock == -1 || bestSellerProduct.stock > 0 ? true : false, + category: bestSellerProduct.stock == -1 || bestSellerProduct.stock > 0 ? bestSellerProduct.category : "Out of Stock", + rating: { + currentScore: 0, + maxScore: 0, + amountOfReviews: 0 + }, + tags: [], + playerData: { + robloxId: pid, + ownsProduct: purchases.some(purchase => purchase.productId == bestSellerProduct.id && purchase.robloxId == pid) + } + } : {}; + // generate array of products const respData = JSON.stringify({ status: 200, @@ -51,15 +82,16 @@ router.get('/getSession', async (req, res) => { }, }, productsData: { + bestsellerProduct: bestSeller, allProducts: products.map(product => ({ productID: product.id, name: product.name, description: product.description, - devproduct_id: product.devProductID, + devproduct_id: (product.stock == 0) ? 0 : product.devProductID, decalID: product.decalId || "0", - stock: product.stock > 0 ? product.stock : false, - onsale: true, - category: product.category, + stock: (product.stock == -1 || purchases.some(purchase => purchase.productId == product.id && purchase.robloxId == pid)) ? false : product.stock, + onsale: product.stock == -1 || product.stock > 0 ? true : false, + category: product.stock == -1 || product.stock > 0 ? product.category : "Out of Stock", rating: { currentScore: 0, maxScore: 0, @@ -78,6 +110,7 @@ router.get('/getSession', async (req, res) => { requestedAt: new Date().toISOString(), } }, (_, v) => typeof v === 'bigint' ? v.toString() : v) + return res.status(200).send(respData) } catch (error) { log.error(error); diff --git a/routes/payments.js b/routes/payments.js index 8b7fdf8..a950a5c 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const pool = global.db_pool; +const client = global.discord_client; // Main payment processor @@ -13,8 +14,8 @@ router.post("/external/hub/order/complete", async (req, res) => { const { robloxID, productID } = req.body; if (!robloxID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' }); const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]); - const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]); - + const [product] = await pool.query('SELECT * FROM products WHERE id = ? AND hubId = ?', [productID, hub.id]); + // Check if user and product exists if (!user || !product) return res.status(404).json({ status: "404", message: 'User or Product not found' }); const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]); @@ -24,6 +25,36 @@ router.post("/external/hub/order/complete", async (req, res) => { // Insert purchase into database await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [robloxID, product.id, hub.id]); + if (product.stock !== -1 && product.stock !== 0) { + await pool.query('UPDATE products SET stock = stock - 1 WHERE id = ?', [product.id]); + } + res.status(200).json({ status: "200", message: 'Purchased product' }); + + // Handle logging + try { + // Assuming you have a function to send a message to the user + dscUser = await client.users.fetch(user.discordId); + dscUser.send(`You have successfully purchased ${product.name}!\nUse \`/retrive ${product.name}\` in the Discord server to download it!`); + } catch (error) { + // Do nothing, user has privacy settings enabled + } + + if (hub.logChannel != null) { + try { + chan = await client.channels.fetch(hub.logChannel); + chan.send({ + embeds: [ + { + title: `New Purchase`, + color: 0x00ff00, + description: `**Roblox ID:** ${user.robloxId}\n**Discord User:** <@${user.discordId}>\n**Product:** ${product.name}\n**Type:** Normal` + } + ] + }) + } catch (error) { + // Do nothing, channel was deleted + } + } }); // Gift validator @@ -48,7 +79,6 @@ router.post("/external/hub/gift/validate", async (req, res) => { // Check if purchase already exists if (purchase) return res.status(409).json({ status: "409", message: 'User already owns product', data: {userExists: true, ownsProduct: true} }); - // All good! return res.status(200).json({ status: "200", message: 'User does not own product', data: {userExists: true, ownsProduct: false} }); }); @@ -74,6 +104,36 @@ router.post("/external/hub/gift/complete", async (req, res) => { // Insert purchase into database await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [recipientID, product.id, hub.id]); + if (product.stock !== -1 && product.stock !== 0) { + await pool.query('UPDATE products SET stock = stock - 1 WHERE id = ?', [product.id]); + } + res.status(200).json({ status: "200", message: 'Gifted product to user' }); + + // Handle logging + try { + // Assuming you have a function to send a message to the user + dscUser = await client.users.fetch(user.discordId); + dscUser.send(`You have successfully purchased ${product.name}!\nUse \`/retrive ${product.name}\` in the Discord server to download it!`); + } catch (error) { + // Do nothing, user has privacy settings enabled + } + + if (hub.logChannel != null) { + try { + chan = await client.channels.fetch(hub.logChannel); + chan.send({ + embeds: [ + { + title: `New Purchase`, + color: 0x00ff00, + description: `**Roblox ID:** ${user.robloxId}\n**Discord User:** <@${user.discordId}>\n**Product:** ${product.name}\n**Type:** Gift` + } + ] + }) + } catch (error) { + // Do nothing, channel was deleted + } + } }); module.exports = router; \ No newline at end of file