310 lines
13 KiB
JavaScript
310 lines
13 KiB
JavaScript
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(64).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.';
|
|
}
|
|
},
|
|
9: {
|
|
step: 29, exec: (hubId, uid) => {
|
|
return 'Provide the full game link to your hub game! 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 29: // Set Hub Game Link
|
|
const gameLink = message.content.trim();
|
|
if (gameLink.toLowerCase() === 'cancel') {
|
|
cancel(message.author);
|
|
return;
|
|
}
|
|
const gameLinkPattern = /^https?:\/\/(www\.)?roblox\.com\/games\/(\d+)(\/[a-zA-Z0-9_-]+)?$/;
|
|
const match = gameLink.match(gameLinkPattern);
|
|
if (!match) {
|
|
message.channel.send('Invalid game link. Please provide a valid Roblox game link (e.g., https://www.roblox.com/games/1234567890/Game-Name), or `cancel` to exit.');
|
|
return;
|
|
}
|
|
await pool.query('UPDATE hubs SET hubUrl = ? WHERE id = ?', [gameLink, global.hubSettingsHandlers[message.author.id].hub]);
|
|
global.hubSettingsHandlers[message.author.id].step = 1;
|
|
message.channel.send('Hub game link updated.\n\nType an option number, or `cancel` to exit.');
|
|
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 |