| const { |
| ActionRowBuilder, |
| ButtonBuilder, |
| ButtonStyle, |
| StringSelectMenuBuilder, |
| AttachmentBuilder, |
| } = require('discord.js'); |
| const { Octokit } = require('@octokit/rest'); |
| const fetch = require('node-fetch'); |
| 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 = ['category', 'title', 'file', 'warnings', 'status', 'about', 'image', 'preview']; |
|
|
| |
| |
| |
| |
| async function canDrop(userId) { |
| if (userId === OWNER_ID) return { allowed: true, remaining: Infinity }; |
|
|
| const wl = await stmts.getWhitelist(userId); |
| if (!wl) return { allowed: false, reason: 'not_whitelisted' }; |
|
|
| const limit = wl.max_drops; |
| const { count } = await stmts.getDropCount24h(userId); |
| if (count >= limit) { |
| const last = await stmts.getLastDrop(userId); |
| const resetTime = new Date(last.dropped_at); |
| 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: 'category', |
| category: null, |
| title: null, |
| file: null, |
| warnings: null, |
| status: null, |
| about: null, |
| image: null, |
| channelId: null, |
| isExternal: false, |
| }; |
| activeSessions.set(userId, session); |
| return session; |
| } |
|
|
| |
| |
| |
| function hasSession(userId) { |
| return activeSessions.has(userId); |
| } |
|
|
| |
| |
| |
| function getPrompt(session) { |
| switch (session.step) { |
| case 'category': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 1/8', |
| description: '> **Which category does this drop belong to?**\n\nClick a button below.', |
| color: Colors.PRIMARY, |
| footer: 'Type "cancel" at any time to abort', |
| })], |
| components: [new ActionRowBuilder().addComponents( |
| new ButtonBuilder().setCustomId('cat_sources').setLabel('Sources').setStyle(ButtonStyle.Secondary), |
| new ButtonBuilder().setCustomId('cat_cracks').setLabel('Cracks').setStyle(ButtonStyle.Secondary), |
| new ButtonBuilder().setCustomId('cat_scripts').setLabel('Scripts').setStyle(ButtonStyle.Secondary), |
| new ButtonBuilder().setCustomId('cat_tools').setLabel('Tools').setStyle(ButtonStyle.Secondary) |
| )], |
| }; |
|
|
| case 'title': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 2/8', |
| description: '> **What is the title for this drop?**\n\nType the title below.', |
| color: Colors.PRIMARY, |
| footer: `Category: ${session.category.toUpperCase()} | Type "cancel" to abort`, |
| })], |
| }; |
|
|
| case 'file': |
| return { |
| embeds: [createEmbed({ |
| title: 'π¦ New Drop β Step 3/8', |
| 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 4/8', |
| 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 5/8', |
| 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 6/8', |
| 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 7/8', |
| 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: ${session.file.size !== 'Unknown' ? formatBytes(session.file.size) : 'External File (Unknown Size)'}`); |
| lines.push(`π Status: ${statusText}`); |
| lines.push('```'); |
| lines.push(''); |
| lines.push('βββββββββββββββββββββββββββββββββββββ'); |
| lines.push('*Wyvern Softworks β Educational Use Only*'); |
|
|
| const embedData = { |
| title: `π¦ [${session.category.toUpperCase()}] ${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 = typeof session.image === 'object' ? session.image.url : 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) { |
| |
| if (content.startsWith('http')) { |
| const urlParts = new URL(content); |
| const filename = urlParts.pathname.split('/').pop() || `file_${Date.now()}`; |
| session.file = { |
| url: content, |
| name: filename, |
| size: 'Unknown', |
| isExternal: true |
| }; |
| session.step = 'warnings'; |
| await message.reply(getPrompt(session)); |
| return true; |
| } |
| await message.reply({ content: 'β Please attach a file or provide a direct link (Mega, MediaFire, etc).' }); |
| return true; |
| } |
| const att = message.attachments.first(); |
| |
| session.file = { |
| url: att.url, |
| name: att.name, |
| size: att.size, |
| attachment: att, |
| isExternal: false |
| }; |
| 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) { |
| const imgAtt = message.attachments.first(); |
| session.image = { |
| url: imgAtt.url, |
| name: imgAtt.name, |
| attachment: imgAtt |
| }; |
| } 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: [row], |
| }); |
| 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.startsWith('cat_')) { |
| session.category = customId.replace('cat_', ''); |
| session.step = 'title'; |
| await interaction.update({ |
| embeds: [createEmbed({ |
| title: `π Category Selected: ${session.category.toUpperCase()}`, |
| color: Colors.PRIMARY, |
| })], components: [] |
| }); |
| await interaction.followUp(getPrompt(session)); |
| return true; |
| } |
|
|
| 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') { |
| await interaction.update({ |
| content: 'β³ *Deploying drop to website API...*', |
| embeds: [], |
| components: [] |
| }); |
|
|
| try { |
| let assetId = null; |
| let fileUrl = session.file.url; |
| |
| |
| if (!session.file.isExternal) { |
| const octokit = new Octokit({ auth: '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' }); |
| const [owner, repo] = 'APRK01/WSB-Storage'.split('/'); |
|
|
| |
| const fileRes = await fetch(session.file.url); |
| const fileBuffer = await fileRes.buffer(); |
|
|
| |
| const releaseTitle = `Drop: ${session.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`; |
| const release = await octokit.rest.repos.createRelease({ |
| owner, |
| repo, |
| tag_name: `drop-${Date.now()}`, |
| name: releaseTitle, |
| body: `Auto-generated drop upload for WSB.\n\nDescription: ${session.about}` |
| }); |
|
|
| |
| const uploadRes = await octokit.rest.repos.uploadReleaseAsset({ |
| owner, |
| repo, |
| release_id: release.data.id, |
| name: session.file.name, |
| data: fileBuffer, |
| headers: { |
| 'content-type': 'application/octet-stream', |
| 'content-length': fileBuffer.length |
| } |
| }); |
|
|
| assetId = uploadRes.data.id.toString(); |
| } |
|
|
| |
| let finalImageUrl = session.image?.url || null; |
|
|
| const payload = { |
| category: session.category, |
| title: session.title, |
| description: session.about, |
| status: session.status, |
| isExternal: session.file.isExternal ? 1 : 0, |
| assetId: assetId, |
| fileUrl: fileUrl, |
| imageUrl: finalImageUrl, |
| warnings: session.warnings |
| }; |
|
|
| console.log('[Website Drop Payload]', payload); |
| |
| |
| const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops'; |
| try { |
| |
| await fetch(WEBSITE_API, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload) |
| }).catch(() => {}); |
| } catch(e) {} |
|
|
| |
| await stmts.addWebDrop( |
| userId, |
| session.category, |
| payload.title, |
| payload.description, |
| payload.status, |
| payload.isExternal, |
| payload.assetId, |
| payload.fileUrl, |
| payload.imageUrl |
| ); |
|
|
| |
| await stmts.logDrop(userId, `[${session.category.toUpperCase()}] ${session.title}`, 'website'); |
|
|
| activeSessions.delete(userId); |
| |
| await interaction.followUp({ |
| embeds: [createEmbed({ |
| title: 'β
Drop Published!', |
| description: `Successfully deployed to the Website Database!\n\n> Note: Embeds are no longer posted to Discord channels. Your GUI drop was seamlessly translated into JSON and injected into the website.`, |
| color: Colors.SUCCESS, |
| })], |
| }); |
|
|
| } catch (err) { |
| console.error('[Web Deploy Error]', err); |
| await interaction.followUp({ content: `β Failed to deploy to website: ${err.message}` }); |
| } |
| 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; |
| } |
|
|
| |
| |
| |
| |
| async function handleDownloadButton(interaction) { |
| const { customId } = interaction; |
| if (!customId.startsWith('dl_')) return false; |
|
|
| const assetId = customId.split('_')[1]; |
| if (!assetId) return false; |
|
|
| try { |
| await interaction.deferReply({ ephemeral: true }); |
|
|
| const GITHUB_TOKEN = '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'; |
| const [owner, repo] = 'APRK01/WSB-Storage'.split('/'); |
|
|
| |
| |
| const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/assets/${assetId}`; |
| const response = await fetch(apiUrl, { |
| headers: { |
| 'Authorization': `Bearer ${GITHUB_TOKEN}`, |
| 'Accept': 'application/octet-stream', |
| 'User-Agent': 'WSB-Bot' |
| }, |
| redirect: 'manual' |
| }); |
|
|
| if (response.status === 302) { |
| const tempUrl = response.headers.get('location'); |
| await interaction.editReply({ |
| content: `π₯ **Your private download link:**\n${tempUrl}\n\n> β οΈ This link expires in a few minutes. Do **not** share it.`, |
| }); |
| } else { |
| |
| const directResponse = await fetch(apiUrl, { |
| headers: { |
| 'Authorization': `Bearer ${GITHUB_TOKEN}`, |
| 'Accept': 'application/octet-stream', |
| 'User-Agent': 'WSB-Bot' |
| } |
| }); |
| if (directResponse.ok) { |
| |
| await interaction.editReply({ |
| content: `π₯ **Your private download link:**\n${directResponse.url}\n\n> β οΈ This link expires in a few minutes. Do **not** share it.`, |
| }); |
| } else { |
| await interaction.editReply({ |
| content: 'β Failed to generate download link. The file may have been deleted.', |
| }); |
| } |
| } |
| return true; |
| } catch (err) { |
| console.error('[Download Button Error]', err); |
| try { |
| await interaction.editReply({ |
| content: 'β Something went wrong generating your download link. Please try again.', |
| }); |
| } catch (_) { } |
| return true; |
| } |
| } |
|
|
| |
| |
| |
| 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, |
| handleDownloadButton, |
| buildDropEmbed, |
| canDrop, |
| activeSessions, |
| }; |
|
|