const client = global.discord_client const pool = global.db_pool; const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); 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: (hubId, uid) => { return 'Please provide the new short description. Say `cancel` to exit.'; } }, 2: { step: 22, exec: (hubId, uid) => { return 'Please provide the new long description. Say `cancel` to exit.'; } }, 3: { step: 1, exec: async (hubId, uid) => { 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: (hubId, uid) => { return 'Please provide the new Terms of Service. Say `cancel` to exit.'; } }, 5: { step: 25, exec: async (hubId, uid) => { 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: (hubId, uid) => { return 'Please provide your Parcel Secret Key. This can be found in your Parcel Settings module, or by running `/settings` on Parcel. Say `cancel` to exit.'; } }, 7: { step: 27, exec: (hubId, uid) => { return 'Please provide a Roblox Audio Asset ID, or say `none` to disable. Say `cancel` to exit.'; } }, 8: { step: 28, exec: (hubId, uid) => { return 'Please provide a Discord Channel ID for logging, or say `none` to unset. Say `cancel` to exit.'; } }, 99: { step: 99, exec: async (hubId, uid) => { // generate a random confirmation code const confirmationCode = crypto.randomBytes(8).toString('hex'); global.hubSettingsHandlers[uid].confirmationCode = confirmationCode; return `***__WARNING: THIS WILL DELETE THE HUB AND ALL PRODUCTS! THIS ACTION CANNOT BE UNDONE.__***\nTo confirm deletion, please type the following: \`confirm ${confirmationCode}\`. Type \`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.author.id); 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; case 26: // Set Parcel Key const parcelKey = message.content.trim(); if (parcelKey.toLowerCase() === 'cancel') { cancel(message.author); return; } if (!/^[a-zA-Z0-9]{16,}$/.test(parcelKey)) { message.channel.send('Invalid Parcel Secret Key. Please provide an alphanumeric string at least 16 characters long, or `cancel` to exit.'); return; } // Validate key against parcel API GET https://hub.parcelroblox.com/getSession Headers: { 'Authorization': parcelKey } should return 400 const resp = await fetch('https://hub.parcelroblox.com/getSession', { method: 'GET', headers: { 'Authorization': parcelKey } }); if (resp.status !== 400) { message.channel.send('Parcel Secret Key validation failed. Please ensure the key is correct and has not been revoked. You can find your key in your Parcel Settings module, or by running `/settings` on Parcel.'); return; } await pool.query('UPDATE hubs SET parcelKey = ? WHERE id = ?', [parcelKey, global.hubSettingsHandlers[message.author.id].hub]); global.hubSettingsHandlers[message.author.id].step = 1; message.channel.send('Parcel Secret Key updated.\n\nType an option number, or `cancel` to exit.'); break; case 27: // Set Background Music const bgmInput = message.content.trim(); if (bgmInput.toLowerCase() === 'cancel') { cancel(message.author); return; } if (bgmInput.toLowerCase() === 'none') { await pool.query('UPDATE hubs SET bgmId = NULL WHERE id = ?', [global.hubSettingsHandlers[message.author.id].hub]); global.hubSettingsHandlers[message.author.id].step = 1; message.channel.send('Background music disabled.\n\nType an option number, or `cancel` to exit.'); return; } const audioId = parseInt(bgmInput); if (isNaN(audioId) || audioId <= 0) { message.channel.send('Invalid Audio Asset ID. Please provide a valid positive integer, or `none` to disable, or `cancel` to exit.'); return; } await pool.query('UPDATE hubs SET bgmId = ? WHERE id = ?', [audioId, global.hubSettingsHandlers[message.author.id].hub]); global.hubSettingsHandlers[message.author.id].step = 1; message.channel.send('Background music updated.\n\nType an option number, or `cancel` to exit.'); break; case 28: // Set Log Channel const channelInput = message.content.trim(); if (channelInput.toLowerCase() === 'cancel') { cancel(message.author); return; } if (channelInput.toLowerCase() === 'none') { await pool.query('UPDATE hubs SET logChannel = NULL WHERE id = ?', [global.hubSettingsHandlers[message.author.id].hub]); global.hubSettingsHandlers[message.author.id].step = 1; message.channel.send('Log channel unset.\n\nType an option number, or `cancel` to exit.'); return; } const channelId = channelInput; const channel = await client.channels.fetch(channelId).catch(() => null); if (!channel) { message.channel.send('Invalid Channel ID. Please provide a valid Discord Channel ID, or `none` to unset, or `cancel` to exit.'); return; } channel.send("NotParcel will use this channel for logging important hub events.").then(() => { pool.query('UPDATE hubs SET logChannel = ? WHERE id = ?', [channelId, global.hubSettingsHandlers[message.author.id].hub]); global.hubSettingsHandlers[message.author.id].step = 1; message.channel.send('Log channel updated.\n\nType an option number, or `cancel` to exit.'); }).catch(() => { message.channel.send('Failed to send a test message to the specified channel. Please ensure the bot has permission to send messages in that channel. Send a valid Discord Channel ID, or `none` to unset, or `cancel` to exit.'); return; }); break; case 99: // Delete Hub const confirmMatch = message.content.trim().match(/^confirm (\w{16})$/); if (message.content.toLowerCase() === 'cancel') { cancel(message.author); return; } if (!confirmMatch || confirmMatch[1] !== global.hubSettingsHandlers[message.author.id].confirmationCode) { message.channel.send('Invalid confirmation code. Please type the exact confirmation code sent to you, or `cancel` to exit.'); return; } message.channel.send('Deletion Confirmed. Starting deletion process...').then(async msg => { curMsg = msg.content; // Proceed with deletion const hubId = global.hubSettingsHandlers[message.author.id].hub; // Delete fileAuth curMsg = curMsg + '\nDeleting file authorizations...' await msg.edit(curMsg); await pool.query('DELETE FROM fileAuth WHERE product IN (SELECT id FROM products WHERE hubId = ?)', [hubId]); // Delete files curMsg = curMsg + '\nDeleting product files...' await msg.edit(curMsg); // const safeFileId = path.basename(product.file); // const filePath = path.join(__dirname, '../productFiles', safeFileId); // Code we use in the CDN route const files = await pool.query('SELECT file FROM products WHERE hubId = ?', [hubId]); for (const fileRow of files) { const safeFileId = path.basename(fileRow.file); const filePath = path.join(__dirname, '../productFiles', safeFileId); // Code we use in the CDN route if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); curMsg = curMsg + `\nDeleted file: ${safeFileId}`; await msg.edit(curMsg); } else { curMsg = curMsg + `\nFile not found (skipping): ${safeFileId}`; await msg.edit(curMsg); } } // Delete purchases curMsg = curMsg + '\nDeleting purchases...'; await msg.edit(curMsg); await pool.query('DELETE FROM purchases WHERE productId IN (SELECT id FROM products WHERE hubId = ?)', [hubId]); // Delete products curMsg = curMsg + '\nDeleting products...'; await msg.edit(curMsg); await pool.query('DELETE FROM products WHERE hubId = ?', [hubId]); // Delete hub curMsg = curMsg + '\nDeleting hub...'; await msg.edit(curMsg); await pool.query('DELETE FROM hubs WHERE id = ?', [hubId]); curMsg = curMsg + '\nHub and all associated data deleted successfully.'; await msg.edit(curMsg); cancel(message.author); }); break; default: message.channel.send('Invalid step.'); log.error(`Invalid hub settings step for user ${message.author.id}: ${global.hubSettingsHandlers[message.author.id].step}`); cancel(message.author); break; } } module.exports = execute