APRKDEV commited on
Commit
e5c9966
·
verified ·
1 Parent(s): 3ce7bc1

Upload 43 files

Browse files
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 };