Upload 43 files
Browse files- Dockerfile +23 -0
- data/wsb.db +0 -0
- data/wsb.db-shm +0 -0
- data/wsb.db-wal +0 -0
- package-lock.json +657 -0
- package.json +16 -0
- src/commands/applyUpdates.js +131 -0
- src/commands/backupLayout.js +67 -0
- src/commands/clearDrops.js +72 -0
- src/commands/coOwnerRole.js +165 -0
- src/commands/createChannels.js +215 -0
- src/commands/createRoles.js +73 -0
- src/commands/deleteDrop.js +50 -0
- src/commands/editDrop.js +63 -0
- src/commands/fixDownloads.js +209 -0
- src/commands/fixPings.js +38 -0
- src/commands/permissionAudit.js +72 -0
- src/commands/postDisclaimer.js +10 -0
- src/commands/postRules.js +10 -0
- src/commands/setup.js +90 -0
- src/commands/shutdown.js +22 -0
- src/commands/ticketStats.js +24 -0
- src/config.js +238 -0
- src/database.js +179 -0
- src/events/guildMemberAdd.js +8 -0
- src/events/guildMemberRemove.js +8 -0
- src/events/guildMemberUpdate.js +8 -0
- src/events/interactionCreate.js +30 -0
- src/events/messageCreate.js +221 -0
- src/events/messageReactionAdd.js +19 -0
- src/events/messageReactionRemove.js +17 -0
- src/events/ready.js +19 -0
- src/index.js +132 -0
- src/systems/booster.js +36 -0
- src/systems/drops.js +579 -0
- src/systems/embeds.js +99 -0
- src/systems/logger.js +68 -0
- src/systems/massdrop.js +530 -0
- src/systems/tickets.js +246 -0
- src/systems/verification.js +100 -0
- src/systems/welcome.js +52 -0
- src/utils/embeds.js +50 -0
- src/utils/permissions.js +53 -0
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight Node.js image
|
| 2 |
+
FROM node:20-slim
|
| 3 |
+
|
| 4 |
+
# Create app directory
|
| 5 |
+
WORKDIR /usr/src/app
|
| 6 |
+
|
| 7 |
+
# Copy package files first to optimize Docker layer caching
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN npm install
|
| 12 |
+
|
| 13 |
+
# Copy all the remaining project files
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Define the port Hugging Face Spaces expects apps to run on
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
# Force the bot's keep-alive web server to run on port 7860
|
| 20 |
+
ENV PORT=7860
|
| 21 |
+
|
| 22 |
+
# Command to start the bot
|
| 23 |
+
CMD [ "npm", "start" ]
|
data/wsb.db
ADDED
|
Binary file (36.9 kB). View file
|
|
|
data/wsb.db-shm
ADDED
|
Binary file (32.8 kB). View file
|
|
|
data/wsb.db-wal
ADDED
|
Binary file (37.1 kB). View file
|
|
|
package-lock.json
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "wsb-bot",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "wsb-bot",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@octokit/rest": "^22.0.1",
|
| 12 |
+
"@supabase/supabase-js": "^2.99.3",
|
| 13 |
+
"discord.js": "^14.16.0",
|
| 14 |
+
"dotenv": "^16.4.0",
|
| 15 |
+
"node-fetch": "^2.7.0"
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
"node_modules/@discordjs/builders": {
|
| 19 |
+
"version": "1.13.1",
|
| 20 |
+
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
| 21 |
+
"integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
|
| 22 |
+
"license": "Apache-2.0",
|
| 23 |
+
"dependencies": {
|
| 24 |
+
"@discordjs/formatters": "^0.6.2",
|
| 25 |
+
"@discordjs/util": "^1.2.0",
|
| 26 |
+
"@sapphire/shapeshift": "^4.0.0",
|
| 27 |
+
"discord-api-types": "^0.38.33",
|
| 28 |
+
"fast-deep-equal": "^3.1.3",
|
| 29 |
+
"ts-mixer": "^6.0.4",
|
| 30 |
+
"tslib": "^2.6.3"
|
| 31 |
+
},
|
| 32 |
+
"engines": {
|
| 33 |
+
"node": ">=16.11.0"
|
| 34 |
+
},
|
| 35 |
+
"funding": {
|
| 36 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"node_modules/@discordjs/collection": {
|
| 40 |
+
"version": "1.5.3",
|
| 41 |
+
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
| 42 |
+
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
| 43 |
+
"license": "Apache-2.0",
|
| 44 |
+
"engines": {
|
| 45 |
+
"node": ">=16.11.0"
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"node_modules/@discordjs/formatters": {
|
| 49 |
+
"version": "0.6.2",
|
| 50 |
+
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
| 51 |
+
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
| 52 |
+
"license": "Apache-2.0",
|
| 53 |
+
"dependencies": {
|
| 54 |
+
"discord-api-types": "^0.38.33"
|
| 55 |
+
},
|
| 56 |
+
"engines": {
|
| 57 |
+
"node": ">=16.11.0"
|
| 58 |
+
},
|
| 59 |
+
"funding": {
|
| 60 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
"node_modules/@discordjs/rest": {
|
| 64 |
+
"version": "2.6.0",
|
| 65 |
+
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
|
| 66 |
+
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
|
| 67 |
+
"license": "Apache-2.0",
|
| 68 |
+
"dependencies": {
|
| 69 |
+
"@discordjs/collection": "^2.1.1",
|
| 70 |
+
"@discordjs/util": "^1.1.1",
|
| 71 |
+
"@sapphire/async-queue": "^1.5.3",
|
| 72 |
+
"@sapphire/snowflake": "^3.5.3",
|
| 73 |
+
"@vladfrangu/async_event_emitter": "^2.4.6",
|
| 74 |
+
"discord-api-types": "^0.38.16",
|
| 75 |
+
"magic-bytes.js": "^1.10.0",
|
| 76 |
+
"tslib": "^2.6.3",
|
| 77 |
+
"undici": "6.21.3"
|
| 78 |
+
},
|
| 79 |
+
"engines": {
|
| 80 |
+
"node": ">=18"
|
| 81 |
+
},
|
| 82 |
+
"funding": {
|
| 83 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
| 87 |
+
"version": "2.1.1",
|
| 88 |
+
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
| 89 |
+
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
| 90 |
+
"license": "Apache-2.0",
|
| 91 |
+
"engines": {
|
| 92 |
+
"node": ">=18"
|
| 93 |
+
},
|
| 94 |
+
"funding": {
|
| 95 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
"node_modules/@discordjs/util": {
|
| 99 |
+
"version": "1.2.0",
|
| 100 |
+
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
| 101 |
+
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
| 102 |
+
"license": "Apache-2.0",
|
| 103 |
+
"dependencies": {
|
| 104 |
+
"discord-api-types": "^0.38.33"
|
| 105 |
+
},
|
| 106 |
+
"engines": {
|
| 107 |
+
"node": ">=18"
|
| 108 |
+
},
|
| 109 |
+
"funding": {
|
| 110 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 111 |
+
}
|
| 112 |
+
},
|
| 113 |
+
"node_modules/@discordjs/ws": {
|
| 114 |
+
"version": "1.2.3",
|
| 115 |
+
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
| 116 |
+
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
| 117 |
+
"license": "Apache-2.0",
|
| 118 |
+
"dependencies": {
|
| 119 |
+
"@discordjs/collection": "^2.1.0",
|
| 120 |
+
"@discordjs/rest": "^2.5.1",
|
| 121 |
+
"@discordjs/util": "^1.1.0",
|
| 122 |
+
"@sapphire/async-queue": "^1.5.2",
|
| 123 |
+
"@types/ws": "^8.5.10",
|
| 124 |
+
"@vladfrangu/async_event_emitter": "^2.2.4",
|
| 125 |
+
"discord-api-types": "^0.38.1",
|
| 126 |
+
"tslib": "^2.6.2",
|
| 127 |
+
"ws": "^8.17.0"
|
| 128 |
+
},
|
| 129 |
+
"engines": {
|
| 130 |
+
"node": ">=16.11.0"
|
| 131 |
+
},
|
| 132 |
+
"funding": {
|
| 133 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
| 137 |
+
"version": "2.1.1",
|
| 138 |
+
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
| 139 |
+
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
| 140 |
+
"license": "Apache-2.0",
|
| 141 |
+
"engines": {
|
| 142 |
+
"node": ">=18"
|
| 143 |
+
},
|
| 144 |
+
"funding": {
|
| 145 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 146 |
+
}
|
| 147 |
+
},
|
| 148 |
+
"node_modules/@octokit/auth-token": {
|
| 149 |
+
"version": "6.0.0",
|
| 150 |
+
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
|
| 151 |
+
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
|
| 152 |
+
"license": "MIT",
|
| 153 |
+
"engines": {
|
| 154 |
+
"node": ">= 20"
|
| 155 |
+
}
|
| 156 |
+
},
|
| 157 |
+
"node_modules/@octokit/core": {
|
| 158 |
+
"version": "7.0.6",
|
| 159 |
+
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
|
| 160 |
+
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
|
| 161 |
+
"license": "MIT",
|
| 162 |
+
"peer": true,
|
| 163 |
+
"dependencies": {
|
| 164 |
+
"@octokit/auth-token": "^6.0.0",
|
| 165 |
+
"@octokit/graphql": "^9.0.3",
|
| 166 |
+
"@octokit/request": "^10.0.6",
|
| 167 |
+
"@octokit/request-error": "^7.0.2",
|
| 168 |
+
"@octokit/types": "^16.0.0",
|
| 169 |
+
"before-after-hook": "^4.0.0",
|
| 170 |
+
"universal-user-agent": "^7.0.0"
|
| 171 |
+
},
|
| 172 |
+
"engines": {
|
| 173 |
+
"node": ">= 20"
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
"node_modules/@octokit/endpoint": {
|
| 177 |
+
"version": "11.0.3",
|
| 178 |
+
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
|
| 179 |
+
"integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
|
| 180 |
+
"license": "MIT",
|
| 181 |
+
"dependencies": {
|
| 182 |
+
"@octokit/types": "^16.0.0",
|
| 183 |
+
"universal-user-agent": "^7.0.2"
|
| 184 |
+
},
|
| 185 |
+
"engines": {
|
| 186 |
+
"node": ">= 20"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
"node_modules/@octokit/graphql": {
|
| 190 |
+
"version": "9.0.3",
|
| 191 |
+
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
|
| 192 |
+
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
|
| 193 |
+
"license": "MIT",
|
| 194 |
+
"dependencies": {
|
| 195 |
+
"@octokit/request": "^10.0.6",
|
| 196 |
+
"@octokit/types": "^16.0.0",
|
| 197 |
+
"universal-user-agent": "^7.0.0"
|
| 198 |
+
},
|
| 199 |
+
"engines": {
|
| 200 |
+
"node": ">= 20"
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
"node_modules/@octokit/openapi-types": {
|
| 204 |
+
"version": "27.0.0",
|
| 205 |
+
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
|
| 206 |
+
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
|
| 207 |
+
"license": "MIT"
|
| 208 |
+
},
|
| 209 |
+
"node_modules/@octokit/plugin-paginate-rest": {
|
| 210 |
+
"version": "14.0.0",
|
| 211 |
+
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
|
| 212 |
+
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
|
| 213 |
+
"license": "MIT",
|
| 214 |
+
"dependencies": {
|
| 215 |
+
"@octokit/types": "^16.0.0"
|
| 216 |
+
},
|
| 217 |
+
"engines": {
|
| 218 |
+
"node": ">= 20"
|
| 219 |
+
},
|
| 220 |
+
"peerDependencies": {
|
| 221 |
+
"@octokit/core": ">=6"
|
| 222 |
+
}
|
| 223 |
+
},
|
| 224 |
+
"node_modules/@octokit/plugin-request-log": {
|
| 225 |
+
"version": "6.0.0",
|
| 226 |
+
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
|
| 227 |
+
"integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
|
| 228 |
+
"license": "MIT",
|
| 229 |
+
"engines": {
|
| 230 |
+
"node": ">= 20"
|
| 231 |
+
},
|
| 232 |
+
"peerDependencies": {
|
| 233 |
+
"@octokit/core": ">=6"
|
| 234 |
+
}
|
| 235 |
+
},
|
| 236 |
+
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
| 237 |
+
"version": "17.0.0",
|
| 238 |
+
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
|
| 239 |
+
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
|
| 240 |
+
"license": "MIT",
|
| 241 |
+
"dependencies": {
|
| 242 |
+
"@octokit/types": "^16.0.0"
|
| 243 |
+
},
|
| 244 |
+
"engines": {
|
| 245 |
+
"node": ">= 20"
|
| 246 |
+
},
|
| 247 |
+
"peerDependencies": {
|
| 248 |
+
"@octokit/core": ">=6"
|
| 249 |
+
}
|
| 250 |
+
},
|
| 251 |
+
"node_modules/@octokit/request": {
|
| 252 |
+
"version": "10.0.8",
|
| 253 |
+
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz",
|
| 254 |
+
"integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==",
|
| 255 |
+
"license": "MIT",
|
| 256 |
+
"dependencies": {
|
| 257 |
+
"@octokit/endpoint": "^11.0.3",
|
| 258 |
+
"@octokit/request-error": "^7.0.2",
|
| 259 |
+
"@octokit/types": "^16.0.0",
|
| 260 |
+
"fast-content-type-parse": "^3.0.0",
|
| 261 |
+
"json-with-bigint": "^3.5.3",
|
| 262 |
+
"universal-user-agent": "^7.0.2"
|
| 263 |
+
},
|
| 264 |
+
"engines": {
|
| 265 |
+
"node": ">= 20"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/@octokit/request-error": {
|
| 269 |
+
"version": "7.1.0",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
|
| 271 |
+
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
|
| 272 |
+
"license": "MIT",
|
| 273 |
+
"dependencies": {
|
| 274 |
+
"@octokit/types": "^16.0.0"
|
| 275 |
+
},
|
| 276 |
+
"engines": {
|
| 277 |
+
"node": ">= 20"
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
"node_modules/@octokit/rest": {
|
| 281 |
+
"version": "22.0.1",
|
| 282 |
+
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz",
|
| 283 |
+
"integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==",
|
| 284 |
+
"license": "MIT",
|
| 285 |
+
"dependencies": {
|
| 286 |
+
"@octokit/core": "^7.0.6",
|
| 287 |
+
"@octokit/plugin-paginate-rest": "^14.0.0",
|
| 288 |
+
"@octokit/plugin-request-log": "^6.0.0",
|
| 289 |
+
"@octokit/plugin-rest-endpoint-methods": "^17.0.0"
|
| 290 |
+
},
|
| 291 |
+
"engines": {
|
| 292 |
+
"node": ">= 20"
|
| 293 |
+
}
|
| 294 |
+
},
|
| 295 |
+
"node_modules/@octokit/types": {
|
| 296 |
+
"version": "16.0.0",
|
| 297 |
+
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
|
| 298 |
+
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
|
| 299 |
+
"license": "MIT",
|
| 300 |
+
"dependencies": {
|
| 301 |
+
"@octokit/openapi-types": "^27.0.0"
|
| 302 |
+
}
|
| 303 |
+
},
|
| 304 |
+
"node_modules/@sapphire/async-queue": {
|
| 305 |
+
"version": "1.5.5",
|
| 306 |
+
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
| 307 |
+
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
| 308 |
+
"license": "MIT",
|
| 309 |
+
"engines": {
|
| 310 |
+
"node": ">=v14.0.0",
|
| 311 |
+
"npm": ">=7.0.0"
|
| 312 |
+
}
|
| 313 |
+
},
|
| 314 |
+
"node_modules/@sapphire/shapeshift": {
|
| 315 |
+
"version": "4.0.0",
|
| 316 |
+
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
| 317 |
+
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
| 318 |
+
"license": "MIT",
|
| 319 |
+
"dependencies": {
|
| 320 |
+
"fast-deep-equal": "^3.1.3",
|
| 321 |
+
"lodash": "^4.17.21"
|
| 322 |
+
},
|
| 323 |
+
"engines": {
|
| 324 |
+
"node": ">=v16"
|
| 325 |
+
}
|
| 326 |
+
},
|
| 327 |
+
"node_modules/@sapphire/snowflake": {
|
| 328 |
+
"version": "3.5.3",
|
| 329 |
+
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
| 330 |
+
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
| 331 |
+
"license": "MIT",
|
| 332 |
+
"engines": {
|
| 333 |
+
"node": ">=v14.0.0",
|
| 334 |
+
"npm": ">=7.0.0"
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
"node_modules/@supabase/auth-js": {
|
| 338 |
+
"version": "2.99.3",
|
| 339 |
+
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.99.3.tgz",
|
| 340 |
+
"integrity": "sha512-vMEVLA1kGGYd/kdsJSwtjiFUZM1nGfrz2DWmgMBZtocV48qL+L2+4QpIkueXyBEumMQZFEyhz57i/5zGHjvdBw==",
|
| 341 |
+
"license": "MIT",
|
| 342 |
+
"dependencies": {
|
| 343 |
+
"tslib": "2.8.1"
|
| 344 |
+
},
|
| 345 |
+
"engines": {
|
| 346 |
+
"node": ">=20.0.0"
|
| 347 |
+
}
|
| 348 |
+
},
|
| 349 |
+
"node_modules/@supabase/functions-js": {
|
| 350 |
+
"version": "2.99.3",
|
| 351 |
+
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.99.3.tgz",
|
| 352 |
+
"integrity": "sha512-6tk2zrcBkzKaaBXPOG5nshn30uJNFGOH9LxOnE8i850eQmsX+jVm7vql9kTPyvUzEHwU4zdjSOkXS9M+9ukMVA==",
|
| 353 |
+
"license": "MIT",
|
| 354 |
+
"dependencies": {
|
| 355 |
+
"tslib": "2.8.1"
|
| 356 |
+
},
|
| 357 |
+
"engines": {
|
| 358 |
+
"node": ">=20.0.0"
|
| 359 |
+
}
|
| 360 |
+
},
|
| 361 |
+
"node_modules/@supabase/postgrest-js": {
|
| 362 |
+
"version": "2.99.3",
|
| 363 |
+
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.99.3.tgz",
|
| 364 |
+
"integrity": "sha512-8HxEf+zNycj7Z8+ONhhlu+7J7Ha+L6weyCtdEeK2mN5OWJbh6n4LPU4iuJ5UlCvvNnbSXMoutY7piITEEAgl2g==",
|
| 365 |
+
"license": "MIT",
|
| 366 |
+
"dependencies": {
|
| 367 |
+
"tslib": "2.8.1"
|
| 368 |
+
},
|
| 369 |
+
"engines": {
|
| 370 |
+
"node": ">=20.0.0"
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
"node_modules/@supabase/realtime-js": {
|
| 374 |
+
"version": "2.99.3",
|
| 375 |
+
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.99.3.tgz",
|
| 376 |
+
"integrity": "sha512-c1azgZ2nZPczbY5k5u5iFrk1InpxN81IvNE+UBAkjrBz3yc5ALLJNkeTQwbJZT4PZBuYXEzqYGLMuh9fdTtTMg==",
|
| 377 |
+
"license": "MIT",
|
| 378 |
+
"dependencies": {
|
| 379 |
+
"@types/phoenix": "^1.6.6",
|
| 380 |
+
"@types/ws": "^8.18.1",
|
| 381 |
+
"tslib": "2.8.1",
|
| 382 |
+
"ws": "^8.18.2"
|
| 383 |
+
},
|
| 384 |
+
"engines": {
|
| 385 |
+
"node": ">=20.0.0"
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
"node_modules/@supabase/storage-js": {
|
| 389 |
+
"version": "2.99.3",
|
| 390 |
+
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.99.3.tgz",
|
| 391 |
+
"integrity": "sha512-lOfIm4hInNcd8x0i1LWphnLKxec42wwbjs+vhaVAvR801Vda0UAMbTooUY6gfqgQb8v29GofqKuQMMTAsl6w/w==",
|
| 392 |
+
"license": "MIT",
|
| 393 |
+
"dependencies": {
|
| 394 |
+
"iceberg-js": "^0.8.1",
|
| 395 |
+
"tslib": "2.8.1"
|
| 396 |
+
},
|
| 397 |
+
"engines": {
|
| 398 |
+
"node": ">=20.0.0"
|
| 399 |
+
}
|
| 400 |
+
},
|
| 401 |
+
"node_modules/@supabase/supabase-js": {
|
| 402 |
+
"version": "2.99.3",
|
| 403 |
+
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.99.3.tgz",
|
| 404 |
+
"integrity": "sha512-GuPbzoEaI51AkLw9VGhLNvnzw4PHbS3p8j2/JlvLeZNQMKwZw4aEYQIDBRtFwL5Nv7/275n9m4DHtakY8nCvgg==",
|
| 405 |
+
"license": "MIT",
|
| 406 |
+
"dependencies": {
|
| 407 |
+
"@supabase/auth-js": "2.99.3",
|
| 408 |
+
"@supabase/functions-js": "2.99.3",
|
| 409 |
+
"@supabase/postgrest-js": "2.99.3",
|
| 410 |
+
"@supabase/realtime-js": "2.99.3",
|
| 411 |
+
"@supabase/storage-js": "2.99.3"
|
| 412 |
+
},
|
| 413 |
+
"engines": {
|
| 414 |
+
"node": ">=20.0.0"
|
| 415 |
+
}
|
| 416 |
+
},
|
| 417 |
+
"node_modules/@types/node": {
|
| 418 |
+
"version": "25.3.3",
|
| 419 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
|
| 420 |
+
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
|
| 421 |
+
"license": "MIT",
|
| 422 |
+
"dependencies": {
|
| 423 |
+
"undici-types": "~7.18.0"
|
| 424 |
+
}
|
| 425 |
+
},
|
| 426 |
+
"node_modules/@types/phoenix": {
|
| 427 |
+
"version": "1.6.7",
|
| 428 |
+
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
|
| 429 |
+
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
| 430 |
+
"license": "MIT"
|
| 431 |
+
},
|
| 432 |
+
"node_modules/@types/ws": {
|
| 433 |
+
"version": "8.18.1",
|
| 434 |
+
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
| 435 |
+
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
| 436 |
+
"license": "MIT",
|
| 437 |
+
"dependencies": {
|
| 438 |
+
"@types/node": "*"
|
| 439 |
+
}
|
| 440 |
+
},
|
| 441 |
+
"node_modules/@vladfrangu/async_event_emitter": {
|
| 442 |
+
"version": "2.4.7",
|
| 443 |
+
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
| 444 |
+
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
| 445 |
+
"license": "MIT",
|
| 446 |
+
"engines": {
|
| 447 |
+
"node": ">=v14.0.0",
|
| 448 |
+
"npm": ">=7.0.0"
|
| 449 |
+
}
|
| 450 |
+
},
|
| 451 |
+
"node_modules/before-after-hook": {
|
| 452 |
+
"version": "4.0.0",
|
| 453 |
+
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
|
| 454 |
+
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
|
| 455 |
+
"license": "Apache-2.0"
|
| 456 |
+
},
|
| 457 |
+
"node_modules/discord-api-types": {
|
| 458 |
+
"version": "0.38.40",
|
| 459 |
+
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.40.tgz",
|
| 460 |
+
"integrity": "sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==",
|
| 461 |
+
"license": "MIT",
|
| 462 |
+
"workspaces": [
|
| 463 |
+
"scripts/actions/documentation"
|
| 464 |
+
]
|
| 465 |
+
},
|
| 466 |
+
"node_modules/discord.js": {
|
| 467 |
+
"version": "14.25.1",
|
| 468 |
+
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
|
| 469 |
+
"integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
|
| 470 |
+
"license": "Apache-2.0",
|
| 471 |
+
"dependencies": {
|
| 472 |
+
"@discordjs/builders": "^1.13.0",
|
| 473 |
+
"@discordjs/collection": "1.5.3",
|
| 474 |
+
"@discordjs/formatters": "^0.6.2",
|
| 475 |
+
"@discordjs/rest": "^2.6.0",
|
| 476 |
+
"@discordjs/util": "^1.2.0",
|
| 477 |
+
"@discordjs/ws": "^1.2.3",
|
| 478 |
+
"@sapphire/snowflake": "3.5.3",
|
| 479 |
+
"discord-api-types": "^0.38.33",
|
| 480 |
+
"fast-deep-equal": "3.1.3",
|
| 481 |
+
"lodash.snakecase": "4.1.1",
|
| 482 |
+
"magic-bytes.js": "^1.10.0",
|
| 483 |
+
"tslib": "^2.6.3",
|
| 484 |
+
"undici": "6.21.3"
|
| 485 |
+
},
|
| 486 |
+
"engines": {
|
| 487 |
+
"node": ">=18"
|
| 488 |
+
},
|
| 489 |
+
"funding": {
|
| 490 |
+
"url": "https://github.com/discordjs/discord.js?sponsor"
|
| 491 |
+
}
|
| 492 |
+
},
|
| 493 |
+
"node_modules/dotenv": {
|
| 494 |
+
"version": "16.6.1",
|
| 495 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
| 496 |
+
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
| 497 |
+
"license": "BSD-2-Clause",
|
| 498 |
+
"engines": {
|
| 499 |
+
"node": ">=12"
|
| 500 |
+
},
|
| 501 |
+
"funding": {
|
| 502 |
+
"url": "https://dotenvx.com"
|
| 503 |
+
}
|
| 504 |
+
},
|
| 505 |
+
"node_modules/fast-content-type-parse": {
|
| 506 |
+
"version": "3.0.0",
|
| 507 |
+
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
|
| 508 |
+
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
|
| 509 |
+
"funding": [
|
| 510 |
+
{
|
| 511 |
+
"type": "github",
|
| 512 |
+
"url": "https://github.com/sponsors/fastify"
|
| 513 |
+
},
|
| 514 |
+
{
|
| 515 |
+
"type": "opencollective",
|
| 516 |
+
"url": "https://opencollective.com/fastify"
|
| 517 |
+
}
|
| 518 |
+
],
|
| 519 |
+
"license": "MIT"
|
| 520 |
+
},
|
| 521 |
+
"node_modules/fast-deep-equal": {
|
| 522 |
+
"version": "3.1.3",
|
| 523 |
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
| 524 |
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
| 525 |
+
"license": "MIT"
|
| 526 |
+
},
|
| 527 |
+
"node_modules/iceberg-js": {
|
| 528 |
+
"version": "0.8.1",
|
| 529 |
+
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
| 530 |
+
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
| 531 |
+
"license": "MIT",
|
| 532 |
+
"engines": {
|
| 533 |
+
"node": ">=20.0.0"
|
| 534 |
+
}
|
| 535 |
+
},
|
| 536 |
+
"node_modules/json-with-bigint": {
|
| 537 |
+
"version": "3.5.7",
|
| 538 |
+
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz",
|
| 539 |
+
"integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==",
|
| 540 |
+
"license": "MIT"
|
| 541 |
+
},
|
| 542 |
+
"node_modules/lodash": {
|
| 543 |
+
"version": "4.17.23",
|
| 544 |
+
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
| 545 |
+
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
| 546 |
+
"license": "MIT"
|
| 547 |
+
},
|
| 548 |
+
"node_modules/lodash.snakecase": {
|
| 549 |
+
"version": "4.1.1",
|
| 550 |
+
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
| 551 |
+
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
| 552 |
+
"license": "MIT"
|
| 553 |
+
},
|
| 554 |
+
"node_modules/magic-bytes.js": {
|
| 555 |
+
"version": "1.13.0",
|
| 556 |
+
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
| 557 |
+
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
| 558 |
+
"license": "MIT"
|
| 559 |
+
},
|
| 560 |
+
"node_modules/node-fetch": {
|
| 561 |
+
"version": "2.7.0",
|
| 562 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
| 563 |
+
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
| 564 |
+
"license": "MIT",
|
| 565 |
+
"dependencies": {
|
| 566 |
+
"whatwg-url": "^5.0.0"
|
| 567 |
+
},
|
| 568 |
+
"engines": {
|
| 569 |
+
"node": "4.x || >=6.0.0"
|
| 570 |
+
},
|
| 571 |
+
"peerDependencies": {
|
| 572 |
+
"encoding": "^0.1.0"
|
| 573 |
+
},
|
| 574 |
+
"peerDependenciesMeta": {
|
| 575 |
+
"encoding": {
|
| 576 |
+
"optional": true
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
},
|
| 580 |
+
"node_modules/tr46": {
|
| 581 |
+
"version": "0.0.3",
|
| 582 |
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
| 583 |
+
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
| 584 |
+
"license": "MIT"
|
| 585 |
+
},
|
| 586 |
+
"node_modules/ts-mixer": {
|
| 587 |
+
"version": "6.0.4",
|
| 588 |
+
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
| 589 |
+
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
| 590 |
+
"license": "MIT"
|
| 591 |
+
},
|
| 592 |
+
"node_modules/tslib": {
|
| 593 |
+
"version": "2.8.1",
|
| 594 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 595 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 596 |
+
"license": "0BSD"
|
| 597 |
+
},
|
| 598 |
+
"node_modules/undici": {
|
| 599 |
+
"version": "6.21.3",
|
| 600 |
+
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
| 601 |
+
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
| 602 |
+
"license": "MIT",
|
| 603 |
+
"engines": {
|
| 604 |
+
"node": ">=18.17"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
"node_modules/undici-types": {
|
| 608 |
+
"version": "7.18.2",
|
| 609 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
| 610 |
+
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
| 611 |
+
"license": "MIT"
|
| 612 |
+
},
|
| 613 |
+
"node_modules/universal-user-agent": {
|
| 614 |
+
"version": "7.0.3",
|
| 615 |
+
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
|
| 616 |
+
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
|
| 617 |
+
"license": "ISC"
|
| 618 |
+
},
|
| 619 |
+
"node_modules/webidl-conversions": {
|
| 620 |
+
"version": "3.0.1",
|
| 621 |
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
| 622 |
+
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
| 623 |
+
"license": "BSD-2-Clause"
|
| 624 |
+
},
|
| 625 |
+
"node_modules/whatwg-url": {
|
| 626 |
+
"version": "5.0.0",
|
| 627 |
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
| 628 |
+
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
| 629 |
+
"license": "MIT",
|
| 630 |
+
"dependencies": {
|
| 631 |
+
"tr46": "~0.0.3",
|
| 632 |
+
"webidl-conversions": "^3.0.0"
|
| 633 |
+
}
|
| 634 |
+
},
|
| 635 |
+
"node_modules/ws": {
|
| 636 |
+
"version": "8.19.0",
|
| 637 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
| 638 |
+
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
| 639 |
+
"license": "MIT",
|
| 640 |
+
"engines": {
|
| 641 |
+
"node": ">=10.0.0"
|
| 642 |
+
},
|
| 643 |
+
"peerDependencies": {
|
| 644 |
+
"bufferutil": "^4.0.1",
|
| 645 |
+
"utf-8-validate": ">=5.0.2"
|
| 646 |
+
},
|
| 647 |
+
"peerDependenciesMeta": {
|
| 648 |
+
"bufferutil": {
|
| 649 |
+
"optional": true
|
| 650 |
+
},
|
| 651 |
+
"utf-8-validate": {
|
| 652 |
+
"optional": true
|
| 653 |
+
}
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
+
}
|
| 657 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "wsb-bot",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "WSB — Wyvern Softworks Bot",
|
| 5 |
+
"main": "src/index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node src/index.js"
|
| 8 |
+
},
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"@octokit/rest": "^22.0.1",
|
| 11 |
+
"@supabase/supabase-js": "^2.99.3",
|
| 12 |
+
"discord.js": "^14.16.0",
|
| 13 |
+
"dotenv": "^16.4.0",
|
| 14 |
+
"node-fetch": "^2.7.0"
|
| 15 |
+
}
|
| 16 |
+
}
|
src/commands/applyUpdates.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PermissionFlagsBits, ChannelType } = require('discord.js');
|
| 2 |
+
const { stmts } = require('../database');
|
| 3 |
+
const { successEmbed, errorEmbed } = require('../utils/embeds');
|
| 4 |
+
const { Colors, Roles } = require('../config');
|
| 5 |
+
|
| 6 |
+
module.exports = {
|
| 7 |
+
async execute(client, message) {
|
| 8 |
+
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
| 9 |
+
await guild.roles.fetch();
|
| 10 |
+
await guild.channels.fetch();
|
| 11 |
+
|
| 12 |
+
const log = [];
|
| 13 |
+
|
| 14 |
+
// ── 1. Create missing roles ──────────────────────────────
|
| 15 |
+
const newRoles = ['@@ Server Manager', '@@ Moderator'];
|
| 16 |
+
for (const roleName of newRoles) {
|
| 17 |
+
const existing = guild.roles.cache.find(r => r.name === roleName);
|
| 18 |
+
if (existing) {
|
| 19 |
+
log.push(`⏭️ Role **${roleName}** already exists`);
|
| 20 |
+
continue;
|
| 21 |
+
}
|
| 22 |
+
const def = Roles.find(r => r.name === roleName);
|
| 23 |
+
if (!def) continue;
|
| 24 |
+
|
| 25 |
+
// Find position: place below Co-Owner, above Staff
|
| 26 |
+
const staffRole = guild.roles.cache.find(r => r.name === '@@ Staff');
|
| 27 |
+
const pos = staffRole ? staffRole.position + 1 : 1;
|
| 28 |
+
|
| 29 |
+
await guild.roles.create({
|
| 30 |
+
name: def.name,
|
| 31 |
+
color: def.color,
|
| 32 |
+
permissions: def.permissions,
|
| 33 |
+
hoist: def.hoist,
|
| 34 |
+
mentionable: def.mentionable,
|
| 35 |
+
position: pos,
|
| 36 |
+
});
|
| 37 |
+
log.push(`✅ Created role **${roleName}**`);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// ── 2. Create owner-chat in STAFF ONLY category ──────────
|
| 41 |
+
const staffCategory = guild.channels.cache.find(
|
| 42 |
+
c => c.type === ChannelType.GuildCategory && c.name.includes('STAFF ONLY')
|
| 43 |
+
);
|
| 44 |
+
const ownerChatExists = guild.channels.cache.find(c => c.name === '👑・owner-chat');
|
| 45 |
+
|
| 46 |
+
if (ownerChatExists) {
|
| 47 |
+
log.push(`⏭️ Channel **👑・owner-chat** already exists`);
|
| 48 |
+
} else if (!staffCategory) {
|
| 49 |
+
log.push(`⚠️ STAFF ONLY category not found — skipped owner-chat`);
|
| 50 |
+
} else {
|
| 51 |
+
// Fetch roles for permissions
|
| 52 |
+
const everyoneRole = guild.roles.everyone;
|
| 53 |
+
const ownerRole = guild.roles.cache.find(r => r.name === '@@ Owner');
|
| 54 |
+
const coOwnerRole = guild.roles.cache.find(r => r.name === '@@ Co-Owner');
|
| 55 |
+
const svrMgrRole = guild.roles.cache.find(r => r.name === '@@ Server Manager');
|
| 56 |
+
|
| 57 |
+
const overwrites = [
|
| 58 |
+
{ id: everyoneRole.id, deny: [PermissionFlagsBits.ViewChannel] },
|
| 59 |
+
];
|
| 60 |
+
if (ownerRole) overwrites.push({ id: ownerRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] });
|
| 61 |
+
if (coOwnerRole) overwrites.push({ id: coOwnerRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] });
|
| 62 |
+
if (svrMgrRole) overwrites.push({ id: svrMgrRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] });
|
| 63 |
+
|
| 64 |
+
// Get staff-chat to position owner-chat above it
|
| 65 |
+
const staffChat = guild.channels.cache.find(c => c.name === '🛡️・staff-chat');
|
| 66 |
+
|
| 67 |
+
const ownerChat = await guild.channels.create({
|
| 68 |
+
name: '👑・owner-chat',
|
| 69 |
+
type: ChannelType.GuildText,
|
| 70 |
+
parent: staffCategory.id,
|
| 71 |
+
permissionOverwrites: overwrites,
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
// Move above staff-chat
|
| 75 |
+
if (staffChat) {
|
| 76 |
+
await ownerChat.setPosition(staffChat.position);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
stmts.setState.run('channel_👑・owner-chat', ownerChat.id);
|
| 80 |
+
log.push(`✅ Created **👑・owner-chat** (above staff-chat)`);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// ── 3. Lock Resources channels to owner-only send ────────
|
| 84 |
+
const resourceChannels = ['🌐・resources', '🌐・free-assets', '🌐・scripts', '🌐・drivers'];
|
| 85 |
+
const everyoneRole = guild.roles.everyone;
|
| 86 |
+
const ownerRole = guild.roles.cache.find(r => r.name === '@@ Owner');
|
| 87 |
+
const verifiedRole = guild.roles.cache.find(r => r.name === '@@ Verified');
|
| 88 |
+
const staffRole = guild.roles.cache.find(r => r.name === '@@ Staff');
|
| 89 |
+
const boosterRole = guild.roles.cache.find(r => r.name === '@@ Booster');
|
| 90 |
+
|
| 91 |
+
for (const chName of resourceChannels) {
|
| 92 |
+
const ch = guild.channels.cache.find(c => c.name === chName);
|
| 93 |
+
if (!ch) {
|
| 94 |
+
log.push(`⚠️ Channel **${chName}** not found — skipped`);
|
| 95 |
+
continue;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
const overwrites = [
|
| 99 |
+
{ id: everyoneRole.id, deny: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
|
| 100 |
+
];
|
| 101 |
+
if (verifiedRole) overwrites.push({ id: verifiedRole.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 102 |
+
if (boosterRole) overwrites.push({ id: boosterRole.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 103 |
+
if (staffRole) overwrites.push({ id: staffRole.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 104 |
+
if (ownerRole) overwrites.push({ id: ownerRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 105 |
+
|
| 106 |
+
await ch.permissionOverwrites.set(overwrites);
|
| 107 |
+
log.push(`🔒 Locked **${chName}** — owner-only send`);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// ── 4. Lock booster-rewards to owner-only send ───────────
|
| 111 |
+
const boosterRewards = guild.channels.cache.find(c => c.name === '💎・booster-rewards');
|
| 112 |
+
if (boosterRewards) {
|
| 113 |
+
const overwrites = [
|
| 114 |
+
{ id: everyoneRole.id, deny: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
|
| 115 |
+
];
|
| 116 |
+
if (boosterRole) overwrites.push({ id: boosterRole.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 117 |
+
if (staffRole) overwrites.push({ id: staffRole.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 118 |
+
if (ownerRole) overwrites.push({ id: ownerRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] });
|
| 119 |
+
|
| 120 |
+
await boosterRewards.permissionOverwrites.set(overwrites);
|
| 121 |
+
log.push(`🔒 Locked **💎・booster-rewards** — owner-only send`);
|
| 122 |
+
} else {
|
| 123 |
+
log.push(`⚠️ Channel **💎・booster-rewards** not found — skipped`);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// ── Done ─────────────────────────────────────────────────
|
| 127 |
+
await message.reply({
|
| 128 |
+
embeds: [successEmbed('✅ Updates Applied', log.join('\n'))],
|
| 129 |
+
});
|
| 130 |
+
},
|
| 131 |
+
};
|
src/commands/backupLayout.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
|
| 4 |
+
module.exports = {
|
| 5 |
+
name: 'backup layout',
|
| 6 |
+
async execute(client, message) {
|
| 7 |
+
const guildId = process.env.GUILD_ID;
|
| 8 |
+
const guild = await client.guilds.fetch(guildId);
|
| 9 |
+
|
| 10 |
+
// Fetch all channels and roles
|
| 11 |
+
await guild.channels.fetch();
|
| 12 |
+
await guild.roles.fetch();
|
| 13 |
+
|
| 14 |
+
const backup = {
|
| 15 |
+
guild: {
|
| 16 |
+
name: guild.name,
|
| 17 |
+
id: guild.id,
|
| 18 |
+
backupDate: new Date().toISOString(),
|
| 19 |
+
},
|
| 20 |
+
roles: guild.roles.cache
|
| 21 |
+
.filter(r => r.name !== '@everyone')
|
| 22 |
+
.sort((a, b) => b.position - a.position)
|
| 23 |
+
.map(r => ({
|
| 24 |
+
name: r.name,
|
| 25 |
+
color: r.hexColor,
|
| 26 |
+
position: r.position,
|
| 27 |
+
hoist: r.hoist,
|
| 28 |
+
managed: r.managed,
|
| 29 |
+
permissions: r.permissions.toArray(),
|
| 30 |
+
})),
|
| 31 |
+
categories: guild.channels.cache
|
| 32 |
+
.filter(c => c.type === 4) // GuildCategory
|
| 33 |
+
.sort((a, b) => a.position - b.position)
|
| 34 |
+
.map(cat => ({
|
| 35 |
+
name: cat.name,
|
| 36 |
+
position: cat.position,
|
| 37 |
+
channels: guild.channels.cache
|
| 38 |
+
.filter(c => c.parentId === cat.id)
|
| 39 |
+
.sort((a, b) => a.position - b.position)
|
| 40 |
+
.map(ch => ({
|
| 41 |
+
name: ch.name,
|
| 42 |
+
type: ch.type === 2 ? 'voice' : 'text',
|
| 43 |
+
position: ch.position,
|
| 44 |
+
})),
|
| 45 |
+
})),
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
const json = JSON.stringify(backup, null, 2);
|
| 49 |
+
const buffer = Buffer.from(json, 'utf-8');
|
| 50 |
+
|
| 51 |
+
const embed = createEmbed({
|
| 52 |
+
title: '💾 Server Backup',
|
| 53 |
+
description: [
|
| 54 |
+
`**Guild:** ${guild.name}`,
|
| 55 |
+
`**Roles:** ${backup.roles.length}`,
|
| 56 |
+
`**Categories:** ${backup.categories.length}`,
|
| 57 |
+
`**Total Channels:** ${backup.categories.reduce((sum, c) => sum + c.channels.length, 0)}`,
|
| 58 |
+
].join('\n'),
|
| 59 |
+
color: Colors.SUCCESS,
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
await message.reply({
|
| 63 |
+
embeds: [embed],
|
| 64 |
+
files: [{ attachment: buffer, name: `backup-${guild.name.replace(/\s+/g, '-')}-${Date.now()}.json` }],
|
| 65 |
+
});
|
| 66 |
+
},
|
| 67 |
+
};
|
src/commands/clearDrops.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ChannelType } = require('discord.js');
|
| 2 |
+
const { successEmbed, errorEmbed, warnEmbed } = require('../utils/embeds');
|
| 3 |
+
|
| 4 |
+
module.exports = {
|
| 5 |
+
async execute(client, message, args) {
|
| 6 |
+
// Usage: `clear <channel_id>`
|
| 7 |
+
const channelIdStr = args[0]?.replace(/[<#>]/g, '');
|
| 8 |
+
if (!channelIdStr || !/^\d{17,20}$/.test(channelIdStr)) {
|
| 9 |
+
return message.reply({
|
| 10 |
+
embeds: [errorEmbed('Invalid Channel', 'Usage: `clear <channel_id>`\nExample: `clear 1234567890` or `clear #resources`')],
|
| 11 |
+
});
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
| 15 |
+
const channel = await guild.channels.fetch(channelIdStr).catch(() => null);
|
| 16 |
+
|
| 17 |
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
| 18 |
+
return message.reply({
|
| 19 |
+
embeds: [errorEmbed('Channel Not Found', 'Could not find a valid text channel with that ID.')],
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
await message.reply({
|
| 24 |
+
embeds: [warnEmbed('⏳ Clearing Drops', `Fetching and deleting messages posted by WSB in <#${channel.id}>...\nThis might take a moment.`)],
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
try {
|
| 28 |
+
let deletedCount = 0;
|
| 29 |
+
let lastId = null;
|
| 30 |
+
let fetched;
|
| 31 |
+
|
| 32 |
+
// Loop to fetch and delete messages in batches of 100
|
| 33 |
+
do {
|
| 34 |
+
const fetchOptions = { limit: 100 };
|
| 35 |
+
if (lastId) fetchOptions.before = lastId;
|
| 36 |
+
|
| 37 |
+
fetched = await channel.messages.fetch(fetchOptions);
|
| 38 |
+
if (fetched.size === 0) break;
|
| 39 |
+
|
| 40 |
+
// Filter for messages sent by the bot
|
| 41 |
+
const botMessages = fetched.filter(m => m.author.id === client.user.id);
|
| 42 |
+
|
| 43 |
+
if (botMessages.size > 0) {
|
| 44 |
+
// Bulk delete is faster for messages under 14 days old
|
| 45 |
+
// For safety and to handle old messages, we'll delete one by one if bulk fails
|
| 46 |
+
try {
|
| 47 |
+
await channel.bulkDelete(botMessages, true); // true = filter out 14+ day old ones
|
| 48 |
+
deletedCount += botMessages.size;
|
| 49 |
+
} catch (bulkErr) {
|
| 50 |
+
console.warn('[Clear] Bulk delete failed or partially failed, falling back to manual deletion');
|
| 51 |
+
// Fallback manual deletion for older messages
|
| 52 |
+
for (const msg of botMessages.values()) {
|
| 53 |
+
await msg.delete().catch(() => { });
|
| 54 |
+
deletedCount++;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
lastId = fetched.last().id;
|
| 60 |
+
} while (fetched.size === 100);
|
| 61 |
+
|
| 62 |
+
await message.reply({
|
| 63 |
+
embeds: [successEmbed('✅ Clear Complete', `Successfully deleted **${deletedCount}** WSB messages from <#${channel.id}>.`)],
|
| 64 |
+
});
|
| 65 |
+
} catch (err) {
|
| 66 |
+
console.error('[Clear Error]', err);
|
| 67 |
+
await message.reply({
|
| 68 |
+
embeds: [errorEmbed('❌ Clear Failed', `An error occurred while clearing messages: \`${err.message}\``)],
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
};
|
src/commands/coOwnerRole.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PermissionFlagsBits } = require('discord.js');
|
| 2 |
+
const { createEmbed } = require('../utils/embeds');
|
| 3 |
+
const { Colors } = require('../config');
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Temporary command: creates a Co-Owner role with limited management permissions.
|
| 7 |
+
* - Can manage server (channels, emojis, stickers, server settings)
|
| 8 |
+
* - Can manage roles (below their own)
|
| 9 |
+
* - Can manage nicknames
|
| 10 |
+
* - Can view audit log
|
| 11 |
+
* - Can manage messages (moderate)
|
| 12 |
+
* - Can mute/deafen/move members in VC
|
| 13 |
+
* - CANNOT ban or kick members
|
| 14 |
+
* - CANNOT send messages in most channels (no SendMessages)
|
| 15 |
+
* - CANNOT use Administrator
|
| 16 |
+
* - Position: directly below the Owner role
|
| 17 |
+
*
|
| 18 |
+
* After creation, adds a permission override to the announcements channel
|
| 19 |
+
* so Co-Owner CAN send messages there.
|
| 20 |
+
*
|
| 21 |
+
* Usage: coownerrole
|
| 22 |
+
*/
|
| 23 |
+
module.exports = {
|
| 24 |
+
async execute(client, message) {
|
| 25 |
+
const guild = client.guilds.cache.first();
|
| 26 |
+
if (!guild) return message.reply({ content: '❌ No guild found.' });
|
| 27 |
+
|
| 28 |
+
const statusMsg = await message.reply({
|
| 29 |
+
embeds: [createEmbed({
|
| 30 |
+
title: '🔧 Creating Co-Owner Role',
|
| 31 |
+
description: 'Setting up limited management role...',
|
| 32 |
+
color: Colors.INFO
|
| 33 |
+
})]
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
try {
|
| 37 |
+
// Check if role already exists
|
| 38 |
+
const existing = guild.roles.cache.find(r => r.name === '👑 Co-Owner');
|
| 39 |
+
if (existing) {
|
| 40 |
+
return statusMsg.edit({
|
| 41 |
+
embeds: [createEmbed({
|
| 42 |
+
title: '⚠️ Role Already Exists',
|
| 43 |
+
description: `The role ${existing} already exists. Delete it manually first if you want to recreate it.`,
|
| 44 |
+
color: Colors.WARNING
|
| 45 |
+
})]
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Find the Owner role to position Co-Owner below it
|
| 50 |
+
const ownerRole = guild.roles.cache.find(r => r.name === '@@ Owner');
|
| 51 |
+
const position = ownerRole ? ownerRole.position - 1 : guild.roles.cache.size - 2;
|
| 52 |
+
|
| 53 |
+
// Create the Co-Owner role with limited permissions
|
| 54 |
+
const coOwnerRole = await guild.roles.create({
|
| 55 |
+
name: '👑 Co-Owner',
|
| 56 |
+
color: '#e74c3c',
|
| 57 |
+
hoist: true,
|
| 58 |
+
mentionable: false,
|
| 59 |
+
permissions: [
|
| 60 |
+
// Server management
|
| 61 |
+
PermissionFlagsBits.ManageGuild, // Server settings, emojis, stickers
|
| 62 |
+
PermissionFlagsBits.ManageChannels, // Create/edit/delete channels
|
| 63 |
+
PermissionFlagsBits.ManageEmojisAndStickers, // Custom emojis & stickers
|
| 64 |
+
PermissionFlagsBits.ManageWebhooks, // Webhooks
|
| 65 |
+
|
| 66 |
+
// Moderation (no ban/kick)
|
| 67 |
+
PermissionFlagsBits.ManageMessages, // Delete/pin messages
|
| 68 |
+
PermissionFlagsBits.ManageNicknames, // Change other's nicknames
|
| 69 |
+
PermissionFlagsBits.ManageEvents, // Server events
|
| 70 |
+
|
| 71 |
+
// Voice
|
| 72 |
+
PermissionFlagsBits.MuteMembers, // Mute in VC
|
| 73 |
+
PermissionFlagsBits.DeafenMembers, // Deafen in VC
|
| 74 |
+
PermissionFlagsBits.MoveMembers, // Move between VCs
|
| 75 |
+
|
| 76 |
+
// Chat & view
|
| 77 |
+
PermissionFlagsBits.ViewChannel, // See channels
|
| 78 |
+
PermissionFlagsBits.SendMessages, // Send messages everywhere
|
| 79 |
+
PermissionFlagsBits.EmbedLinks, // Embed links
|
| 80 |
+
PermissionFlagsBits.AttachFiles, // Attach files
|
| 81 |
+
PermissionFlagsBits.AddReactions, // Add reactions
|
| 82 |
+
PermissionFlagsBits.UseExternalEmojis, // External emojis
|
| 83 |
+
PermissionFlagsBits.ViewAuditLog, // Audit log access
|
| 84 |
+
PermissionFlagsBits.ReadMessageHistory, // Read history
|
| 85 |
+
|
| 86 |
+
// Voice join
|
| 87 |
+
PermissionFlagsBits.Connect, // Join VCs
|
| 88 |
+
PermissionFlagsBits.Speak, // Speak in VCs
|
| 89 |
+
|
| 90 |
+
// NOTE: No ManageRoles, no BanMembers, no KickMembers, no Administrator
|
| 91 |
+
],
|
| 92 |
+
reason: 'Co-Owner role created via coownerrole command'
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
// Position the role below Owner
|
| 96 |
+
await coOwnerRole.setPosition(position).catch(() => { });
|
| 97 |
+
|
| 98 |
+
// Find announcements channel and add SendMessages override
|
| 99 |
+
const announcementChannel = guild.channels.cache.find(
|
| 100 |
+
c => c.name.toLowerCase().includes('announcement') || c.name.toLowerCase().includes('📢')
|
| 101 |
+
);
|
| 102 |
+
|
| 103 |
+
let announcementNote = '';
|
| 104 |
+
|
| 105 |
+
// Block Co-Owner from ALL ticket channels/categories
|
| 106 |
+
const ticketChannels = guild.channels.cache.filter(
|
| 107 |
+
c => c.name.toLowerCase().includes('ticket') ||
|
| 108 |
+
c.name.toLowerCase().includes('🎫')
|
| 109 |
+
);
|
| 110 |
+
|
| 111 |
+
let ticketCount = 0;
|
| 112 |
+
for (const [, ch] of ticketChannels) {
|
| 113 |
+
await ch.permissionOverwrites.edit(coOwnerRole, {
|
| 114 |
+
ViewChannel: false,
|
| 115 |
+
SendMessages: false,
|
| 116 |
+
}).catch(() => { });
|
| 117 |
+
ticketCount++;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
if (ticketCount > 0) {
|
| 121 |
+
announcementNote += `\n🔒 **Blocked from ${ticketCount} ticket channel(s)**`;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
await statusMsg.edit({
|
| 125 |
+
embeds: [createEmbed({
|
| 126 |
+
title: '✅ Co-Owner Role Created!',
|
| 127 |
+
description: [
|
| 128 |
+
`**Role:** ${coOwnerRole}`,
|
| 129 |
+
`**Position:** Below Owner`,
|
| 130 |
+
`**Color:** Red (#e74c3c)`,
|
| 131 |
+
'',
|
| 132 |
+
'**✅ CAN:**',
|
| 133 |
+
'> Send messages in all channels',
|
| 134 |
+
'> Manage server, channels, emojis',
|
| 135 |
+
'> Manage messages, nicknames, events',
|
| 136 |
+
'> Mute/deafen/move in voice',
|
| 137 |
+
'> View audit log',
|
| 138 |
+
'',
|
| 139 |
+
'**❌ CANNOT:**',
|
| 140 |
+
'> Assign/manage roles',
|
| 141 |
+
'> Ban or kick members',
|
| 142 |
+
'> Access ticket channels',
|
| 143 |
+
'> Use Administrator',
|
| 144 |
+
'> Override Owner permissions',
|
| 145 |
+
'',
|
| 146 |
+
announcementNote,
|
| 147 |
+
'',
|
| 148 |
+
'> Assign this role to your co-owner manually.',
|
| 149 |
+
].join('\n'),
|
| 150 |
+
color: Colors.SUCCESS
|
| 151 |
+
})]
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
} catch (err) {
|
| 155 |
+
console.error('[CoOwnerRole Error]', err);
|
| 156 |
+
await statusMsg.edit({
|
| 157 |
+
embeds: [createEmbed({
|
| 158 |
+
title: '❌ Failed',
|
| 159 |
+
description: `Error: ${err.message}`,
|
| 160 |
+
color: Colors.ACCENT
|
| 161 |
+
})]
|
| 162 |
+
});
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
};
|
src/commands/createChannels.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ChannelType, PermissionFlagsBits } = require('discord.js');
|
| 2 |
+
const { Categories } = require('../config');
|
| 3 |
+
const { stmts } = require('../database');
|
| 4 |
+
const { successEmbed, infoEmbed } = require('../utils/embeds');
|
| 5 |
+
|
| 6 |
+
module.exports = {
|
| 7 |
+
name: 'create channels',
|
| 8 |
+
async execute(client, message) {
|
| 9 |
+
const guildId = process.env.GUILD_ID;
|
| 10 |
+
const guild = await client.guilds.fetch(guildId);
|
| 11 |
+
|
| 12 |
+
await message.reply({ embeds: [infoEmbed('Creating Channels', 'Setting up categories and channels...')] });
|
| 13 |
+
|
| 14 |
+
const results = [];
|
| 15 |
+
|
| 16 |
+
// Build a role map for permission overwrites
|
| 17 |
+
const roleMap = new Map();
|
| 18 |
+
roleMap.set('everyone', guild.roles.everyone);
|
| 19 |
+
guild.roles.cache.forEach(r => roleMap.set(r.name, r));
|
| 20 |
+
|
| 21 |
+
for (const catDef of Categories) {
|
| 22 |
+
// Check if category already exists
|
| 23 |
+
let category = guild.channels.cache.find(
|
| 24 |
+
c => c.type === ChannelType.GuildCategory && c.name === catDef.name
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
if (!category) {
|
| 28 |
+
// Build category-level permission overwrites
|
| 29 |
+
const catOverwrites = buildCategoryOverwrites(catDef, roleMap);
|
| 30 |
+
|
| 31 |
+
category = await guild.channels.create({
|
| 32 |
+
name: catDef.name,
|
| 33 |
+
type: ChannelType.GuildCategory,
|
| 34 |
+
permissionOverwrites: catOverwrites,
|
| 35 |
+
reason: 'WSB Setup',
|
| 36 |
+
});
|
| 37 |
+
results.push(`📁 **${catDef.name}** — category created`);
|
| 38 |
+
} else {
|
| 39 |
+
results.push(`📁 **${catDef.name}** — already exists`);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Create child channels
|
| 43 |
+
for (const chDef of catDef.channels) {
|
| 44 |
+
const existing = guild.channels.cache.find(
|
| 45 |
+
c => c.name === chDef.name && c.parentId === category.id
|
| 46 |
+
);
|
| 47 |
+
if (existing) {
|
| 48 |
+
// Store channel ID in bot state for later reference
|
| 49 |
+
stmts.setState.run(`channel_${chDef.name}`, existing.id);
|
| 50 |
+
results.push(` └ **${chDef.name}** — already exists`);
|
| 51 |
+
continue;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const channelType = chDef.type === 'voice'
|
| 55 |
+
? ChannelType.GuildVoice
|
| 56 |
+
: ChannelType.GuildText;
|
| 57 |
+
|
| 58 |
+
// Build channel-specific permission overwrites
|
| 59 |
+
const channelOverwrites = buildChannelOverwrites(catDef, chDef, roleMap);
|
| 60 |
+
|
| 61 |
+
const channel = await guild.channels.create({
|
| 62 |
+
name: chDef.name,
|
| 63 |
+
type: channelType,
|
| 64 |
+
parent: category.id,
|
| 65 |
+
permissionOverwrites: channelOverwrites,
|
| 66 |
+
reason: 'WSB Setup',
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
// Store in bot state
|
| 70 |
+
stmts.setState.run(`channel_${chDef.name}`, channel.id);
|
| 71 |
+
|
| 72 |
+
results.push(` └ **${chDef.name}** — created`);
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
await message.reply({
|
| 77 |
+
embeds: [successEmbed('Channels Created', results.join('\n'))],
|
| 78 |
+
});
|
| 79 |
+
},
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Build permission overwrites for a category.
|
| 84 |
+
*/
|
| 85 |
+
function buildCategoryOverwrites(catDef, roleMap) {
|
| 86 |
+
const overrideMap = catDef.permOverrides(roleMap);
|
| 87 |
+
const overwrites = [];
|
| 88 |
+
|
| 89 |
+
for (const [roleName, perms] of Object.entries(overrideMap)) {
|
| 90 |
+
const role = roleName === 'everyone' ? roleMap.get('everyone') : roleMap.get(roleName);
|
| 91 |
+
if (!role) continue;
|
| 92 |
+
|
| 93 |
+
const allow = [];
|
| 94 |
+
const deny = [];
|
| 95 |
+
|
| 96 |
+
if (perms.view === true) allow.push(PermissionFlagsBits.ViewChannel);
|
| 97 |
+
if (perms.view === false) deny.push(PermissionFlagsBits.ViewChannel);
|
| 98 |
+
if (perms.send === true) allow.push(PermissionFlagsBits.SendMessages);
|
| 99 |
+
if (perms.send === false) deny.push(PermissionFlagsBits.SendMessages);
|
| 100 |
+
|
| 101 |
+
overwrites.push({ id: role.id || role, allow, deny });
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
return overwrites;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Build channel-specific overwrites, handling readOnly, staffOnly, noEmbeds, and special flags.
|
| 109 |
+
*/
|
| 110 |
+
function buildChannelOverwrites(catDef, chDef, roleMap) {
|
| 111 |
+
const overrideMap = catDef.permOverrides(roleMap);
|
| 112 |
+
const overwrites = [];
|
| 113 |
+
|
| 114 |
+
for (const [roleName, perms] of Object.entries(overrideMap)) {
|
| 115 |
+
const role = roleName === 'everyone' ? roleMap.get('everyone') : roleMap.get(roleName);
|
| 116 |
+
if (!role) continue;
|
| 117 |
+
|
| 118 |
+
const allow = [];
|
| 119 |
+
const deny = [];
|
| 120 |
+
|
| 121 |
+
// Public channels: @everyone can view (overrides category deny)
|
| 122 |
+
if (chDef.public && roleName === 'everyone') {
|
| 123 |
+
allow.push(PermissionFlagsBits.ViewChannel);
|
| 124 |
+
allow.push(PermissionFlagsBits.ReadMessageHistory);
|
| 125 |
+
allow.push(PermissionFlagsBits.AddReactions);
|
| 126 |
+
} else {
|
| 127 |
+
if (perms.view === true) allow.push(PermissionFlagsBits.ViewChannel);
|
| 128 |
+
if (perms.view === false) deny.push(PermissionFlagsBits.ViewChannel);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// hideFromVerified: deny view for @@ Verified (verify channel after verification)
|
| 132 |
+
if (chDef.hideFromVerified && roleName === '@@ Verified') {
|
| 133 |
+
// Override: verified users cannot see this channel
|
| 134 |
+
const viewIdx = allow.indexOf(PermissionFlagsBits.ViewChannel);
|
| 135 |
+
if (viewIdx !== -1) allow.splice(viewIdx, 1);
|
| 136 |
+
deny.push(PermissionFlagsBits.ViewChannel);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
if (chDef.ownerOnly) {
|
| 140 |
+
// Only Owner and Co-Owner can send
|
| 141 |
+
if (['@@ Owner', '@@ Co-Owner'].includes(roleName)) {
|
| 142 |
+
allow.push(PermissionFlagsBits.SendMessages);
|
| 143 |
+
} else {
|
| 144 |
+
deny.push(PermissionFlagsBits.SendMessages);
|
| 145 |
+
}
|
| 146 |
+
} else if (chDef.readOnly) {
|
| 147 |
+
// Only Staff and Owner can send in read-only channels
|
| 148 |
+
if (['@@ Staff', '@@ Server Manager', '@@ Owner', '@@ Co-Owner'].includes(roleName)) {
|
| 149 |
+
if (perms.send === true) allow.push(PermissionFlagsBits.SendMessages);
|
| 150 |
+
} else {
|
| 151 |
+
deny.push(PermissionFlagsBits.SendMessages);
|
| 152 |
+
}
|
| 153 |
+
} else if (chDef.staffOnly) {
|
| 154 |
+
// Only Staff and Owner can see/send
|
| 155 |
+
if (['@@ Staff', '@@ Server Manager', '@@ Owner', '@@ Co-Owner'].includes(roleName)) {
|
| 156 |
+
allow.push(PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages);
|
| 157 |
+
} else {
|
| 158 |
+
deny.push(PermissionFlagsBits.ViewChannel);
|
| 159 |
+
}
|
| 160 |
+
} else if (chDef.special === 'owner-chat') {
|
| 161 |
+
// Only Owner, Co-Owner, and Server Manager can see/send
|
| 162 |
+
if (['@@ Owner', '@@ Co-Owner', '@@ Server Manager'].includes(roleName)) {
|
| 163 |
+
allow.push(PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages);
|
| 164 |
+
} else {
|
| 165 |
+
deny.push(PermissionFlagsBits.ViewChannel);
|
| 166 |
+
}
|
| 167 |
+
} else if (chDef.special === 'community-content') {
|
| 168 |
+
// Known + Helper + Staff + Owner can send; everyone else view-only
|
| 169 |
+
if (['@@ Known', '@@ Helper', '@@ Staff', '@@ Owner', '@@ Co-Owner'].includes(roleName)) {
|
| 170 |
+
allow.push(PermissionFlagsBits.SendMessages);
|
| 171 |
+
} else {
|
| 172 |
+
if (perms.view === true) {
|
| 173 |
+
// Can view but not send
|
| 174 |
+
deny.push(PermissionFlagsBits.SendMessages);
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
} else {
|
| 178 |
+
if (perms.send === true) allow.push(PermissionFlagsBits.SendMessages);
|
| 179 |
+
if (perms.send === false) deny.push(PermissionFlagsBits.SendMessages);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// noEmbeds: block attachments, embeds, and external emojis (text-only channel)
|
| 183 |
+
if (chDef.noEmbeds && !['@@ Staff', '@@ Server Manager', '@@ Owner', '@@ Co-Owner'].includes(roleName)) {
|
| 184 |
+
deny.push(PermissionFlagsBits.AttachFiles);
|
| 185 |
+
deny.push(PermissionFlagsBits.EmbedLinks);
|
| 186 |
+
deny.push(PermissionFlagsBits.UseExternalEmojis);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Add reactions for verify and ticket channels
|
| 190 |
+
if (chDef.special === 'verify' || chDef.special === 'ticket') {
|
| 191 |
+
allow.push(PermissionFlagsBits.AddReactions);
|
| 192 |
+
allow.push(PermissionFlagsBits.ReadMessageHistory);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
overwrites.push({ id: role.id || role, allow, deny });
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// For community-content, add Known and Helper overwrites if they aren't in the category overrides
|
| 199 |
+
if (chDef.special === 'community-content') {
|
| 200 |
+
for (const extraRole of ['@@ Known', '@@ Helper']) {
|
| 201 |
+
if (!overrideMap[extraRole]) {
|
| 202 |
+
const role = roleMap.get(extraRole);
|
| 203 |
+
if (role) {
|
| 204 |
+
overwrites.push({
|
| 205 |
+
id: role.id,
|
| 206 |
+
allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages],
|
| 207 |
+
deny: [],
|
| 208 |
+
});
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
return overwrites;
|
| 215 |
+
}
|
src/commands/createRoles.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PermissionsBitField } = require('discord.js');
|
| 2 |
+
const { Roles } = require('../config');
|
| 3 |
+
const { successEmbed, infoEmbed, errorEmbed } = require('../utils/embeds');
|
| 4 |
+
|
| 5 |
+
module.exports = {
|
| 6 |
+
name: 'create roles',
|
| 7 |
+
async execute(client, message) {
|
| 8 |
+
const guildId = process.env.GUILD_ID;
|
| 9 |
+
const guild = await client.guilds.fetch(guildId);
|
| 10 |
+
|
| 11 |
+
await message.reply({ embeds: [infoEmbed('Creating Roles', 'Setting up role hierarchy...')] });
|
| 12 |
+
|
| 13 |
+
const createdRoles = [];
|
| 14 |
+
|
| 15 |
+
// Create roles in reverse order so the first role ends up highest
|
| 16 |
+
for (let i = Roles.length - 1; i >= 0; i--) {
|
| 17 |
+
const roleDef = Roles[i];
|
| 18 |
+
|
| 19 |
+
// Check if role already exists
|
| 20 |
+
const existing = guild.roles.cache.find(r => r.name === roleDef.name);
|
| 21 |
+
if (existing) {
|
| 22 |
+
createdRoles.unshift(`⚠️ **${roleDef.name}** — already exists`);
|
| 23 |
+
continue;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Build permissions
|
| 27 |
+
const permissions = new PermissionsBitField();
|
| 28 |
+
for (const perm of roleDef.permissions) {
|
| 29 |
+
permissions.add(perm);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const role = await guild.roles.create({
|
| 34 |
+
name: roleDef.name,
|
| 35 |
+
color: parseInt(roleDef.color.replace('#', ''), 16),
|
| 36 |
+
permissions,
|
| 37 |
+
hoist: roleDef.hoist,
|
| 38 |
+
mentionable: roleDef.mentionable,
|
| 39 |
+
reason: 'WSB Setup',
|
| 40 |
+
});
|
| 41 |
+
createdRoles.unshift(`✅ **${role.name}** — created`);
|
| 42 |
+
} catch (err) {
|
| 43 |
+
createdRoles.unshift(`❌ **${roleDef.name}** — failed: ${err.message}`);
|
| 44 |
+
console.error(`[CreateRoles] Failed to create ${roleDef.name}:`, err.message);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Reorder roles to match hierarchy (Owner highest)
|
| 49 |
+
const roleOrder = [];
|
| 50 |
+
for (const roleDef of Roles) {
|
| 51 |
+
const role = guild.roles.cache.find(r => r.name === roleDef.name);
|
| 52 |
+
if (role) roleOrder.push(role);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Set positions (higher index = higher position)
|
| 56 |
+
const botRole = guild.members.me.roles.highest;
|
| 57 |
+
let position = botRole.position - 1;
|
| 58 |
+
|
| 59 |
+
for (const role of roleOrder) {
|
| 60 |
+
try {
|
| 61 |
+
await role.setPosition(Math.max(position, 1));
|
| 62 |
+
position--;
|
| 63 |
+
} catch (err) {
|
| 64 |
+
// May fail if bot role isn't high enough
|
| 65 |
+
console.warn(`Could not reposition ${role.name}: ${err.message}`);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
await message.reply({
|
| 70 |
+
embeds: [successEmbed('Roles Created', createdRoles.join('\n'))],
|
| 71 |
+
});
|
| 72 |
+
},
|
| 73 |
+
};
|
src/commands/deleteDrop.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
const { stmts } = require('../database');
|
| 4 |
+
const fetch = require('node-fetch');
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* usage: deletedrop <id>
|
| 8 |
+
*/
|
| 9 |
+
module.exports = {
|
| 10 |
+
async execute(client, message, args) {
|
| 11 |
+
if (args.length < 1) {
|
| 12 |
+
return message.reply({ content: '❌ Usage: `deletedrop <id>`' });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const id = parseInt(args[0]);
|
| 16 |
+
if (isNaN(id)) return message.reply({ content: '❌ Invalid Drop ID. Must be a number.' });
|
| 17 |
+
|
| 18 |
+
const drop = await stmts.getWebDrop(id);
|
| 19 |
+
if (!drop) {
|
| 20 |
+
return message.reply({ content: `❌ No drop found in database with ID: **${id}**` });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
try {
|
| 24 |
+
// 1. Delete from Supabase
|
| 25 |
+
await stmts.deleteWebDrop(id);
|
| 26 |
+
|
| 27 |
+
// 2. Send DELETE request to Website Backend API
|
| 28 |
+
const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
// Mock request for now
|
| 32 |
+
await fetch(`${WEBSITE_API}/${id}`, {
|
| 33 |
+
method: 'DELETE',
|
| 34 |
+
}).catch(() => {});
|
| 35 |
+
} catch (e) {}
|
| 36 |
+
|
| 37 |
+
await message.reply({
|
| 38 |
+
embeds: [createEmbed({
|
| 39 |
+
title: '🗑️ Drop Deleted',
|
| 40 |
+
description: `Successfully deleted Drop **#${id}** (${drop.title}) from the website backend.`,
|
| 41 |
+
color: Colors.SUCCESS
|
| 42 |
+
})]
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
} catch (err) {
|
| 46 |
+
console.error('[Delete Drop Error]', err);
|
| 47 |
+
await message.reply({ content: `❌ Error deleting drop: ${err.message}` });
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
};
|
src/commands/editDrop.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { Octokit } = require('@octokit/rest');
|
| 2 |
+
const { createEmbed } = require('../utils/embeds');
|
| 3 |
+
const { Colors } = require('../config');
|
| 4 |
+
const { stmts } = require('../database');
|
| 5 |
+
const fetch = require('node-fetch');
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* usage: editdrop <id> <property> <new_value>
|
| 9 |
+
* properties: title, description, status
|
| 10 |
+
*/
|
| 11 |
+
module.exports = {
|
| 12 |
+
async execute(client, message, args) {
|
| 13 |
+
if (args.length < 3) {
|
| 14 |
+
return message.reply({ content: '❌ Usage: `editdrop <id> <title|description|status> <new value>`' });
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const id = parseInt(args[0]);
|
| 18 |
+
const property = args[1].toLowerCase();
|
| 19 |
+
const newValue = args.slice(2).join(' ');
|
| 20 |
+
|
| 21 |
+
if (isNaN(id)) return message.reply({ content: '❌ Invalid Drop ID. Must be a number.' });
|
| 22 |
+
|
| 23 |
+
const drop = await stmts.getWebDrop(id);
|
| 24 |
+
if (!drop) {
|
| 25 |
+
return message.reply({ content: `❌ No drop found in database with ID: **${id}**` });
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const validProps = ['title', 'description', 'status'];
|
| 29 |
+
if (!validProps.includes(property)) {
|
| 30 |
+
return message.reply({ content: `❌ Invalid property. Use: ${validProps.join(', ')}` });
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
try {
|
| 34 |
+
// 1. Update Supabase
|
| 35 |
+
const { db } = require('../database');
|
| 36 |
+
await db.from('web_drops').update({ [property]: newValue }).eq('id', id);
|
| 37 |
+
|
| 38 |
+
// 2. Send update request to Website Backend API
|
| 39 |
+
const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
// Mock request for now
|
| 43 |
+
await fetch(`${WEBSITE_API}/${id}`, {
|
| 44 |
+
method: 'PATCH',
|
| 45 |
+
headers: { 'Content-Type': 'application/json' },
|
| 46 |
+
body: JSON.stringify({ [property]: newValue })
|
| 47 |
+
}).catch(() => {});
|
| 48 |
+
} catch (e) {}
|
| 49 |
+
|
| 50 |
+
await message.reply({
|
| 51 |
+
embeds: [createEmbed({
|
| 52 |
+
title: '✅ Drop Updated',
|
| 53 |
+
description: `Successfully updated Drop **#${id}**.\n\n**${property}** is now:\n> ${newValue}`,
|
| 54 |
+
color: Colors.SUCCESS
|
| 55 |
+
})]
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
} catch (err) {
|
| 59 |
+
console.error('[Edit Drop Error]', err);
|
| 60 |
+
await message.reply({ content: `❌ Error updating drop: ${err.message}` });
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
};
|
src/commands/fixDownloads.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
| 2 |
+
const { Octokit } = require('@octokit/rest');
|
| 3 |
+
const { createEmbed } = require('../utils/embeds');
|
| 4 |
+
const { Colors } = require('../config');
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Scans a channel for old drop embeds with broken GitHub Link buttons
|
| 8 |
+
* and replaces them with new interactive download buttons (dl_<assetId>).
|
| 9 |
+
*
|
| 10 |
+
* Usage: fix downloads <channel_id>
|
| 11 |
+
*/
|
| 12 |
+
module.exports = {
|
| 13 |
+
async execute(client, message, args) {
|
| 14 |
+
const channelId = args[0]?.replace(/[<#>]/g, '');
|
| 15 |
+
if (!channelId) {
|
| 16 |
+
return message.reply({ content: '❌ Usage: `fix downloads <channel_id>`' });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const guild = client.guilds.cache.first();
|
| 20 |
+
const channel = await guild.channels.fetch(channelId).catch(() => null);
|
| 21 |
+
if (!channel) {
|
| 22 |
+
return message.reply({ content: '❌ Channel not found.' });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const statusMsg = await message.reply({
|
| 26 |
+
embeds: [createEmbed({
|
| 27 |
+
title: '🔧 Fixing Downloads',
|
| 28 |
+
description: `Scanning <#${channelId}> for broken GitHub links...`,
|
| 29 |
+
color: Colors.INFO
|
| 30 |
+
})]
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
try {
|
| 34 |
+
const octokit = new Octokit({ auth: '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' });
|
| 35 |
+
const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
|
| 36 |
+
|
| 37 |
+
// 1. Build a lookup map of all release assets: { "tag/filename" -> assetId }
|
| 38 |
+
const assetMap = new Map();
|
| 39 |
+
let page = 1;
|
| 40 |
+
let hasMore = true;
|
| 41 |
+
|
| 42 |
+
while (hasMore) {
|
| 43 |
+
const { data: releases } = await octokit.rest.repos.listReleases({
|
| 44 |
+
owner,
|
| 45 |
+
repo,
|
| 46 |
+
per_page: 100,
|
| 47 |
+
page
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
if (releases.length === 0) {
|
| 51 |
+
hasMore = false;
|
| 52 |
+
break;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
for (const release of releases) {
|
| 56 |
+
for (const asset of release.assets) {
|
| 57 |
+
// Map by tag_name/filename for lookup
|
| 58 |
+
assetMap.set(`${release.tag_name}/${asset.name}`, asset.id);
|
| 59 |
+
// Also map by just the browser_download_url for direct matching
|
| 60 |
+
assetMap.set(asset.browser_download_url, asset.id);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
page++;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
await statusMsg.edit({
|
| 68 |
+
embeds: [createEmbed({
|
| 69 |
+
title: '🔧 Fixing Downloads',
|
| 70 |
+
description: `Found **${assetMap.size}** GitHub assets. Now scanning messages in <#${channelId}>...`,
|
| 71 |
+
color: Colors.INFO
|
| 72 |
+
})]
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
// 2. Fetch all messages in the channel and find ones with GitHub Link buttons
|
| 76 |
+
let fixedCount = 0;
|
| 77 |
+
let skippedCount = 0;
|
| 78 |
+
let lastId = null;
|
| 79 |
+
let scannedCount = 0;
|
| 80 |
+
|
| 81 |
+
while (true) {
|
| 82 |
+
const options = { limit: 100 };
|
| 83 |
+
if (lastId) options.before = lastId;
|
| 84 |
+
|
| 85 |
+
const messages = await channel.messages.fetch(options);
|
| 86 |
+
if (messages.size === 0) break;
|
| 87 |
+
|
| 88 |
+
for (const [, msg] of messages) {
|
| 89 |
+
scannedCount++;
|
| 90 |
+
|
| 91 |
+
// Only process bot messages with components
|
| 92 |
+
if (msg.author.id !== client.user.id) continue;
|
| 93 |
+
if (!msg.components || msg.components.length === 0) continue;
|
| 94 |
+
|
| 95 |
+
// Check if any component row has a Link button pointing to WSB-Storage
|
| 96 |
+
let needsFix = false;
|
| 97 |
+
const newRows = [];
|
| 98 |
+
|
| 99 |
+
for (const row of msg.components) {
|
| 100 |
+
const newComponents = [];
|
| 101 |
+
|
| 102 |
+
for (const component of row.components) {
|
| 103 |
+
// Check if it's a Link button pointing to our GitHub repo
|
| 104 |
+
if (component.type === 2 && component.style === ButtonStyle.Link &&
|
| 105 |
+
component.url && component.url.includes('APRK01/WSB-Storage')) {
|
| 106 |
+
|
| 107 |
+
// Try to find the asset ID from the URL
|
| 108 |
+
const assetId = assetMap.get(component.url);
|
| 109 |
+
|
| 110 |
+
if (assetId) {
|
| 111 |
+
// Replace with interactive button
|
| 112 |
+
newComponents.push(
|
| 113 |
+
new ButtonBuilder()
|
| 114 |
+
.setCustomId(`dl_${assetId}`)
|
| 115 |
+
.setLabel(component.label || '📥 Download Drop')
|
| 116 |
+
.setStyle(ButtonStyle.Success)
|
| 117 |
+
);
|
| 118 |
+
needsFix = true;
|
| 119 |
+
} else {
|
| 120 |
+
// Can't find asset ID — try parsing from URL
|
| 121 |
+
// URL format: https://github.com/APRK01/WSB-Storage/releases/download/TAG/FILENAME
|
| 122 |
+
const urlMatch = component.url.match(/\/releases\/download\/([^/]+)\/(.+)$/);
|
| 123 |
+
if (urlMatch) {
|
| 124 |
+
const lookupKey = `${decodeURIComponent(urlMatch[1])}/${decodeURIComponent(urlMatch[2])}`;
|
| 125 |
+
const foundId = assetMap.get(lookupKey);
|
| 126 |
+
|
| 127 |
+
if (foundId) {
|
| 128 |
+
newComponents.push(
|
| 129 |
+
new ButtonBuilder()
|
| 130 |
+
.setCustomId(`dl_${foundId}`)
|
| 131 |
+
.setLabel(component.label || '📥 Download Drop')
|
| 132 |
+
.setStyle(ButtonStyle.Success)
|
| 133 |
+
);
|
| 134 |
+
needsFix = true;
|
| 135 |
+
} else {
|
| 136 |
+
// Keep original if we can't resolve
|
| 137 |
+
newComponents.push(ButtonBuilder.from(component));
|
| 138 |
+
skippedCount++;
|
| 139 |
+
}
|
| 140 |
+
} else {
|
| 141 |
+
newComponents.push(ButtonBuilder.from(component));
|
| 142 |
+
skippedCount++;
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
} else if (component.type === 2 && component.style !== ButtonStyle.Link) {
|
| 146 |
+
// Already an interactive button (already fixed or dl_ button)
|
| 147 |
+
newComponents.push(ButtonBuilder.from(component));
|
| 148 |
+
} else {
|
| 149 |
+
newComponents.push(ButtonBuilder.from(component));
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
newRows.push(new ActionRowBuilder().addComponents(newComponents));
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if (needsFix) {
|
| 157 |
+
try {
|
| 158 |
+
await msg.edit({ components: newRows });
|
| 159 |
+
fixedCount++;
|
| 160 |
+
} catch (editErr) {
|
| 161 |
+
console.error(`[Fix Downloads] Failed to edit message ${msg.id}:`, editErr.message);
|
| 162 |
+
skippedCount++;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
lastId = messages.last().id;
|
| 168 |
+
|
| 169 |
+
// Update status periodically
|
| 170 |
+
if (scannedCount % 200 === 0) {
|
| 171 |
+
await statusMsg.edit({
|
| 172 |
+
embeds: [createEmbed({
|
| 173 |
+
title: '🔧 Fixing Downloads',
|
| 174 |
+
description: `Scanned **${scannedCount}** messages... Fixed **${fixedCount}** so far.`,
|
| 175 |
+
color: Colors.INFO
|
| 176 |
+
})]
|
| 177 |
+
}).catch(() => { });
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
await statusMsg.edit({
|
| 182 |
+
embeds: [createEmbed({
|
| 183 |
+
title: '✅ Downloads Fixed!',
|
| 184 |
+
description: [
|
| 185 |
+
`**Channel:** <#${channelId}>`,
|
| 186 |
+
`**Messages scanned:** ${scannedCount}`,
|
| 187 |
+
`**Buttons fixed:** ${fixedCount}`,
|
| 188 |
+
skippedCount > 0 ? `**Skipped (no match):** ${skippedCount}` : '',
|
| 189 |
+
'',
|
| 190 |
+
fixedCount > 0
|
| 191 |
+
? '> All download buttons now use the private ephemeral system. 🔒'
|
| 192 |
+
: '> No broken buttons found — everything looks good!',
|
| 193 |
+
].filter(Boolean).join('\n'),
|
| 194 |
+
color: Colors.SUCCESS
|
| 195 |
+
})]
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
} catch (err) {
|
| 199 |
+
console.error('[Fix Downloads Error]', err);
|
| 200 |
+
await statusMsg.edit({
|
| 201 |
+
embeds: [createEmbed({
|
| 202 |
+
title: '❌ Fix Failed',
|
| 203 |
+
description: `Error: ${err.message}`,
|
| 204 |
+
color: Colors.ACCENT
|
| 205 |
+
})]
|
| 206 |
+
});
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
};
|
src/commands/fixPings.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PermissionFlagsBits } = require('discord.js');
|
| 2 |
+
const { successEmbed } = require('../utils/embeds');
|
| 3 |
+
|
| 4 |
+
module.exports = {
|
| 5 |
+
async execute(client, message) {
|
| 6 |
+
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
| 7 |
+
await guild.roles.fetch();
|
| 8 |
+
|
| 9 |
+
const log = [];
|
| 10 |
+
|
| 11 |
+
// 1. Fix @everyone role (remove MentionEveryone)
|
| 12 |
+
const everyoneRole = guild.roles.everyone;
|
| 13 |
+
if (everyoneRole.permissions.has(PermissionFlagsBits.MentionEveryone)) {
|
| 14 |
+
const newPerms = everyoneRole.permissions.remove(PermissionFlagsBits.MentionEveryone);
|
| 15 |
+
await everyoneRole.setPermissions(newPerms);
|
| 16 |
+
log.push(`✅ Removed MentionEveryone permission from **@everyone**`);
|
| 17 |
+
} else {
|
| 18 |
+
log.push(`⏭️ **@everyone** already cannot mention everyone/here`);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// 2. Make large/public roles unmentionable
|
| 22 |
+
const unmentionableRoles = [
|
| 23 |
+
'@@ Server Manager', '@@ Staff', '@@ Moderator',
|
| 24 |
+
'@@ Verified', '@@ Buyer', '@@ Booster', '@@ Known', '@@ Helper'
|
| 25 |
+
];
|
| 26 |
+
for (const rName of unmentionableRoles) {
|
| 27 |
+
const role = guild.roles.cache.find(r => r.name === rName);
|
| 28 |
+
if (role && role.mentionable) {
|
| 29 |
+
await role.setMentionable(false);
|
| 30 |
+
log.push(`✅ Made **${rName}** unmentionable`);
|
| 31 |
+
} else if (role) {
|
| 32 |
+
log.push(`⏭️ **${rName}** is already unmentionable`);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
await message.reply({ embeds: [successEmbed('✅ Ping Exploit Fixed', log.join('\n'))] });
|
| 37 |
+
},
|
| 38 |
+
};
|
src/commands/permissionAudit.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ChannelType, PermissionFlagsBits } = require('discord.js');
|
| 2 |
+
const { createEmbed } = require('../utils/embeds');
|
| 3 |
+
const { Colors } = require('../config');
|
| 4 |
+
|
| 5 |
+
module.exports = {
|
| 6 |
+
name: 'permission audit',
|
| 7 |
+
async execute(client, message) {
|
| 8 |
+
const guildId = process.env.GUILD_ID;
|
| 9 |
+
const guild = await client.guilds.fetch(guildId);
|
| 10 |
+
await guild.channels.fetch();
|
| 11 |
+
|
| 12 |
+
const issues = [];
|
| 13 |
+
const report = [];
|
| 14 |
+
|
| 15 |
+
const categories = guild.channels.cache
|
| 16 |
+
.filter(c => c.type === ChannelType.GuildCategory)
|
| 17 |
+
.sort((a, b) => a.position - b.position);
|
| 18 |
+
|
| 19 |
+
for (const [, category] of categories) {
|
| 20 |
+
const catReport = [`📁 **${category.name}**`];
|
| 21 |
+
const children = guild.channels.cache
|
| 22 |
+
.filter(c => c.parentId === category.id)
|
| 23 |
+
.sort((a, b) => a.position - b.position);
|
| 24 |
+
|
| 25 |
+
for (const [, channel] of children) {
|
| 26 |
+
const everyone = channel.permissionOverwrites.cache.get(guild.roles.everyone.id);
|
| 27 |
+
const everyoneView = everyone?.deny?.has(PermissionFlagsBits.ViewChannel) ? '🔒' : '👁️';
|
| 28 |
+
const everyoneSend = everyone?.deny?.has(PermissionFlagsBits.SendMessages) ? '🔇' : '💬';
|
| 29 |
+
|
| 30 |
+
catReport.push(` └ ${channel.name} — ${everyoneView} ${everyoneSend}`);
|
| 31 |
+
|
| 32 |
+
// Check for potential issues
|
| 33 |
+
if (!everyone) {
|
| 34 |
+
issues.push(`⚠️ ${channel.name} — no @everyone override set`);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
report.push(catReport.join('\n'));
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Legend
|
| 42 |
+
const legend = '```\n👁️ = @everyone can view\n🔒 = @everyone cannot view\n💬 = @everyone can send\n🔇 = @everyone cannot send\n```';
|
| 43 |
+
|
| 44 |
+
const embed = createEmbed({
|
| 45 |
+
title: '🛡️ Permission Audit',
|
| 46 |
+
description: legend + '\n\n' + report.join('\n\n'),
|
| 47 |
+
color: Colors.INFO,
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
const parts = [];
|
| 51 |
+
const embedStr = legend + '\n\n' + report.join('\n\n');
|
| 52 |
+
|
| 53 |
+
// Discord embed limit is 4096 characters
|
| 54 |
+
if (embedStr.length > 4000) {
|
| 55 |
+
// Send as file instead
|
| 56 |
+
const buffer = Buffer.from(report.join('\n\n'), 'utf-8');
|
| 57 |
+
await message.reply({
|
| 58 |
+
embeds: [createEmbed({
|
| 59 |
+
title: '🛡️ Permission Audit',
|
| 60 |
+
description: `${legend}\n\nFull report attached (too long for embed).${issues.length ? '\n\n**Issues Found:**\n' + issues.join('\n') : '\n\n✅ No issues found.'}`,
|
| 61 |
+
color: Colors.INFO,
|
| 62 |
+
})],
|
| 63 |
+
files: [{ attachment: buffer, name: 'permission-audit.txt' }],
|
| 64 |
+
});
|
| 65 |
+
} else {
|
| 66 |
+
const desc = embedStr + (issues.length ? '\n\n**Issues Found:**\n' + issues.join('\n') : '\n\n✅ No issues found.');
|
| 67 |
+
await message.reply({
|
| 68 |
+
embeds: [createEmbed({ title: '🛡️ Permission Audit', description: desc, color: Colors.INFO })],
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
};
|
src/commands/postDisclaimer.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { sendDisclaimerEmbed } = require('../systems/embeds');
|
| 2 |
+
const { successEmbed } = require('../utils/embeds');
|
| 3 |
+
|
| 4 |
+
module.exports = {
|
| 5 |
+
name: 'post disclaimer',
|
| 6 |
+
async execute(client, message) {
|
| 7 |
+
await sendDisclaimerEmbed(client);
|
| 8 |
+
await message.reply({ embeds: [successEmbed('Disclaimer Posted', 'Disclaimer embed sent to ⚠️・disclaimer')] });
|
| 9 |
+
},
|
| 10 |
+
};
|
src/commands/postRules.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { sendRulesEmbed } = require('../systems/embeds');
|
| 2 |
+
const { successEmbed } = require('../utils/embeds');
|
| 3 |
+
|
| 4 |
+
module.exports = {
|
| 5 |
+
name: 'post rules',
|
| 6 |
+
async execute(client, message) {
|
| 7 |
+
await sendRulesEmbed(client);
|
| 8 |
+
await message.reply({ embeds: [successEmbed('Rules Posted', 'Rules embed sent to 📜・rules')] });
|
| 9 |
+
},
|
| 10 |
+
};
|
src/commands/setup.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const createRoles = require('./createRoles');
|
| 2 |
+
const createChannels = require('./createChannels');
|
| 3 |
+
const { sendVerificationEmbed } = require('../systems/verification');
|
| 4 |
+
const { sendTicketEmbed } = require('../systems/tickets');
|
| 5 |
+
const { sendDisclaimerEmbed, sendRulesEmbed } = require('../systems/embeds');
|
| 6 |
+
const { successEmbed, infoEmbed, createEmbed } = require('../utils/embeds');
|
| 7 |
+
const { Colors } = require('../config');
|
| 8 |
+
|
| 9 |
+
module.exports = {
|
| 10 |
+
name: 'setup server',
|
| 11 |
+
async execute(client, message) {
|
| 12 |
+
const startEmbed = createEmbed({
|
| 13 |
+
title: '🚀 Server Setup',
|
| 14 |
+
description: [
|
| 15 |
+
'> Starting full server setup...',
|
| 16 |
+
'',
|
| 17 |
+
'```',
|
| 18 |
+
'Phase 1: Creating Roles',
|
| 19 |
+
'Phase 2: Creating Channels',
|
| 20 |
+
'Phase 3: Rules & Disclaimer Embeds',
|
| 21 |
+
'Phase 4: Verification System',
|
| 22 |
+
'Phase 5: Ticket System',
|
| 23 |
+
'```',
|
| 24 |
+
].join('\n'),
|
| 25 |
+
color: Colors.PRIMARY,
|
| 26 |
+
});
|
| 27 |
+
await message.reply({ embeds: [startEmbed] });
|
| 28 |
+
|
| 29 |
+
// Phase 1: Roles
|
| 30 |
+
await message.reply({ embeds: [infoEmbed('Phase 1/5', 'Creating roles...')] });
|
| 31 |
+
await createRoles.execute(client, message);
|
| 32 |
+
|
| 33 |
+
// Phase 2: Channels
|
| 34 |
+
await message.reply({ embeds: [infoEmbed('Phase 2/5', 'Creating channels...')] });
|
| 35 |
+
await createChannels.execute(client, message);
|
| 36 |
+
|
| 37 |
+
// Phase 3: Rules & Disclaimer
|
| 38 |
+
await message.reply({ embeds: [infoEmbed('Phase 3/5', 'Posting rules & disclaimer embeds...')] });
|
| 39 |
+
try {
|
| 40 |
+
await sendRulesEmbed(client);
|
| 41 |
+
await message.reply({ embeds: [successEmbed('Rules', 'Rules embed sent to 📜・rules')] });
|
| 42 |
+
} catch (err) {
|
| 43 |
+
await message.reply({ content: `⚠️ Rules embed issue: ${err.message}` });
|
| 44 |
+
}
|
| 45 |
+
try {
|
| 46 |
+
await sendDisclaimerEmbed(client);
|
| 47 |
+
await message.reply({ embeds: [successEmbed('Disclaimer', 'Disclaimer embed sent to ⚠️・disclaimer')] });
|
| 48 |
+
} catch (err) {
|
| 49 |
+
await message.reply({ content: `⚠️ Disclaimer embed issue: ${err.message}` });
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Phase 4: Verification
|
| 53 |
+
await message.reply({ embeds: [infoEmbed('Phase 4/5', 'Setting up verification...')] });
|
| 54 |
+
try {
|
| 55 |
+
await sendVerificationEmbed(client);
|
| 56 |
+
await message.reply({ embeds: [successEmbed('Verification', 'Verification embed sent to ✅・verify')] });
|
| 57 |
+
} catch (err) {
|
| 58 |
+
await message.reply({ content: `⚠️ Verification setup issue: ${err.message}` });
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Phase 5: Tickets
|
| 62 |
+
await message.reply({ embeds: [infoEmbed('Phase 5/5', 'Setting up ticket system...')] });
|
| 63 |
+
try {
|
| 64 |
+
await sendTicketEmbed(client);
|
| 65 |
+
await message.reply({ embeds: [successEmbed('Tickets', 'Ticket embed sent to 🎫・open-ticket')] });
|
| 66 |
+
} catch (err) {
|
| 67 |
+
await message.reply({ content: `⚠️ Ticket setup issue: ${err.message}` });
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Done
|
| 71 |
+
const completeEmbed = createEmbed({
|
| 72 |
+
title: '✅ Setup Complete',
|
| 73 |
+
description: [
|
| 74 |
+
'> **Wyvern Softworks** server is ready!',
|
| 75 |
+
'',
|
| 76 |
+
'```',
|
| 77 |
+
'✓ Roles created and ordered',
|
| 78 |
+
'✓ All categories & channels set up',
|
| 79 |
+
'✓ Rules & disclaimer posted',
|
| 80 |
+
'✓ Verification system active',
|
| 81 |
+
'✓ Ticket system active',
|
| 82 |
+
'✓ Booster auto-role enabled',
|
| 83 |
+
'✓ Welcome system enabled',
|
| 84 |
+
'```',
|
| 85 |
+
].join('\n'),
|
| 86 |
+
color: Colors.SUCCESS,
|
| 87 |
+
});
|
| 88 |
+
await message.reply({ embeds: [completeEmbed] });
|
| 89 |
+
},
|
| 90 |
+
};
|
src/commands/shutdown.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
const { db } = require('../database');
|
| 4 |
+
|
| 5 |
+
module.exports = {
|
| 6 |
+
name: 'shutdown bot',
|
| 7 |
+
async execute(client, message) {
|
| 8 |
+
const embed = createEmbed({
|
| 9 |
+
title: '🔌 Shutting Down',
|
| 10 |
+
description: 'WSB is going offline. Goodbye!',
|
| 11 |
+
color: Colors.ACCENT,
|
| 12 |
+
});
|
| 13 |
+
await message.reply({ embeds: [embed] });
|
| 14 |
+
|
| 15 |
+
// Close database
|
| 16 |
+
db.close();
|
| 17 |
+
|
| 18 |
+
// Destroy client and exit
|
| 19 |
+
client.destroy();
|
| 20 |
+
process.exit(0);
|
| 21 |
+
},
|
| 22 |
+
};
|
src/commands/ticketStats.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
const { stmts } = require('../database');
|
| 4 |
+
|
| 5 |
+
module.exports = {
|
| 6 |
+
name: 'ticket stats',
|
| 7 |
+
async execute(client, message) {
|
| 8 |
+
const stats = await stmts.ticketStats();
|
| 9 |
+
|
| 10 |
+
const embed = createEmbed({
|
| 11 |
+
title: '🎫 Ticket Statistics',
|
| 12 |
+
description: '> Overview of all support tickets',
|
| 13 |
+
color: Colors.PRIMARY,
|
| 14 |
+
fields: [
|
| 15 |
+
{ name: '📊 Total', value: `\`${stats.total || 0}\``, inline: true },
|
| 16 |
+
{ name: '🟢 Open', value: `\`${stats.open_count || 0}\``, inline: true },
|
| 17 |
+
{ name: '🔴 Closed', value: `\`${stats.closed_count || 0}\``, inline: true },
|
| 18 |
+
{ name: '🗑️ Deleted', value: `\`${stats.deleted_count || 0}\``, inline: true },
|
| 19 |
+
],
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
await message.reply({ embeds: [embed] });
|
| 23 |
+
},
|
| 24 |
+
};
|
src/config.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PermissionFlagsBits } = require('discord.js');
|
| 2 |
+
|
| 3 |
+
// ── Theme Colors ──────────────────────────────────────────────
|
| 4 |
+
const Colors = {
|
| 5 |
+
PRIMARY: 0x9b59b6, // Purple
|
| 6 |
+
ACCENT: 0xe74c3c, // Red
|
| 7 |
+
DARK: 0x0d0d0d, // Near-black
|
| 8 |
+
SUCCESS: 0x2ecc71, // Green
|
| 9 |
+
WARNING: 0xf39c12, // Orange
|
| 10 |
+
INFO: 0x3498db, // Blue
|
| 11 |
+
MUTED: 0x2c2f33, // Dark grey
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// ── Role Definitions (top → bottom hierarchy) ─────────────────
|
| 15 |
+
const Roles = [
|
| 16 |
+
{
|
| 17 |
+
name: '@@ Owner',
|
| 18 |
+
color: '#9b59b6',
|
| 19 |
+
permissions: [PermissionFlagsBits.Administrator],
|
| 20 |
+
hoist: true,
|
| 21 |
+
mentionable: true,
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
name: '@@ Co-Owner',
|
| 25 |
+
color: '#e74c3c',
|
| 26 |
+
permissions: [PermissionFlagsBits.Administrator],
|
| 27 |
+
hoist: true,
|
| 28 |
+
mentionable: false,
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
name: '@@ Server Manager',
|
| 32 |
+
color: '#e91e63',
|
| 33 |
+
permissions: [
|
| 34 |
+
PermissionFlagsBits.ManageGuild,
|
| 35 |
+
PermissionFlagsBits.ManageChannels,
|
| 36 |
+
PermissionFlagsBits.ManageRoles,
|
| 37 |
+
PermissionFlagsBits.KickMembers,
|
| 38 |
+
PermissionFlagsBits.BanMembers,
|
| 39 |
+
PermissionFlagsBits.ManageMessages,
|
| 40 |
+
PermissionFlagsBits.MuteMembers,
|
| 41 |
+
PermissionFlagsBits.DeafenMembers,
|
| 42 |
+
PermissionFlagsBits.MoveMembers,
|
| 43 |
+
PermissionFlagsBits.ManageNicknames,
|
| 44 |
+
PermissionFlagsBits.ViewChannel,
|
| 45 |
+
PermissionFlagsBits.SendMessages,
|
| 46 |
+
],
|
| 47 |
+
hoist: true,
|
| 48 |
+
mentionable: false,
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
name: '@@ Staff',
|
| 52 |
+
color: '#e67e22',
|
| 53 |
+
permissions: [
|
| 54 |
+
PermissionFlagsBits.KickMembers,
|
| 55 |
+
PermissionFlagsBits.BanMembers,
|
| 56 |
+
PermissionFlagsBits.ManageMessages,
|
| 57 |
+
PermissionFlagsBits.MuteMembers,
|
| 58 |
+
PermissionFlagsBits.DeafenMembers,
|
| 59 |
+
PermissionFlagsBits.MoveMembers,
|
| 60 |
+
PermissionFlagsBits.ManageNicknames,
|
| 61 |
+
PermissionFlagsBits.ViewChannel,
|
| 62 |
+
PermissionFlagsBits.SendMessages,
|
| 63 |
+
PermissionFlagsBits.ManageChannels,
|
| 64 |
+
],
|
| 65 |
+
hoist: true,
|
| 66 |
+
mentionable: false,
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
name: '@@ Moderator',
|
| 70 |
+
color: '#f1c40f',
|
| 71 |
+
permissions: [
|
| 72 |
+
PermissionFlagsBits.ManageMessages,
|
| 73 |
+
PermissionFlagsBits.ViewChannel,
|
| 74 |
+
PermissionFlagsBits.SendMessages,
|
| 75 |
+
],
|
| 76 |
+
hoist: true,
|
| 77 |
+
mentionable: false,
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
name: '@@ Helper',
|
| 81 |
+
color: '#1abc9c',
|
| 82 |
+
permissions: [],
|
| 83 |
+
hoist: true,
|
| 84 |
+
mentionable: false,
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
name: '@@ Known',
|
| 88 |
+
color: '#9b59b6',
|
| 89 |
+
permissions: [],
|
| 90 |
+
hoist: true,
|
| 91 |
+
mentionable: false,
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
name: '@@ Booster',
|
| 95 |
+
color: '#f47fff',
|
| 96 |
+
permissions: [],
|
| 97 |
+
hoist: true,
|
| 98 |
+
mentionable: false,
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
name: '@@ Verified',
|
| 102 |
+
color: '#2ecc71',
|
| 103 |
+
permissions: [],
|
| 104 |
+
hoist: false,
|
| 105 |
+
mentionable: false,
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
name: '@@ Buyer',
|
| 109 |
+
color: '#3498db',
|
| 110 |
+
permissions: [],
|
| 111 |
+
hoist: true,
|
| 112 |
+
mentionable: false,
|
| 113 |
+
},
|
| 114 |
+
];
|
| 115 |
+
|
| 116 |
+
// ── Channel / Category Layout ─────────────────────────────────
|
| 117 |
+
const Categories = [
|
| 118 |
+
{
|
| 119 |
+
name: '📌・INFORMATION',
|
| 120 |
+
channels: [
|
| 121 |
+
{ name: '📜・rules', type: 'text', readOnly: true, public: true },
|
| 122 |
+
{ name: '⚠️・disclaimer', type: 'text', readOnly: true, public: true },
|
| 123 |
+
{ name: '✅・verify', type: 'text', special: 'verify', public: true, hideFromVerified: true },
|
| 124 |
+
{ name: '📢・announcements', type: 'text', readOnly: true },
|
| 125 |
+
{ name: '⚡・updates', type: 'text' },
|
| 126 |
+
{ name: '📊・polls', type: 'text', ownerOnly: true },
|
| 127 |
+
],
|
| 128 |
+
permOverrides: () => ({
|
| 129 |
+
everyone: { view: false, send: false },
|
| 130 |
+
'@@ Verified': { view: true, send: true },
|
| 131 |
+
'@@ Booster': { view: true, send: true },
|
| 132 |
+
'@@ Moderator': { view: true, send: true },
|
| 133 |
+
'@@ Staff': { view: true, send: true },
|
| 134 |
+
'@@ Server Manager': { view: true, send: true },
|
| 135 |
+
'@@ Owner': { view: true, send: true },
|
| 136 |
+
}),
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
name: '💬・COMMUNITY',
|
| 140 |
+
channels: [
|
| 141 |
+
{ name: '💬・general', type: 'text', noEmbeds: true },
|
| 142 |
+
{ name: '🎨・media', type: 'text' },
|
| 143 |
+
{ name: '🎧・voice-chat', type: 'voice' },
|
| 144 |
+
],
|
| 145 |
+
permOverrides: () => ({
|
| 146 |
+
everyone: { view: false, send: false },
|
| 147 |
+
'@@ Verified': { view: true, send: true },
|
| 148 |
+
'@@ Booster': { view: true, send: true },
|
| 149 |
+
'@@ Moderator': { view: true, send: true },
|
| 150 |
+
'@@ Staff': { view: true, send: true },
|
| 151 |
+
'@@ Server Manager': { view: true, send: true },
|
| 152 |
+
'@@ Owner': { view: true, send: true },
|
| 153 |
+
}),
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
name: '🎫・SUPPORT & TICKETS',
|
| 157 |
+
channels: [
|
| 158 |
+
{ name: '🎫・open-ticket', type: 'text', special: 'ticket' },
|
| 159 |
+
{ name: '📂・ticket-logs', type: 'text', staffOnly: true },
|
| 160 |
+
],
|
| 161 |
+
permOverrides: () => ({
|
| 162 |
+
everyone: { view: false, send: false },
|
| 163 |
+
'@@ Verified': { view: true, send: false },
|
| 164 |
+
'@@ Booster': { view: true, send: false },
|
| 165 |
+
'@@ Moderator': { view: true, send: false },
|
| 166 |
+
'@@ Staff': { view: true, send: true },
|
| 167 |
+
'@@ Server Manager': { view: true, send: true },
|
| 168 |
+
'@@ Owner': { view: true, send: true },
|
| 169 |
+
}),
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
name: '🤖・DEVELOPMENT',
|
| 173 |
+
channels: [
|
| 174 |
+
{ name: '🤖・offsets', type: 'text' },
|
| 175 |
+
{ name: '🤖・request-offsets', type: 'text' },
|
| 176 |
+
{ name: '🤖・offset-dumpers', type: 'text' },
|
| 177 |
+
{ name: '🧠・dev-chat', type: 'text' },
|
| 178 |
+
],
|
| 179 |
+
permOverrides: () => ({
|
| 180 |
+
everyone: { view: false, send: false },
|
| 181 |
+
'@@ Verified': { view: true, send: false },
|
| 182 |
+
'@@ Booster': { view: true, send: false },
|
| 183 |
+
'@@ Staff': { view: true, send: true },
|
| 184 |
+
'@@ Server Manager': { view: true, send: true },
|
| 185 |
+
'@@ Owner': { view: true, send: true },
|
| 186 |
+
}),
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
name: '🌐・RESOURCES',
|
| 190 |
+
channels: [
|
| 191 |
+
{ name: '🌐・resources', type: 'text', ownerOnly: true },
|
| 192 |
+
{ name: '🌐・free-assets', type: 'text', ownerOnly: true },
|
| 193 |
+
{ name: '🌐・scripts', type: 'text', ownerOnly: true },
|
| 194 |
+
{ name: '🌐・drivers', type: 'text', ownerOnly: true },
|
| 195 |
+
{ name: '🌐・community-content', type: 'text', special: 'community-content' },
|
| 196 |
+
],
|
| 197 |
+
permOverrides: () => ({
|
| 198 |
+
everyone: { view: false, send: false },
|
| 199 |
+
'@@ Verified': { view: true, send: false },
|
| 200 |
+
'@@ Booster': { view: true, send: false },
|
| 201 |
+
'@@ Staff': { view: true, send: false },
|
| 202 |
+
'@@ Server Manager': { view: true, send: false },
|
| 203 |
+
'@@ Owner': { view: true, send: true },
|
| 204 |
+
}),
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
name: '💜・BOOSTER ZONE',
|
| 208 |
+
channels: [
|
| 209 |
+
{ name: '💜・booster-chat', type: 'text' },
|
| 210 |
+
{ name: '💎・booster-rewards', type: 'text', ownerOnly: true },
|
| 211 |
+
{ name: '🚀・booster-updates', type: 'text' },
|
| 212 |
+
],
|
| 213 |
+
permOverrides: () => ({
|
| 214 |
+
everyone: { view: false, send: false },
|
| 215 |
+
'@@ Booster': { view: true, send: true },
|
| 216 |
+
'@@ Staff': { view: true, send: true },
|
| 217 |
+
'@@ Server Manager': { view: true, send: true },
|
| 218 |
+
'@@ Owner': { view: true, send: true },
|
| 219 |
+
}),
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
name: '🛡️・STAFF ONLY',
|
| 223 |
+
channels: [
|
| 224 |
+
{ name: '👑・owner-chat', type: 'text', special: 'owner-chat' },
|
| 225 |
+
{ name: '🛡️・staff-chat', type: 'text' },
|
| 226 |
+
{ name: '📁・staff-logs', type: 'text', special: 'staff-logs' },
|
| 227 |
+
{ name: '⚙️・bot-control', type: 'text' },
|
| 228 |
+
],
|
| 229 |
+
permOverrides: () => ({
|
| 230 |
+
everyone: { view: false, send: false },
|
| 231 |
+
'@@ Staff': { view: true, send: true },
|
| 232 |
+
'@@ Server Manager': { view: true, send: true },
|
| 233 |
+
'@@ Owner': { view: true, send: true },
|
| 234 |
+
}),
|
| 235 |
+
},
|
| 236 |
+
];
|
| 237 |
+
|
| 238 |
+
module.exports = { Colors, Roles, Categories };
|
src/database.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createClient } = require('@supabase/supabase-js');
|
| 2 |
+
|
| 3 |
+
const supabase = createClient(
|
| 4 |
+
process.env.SUPABASE_URL,
|
| 5 |
+
process.env.SUPABASE_SERVICE_ROLE_KEY
|
| 6 |
+
);
|
| 7 |
+
|
| 8 |
+
// ── Prepared Statements (Wrappers for Supabase) ─────────────────
|
| 9 |
+
const stmts = {
|
| 10 |
+
// Tickets
|
| 11 |
+
createTicket: async (userId, username, channelId) => {
|
| 12 |
+
return await supabase
|
| 13 |
+
.from('tickets')
|
| 14 |
+
.insert({ user_id: userId, username, channel_id: channelId });
|
| 15 |
+
},
|
| 16 |
+
closeTicket: async (status, channelId) => {
|
| 17 |
+
return await supabase
|
| 18 |
+
.from('tickets')
|
| 19 |
+
.update({ status, closed_at: new Date().toISOString() })
|
| 20 |
+
.eq('channel_id', channelId);
|
| 21 |
+
},
|
| 22 |
+
getTicket: async (channelId) => {
|
| 23 |
+
const { data } = await supabase
|
| 24 |
+
.from('tickets')
|
| 25 |
+
.select('*')
|
| 26 |
+
.eq('channel_id', channelId)
|
| 27 |
+
.single();
|
| 28 |
+
return data;
|
| 29 |
+
},
|
| 30 |
+
getOpenTickets: async (status) => {
|
| 31 |
+
const { data } = await supabase
|
| 32 |
+
.from('tickets')
|
| 33 |
+
.select('*')
|
| 34 |
+
.eq('status', status);
|
| 35 |
+
return data || [];
|
| 36 |
+
},
|
| 37 |
+
getUserTicket: async (userId, status) => {
|
| 38 |
+
const { data } = await supabase
|
| 39 |
+
.from('tickets')
|
| 40 |
+
.select('*')
|
| 41 |
+
.eq('user_id', userId)
|
| 42 |
+
.eq('status', status)
|
| 43 |
+
.single();
|
| 44 |
+
return data;
|
| 45 |
+
},
|
| 46 |
+
ticketStats: async () => {
|
| 47 |
+
const { data, error } = await supabase.rpc('get_ticket_stats');
|
| 48 |
+
if (data) return data;
|
| 49 |
+
|
| 50 |
+
// Manual fallback if RPC not setup
|
| 51 |
+
const { count: total } = await supabase.from('tickets').select('*', { count: 'exact', head: true });
|
| 52 |
+
const { count: open } = await supabase.from('tickets').select('*', { count: 'exact', head: true }).eq('status', 'open');
|
| 53 |
+
const { count: closed } = await supabase.from('tickets').select('*', { count: 'exact', head: true }).eq('status', 'closed');
|
| 54 |
+
const { count: deleted } = await supabase.from('tickets').select('*', { count: 'exact', head: true }).eq('status', 'deleted');
|
| 55 |
+
|
| 56 |
+
return { total, open_count: open, closed_count: closed, deleted_count: deleted };
|
| 57 |
+
},
|
| 58 |
+
|
| 59 |
+
// Verification log
|
| 60 |
+
logVerification: async (userId, username, action) => {
|
| 61 |
+
return await supabase
|
| 62 |
+
.from('verification_log')
|
| 63 |
+
.insert({ user_id: userId, username, action });
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
// Bot state (key-value)
|
| 67 |
+
setState: async (key, value) => {
|
| 68 |
+
return await supabase
|
| 69 |
+
.from('bot_state')
|
| 70 |
+
.upsert({ key, value });
|
| 71 |
+
},
|
| 72 |
+
getState: async (key) => {
|
| 73 |
+
const { data } = await supabase
|
| 74 |
+
.from('bot_state')
|
| 75 |
+
.select('value')
|
| 76 |
+
.eq('key', key)
|
| 77 |
+
.single();
|
| 78 |
+
return data ? data.value : null;
|
| 79 |
+
},
|
| 80 |
+
delState: async (key) => {
|
| 81 |
+
return await supabase
|
| 82 |
+
.from('bot_state')
|
| 83 |
+
.delete()
|
| 84 |
+
.eq('key', key);
|
| 85 |
+
},
|
| 86 |
+
|
| 87 |
+
// Whitelist
|
| 88 |
+
addWhitelist: async (userId, maxDrops, addedBy) => {
|
| 89 |
+
return await supabase
|
| 90 |
+
.from('whitelist')
|
| 91 |
+
.upsert({ user_id: userId, max_drops: maxDrops, added_by: addedBy });
|
| 92 |
+
},
|
| 93 |
+
removeWhitelist: async (userId) => {
|
| 94 |
+
return await supabase
|
| 95 |
+
.from('whitelist')
|
| 96 |
+
.delete()
|
| 97 |
+
.eq('user_id', userId);
|
| 98 |
+
},
|
| 99 |
+
getWhitelist: async (userId) => {
|
| 100 |
+
const { data } = await supabase
|
| 101 |
+
.from('whitelist')
|
| 102 |
+
.select('*')
|
| 103 |
+
.eq('user_id', userId)
|
| 104 |
+
.single();
|
| 105 |
+
return data;
|
| 106 |
+
},
|
| 107 |
+
getAllWhitelist: async () => {
|
| 108 |
+
const { data } = await supabase
|
| 109 |
+
.from('whitelist')
|
| 110 |
+
.select('*');
|
| 111 |
+
return data || [];
|
| 112 |
+
},
|
| 113 |
+
|
| 114 |
+
// Drops array
|
| 115 |
+
logDrop: async (userId, title, channelId) => {
|
| 116 |
+
return await supabase
|
| 117 |
+
.from('drop_log')
|
| 118 |
+
.insert({ user_id: userId, title, channel_id: channelId });
|
| 119 |
+
},
|
| 120 |
+
getDropCount24h: async (userId) => {
|
| 121 |
+
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
| 122 |
+
const { count } = await supabase
|
| 123 |
+
.from('drop_log')
|
| 124 |
+
.select('*', { count: 'exact', head: true })
|
| 125 |
+
.eq('user_id', userId)
|
| 126 |
+
.gt('dropped_at', twentyFourHoursAgo);
|
| 127 |
+
return { count: count || 0 };
|
| 128 |
+
},
|
| 129 |
+
getLastDrop: async (userId) => {
|
| 130 |
+
const { data } = await supabase
|
| 131 |
+
.from('drop_log')
|
| 132 |
+
.select('dropped_at')
|
| 133 |
+
.eq('user_id', userId)
|
| 134 |
+
.order('dropped_at', { ascending: false })
|
| 135 |
+
.limit(1)
|
| 136 |
+
.single();
|
| 137 |
+
return data;
|
| 138 |
+
},
|
| 139 |
+
|
| 140 |
+
addWebDrop: async (userId, category, title, description, status, isExternal, assetId, fileUrl, imageUrl) => {
|
| 141 |
+
return await supabase
|
| 142 |
+
.from('web_drops')
|
| 143 |
+
.insert({
|
| 144 |
+
user_id: userId,
|
| 145 |
+
category,
|
| 146 |
+
title,
|
| 147 |
+
description,
|
| 148 |
+
status,
|
| 149 |
+
is_external: isExternal,
|
| 150 |
+
asset_id: assetId,
|
| 151 |
+
file_url: fileUrl,
|
| 152 |
+
image_url: imageUrl
|
| 153 |
+
});
|
| 154 |
+
},
|
| 155 |
+
getWebDrop: async (id) => {
|
| 156 |
+
const { data } = await supabase
|
| 157 |
+
.from('web_drops')
|
| 158 |
+
.select('*')
|
| 159 |
+
.eq('id', id)
|
| 160 |
+
.single();
|
| 161 |
+
return data;
|
| 162 |
+
},
|
| 163 |
+
deleteWebDrop: async (id) => {
|
| 164 |
+
return await supabase
|
| 165 |
+
.from('web_drops')
|
| 166 |
+
.delete()
|
| 167 |
+
.eq('id', id);
|
| 168 |
+
},
|
| 169 |
+
getAllWebDrops: async () => {
|
| 170 |
+
const { data } = await supabase
|
| 171 |
+
.from('web_drops')
|
| 172 |
+
.select('*')
|
| 173 |
+
.order('published_at', { ascending: false })
|
| 174 |
+
.limit(50);
|
| 175 |
+
return data || [];
|
| 176 |
+
},
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
module.exports = { db: supabase, stmts };
|
src/events/guildMemberAdd.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { handleMemberJoin, handleMemberLeave } = require('../systems/welcome');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
name: 'guildMemberAdd',
|
| 5 |
+
async execute(client, member) {
|
| 6 |
+
await handleMemberJoin(member, client);
|
| 7 |
+
},
|
| 8 |
+
};
|
src/events/guildMemberRemove.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { handleMemberLeave } = require('../systems/welcome');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
name: 'guildMemberRemove',
|
| 5 |
+
async execute(client, member) {
|
| 6 |
+
await handleMemberLeave(member, client);
|
| 7 |
+
},
|
| 8 |
+
};
|
src/events/guildMemberUpdate.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { handleBoostChange } = require('../systems/booster');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
name: 'guildMemberUpdate',
|
| 5 |
+
async execute(client, oldMember, newMember) {
|
| 6 |
+
await handleBoostChange(oldMember, newMember, client);
|
| 7 |
+
},
|
| 8 |
+
};
|
src/events/interactionCreate.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { handleTicketButton } = require('../systems/tickets');
|
| 2 |
+
const { handleDropButton, handleDownloadButton } = require('../systems/drops');
|
| 3 |
+
const { handleMassDropInteraction } = require('../systems/massdrop');
|
| 4 |
+
|
| 5 |
+
module.exports = {
|
| 6 |
+
name: 'interactionCreate',
|
| 7 |
+
async execute(client, interaction) {
|
| 8 |
+
// Handle select menus and modals as well
|
| 9 |
+
if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit()) return;
|
| 10 |
+
|
| 11 |
+
// Handle download buttons from server channels (dl_<assetId>)
|
| 12 |
+
if (interaction.isButton() && interaction.customId.startsWith('dl_')) {
|
| 13 |
+
await handleDownloadButton(interaction);
|
| 14 |
+
return;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Try mass drop interactions (buttons, dropdowns, modals)
|
| 18 |
+
const massDropHandled = await handleMassDropInteraction(interaction);
|
| 19 |
+
if (massDropHandled) return;
|
| 20 |
+
|
| 21 |
+
// Try drop buttons first (DM interactions)
|
| 22 |
+
if (interaction.isButton()) {
|
| 23 |
+
const dropHandled = await handleDropButton(interaction);
|
| 24 |
+
if (dropHandled) return;
|
| 25 |
+
|
| 26 |
+
// Then ticket buttons (server interactions)
|
| 27 |
+
await handleTicketButton(interaction, client);
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
};
|
src/events/messageCreate.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ChannelType } = require('discord.js');
|
| 2 |
+
const { errorEmbed, infoEmbed, successEmbed, createEmbed } = require('../utils/embeds');
|
| 3 |
+
const { logCommand } = require('../systems/logger');
|
| 4 |
+
const { startDropSession, hasSession, getPrompt, handleDropMessage, canDrop } = require('../systems/drops');
|
| 5 |
+
const { startMassDropSession, hasMassSession, getMassPrompt, handleMassDropMessage } = require('../systems/massdrop');
|
| 6 |
+
const { stmts } = require('../database');
|
| 7 |
+
const { Colors } = require('../config');
|
| 8 |
+
|
| 9 |
+
// Import command handlers
|
| 10 |
+
const setup = require('../commands/setup');
|
| 11 |
+
const createRoles = require('../commands/createRoles');
|
| 12 |
+
const createChannels = require('../commands/createChannels');
|
| 13 |
+
const backupLayout = require('../commands/backupLayout');
|
| 14 |
+
const shutdown = require('../commands/shutdown');
|
| 15 |
+
const ticketStats = require('../commands/ticketStats');
|
| 16 |
+
const permissionAudit = require('../commands/permissionAudit');
|
| 17 |
+
const postRules = require('../commands/postRules');
|
| 18 |
+
const postDisclaimer = require('../commands/postDisclaimer');
|
| 19 |
+
const applyUpdates = require('../commands/applyUpdates');
|
| 20 |
+
const fixPings = require('../commands/fixPings');
|
| 21 |
+
const clearDrops = require('../commands/clearDrops');
|
| 22 |
+
const fixDownloads = require('../commands/fixDownloads');
|
| 23 |
+
const coOwnerRole = require('../commands/coOwnerRole');
|
| 24 |
+
const editDrop = require('../commands/editDrop');
|
| 25 |
+
const deleteDrop = require('../commands/deleteDrop');
|
| 26 |
+
|
| 27 |
+
const OWNER_ID = process.env.OWNER_ID;
|
| 28 |
+
|
| 29 |
+
// Owner-only command registry
|
| 30 |
+
const commands = {
|
| 31 |
+
'setup server': setup,
|
| 32 |
+
'create roles': createRoles,
|
| 33 |
+
'create channels': createChannels,
|
| 34 |
+
'backup layout': backupLayout,
|
| 35 |
+
'shutdown bot': shutdown,
|
| 36 |
+
'ticket stats': ticketStats,
|
| 37 |
+
'permission audit': permissionAudit,
|
| 38 |
+
'post rules': postRules,
|
| 39 |
+
'post disclaimer': postDisclaimer,
|
| 40 |
+
'apply updates': applyUpdates,
|
| 41 |
+
'fix pings': fixPings,
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
module.exports = {
|
| 45 |
+
name: 'messageCreate',
|
| 46 |
+
async execute(client, message) {
|
| 47 |
+
if (message.author.bot) return;
|
| 48 |
+
if (message.channel.type !== ChannelType.DM) return;
|
| 49 |
+
|
| 50 |
+
const userId = message.author.id;
|
| 51 |
+
const content = message.content.trim().toLowerCase();
|
| 52 |
+
|
| 53 |
+
// ── Handle active drop session (owner OR whitelisted) ──
|
| 54 |
+
if (hasSession(userId)) {
|
| 55 |
+
try {
|
| 56 |
+
const handled = await handleDropMessage(message);
|
| 57 |
+
if (handled) return;
|
| 58 |
+
} catch (err) {
|
| 59 |
+
console.error('[Drop Error]', err);
|
| 60 |
+
await message.reply({ content: `❌ Drop error: ${err.message}` }).catch(() => { });
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ── Handle active mass drop session ──
|
| 66 |
+
if (hasMassSession(userId)) {
|
| 67 |
+
try {
|
| 68 |
+
const handled = await handleMassDropMessage(message);
|
| 69 |
+
if (handled) return;
|
| 70 |
+
} catch (err) {
|
| 71 |
+
console.error('[Mass Drop Error]', err);
|
| 72 |
+
await message.reply({ content: `❌ Mass Drop error: ${err.message}` }).catch(() => { });
|
| 73 |
+
return;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// ── Whitelisted user: only allow "drop" ──
|
| 78 |
+
if (userId !== OWNER_ID) {
|
| 79 |
+
if (content === 'drop' || content === 'linkdrop') {
|
| 80 |
+
const check = await canDrop(userId);
|
| 81 |
+
if (!check.allowed) {
|
| 82 |
+
if (check.reason === 'not_whitelisted') {
|
| 83 |
+
return; // Silently ignore non-whitelisted users
|
| 84 |
+
}
|
| 85 |
+
// Rate limited
|
| 86 |
+
return message.reply({
|
| 87 |
+
embeds: [createEmbed({
|
| 88 |
+
title: '⏳ Drop Cooldown',
|
| 89 |
+
description: `You've used all **${check.limit} drops** in the last 24 hours.\n\n> Resets in **${check.resetIn}**`,
|
| 90 |
+
color: Colors.WARNING,
|
| 91 |
+
})],
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
const session = startDropSession(userId);
|
| 95 |
+
await message.reply({
|
| 96 |
+
...getPrompt(session),
|
| 97 |
+
content: `📦 **Drop ${check.limit - check.remaining + 1}/${check.limit} for today**`,
|
| 98 |
+
});
|
| 99 |
+
return;
|
| 100 |
+
}
|
| 101 |
+
return; // Whitelisted users can only use "drop" or "linkdrop"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// ── Owner-only commands below ──────────────────────────
|
| 105 |
+
|
| 106 |
+
// Drop (no rate limit for owner)
|
| 107 |
+
if (content === 'drop' || content === 'linkdrop') {
|
| 108 |
+
await logCommand(client, content);
|
| 109 |
+
const session = startDropSession(userId);
|
| 110 |
+
await message.reply(getPrompt(session));
|
| 111 |
+
return;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Mass Drop (no rate limit for owner)
|
| 115 |
+
if (content === 'massdrop') {
|
| 116 |
+
await logCommand(client, 'massdrop');
|
| 117 |
+
const session = startMassDropSession(userId);
|
| 118 |
+
await message.reply(getMassPrompt(session));
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Whitelist management
|
| 123 |
+
if (content.startsWith('whitelist ') && !content.startsWith('whitelist <')) {
|
| 124 |
+
const args = content.split(' ').slice(1);
|
| 125 |
+
const targetId = args[0]?.trim();
|
| 126 |
+
const limit = parseInt(args[1]) || 3;
|
| 127 |
+
|
| 128 |
+
if (!targetId || !/^\d{17,20}$/.test(targetId)) {
|
| 129 |
+
return message.reply({ embeds: [errorEmbed('Invalid ID', 'Usage: `whitelist <user_id> [limit]`\nExample: `whitelist 123456789 5`')] });
|
| 130 |
+
}
|
| 131 |
+
await stmts.addWhitelist(targetId, limit, userId);
|
| 132 |
+
const user = await client.users.fetch(targetId).catch(() => null);
|
| 133 |
+
const name = user ? `**${user.tag}**` : `ID \`${targetId}\``;
|
| 134 |
+
return message.reply({ embeds: [successEmbed('✅ Whitelisted', `${name} can now use \`drop\` (**${limit}** per day)`)] });
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
if (content.startsWith('unwhitelist ')) {
|
| 138 |
+
const targetId = content.split(' ')[1]?.trim();
|
| 139 |
+
if (!targetId) return message.reply({ embeds: [errorEmbed('Invalid ID', 'Usage: `unwhitelist <user_id>`')] });
|
| 140 |
+
await stmts.removeWhitelist(targetId);
|
| 141 |
+
return message.reply({ embeds: [successEmbed('✅ Removed', `User \`${targetId}\` removed from whitelist`)] });
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Clear Drops
|
| 145 |
+
if (content.startsWith('clear ')) {
|
| 146 |
+
const args = content.split(' ').slice(1);
|
| 147 |
+
return clearDrops.execute(client, message, args);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// Fix Downloads (migrate old Link buttons to interactive buttons)
|
| 151 |
+
if (content.startsWith('fix downloads')) {
|
| 152 |
+
const args = content.split(' ').slice(2);
|
| 153 |
+
return fixDownloads.execute(client, message, args);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Co-Owner Role (temporary)
|
| 157 |
+
if (content === 'coownerrole') {
|
| 158 |
+
await logCommand(client, 'coownerrole');
|
| 159 |
+
return coOwnerRole.execute(client, message);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// Web Administration Commands
|
| 163 |
+
if (content.startsWith('editdrop ')) {
|
| 164 |
+
const args = content.split(' ').slice(1);
|
| 165 |
+
return editDrop.execute(client, message, args);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if (content.startsWith('deletedrop ')) {
|
| 169 |
+
const args = content.split(' ').slice(1);
|
| 170 |
+
return deleteDrop.execute(client, message, args);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
if (content === 'whitelist') {
|
| 174 |
+
const all = await stmts.getAllWhitelist();
|
| 175 |
+
if (all.length === 0) {
|
| 176 |
+
return message.reply({ embeds: [infoEmbed('Whitelist', 'No users whitelisted.\n\nUse `whitelist <user_id>` to add.')] });
|
| 177 |
+
}
|
| 178 |
+
const lines = [];
|
| 179 |
+
for (const w of all) {
|
| 180 |
+
const user = await client.users.fetch(w.user_id).catch(() => null);
|
| 181 |
+
const name = user ? user.tag : 'Unknown';
|
| 182 |
+
const drops = await stmts.getDropCount24h(w.user_id);
|
| 183 |
+
lines.push(`• **${name}** (\`${w.user_id}\`) — ${drops.count}/${w.max_drops} drops today`);
|
| 184 |
+
}
|
| 185 |
+
return message.reply({ embeds: [infoEmbed('📋 Whitelist', lines.join('\n'))] });
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// Show help
|
| 189 |
+
if (content === 'help') {
|
| 190 |
+
const allCommands = [...Object.keys(commands), 'drop', 'massdrop', 'linkdrop', 'whitelist', 'whitelist <id> [limit]', 'unwhitelist <id>', 'clear <channel_id>'];
|
| 191 |
+
const embed = infoEmbed('WSB Commands', [
|
| 192 |
+
'All commands are **DM-only** and **Owner-only**.\n',
|
| 193 |
+
...allCommands.map(cmd => `\`${cmd}\``),
|
| 194 |
+
].join('\n'));
|
| 195 |
+
return message.reply({ embeds: [embed] });
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// Find matching command
|
| 199 |
+
const handler = commands[content];
|
| 200 |
+
if (!handler) {
|
| 201 |
+
if (content.startsWith('setup') || content.startsWith('create') ||
|
| 202 |
+
content.startsWith('backup') || content.startsWith('shutdown') ||
|
| 203 |
+
content.startsWith('ticket') || content.startsWith('permission') ||
|
| 204 |
+
content.startsWith('post') || content.startsWith('drop') ||
|
| 205 |
+
content.startsWith('mass') || content.startsWith('link') ||
|
| 206 |
+
content.startsWith('apply') || content.startsWith('fix') ||
|
| 207 |
+
content.startsWith('clear')) {
|
| 208 |
+
return message.reply({ embeds: [errorEmbed('Unknown Command', `Did you mean one of these?\n${[...Object.keys(commands), 'drop', 'massdrop', 'linkdrop', 'clear <channel_id>'].map(c => `\`${c}\``).join('\n')}`)] });
|
| 209 |
+
}
|
| 210 |
+
return;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
try {
|
| 214 |
+
await logCommand(client, content);
|
| 215 |
+
await handler.execute(client, message);
|
| 216 |
+
} catch (err) {
|
| 217 |
+
console.error(`[Command Error] ${content}:`, err);
|
| 218 |
+
await message.reply({ embeds: [errorEmbed('Error', `Command failed: \`${err.message}\``)] }).catch(() => { });
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
};
|
src/events/messageReactionAdd.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { handleVerifyReaction } = require('../systems/verification');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
name: 'messageReactionAdd',
|
| 5 |
+
async execute(client, reaction, user) {
|
| 6 |
+
if (user.bot) return;
|
| 7 |
+
|
| 8 |
+
// Handle partial reactions (uncached messages)
|
| 9 |
+
if (reaction.partial) {
|
| 10 |
+
try { await reaction.fetch(); } catch { return; }
|
| 11 |
+
}
|
| 12 |
+
if (reaction.message.partial) {
|
| 13 |
+
try { await reaction.message.fetch(); } catch { return; }
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Handle verification
|
| 17 |
+
await handleVerifyReaction(reaction, user, client);
|
| 18 |
+
},
|
| 19 |
+
};
|
src/events/messageReactionRemove.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { handleVerifyReactionRemove } = require('../systems/verification');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
name: 'messageReactionRemove',
|
| 5 |
+
async execute(client, reaction, user) {
|
| 6 |
+
if (user.bot) return;
|
| 7 |
+
|
| 8 |
+
if (reaction.partial) {
|
| 9 |
+
try { await reaction.fetch(); } catch { return; }
|
| 10 |
+
}
|
| 11 |
+
if (reaction.message.partial) {
|
| 12 |
+
try { await reaction.message.fetch(); } catch { return; }
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
await handleVerifyReactionRemove(reaction, user, client);
|
| 16 |
+
},
|
| 17 |
+
};
|
src/events/ready.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ActivityType } = require('discord.js');
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
name: 'ready',
|
| 5 |
+
once: true,
|
| 6 |
+
execute(client) {
|
| 7 |
+
const sessionId = Math.random().toString(36).substring(7).toUpperCase();
|
| 8 |
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
| 9 |
+
console.log(` 🟣 WSB — Wyvern Softworks Bot [ID: ${sessionId}]`);
|
| 10 |
+
console.log(` ✅ Logged in as ${client.user.tag}`);
|
| 11 |
+
console.log(` 🏠 Serving ${client.guilds.cache.size} guild(s)`);
|
| 12 |
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
| 13 |
+
|
| 14 |
+
client.user.setPresence({
|
| 15 |
+
activities: [{ name: 'Wyvern Softworks', type: ActivityType.Watching }],
|
| 16 |
+
status: 'dnd',
|
| 17 |
+
});
|
| 18 |
+
},
|
| 19 |
+
};
|
src/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config();
|
| 2 |
+
|
| 3 |
+
// Fix for Node 18+ IPv6 DNS resolution bug in Docker
|
| 4 |
+
const dns = require('node:dns');
|
| 5 |
+
dns.setDefaultResultOrder('ipv4first');
|
| 6 |
+
|
| 7 |
+
const {
|
| 8 |
+
Client,
|
| 9 |
+
GatewayIntentBits,
|
| 10 |
+
Partials,
|
| 11 |
+
} = require('discord.js');
|
| 12 |
+
|
| 13 |
+
// ── Validate Environment ──────────────────────────────────────
|
| 14 |
+
const required = ['BOT_TOKEN', 'OWNER_ID'];
|
| 15 |
+
console.log(' 📋 ENV CHECK:', required.map(k => `${k}=${process.env[k] ? '✅' : '❌'}`).join(' | '));
|
| 16 |
+
for (const key of required) {
|
| 17 |
+
if (!process.env[key] || process.env[key].includes('YOUR_')) {
|
| 18 |
+
console.error(`❌ Missing or placeholder env var: ${key}`);
|
| 19 |
+
console.error(' Please fill in your .env file before starting.');
|
| 20 |
+
process.exit(1);
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// ── Create Client ─────────────────────────────────────────────
|
| 25 |
+
const client = new Client({
|
| 26 |
+
intents: [
|
| 27 |
+
GatewayIntentBits.Guilds,
|
| 28 |
+
GatewayIntentBits.GuildMembers,
|
| 29 |
+
GatewayIntentBits.GuildMessages,
|
| 30 |
+
GatewayIntentBits.GuildMessageReactions,
|
| 31 |
+
GatewayIntentBits.DirectMessages,
|
| 32 |
+
GatewayIntentBits.MessageContent,
|
| 33 |
+
GatewayIntentBits.GuildPresences,
|
| 34 |
+
],
|
| 35 |
+
partials: [
|
| 36 |
+
Partials.Message,
|
| 37 |
+
Partials.Channel,
|
| 38 |
+
Partials.Reaction,
|
| 39 |
+
Partials.User,
|
| 40 |
+
Partials.GuildMember,
|
| 41 |
+
],
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// ── Load Event Handlers ───────────────────────────────────────
|
| 45 |
+
const events = [
|
| 46 |
+
require('./events/ready'),
|
| 47 |
+
require('./events/messageCreate'),
|
| 48 |
+
require('./events/messageReactionAdd'),
|
| 49 |
+
require('./events/messageReactionRemove'),
|
| 50 |
+
require('./events/guildMemberUpdate'),
|
| 51 |
+
require('./events/guildMemberAdd'),
|
| 52 |
+
require('./events/guildMemberRemove'),
|
| 53 |
+
require('./events/interactionCreate'),
|
| 54 |
+
];
|
| 55 |
+
|
| 56 |
+
for (const event of events) {
|
| 57 |
+
if (event.once) {
|
| 58 |
+
client.once(event.name, (...args) => event.execute(client, ...args));
|
| 59 |
+
} else {
|
| 60 |
+
client.on(event.name, (...args) => event.execute(client, ...args));
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// ── Error Handling ────────────────────────────────────────────
|
| 65 |
+
client.on('error', (err) => {
|
| 66 |
+
console.error('[Client Error]', err);
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
process.on('unhandledRejection', (err) => {
|
| 70 |
+
console.error('[Unhandled Rejection]', err);
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
process.on('uncaughtException', (err) => {
|
| 74 |
+
console.error('[Uncaught Exception]', err);
|
| 75 |
+
process.exit(1);
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
// ── Keep-Alive HTTP Server (for Render / Glitch + UptimeRobot) ─
|
| 79 |
+
const http = require('http');
|
| 80 |
+
const PORT = process.env.PORT || 3001;
|
| 81 |
+
|
| 82 |
+
http.createServer((req, res) => {
|
| 83 |
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 84 |
+
res.end(JSON.stringify({
|
| 85 |
+
status: 'alive',
|
| 86 |
+
bot: client.user?.tag || 'starting...',
|
| 87 |
+
uptime: Math.floor(process.uptime()) + 's',
|
| 88 |
+
}));
|
| 89 |
+
}).listen(PORT, () => {
|
| 90 |
+
console.log(` 🌐 Keep-alive server on port ${PORT}`);
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
// ── Login ─────────────────────────────────────────────────────
|
| 94 |
+
const token = process.env.BOT_TOKEN;
|
| 95 |
+
console.log(` 🔑 Token: ${token ? token.slice(0, 10) + '...' + token.slice(-5) : 'MISSING'}`);
|
| 96 |
+
|
| 97 |
+
async function startBot(retryCount = 0) {
|
| 98 |
+
if (retryCount > 10) {
|
| 99 |
+
console.error('❌ MAX RETRIES REACHED. Bot failed to connect.');
|
| 100 |
+
process.exit(1);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
console.log(` ⏳ Connecting to Discord... (Attempt ${retryCount + 1})`);
|
| 104 |
+
|
| 105 |
+
const loginTimeout = setTimeout(() => {
|
| 106 |
+
console.error('❌ LOGIN TIMED OUT after 60s — Discord gateway unreachable');
|
| 107 |
+
}, 60000);
|
| 108 |
+
|
| 109 |
+
try {
|
| 110 |
+
await client.login(token);
|
| 111 |
+
clearTimeout(loginTimeout);
|
| 112 |
+
console.log(' ✅ Login promise resolved');
|
| 113 |
+
} catch (err) {
|
| 114 |
+
clearTimeout(loginTimeout);
|
| 115 |
+
console.error(`❌ LOGIN FAILED: ${err.message}`);
|
| 116 |
+
|
| 117 |
+
if (err.message.includes('ENOTFOUND') || err.message.includes('EAI_AGAIN')) {
|
| 118 |
+
console.log(' 📡 DNS/Network error detected. Retrying in 10s...');
|
| 119 |
+
setTimeout(() => startBot(retryCount + 1), 10000);
|
| 120 |
+
} else {
|
| 121 |
+
console.error(' ⚠️ Non-network error. Exiting.');
|
| 122 |
+
process.exit(1);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Debug logging for network issues
|
| 128 |
+
client.on('debug', (info) => {
|
| 129 |
+
if (info.includes('Gateway')) console.log(`[DEBUG] ${info}`);
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
startBot();
|
src/systems/booster.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { logRoleChange } = require('./logger');
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Detect server boost changes via guildMemberUpdate.
|
| 5 |
+
* If a member starts boosting → add Booster role.
|
| 6 |
+
* If a member stops boosting → remove Booster role.
|
| 7 |
+
*/
|
| 8 |
+
async function handleBoostChange(oldMember, newMember, client) {
|
| 9 |
+
const wasBoosting = oldMember.premiumSince !== null;
|
| 10 |
+
const isBoosting = newMember.premiumSince !== null;
|
| 11 |
+
|
| 12 |
+
if (wasBoosting === isBoosting) return; // No change
|
| 13 |
+
|
| 14 |
+
const boosterRole = newMember.guild.roles.cache.find(r => r.name === '@@ Booster');
|
| 15 |
+
if (!boosterRole) return;
|
| 16 |
+
|
| 17 |
+
if (!wasBoosting && isBoosting) {
|
| 18 |
+
// Started boosting
|
| 19 |
+
await newMember.roles.add(boosterRole).catch(() => { });
|
| 20 |
+
await logRoleChange(client, {
|
| 21 |
+
user: newMember.user,
|
| 22 |
+
role: 'Booster',
|
| 23 |
+
action: 'added',
|
| 24 |
+
});
|
| 25 |
+
} else if (wasBoosting && !isBoosting) {
|
| 26 |
+
// Stopped boosting
|
| 27 |
+
await newMember.roles.remove(boosterRole).catch(() => { });
|
| 28 |
+
await logRoleChange(client, {
|
| 29 |
+
user: newMember.user,
|
| 30 |
+
role: 'Booster',
|
| 31 |
+
action: 'removed',
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
module.exports = { handleBoostChange };
|
src/systems/drops.js
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const {
|
| 2 |
+
ActionRowBuilder,
|
| 3 |
+
ButtonBuilder,
|
| 4 |
+
ButtonStyle,
|
| 5 |
+
StringSelectMenuBuilder,
|
| 6 |
+
AttachmentBuilder,
|
| 7 |
+
} = require('discord.js');
|
| 8 |
+
const { Octokit } = require('@octokit/rest');
|
| 9 |
+
const fetch = require('node-fetch');
|
| 10 |
+
const { createEmbed } = require('../utils/embeds');
|
| 11 |
+
const { Colors } = require('../config');
|
| 12 |
+
const { stmts } = require('../database');
|
| 13 |
+
|
| 14 |
+
const OWNER_ID = process.env.OWNER_ID;
|
| 15 |
+
|
| 16 |
+
// Active drop sessions: Map<userId, session>
|
| 17 |
+
const activeSessions = new Map();
|
| 18 |
+
|
| 19 |
+
const STEPS = ['category', 'title', 'file', 'warnings', 'status', 'about', 'image', 'preview'];
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Check if a user can drop (owner = unlimited, whitelisted = custom limit).
|
| 23 |
+
* Returns { allowed: boolean, remaining?: number, resetIn?: string, limit?: number }
|
| 24 |
+
*/
|
| 25 |
+
async function canDrop(userId) {
|
| 26 |
+
if (userId === OWNER_ID) return { allowed: true, remaining: Infinity };
|
| 27 |
+
|
| 28 |
+
const wl = await stmts.getWhitelist(userId);
|
| 29 |
+
if (!wl) return { allowed: false, reason: 'not_whitelisted' };
|
| 30 |
+
|
| 31 |
+
const limit = wl.max_drops;
|
| 32 |
+
const { count } = await stmts.getDropCount24h(userId);
|
| 33 |
+
if (count >= limit) {
|
| 34 |
+
const last = await stmts.getLastDrop(userId);
|
| 35 |
+
const resetTime = new Date(last.dropped_at);
|
| 36 |
+
resetTime.setHours(resetTime.getHours() + 24);
|
| 37 |
+
const diff = resetTime - new Date();
|
| 38 |
+
const hours = Math.floor(diff / 3600000);
|
| 39 |
+
const mins = Math.floor((diff % 3600000) / 60000);
|
| 40 |
+
return { allowed: false, reason: 'rate_limited', resetIn: `${hours}h ${mins}m`, limit };
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return { allowed: true, remaining: limit - count, limit };
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Start a new drop session for the owner.
|
| 48 |
+
*/
|
| 49 |
+
function startDropSession(userId) {
|
| 50 |
+
const session = {
|
| 51 |
+
step: 'category',
|
| 52 |
+
category: null,
|
| 53 |
+
title: null,
|
| 54 |
+
file: null, // { url, name, size }
|
| 55 |
+
warnings: null,
|
| 56 |
+
status: null, // 'checked' or 'unchecked'
|
| 57 |
+
about: null,
|
| 58 |
+
image: null, // url or null
|
| 59 |
+
channelId: null,
|
| 60 |
+
isExternal: false,
|
| 61 |
+
};
|
| 62 |
+
activeSessions.set(userId, session);
|
| 63 |
+
return session;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Check if a user has an active drop session.
|
| 68 |
+
*/
|
| 69 |
+
function hasSession(userId) {
|
| 70 |
+
return activeSessions.has(userId);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Get the current prompt message for the active step.
|
| 75 |
+
*/
|
| 76 |
+
function getPrompt(session) {
|
| 77 |
+
switch (session.step) {
|
| 78 |
+
case 'category':
|
| 79 |
+
return {
|
| 80 |
+
embeds: [createEmbed({
|
| 81 |
+
title: '📦 New Drop — Step 1/8',
|
| 82 |
+
description: '> **Which category does this drop belong to?**\n\nClick a button below.',
|
| 83 |
+
color: Colors.PRIMARY,
|
| 84 |
+
footer: 'Type "cancel" at any time to abort',
|
| 85 |
+
})],
|
| 86 |
+
components: [new ActionRowBuilder().addComponents(
|
| 87 |
+
new ButtonBuilder().setCustomId('cat_sources').setLabel('Sources').setStyle(ButtonStyle.Secondary),
|
| 88 |
+
new ButtonBuilder().setCustomId('cat_cracks').setLabel('Cracks').setStyle(ButtonStyle.Secondary),
|
| 89 |
+
new ButtonBuilder().setCustomId('cat_scripts').setLabel('Scripts').setStyle(ButtonStyle.Secondary),
|
| 90 |
+
new ButtonBuilder().setCustomId('cat_tools').setLabel('Tools').setStyle(ButtonStyle.Secondary)
|
| 91 |
+
)],
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
case 'title':
|
| 95 |
+
return {
|
| 96 |
+
embeds: [createEmbed({
|
| 97 |
+
title: '📦 New Drop — Step 2/8',
|
| 98 |
+
description: '> **What is the title for this drop?**\n\nType the title below.',
|
| 99 |
+
color: Colors.PRIMARY,
|
| 100 |
+
footer: `Category: ${session.category.toUpperCase()} | Type "cancel" to abort`,
|
| 101 |
+
})],
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
case 'file':
|
| 105 |
+
return {
|
| 106 |
+
embeds: [createEmbed({
|
| 107 |
+
title: '📦 New Drop — Step 3/8',
|
| 108 |
+
description: '> **Upload the file for this drop.**\n\nAttach the file to your next message.',
|
| 109 |
+
color: Colors.PRIMARY,
|
| 110 |
+
footer: `Title: ${session.title}`,
|
| 111 |
+
})],
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
case 'warnings':
|
| 115 |
+
return {
|
| 116 |
+
embeds: [createEmbed({
|
| 117 |
+
title: '📦 New Drop — Step 4/8',
|
| 118 |
+
description: '> **Any warnings for this drop?**\n\nType warnings below, or type `none` to skip.',
|
| 119 |
+
color: Colors.WARNING,
|
| 120 |
+
footer: `Title: ${session.title}`,
|
| 121 |
+
})],
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
case 'status':
|
| 125 |
+
return {
|
| 126 |
+
embeds: [createEmbed({
|
| 127 |
+
title: '📦 New Drop — Step 5/8',
|
| 128 |
+
description: '> **Is this source checked or unchecked?**\n\nClick a button below.',
|
| 129 |
+
color: Colors.PRIMARY,
|
| 130 |
+
footer: `Title: ${session.title}`,
|
| 131 |
+
})],
|
| 132 |
+
components: [new ActionRowBuilder().addComponents(
|
| 133 |
+
new ButtonBuilder()
|
| 134 |
+
.setCustomId('drop_checked')
|
| 135 |
+
.setLabel('✅ Checked')
|
| 136 |
+
.setStyle(ButtonStyle.Success),
|
| 137 |
+
new ButtonBuilder()
|
| 138 |
+
.setCustomId('drop_unchecked')
|
| 139 |
+
.setLabel('⚠️ Unchecked')
|
| 140 |
+
.setStyle(ButtonStyle.Danger),
|
| 141 |
+
)],
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
case 'about':
|
| 145 |
+
return {
|
| 146 |
+
embeds: [createEmbed({
|
| 147 |
+
title: '📦 New Drop — Step 6/8',
|
| 148 |
+
description: '> **Describe this source.**\n\nWrite a short description about what this is.',
|
| 149 |
+
color: Colors.PRIMARY,
|
| 150 |
+
footer: `Title: ${session.title}`,
|
| 151 |
+
})],
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
case 'image':
|
| 155 |
+
return {
|
| 156 |
+
embeds: [createEmbed({
|
| 157 |
+
title: '📦 New Drop — Step 7/8',
|
| 158 |
+
description: '> **Reference image?**\n\nUpload an image or type `none` to skip.',
|
| 159 |
+
color: Colors.PRIMARY,
|
| 160 |
+
footer: `Title: ${session.title}`,
|
| 161 |
+
})],
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
default:
|
| 165 |
+
return null;
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Build the final preview embed for the drop.
|
| 171 |
+
*/
|
| 172 |
+
function buildDropEmbed(session) {
|
| 173 |
+
const statusIcon = session.status === 'checked' ? '✅' : '⚠️';
|
| 174 |
+
const statusText = session.status === 'checked' ? 'Verified / Checked' : 'Unchecked — Use at your own risk';
|
| 175 |
+
|
| 176 |
+
const lines = [
|
| 177 |
+
`${statusIcon} **${statusText}**`,
|
| 178 |
+
'',
|
| 179 |
+
'> **About**',
|
| 180 |
+
`> ${session.about}`,
|
| 181 |
+
'',
|
| 182 |
+
];
|
| 183 |
+
|
| 184 |
+
if (session.warnings && session.warnings.toLowerCase() !== 'none') {
|
| 185 |
+
lines.push('⚠️ **Warnings**');
|
| 186 |
+
lines.push(`> ${session.warnings}`);
|
| 187 |
+
lines.push('');
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
lines.push('```');
|
| 191 |
+
lines.push(`📁 File: ${session.file.name}`);
|
| 192 |
+
lines.push(`📊 Size: ${session.file.size !== 'Unknown' ? formatBytes(session.file.size) : 'External File (Unknown Size)'}`);
|
| 193 |
+
lines.push(`🔎 Status: ${statusText}`);
|
| 194 |
+
lines.push('```');
|
| 195 |
+
lines.push('');
|
| 196 |
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
| 197 |
+
lines.push('*Wyvern Softworks — Educational Use Only*');
|
| 198 |
+
|
| 199 |
+
const embedData = {
|
| 200 |
+
title: `📦 [${session.category.toUpperCase()}] ${session.title}`,
|
| 201 |
+
description: lines.join('\n'),
|
| 202 |
+
color: session.status === 'checked' ? Colors.SUCCESS : Colors.WARNING,
|
| 203 |
+
footer: 'Wyvern Softworks • ' + new Date().toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }),
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
if (session.image) {
|
| 207 |
+
// Handle both object-style (from re-upload) and string-style (from direct URL/preview)
|
| 208 |
+
embedData.image = typeof session.image === 'object' ? session.image.url : session.image;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
return createEmbed(embedData);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
/**
|
| 215 |
+
* Build the download button row for a drop.
|
| 216 |
+
*/
|
| 217 |
+
function buildDownloadButton(session) {
|
| 218 |
+
return new ActionRowBuilder().addComponents(
|
| 219 |
+
new ButtonBuilder()
|
| 220 |
+
.setLabel('📥 Download')
|
| 221 |
+
.setStyle(ButtonStyle.Link)
|
| 222 |
+
.setURL(session.file.url),
|
| 223 |
+
);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
/**
|
| 227 |
+
* Handle a message in an active drop session.
|
| 228 |
+
* Returns true if handled, false if not.
|
| 229 |
+
*/
|
| 230 |
+
async function handleDropMessage(message) {
|
| 231 |
+
const userId = message.author.id;
|
| 232 |
+
const session = activeSessions.get(userId);
|
| 233 |
+
if (!session) return false;
|
| 234 |
+
|
| 235 |
+
const content = message.content.trim();
|
| 236 |
+
|
| 237 |
+
// Cancel
|
| 238 |
+
if (content.toLowerCase() === 'cancel') {
|
| 239 |
+
activeSessions.delete(userId);
|
| 240 |
+
await message.reply({ embeds: [createEmbed({ title: '❌ Drop Cancelled', color: Colors.ACCENT })] });
|
| 241 |
+
return true;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
switch (session.step) {
|
| 245 |
+
case 'title':
|
| 246 |
+
session.title = content;
|
| 247 |
+
session.step = 'file';
|
| 248 |
+
await message.reply(getPrompt(session));
|
| 249 |
+
return true;
|
| 250 |
+
|
| 251 |
+
case 'file':
|
| 252 |
+
if (message.attachments.size === 0) {
|
| 253 |
+
// Check if it's a URL
|
| 254 |
+
if (content.startsWith('http')) {
|
| 255 |
+
const urlParts = new URL(content);
|
| 256 |
+
const filename = urlParts.pathname.split('/').pop() || `file_${Date.now()}`;
|
| 257 |
+
session.file = {
|
| 258 |
+
url: content,
|
| 259 |
+
name: filename,
|
| 260 |
+
size: 'Unknown',
|
| 261 |
+
isExternal: true
|
| 262 |
+
};
|
| 263 |
+
session.step = 'warnings';
|
| 264 |
+
await message.reply(getPrompt(session));
|
| 265 |
+
return true;
|
| 266 |
+
}
|
| 267 |
+
await message.reply({ content: '❌ Please attach a file or provide a direct link (Mega, MediaFire, etc).' });
|
| 268 |
+
return true;
|
| 269 |
+
}
|
| 270 |
+
const att = message.attachments.first();
|
| 271 |
+
// Store the full attachment object to re-upload it later
|
| 272 |
+
session.file = {
|
| 273 |
+
url: att.url,
|
| 274 |
+
name: att.name,
|
| 275 |
+
size: att.size,
|
| 276 |
+
attachment: att, // Save reference to original attachment
|
| 277 |
+
isExternal: false
|
| 278 |
+
};
|
| 279 |
+
session.step = 'warnings';
|
| 280 |
+
await message.reply(getPrompt(session));
|
| 281 |
+
return true;
|
| 282 |
+
|
| 283 |
+
case 'warnings':
|
| 284 |
+
session.warnings = content.toLowerCase() === 'none' ? null : content;
|
| 285 |
+
session.step = 'status';
|
| 286 |
+
await message.reply(getPrompt(session));
|
| 287 |
+
return true;
|
| 288 |
+
|
| 289 |
+
case 'about':
|
| 290 |
+
session.about = content;
|
| 291 |
+
session.step = 'image';
|
| 292 |
+
await message.reply(getPrompt(session));
|
| 293 |
+
return true;
|
| 294 |
+
|
| 295 |
+
case 'image':
|
| 296 |
+
if (content.toLowerCase() === 'none') {
|
| 297 |
+
session.image = null;
|
| 298 |
+
} else if (message.attachments.size > 0) {
|
| 299 |
+
const imgAtt = message.attachments.first();
|
| 300 |
+
session.image = {
|
| 301 |
+
url: imgAtt.url,
|
| 302 |
+
name: imgAtt.name,
|
| 303 |
+
attachment: imgAtt
|
| 304 |
+
};
|
| 305 |
+
} else {
|
| 306 |
+
session.image = null;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Show preview
|
| 310 |
+
session.step = 'preview';
|
| 311 |
+
const previewEmbed = buildDropEmbed(session);
|
| 312 |
+
const row = new ActionRowBuilder().addComponents(
|
| 313 |
+
new ButtonBuilder()
|
| 314 |
+
.setCustomId('drop_approve')
|
| 315 |
+
.setLabel('✅ Approve & Post')
|
| 316 |
+
.setStyle(ButtonStyle.Success),
|
| 317 |
+
new ButtonBuilder()
|
| 318 |
+
.setCustomId('drop_cancel')
|
| 319 |
+
.setLabel('❌ Cancel')
|
| 320 |
+
.setStyle(ButtonStyle.Danger),
|
| 321 |
+
);
|
| 322 |
+
|
| 323 |
+
await message.reply({
|
| 324 |
+
content: '**📋 Preview — This is how the drop will look:**',
|
| 325 |
+
embeds: [previewEmbed],
|
| 326 |
+
components: [row],
|
| 327 |
+
});
|
| 328 |
+
return true;
|
| 329 |
+
|
| 330 |
+
// (case 'channel' was deliberately removed because the bot now deploys to a Website API)
|
| 331 |
+
|
| 332 |
+
default:
|
| 333 |
+
return false;
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/**
|
| 338 |
+
* Handle drop button interactions (status select, approve/cancel).
|
| 339 |
+
*/
|
| 340 |
+
async function handleDropButton(interaction) {
|
| 341 |
+
const userId = interaction.user.id;
|
| 342 |
+
const session = activeSessions.get(userId);
|
| 343 |
+
if (!session) return false;
|
| 344 |
+
|
| 345 |
+
const { customId } = interaction;
|
| 346 |
+
|
| 347 |
+
if (customId.startsWith('cat_')) {
|
| 348 |
+
session.category = customId.replace('cat_', '');
|
| 349 |
+
session.step = 'title';
|
| 350 |
+
await interaction.update({
|
| 351 |
+
embeds: [createEmbed({
|
| 352 |
+
title: `📂 Category Selected: ${session.category.toUpperCase()}`,
|
| 353 |
+
color: Colors.PRIMARY,
|
| 354 |
+
})], components: []
|
| 355 |
+
});
|
| 356 |
+
await interaction.followUp(getPrompt(session));
|
| 357 |
+
return true;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
if (customId === 'drop_checked' || customId === 'drop_unchecked') {
|
| 361 |
+
session.status = customId === 'drop_checked' ? 'checked' : 'unchecked';
|
| 362 |
+
session.step = 'about';
|
| 363 |
+
await interaction.update({
|
| 364 |
+
embeds: [createEmbed({
|
| 365 |
+
title: `${customId === 'drop_checked' ? '✅' : '⚠️'} Status: ${session.status}`,
|
| 366 |
+
color: customId === 'drop_checked' ? Colors.SUCCESS : Colors.WARNING,
|
| 367 |
+
})], components: []
|
| 368 |
+
});
|
| 369 |
+
await interaction.followUp(getPrompt(session));
|
| 370 |
+
return true;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
if (customId === 'drop_approve') {
|
| 374 |
+
await interaction.update({
|
| 375 |
+
content: '⏳ *Deploying drop to website API...*',
|
| 376 |
+
embeds: [],
|
| 377 |
+
components: []
|
| 378 |
+
});
|
| 379 |
+
|
| 380 |
+
try {
|
| 381 |
+
let assetId = null;
|
| 382 |
+
let fileUrl = session.file.url;
|
| 383 |
+
|
| 384 |
+
// 1. GitHub Proxy (if not external)
|
| 385 |
+
if (!session.file.isExternal) {
|
| 386 |
+
const octokit = new Octokit({ auth: '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' });
|
| 387 |
+
const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
|
| 388 |
+
|
| 389 |
+
// Download the file from Discord's temporary DM CDN
|
| 390 |
+
const fileRes = await fetch(session.file.url);
|
| 391 |
+
const fileBuffer = await fileRes.buffer();
|
| 392 |
+
|
| 393 |
+
// Create the release
|
| 394 |
+
const releaseTitle = `Drop: ${session.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`;
|
| 395 |
+
const release = await octokit.rest.repos.createRelease({
|
| 396 |
+
owner,
|
| 397 |
+
repo,
|
| 398 |
+
tag_name: `drop-${Date.now()}`,
|
| 399 |
+
name: releaseTitle,
|
| 400 |
+
body: `Auto-generated drop upload for WSB.\n\nDescription: ${session.about}`
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
// Upload the asset to the newly created release
|
| 404 |
+
const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
|
| 405 |
+
owner,
|
| 406 |
+
repo,
|
| 407 |
+
release_id: release.data.id,
|
| 408 |
+
name: session.file.name,
|
| 409 |
+
data: fileBuffer,
|
| 410 |
+
headers: {
|
| 411 |
+
'content-type': 'application/octet-stream',
|
| 412 |
+
'content-length': fileBuffer.length
|
| 413 |
+
}
|
| 414 |
+
});
|
| 415 |
+
|
| 416 |
+
assetId = uploadRes.data.id.toString();
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
// 2. Prepare JSON Payload
|
| 420 |
+
let finalImageUrl = session.image?.url || null;
|
| 421 |
+
|
| 422 |
+
const payload = {
|
| 423 |
+
category: session.category,
|
| 424 |
+
title: session.title,
|
| 425 |
+
description: session.about,
|
| 426 |
+
status: session.status,
|
| 427 |
+
isExternal: session.file.isExternal ? 1 : 0,
|
| 428 |
+
assetId: assetId,
|
| 429 |
+
fileUrl: fileUrl,
|
| 430 |
+
imageUrl: finalImageUrl,
|
| 431 |
+
warnings: session.warnings
|
| 432 |
+
};
|
| 433 |
+
|
| 434 |
+
console.log('[Website Drop Payload]', payload);
|
| 435 |
+
|
| 436 |
+
// 3. Mock Website API POST (we'll implement the real one later)
|
| 437 |
+
const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
|
| 438 |
+
try {
|
| 439 |
+
// Kick off request, but don't strictly require it to succeed while the site is down
|
| 440 |
+
await fetch(WEBSITE_API, {
|
| 441 |
+
method: 'POST',
|
| 442 |
+
headers: { 'Content-Type': 'application/json' },
|
| 443 |
+
body: JSON.stringify(payload)
|
| 444 |
+
}).catch(() => {});
|
| 445 |
+
} catch(e) {}
|
| 446 |
+
|
| 447 |
+
// 4. Save to Supabase Web Tracking
|
| 448 |
+
await stmts.addWebDrop(
|
| 449 |
+
userId,
|
| 450 |
+
session.category,
|
| 451 |
+
payload.title,
|
| 452 |
+
payload.description,
|
| 453 |
+
payload.status,
|
| 454 |
+
payload.isExternal,
|
| 455 |
+
payload.assetId,
|
| 456 |
+
payload.fileUrl,
|
| 457 |
+
payload.imageUrl
|
| 458 |
+
);
|
| 459 |
+
|
| 460 |
+
// Log drop for Discord rate limits
|
| 461 |
+
await stmts.logDrop(userId, `[${session.category.toUpperCase()}] ${session.title}`, 'website');
|
| 462 |
+
|
| 463 |
+
activeSessions.delete(userId);
|
| 464 |
+
|
| 465 |
+
await interaction.followUp({
|
| 466 |
+
embeds: [createEmbed({
|
| 467 |
+
title: '✅ Drop Published!',
|
| 468 |
+
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.`,
|
| 469 |
+
color: Colors.SUCCESS,
|
| 470 |
+
})],
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
} catch (err) {
|
| 474 |
+
console.error('[Web Deploy Error]', err);
|
| 475 |
+
await interaction.followUp({ content: `❌ Failed to deploy to website: ${err.message}` });
|
| 476 |
+
}
|
| 477 |
+
return true;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
if (customId === 'drop_cancel') {
|
| 481 |
+
activeSessions.delete(userId);
|
| 482 |
+
await interaction.update({
|
| 483 |
+
embeds: [createEmbed({ title: '❌ Drop Cancelled', color: Colors.ACCENT })],
|
| 484 |
+
components: [],
|
| 485 |
+
});
|
| 486 |
+
return true;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
return false;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
/**
|
| 493 |
+
* Handle download button clicks — generates a temporary download URL from the private GitHub repo.
|
| 494 |
+
* Button customId format: dl_<assetId>
|
| 495 |
+
*/
|
| 496 |
+
async function handleDownloadButton(interaction) {
|
| 497 |
+
const { customId } = interaction;
|
| 498 |
+
if (!customId.startsWith('dl_')) return false;
|
| 499 |
+
|
| 500 |
+
const assetId = customId.split('_')[1];
|
| 501 |
+
if (!assetId) return false;
|
| 502 |
+
|
| 503 |
+
try {
|
| 504 |
+
await interaction.deferReply({ ephemeral: true });
|
| 505 |
+
|
| 506 |
+
const GITHUB_TOKEN = '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';
|
| 507 |
+
const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
|
| 508 |
+
|
| 509 |
+
// Request the asset with octet-stream accept header — GitHub returns a 302 redirect
|
| 510 |
+
// to a temporary signed S3 URL that works without authentication
|
| 511 |
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/assets/${assetId}`;
|
| 512 |
+
const response = await fetch(apiUrl, {
|
| 513 |
+
headers: {
|
| 514 |
+
'Authorization': `Bearer ${GITHUB_TOKEN}`,
|
| 515 |
+
'Accept': 'application/octet-stream',
|
| 516 |
+
'User-Agent': 'WSB-Bot'
|
| 517 |
+
},
|
| 518 |
+
redirect: 'manual'
|
| 519 |
+
});
|
| 520 |
+
|
| 521 |
+
if (response.status === 302) {
|
| 522 |
+
const tempUrl = response.headers.get('location');
|
| 523 |
+
await interaction.editReply({
|
| 524 |
+
content: `📥 **Your private download link:**\n${tempUrl}\n\n> ⚠️ This link expires in a few minutes. Do **not** share it.`,
|
| 525 |
+
});
|
| 526 |
+
} else {
|
| 527 |
+
// Fallback: try following the redirect automatically
|
| 528 |
+
const directResponse = await fetch(apiUrl, {
|
| 529 |
+
headers: {
|
| 530 |
+
'Authorization': `Bearer ${GITHUB_TOKEN}`,
|
| 531 |
+
'Accept': 'application/octet-stream',
|
| 532 |
+
'User-Agent': 'WSB-Bot'
|
| 533 |
+
}
|
| 534 |
+
});
|
| 535 |
+
if (directResponse.ok) {
|
| 536 |
+
// If the URL resolved, the response URL is the temporary link
|
| 537 |
+
await interaction.editReply({
|
| 538 |
+
content: `📥 **Your private download link:**\n${directResponse.url}\n\n> ⚠️ This link expires in a few minutes. Do **not** share it.`,
|
| 539 |
+
});
|
| 540 |
+
} else {
|
| 541 |
+
await interaction.editReply({
|
| 542 |
+
content: '❌ Failed to generate download link. The file may have been deleted.',
|
| 543 |
+
});
|
| 544 |
+
}
|
| 545 |
+
}
|
| 546 |
+
return true;
|
| 547 |
+
} catch (err) {
|
| 548 |
+
console.error('[Download Button Error]', err);
|
| 549 |
+
try {
|
| 550 |
+
await interaction.editReply({
|
| 551 |
+
content: '❌ Something went wrong generating your download link. Please try again.',
|
| 552 |
+
});
|
| 553 |
+
} catch (_) { }
|
| 554 |
+
return true;
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
/**
|
| 559 |
+
* Format bytes to human readable string.
|
| 560 |
+
*/
|
| 561 |
+
function formatBytes(bytes) {
|
| 562 |
+
if (bytes === 0) return '0 B';
|
| 563 |
+
const k = 1024;
|
| 564 |
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
| 565 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 566 |
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
module.exports = {
|
| 570 |
+
startDropSession,
|
| 571 |
+
hasSession,
|
| 572 |
+
getPrompt,
|
| 573 |
+
handleDropMessage,
|
| 574 |
+
handleDropButton,
|
| 575 |
+
handleDownloadButton,
|
| 576 |
+
buildDropEmbed,
|
| 577 |
+
canDrop,
|
| 578 |
+
activeSessions,
|
| 579 |
+
};
|
src/systems/embeds.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
const { stmts } = require('../database');
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Send the disclaimer embed to ⚠️・disclaimer channel.
|
| 7 |
+
*/
|
| 8 |
+
async function sendDisclaimerEmbed(client) {
|
| 9 |
+
const row = stmts.getState.get('channel_⚠️・disclaimer');
|
| 10 |
+
if (!row) throw new Error('Disclaimer channel not found in bot state.');
|
| 11 |
+
|
| 12 |
+
const channel = await client.channels.fetch(row.value);
|
| 13 |
+
if (!channel) throw new Error('Could not fetch disclaimer channel.');
|
| 14 |
+
|
| 15 |
+
// Fetch the @@ Owner role for a proper mention
|
| 16 |
+
const guild = channel.guild;
|
| 17 |
+
const ownerRole = guild.roles.cache.find(r => r.name === '@@ Owner');
|
| 18 |
+
const ownerMention = ownerRole ? `<@&${ownerRole.id}>` : '**Server Owner**';
|
| 19 |
+
|
| 20 |
+
const embed = createEmbed({
|
| 21 |
+
title: '🔴 Disclaimer 🔴',
|
| 22 |
+
description: [
|
| 23 |
+
'> **Wyvern Softworks** and all associated contributors do **NOT** endorse or support any form of cheating, unlawful activity, or malicious conduct.\n',
|
| 24 |
+
|
| 25 |
+
'```',
|
| 26 |
+
'Our server provides source code and resources for',
|
| 27 |
+
'penetration testing tools and security research,',
|
| 28 |
+
'intended strictly for educational purposes and',
|
| 29 |
+
'authorized testing in controlled environments.',
|
| 30 |
+
'```\n',
|
| 31 |
+
|
| 32 |
+
'> All materials shared within this server are provided **as-is** for learning and development purposes only. Users are solely responsible for how they use the resources provided.\n',
|
| 33 |
+
|
| 34 |
+
'**By joining this server, you acknowledge and agree that:**',
|
| 35 |
+
'• You will use all resources **legally and ethically**',
|
| 36 |
+
'• You will **not** use any tools for unauthorized access',
|
| 37 |
+
'• You accept full responsibility for your own actions',
|
| 38 |
+
'• Wyvern Softworks bears **no liability** for misuse\n',
|
| 39 |
+
|
| 40 |
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
| 41 |
+
'> **Wyvern Softworks** is not affiliated with any game studio, publisher, or platform.',
|
| 42 |
+
`> For legal inquiries or DMCA notices, contact the server ${ownerMention}.`,
|
| 43 |
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
| 44 |
+
].join('\n'),
|
| 45 |
+
color: 0xe74c3c,
|
| 46 |
+
footer: 'Wyvern Softworks — Read before proceeding',
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
await channel.send({ embeds: [embed] });
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/**
|
| 53 |
+
* Send the rules embed to 📜・rules channel.
|
| 54 |
+
*/
|
| 55 |
+
async function sendRulesEmbed(client) {
|
| 56 |
+
const row = stmts.getState.get('channel_📜・rules');
|
| 57 |
+
if (!row) throw new Error('Rules channel not found in bot state.');
|
| 58 |
+
|
| 59 |
+
const channel = await client.channels.fetch(row.value);
|
| 60 |
+
if (!channel) throw new Error('Could not fetch rules channel.');
|
| 61 |
+
|
| 62 |
+
const embed = createEmbed({
|
| 63 |
+
title: '📜 Server Rules',
|
| 64 |
+
description: [
|
| 65 |
+
'> Welcome to **Wyvern Softworks**. To maintain a safe and productive community, all members must follow these rules.\n',
|
| 66 |
+
|
| 67 |
+
'**1.** 🤝 **Respect Everyone** — Treat all members with respect. No harassment, hate speech, racism, sexism, or personal attacks.',
|
| 68 |
+
'**2.** 🚫 **No Spam** — Do not spam messages, emojis, images, or links in any channel.',
|
| 69 |
+
'**3.** 🔇 **No NSFW Content** — Absolutely no NSFW, gore, or disturbing content anywhere in the server.',
|
| 70 |
+
'**4.** 📛 **Appropriate Names** — Keep your username and nickname clean and appropriate.',
|
| 71 |
+
'**5.** 📢 **No Self-Promotion** — Do not advertise other servers, products, or services without explicit permission from Staff.',
|
| 72 |
+
'**6.** 🧠 **Use Channels Correctly** — Post content in the appropriate channels. Off-topic messages will be removed.',
|
| 73 |
+
'**7.** 🔒 **No Leaking** — Do not share, redistribute, or leak any paid or exclusive content from this server.',
|
| 74 |
+
'**8.** ⚖️ **No Illegal Activity** — Do not share, promote, or discuss anything that violates local or international law.',
|
| 75 |
+
'**9.** 🛡️ **No Cheating Encouragement** — We provide pentesting tools for educational purposes only. Do not encourage or discuss using tools for cheating in live environments.',
|
| 76 |
+
'**10.** 🤖 **No Alt Accounts** — Using alternate accounts to bypass bans or restrictions will result in a permanent ban.',
|
| 77 |
+
'**11.** 👮 **Listen to Staff** — Staff decisions are final. If you disagree, open a ticket to appeal.',
|
| 78 |
+
'**12.** 🎫 **Ticket System** — Use the ticket system for support. Do not DM staff directly unless asked.',
|
| 79 |
+
'**13.** 🔊 **Voice Chat Etiquette** — No mic spamming, soundboards, or disruptive audio in voice channels.',
|
| 80 |
+
'**14.** 💬 **English Only** — Please communicate in English in all public channels.',
|
| 81 |
+
'**15.** 📎 **No Malicious Files** — Do not upload malware, viruses, IP loggers, or any harmful files.',
|
| 82 |
+
'**16.** 📖 **Follow Discord ToS** — All members must abide by Discord\'s Terms of Service and Community Guidelines at all times.\n',
|
| 83 |
+
|
| 84 |
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
| 85 |
+
'> **Violating any of the above rules may result in a mute, kick, or permanent ban at the discretion of Staff.**\n',
|
| 86 |
+
|
| 87 |
+
'🔗 **Discord Terms of Service**',
|
| 88 |
+
'[Terms of Service](https://discord.com/terms)',
|
| 89 |
+
'[Community Guidelines](https://discord.com/guidelines)',
|
| 90 |
+
'[Privacy Policy](https://discord.com/privacy)',
|
| 91 |
+
].join('\n'),
|
| 92 |
+
color: 0x9b59b6,
|
| 93 |
+
footer: 'Wyvern Softworks — Last updated ' + new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' }),
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
await channel.send({ embeds: [embed] });
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
module.exports = { sendDisclaimerEmbed, sendRulesEmbed };
|
src/systems/logger.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { stmts } = require('../database');
|
| 2 |
+
const { logEmbed } = require('../utils/embeds');
|
| 3 |
+
const { Colors } = require('../config');
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Log an action to the staff-logs channel.
|
| 7 |
+
*/
|
| 8 |
+
async function log(client, { title, description, fields = [], color = Colors.MUTED }) {
|
| 9 |
+
try {
|
| 10 |
+
const row = await stmts.getState('channel_staff-logs');
|
| 11 |
+
if (!row) return;
|
| 12 |
+
|
| 13 |
+
const channelId = row;
|
| 14 |
+
const channel = await client.channels.fetch(channelId).catch(() => null);
|
| 15 |
+
if (!channel) return;
|
| 16 |
+
|
| 17 |
+
const embed = logEmbed({ title, description, color, fields });
|
| 18 |
+
await channel.send({ embeds: [embed] });
|
| 19 |
+
} catch (err) {
|
| 20 |
+
console.error('[Logger] Failed to log:', err.message);
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Convenience wrappers
|
| 25 |
+
async function logVerification(client, user, action) {
|
| 26 |
+
await stmts.logVerification(user.id, user.tag, action);
|
| 27 |
+
await log(client, {
|
| 28 |
+
title: '🔐 Verification',
|
| 29 |
+
description: `**${user.tag}** (${user.id})`,
|
| 30 |
+
fields: [{ name: 'Action', value: action, inline: true }],
|
| 31 |
+
color: action === 'verified' ? Colors.SUCCESS : Colors.WARNING,
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
async function logTicket(client, { user, action, channelName }) {
|
| 36 |
+
await log(client, {
|
| 37 |
+
title: '🎫 Ticket',
|
| 38 |
+
description: `**${user.tag}** (${user.id})`,
|
| 39 |
+
fields: [
|
| 40 |
+
{ name: 'Action', value: action, inline: true },
|
| 41 |
+
{ name: 'Channel', value: channelName || 'N/A', inline: true },
|
| 42 |
+
],
|
| 43 |
+
color: Colors.INFO,
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
async function logCommand(client, command) {
|
| 48 |
+
await log(client, {
|
| 49 |
+
title: '⚙️ Command Executed',
|
| 50 |
+
description: `\`${command}\``,
|
| 51 |
+
fields: [{ name: 'Executed by', value: 'Owner (DM)', inline: true }],
|
| 52 |
+
color: Colors.PRIMARY,
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
async function logRoleChange(client, { user, role, action }) {
|
| 57 |
+
await log(client, {
|
| 58 |
+
title: '👤 Role Change',
|
| 59 |
+
description: `**${user.tag}** (${user.id})`,
|
| 60 |
+
fields: [
|
| 61 |
+
{ name: 'Role', value: role, inline: true },
|
| 62 |
+
{ name: 'Action', value: action, inline: true },
|
| 63 |
+
],
|
| 64 |
+
color: action === 'added' ? Colors.SUCCESS : Colors.WARNING,
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
module.exports = { log, logVerification, logTicket, logCommand, logRoleChange };
|
src/systems/massdrop.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } = require('discord.js');
|
| 2 |
+
const { createEmbed, errorEmbed } = require('../utils/embeds');
|
| 3 |
+
const { Colors } = require('../config');
|
| 4 |
+
const { Octokit } = require('@octokit/rest');
|
| 5 |
+
const fetch = require('node-fetch');
|
| 6 |
+
const { stmts } = require('../database');
|
| 7 |
+
const { buildDropEmbed } = require('./drops');
|
| 8 |
+
|
| 9 |
+
// Holds active mass drop sessions
|
| 10 |
+
// Map structure: userId => { step, config: { status, about }, files: [] }
|
| 11 |
+
const massDropSessions = new Map();
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Initializes a new mass drop session for the user
|
| 15 |
+
* Starts at the 'config' step where they set the default About text.
|
| 16 |
+
*/
|
| 17 |
+
function startMassDropSession(userId) {
|
| 18 |
+
const session = {
|
| 19 |
+
step: 'config_status',
|
| 20 |
+
config: {
|
| 21 |
+
status: 'unchecked',
|
| 22 |
+
about: 'enjoy the drop :))'
|
| 23 |
+
},
|
| 24 |
+
files: [] // Array of { title, type, url, description, status, name }
|
| 25 |
+
};
|
| 26 |
+
massDropSessions.set(userId, session);
|
| 27 |
+
return session;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Checks if user is in an active mass drop session
|
| 32 |
+
*/
|
| 33 |
+
function hasMassSession(userId) {
|
| 34 |
+
return massDropSessions.has(userId);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Generates the UI prompt based on the current step of the mass drop session
|
| 39 |
+
*/
|
| 40 |
+
function getMassPrompt(session) {
|
| 41 |
+
switch (session.step) {
|
| 42 |
+
case 'config_status':
|
| 43 |
+
return {
|
| 44 |
+
embeds: [createEmbed({
|
| 45 |
+
title: '📦 Mass Drop Initialization',
|
| 46 |
+
description: 'Let\'s set up the defaults for this batch of drops.\n\nFirst, what should the **Default Verification Status** be for all files?',
|
| 47 |
+
color: Colors.PRIMARY
|
| 48 |
+
})],
|
| 49 |
+
components: [
|
| 50 |
+
new ActionRowBuilder().addComponents(
|
| 51 |
+
new ButtonBuilder().setCustomId('mass_checked').setLabel('Verified / Checked').setStyle(ButtonStyle.Success).setEmoji('✅'),
|
| 52 |
+
new ButtonBuilder().setCustomId('mass_unchecked').setLabel('Unchecked / Unknown').setStyle(ButtonStyle.Secondary).setEmoji('❓'),
|
| 53 |
+
new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel Mass Drop').setStyle(ButtonStyle.Danger)
|
| 54 |
+
)
|
| 55 |
+
]
|
| 56 |
+
};
|
| 57 |
+
case 'config_about':
|
| 58 |
+
return {
|
| 59 |
+
embeds: [createEmbed({
|
| 60 |
+
title: '📝 Set Default Description',
|
| 61 |
+
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 :))")*`,
|
| 62 |
+
color: Colors.PRIMARY
|
| 63 |
+
})],
|
| 64 |
+
components: [
|
| 65 |
+
new ActionRowBuilder().addComponents(
|
| 66 |
+
new ButtonBuilder().setCustomId('mass_skip_about').setLabel('Use Default ("enjoy the drop :))")').setStyle(ButtonStyle.Secondary),
|
| 67 |
+
new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger)
|
| 68 |
+
)
|
| 69 |
+
]
|
| 70 |
+
};
|
| 71 |
+
case 'listening':
|
| 72 |
+
return {
|
| 73 |
+
embeds: [createEmbed({
|
| 74 |
+
title: '📥 Listening for Drops',
|
| 75 |
+
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**.`,
|
| 76 |
+
color: Colors.SUCCESS
|
| 77 |
+
})],
|
| 78 |
+
components: [
|
| 79 |
+
new ActionRowBuilder().addComponents(
|
| 80 |
+
new ButtonBuilder().setCustomId('mass_finish').setLabel('Finish Uploading').setStyle(ButtonStyle.Primary).setEmoji('📦'),
|
| 81 |
+
new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger)
|
| 82 |
+
)
|
| 83 |
+
]
|
| 84 |
+
};
|
| 85 |
+
// The dashboard step is handled dynamically during interactions
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Generates the unified Mass Drop Dashboard summarizing all files,
|
| 91 |
+
* along with the dropdown menu to edit individual items.
|
| 92 |
+
*/
|
| 93 |
+
function generateDashboard(session) {
|
| 94 |
+
if (session.files.length === 0) {
|
| 95 |
+
return {
|
| 96 |
+
embeds: [createEmbed({
|
| 97 |
+
title: '❌ Empty Batch',
|
| 98 |
+
description: 'You did not upload any files. The mass drop session has been cancelled.',
|
| 99 |
+
color: Colors.WARNING
|
| 100 |
+
})],
|
| 101 |
+
components: []
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
let descriptionObj = '### 📦 Pending Drops\n\n';
|
| 106 |
+
session.files.forEach((file, index) => {
|
| 107 |
+
descriptionObj += `**${index + 1}.** [${file.status === 'checked' ? '✅' : '❓'}] **${file.title}**\n`;
|
| 108 |
+
});
|
| 109 |
+
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.`;
|
| 110 |
+
|
| 111 |
+
const embed = createEmbed({
|
| 112 |
+
title: '🎛️ Mass Drop Dashboard',
|
| 113 |
+
description: descriptionObj,
|
| 114 |
+
color: Colors.PRIMARY
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
// Create the dropdown menu for editing
|
| 118 |
+
const options = session.files.map((file, index) => {
|
| 119 |
+
return new StringSelectMenuOptionBuilder()
|
| 120 |
+
.setLabel(`${index + 1}. ${file.title.substring(0, 50)}`)
|
| 121 |
+
.setDescription(`Status: ${file.status}`)
|
| 122 |
+
.setValue(`mass_edit_${index}`)
|
| 123 |
+
// Emoji optional: .setEmoji(file.status === 'checked' ? '✅' : '❓')
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
// Discord limits dropdowns to 25 options per exact menu.
|
| 127 |
+
// If they drop >25, we'll slice it for the UI to prevent a crash,
|
| 128 |
+
// though the core engine still processes all 50+.
|
| 129 |
+
const safeOptions = options.slice(0, 25);
|
| 130 |
+
|
| 131 |
+
const components = [];
|
| 132 |
+
|
| 133 |
+
if (safeOptions.length > 0) {
|
| 134 |
+
components.push(
|
| 135 |
+
new ActionRowBuilder().addComponents(
|
| 136 |
+
new StringSelectMenuBuilder()
|
| 137 |
+
.setCustomId('mass_select_edit')
|
| 138 |
+
.setPlaceholder('✏️ Select a drop to edit text...')
|
| 139 |
+
.addOptions(safeOptions)
|
| 140 |
+
)
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
components.push(
|
| 144 |
+
new ActionRowBuilder().addComponents(
|
| 145 |
+
new StringSelectMenuBuilder()
|
| 146 |
+
.setCustomId('mass_select_image')
|
| 147 |
+
.setPlaceholder('🖼️ Select a drop to attach an image...')
|
| 148 |
+
.addOptions(safeOptions)
|
| 149 |
+
)
|
| 150 |
+
);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
components.push(
|
| 154 |
+
new ActionRowBuilder().addComponents(
|
| 155 |
+
new ButtonBuilder().setCustomId('mass_deploy').setLabel(`Deploy ${session.files.length} Drops`).setStyle(ButtonStyle.Success).setEmoji('🚀'),
|
| 156 |
+
new ButtonBuilder().setCustomId('mass_cancel').setLabel('Cancel All').setStyle(ButtonStyle.Danger)
|
| 157 |
+
)
|
| 158 |
+
);
|
| 159 |
+
|
| 160 |
+
return { embeds: [embed], components };
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/**
|
| 164 |
+
* Handles incoming messages while a mass drop session is active
|
| 165 |
+
*/
|
| 166 |
+
async function handleMassDropMessage(message) {
|
| 167 |
+
const userId = message.author.id;
|
| 168 |
+
const session = massDropSessions.get(userId);
|
| 169 |
+
if (!session) return false;
|
| 170 |
+
|
| 171 |
+
// We only care about text messages if we're in the config_about step.
|
| 172 |
+
// Otherwise, we only care about attachments in the listening step.
|
| 173 |
+
|
| 174 |
+
if (session.step === 'config_about') {
|
| 175 |
+
const content = message.content.trim();
|
| 176 |
+
if (content) {
|
| 177 |
+
session.config.about = content;
|
| 178 |
+
session.step = 'listening';
|
| 179 |
+
await message.reply(getMassPrompt(session));
|
| 180 |
+
} else {
|
| 181 |
+
// Ignored, must be text
|
| 182 |
+
return true;
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
else if (session.step === 'listening') {
|
| 186 |
+
// Collect any attachments dropped
|
| 187 |
+
if (message.attachments.size > 0) {
|
| 188 |
+
// First, see if there's a main file and an image file in this single message
|
| 189 |
+
let mainFile = null;
|
| 190 |
+
let imageFile = null;
|
| 191 |
+
|
| 192 |
+
message.attachments.forEach(attachment => {
|
| 193 |
+
const type = attachment.contentType || '';
|
| 194 |
+
if (type.startsWith('image/')) {
|
| 195 |
+
if (!imageFile) imageFile = attachment;
|
| 196 |
+
} else {
|
| 197 |
+
if (!mainFile) mainFile = attachment;
|
| 198 |
+
}
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
// If there's NO main file, but there IS an image, treat the image AS the main file
|
| 202 |
+
if (!mainFile && imageFile) {
|
| 203 |
+
mainFile = imageFile;
|
| 204 |
+
imageFile = null;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
if (mainFile) {
|
| 208 |
+
session.files.push({
|
| 209 |
+
title: mainFile.name,
|
| 210 |
+
name: mainFile.name,
|
| 211 |
+
url: mainFile.url,
|
| 212 |
+
size: mainFile.size,
|
| 213 |
+
description: session.config.about,
|
| 214 |
+
status: session.config.status,
|
| 215 |
+
isExternal: false,
|
| 216 |
+
imageUrl: imageFile ? imageFile.url : null
|
| 217 |
+
});
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Brief confirmation logic
|
| 221 |
+
await message.react('✅').catch(() => { });
|
| 222 |
+
} else {
|
| 223 |
+
// If they just type text while listening, we might want to allow external links
|
| 224 |
+
const content = message.content.trim();
|
| 225 |
+
if (content.startsWith('http')) {
|
| 226 |
+
const urlParts = new URL(content);
|
| 227 |
+
const filename = urlParts.pathname.split('/').pop() || `file_${Date.now()}`;
|
| 228 |
+
|
| 229 |
+
session.files.push({
|
| 230 |
+
title: filename,
|
| 231 |
+
name: filename,
|
| 232 |
+
url: content,
|
| 233 |
+
size: 0,
|
| 234 |
+
description: session.config.about,
|
| 235 |
+
status: session.config.status,
|
| 236 |
+
isExternal: true
|
| 237 |
+
});
|
| 238 |
+
await message.react('🔗').catch(() => { });
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
else if (session.step === 'waiting_image') {
|
| 243 |
+
// Stop waiting if they click cancel (handled in interactions) or upload an image
|
| 244 |
+
if (message.attachments.size > 0) {
|
| 245 |
+
const imageAttachment = message.attachments.find(a => (a.contentType || '').startsWith('image/'));
|
| 246 |
+
if (imageAttachment) {
|
| 247 |
+
const targetIndex = session.pendingImageIndex;
|
| 248 |
+
if (targetIndex !== undefined && session.files[targetIndex]) {
|
| 249 |
+
session.files[targetIndex].imageUrl = imageAttachment.url;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Return to dashboard
|
| 253 |
+
session.step = 'dashboard';
|
| 254 |
+
session.pendingImageIndex = null;
|
| 255 |
+
|
| 256 |
+
await message.react('🖼️').catch(() => { });
|
| 257 |
+
await message.reply(generateDashboard(session));
|
| 258 |
+
} else {
|
| 259 |
+
await message.reply({ content: '⚠️ Please attach a valid image file, or click Cancel on the dashboard to abort.', ephemeral: true });
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
else if (session.step === 'deploying') {
|
| 264 |
+
const processingMsg = await message.reply({ content: `⏳ *Deploying **${session.files.length}** drops to website API...*\n> This may take a while depending on file sizes.` });
|
| 265 |
+
|
| 266 |
+
try {
|
| 267 |
+
const octokit = new Octokit({ auth: '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' });
|
| 268 |
+
const [owner, repo] = 'APRK01/WSB-Storage'.split('/');
|
| 269 |
+
|
| 270 |
+
let successCount = 0;
|
| 271 |
+
let failCount = 0;
|
| 272 |
+
|
| 273 |
+
const WEBSITE_API = process.env.WEBSITE_API_URL || 'http://localhost:3000/api/drops';
|
| 274 |
+
|
| 275 |
+
for (const fileConf of session.files) {
|
| 276 |
+
try {
|
| 277 |
+
let assetId = null;
|
| 278 |
+
let fileUrl = fileConf.url;
|
| 279 |
+
|
| 280 |
+
// 1. GitHub Proxy (if not external)
|
| 281 |
+
if (!fileConf.isExternal) {
|
| 282 |
+
const fileRes = await fetch(fileConf.url);
|
| 283 |
+
const fileBuffer = await fileRes.buffer();
|
| 284 |
+
|
| 285 |
+
const releaseTitle = `Drop: ${fileConf.title.replace(/[^a-zA-Z0-9 -]/g, '')} - ${Date.now()}`;
|
| 286 |
+
const release = await octokit.rest.repos.createRelease({
|
| 287 |
+
owner,
|
| 288 |
+
repo,
|
| 289 |
+
tag_name: `drop-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
| 290 |
+
name: releaseTitle,
|
| 291 |
+
body: `Auto-generated drop upload for WSB.\n\nDescription: ${fileConf.description}`
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
+
const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
|
| 295 |
+
owner,
|
| 296 |
+
repo,
|
| 297 |
+
release_id: release.data.id,
|
| 298 |
+
name: fileConf.name,
|
| 299 |
+
data: fileBuffer,
|
| 300 |
+
headers: {
|
| 301 |
+
'content-type': 'application/octet-stream',
|
| 302 |
+
'content-length': fileBuffer.length
|
| 303 |
+
}
|
| 304 |
+
});
|
| 305 |
+
|
| 306 |
+
assetId = uploadRes.data.id.toString();
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// 2. Prepare JSON Payload
|
| 310 |
+
const payload = {
|
| 311 |
+
title: fileConf.title,
|
| 312 |
+
description: fileConf.description,
|
| 313 |
+
status: fileConf.status,
|
| 314 |
+
isExternal: fileConf.isExternal ? 1 : 0,
|
| 315 |
+
assetId: assetId,
|
| 316 |
+
fileUrl: fileUrl,
|
| 317 |
+
imageUrl: fileConf.imageUrl || null
|
| 318 |
+
};
|
| 319 |
+
|
| 320 |
+
console.log(`[Mass Drop -> Web payload]`, payload);
|
| 321 |
+
|
| 322 |
+
// 3. Mock Website API POST
|
| 323 |
+
try {
|
| 324 |
+
await fetch(WEBSITE_API, {
|
| 325 |
+
method: 'POST',
|
| 326 |
+
headers: { 'Content-Type': 'application/json' },
|
| 327 |
+
body: JSON.stringify(payload)
|
| 328 |
+
}).catch(() => {});
|
| 329 |
+
} catch(e) {}
|
| 330 |
+
|
| 331 |
+
// 4. Save to Supabase Web Tracking
|
| 332 |
+
await stmts.addWebDrop(
|
| 333 |
+
userId,
|
| 334 |
+
'sources', // Defaulting to sources for mass drop for now, or use from config
|
| 335 |
+
payload.title,
|
| 336 |
+
payload.description,
|
| 337 |
+
payload.status,
|
| 338 |
+
payload.isExternal,
|
| 339 |
+
payload.assetId,
|
| 340 |
+
payload.fileUrl,
|
| 341 |
+
payload.imageUrl
|
| 342 |
+
);
|
| 343 |
+
|
| 344 |
+
successCount++;
|
| 345 |
+
await stmts.logDrop(userId, fileConf.title, 'website-mass');
|
| 346 |
+
|
| 347 |
+
} catch (pushErr) {
|
| 348 |
+
console.error(`[Mass Drop Deploy Error] ${fileConf.title}:`, pushErr);
|
| 349 |
+
failCount++;
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
activeSessions = require('./drops').activeSessions;
|
| 354 |
+
activeSessions?.delete(userId); // Safety catch
|
| 355 |
+
massDropSessions.delete(userId);
|
| 356 |
+
|
| 357 |
+
await processingMsg.edit({
|
| 358 |
+
content: `✅ **Mass Web Deployment Complete!**\n> Successfully deployed: **${successCount}** files to the Website Database.\n> Failed: **${failCount}** files.`
|
| 359 |
+
});
|
| 360 |
+
|
| 361 |
+
} catch (err) {
|
| 362 |
+
await message.reply({ content: `❌ Failed deployment loop: ${err.message}` });
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
return true; // We consumed the message
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/**
|
| 370 |
+
* Handle all interactive components (buttons, select menus, modals)
|
| 371 |
+
* related to the Mass Drop system.
|
| 372 |
+
*/
|
| 373 |
+
async function handleMassDropInteraction(interaction) {
|
| 374 |
+
const userId = interaction.user.id;
|
| 375 |
+
const session = massDropSessions.get(userId);
|
| 376 |
+
if (!session) return false;
|
| 377 |
+
|
| 378 |
+
// ── BUTTONS ──
|
| 379 |
+
if (interaction.isButton()) {
|
| 380 |
+
const { customId } = interaction;
|
| 381 |
+
|
| 382 |
+
if (customId === 'mass_cancel') {
|
| 383 |
+
massDropSessions.delete(userId);
|
| 384 |
+
await interaction.update({
|
| 385 |
+
embeds: [createEmbed({ title: '❌ Mass Drop Cancelled', color: Colors.ACCENT })],
|
| 386 |
+
components: [],
|
| 387 |
+
});
|
| 388 |
+
return true;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
if (customId === 'mass_checked' || customId === 'mass_unchecked') {
|
| 392 |
+
session.config.status = customId === 'mass_checked' ? 'checked' : 'unchecked';
|
| 393 |
+
session.step = 'config_about';
|
| 394 |
+
await interaction.update(getMassPrompt(session));
|
| 395 |
+
return true;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
if (customId === 'mass_skip_about') {
|
| 399 |
+
session.step = 'listening';
|
| 400 |
+
await interaction.update(getMassPrompt(session));
|
| 401 |
+
return true;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
if (customId === 'mass_finish') {
|
| 405 |
+
session.step = 'dashboard';
|
| 406 |
+
await interaction.update(generateDashboard(session));
|
| 407 |
+
return true;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
if (customId === 'mass_cancel_image') {
|
| 411 |
+
session.step = 'dashboard';
|
| 412 |
+
session.pendingImageIndex = null;
|
| 413 |
+
await interaction.update(generateDashboard(session));
|
| 414 |
+
return true;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
if (customId === 'mass_deploy') {
|
| 418 |
+
session.step = 'deploying';
|
| 419 |
+
await interaction.update({
|
| 420 |
+
embeds: [createEmbed({
|
| 421 |
+
title: '🚀 Ready to Deploy',
|
| 422 |
+
description: `> Type **confirm** to deploy these **${session.files.length}** drops to the Website Database.`,
|
| 423 |
+
color: Colors.INFO
|
| 424 |
+
})],
|
| 425 |
+
components: []
|
| 426 |
+
});
|
| 427 |
+
return true;
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
// ── DROPDOWN MENUS ──
|
| 432 |
+
if (interaction.isStringSelectMenu() && interaction.customId === 'mass_select_edit') {
|
| 433 |
+
const selectedValue = interaction.values[0]; // e.g. mass_edit_0
|
| 434 |
+
const itemIndex = parseInt(selectedValue.split('_').pop(), 10);
|
| 435 |
+
const file = session.files[itemIndex];
|
| 436 |
+
|
| 437 |
+
// Pop up a modal for the user to edit Title/Description
|
| 438 |
+
const modal = new ModalBuilder()
|
| 439 |
+
.setCustomId(`mass_modal_${itemIndex}`)
|
| 440 |
+
.setTitle(`Edit Drop #${itemIndex + 1}`);
|
| 441 |
+
|
| 442 |
+
const titleInput = new TextInputBuilder()
|
| 443 |
+
.setCustomId('edit_title')
|
| 444 |
+
.setLabel('Drop Title')
|
| 445 |
+
.setStyle(TextInputStyle.Short)
|
| 446 |
+
.setRequired(true)
|
| 447 |
+
.setValue(file.title);
|
| 448 |
+
|
| 449 |
+
const descInput = new TextInputBuilder()
|
| 450 |
+
.setCustomId('edit_desc')
|
| 451 |
+
.setLabel('Description / About')
|
| 452 |
+
.setStyle(TextInputStyle.Paragraph)
|
| 453 |
+
.setRequired(false)
|
| 454 |
+
.setValue(file.description);
|
| 455 |
+
|
| 456 |
+
// Status Toggle (Since modals don't support checkboxes yet,
|
| 457 |
+
// we use a short text input that accepts yes/no, verified/unverified, etc)
|
| 458 |
+
const statusInput = new TextInputBuilder()
|
| 459 |
+
.setCustomId('edit_status')
|
| 460 |
+
.setLabel('Status (checked/unchecked)')
|
| 461 |
+
.setStyle(TextInputStyle.Short)
|
| 462 |
+
.setRequired(true)
|
| 463 |
+
.setValue(file.status);
|
| 464 |
+
|
| 465 |
+
modal.addComponents(
|
| 466 |
+
new ActionRowBuilder().addComponents(titleInput),
|
| 467 |
+
new ActionRowBuilder().addComponents(statusInput),
|
| 468 |
+
new ActionRowBuilder().addComponents(descInput)
|
| 469 |
+
);
|
| 470 |
+
|
| 471 |
+
await interaction.showModal(modal);
|
| 472 |
+
return true;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
if (interaction.isStringSelectMenu() && interaction.customId === 'mass_select_image') {
|
| 476 |
+
const selectedValue = interaction.values[0]; // e.g. mass_edit_0
|
| 477 |
+
const itemIndex = parseInt(selectedValue.split('_').pop(), 10);
|
| 478 |
+
const file = session.files[itemIndex];
|
| 479 |
+
|
| 480 |
+
// Put session into waiting state for this specific index
|
| 481 |
+
session.step = 'waiting_image';
|
| 482 |
+
session.pendingImageIndex = itemIndex;
|
| 483 |
+
|
| 484 |
+
await interaction.update({
|
| 485 |
+
embeds: [createEmbed({
|
| 486 |
+
title: '🖼️ Attach Image',
|
| 487 |
+
description: `> Drop the image file here to attach it to **${file.title}**.\n\n*Dashboard will automatically reload once received.*`,
|
| 488 |
+
color: Colors.INFO
|
| 489 |
+
})],
|
| 490 |
+
components: [
|
| 491 |
+
new ActionRowBuilder().addComponents(
|
| 492 |
+
new ButtonBuilder().setCustomId('mass_cancel_image').setLabel('Cancel Attachment').setStyle(ButtonStyle.Secondary)
|
| 493 |
+
)
|
| 494 |
+
]
|
| 495 |
+
});
|
| 496 |
+
return true;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// ── MODAL SUBMISSIONS ──
|
| 500 |
+
if (interaction.isModalSubmit() && interaction.customId.startsWith('mass_modal_')) {
|
| 501 |
+
const itemIndex = parseInt(interaction.customId.split('_').pop(), 10);
|
| 502 |
+
|
| 503 |
+
const newTitle = interaction.fields.getTextInputValue('edit_title');
|
| 504 |
+
const newStatusRaw = interaction.fields.getTextInputValue('edit_status').toLowerCase();
|
| 505 |
+
const newDesc = interaction.fields.getTextInputValue('edit_desc');
|
| 506 |
+
|
| 507 |
+
const newStatus = newStatusRaw.includes('uncheck') ? 'unchecked' : 'checked';
|
| 508 |
+
|
| 509 |
+
// Apply edits to memory
|
| 510 |
+
session.files[itemIndex].title = newTitle;
|
| 511 |
+
session.files[itemIndex].status = newStatus;
|
| 512 |
+
session.files[itemIndex].description = newDesc || 'enjoy the drop :))';
|
| 513 |
+
|
| 514 |
+
// Re-render the dashboard
|
| 515 |
+
await interaction.update(generateDashboard(session));
|
| 516 |
+
return true;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
return false;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
module.exports = {
|
| 523 |
+
massDropSessions,
|
| 524 |
+
startMassDropSession,
|
| 525 |
+
hasMassSession,
|
| 526 |
+
getMassPrompt,
|
| 527 |
+
handleMassDropMessage,
|
| 528 |
+
generateDashboard,
|
| 529 |
+
handleMassDropInteraction
|
| 530 |
+
};
|
src/systems/tickets.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const {
|
| 2 |
+
ChannelType,
|
| 3 |
+
PermissionFlagsBits,
|
| 4 |
+
ActionRowBuilder,
|
| 5 |
+
ButtonBuilder,
|
| 6 |
+
ButtonStyle,
|
| 7 |
+
} = require('discord.js');
|
| 8 |
+
const { createEmbed } = require('../utils/embeds');
|
| 9 |
+
const { Colors } = require('../config');
|
| 10 |
+
const { stmts } = require('../database');
|
| 11 |
+
const { logTicket } = require('./logger');
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Send the ticket embed with a "Create a Ticket" button to 🎫・open-ticket channel.
|
| 15 |
+
*/
|
| 16 |
+
async function sendTicketEmbed(client) {
|
| 17 |
+
const row = await stmts.getState('channel_🎫・open-ticket');
|
| 18 |
+
if (!row) throw new Error('Ticket channel not found in bot state.');
|
| 19 |
+
|
| 20 |
+
const channel = await client.channels.fetch(row);
|
| 21 |
+
if (!channel) throw new Error('Could not fetch ticket channel.');
|
| 22 |
+
|
| 23 |
+
const embed = createEmbed({
|
| 24 |
+
title: '🎫 Support Tickets',
|
| 25 |
+
description: [
|
| 26 |
+
'> Need help? Open a support ticket!',
|
| 27 |
+
'',
|
| 28 |
+
'Click the button below to create a private ticket.',
|
| 29 |
+
'',
|
| 30 |
+
'```',
|
| 31 |
+
'• A private channel will be created for you',
|
| 32 |
+
'• Staff will assist you as soon as possible',
|
| 33 |
+
'• Only you and staff can see the ticket',
|
| 34 |
+
'```',
|
| 35 |
+
].join('\n'),
|
| 36 |
+
color: Colors.PRIMARY,
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
const actionRow = new ActionRowBuilder().addComponents(
|
| 40 |
+
new ButtonBuilder()
|
| 41 |
+
.setCustomId('ticket_create')
|
| 42 |
+
.setLabel('Create a Ticket')
|
| 43 |
+
.setEmoji('🎫')
|
| 44 |
+
.setStyle(ButtonStyle.Primary),
|
| 45 |
+
);
|
| 46 |
+
|
| 47 |
+
const msg = await channel.send({ embeds: [embed], components: [actionRow] });
|
| 48 |
+
|
| 49 |
+
await stmts.setState('ticket_message_id', msg.id);
|
| 50 |
+
await stmts.setState('ticket_channel_id', channel.id);
|
| 51 |
+
|
| 52 |
+
return msg;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Create a ticket channel for a user.
|
| 57 |
+
*/
|
| 58 |
+
async function createTicket(guild, user, client) {
|
| 59 |
+
// Check if user already has an open ticket
|
| 60 |
+
const existing = await stmts.getUserTicket(user.id, 'open');
|
| 61 |
+
if (existing) return null;
|
| 62 |
+
|
| 63 |
+
const staffRole = guild.roles.cache.find(r => r.name === '@@ Staff');
|
| 64 |
+
const ownerRole = guild.roles.cache.find(r => r.name === '@@ Owner');
|
| 65 |
+
|
| 66 |
+
// Find the SUPPORT & TICKETS category
|
| 67 |
+
const category = guild.channels.cache.find(
|
| 68 |
+
c => c.type === ChannelType.GuildCategory && c.name.includes('SUPPORT')
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
const channelName = `ticket-${user.username.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
|
| 72 |
+
|
| 73 |
+
const overwrites = [
|
| 74 |
+
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
| 75 |
+
{ id: user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory] },
|
| 76 |
+
];
|
| 77 |
+
if (staffRole) overwrites.push({ id: staffRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.ManageMessages] });
|
| 78 |
+
if (ownerRole) overwrites.push({ id: ownerRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.ManageMessages] });
|
| 79 |
+
|
| 80 |
+
const ticketChannel = await guild.channels.create({
|
| 81 |
+
name: channelName,
|
| 82 |
+
type: ChannelType.GuildText,
|
| 83 |
+
parent: category?.id,
|
| 84 |
+
permissionOverwrites: overwrites,
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
// Save to database
|
| 88 |
+
await stmts.createTicket(user.id, user.tag, ticketChannel.id);
|
| 89 |
+
|
| 90 |
+
// Send welcome embed with action buttons
|
| 91 |
+
const embed = createEmbed({
|
| 92 |
+
title: '🎫 Ticket Opened',
|
| 93 |
+
description: [
|
| 94 |
+
`Welcome <@${user.id}>!`,
|
| 95 |
+
'',
|
| 96 |
+
'A staff member will be with you shortly.',
|
| 97 |
+
'Please describe your issue below.',
|
| 98 |
+
'',
|
| 99 |
+
'> Use the buttons to manage this ticket.',
|
| 100 |
+
].join('\n'),
|
| 101 |
+
color: Colors.PRIMARY,
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
const row = new ActionRowBuilder().addComponents(
|
| 105 |
+
new ButtonBuilder()
|
| 106 |
+
.setCustomId('ticket_close')
|
| 107 |
+
.setLabel('Close Ticket')
|
| 108 |
+
.setEmoji('🔒')
|
| 109 |
+
.setStyle(ButtonStyle.Secondary),
|
| 110 |
+
new ButtonBuilder()
|
| 111 |
+
.setCustomId('ticket_transcript')
|
| 112 |
+
.setLabel('Transcript')
|
| 113 |
+
.setEmoji('📄')
|
| 114 |
+
.setStyle(ButtonStyle.Primary),
|
| 115 |
+
new ButtonBuilder()
|
| 116 |
+
.setCustomId('ticket_delete')
|
| 117 |
+
.setLabel('Delete Ticket')
|
| 118 |
+
.setEmoji('🗑️')
|
| 119 |
+
.setStyle(ButtonStyle.Danger),
|
| 120 |
+
);
|
| 121 |
+
|
| 122 |
+
await ticketChannel.send({ embeds: [embed], components: [row] });
|
| 123 |
+
await logTicket(client, { user, action: 'opened', channelName });
|
| 124 |
+
|
| 125 |
+
return ticketChannel;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* Generate a transcript of a ticket channel.
|
| 130 |
+
*/
|
| 131 |
+
async function generateTranscript(channel) {
|
| 132 |
+
const messages = [];
|
| 133 |
+
let lastId;
|
| 134 |
+
|
| 135 |
+
// Fetch all messages (paginated)
|
| 136 |
+
while (true) {
|
| 137 |
+
const batch = await channel.messages.fetch({ limit: 100, ...(lastId ? { before: lastId } : {}) });
|
| 138 |
+
if (batch.size === 0) break;
|
| 139 |
+
messages.push(...batch.values());
|
| 140 |
+
lastId = batch.last().id;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
messages.reverse();
|
| 144 |
+
|
| 145 |
+
const lines = messages.map(m => {
|
| 146 |
+
const time = m.createdAt.toISOString().replace('T', ' ').slice(0, 19);
|
| 147 |
+
return `[${time}] ${m.author.tag}: ${m.content || '(embed/attachment)'}`;
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
return lines.join('\n') || '(no messages)';
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Handle ticket button interactions.
|
| 155 |
+
*/
|
| 156 |
+
async function handleTicketButton(interaction, client) {
|
| 157 |
+
const { customId, channel, guild, member } = interaction;
|
| 158 |
+
|
| 159 |
+
// Handle "Create a Ticket" button
|
| 160 |
+
if (customId === 'ticket_create') {
|
| 161 |
+
await interaction.deferReply({ ephemeral: true });
|
| 162 |
+
const user = interaction.user;
|
| 163 |
+
const ticketChannel = await createTicket(guild, user, client);
|
| 164 |
+
|
| 165 |
+
if (!ticketChannel) {
|
| 166 |
+
await interaction.editReply({ content: '❌ You already have an open ticket.' });
|
| 167 |
+
} else {
|
| 168 |
+
await interaction.editReply({ content: `✅ Ticket created: <#${ticketChannel.id}>` });
|
| 169 |
+
}
|
| 170 |
+
return true;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
if (!['ticket_close', 'ticket_delete', 'ticket_transcript'].includes(customId)) return false;
|
| 174 |
+
|
| 175 |
+
const ticket = await stmts.getTicket(channel.id);
|
| 176 |
+
if (!ticket) {
|
| 177 |
+
await interaction.reply({ content: '❌ This is not a ticket channel.', ephemeral: true });
|
| 178 |
+
return true;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Permission check: only staff, owner, or ticket creator
|
| 182 |
+
const isStaff = member.roles.cache.some(r => ['@@ Staff', '@@ Owner', '@@ Co-Owner'].includes(r.name));
|
| 183 |
+
const isCreator = ticket.user_id === member.id;
|
| 184 |
+
if (!isStaff && !isCreator) {
|
| 185 |
+
await interaction.reply({ content: '❌ You do not have permission.', ephemeral: true });
|
| 186 |
+
return true;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
if (customId === 'ticket_transcript') {
|
| 190 |
+
await interaction.deferReply({ ephemeral: true });
|
| 191 |
+
const transcript = await generateTranscript(channel);
|
| 192 |
+
const buffer = Buffer.from(transcript, 'utf-8');
|
| 193 |
+
await interaction.editReply({
|
| 194 |
+
content: '📄 Transcript generated.',
|
| 195 |
+
files: [{ attachment: buffer, name: `transcript-${channel.name}.txt` }],
|
| 196 |
+
});
|
| 197 |
+
return true;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
if (customId === 'ticket_close') {
|
| 201 |
+
await stmts.closeTicket('closed', channel.id);
|
| 202 |
+
|
| 203 |
+
// Save transcript to ticket-logs
|
| 204 |
+
const transcript = await generateTranscript(channel);
|
| 205 |
+
const logsRow = await stmts.getState('channel_📂・ticket-logs');
|
| 206 |
+
if (logsRow) {
|
| 207 |
+
const logsChannel = await client.channels.fetch(logsRow).catch(() => null);
|
| 208 |
+
if (logsChannel) {
|
| 209 |
+
const embed = createEmbed({
|
| 210 |
+
title: '📂 Ticket Closed',
|
| 211 |
+
description: `**Ticket:** ${channel.name}\n**User:** <@${ticket.user_id}>\n**Closed by:** <@${member.id}>`,
|
| 212 |
+
color: Colors.WARNING,
|
| 213 |
+
});
|
| 214 |
+
const buffer = Buffer.from(transcript, 'utf-8');
|
| 215 |
+
await logsChannel.send({
|
| 216 |
+
embeds: [embed],
|
| 217 |
+
files: [{ attachment: buffer, name: `transcript-${channel.name}.txt` }],
|
| 218 |
+
});
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
await logTicket(client, { user: { tag: ticket.username, id: ticket.user_id }, action: 'closed', channelName: channel.name });
|
| 223 |
+
|
| 224 |
+
// Delete the channel after a short delay
|
| 225 |
+
const closeEmbed = createEmbed({
|
| 226 |
+
title: '🔒 Ticket Closed',
|
| 227 |
+
description: 'This ticket has been closed. The channel will be deleted in 5 seconds.',
|
| 228 |
+
color: Colors.WARNING,
|
| 229 |
+
});
|
| 230 |
+
await interaction.reply({ embeds: [closeEmbed] });
|
| 231 |
+
setTimeout(() => channel.delete().catch(() => { }), 5000);
|
| 232 |
+
return true;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
if (customId === 'ticket_delete') {
|
| 236 |
+
await stmts.closeTicket('deleted', channel.id);
|
| 237 |
+
await logTicket(client, { user: { tag: ticket.username, id: ticket.user_id }, action: 'deleted', channelName: channel.name });
|
| 238 |
+
await interaction.reply({ content: '🗑️ Deleting ticket...' });
|
| 239 |
+
setTimeout(() => channel.delete().catch(() => { }), 1000);
|
| 240 |
+
return true;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return false;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
module.exports = { sendTicketEmbed, createTicket, handleTicketButton, generateTranscript };
|
src/systems/verification.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
const { stmts } = require('../database');
|
| 4 |
+
const { logVerification } = require('./logger');
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Send the verification embed to the ✅・verify channel.
|
| 8 |
+
*/
|
| 9 |
+
async function sendVerificationEmbed(client) {
|
| 10 |
+
const row = await stmts.getState('channel_✅・verify');
|
| 11 |
+
if (!row) throw new Error('Verify channel not found in bot state.');
|
| 12 |
+
|
| 13 |
+
const channel = await client.channels.fetch(row.value);
|
| 14 |
+
if (!channel) throw new Error('Could not fetch verify channel.');
|
| 15 |
+
|
| 16 |
+
const embed = createEmbed({
|
| 17 |
+
title: '✅ Verification',
|
| 18 |
+
description: [
|
| 19 |
+
'> Welcome to **Wyvern Softworks**!',
|
| 20 |
+
'',
|
| 21 |
+
'React with ✅ below to verify yourself and gain access to the server.',
|
| 22 |
+
'',
|
| 23 |
+
'```',
|
| 24 |
+
'• You will receive the Verified role',
|
| 25 |
+
'• Removing your reaction will remove the role',
|
| 26 |
+
'```',
|
| 27 |
+
].join('\n'),
|
| 28 |
+
color: Colors.SUCCESS,
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
const msg = await channel.send({ embeds: [embed] });
|
| 32 |
+
await msg.react('✅');
|
| 33 |
+
|
| 34 |
+
// Store the message ID so we can listen for reactions
|
| 35 |
+
await stmts.setState('verify_message_id', msg.id);
|
| 36 |
+
await stmts.setState('verify_channel_id', channel.id);
|
| 37 |
+
|
| 38 |
+
return msg;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Handle verification reaction add.
|
| 43 |
+
* Works by checking channel name OR stored message ID (resilient to DB resets).
|
| 44 |
+
*/
|
| 45 |
+
async function handleVerifyReaction(reaction, user, client) {
|
| 46 |
+
// Check if this is a reaction in the verify channel on a bot message
|
| 47 |
+
const channel = reaction.message.channel;
|
| 48 |
+
const isVerifyChannel = channel.name === '✅・verify';
|
| 49 |
+
const isBotMessage = reaction.message.author?.id === client.user.id;
|
| 50 |
+
|
| 51 |
+
// Also check stored message ID as fallback
|
| 52 |
+
const stateVal = await stmts.getState('verify_message_id');
|
| 53 |
+
const verifyMsgId = stateVal;
|
| 54 |
+
const isStoredMsg = verifyMsgId && reaction.message.id === verifyMsgId;
|
| 55 |
+
|
| 56 |
+
if (!isVerifyChannel && !isStoredMsg) return false;
|
| 57 |
+
if (isVerifyChannel && !isBotMessage) return false;
|
| 58 |
+
if (reaction.emoji.name !== '✅') return false;
|
| 59 |
+
|
| 60 |
+
const guild = reaction.message.guild;
|
| 61 |
+
const member = await guild.members.fetch(user.id).catch(() => null);
|
| 62 |
+
if (!member) return false;
|
| 63 |
+
|
| 64 |
+
const role = guild.roles.cache.find(r => r.name === '@@ Verified');
|
| 65 |
+
if (!role) return false;
|
| 66 |
+
|
| 67 |
+
await member.roles.add(role);
|
| 68 |
+
await logVerification(client, user, 'verified');
|
| 69 |
+
return true;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/**
|
| 73 |
+
* Handle verification reaction remove.
|
| 74 |
+
*/
|
| 75 |
+
async function handleVerifyReactionRemove(reaction, user, client) {
|
| 76 |
+
const channel = reaction.message.channel;
|
| 77 |
+
const isVerifyChannel = channel.name === '✅・verify';
|
| 78 |
+
const isBotMessage = reaction.message.author?.id === client.user.id;
|
| 79 |
+
|
| 80 |
+
const stateVal = await stmts.getState('verify_message_id');
|
| 81 |
+
const verifyMsgId = stateVal;
|
| 82 |
+
const isStoredMsg = verifyMsgId && reaction.message.id === verifyMsgId;
|
| 83 |
+
|
| 84 |
+
if (!isVerifyChannel && !isStoredMsg) return false;
|
| 85 |
+
if (isVerifyChannel && !isBotMessage) return false;
|
| 86 |
+
if (reaction.emoji.name !== '✅') return false;
|
| 87 |
+
|
| 88 |
+
const guild = reaction.message.guild;
|
| 89 |
+
const member = await guild.members.fetch(user.id).catch(() => null);
|
| 90 |
+
if (!member) return false;
|
| 91 |
+
|
| 92 |
+
const role = guild.roles.cache.find(r => r.name === '@@ Verified');
|
| 93 |
+
if (!role) return false;
|
| 94 |
+
|
| 95 |
+
await member.roles.remove(role);
|
| 96 |
+
await logVerification(client, user, 'unverified');
|
| 97 |
+
return true;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
module.exports = { sendVerificationEmbed, handleVerifyReaction, handleVerifyReactionRemove };
|
src/systems/welcome.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { createEmbed } = require('../utils/embeds');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Handle new member join — send a welcome embed via DM and optionally in a channel.
|
| 6 |
+
*/
|
| 7 |
+
async function handleMemberJoin(member, client) {
|
| 8 |
+
// Build welcome DM embed
|
| 9 |
+
const embed = createEmbed({
|
| 10 |
+
title: '🟣 Welcome to Wyvern Softworks',
|
| 11 |
+
description: [
|
| 12 |
+
`Hey **${member.user.username}**, welcome to the server!\n`,
|
| 13 |
+
|
| 14 |
+
'> Before you get started, here\'s what you need to do:\n',
|
| 15 |
+
|
| 16 |
+
'**1.** Read the 📜・rules and ⚠️・disclaimer channels',
|
| 17 |
+
'**2.** Head to ✅・verify and react to get verified',
|
| 18 |
+
'**3.** Once verified, the full server will unlock\n',
|
| 19 |
+
|
| 20 |
+
'```',
|
| 21 |
+
'🔒 Most channels are locked until verification',
|
| 22 |
+
'🎫 Need help? Open a ticket after verifying',
|
| 23 |
+
'💜 Boost the server for exclusive perks',
|
| 24 |
+
'```\n',
|
| 25 |
+
|
| 26 |
+
'> Enjoy your stay — **Wyvern Softworks** 🐉',
|
| 27 |
+
].join('\n'),
|
| 28 |
+
color: Colors.PRIMARY,
|
| 29 |
+
footer: 'WSB — Wyvern Softworks Bot',
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Try to send DM
|
| 33 |
+
try {
|
| 34 |
+
await member.send({ embeds: [embed] });
|
| 35 |
+
} catch {
|
| 36 |
+
// User has DMs disabled — silently ignore
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Handle member leave (optional logging).
|
| 42 |
+
*/
|
| 43 |
+
async function handleMemberLeave(member, client) {
|
| 44 |
+
const { log } = require('./logger');
|
| 45 |
+
await log(client, {
|
| 46 |
+
title: '👋 Member Left',
|
| 47 |
+
description: `**${member.user.tag}** (${member.id}) has left the server.`,
|
| 48 |
+
color: Colors.WARNING,
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
module.exports = { handleMemberJoin, handleMemberLeave };
|
src/utils/embeds.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { EmbedBuilder } = require('discord.js');
|
| 2 |
+
const { Colors } = require('../config');
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Build a themed embed with consistent WSB branding.
|
| 6 |
+
*/
|
| 7 |
+
function createEmbed({
|
| 8 |
+
title = null,
|
| 9 |
+
description = null,
|
| 10 |
+
color = Colors.PRIMARY,
|
| 11 |
+
fields = [],
|
| 12 |
+
thumbnail = null,
|
| 13 |
+
image = null,
|
| 14 |
+
footer = 'WSB — Wyvern Softworks Bot',
|
| 15 |
+
timestamp = true,
|
| 16 |
+
} = {}) {
|
| 17 |
+
const embed = new EmbedBuilder().setColor(color);
|
| 18 |
+
|
| 19 |
+
if (title) embed.setTitle(title);
|
| 20 |
+
if (description) embed.setDescription(description);
|
| 21 |
+
if (thumbnail) embed.setThumbnail(thumbnail);
|
| 22 |
+
if (image) embed.setImage(image);
|
| 23 |
+
if (fields.length) embed.addFields(fields);
|
| 24 |
+
if (footer) embed.setFooter({ text: footer });
|
| 25 |
+
if (timestamp) embed.setTimestamp();
|
| 26 |
+
|
| 27 |
+
return embed;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function successEmbed(title, description) {
|
| 31 |
+
return createEmbed({ title: `✅ ${title}`, description, color: Colors.SUCCESS });
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function errorEmbed(title, description) {
|
| 35 |
+
return createEmbed({ title: `❌ ${title}`, description, color: Colors.ACCENT });
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function infoEmbed(title, description) {
|
| 39 |
+
return createEmbed({ title: `ℹ️ ${title}`, description, color: Colors.INFO });
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function warnEmbed(title, description) {
|
| 43 |
+
return createEmbed({ title: `⚠️ ${title}`, description, color: Colors.WARNING });
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function logEmbed({ title, description, color = Colors.MUTED, fields = [] }) {
|
| 47 |
+
return createEmbed({ title, description, color, fields });
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
module.exports = { createEmbed, successEmbed, errorEmbed, infoEmbed, warnEmbed, logEmbed };
|
src/utils/permissions.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PermissionFlagsBits } = require('discord.js');
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Build permission overwrite objects for a channel from the config shorthand.
|
| 5 |
+
* @param {Object} overrides - { roleName: { view, send, connect, speak } }
|
| 6 |
+
* @param {Map} roleMap - Map<roleName, Role> (includes 'everyone' → @everyone)
|
| 7 |
+
* @returns {Array} Array of permission overwrite objects for channel creation
|
| 8 |
+
*/
|
| 9 |
+
function buildOverwrites(overrides, roleMap) {
|
| 10 |
+
const result = [];
|
| 11 |
+
|
| 12 |
+
for (const [roleName, perms] of Object.entries(overrides)) {
|
| 13 |
+
const role = roleMap.get(roleName === 'everyone' ? 'everyone' : roleName);
|
| 14 |
+
if (!role) continue;
|
| 15 |
+
|
| 16 |
+
const allow = [];
|
| 17 |
+
const deny = [];
|
| 18 |
+
|
| 19 |
+
// View
|
| 20 |
+
if (perms.view === true) allow.push(PermissionFlagsBits.ViewChannel);
|
| 21 |
+
if (perms.view === false) deny.push(PermissionFlagsBits.ViewChannel);
|
| 22 |
+
|
| 23 |
+
// Send
|
| 24 |
+
if (perms.send === true) allow.push(PermissionFlagsBits.SendMessages);
|
| 25 |
+
if (perms.send === false) deny.push(PermissionFlagsBits.SendMessages);
|
| 26 |
+
|
| 27 |
+
// Connect (voice)
|
| 28 |
+
if (perms.connect === true) allow.push(PermissionFlagsBits.Connect);
|
| 29 |
+
if (perms.connect === false) deny.push(PermissionFlagsBits.Connect);
|
| 30 |
+
|
| 31 |
+
// Speak (voice)
|
| 32 |
+
if (perms.speak === true) allow.push(PermissionFlagsBits.Speak);
|
| 33 |
+
if (perms.speak === false) deny.push(PermissionFlagsBits.Speak);
|
| 34 |
+
|
| 35 |
+
// React
|
| 36 |
+
if (perms.react === true) allow.push(PermissionFlagsBits.AddReactions);
|
| 37 |
+
if (perms.react === false) deny.push(PermissionFlagsBits.AddReactions);
|
| 38 |
+
|
| 39 |
+
// Read history
|
| 40 |
+
if (perms.readHistory === true) allow.push(PermissionFlagsBits.ReadMessageHistory);
|
| 41 |
+
if (perms.readHistory === false) deny.push(PermissionFlagsBits.ReadMessageHistory);
|
| 42 |
+
|
| 43 |
+
result.push({
|
| 44 |
+
id: role.id || role,
|
| 45 |
+
allow,
|
| 46 |
+
deny,
|
| 47 |
+
});
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return result;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
module.exports = { buildOverwrites };
|