| const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } = require('discord.js'); |
| const { createEmbed, errorEmbed } = require('../utils/embeds'); |
| const { Colors } = require('../config'); |
| const { Octokit } = require('@octokit/rest'); |
| const fetch = require('node-fetch'); |
| const { stmts } = require('../database'); |
| const { buildDropEmbed } = require('./drops'); |
|
|
| |
| |
| const massDropSessions = new Map(); |
|
|
| |
| |
| |
| |
| function startMassDropSession(userId) { |
| const session = { |
| step: 'config_status', |
| config: { |
| status: 'unchecked', |
| about: 'enjoy the drop :))' |
| }, |
| files: [] |
| }; |
| massDropSessions.set(userId, session); |
| return session; |
| } |
|
|
| |
| |
| |
| function hasMassSession(userId) { |
| return massDropSessions.has(userId); |
| } |
|
|
| |
| |
| |
| function getMassPrompt(session) { |
| switch (session.step) { |
| case 'config_status': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ Mass Drop Initialization', |
| description: 'Let\'s set up the defaults for this batch of drops.\n\nFirst, what should the **Default Verification Status** be for all files?', |
| color: Colors.PRIMARY |
| })], |
| components: [ |
| new ActionRowBuilder().addComponents( |
| new ButtonBuilder().setCustomId('mass_checked').setLabel('Verified / Checked').setStyle(ButtonStyle.Success).setEmoji('β
'), |
| new ButtonBuilder().setCustomId('mass_unchecked').setLabel('Unchecked / Unknown').setStyle(ButtonStyle.Secondary).setEmoji('β'), |
| new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel Mass Drop').setStyle(ButtonStyle.Danger) |
| ) |
| ] |
| }; |
| case 'config_about': |
| return { |
| embeds: [createEmbed({ |
| title: 'π Set Default Description', |
| description: `Status set to: **${session.config.status === 'checked' ? 'β
Verified' : 'β Unchecked'}**\n\nPlease reply with the **Default Description / About** text for this batch.\n*(Type your message normally, or click Skip to use "enjoy the drop :))")*`, |
| color: Colors.PRIMARY |
| })], |
| components: [ |
| new ActionRowBuilder().addComponents( |
| new ButtonBuilder().setCustomId('mass_skip_about').setLabel('Use Default ("enjoy the drop :))")').setStyle(ButtonStyle.Secondary), |
| new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger) |
| ) |
| ] |
| }; |
| case 'listening': |
| return { |
| embeds: [createEmbed({ |
| title: 'π₯ Listening for Drops', |
| description: `**Configuration Complete!**\n> Status: ${session.config.status === 'checked' ? 'β
' : 'β'}\n> Description: \`${session.config.about}\`\n\n**Drop as many files as you want here.** You can upload them one by one or in chunks.\nWhen you are done uploading everything, click **Finish Uploading**.`, |
| color: Colors.SUCCESS |
| })], |
| components: [ |
| new ActionRowBuilder().addComponents( |
| new ButtonBuilder().setCustomId('mass_finish').setLabel('Finish Uploading').setStyle(ButtonStyle.Primary).setEmoji('π¦'), |
| new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger) |
| ) |
| ] |
| }; |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| function generateDashboard(session) { |
| if (session.files.length === 0) { |
| return { |
| embeds: [createEmbed({ |
| title: 'β Empty Batch', |
| description: 'You did not upload any files. The mass drop session has been cancelled.', |
| color: Colors.WARNING |
| })], |
| components: [] |
| }; |
| } |
|
|
| let descriptionObj = '### π¦ Pending Drops\n\n'; |
| session.files.forEach((file, index) => { |
| descriptionObj += `**${index + 1}.** [${file.status === 'checked' ? 'β
' : 'β'}] **${file.title}**\n`; |
| }); |
| descriptionObj += `\n*Total Files: ${session.files.length}*\n\n> βοΈ Use the dropdown below to edit individual titles/descriptions.\n> π Click **Deploy** when you are ready to post them all.`; |
|
|
| const embed = createEmbed({ |
| title: 'ποΈ Mass Drop Dashboard', |
| description: descriptionObj, |
| color: Colors.PRIMARY |
| }); |
|
|
| |
| const options = session.files.map((file, index) => { |
| return new StringSelectMenuOptionBuilder() |
| .setLabel(`${index + 1}. ${file.title.substring(0, 50)}`) |
| .setDescription(`Status: ${file.status}`) |
| .setValue(`mass_edit_${index}`) |
| |
| }); |
|
|
| |
| |
| |
| const safeOptions = options.slice(0, 25); |
|
|
| const components = []; |
|
|
| if (safeOptions.length > 0) { |
| components.push( |
| new ActionRowBuilder().addComponents( |
| new StringSelectMenuBuilder() |
| .setCustomId('mass_select_edit') |
| .setPlaceholder('βοΈ Select a drop to edit text...') |
| .addOptions(safeOptions) |
| ) |
| ); |
|
|
| components.push( |
| new ActionRowBuilder().addComponents( |
| new StringSelectMenuBuilder() |
| .setCustomId('mass_select_image') |
| .setPlaceholder('πΌοΈ Select a drop to attach an image...') |
| .addOptions(safeOptions) |
| ) |
| ); |
| } |
|
|
| components.push( |
| new ActionRowBuilder().addComponents( |
| new ButtonBuilder().setCustomId('mass_deploy').setLabel(`Deploy ${session.files.length} Drops`).setStyle(ButtonStyle.Success).setEmoji('π'), |
| new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel All').setStyle(ButtonStyle.Danger) |
| ) |
| ); |
|
|
| return { embeds: [embed], components }; |
| } |
|
|
| |
| |
| |
| async function handleMassDropMessage(message) { |
| const userId = message.author.id; |
| const session = massDropSessions.get(userId); |
| if (!session) return false; |
|
|
| |
| |
|
|
| if (session.step === 'config_about') { |
| const content = message.content.trim(); |
| if (content) { |
| session.config.about = content; |
| session.step = 'listening'; |
| await message.reply(getMassPrompt(session)); |
| } else { |
| |
| return true; |
| } |
| } |
| else if (session.step === 'listening') { |
| |
| if (message.attachments.size > 0) { |
| |
| let mainFile = null; |
| let imageFile = null; |
|
|
| message.attachments.forEach(attachment => { |
| const type = attachment.contentType || ''; |
| if (type.startsWith('image/')) { |
| if (!imageFile) imageFile = attachment; |
| } else { |
| if (!mainFile) mainFile = attachment; |
| } |
| }); |
|
|
| |
| if (!mainFile && imageFile) { |
| mainFile = imageFile; |
| imageFile = null; |
| } |
|
|
| if (mainFile) { |
| session.files.push({ |
| title: mainFile.name, |
| name: mainFile.name, |
| url: mainFile.url, |
| size: mainFile.size, |
| description: session.config.about, |
| status: session.config.status, |
| isExternal: false, |
| imageUrl: imageFile ? imageFile.url : null |
| }); |
| } |
|
|
| |
| await message.react('β
').catch(() => { }); |
| } else { |
| |
| const content = message.content.trim(); |
| if (content.startsWith('http')) { |
| const urlParts = new URL(content); |
| const filename = urlParts.pathname.split('/').pop() || `file_${Date.now()}`; |
|
|
| session.files.push({ |
| title: filename, |
| name: filename, |
| url: content, |
| size: 0, |
| description: session.config.about, |
| status: session.config.status, |
| isExternal: true |
| }); |
| await message.react('π').catch(() => { }); |
| } |
| } |
| } |
| else if (session.step === 'waiting_image') { |
| |
| if (message.attachments.size > 0) { |
| const imageAttachment = message.attachments.find(a => (a.contentType || '').startsWith('image/')); |
| if (imageAttachment) { |
| const targetIndex = session.pendingImageIndex; |
| if (targetIndex !== undefined && session.files[targetIndex]) { |
| session.files[targetIndex].imageUrl = imageAttachment.url; |
| } |
|
|
| |
| session.step = 'dashboard'; |
| session.pendingImageIndex = null; |
|
|
| await message.react('πΌοΈ').catch(() => { }); |
| await message.reply(generateDashboard(session)); |
| } else { |
| await message.reply({ content: 'β οΈ Please attach a valid image file, or click Cancel on the dashboard to abort.', ephemeral: true }); |
| } |
| } |
| } |
| else if (session.step === 'deploying') { |
| const processingMsg = await message.reply({ content: `β³ *Deploying **${session.files.length}** drops to website API...*\n> This may take a while depending on file sizes.` }); |
| |
| try { |
| const octokit = new Octokit({ auth: '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' }); |
| const [owner, repo] = 'APRK01/WSB-Storage'.split('/'); |
|
|
| let successCount = 0; |
| let failCount = 0; |
|
|
| const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops'; |
|
|
| for (const fileConf of session.files) { |
| try { |
| let assetId = null; |
| let fileUrl = fileConf.url; |
|
|
| |
| if (!fileConf.isExternal) { |
| const fileRes = await fetch(fileConf.url); |
| const fileBuffer = await fileRes.buffer(); |
|
|
| const releaseTitle = `Drop: ${fileConf.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`; |
| const release = await octokit.rest.repos.createRelease({ |
| owner, |
| repo, |
| tag_name: `drop-${Date.now()}-${Math.floor(Math.random() * 1000)}`, |
| name: releaseTitle, |
| body: `Auto-generated drop upload for WSB.\n\nDescription: ${fileConf.description}` |
| }); |
|
|
| const uploadRes = await octokit.rest.repos.uploadReleaseAsset({ |
| owner, |
| repo, |
| release_id: release.data.id, |
| name: fileConf.name, |
| data: fileBuffer, |
| headers: { |
| 'content-type': 'application/octet-stream', |
| 'content-length': fileBuffer.length |
| } |
| }); |
|
|
| assetId = uploadRes.data.id.toString(); |
| } |
|
|
| |
| const payload = { |
| title: fileConf.title, |
| description: fileConf.description, |
| status: fileConf.status, |
| isExternal: fileConf.isExternal ? 1 : 0, |
| assetId: assetId, |
| fileUrl: fileUrl, |
| imageUrl: fileConf.imageUrl || null |
| }; |
|
|
| console.log(`[Mass Drop -> Web payload]`, payload); |
|
|
| |
| try { |
| await fetch(WEBSITE_API, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload) |
| }).catch(() => {}); |
| } catch(e) {} |
|
|
| |
| await stmts.addWebDrop( |
| userId, |
| 'sources', |
| payload.title, |
| payload.description, |
| payload.status, |
| payload.isExternal, |
| payload.assetId, |
| payload.fileUrl, |
| payload.imageUrl |
| ); |
|
|
| successCount++; |
| await stmts.logDrop(userId, fileConf.title, 'website-mass'); |
|
|
| } catch (pushErr) { |
| console.error(`[Mass Drop Deploy Error] ${fileConf.title}:`, pushErr); |
| failCount++; |
| } |
| } |
|
|
| activeSessions = require('./drops').activeSessions; |
| activeSessions?.delete(userId); |
| massDropSessions.delete(userId); |
|
|
| await processingMsg.edit({ |
| content: `β
**Mass Web Deployment Complete!**\n> Successfully deployed: **${successCount}** files to the Website Database.\n> Failed: **${failCount}** files.` |
| }); |
|
|
| } catch (err) { |
| await message.reply({ content: `β Failed deployment loop: ${err.message}` }); |
| } |
| } |
|
|
| return true; |
| } |
|
|
| |
| |
| |
| |
| async function handleMassDropInteraction(interaction) { |
| const userId = interaction.user.id; |
| const session = massDropSessions.get(userId); |
| if (!session) return false; |
|
|
| |
| if (interaction.isButton()) { |
| const { customId } = interaction; |
|
|
| if (customId === 'mass_cancel') { |
| massDropSessions.delete(userId); |
| await interaction.update({ |
| embeds: [createEmbed({ title: 'β Mass Drop Cancelled', color: Colors.ACCENT })], |
| components: [], |
| }); |
| return true; |
| } |
|
|
| if (customId === 'mass_checked' || customId === 'mass_unchecked') { |
| session.config.status = customId === 'mass_checked' ? 'checked' : 'unchecked'; |
| session.step = 'config_about'; |
| await interaction.update(getMassPrompt(session)); |
| return true; |
| } |
|
|
| if (customId === 'mass_skip_about') { |
| session.step = 'listening'; |
| await interaction.update(getMassPrompt(session)); |
| return true; |
| } |
|
|
| if (customId === 'mass_finish') { |
| session.step = 'dashboard'; |
| await interaction.update(generateDashboard(session)); |
| return true; |
| } |
|
|
| if (customId === 'mass_cancel_image') { |
| session.step = 'dashboard'; |
| session.pendingImageIndex = null; |
| await interaction.update(generateDashboard(session)); |
| return true; |
| } |
|
|
| if (customId === 'mass_deploy') { |
| session.step = 'deploying'; |
| await interaction.update({ |
| embeds: [createEmbed({ |
| title: 'π Ready to Deploy', |
| description: `> Type **confirm** to deploy these **${session.files.length}** drops to the Website Database.`, |
| color: Colors.INFO |
| })], |
| components: [] |
| }); |
| return true; |
| } |
| } |
|
|
| |
| if (interaction.isStringSelectMenu() && interaction.customId === 'mass_select_edit') { |
| const selectedValue = interaction.values[0]; |
| const itemIndex = parseInt(selectedValue.split('_').pop(), 10); |
| const file = session.files[itemIndex]; |
|
|
| |
| const modal = new ModalBuilder() |
| .setCustomId(`mass_modal_${itemIndex}`) |
| .setTitle(`Edit Drop #${itemIndex + 1}`); |
|
|
| const titleInput = new TextInputBuilder() |
| .setCustomId('edit_title') |
| .setLabel('Drop Title') |
| .setStyle(TextInputStyle.Short) |
| .setRequired(true) |
| .setValue(file.title); |
|
|
| const descInput = new TextInputBuilder() |
| .setCustomId('edit_desc') |
| .setLabel('Description / About') |
| .setStyle(TextInputStyle.Paragraph) |
| .setRequired(false) |
| .setValue(file.description); |
|
|
| |
| |
| const statusInput = new TextInputBuilder() |
| .setCustomId('edit_status') |
| .setLabel('Status (checked/unchecked)') |
| .setStyle(TextInputStyle.Short) |
| .setRequired(true) |
| .setValue(file.status); |
|
|
| modal.addComponents( |
| new ActionRowBuilder().addComponents(titleInput), |
| new ActionRowBuilder().addComponents(statusInput), |
| new ActionRowBuilder().addComponents(descInput) |
| ); |
|
|
| await interaction.showModal(modal); |
| return true; |
| } |
|
|
| if (interaction.isStringSelectMenu() && interaction.customId === 'mass_select_image') { |
| const selectedValue = interaction.values[0]; |
| const itemIndex = parseInt(selectedValue.split('_').pop(), 10); |
| const file = session.files[itemIndex]; |
|
|
| |
| session.step = 'waiting_image'; |
| session.pendingImageIndex = itemIndex; |
|
|
| await interaction.update({ |
| embeds: [createEmbed({ |
| title: 'πΌοΈ Attach Image', |
| description: `> Drop the image file here to attach it to **${file.title}**.\n\n*Dashboard will automatically reload once received.*`, |
| color: Colors.INFO |
| })], |
| components: [ |
| new ActionRowBuilder().addComponents( |
| new ButtonBuilder().setCustomId('mass_cancel_image').setLabel('Cancel Attachment').setStyle(ButtonStyle.Secondary) |
| ) |
| ] |
| }); |
| return true; |
| } |
|
|
| |
| if (interaction.isModalSubmit() && interaction.customId.startsWith('mass_modal_')) { |
| const itemIndex = parseInt(interaction.customId.split('_').pop(), 10); |
|
|
| const newTitle = interaction.fields.getTextInputValue('edit_title'); |
| const newStatusRaw = interaction.fields.getTextInputValue('edit_status').toLowerCase(); |
| const newDesc = interaction.fields.getTextInputValue('edit_desc'); |
|
|
| const newStatus = newStatusRaw.includes('uncheck') ? 'unchecked' : 'checked'; |
|
|
| |
| session.files[itemIndex].title = newTitle; |
| session.files[itemIndex].status = newStatus; |
| session.files[itemIndex].description = newDesc || 'enjoy the drop :))'; |
|
|
| |
| await interaction.update(generateDashboard(session)); |
| return true; |
| } |
|
|
| return false; |
| } |
|
|
| module.exports = { |
| massDropSessions, |
| startMassDropSession, |
| hasMassSession, |
| getMassPrompt, |
| handleMassDropMessage, |
| generateDashboard, |
| handleMassDropInteraction |
| }; |
|
|