| const { |
| ActionRowBuilder, |
| ButtonBuilder, |
| ButtonStyle, |
| StringSelectMenuBuilder, |
| } = require('discord.js'); |
| const { createEmbed } = require('../utils/embeds'); |
| const { Colors } = require('../config'); |
| const { stmts } = require('../database'); |
|
|
| const OWNER_ID = process.env.OWNER_ID; |
|
|
| |
| const activeSessions = new Map(); |
|
|
| const STEPS = ['title', 'file', 'warnings', 'status', 'about', 'image', 'preview']; |
|
|
| |
| |
| |
| |
| function canDrop(userId) { |
| if (userId === OWNER_ID) return { allowed: true, remaining: Infinity }; |
|
|
| const wl = stmts.getWhitelist.get(userId); |
| if (!wl) return { allowed: false, reason: 'not_whitelisted' }; |
|
|
| const limit = wl.max_drops; |
| const { count } = stmts.getDropCount24h.get(userId); |
| if (count >= limit) { |
| const last = stmts.getLastDrop.get(userId); |
| const resetTime = new Date(last.dropped_at + 'Z'); |
| resetTime.setHours(resetTime.getHours() + 24); |
| const diff = resetTime - new Date(); |
| const hours = Math.floor(diff / 3600000); |
| const mins = Math.floor((diff % 3600000) / 60000); |
| return { allowed: false, reason: 'rate_limited', resetIn: `${hours}h ${mins}m`, limit }; |
| } |
|
|
| return { allowed: true, remaining: limit - count, limit }; |
| } |
|
|
| |
| |
| |
| function startDropSession(userId) { |
| const session = { |
| step: 'title', |
| title: null, |
| file: null, |
| warnings: null, |
| status: null, |
| about: null, |
| image: null, |
| channelId: null, |
| }; |
| activeSessions.set(userId, session); |
| return session; |
| } |
|
|
| |
| |
| |
| function hasSession(userId) { |
| return activeSessions.has(userId); |
| } |
|
|
| |
| |
| |
| function getPrompt(session) { |
| switch (session.step) { |
| case 'title': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 1/6', |
| description: '> **What is the title for this drop?**\n\nType the title below.', |
| color: Colors.PRIMARY, |
| footer: 'Type "cancel" at any time to abort', |
| })], |
| }; |
|
|
| case 'file': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 2/6', |
| description: '> **Upload the file for this drop.**\n\nAttach the file to your next message.', |
| color: Colors.PRIMARY, |
| footer: `Title: ${session.title}`, |
| })], |
| }; |
|
|
| case 'warnings': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 3/6', |
| description: '> **Any warnings for this drop?**\n\nType warnings below, or type `none` to skip.', |
| color: Colors.WARNING, |
| footer: `Title: ${session.title}`, |
| })], |
| }; |
|
|
| case 'status': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 4/6', |
| description: '> **Is this source checked or unchecked?**\n\nClick a button below.', |
| color: Colors.PRIMARY, |
| footer: `Title: ${session.title}`, |
| })], |
| components: [new ActionRowBuilder().addComponents( |
| new ButtonBuilder() |
| .setCustomId('drop_checked') |
| .setLabel('β
Checked') |
| .setStyle(ButtonStyle.Success), |
| new ButtonBuilder() |
| .setCustomId('drop_unchecked') |
| .setLabel('β οΈ Unchecked') |
| .setStyle(ButtonStyle.Danger), |
| )], |
| }; |
|
|
| case 'about': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 5/6', |
| description: '> **Describe this source.**\n\nWrite a short description about what this is.', |
| color: Colors.PRIMARY, |
| footer: `Title: ${session.title}`, |
| })], |
| }; |
|
|
| case 'image': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 6/6', |
| description: '> **Reference image?**\n\nUpload an image or type `none` to skip.', |
| color: Colors.PRIMARY, |
| footer: `Title: ${session.title}`, |
| })], |
| }; |
|
|
| default: |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| function buildDropEmbed(session) { |
| const statusIcon = session.status === 'checked' ? 'β
' : 'β οΈ'; |
| const statusText = session.status === 'checked' ? 'Verified / Checked' : 'Unchecked β Use at your own risk'; |
|
|
| const lines = [ |
| `${statusIcon} **${statusText}**`, |
| '', |
| '> **About**', |
| `> ${session.about}`, |
| '', |
| ]; |
|
|
| if (session.warnings && session.warnings.toLowerCase() !== 'none') { |
| lines.push('β οΈ **Warnings**'); |
| lines.push(`> ${session.warnings}`); |
| lines.push(''); |
| } |
|
|
| lines.push('```'); |
| lines.push(`π File: ${session.file.name}`); |
| lines.push(`π Size: ${formatBytes(session.file.size)}`); |
| lines.push(`π Status: ${statusText}`); |
| lines.push('```'); |
| lines.push(''); |
| lines.push('βββββββββββββββββββββββββββββββββββββ'); |
| lines.push('*Wyvern Softworks β Educational Use Only*'); |
|
|
| const embedData = { |
| title: `π¦ ${session.title}`, |
| description: lines.join('\n'), |
| color: session.status === 'checked' ? Colors.SUCCESS : Colors.WARNING, |
| footer: 'Wyvern Softworks β’ ' + new Date().toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }), |
| }; |
|
|
| if (session.image) { |
| embedData.image = session.image; |
| } |
|
|
| return createEmbed(embedData); |
| } |
|
|
| |
| |
| |
| function buildDownloadButton(session) { |
| return new ActionRowBuilder().addComponents( |
| new ButtonBuilder() |
| .setLabel('π₯ Download') |
| .setStyle(ButtonStyle.Link) |
| .setURL(session.file.url), |
| ); |
| } |
|
|
| |
| |
| |
| |
| async function handleDropMessage(message) { |
| const userId = message.author.id; |
| const session = activeSessions.get(userId); |
| if (!session) return false; |
|
|
| const content = message.content.trim(); |
|
|
| |
| if (content.toLowerCase() === 'cancel') { |
| activeSessions.delete(userId); |
| await message.reply({ embeds: [createEmbed({ title: 'β Drop Cancelled', color: Colors.ACCENT })] }); |
| return true; |
| } |
|
|
| switch (session.step) { |
| case 'title': |
| session.title = content; |
| session.step = 'file'; |
| await message.reply(getPrompt(session)); |
| return true; |
|
|
| case 'file': |
| if (message.attachments.size === 0) { |
| await message.reply({ content: 'β Please attach a file.' }); |
| return true; |
| } |
| const att = message.attachments.first(); |
| session.file = { url: att.url, name: att.name, size: att.size }; |
| session.step = 'warnings'; |
| await message.reply(getPrompt(session)); |
| return true; |
|
|
| case 'warnings': |
| session.warnings = content.toLowerCase() === 'none' ? null : content; |
| session.step = 'status'; |
| await message.reply(getPrompt(session)); |
| return true; |
|
|
| case 'about': |
| session.about = content; |
| session.step = 'image'; |
| await message.reply(getPrompt(session)); |
| return true; |
|
|
| case 'image': |
| if (content.toLowerCase() === 'none') { |
| session.image = null; |
| } else if (message.attachments.size > 0) { |
| session.image = message.attachments.first().url; |
| } else { |
| session.image = null; |
| } |
|
|
| |
| session.step = 'preview'; |
| const previewEmbed = buildDropEmbed(session); |
| const row = new ActionRowBuilder().addComponents( |
| new ButtonBuilder() |
| .setCustomId('drop_approve') |
| .setLabel('β
Approve & Post') |
| .setStyle(ButtonStyle.Success), |
| new ButtonBuilder() |
| .setCustomId('drop_cancel') |
| .setLabel('β Cancel') |
| .setStyle(ButtonStyle.Danger), |
| ); |
|
|
| await message.reply({ |
| content: '**π Preview β This is how the drop will look:**', |
| embeds: [previewEmbed], |
| components: [buildDownloadButton(session), row], |
| }); |
| return true; |
|
|
| case 'channel': |
| |
| const channelId = content.replace(/[<#>]/g, ''); |
| session.channelId = channelId; |
|
|
| try { |
| const guild = message.client.guilds.cache.first(); |
| const channel = await guild.channels.fetch(channelId); |
| if (!channel) throw new Error('Channel not found'); |
|
|
| const dropEmbed = buildDropEmbed(session); |
| const downloadRow = buildDownloadButton(session); |
| await channel.send({ |
| embeds: [dropEmbed], |
| components: [downloadRow], |
| }); |
|
|
| |
| stmts.logDrop.run(userId, session.title, channelId); |
|
|
| activeSessions.delete(userId); |
| await message.reply({ |
| embeds: [createEmbed({ |
| title: 'β
Drop Posted!', |
| description: `Successfully posted to <#${channelId}>`, |
| color: Colors.SUCCESS, |
| })], |
| }); |
| } catch (err) { |
| await message.reply({ content: `β Failed to post: ${err.message}\nPlease send a valid channel ID.` }); |
| } |
| return true; |
|
|
| default: |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| async function handleDropButton(interaction) { |
| const userId = interaction.user.id; |
| const session = activeSessions.get(userId); |
| if (!session) return false; |
|
|
| const { customId } = interaction; |
|
|
| if (customId === 'drop_checked' || customId === 'drop_unchecked') { |
| session.status = customId === 'drop_checked' ? 'checked' : 'unchecked'; |
| session.step = 'about'; |
| await interaction.update({ |
| embeds: [createEmbed({ |
| title: `${customId === 'drop_checked' ? 'β
' : 'β οΈ'} Status: ${session.status}`, |
| color: customId === 'drop_checked' ? Colors.SUCCESS : Colors.WARNING, |
| })], components: [] |
| }); |
| await interaction.followUp(getPrompt(session)); |
| return true; |
| } |
|
|
| if (customId === 'drop_approve') { |
| session.step = 'channel'; |
| await interaction.update({ |
| embeds: [interaction.message.embeds[0]], |
| components: [], |
| }); |
| await interaction.followUp({ |
| embeds: [createEmbed({ |
| title: 'π€ Where to post?', |
| description: '> Send the **channel ID** where this drop should be posted.\n\nYou can right-click a channel β Copy Channel ID.', |
| color: Colors.INFO, |
| })], |
| }); |
| return true; |
| } |
|
|
| if (customId === 'drop_cancel') { |
| activeSessions.delete(userId); |
| await interaction.update({ |
| embeds: [createEmbed({ title: 'β Drop Cancelled', color: Colors.ACCENT })], |
| components: [], |
| }); |
| return true; |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
| function formatBytes(bytes) { |
| if (bytes === 0) return '0 B'; |
| const k = 1024; |
| const sizes = ['B', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
|
|
| module.exports = { |
| startDropSession, |
| hasSession, |
| getPrompt, |
| handleDropMessage, |
| handleDropButton, |
| buildDropEmbed, |
| canDrop, |
| }; |
|
|