Compare commits

..

276 commits

Author SHA1 Message Date
1ea8a6f26e UNTESTED: auto detect source fps 2024-09-22 01:34:35 +02:00
d18275cd7a reverse playlist so it download the right video on twitter quotes 2024-09-22 01:04:27 +02:00
7926820e70 Fix download format 2024-08-31 15:44:31 +02:00
126d29fdf9 Forgot to make video max res be 480p 2024-08-31 14:53:39 +02:00
64c45f87c0 Enforce file size limit before downloading 2024-08-28 20:52:38 +02:00
a5cc9c45ed Fix downloads not working when proxy isn't set 2024-08-28 20:52:19 +02:00
7ad4618dd0 return if guild is not cached 2024-08-28 14:44:48 +02:00
705921b8d0 forgot proxy here 2024-08-28 14:42:22 +02:00
0b12e2d496 Add proxy option to yt-dlp 2024-08-28 14:17:46 +02:00
4f84a09a7e fix use of single quote 2024-08-28 00:24:16 +02:00
d604f0ad8e Remove from ratelimiter if command crash
(Untested because I live on the edge)
2024-08-28 00:13:14 +02:00
19855f82f0 reencode av1 since discord don't support embed with it yet 2024-08-02 21:33:26 +02:00
9855987cbc Don't reencode HEVC as discord now supports it 2024-07-13 14:05:37 +02:00
c0507dc981 Show a message instead of error when an argument is required (for real this time) 2024-07-11 07:32:14 +02:00
26db60e95f Revert "Show a message instead of error when an argument is required"
This reverts commit 97254f619c.
2024-07-11 07:26:25 +02:00
6983ea58b7 eslint fix 2024-07-11 07:19:25 +02:00
6ce2faf21b Update eslint 2024-07-11 07:18:43 +02:00
97254f619c Show a message instead of error when an argument is required 2024-07-11 07:18:36 +02:00
23bcd036c0 Remove dotenv to use --env-file instead 2024-07-11 06:40:34 +02:00
34ab603462 Consider user already opted out when they run /optout to avoid logging them 2024-07-10 03:40:22 +02:00
4c5d879650 add autocrop 2024-07-08 23:50:28 +02:00
fe641132da enable invite when user installed 2024-07-08 23:50:20 +02:00
8c6b06a3d0 Remove useless packet tags 2024-07-07 01:11:43 +02:00
4cf0d0bac1 Added autocrop function to download command 2024-07-07 01:05:48 +02:00
77a5ac6137 Move yt-dlp version check to be execute when its needed as to not delay the logs 2024-07-07 00:56:09 +02:00
a92b16fba4 Update deploy and deployGlobally script 2024-07-07 00:55:37 +02:00
c3fd22f02f Fix for windows 2024-07-07 00:55:15 +02:00
2afbca10ec Show architechture 2024-07-04 19:16:00 +02:00
5e012e0701 ignore no matter the extension 2024-07-04 19:15:46 +02:00
3f28a897b4 Replace "assert" with "with" 2024-07-04 19:15:29 +02:00
4a236497bd Update for new HandBrakeCLI 2024-07-04 05:53:54 +02:00
09a180e36e Fix for windows 2024-07-04 05:53:39 +02:00
0d4b88c465 Update dependencies 2024-07-04 05:53:27 +02:00
49c756bd62 Enable guild block list 2024-06-30 20:05:09 +02:00
50b55edae3 Add option to control fps 2024-06-30 02:01:57 +02:00
281edd2d1d Command is outdated. 2024-06-30 02:01:48 +02:00
62666e2a3f Ping for user installed 2024-06-25 18:06:34 +02:00
427d3449d5 Let user optout when the bot is installed to the user 2024-06-25 18:04:33 +02:00
a86044c7f6 Add alias "togif" et enable working in global commands 2024-06-15 14:13:44 +02:00
ec08c4fa80 fix when not in guild 2024-06-12 00:51:24 +02:00
454f2c4296 added default context and moved integration types to the command 2024-06-12 00:47:50 +02:00
c44ae79640 user app invitation 2024-06-12 00:43:17 +02:00
5d0d9bce08 prepare for user applications 2024-06-12 00:31:02 +02:00
7d1151e6ce Prepare for user applciation 2024-06-12 00:30:34 +02:00
2a8356d219 option to add description 2024-03-05 19:41:31 +01:00
e4b441e5f5 Fix "thinking" message not getting deleted 2024-02-04 17:36:42 +01:00
89f00a2fcf use ytdlpMaxResolution instead of the temporary hardcoded value 2024-02-04 03:31:09 +01:00
e6dca692ed make ratelimiter async (also actually fix message) 2024-02-04 01:51:17 +01:00
12ba7621b6 remove console.log 2024-02-04 01:40:54 +01:00
bf9c87f3b8 Fix lazy copy paste to say prefix instead of slash 2024-02-04 01:40:07 +01:00
2c25890f5b Fix rate limit message not working correctly with optout setting 2024-02-04 01:39:56 +01:00
ceafee8287 Undo preset change 2024-02-02 03:29:56 +01:00
76f7e40d8f replace exec with execFIle
(Should've done this long ago)
2024-02-02 03:28:46 +01:00
35b57c219f Hide warnings 2024-01-30 00:28:27 +01:00
d96d32f008 Show error message 2024-01-30 00:25:41 +01:00
fe014ca7d7 Limit to 30 ytp at the same time and 2 per users 2024-01-30 00:07:26 +01:00
995634a4b2 Limit videos to 480p 2024-01-28 22:11:33 +01:00
3464736d85 Fix anything triggering boolean options in commands 2024-01-28 21:55:19 +01:00
fd7ca30e1c Fix cleanup 2024-01-26 18:39:22 +01:00
38b8f80c43 update .env 2024-01-26 18:34:24 +01:00
ff8c6c29b0 Fix the stupid huge mistake i made 2024-01-26 18:33:57 +01:00
44a629c7fc Better UX and fixed format option 2024-01-26 18:23:09 +01:00
28ff4f518e Show current discord.js version 2024-01-12 23:59:22 +01:00
e8fc57394f Return when the video is too big 2024-01-07 01:10:02 +01:00
3b9d2dc556 Limit videos to 720p 2024-01-07 01:06:23 +01:00
b095d5ce3a use vxtwitter 2023-12-26 16:39:42 +01:00
da3e0185e1 Quality option 2023-12-26 16:39:34 +01:00
50e49db47c show how many execution in parallel are currently running in help 2023-12-14 00:23:49 +01:00
591652f33f update tag to username 2023-12-12 21:46:41 +01:00
49e13885fe Added timestamp 2023-12-12 21:45:09 +01:00
0bde6afdce Remove console.log and ignore execution limit if bot owner 2023-12-12 21:40:41 +01:00
c782708fa6 Update some more strings 2023-12-12 21:21:09 +01:00
1cd6a6009d Adding a limit to how many time a command can be executed at the same time 2023-12-12 21:20:48 +01:00
520ca95b29 added some ytp files 2023-12-12 21:18:58 +01:00
ff98b259e7 Updated some strings 2023-12-12 21:18:50 +01:00
fa4b5165e8 Show user id in the feedback 2023-09-13 21:01:20 +02:00
162a91ca48 Fix admin permission check 2023-09-13 20:59:56 +02:00
5d6746a233 Replace tag with username 2023-09-13 20:56:06 +02:00
16842d7127 Don't accept playlist (They didn't work already before) 2023-09-06 12:28:17 +02:00
0c38de9ea2 Non-ephemeral so I can keep a record of it 2023-09-06 12:24:48 +02:00
e235f064d8 Fix custom emotes in starboard/shameboard 2023-07-10 01:13:29 +02:00
8546fb30f5 removed comments and console.log 2023-06-04 19:03:57 +02:00
ba42ef6f37 Plugging friend sound 2023-06-04 19:02:44 +02:00
b559edcd10 Use twitter-api-v2 instead of twit 2023-06-04 19:02:30 +02:00
1269403787 Handle some error and send error message 2023-05-20 19:20:11 +02:00
de6e0dd3c7 fix getVideoSize 2023-04-20 19:54:21 +02:00
4e5324155d Fix some occasional error 2023-04-20 19:51:03 +02:00
65eb5b997f Tweet is not feedback 2023-04-20 19:50:41 +02:00
fb4db75f09 Don't apply rate limit to bot owner 2023-04-19 17:05:43 +02:00
59bf0b9430 Update file size limit 2023-04-19 17:05:35 +02:00
543ab35c9e up file limit to 25 2023-04-19 16:58:42 +02:00
88ff7390cd adding more ban words 2023-04-17 17:28:09 +02:00
ccf9dc5785 add alias and ability to not loop 2023-04-15 23:49:34 +02:00
5cc94e54a3 Added some aliases 2023-04-15 19:49:36 +02:00
d925e62004 "using slash" instead of "with slash" 2023-04-15 19:48:08 +02:00
633f0a6fec Added more restrictions.
Only work in guilds.
Server need to be 1 month old.
Bot need to be in server for 1 week.
2023-04-15 19:32:35 +02:00
780aef27c5 guildOnly check 2023-04-14 17:46:12 +02:00
408176cc9d Fix stupid censor 2023-04-14 04:24:46 +02:00
cd4ffa8b53 fix getVideoSize 2023-04-11 20:32:55 +02:00
aacd7aa9fa Fix compression, add file size to author 2023-04-11 20:31:25 +02:00
f294e8cee1 getVideoSize 2023-04-11 20:16:18 +02:00
3780fad9ae await getMaxFIleSize 2023-04-11 20:16:07 +02:00
d4e3693be6 Use the guild max file size 2023-04-11 14:44:37 +02:00
d926931e37 Exclude some more files 2023-04-11 14:36:04 +02:00
6a9425eccc get the max file size from a guild 2023-04-11 14:34:19 +02:00
1991925213 Download and load a command 2023-04-11 14:34:01 +02:00
0f72e8c180 Keep some more folders 2023-04-11 14:33:31 +02:00
64988e340f Update text to say bug report instead of feature request 2023-04-11 14:33:17 +02:00
bd4dcd087e rename owner to creator 2023-04-10 14:48:57 +02:00
c68d4fca00 Update to StringSelectMenu 2023-04-10 14:48:47 +02:00
56d06cedc4 remux-video instead of merge-output-format 2023-04-10 14:48:26 +02:00
1585941e8a Ported audio2image and image2audio 2023-04-08 00:29:49 +00:00
646421df8f ffmpeg hide_banner 2023-04-08 00:28:59 +00:00
cb13a55c0c Play error video 2023-04-07 16:05:23 +00:00
2f34b9fcc8 Tell what opting out does 2023-04-07 01:08:16 +00:00
dceb4fbbb8 replace arg command with a nice arrow 2023-04-07 00:25:46 +00:00
39ff404deb Minor style change 2023-04-05 16:13:19 +00:00
2cc13e8328 Update a bunch of buttons 2023-04-05 16:12:56 +00:00
fce229e73a Fix status 2023-04-05 15:47:02 +00:00
32fb7bc005 Really did a sloppy job on that one, remember kids, copy pasting is bad! 2023-04-04 23:20:30 +00:00
f8c958af91 Fix permission check and remove useless piece of old code 2023-04-04 22:46:13 +00:00
132d387e49 Merge pull request 'Async loading of commands and events' (#1) from joan/Haha-Yes:jmr/import-then into Slash-V14
Reviewed-on: Supositware/Haha-Yes#1
2023-04-05 00:38:14 +02:00
fa95596906
Async loading of commands and events 2023-04-04 18:21:12 -04:00
5b2fa020d1 Ported tag command 2023-04-04 17:51:44 +00:00
c0fad4c460 Remove ban and kick tag 2023-04-04 17:51:36 +00:00
2f4caf0e72 fix stupid mistake 2023-04-04 16:18:07 +00:00
81e96974b9 sqlite example 2023-04-04 16:11:09 +00:00
a48a696d50 Ignore sqlite3 database 2023-04-04 16:07:54 +00:00
e50a97848f Updated discord.js, twit and added sqlite3 for dev 2023-04-04 16:07:46 +00:00
9ef244fc3f Added todo, updated image fetch and added optout of sharing the end result 2023-04-04 16:06:31 +00:00
bd7f0d12e5 Removed useless logging and updated image fetch 2023-04-04 16:06:05 +00:00
b960829e72 Fix command 2023-04-04 16:00:40 +00:00
60e5152bd9 Remove minecraft server text 2023-04-04 15:26:00 +00:00
29e97c27f6 make optout work here 2023-04-04 01:07:08 +02:00
f29d721771 Let user opt out of join/leave event 2023-04-04 00:45:43 +02:00
cfad048b8e Log command args and allow users to opt out 2023-04-04 00:34:24 +02:00
fa3671efdf Update description 2023-04-04 00:30:30 +02:00
ffc51c5503 Load commands async 2023-04-04 00:26:52 +02:00
a0de902935 Moved download function to its own file 2023-03-20 03:58:21 +01:00
c237f2fb7c Re encode if video is using an incompatible format, currently hevc 2023-03-20 03:57:52 +01:00
0b63dd3a60 Get video codec 2023-03-20 03:55:53 +01:00
c66f3e8403 Removed useless file and update readme accordingly 2023-03-20 03:54:10 +01:00
008029b49f Temporarily disable this command 2023-02-20 23:05:36 +01:00
babfd52eca ACTUALLY fix the command 2023-02-20 23:05:21 +01:00
6fa8f7d33d Fix txt2img AI generation 2023-02-20 22:28:01 +01:00
d45ac9be4f (Maybe) Parse prefix commands better 2022-12-21 21:54:23 +01:00
3e236cc580 ignore unloaded folder 2022-12-21 21:20:40 +01:00
0b01712712 load/unload commands 2022-12-21 21:19:50 +01:00
2f0d6d6a42 Keep that folder 2022-12-21 21:08:37 +01:00
f8cf11e7ff Always register a cooldown even when the limit has not been hit 2022-12-20 19:53:41 +01:00
2c37b3a64f Display the cooldown more nicely 2022-12-19 05:03:39 +01:00
5311aa5847 Remove useless console.log that caused issue 2022-12-19 04:45:33 +01:00
0c793d73a6 Fix cooldown (Was set to 1 hour, supposed to be 24 hours) 2022-12-19 04:40:49 +01:00
7254a242db No ratelimit global variable 2022-12-18 23:30:34 +01:00
d225d7037c Move ratelimiter to its own function 2022-12-18 23:30:25 +01:00
fcb7776f60 Fix cooldown display 2022-12-18 23:00:08 +01:00
467d282a3c Removed some useless things 2022-12-18 22:54:26 +01:00
56368acf74 Make it owner only 2022-12-18 22:54:03 +01:00
a8e98740b3 Don't init global variables here 2022-12-18 22:53:50 +01:00
df722f8ffb Init some global variables 2022-12-18 22:53:31 +01:00
cfa6a55d39 Fix rate limit 2022-12-18 22:53:23 +01:00
8f9bb6b4f8 Limit to 5 ytp per day 2022-11-24 21:38:34 +01:00
f5f7b48935 Add the prefix used 2022-11-24 21:34:54 +01:00
769fecfd87 Fix username 2022-11-24 21:34:39 +01:00
0a3de197ff Change prefix 2022-11-24 21:30:59 +01:00
a6449b93e4 Command to dm users 2022-11-24 21:30:42 +01:00
3b60fee0df Handle attachment correctly 2022-11-24 21:11:53 +01:00
5a6119bc42 use the nickname of the member and don't send the status reply on messages 2022-11-24 21:11:24 +01:00
2f7c03e011 Fix not sending the tweet link & embed 2022-11-24 20:29:09 +01:00
983bfcfc9e cfg scale to 9 2022-10-17 21:32:36 +02:00
c752eebd9a AI category 2022-10-17 21:30:38 +02:00
24391ec40c Rename to txt2img 2022-10-17 21:30:29 +02:00
900a633f0d img2img (Need to improve the arg in messages) 2022-10-17 21:30:19 +02:00
9e621a88e8 Use everything 2022-10-17 16:24:38 +02:00
1c3c9a6cca censor nsfw in non nsfw channel 2022-10-16 23:08:23 +02:00
06d0d3d5c5 Show credit left 2022-10-16 22:58:31 +02:00
5a8ec1dbe7 Avoid stupid leak? 2022-10-16 22:37:34 +02:00
332f1730d6 stableHorde api key 2022-10-16 22:30:00 +02:00
c5cacbb78b Maybe fix that thing 2022-10-16 20:09:59 +02:00
5b426b55ff Stable diffusion! 2022-10-16 20:09:48 +02:00
cc35373749 Improve mentionable args type and match the rest of the arguments for the last one 2022-10-13 16:48:23 +02:00
c908a524aa Fix deploy 2022-10-13 16:32:58 +02:00
16ddd8b388 Resolve the user first 2022-10-13 15:55:56 +02:00
cc25953e90 fetch 2022-10-13 15:53:47 +02:00
366e15f7e4 Use args 2022-10-13 15:49:31 +02:00
bc3e596356 Don't execute it once 2022-10-13 01:37:48 +02:00
0ae54bbbca Script to update bots.gg stats 2022-10-10 20:18:14 +02:00
118954f795 Fix join/leave message 2022-10-10 19:07:53 +02:00
e1c6c9dc39 Invite command 2022-10-10 19:04:20 +02:00
458e913ad0 Fixing quotation and autoresponses 2022-10-10 18:56:58 +02:00
ac93981fd6 Show permissions 2022-10-07 01:33:47 +02:00
0420fcd50c Use case and added fartpiss 2022-10-07 01:11:53 +02:00
424d5ab02a Don't hardcode the folders 2022-09-28 16:05:07 +02:00
2714c748bf Copy instead of moving the file 2022-09-28 16:00:59 +02:00
eecb6020ea Some fix? 2022-09-23 19:45:59 +02:00
605390bfe2 dectalk! 2022-09-23 19:41:18 +02:00
91b3b6bc1e Add "voice" category 2022-09-23 19:41:07 +02:00
9dd060aa28 Server and user info command 2022-09-21 20:55:47 +02:00
66736a5557 Fix reddit command 2022-09-21 20:13:20 +02:00
92c91928e7 user avatar 2022-09-14 21:31:38 +02:00
6c49cc548c fetch members 2022-09-14 21:31:31 +02:00
5a6f785c61 Automatically get the commands for deploy 2022-09-14 21:31:25 +02:00
e926034644 fix embed colour 2022-09-14 14:38:35 +02:00
bf58138205 Fix editing of embed 2022-09-14 14:38:27 +02:00
5cb52fb371 Fix errors that could occur 2022-09-14 14:25:06 +02:00
56768640ac Log stdout on NODE_ENV=development 2022-09-14 11:32:01 +02:00
3143e478cd Finally(?) fix command flags 2022-09-14 11:31:43 +02:00
1a87dbb325 Make the custom id unique 2022-09-14 11:31:19 +02:00
fb31f38c97 Add attachment to feedback 2022-09-14 11:30:56 +02:00
226ae3b096 Allow sending attachment 2022-09-14 11:30:45 +02:00
1eb3b8a013 Actually fix command flags? 2022-09-12 11:36:51 +02:00
b637a73a85 Don't show stdout 2022-09-12 11:33:25 +02:00
f13c1f3ca2 fix flag? 2022-09-12 11:28:24 +02:00
fcc23324f2 Link to status page 2022-09-12 11:21:30 +02:00
c4574e47f6 trigger cleanUp() only if it was from a message 2022-09-10 10:53:21 +02:00
d4a56009fd star/shameboard command 2022-09-10 10:53:05 +02:00
d1e8dda148 Fix command 2022-09-10 10:33:41 +02:00
b9b7b02dc1 Attempted to port the guess command 2022-09-10 10:31:55 +02:00
230b77f2ee Fix ownage 2022-09-10 09:42:10 +02:00
f4772274ac Replace deprecated substr with substring 2022-09-10 09:37:06 +02:00
7c39b62d2c Fix the boolean/flags args 2022-09-10 09:36:07 +02:00
9dcdf9f188 Fix interactionCreate listener and ignore storyboard formats 2022-09-10 09:35:52 +02:00
7e17f7564e Fix interactionCreate listener 2022-09-10 09:35:32 +02:00
cdebcd92a0 Merge output format to mp4/webm/mov 2022-09-10 09:34:57 +02:00
a1f995855d (Maybe?) Fix starboards 2022-09-10 09:10:34 +02:00
138ddb261c owned 2022-09-08 17:29:13 +02:00
0ecafafc96 owned 2022-09-08 17:28:48 +02:00
043c18b216 Fix embeds 2022-09-08 16:56:15 +02:00
4abff9771f Fix tags 2022-09-04 02:26:02 +02:00
22079fcc4e Fix command with args 2022-09-02 22:31:53 +02:00
84aaeefe00 Removed some useless things 2022-09-02 10:00:10 +02:00
5a43d46975 Add help command 2022-09-02 09:54:46 +02:00
8dab14444a Create a generic example if none is provided and human readable permissions 2022-09-02 09:54:40 +02:00
1edb4d8fef Add opt out 2022-09-02 09:24:12 +02:00
8ae824f025 Update description 2022-09-02 09:24:03 +02:00
009f365728 Make booleans be flags 2022-09-02 09:18:52 +02:00
59f0f85df4 Removed some useless console.log 2022-09-02 09:18:39 +02:00
287b89332e Tell people they can add video again 2022-09-02 09:00:41 +02:00
02c1b4b2f7 addytp command 2022-09-02 09:00:28 +02:00
189483d31e ib alias 2022-09-02 08:35:59 +02:00
7160681931 Removed useless console.log 2022-09-02 08:35:03 +02:00
4915420c90 Fix the help command 2022-09-02 08:32:51 +02:00
8d59ac37a6 New way to handle args 2022-09-01 01:43:59 +02:00
f82ff3d380 ytp command 2022-08-31 23:09:50 +02:00
9765680f1c force generation option 2022-08-31 23:09:46 +02:00
a73fd2b893 Fix showing example image of commands 2022-08-31 23:03:10 +02:00
4846604fdf keep userVid folder 2022-08-31 23:02:51 +02:00
63162ebe6d ytp asset 2022-08-31 23:02:40 +02:00
738b5bb4ff ytpplus-node 2022-08-31 23:02:19 +02:00
5f6cd0ea9e Basic ytp generation, lacks the configurtion that was possible before... 2022-08-31 22:55:22 +02:00
88a8bff37d Updated the description 2022-08-31 22:32:12 +02:00
b0397fff78 Added the new commands 2022-08-30 23:33:10 +02:00
6902a10a97 Don't show "secret" category 2022-08-30 23:30:08 +02:00
23d230104b dl alias 2022-08-30 23:29:57 +02:00
1d9bcc9597 Secret category 2022-08-30 23:29:50 +02:00
18607ba90e Removed useless dotenv 2022-08-30 23:02:36 +02:00
069639a4c5 Use the built in permission check for users 2022-08-30 22:58:23 +02:00
d36d086388 Basic help command 2022-08-30 22:57:48 +02:00
c3e8ea2152 Leave and join command (Untested) 2022-08-30 04:14:14 +02:00
2b1f5e4d07 Made messages ephemeral and fixed the category 2022-08-30 04:06:15 +02:00
850a6fb827 Enable/Disable quotation server wide 2022-08-29 21:06:27 +02:00
0f64fd79f2 Add user permission 2022-08-29 21:06:18 +02:00
c24520dd24 Optout of quotation feature 2022-08-29 20:56:26 +02:00
099d0d75e4 Enable/Disable autoresponse 2022-08-29 20:37:59 +02:00
1851a29cd0 Check if command has alias 2022-08-29 20:37:38 +02:00
73e5050ded json/board/ 2022-08-28 17:23:02 +02:00
afed8e9108 Add owner commands 2022-08-28 17:22:49 +02:00
7db77c9916 Fix name 2022-08-28 17:22:26 +02:00
03b3b9189e rand text thigny 2022-08-28 17:04:51 +02:00
8e9e93ca50 Star/shameboard 2022-08-28 17:04:39 +02:00
d00ddbbbf5 Command from event message 2022-08-28 17:04:31 +02:00
01a2c9bab5 Join/Leave message 2022-08-28 17:04:23 +02:00
7e186a07a9 V14 update 2022-08-28 17:04:11 +02:00
6272cde878 Rantionary 2022-08-28 17:03:37 +02:00
ef61db520b V14 update, added category and made it work with message event 2022-08-28 17:03:15 +02:00
bb49cfd490 More intents and partials. Don't load process event if NODE_ENV is development and log loaded commands 2022-08-28 17:02:22 +02:00
105 changed files with 6839 additions and 4458 deletions

View file

@ -2,7 +2,7 @@ token=YourToken
clientId=BotClientId clientId=BotClientId
guildId=DevGuildId guildId=DevGuildId
ownerId=OwnerUserId ownerId=OwnerUserId
statusChannel= statusChannel=CHannelIdForStatus
uptimeURL=UptimeKumaOrWhateverStatusThingYouUseOrJustLeaveEmpty uptimeURL=UptimeKumaOrWhateverStatusThingYouUseOrJustLeaveEmpty
uptimeInterval=60 uptimeInterval=60
twiConsumer=TwitterConsumerToken twiConsumer=TwitterConsumerToken
@ -11,3 +11,10 @@ twiToken=TwitterToken
twiTokenSecret=TwitterSecretToken twiTokenSecret=TwitterSecretToken
twiChannel=ChannelWhereJustTheTwitterLinkAreSent twiChannel=ChannelWhereJustTheTwitterLinkAreSent
twiLogChannel=ChannelWhereTheDetailedInfoOfTheCommandIsSent twiLogChannel=ChannelWhereTheDetailedInfoOfTheCommandIsSent
botsggToken=APITokenForBots.gg
botsggEndpoint=https://discord.bots.gg/api/v1
stableHordeApi=0000000000
stableHordeID=0000
NODE_ENV=development
ytdlpMaxResolution=720
proxy=socks5://localhost:3128

View file

@ -1,57 +0,0 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"requireConfigFile": false,
"babelOptions": {
"plugins": [
"@babel/plugin-syntax-import-assertions"
]
}
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}

View file

@ -33,5 +33,5 @@ labels:
**Did someone already report that bug?** **Did someone already report that bug?**
- [ ] Yes <!-- If you have to put yes you don't need to submit that feature request. --> - [ ] Yes <!-- If you have to put yes you don't need to submit that bug report. -->
- [ ] No - [ ] No

14
.gitignore vendored
View file

@ -1,4 +1,16 @@
.env .env
node_modules/ node_modules/
bin/
config/config.json config/config.json
json/board/
unloaded/
database.sqlite3
tmp/*.js
bin/yt-dlp*
bin/HandBrakeCLI*
bin/upload.sh
bin/dectalk
asset/ytp/sources
asset/ytp/music
asset/ytp/sounds

BIN
asset/ytp/error1.mp4 Normal file

Binary file not shown.

BIN
asset/ytp/error2.mp4 Normal file

Binary file not shown.

BIN
asset/ytp/intro.mp4 Normal file

Binary file not shown.

BIN
asset/ytp/outro.mp4 Normal file

Binary file not shown.

0
bin/.keep Normal file
View file

171
commands/AI/img2img.js Normal file
View file

@ -0,0 +1,171 @@
/* TODO
*
* To be merged with commands/AI/txt2img.js
*
*/
import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js';
import fetch from 'node-fetch';
import os from 'node:os';
import fs from 'node:fs';
import stream from 'node:stream';
import util from 'node:util';
import db from '../../models/index.js';
const { stableHordeApi, stableHordeID } = process.env;
export default {
data: new SlashCommandBuilder()
.setName('img2img')
.setDescription('AI generated image with stable diffusion (If credit are low it may be slow)')
.addAttachmentOption(option =>
option.setName('image')
.setDescription('Image you want to modify')
.setRequired(true))
.addStringOption(option =>
option.setName('prompt')
.setDescription('What do you want the AI to generate?')
.setRequired(true)),
category: 'AI',
alias: ['i2i'],
async execute(interaction, args, client) {
await interaction.deferReply();
const streamPipeline = util.promisify(stream.pipeline);
const res = await fetch(args.image.url);
if (!res.ok) return interaction.editReply('An error has occured while trying to download your image.');
await streamPipeline(res.body, fs.createWriteStream(`${os.tmpdir()}/${args.image.name}.webp`));
const b64Image = fs.readFileSync(`${os.tmpdir()}/${args.image.name}.webp`, { encoding: 'base64' });
generate(interaction, args.prompt, client, b64Image);
},
};
async function generate(i, prompt, client, b64Img) {
console.log('Generating image');
const body = {
prompt: prompt,
params: {
n: 1,
width: 512,
height: 512,
},
cfg_scale: 9,
use_gfpgan: true,
use_real_esrgan: true,
use_ldsr: true,
use_upscaling: true,
steps: 50,
nsfw: i.channel.nsfw ? true : false,
censor_nsfw: i.channel.nsfw ? true : false,
source_image: b64Img,
source_processing: 'img2img',
shared: true,
};
const isOptOut = await db.optout.findOne({ where: { userID: i.user.id } });
if (isOptOut) {
body.shared = false;
}
const fetchParameters = {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json', 'apikey': stableHordeApi },
};
let response = await fetch('https://stablehorde.net/api/v2/generate/async', fetchParameters);
response = await response.json();
if (!response.id) {
console.log(response);
return i.editReply({ content: `An error has occured, please try again later. \`${response.message}\`` });
}
let wait_time = 5000;
let checkURL = `https://stablehorde.net/api/v2/generate/check/${response.id}`;
const checking = setInterval(async () => {
const checkResult = await checkGeneration(checkURL);
if (checkResult === undefined) return;
if (!checkResult.done) {
if (checkResult.wait_time === -1) {
console.log(checkResult.raw);
return i.editReply({ content: `An error has occured, please try again later. \`${checkResult.raw.message}\`` });
}
if (checkResult.wait_time < 0) {
console.log(checkResult.raw);
clearInterval(checking);
return i.editReply({ content: 'No servers are currently available to fulfill your request, please try again later.' });
}
if (checkResult.wait_time === 0) {
checkURL = `https://stablehorde.net/api/v2/generate/status/${response.id}`;
}
wait_time = checkResult.wait_time;
}
else if (checkResult.done && checkResult.image) {
clearInterval(checking);
let creditResponse = await fetch(`https://stablehorde.net/api/v2/users/${stableHordeID}`);
creditResponse = await creditResponse.json();
const streamPipeline = util.promisify(stream.pipeline);
const res = await fetch(checkResult.image);
if (!res.ok) return i.editReply('An error has occured while trying to download your image.');
await streamPipeline(res.body, fs.createWriteStream(`${os.tmpdir()}/${i.id}.webp`));
const generatedImg = new AttachmentBuilder(`${os.tmpdir()}/${i.id}.webp`);
const stableEmbed = new EmbedBuilder()
.setColor(i.member ? i.member.displayHexColor : 'Navy')
.setTitle(prompt)
.setURL('https://aqualxx.github.io/stable-ui/')
.setImage(`attachment://${i.id}.webp`)
.setFooter({ text: `**Credit left: ${creditResponse.kudos}** Seed: ${checkResult.seed} worker ID: ${checkResult.worker_id} worker name: ${checkResult.worker_name}` });
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`regenerate${i.user.id}${i.id}`)
.setLabel('🔄 Regenerate')
.setStyle(ButtonStyle.Primary),
);
await i.editReply({ embeds: [stableEmbed], components: [row], files: [generatedImg] });
listenButton(client, i, prompt);
}
}, wait_time);
}
async function checkGeneration(url) {
let check = await fetch(url);
check = await check.json();
if (!check.is_possible) {
return { done: false, wait_time: -1, raw: check };
}
if (check.done) {
if (!check.generations) {
return { done: false, wait_time: check.wait_time * 1000, raw: check };
}
return { done: true, image: check.generations[0].img, seed: check.generations[0].seed, worker_id: check.generations[0].worker_id, worker_name: check.generations[0].worker_name, raw: check };
}
}
async function listenButton(client, interaction, prompt) {
client.once('interactionCreate', async (interactionMenu) => {
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `regenerate${interactionMenu.user.id}${interaction.id}`) {
await interactionMenu.deferReply();
await generate(interactionMenu, prompt, client);
}
});
}

145
commands/AI/txt2img.js Normal file
View file

@ -0,0 +1,145 @@
/* TODO
*
* To be merged with commands/AI/img2img.js
*
*/
import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js';
import fetch from 'node-fetch';
import os from 'node:os';
import fs from 'node:fs';
import stream from 'node:stream';
import util from 'node:util';
import db from '../../models/index.js';
const { stableHordeApi, stableHordeID } = process.env;
export default {
data: new SlashCommandBuilder()
.setName('txt2img')
.setDescription('AI generated image with stable diffusion (If credit are low it may be slow)')
.addStringOption(option =>
option.setName('prompt')
.setDescription('What do you want the AI to generate?')
.setRequired(true)),
category: 'AI',
alias: ['t2i'],
async execute(interaction, args, client) {
await interaction.deferReply();
generate(interaction, args.prompt, client);
},
};
async function generate(i, prompt, client) {
const body = {
prompt: prompt,
params: {
n: 1,
width: 512,
height: 512,
},
cfg_scale: 9,
use_gfpgan: true,
use_real_esrgan: true,
use_ldsr: true,
use_upscaling: true,
steps: 50,
nsfw: i.channel.nsfw ? true : false,
censor_nsfw: i.channel.nsfw ? true : false,
shared: true,
};
const isOptOut = await db.optout.findOne({ where: { userID: i.user.id } });
if (isOptOut) {
body.shared = false;
}
const fetchParameters = {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json', 'apikey': stableHordeApi },
};
let response = await fetch('https://stablehorde.net/api/v2/generate/async', fetchParameters);
response = await response.json();
let wait_time = 5000;
let checkURL = `https://stablehorde.net/api/v2/generate/check/${response.id}`;
const checking = setInterval(async () => {
const checkResult = await checkGeneration(checkURL);
if (checkResult === undefined) return;
if (!checkResult.done) {
if (checkResult.wait_time < 0) {
clearInterval(checking);
return i.editReply({ content: 'No servers are currently available to fulfill your request, please try again later.' });
}
if (checkResult.wait_time === 0) {
checkURL = `https://stablehorde.net/api/v2/generate/status/${response.id}`;
}
wait_time = checkResult.wait_time;
}
else if (checkResult.done && checkResult.image) {
clearInterval(checking);
let creditResponse = await fetch(`https://stablehorde.net/api/v2/users/${stableHordeID}`);
creditResponse = await creditResponse.json();
const streamPipeline = util.promisify(stream.pipeline);
const res = await fetch(checkResult.image);
if (!res.ok) return i.editReply('An error has occured while trying to download your image.');
await streamPipeline(res.body, fs.createWriteStream(`${os.tmpdir()}/${i.id}.webp`));
const generatedImg = new AttachmentBuilder(`${os.tmpdir()}/${i.id}.webp`);
const stableEmbed = new EmbedBuilder()
.setColor(i.member ? i.member.displayHexColor : 'Navy')
.setTitle(prompt)
.setURL('https://aqualxx.github.io/stable-ui/')
.setImage(`attachment://${i.id}.webp`)
.setFooter({ text: `**Credit left: ${creditResponse.kudos}** Seed: ${checkResult.seed} worker ID: ${checkResult.worker_id} worker name: ${checkResult.worker_name}` });
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`regenerate${i.user.id}${i.id}`)
.setLabel('🔄 Regenerate')
.setStyle(ButtonStyle.Primary),
);
await i.editReply({ embeds: [stableEmbed], components: [row], files: [generatedImg] });
listenButton(client, i, prompt);
}
}, wait_time);
}
async function checkGeneration(url) {
let check = await fetch(url);
check = await check.json();
if (!check.is_possible) {
return { done: false, wait_time: -1 };
}
if (check.done) {
if (!check.generations) {
return { done: false, wait_time: check.wait_time * 1000 };
}
return { done: true, image: check.generations[0].img, seed: check.generations[0].seed, worker_id: check.generations[0].worker_id, worker_name: check.generations[0].worker_name };
}
}
async function listenButton(client, interaction, prompt) {
client.once('interactionCreate', async (interactionMenu) => {
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `regenerate${interactionMenu.user.id}${interaction.id}`) {
await interactionMenu.deferReply();
await generate(interactionMenu, prompt, client);
}
});
}

View file

@ -0,0 +1,62 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits } from 'discord.js';
import db from '../../models/index.js';
export default {
data: new SlashCommandBuilder()
.setName('autoresponse')
.setDescription('Enable or disable autoresponse')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
category: 'admin',
async execute(interaction, args, client) {
const autoresponseStat = await db.autoresponseStat.findOne({ where: { serverID: interaction.guild.id } });
if (!autoresponseStat) {
const body = { serverID: interaction.guild.id, stat: 'enable' };
await db.autoresponseStat.create(body);
return await interaction.reply({ content: 'Autoresponse has been enabled.', ephemeral: true });
}
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
if (autoresponseStat.stat === 'enable') {
await interaction.reply({ content: 'Autoresponse is already enabled, do you wish to disable it?', components: [row], ephemeral: true });
}
else {
const body = { serverID: interaction.guild.id, stat: 'enable' };
await db.autoresponseStat.update(body, { where: { serverID: interaction.guild.id } });
return interaction.editReply({ content: 'Auto response has been enabled.', ephemeral: true });
}
return listenButton(client, interaction, interaction.user);
},
};
async function listenButton(client, interaction, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}${originalId}`) {
const body = { serverID: interaction.guild.id, stat: 'disable' };
await db.autoresponseStat.update(body, { where: { serverID: interaction.guild.id } });
return interaction.editReply({ content: 'Auto response has been disabled.', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

74
commands/admin/bye.js Normal file
View file

@ -0,0 +1,74 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits } from 'discord.js';
import db from '../../models/index.js';
export default {
data: new SlashCommandBuilder()
.setName('bye')
.setDescription('Set a leave message')
.addStringOption(option =>
option.setName('message')
.setDescription('The message you want the bot to say when someone leave in the current channel.')),
category: 'admin',
userPermissions: [PermissionFlagsBits.ManageChannels],
async execute(interaction, args, client) {
const leave = await db.leaveChannel.findOne({ where: { guildID: interaction.guild.id } });
if (!leave && !args.message) {
return interaction.reply({ content: 'You need a message for me to say anything!', ephemeral: true });
}
else if (!leave) {
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.leaveChannel.create(body);
return interaction.reply({ content: `The leave message have been set with ${args.message}`, ephemeral: true });
}
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user);
},
};
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}${originalId}`) {
if (!args.message) {
return interaction.reply({ content: 'You need to input a message for me to edit!', ephemeral: true });
}
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.leaveChannel.update(body, { where: { guildID: interaction.guild.id } });
return interaction.editReply({ content: `The leave message has been set to ${args.message}`, ephemeral: true });
}
else if (interactionMenu.customId === `remove${interaction.user.id}${originalId}`) {
db.leaveChannel.destroy({ where: { guildID: interaction.guild.id, channelID: interaction.channel.id } });
return interaction.editReply({ content: 'The leave message has been deleted.', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

View file

@ -0,0 +1,62 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits } from 'discord.js';
import db from '../../models/index.js';
export default {
data: new SlashCommandBuilder()
.setName('quotation')
.setDescription('Enable or disable quotations')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
category: 'admin',
async execute(interaction, args, client) {
const quotationstat = await db.quotationStat.findOne({ where: { serverID: interaction.guild.id } });
if (!quotationstat) {
const body = { serverID: interaction.guild.id, stat: 'enable' };
await db.quotationStat.create(body);
return await interaction.reply({ content: 'Quotation has been enabled.', ephemeral: true });
}
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
if (quotationstat.stat === 'enable') {
await interaction.reply({ content: 'Quotation is already enabled, do you wish to disable it?', components: [row], ephemeral: true });
}
else {
const body = { serverID: interaction.guild.id, stat: 'enable' };
await db.autoresponseStat.update(body, { where: { serverID: interaction.guild.id } });
return interaction.editReply({ content: 'Quotation has been enabled.', ephemeral: true });
}
return listenButton(client, interaction, interaction.user);
},
};
async function listenButton(client, interaction, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, user, originalId);
if (!interactionMenu.isButton()) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}${originalId}`) {
await db.quotationStat.destroy({ where: { serverID: interaction.guild.id } });
return interaction.editReply({ content: 'Quotation has been disabled.', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

View file

@ -0,0 +1,38 @@
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
import fs from 'node:fs';
export default {
data: new SlashCommandBuilder()
.setName('shameboard')
.setDescription('Set shameboard to the current channel.')
.addStringOption(option =>
option.setName('emote')
.setDescription('The emote that should be used to enter the shameboard.'))
.addStringOption(option =>
option.setName('count')
.setDescription('How many react for it to enter shameboard.'))
.addBooleanOption(option =>
option.setName('remove')
.setDescription('Remove the shameboard')
.setRequired(false)),
category: 'admin',
userPermissions: [PermissionFlagsBits.ManageChannels],
async execute(interaction, args) {
if (args.remove) {
fs.unlink(`./json/board/shame${interaction.guild.id}.json`, (err) => {
if (err) {return interaction.reply('There is no shameboard');}
return interaction.reply('Deleted the shameboard');
});
}
else {
if (!args.emote || !args.count) return interaction.reply('You are missing the emote or the count arg!');
fs.writeFile(`./json/board/shame${interaction.guild.id}.json`, `{"shameboard": "${interaction.channel.id}", "emote": "${args.emote}", "count": "${args.count}"}`, (err) => {
if (err) {
console.log(err);
}
});
return interaction.reply(`This channel have been set as the shameboard with ${args.emote} with the minimum of ${args.count}`);
}
},
};

View file

@ -0,0 +1,38 @@
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
import fs from 'node:fs';
export default {
data: new SlashCommandBuilder()
.setName('starboard')
.setDescription('Set starboard to the current channel.')
.addStringOption(option =>
option.setName('emote')
.setDescription('The emote that should be used to enter the starboard.'))
.addStringOption(option =>
option.setName('count')
.setDescription('How many react for it to enter starboard.'))
.addBooleanOption(option =>
option.setName('remove')
.setDescription('Remove the starboard')
.setRequired(false)),
category: 'admin',
userPermissions: [PermissionFlagsBits.ManageChannels],
async execute(interaction, args) {
if (args.remove) {
fs.unlink(`./json/board/star${interaction.guild.id}.json`, (err) => {
if (err) {return interaction.reply('There is no starboard');}
return interaction.reply('Deleted the starboard');
});
}
else {
if (!args.emote || !args.count) return interaction.reply('You are missing the emote or the count arg!');
fs.writeFile(`./json/board/star${interaction.guild.id}.json`, `{"starboard": "${interaction.channel.id}", "emote": "${args.emote}", "count": "${args.count}"}`, (err) => {
if (err) {
console.log(err);
}
});
return interaction.reply(`This channel have been set as the starboard with ${args.emote} with the minimum of ${args.count}`);
}
},
};

127
commands/admin/tag.js Normal file
View file

@ -0,0 +1,127 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits, PermissionsBitField } from 'discord.js';
import os from 'node:os';
import fs from 'node:fs';
import db from '../../models/index.js';
const { ownerId } = process.env;
export default {
data: new SlashCommandBuilder()
.setName('tag')
.setDescription('Create custom autoresponse')
.addStringOption(option =>
option.setName('trigger')
.setDescription('The strings that will trigger the tag')
.setRequired(false))
.addStringOption(option =>
option.setName('response')
.setDescription('What it will answer back')
.setRequired(false))
.addBooleanOption(option =>
option.setName('remove')
.setDescription('(ADMIN ONLY!) Remove the tag')
.setRequired(false))
.addBooleanOption(option =>
option.setName('list')
.setDescription('List all the tags for the server')
.setRequired(false)),
category: 'admin',
userPermissions: [PermissionFlagsBits.ManageChannels],
async execute(interaction, args, client) {
await interaction.deferReply();
if (args.list) {
let tagList = await db.Tag.findAll({ attributes: ['trigger', 'response', 'ownerID'], where: { serverID: interaction.guild.id } });
if (args.trigger) {
tagList = await db.Tag.findOne({ attributes: ['trigger', 'response', 'ownerID'], where: { trigger: args.trigger, serverID: interaction.guild.id } });
}
if (!tagList) return interaction.editReply('It looks like the server has no tags.');
const path = `${os.tmpdir()}/${interaction.guild.id}.json`;
fs.writeFile(path, JSON.stringify(tagList, null, 2), function(err) {
if (err) return console.error(err);
});
return interaction.editReply({ files: [path] });
}
const tag = await db.Tag.findOne({ where: { trigger: args.trigger, serverID: interaction.guild.id } });
if (args.remove) {
if (tag) {
if (tag.get('ownerID') == interaction.user.id || interaction.member.permissionsIn(interaction.channel).has(PermissionsBitField.Flags.Administrator) || interaction.user.id == ownerId) {
db.Tag.destroy({ where: { trigger: args.trigger, serverID: interaction.guild.id } });
return interaction.editReply('successfully deleted the following tag: ' + args.trigger);
}
else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask a user with the 'Administrator' permission to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`);
}
}
else {
return interaction.editReply('Did not find the specified tag, are you sure it exist?');
}
}
if (!args.trigger) return interaction.editReply('You need to specify what you want me to respond to.');
if (!args.response) return interaction.editReply('You need to specify what you want me to answer with.');
if (!tag) {
const body = { trigger: args.trigger, response: args.response, ownerID: interaction.user.id, serverID: interaction.guild.id };
await db.Tag.create(body);
return interaction.editReply(`tag have been set to ${args.trigger} : ${args.response}`);
}
else if (tag.get('ownerID') == interaction.user.id || interaction.member.permissionsIn(interaction.channel).has('ADMINISTRATOR') || interaction.user.id == ownerId) {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.editReply({ content: 'This tag already exist, do you want to update it, remove it or do nothing?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user);
}
else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask an admin to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`);
}
},
};
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}${originalId}`) {
const body = { trigger: args.trigger, response: args.response, ownerID: interaction.user.id, serverID: interaction.guild.id };
db.Tag.update(body, { where: { serverID: interaction.guild.id } });
return interaction.editReply({ content: `The tag ${args.trigger} has been set to ${args.response}`, ephemeral: true });
}
else if (interactionMenu.customId === `remove${interaction.user.id}${originalId}`) {
db.Tag.destroy({ where: { trigger: args.trigger, serverID: interaction.guild.id } });
return interaction.editReply({ content: `The tag ${args.trigger} has been deleted`, ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

75
commands/admin/welcome.js Normal file
View file

@ -0,0 +1,75 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits } from 'discord.js';
import db from '../../models/index.js';
export default {
data: new SlashCommandBuilder()
.setName('welcome')
.setDescription('Set a join message')
.addStringOption(option =>
option.setName('message')
.setDescription('The message you want the bot to say when someone join in the current channel.')),
category: 'admin',
userPermissions: [PermissionFlagsBits.ManageChannels],
async execute(interaction, args, client) {
const join = await db.joinChannel.findOne({ where: { guildID: interaction.guild.id } });
if (!join && !args.message) {
return interaction.reply({ content: 'You need a message for me to say anything!', ephemeral: true });
}
else if (!join) {
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.joinChannel.create(body);
return interaction.reply({ content: `The join message have been set with ${args.message}`, ephemeral: true });
}
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user);
},
};
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}${originalId}`) {
if (!args.message) {
return interaction.reply({ content: 'You need to input a message for me to edit!', ephemeral: true });
}
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.joinChannel.update(body, { where: { guildID: interaction.guild.id } });
return interaction.editReply({ content: `The join message has been set to ${args.message}`, ephemeral: true });
}
else if (interactionMenu.customId === `remove${interaction.user.id}${originalId}`) {
db.joinChannel.destroy({ where: { guildID: interaction.guild.id, channelID: interaction.channel.id } });
return interaction.editReply({ content: 'The join message has been deleted.', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

View file

@ -1,10 +1,10 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import TurndownService from 'turndown'; import TurndownService from 'turndown';
const turndown = new TurndownService(); const turndown = new TurndownService();
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import fourChan from '../../json/4chan.json' assert {type: 'json'}; import fourChan from '../../json/4chan.json' with {type: 'json'};
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -14,8 +14,9 @@ export default {
option.setName('board') option.setName('board')
.setDescription('The board you wish to see') .setDescription('The board you wish to see')
.setRequired(true)), .setRequired(true)),
async execute(interaction) { category: 'fun',
let board = interaction.options.getString('board'); async execute(interaction, args) {
let board = args.board;
if (fourChan[board] == undefined) { if (fourChan[board] == undefined) {
return interaction.reply({ content: 'Uh oh! The board you are looking for does not exist? You think this is a mistake? Please send a feedback telling me so!', ephemeral: true }); return interaction.reply({ content: 'Uh oh! The board you are looking for does not exist? You think this is a mistake? Please send a feedback telling me so!', ephemeral: true });
@ -65,8 +66,8 @@ export default {
title = 'No title'; title = 'No title';
} }
const FourchanEmbed = new MessageEmbed() const FourchanEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY') .setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle(turndown.turndown(title)) .setTitle(turndown.turndown(title))
.setDescription(turndown.turndown(description)) .setDescription(turndown.turndown(description))
.setImage(`https://i.4cdn.org/${board}/${response.threads[i].posts[0].tim}${response.threads[i].posts[0].ext}`) .setImage(`https://i.4cdn.org/${board}/${response.threads[i].posts[0].tim}${response.threads[i].posts[0].ext}`)

View file

@ -0,0 +1,60 @@
/* TODO
*
* Merge with commands/fun/image2audio.js
*
*/
import { SlashCommandBuilder } from 'discord.js';
import fs from 'node:fs';
import os from 'node:os';
import fetch from 'node-fetch';
import util from 'node:util';
import stream from 'node:stream';
import utils from '../../utils/videos.js';
export default {
data: new SlashCommandBuilder()
.setName('audio2image')
.setDescription('Transform an audio file into an image.')
.addAttachmentOption(option =>
option.setName('audio')
.setDescription('The audio that will become image.')
.setRequired(true)),
category: 'fun',
alias: ['a2i'],
async execute(interaction, args) {
if (!args.audio) return interaction.reply('Please attach an image with your message.');
await interaction.deferReply();
ifExistDelete(`${os.tmpdir()}/${args.audio.name}`);
ifExistDelete(`${os.tmpdir()}/${args.audio.name}.png`);
ifExistDelete(`${os.tmpdir()}/${args.audio.name}.sw`);
ifExistDelete(`${os.tmpdir()}/${args.audio.name}.mp3`);
const streamPipeline = util.promisify(stream.pipeline);
const res = await fetch(args.audio.url);
if (!res.ok) return interaction.editReply('An error has occured while trying to download your image.');
await streamPipeline(res.body, fs.createWriteStream(`${os.tmpdir()}/${args.audio.name}`));
await utils.ffmpeg(['-i', `${os.tmpdir()}/${args.audio.name}`, '-sample_rate', '44100', '-ac', '1', '-f', 's16le', '-acodec', 'pcm_s16le', `${os.tmpdir()}/${args.audio.name}.sw`]);
await utils.ffmpeg(['-pixel_format', 'rgb24', '-video_size', '128x128', '-f', 'rawvideo', '-i', `${os.tmpdir()}/${args.audio.name}.sw`, '-frames:v', '1', `${os.tmpdir()}/${args.audio.name}.png`]);
const file = fs.statSync(`${os.tmpdir()}/${args.audio.name}.png`);
const fileSize = (file.size / 1000000.0).toFixed(2);
if (fileSize > await utils.getMaxFileSize(interaction.guild)) return interaction.editReply('error');
interaction.editReply({ content: `Image file is ${fileSize} MB` });
return interaction.followUp({ files: [`${os.tmpdir()}/${args.audio.name}.png`] });
},
};
async function ifExistDelete(path) {
if (fs.existsSync(path)) {
fs.rm(path, (err) => {
console.log('deleted');
if (err) {
return;
}
});
}
}

View file

@ -1,5 +1,5 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { Permissions } from 'discord.js'; import { PermissionFlagsBits } from 'discord.js';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('fakeuser') .setName('fakeuser')
@ -16,14 +16,18 @@ export default {
option.setName('image') option.setName('image')
.setDescription('Optional attachment.') .setDescription('Optional attachment.')
.setRequired(false)), .setRequired(false)),
clientPermissions: [ Permissions.FLAGS.MANAGE_WEBHOOKS ], category: 'fun',
async execute(interaction) { clientPermissions: [ PermissionFlagsBits.ManageWebhooks ],
async execute(interaction, args) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
const attachment = interaction.options.getAttachment('image'); await interaction.guild.members.fetch();
const message = interaction.options.getString('message'); const member = args.user;
const member = interaction.options.getMentionable('user'); const message = args.message;
const attachment = args.image;
const username = member.nickname ? member.nickname : member.user.username;
const webhook = await interaction.channel.createWebhook(member.user.username, { const webhook = await interaction.channel.createWebhook({
name: username,
avatar: member.user.displayAvatarURL(), avatar: member.user.displayAvatarURL(),
reason: `Fakebot/user command triggered by: ${interaction.user.username}`, reason: `Fakebot/user command triggered by: ${interaction.user.username}`,
}); });
@ -34,6 +38,12 @@ export default {
await webhook.send({ content: message }); await webhook.send({ content: message });
} }
await webhook.delete(`Fakebot/user command triggered by: ${interaction.user.username}`); await webhook.delete(`Fakebot/user command triggered by: ${interaction.user.username}`);
if (interaction.isMessage) {
await interaction.delete();
await interaction.deleteReply();
}
else {
await interaction.editReply({ content: `Faked the user ${member}` }); await interaction.editReply({ content: `Faked the user ${member}` });
}
}, },
}; };

93
commands/fun/guess Normal file
View file

@ -0,0 +1,93 @@
// Tried something to make it work purely with slash commands but it did not work.
// The interaction created from a modal lack the showModal function so I can't just call another modal on top
// A "solution" for this that I could see is creating a button between each response asking the player to continue or stop which would create a new interaction and (maybe) allow to display a new modal
import { SlashCommandBuilder, ActionRowBuilder, TextInputBuilder, SelectMenuBuilder, ModalBuilder, TextInputStyle, InteractionType } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('guess')
.setDescription('Guess the number'),
category: 'fun',
async execute(interaction, args, client) {
const row = new ActionRowBuilder()
.addComponents(
new SelectMenuBuilder()
.setCustomId('difficulty')
.setPlaceholder('Nothing selected')
.addOptions([
{ label: 'Easy', value: '100' },
{ label: 'Normal', value: '1000' },
{ label: 'Hard', value: '10000' },
]),
);
await interaction.reply({ content: 'Which difficulty do you want to play?', ephemeral: true, components: [row] });
let numberTry = 0;
let secretnumber = 0;
client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return;
const modal = new ModalBuilder()
.setCustomId('guessModal')
.setTitle('Your guess');
const textRow = new ActionRowBuilder()
.addComponents(
new TextInputBuilder()
.setCustomId('input')
.setLabel('What is the number?')
.setStyle(TextInputStyle.Short),
);
modal.addComponents(textRow);
async function tryAgain(input) {
if (input != secretnumber) {
if (input > secretnumber) {
modal.setTitle('Its less!\nWhat is the number?');
}
else if (input < secretnumber) {
modal.setTitle('Its more!\nWhat is the number?');
}
}
await interactionMenu.showModal(modal);
}
async function checkNumber(input) {
numberTry++;
if (input.toLowerCase() === 'stop') {
return interaction.reply('Ok, let\'s stop playing :(');
}
else if (input != secretnumber) {
console.log('trying again');
tryAgain(input);
}
else if (numberTry > 1) {
return interaction.reply(`Congratulations! You won! It took you ${numberTry} turns!`);
}
else {
return interaction.reply('Congratulations! You won! It took you 1 Turn!');
}
}
if (interactionMenu.type === InteractionType.ModalSubmit) {
if (interactionMenu.customId === 'guessModal') {
const input = interactionMenu.fields.getTextInputValue('input');
checkNumber(input);
}
}
else if (interactionMenu.isSelectMenu()) {
if (interactionMenu.customId === 'difficulty') {
secretnumber = Math.floor((Math.random() * parseInt(interactionMenu.values[0])) + 1);
console.log(secretnumber);
// await interaction.followUp({ content: 'What is the number?', ephemeral: true });
await interactionMenu.showModal(modal);
}
}
});
},
};

View file

@ -0,0 +1,59 @@
/* TODO
*
* Merge with commands/fun/audio2image.js
*
*/
import { SlashCommandBuilder } from 'discord.js';
import fs from 'node:fs';
import os from 'node:os';
import fetch from 'node-fetch';
import util from 'node:util';
import stream from 'node:stream';
import utils from '../../utils/videos.js';
export default {
data: new SlashCommandBuilder()
.setName('image2audio')
.setDescription('Transform an image binary data into audio ( MIGHT BE VERY LOUD )')
.addAttachmentOption(option =>
option.setName('img')
.setDescription('The image that will become audio. Only tested with png and jpg.')
.setRequired(true)),
category: 'fun',
alias: ['i2a'],
async execute(interaction, args) {
if (!args.img) return interaction.reply('Please attach an image with your message.');
await interaction.deferReply();
ifExistDelete(`${os.tmpdir()}/${args.img.name}`);
ifExistDelete(`${os.tmpdir()}/1${args.img.name}`);
ifExistDelete(`${os.tmpdir()}/${args.img.name}.mp3`);
const streamPipeline = util.promisify(stream.pipeline);
const res = await fetch(args.img.url);
if (!res.ok) return interaction.editReply('An error has occured while trying to download your image.');
await streamPipeline(res.body, fs.createWriteStream(`${os.tmpdir()}/${args.img.name}`));
await utils.ffmpeg(['-i', `${os.tmpdir()}/${args.img.name}`, '-f', 'rawvideo', `${os.tmpdir()}/1${args.img.name}`]);
await utils.ffmpeg(['-sample_rate', '44100', '-ac', '1', '-f', 's16le', '-i', `${os.tmpdir()}/1${args.img.name}`, `${os.tmpdir()}/${args.img.name}.mp3`]);
const file = fs.statSync(`${os.tmpdir()}/${args.img.name}.mp3`);
const fileSize = (file.size / 1000000.0).toFixed(2);
if (fileSize > await utils.getMaxFileSize(interaction.guild)) return interaction.editReply('error');
interaction.editReply({ content: `Audio file is ${fileSize} MB` });
return interaction.followUp({ files: [`${os.tmpdir()}/${args.img.name}.mp3`] });
},
};
async function ifExistDelete(path) {
if (fs.existsSync(path)) {
fs.rm(path, (err) => {
console.log('deleted');
if (err) {
return;
}
});
}
}

View file

@ -1,10 +1,12 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('inspirobot') .setName('inspirobot')
.setDescription('Get an image from inspirobot'), .setDescription('Get an image from inspirobot'),
category: 'fun',
alias: ['ib'],
async execute(interaction) { async execute(interaction) {
fetch('http://inspirobot.me/api?generate=true') fetch('http://inspirobot.me/api?generate=true')
.then(res => res.text()) .then(res => res.text())

View file

@ -1,5 +1,4 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
import { MessageEmbed } from 'discord.js';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
export default { export default {
@ -10,10 +9,11 @@ export default {
option.setName('subreddit') option.setName('subreddit')
.setDescription('The subreddit you wish to see') .setDescription('The subreddit you wish to see')
.setRequired(true)), .setRequired(true)),
async execute(interaction) { category: 'fun',
async execute(interaction, args) {
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
const subreddit = args.subreddit;
fetch('https://www.reddit.com/r/' + interaction.options.getString('subreddit') + '.json?limit=100').then((response) => { fetch('https://www.reddit.com/r/' + subreddit + '.json?limit=100').then((response) => {
return response.json(); return response.json();
}).then((response) => { }).then((response) => {
if (response.error == 404) { if (response.error == 404) {
@ -27,10 +27,15 @@ export default {
if (response.data.children[i].data.over_18 == true && !interaction.channel.nsfw) { if (response.data.children[i].data.over_18 == true && !interaction.channel.nsfw) {
return interaction.editReply('No nsfw'); return interaction.editReply('No nsfw');
} }
const redditEmbed = new MessageEmbed()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY') let description = response.data.children[i].data.selftext;
if (description === '') {
description = 'No description.';
}
const redditEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle(response.data.children[i].data.title) .setTitle(response.data.children[i].data.title)
.setDescription(response.data.children[i].data.selftext) .setDescription(description)
.setURL('https://reddit.com' + response.data.children[i].data.permalink) .setURL('https://reddit.com' + response.data.children[i].data.permalink)
.setFooter({ text: `/r/${response.data.children[i].data.subreddit} | ⬆ ${response.data.children[i].data.ups} 🗨 ${response.data.children[i].data.num_comments}` }); .setFooter({ text: `/r/${response.data.children[i].data.subreddit} | ⬆ ${response.data.children[i].data.ups} 🗨 ${response.data.children[i].data.num_comments}` });

View file

@ -1,18 +0,0 @@
import { SlashCommandBuilder } from '@discordjs/builders';
export default {
data: new SlashCommandBuilder()
.setName('s')
.setDescription('What could this be 🤫')
.addStringOption(option =>
option.setName('something')
.setDescription('🤫')
.setRequired(true)),
async execute(interaction) {
const command = interaction.options.getString('something');
if (command === 'levertowned') {
interaction.reply('Hello buddy bro <:youngtroll:488559163832795136> <@434762632004894746>');
}
},
};

View file

@ -1,14 +1,14 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import Twit from 'twit'; import { TwitterApi } from 'twitter-api-v2';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import os from 'node:os'; import os from 'node:os';
import fs from 'node:fs'; import fs from 'node:fs';
import util from 'node:util';
import stream from 'node:stream';
import db from '../../models/index.js'; import db from '../../models/index.js';
import wordToCensor from '../../json/censor.json' assert {type: 'json'}; import wordToCensor from '../../json/censor.json' with {type: 'json'};
import dotenv from 'dotenv';
dotenv.config();
const { twiConsumer, twiConsumerSecret, twiToken, twiTokenSecret, twiChannel, twiLogChannel } = process.env; const { twiConsumer, twiConsumerSecret, twiToken, twiTokenSecret, twiChannel, twiLogChannel } = process.env;
const Blacklists = db.Blacklists; const Blacklists = db.Blacklists;
@ -16,33 +16,55 @@ const Blacklists = db.Blacklists;
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('tweet') .setName('tweet')
.setDescription('Send tweet from Haha yes twitter account. Please do not use it for advertisement and keep it english') .setDescription('Send tweet from the bot twitter account. Please do not use it for advertisement and keep it english')
.addStringOption(option => .addStringOption(option =>
option.setName('content') option.setName('content')
.setDescription('The content of the tweet you want to send me.') .setDescription('!THIS IS NOT FEEDBACK! The content of the tweet you want to send me.')
.setRequired(false)) .setRequired(false))
.addAttachmentOption(option => .addAttachmentOption(option =>
option.setName('image') option.setName('image')
.setDescription('Optional attachment (Image only.)') .setDescription('Optional attachment (Image only.)')
.setRequired(false)), .setRequired(false)),
category: 'fun',
ratelimit: 3, ratelimit: 3,
cooldown: 3600, cooldown: 86400,
async execute(interaction) { guildOnly: true,
if (!interaction.options.getString('content') && !interaction.options.getAttachment('image')) { async execute(interaction, args, client) {
const content = args.content;
const attachment = args.image;
if (!content && !attachment) {
return interaction.reply({ content: 'Uh oh! You are missing any content for me to tweet!', ephemeral: true }); return interaction.reply({ content: 'Uh oh! You are missing any content for me to tweet!', ephemeral: true });
} }
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
let tweet = interaction.options.getString('content'); let tweet = content;
const attachment = interaction.options.getAttachment('image');
const date = new Date(); const date = new Date();
// If guild is less than 1 month old don't accept the tweet
if (interaction.guild.createdAt > date.setMonth(date.getMonth() - 1)) {
await interaction.editReply({ content: 'The server need to be 1 month old to be able to use this command!' });
return;
}
// Reset the date for the next check
date.setTime(Date.now());
// If the bot has been in the guild for less than 1 week don't accept the tweet.
if (interaction.guild.createdAt > date.setDate(date.getDate() - 7)) {
await interaction.editReply({ content: 'I need to be in this server for a week to be able to use this command!' });
}
// Reset the date for the next check
date.setTime(Date.now());
// If account is less than 6 months old don't accept the tweet ( alt prevention ) // If account is less than 6 months old don't accept the tweet ( alt prevention )
if (interaction.user.createdAt > date.setMonth(date.getMonth() - 6)) { if (interaction.user.createdAt > date.setMonth(date.getMonth() - 6)) {
await interaction.editReply({ content: 'Your account is too new to be able to use this command!' }); await interaction.editReply({ content: 'Your account is too new to be able to use this command!' });
return; return;
} }
// Reset the current date so it checks correctly for the 1 year requirement. // Reset the date for the next check
date.setTime(Date.now()); date.setTime(Date.now());
// If account is less than 1 year old don't accept attachment // If account is less than 1 year old don't accept attachment
@ -51,20 +73,33 @@ export default {
return; return;
} }
// remove zero width space
if (tweet) { if (tweet) {
// remove zero width space
tweet = tweet.replace('', ''); tweet = tweet.replace('', '');
// This should only happen if someone tweets a zero width space
if (tweet.length === 0) {
return interaction.reply({ content: 'Uh oh! You are missing any content for me to tweet!', ephemeral: true });
} }
if (tweet) { wordToCensor.forEach(async word => {
// Detect banned word (Blacklist the user directly) if (tweet.toLowerCase().includes(word.toLowerCase())) {
if (wordToCensor.includes(tweet) || wordToCensor.includes(tweet.substr(0, tweet.length - 1)) || wordToCensor.includes(tweet.substr(1, tweet.length))) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' }; const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body); Blacklists.create(body);
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' }); await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return; return;
} }
});
// Detect banned word (Blacklist the user directly)
/* No worky (I don't remember what the fuck I wrote here)
if (wordToCensor.includes(tweet) || wordToCensor.includes(tweet.substring(0, tweet.length - 1)) || wordToCensor.includes(tweet.substring(1, tweet.length))) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body);
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return;
}
*/
// Very simple link detection // Very simple link detection
if (new RegExp('([a-zA-Z0-9]+://)?([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?').test(tweet) && !tweet.includes('twitter.com')) { if (new RegExp('([a-zA-Z0-9]+://)?([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?').test(tweet) && !tweet.includes('twitter.com')) {
@ -78,22 +113,23 @@ export default {
} }
} }
const T = new Twit({
consumer_key: twiConsumer, const userClient = new TwitterApi({
consumer_secret: twiConsumerSecret, appKey: twiConsumer,
access_token: twiToken, appSecret: twiConsumerSecret,
access_token_secret: twiTokenSecret, accessToken: twiToken,
accessSecret: twiTokenSecret,
}); });
try { try {
// Make sure there is an attachment and if its an image // Make sure there is an attachment and if its an image
if (attachment) { if (attachment) {
if (attachment.name.toLowerCase().endsWith('.jpg') || attachment.name.toLowerCase().endsWith('.png') || attachment.name.toLowerCase().endsWith('.gif')) { if (attachment.name.toLowerCase().endsWith('.jpg') || attachment.name.toLowerCase().endsWith('.png') || attachment.name.toLowerCase().endsWith('.gif')) {
fetch(attachment.url) const streamPipeline = util.promisify(stream.pipeline);
.then(res => { const res = await fetch(attachment.url);
const dest = fs.createWriteStream(`${os.tmpdir()}/${attachment.name}`); if (!res.ok) return interaction.editReply('An error has occured while trying to download your image.');
res.body.pipe(dest); await streamPipeline(res.body, fs.createWriteStream(`${os.tmpdir()}/${attachment.name}`));
dest.on('finish', () => {
const file = fs.statSync(`${os.tmpdir()}/${attachment.name}`); const file = fs.statSync(`${os.tmpdir()}/${attachment.name}`);
const fileSize = file.size / 1000000.0; const fileSize = file.size / 1000000.0;
@ -104,19 +140,8 @@ export default {
return interaction.editReply({ content: 'Gifs can\'t be larger than 15 MB!' }); return interaction.editReply({ content: 'Gifs can\'t be larger than 15 MB!' });
} }
const b64Image = fs.readFileSync(`${os.tmpdir()}/${attachment.name}`, { encoding: 'base64' }); const image = await userClient.v1.uploadMedia(`${os.tmpdir()}/${attachment.name}`);
T.post('media/upload', { media_data: b64Image }, function(err, data) { Tweet(image);
if (err) {
console.log('OH NO AN ERROR!!!!!!!');
console.error(err);
return interaction.editReply({ content: 'OH NO!!! AN ERROR HAS occurred!!! please hold on while i find what\'s causing this issue! ' });
}
else {
Tweet(data);
}
});
});
});
} }
else { else {
await interaction.editReply({ content: 'File type not supported, you can only send jpg/png/gif' }); await interaction.editReply({ content: 'File type not supported, you can only send jpg/png/gif' });
@ -133,70 +158,47 @@ export default {
return; return;
} }
function Tweet(data) { async function Tweet(img) {
let options = { let options = null;
status: tweet, if (img) {
}; options = { media: { media_ids: new Array(img) } };
if (data && tweet) {
options = {
status: tweet,
media_ids: new Array(data.media_id_string),
};
}
else if (data) {
options = {
media_ids: new Array(data.media_id_string),
};
} }
const tweeted = await userClient.v2.tweet(tweet, options);
T.post('statuses/update', options, function(err, response) { const tweetid = tweeted.data.id;
if (err) { const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
// Rate limit exceeded const TweetLink = `https://vxtwitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`;
if (err.code == 88) return interaction.editReply({ content: err.interaction });
// Tweet needs to be a bit shorter.
if (err.code == 186) return interaction.editReply({ content: `${err.interaction} Your interaction was ${tweet.length} characters, you need to remove ${tweet.length - 280} characters (This count may be inaccurate if your interaction contained link)` });
// Status is a duplicate.
if (err.code == 187) return interaction.editReply({ content: err.interaction });
// To protect our users from spam and other malicious activity, this account is temporarily locked.
if (err.code == 326) return interaction.editReply({ content: err.interaction });
console.error('OH NO!!!!');
console.error(err);
return interaction.editReply({ content: 'OH NO!!! AN ERROR HAS occurred!!! please hold on while i find what\'s causing this issue!' });
}
const tweetid = response.id_str; let channel = await client.channels.resolve(twiChannel);
const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'mcDotnamejeffDotxyz', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
const TweetLink = `https://twitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`;
// Im too lazy for now to make an entry in config.json
let channel = interaction.client.channels.resolve(twiChannel);
channel.send(TweetLink); channel.send(TweetLink);
const Embed = new MessageEmbed() const Embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() }) .setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setDescription(tweet) .setDescription(tweet ? tweet : 'No content.')
.addField('Link', TweetLink, true) .addFields(
.addField('Tweet ID', tweetid, true) { name: 'Link', value: TweetLink, inline: true },
.addField('Channel ID', interaction.channel.id, true) { name: 'Tweet ID', value: tweetid, inline: true },
.addField('Messsage ID', interaction.id, true) { name: 'Channel ID', value: interaction.channel.id, inline: true },
.addField('Author', `${interaction.user.username} (${interaction.user.id})`, true) { name: 'Message ID', value: interaction.id, inline: true },
{ name: 'Author', value: `${interaction.user.username} (${interaction.user.id})`, inline: true },
)
.setTimestamp(); .setTimestamp();
if (interaction.guild) { if (interaction.guild) {
Embed.addField('Guild', `${interaction.guild.name} (${interaction.guild.id})`, true); Embed.addFields(
Embed.addField('message link', `https://discord.com/channels/${interaction.guild.id}/${interaction.channel.id}/${interaction.id}`); { name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true },
{ name: 'message link', value: `https://discord.com/channels/${interaction.guild.id}/${interaction.channel.id}/${interaction.id}`, inline: true },
);
} }
else { else {
Embed.addField('message link', `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}`); Embed.addFields({ name: 'message link', value: `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}` });
} }
if (attachment) Embed.setImage(attachment.url); if (attachment) Embed.setImage(attachment.url);
channel = interaction.client.channels.resolve(twiLogChannel); channel = await client.channels.resolve(twiLogChannel);
channel.send({ embeds: [Embed] }); channel.send({ embeds: [Embed] });
return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` }); return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` });
});
} }
}, },
}; };

90
commands/fun/ytp.js Normal file
View file

@ -0,0 +1,90 @@
import { SlashCommandBuilder } from 'discord.js';
import fs from 'node:fs';
import os from 'node:os';
import YTPGenerator from 'ytpplus-node';
export default {
data: new SlashCommandBuilder()
.setName('ytp')
.setDescription('Generate a YTP')
.addBooleanOption(option =>
option.setName('force')
.setDescription('Force the generation of the video in non-nsfw channel.')
.setRequired(false)),
category: 'fun',
ratelimit: 2,
cooldown: 60,
parallelLimit: 30,
async execute(interaction, args) {
if (!interaction.channel.nsfw && !args.force) return interaction.reply(`Please execute this command in an NSFW channel ( Content might not be NSFW but since the video are user submitted better safe than sorry ) OR do \`\`${interaction.prefix}ytp --force\`\` to make the command work outside of nsfw channel BE AWARE THAT IT WON'T CHANGE THE FINAL RESULT SO NSFW CAN STILL HAPPEN`);
// Read userVid folder and select random vid and only take .mp4
const mp4 = [];
const asset = [];
// Count number of total vid
fs.readdirSync('./asset/ytp/userVid/').forEach(file => {
if (file.endsWith('mp4')) {
mp4.push(file);
}
});
const MAX_CLIPS = 20;
// Select random vid depending on the amount of MAX_CLIPS
for (let i = 0; i < MAX_CLIPS; i++) {
const random = Math.floor(Math.random() * mp4.length);
const vid = `./asset/ytp/userVid/${mp4[random]}`;
if (mp4[random].endsWith('mp4')) {
if (!asset.includes(vid)) {
asset.push(vid);
}
}
}
const loadingmsg = await interaction.reply(`Processing, this can take a ***long*** time, i'll ping you when I finished <a:loadingmin:527579785212329984>\nSome info: There are currently ${mp4.length} videos, why not add yours? You can do so with the \`\`addytp\`\` command.\nLike ytp? Why not check out https://ytp.namejeff.xyz/`);
const options = {
debug: false,
MAX_STREAM_DURATION: Math.floor((Math.random() * 3) + 1),
sources: './asset/ytp/sources/',
sounds: './asset/ytp/sounds/',
music: './asset/ytp/music/',
resources: './asset/ytp/resources/',
temp: os.tmpdir(),
sourceList: asset,
intro: args.force ? './asset/ytp/intro.mp4' : null,
outro: './asset/ytp/outro.mp4',
OUTPUT_FILE: `${os.tmpdir()}/${interaction.id}_YTP.mp4`,
MAX_CLIPS: MAX_CLIPS,
transitions: true,
showFileNames: true,
effects: {
effect_RandomSound: true,
effect_RandomSoundMute: true,
effect_Reverse: true,
effect_Chorus: true,
effect_Vibrato: true,
effect_HighPitch: true,
effect_LowPitch: true,
effect_SpeedUp: true,
effect_SlowDown: true,
effect_Dance: true,
effect_Squidward: true,
effect_How: true,
},
};
await new YTPGenerator().configurateAndGo(options)
.then(() => {
loadingmsg.delete();
return interaction.followUp({ content: 'Here is your YTP! Remember, it might contain nsfw, so be careful!', files: [`${os.tmpdir()}/${interaction.id}_YTP.mp4`] })
.catch(err => {
console.error(err);
return interaction.followUp({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] });
});
})
.catch(err => {
console.error(err);
loadingmsg.delete();
return interaction.followUp({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] });
});
},
};

View file

@ -1,20 +1,21 @@
import { SlashCommandBuilder } from '@discordjs/builders'; // TODO
// Switch to 'twitter-api-v2'
import { SlashCommandBuilder } from 'discord.js';
import Twit from 'twit'; import Twit from 'twit';
import dotenv from 'dotenv';
dotenv.config();
const { twiConsumer, twiConsumerSecret, twiToken, twiTokenSecret } = process.env; const { twiConsumer, twiConsumerSecret, twiToken, twiTokenSecret } = process.env;
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('deletewteet') .setName('deletetweet')
.setDescription('Delete a tweet') .setDescription('Delete a tweet')
.addStringOption(option => .addStringOption(option =>
option.setName('tweetid') option.setName('tweetid')
.setDescription('The id of the tweet you wish to delete.') .setDescription('The id of the tweet you wish to delete.')
.setRequired(true)), .setRequired(true)),
category: 'owner',
ownerOnly: true, ownerOnly: true,
async execute(interaction) { async execute(interaction, args) {
await interaction.deferReply(); await interaction.deferReply();
try { try {
const T = new Twit({ const T = new Twit({
@ -25,7 +26,7 @@ export default {
}); });
T.post('statuses/destroy', { T.post('statuses/destroy', {
id: interaction.options.getString('tweetid'), id: args.tweetid,
}); });
return interaction.editReply('Tweet have been deleted!'); return interaction.editReply('Tweet have been deleted!');
} }

View file

@ -1,9 +1,10 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('die') .setName('die')
.setDescription('Kill the bot'), .setDescription('Kill the bot'),
category: 'owner',
ownerOnly: true, ownerOnly: true,
async execute(interaction) { async execute(interaction) {
console.log('\x1b[31m\x1b[47m\x1b[5mSHUTING DOWN!!!!!\x1b[0m'); console.log('\x1b[31m\x1b[47m\x1b[5mSHUTING DOWN!!!!!\x1b[0m');

69
commands/owner/dm.js Normal file
View file

@ -0,0 +1,69 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
// const feedbackID = [];
export default {
data: new SlashCommandBuilder()
.setName('dm')
.setDescription('Replies with Pong!')
.addStringOption(option =>
option.setName('userid')
.setDescription('The user to who you want to send the message to.')
.setRequired(true))
.addStringOption(option =>
option.setName('message')
.setDescription('What do you want to tell them?')
.setRequired(true))
.addAttachmentOption(option =>
option.setName('image')
.setDescription('Optional attachment.')
.setRequired(false)),
category: 'owner',
ownerOnly: true,
async execute(interaction, args, client) {
/* Too lazy to implement that now (Watch it rest untouched for months)
async function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const uuid = uuidv4();
feedbackID[uuid] = args.message;
*/
await client.users.fetch(args.userid);
const user = client.users.resolve(args.userid);
if (!user) return interaction.reply('Not a valid ID');
const text = args.message;
const Embed = new EmbedBuilder()
.setTitle('You received a message from the developer!')
.setDescription(text)
.setFooter({ text: `If you wish to respond use the following command: ${interaction.prefix}feedback <message>` })
.setTimestamp();
user.send({ embeds: [Embed] });
return interaction.reply({ content: `DM sent to ${user.username} (${user.id})` });
/*
const Attachment = (message.attachments).array();
if (Attachment[0]) {
client.users.resolve(user).send(Embed, { files: [Attachment[0].url] })
.then(() => {
return interaction.reply(`DM sent to ${user.username}`);
})
.catch(() => {
return interaction.reply(`Could not send a DM to ${user.username}`);
});
}
else {
client.users.resolve(user).send(Embed)
.then(() => {
return interaction.reply(`DM sent to ${user.username}`);
})
.catch(() => {
return interaction.reply(`Could not send a DM to ${user.username}`);
});
}
*/
},
};

View file

@ -0,0 +1,30 @@
import { SlashCommandBuilder } from 'discord.js';
import util from 'node:util';
import stream from 'node:stream';
import fs from 'node:fs';
export default {
data: new SlashCommandBuilder()
.setName('downloadandload')
.setDescription('Download a command and load it.')
.addAttachmentOption(option =>
option.setName('file')
.setDescription('The .js file that will be loaded by the bot.')
.setRequired(true)),
category: 'owner',
ownerOnly: true,
async execute(interaction, args, client) {
await interaction.deferReply();
const streamPipeline = util.promisify(stream.pipeline);
const res = await fetch(args.file.url);
if (!res.ok) return interaction.editReply('An error has occured while trying to download the command.');
await streamPipeline(res.body, fs.createWriteStream(`./tmp/${args.file.name}`));
let command = await import(`../../tmp/${args.file.name}`);
command = command.default;
client.commands.set(command.data.name, command);
return await interaction.editReply(`${command.data.name} has been loaded.`);
},
};

22
commands/owner/load.js Normal file
View file

@ -0,0 +1,22 @@
import { SlashCommandBuilder } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('load')
.setDescription('load a command.')
.addStringOption(option =>
option.setName('file')
.setDescription('File location of the command.')
.setRequired(true)),
category: 'owner',
ownerOnly: true,
async execute(interaction, args, client) {
await interaction.deferReply();
let command = await import(`../../${args.file}`);
command = command.default;
client.commands.set(command.data.name, command);
return await interaction.editReply(`${command.data.name} has been loaded.`);
},
};

View file

@ -1,5 +1,4 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { ButtonStyle, SlashCommandBuilder, ButtonBuilder, ActionRowBuilder } from 'discord.js';
import { MessageButton, MessageActionRow } from 'discord.js';
import db from '../../models/index.js'; import db from '../../models/index.js';
const Blacklists = db.Blacklists; const Blacklists = db.Blacklists;
@ -19,45 +18,67 @@ export default {
option.setName('reason') option.setName('reason')
.setDescription('The reason of the blacklist.') .setDescription('The reason of the blacklist.')
.setRequired(false)), .setRequired(false)),
category: 'owner',
ownerOnly: true, ownerOnly: true,
async execute(interaction) { async execute(interaction, args) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
const client = interaction.client; const client = interaction.client;
const command = interaction.options.getString('command'); const command = args.command;
const userid = interaction.options.getString('userid'); const userid = args.userid;
const reason = interaction.options.getString('reason'); const reason = args.reason ? args.reason : 'No reason has been specified.';
const blacklist = await Blacklists.findOne({ where: { type:command, uid:userid } }); const blacklist = await Blacklists.findOne({ where: { type:command, uid:userid } });
if (!blacklist) { if (!blacklist) {
const body = { type:command, uid: userid, reason: reason }; const body = { type:command, uid: userid, reason: reason };
Blacklists.create(body); Blacklists.create(body);
let user = userid; if (command === 'guild') {
if (command !== 'guild') {user = client.users.resolve(userid).tag;} const guildid = userid;
await client.guilds.fetch(guildid);
const guild = client.guilds.resolve(guildid).name;
return interaction.editReply(`The guild ${guild} (${guildid}) has been blacklisted with the following reason \`${reason}\``);
return interaction.editReply(`${user} has been blacklisted from ${command} with the following reason ${reason}`);
} }
else { else {
const row = new MessageActionRow() let user = userid;
await client.users.fetch(userid);
user = client.users.resolve(userid).username;
return interaction.editReply(`${user} (${userid}) has been blacklisted from ${command} with the following reason \`${reason}\``);
}
}
else {
const row = new ActionRowBuilder()
.addComponents( .addComponents(
new MessageButton() new ButtonBuilder()
.setCustomId('yes') .setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes') .setLabel('Yes')
.setStyle('PRIMARY'), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new MessageButton() new ButtonBuilder()
.setCustomId('no') .setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No') .setLabel('No')
.setStyle('DANGER'), .setStyle(ButtonStyle.Danger),
); );
await interaction.editReply({ content: 'This user is already blacklisted, do you want to unblacklist him?', ephemeral: true, components: [row] }); await interaction.editReply({ content: 'This user is already blacklisted, do you want to unblacklist him?', ephemeral: true, components: [row] });
interaction.client.once('interactionCreate', async (interactionMenu) => { return listenButton(client, interaction, command, userid, interaction.user);
if (!interactionMenu.isButton) return; }
interactionMenu.update({ components: [] }); },
if (interactionMenu.customId === 'yes') { };
async function listenButton(client, interaction, command, userid, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, command, userid, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}${originalId}`) {
Blacklists.destroy({ where: { type:command, uid:userid } }); Blacklists.destroy({ where: { type:command, uid:userid } });
return interaction.editReply(`The following ID have been unblacklisted from ${command}: ${userid}`); return interaction.editReply(`The following ID have been unblacklisted from ${command}: ${userid}`);
} }
@ -65,6 +86,4 @@ export default {
return interaction.editReply('No one has been unblacklisted.'); return interaction.editReply('No one has been unblacklisted.');
} }
}); });
} }
},
};

50
commands/owner/unload.js Normal file
View file

@ -0,0 +1,50 @@
import { SlashCommandBuilder } from 'discord.js';
import fs from 'node:fs';
export default {
data: new SlashCommandBuilder()
.setName('unload')
.setDescription('Unload a command and replace it with a placeholder')
.addStringOption(option =>
option.setName('commandname')
.setDescription('The command to unload.')
.setRequired(true))
.addStringOption(option =>
option.setName('placeholder')
.setDescription('The placeholder message you want for the command.'))
.addBooleanOption(option =>
option.setName('nofile')
.setDescription('Don\'t create the placeholder file')),
category: 'owner',
ownerOnly: true,
async execute(interaction, args, client) {
await interaction.deferReply();
if (!client.commands.has(args.commandname)) return await interaction.editReply('Command not found.');
if (!args.placeholder) args.placeholder = 'This command is unloaded, please check back later.';
if (!args.nofile) {
fs.writeFileSync(`./unloaded/${args.commandname}.js`, `
import { SlashCommandBuilder } from 'discord.js';
export default {
data: ${JSON.stringify(client.commands.get(args.commandname).data)},
category: '${client.commands.get(args.commandname).category}',
async execute(interaction) {
return interaction.reply('${args.placeholder.replace(/'/g, '\\\'')}');
},
};
`);
}
client.commands.delete(args.commandname);
if (!args.nofile) {
let command = await import(`../../unloaded/${args.commandname}.js`);
command = command.default;
client.commands.set(args.commandname, command);
}
return await interaction.editReply(`${args.commandname} has been unloaded.`);
},
};

90
commands/secret/s.js Normal file
View file

@ -0,0 +1,90 @@
/* eslint-disable no-case-declarations */
import { SlashCommandBuilder } from 'discord.js';
const { ownerId } = process.env;
export default {
data: new SlashCommandBuilder()
.setName('s')
.setDescription('What could this be 🤫')
.addStringOption(option =>
option.setName('something')
.setDescription('🤫')
.setRequired(true))
.addStringOption(option =>
option.setName('somethingelse')
.setDescription('🤫')
.setRequired(false)),
category: 'secret',
async execute(interaction, args, client) {
const command = args.something;
switch (command) {
case 'levertowned':
return interaction.reply('Hello buddy bro <:youngtroll:488559163832795136> <@434762632004894746>');
case 'owned':
const epicMessage = ['You cheated not ONLY the GAME, BUT yourself. You didn\'t GROW. You didn\'t IMPROVE. You TOOK a SHORTCUT and gained NOTHING. You EXPERIENCED a HOLLOW victory. NOTHING WAS risked and NOTHING WAS gained. It\'s SAD that you don\'t KNOW the DIFFERENCE.', 'TROLOLOLO OWNED EPIC STYLE', 'Owned noob', 'HAHA BRO YOU JUST GOT OOOOOOOOWNED HAHAHAHAHHAHA NOOOB NOOOOB NOOOB OWNED NOOB <:youngtroll:488559163832795136>', '<a:op:516341492982218756> op op op owned epic style <a:op:516341492982218756>', 'HAHAHA BRO YOU HAVE BEEN OWNED TROLL BRO STYLE', 'OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED YOU JUST GOT OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED YOU JUST GOT OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED YOU JUST GOT OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED OWNED YOU JUST GOT OWNED', 'OWNED\nFUCKING OWNED', 'OWNED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!', 'You just got owned <a:troll:525724709833277442>', 'you just been....epicly trolled <:trole:241942993022615552>', 'I hope you have a nice day, just a simple reminder that I love you and I think you\'re pretty epic!!!!'];
const ownedMessage = epicMessage[Math.floor(Math.random() * epicMessage.length)];
await interaction.guild.members.fetch();
let owned = args.somethingelse ? interaction.guild.members.cache.find(u => u.user.username.toLowerCase().includes(args.somethingelse.toLowerCase())) : null;
if (interaction.isMessage) {
if (interaction.mentions.members.first()) {
console.log('test');
owned = interaction.mentions.members.first();
}
}
if (args.somethingelse) {
if (owned.id === client.user.id) {
return interaction.reply('You really thought you could own me?, pathetic...');
}
else if (owned.id === ownerId) {
return interaction.reply('You really thought you could own him?, pathetic...');
}
else if (owned.id === '286054184623538177' || owned.id === '172112210863194113') {
owned = interaction.user;
}
if (ownedMessage === epicMessage[0]) {
return interaction.reply(ownedMessage);
}
return interaction.reply(`${owned}, ${ownedMessage}`);
}
else {
return interaction.reply(ownedMessage);
}
case 'fartpiss':
if (interaction.guild.id != '240843640375607296') {
return;
}
await interaction.guild.members.fetch();
const member = args.somethingelse ? interaction.guild.members.cache.find(u => u.user.username.toLowerCase().includes(args.somethingelse.toLowerCase())) : null;
if (member) {
return await member.setNickname('fart piss')
.then(() => interaction.reply(`sucessfully fart pissed on ${member.user.username} <:youngtroll:488559163832795136>`))
.catch((error) => {
if (process.env.NODE_ENV === 'development') console.error(error);
interaction.reply(`Sorry i could not fart piss on ${member.user.username} :(`);
});
}
else {
return await interaction.member.setNickname('fart piss')
.then(() => interaction.reply('sucessfully fart pissed on you <:youngtroll:488559163832795136>'))
.catch((error) => {
if (process.env.NODE_ENV === 'development') console.error(error);
interaction.reply('Sorry i could not fart piss on you :(');
});
}
default:
break;
}
},
};

View file

@ -1,31 +1,31 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { exec } from 'node:child_process'; import { execFile } from 'node:child_process';
import db from '../../models/index.js'; import db from '../../models/index.js';
const donator = db.donator; const donator = db.donator;
import dotenv from 'dotenv'; const { ownerId, uptimePage } = process.env;
dotenv.config();
const { ownerId } = process.env;
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('about') .setName('about')
.setDescription('About me (The bot)'), .setDescription('About me (The bot)'),
category: 'utility',
async execute(interaction) { async execute(interaction) {
const Donator = await donator.findAll({ order: ['id'] }); const Donator = await donator.findAll({ order: ['id'] });
const client = interaction.client; const client = interaction.client;
const tina = await client.users.fetch('336492042299637771'); const tina = await client.users.fetch('336492042299637771');
const owner = await client.users.fetch('267065637183029248'); const creator = await client.users.fetch('267065637183029248');
const maintainer = await client.users.fetch(ownerId); const maintainer = await client.users.fetch(ownerId);
let description = `This bot is made using [discord.js](https://github.com/discordjs/discord.js)\nThanks to ${tina.tag} (336492042299637771) for inspiring me for making this bot!\n\nThe people who donated for the bot <3\n`; let description = 'I\'m a fun multipurpose bot made using [discord.js](https://github.com/discordjs/discord.js)'
+ '\nFor a better experience use the slash commands!\n\nThe people who donated for the bot <3\n';
if (Donator[0]) { if (Donator[0]) {
for (let i = 0; i < Donator.length; i++) { for (let i = 0; i < Donator.length; i++) {
const user = await client.users.fetch(Donator[i].get('userID').toString()); const user = await client.users.fetch(Donator[i].get('userID').toString());
if (user !== null) { if (user !== null) {
description += `**${user.tag} (${user.id}) | ${Donator[i].get('comment')}**\n`; description += `**${user.username} (${user.id}) | ${Donator[i].get('comment')}**\n`;
} }
else { else {
description += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`; description += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`;
@ -36,20 +36,26 @@ export default {
description += 'No one :(\n'; description += 'No one :(\n';
} }
description += `\nThanks to ${tina.username} (336492042299637771) for inspiring me for making this bot!`;
// description += '\nThanks to Jetbrains for providing their IDE!'; // description += '\nThanks to Jetbrains for providing their IDE!';
exec('git rev-parse --short HEAD', (err, stdout) => { execFile('git', ['rev-parse', '--short', 'HEAD'], (err, stdout) => {
const aboutEmbed = new MessageEmbed() const aboutEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY') .setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setAuthor({ name: client.user.tag, iconURL: client.user.displayAvatarURL(), url: 'https://libtar.de' }) .setAuthor({ name: client.user.username, iconURL: client.user.displayAvatarURL(), url: 'https://libtar.de' })
.setTitle('About me') .setTitle('About me')
.setDescription(description) .setDescription(description)
.addField('Current commit', stdout) .addFields(
.addField('Current maintainer: ', `${maintainer.tag} (${ownerId})`) { name: 'Current commit', value: stdout },
.addField('Gitea (Main)', 'https://git.namejeff.xyz/Supositware/Haha-Yes', true) { name: 'Current maintainer', value: `${maintainer.username} (${ownerId})` },
.addField('Github (Mirror)', 'https://github.com/Supositware/Haha-yes', true) { name: 'Gitea (Main)', value: 'https://git.namejeff.xyz/Supositware/Haha-Yes', inline: true },
.addField('Privacy Policy', 'https://libtar.de/discordprivacy.txt') { name: 'Github (Mirror)', value: 'https://github.com/Supositware/Haha-yes', inline: true },
.setFooter({ text: `Original bot made by ${owner.tag} (267065637183029248)` }); { name: 'Privacy Policy', value: 'https://libtar.de/discordprivacy.txt', inline: true },
{ name: 'Status page', value: uptimePage.toString(), inline: true },
)
.setFooter({ text: `Original bot made by ${creator.username} (267065637183029248)` });
interaction.reply({ embeds: [aboutEmbed] }); interaction.reply({ embeds: [aboutEmbed] });
}); });

View file

@ -0,0 +1,99 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
import utils from '../../utils/videos.js';
import fs from 'node:fs';
import os from 'node:os';
const { ytpChannelId } = process.env;
export default {
data: new SlashCommandBuilder()
.setName('addytp')
.setDescription('Add a video to the pool of ytps. You can add 5 per day.')
.addStringOption(option =>
option.setName('url')
.setDescription('URL of the video you want to add.')
.setRequired(true)),
category: 'utility',
ratelimit: 5,
cooldown: 86400,
async execute(interaction, args) {
const url = args.url;
// This is rather rudementary, a proper way would be using yt-dlp to know if it is a playlist
if (url.includes('list=')) {
return interaction.reply({ content: '❌ Playlists are not allowed!', ephemeral: true });
}
if (!await utils.stringIsAValidurl(url)) {
console.error(`Not a url!!! ${url}`);
return interaction.reply({ content: '❌ This does not look like a valid url!', ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
utils.downloadVideo(url, interaction.id, 'bestvideo[height<=?480]+bestaudio/best')
.then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
const output = `${os.tmpdir()}/${file}`;
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
if (fileSize > 50) {
// await interaction.deleteReply();
await interaction.editReply({ content: '❌ Uh oh! The video is too big!', ephemeral: true });
}
else {
// CopyFile instead of rename in case you have /tmp and the asset folder on different a disk.
fs.copyFileSync(output, `./asset/ytp/userVid/${file}`);
const mp4 = [];
fs.readdirSync('./asset/ytp/userVid/').forEach(f => {
if (f.endsWith('mp4')) {
mp4.push(f);
}
});
// (Hopefully) limit video to 2k
if (mp4.length > 2000) {
const f = mp4.sort((a, b) => {
const time1 = fs.statSync(`./asset/ytp/userVid/${b}`).ctime;
const time2 = fs.statSync(`./asset/ytp/userVid/${a}`).ctime;
if (time1 < time2) return 1;
if (time1 > time2) return -1;
return 0;
}).slice(0, 1);
fs.unlinkSync(`./asset/ytp/userVid/${f[0]}`);
}
interaction.editReply({ content: `Video successfully added to the pool! There is now ${mp4.length} videos`, ephemeral: true });
const Embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.addFields([
{ name: 'Channel ID', value: interaction.channel.id.toString(), inline: true },
{ name: 'Message ID', value: interaction.id.toString(), inline: true },
{ name: 'Author', value: `${interaction.user.username} (${interaction.user.id})`, inline: true },
])
.setTimestamp();
if (interaction.guild) {
Embed.addFields([
{ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true },
{ name: 'Message link', value: `https://discord.com/channels/${interaction.guild.id}/${interaction.channel.id}/${interaction.id}` },
]);
}
else {
Embed.addFields([
{ name: 'Message link', value: `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}` },
]);
}
const channel = interaction.client.channels.resolve(ytpChannelId);
// Send as 2 separate message otherwise the url won't get embedded.
channel.send({ content: url });
return channel.send({ embeds: [Embed] });
}
});
},
};

View file

@ -0,0 +1,49 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('avatar')
.setDescription('Show user avatar')
.addMentionableOption(option =>
option.setName('member')
.setDescription('Who do you want to fake?')
.setRequired(false)),
category: 'utility',
async execute(interaction, args) {
const avatarEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle('Avatar');
if (!args.member) {
const format = interaction.user.displayAvatarURL({ dynamic: true }).substr(interaction.user.displayAvatarURL({ dynamic: true }).length - 3);
if (format == 'gif') {
avatarEmbed.setAuthor({ name: interaction.user.username });
avatarEmbed.setDescription(`[gif](${interaction.user.displayAvatarURL({ format: 'gif', size: 2048 })})`);
avatarEmbed.setImage(interaction.user.displayAvatarURL({ format: 'gif', size: 2048 }));
}
else {
avatarEmbed.setAuthor({ name: interaction.user.username });
avatarEmbed.setDescription(`[png](${interaction.user.displayAvatarURL({ format: 'png', size: 2048 })}) | [jpeg](${interaction.user.displayAvatarURL({ format: 'jpg', size: 2048 })}) | [webp](${interaction.user.displayAvatarURL({ format: 'webp', size: 2048 })})`);
avatarEmbed.setImage(interaction.user.displayAvatarURL({ format: 'png', size: 2048 }));
}
return interaction.reply({ embeds: [avatarEmbed] });
}
else {
await interaction.guild.members.fetch();
const format = args.member.displayAvatarURL({ dynamic: true }).substr(args.member.displayAvatarURL({ dynamic: true }).length - 3);
if (format == 'gif') {
avatarEmbed.setAuthor({ name: args.member.user.username });
avatarEmbed.setDescription(`[gif](${args.member.displayAvatarURL({ format: 'gif', size: 2048 })})`);
avatarEmbed.setImage(args.member.displayAvatarURL({ format: 'gif', size: 2048 }));
}
else {
avatarEmbed.setAuthor({ name: args.member.user.username });
avatarEmbed.setDescription(`[png](${args.member.displayAvatarURL({ format: 'png', size: 2048 })}) | [jpeg](${args.member.displayAvatarURL({ format: 'jpg', size: 2048 })}) | [webp](${args.member.displayAvatarURL({ format: 'webp', size: 2048 })})`);
avatarEmbed.setImage(args.member.displayAvatarURL({ format: 'png', size: 2048 }));
}
return interaction.reply({ embeds: [avatarEmbed] });
}
},
};

View file

@ -1,19 +1,20 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import donations from '../../json/donations.json' assert {type: 'json'}; import donations from '../../json/donations.json' with {type: 'json'};
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('donate') .setName('donate')
.setDescription('Show donation link for the bot.'), .setDescription('Show donation link for the bot.'),
category: 'utility',
async execute(interaction) { async execute(interaction) {
let desc = 'If you decide to donate, please do /feedback to let the owner know about it so he can put you in the about and donator command.'; let desc = 'If you decide to donate, please do /feedback to let the owner know about it so he can put you in the about and donator command.';
donations.forEach(m => { donations.forEach(m => {
desc += `\n${m}`; desc += `\n${m}`;
}); });
const Embed = new MessageEmbed() const Embed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY') .setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle('Donation link') .setTitle('Donation link')
.setDescription(desc); .setDescription(desc);

View file

@ -1,4 +1,4 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import db from '../../models/index.js'; import db from '../../models/index.js';
const donator = db.donator; const donator = db.donator;
@ -6,6 +6,7 @@ export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('donator') .setName('donator')
.setDescription('All the people who donated for this bot <3'), .setDescription('All the people who donated for this bot <3'),
category: 'utility',
async execute(interaction) { async execute(interaction) {
await interaction.deferReply(); await interaction.deferReply();
const client = interaction.client; const client = interaction.client;
@ -16,7 +17,7 @@ export default {
if (Donator[0]) { if (Donator[0]) {
for (let i = 0; i < Donator.length; i++) { for (let i = 0; i < Donator.length; i++) {
const user = await client.users.fetch(Donator[i].get('userID').toString()); const user = await client.users.fetch(Donator[i].get('userID').toString());
if (user !== null) {donatorMessage += `**${user.tag} (${user.id}) | ${Donator[i].get('comment')}**\n`;} if (user !== null) {donatorMessage += `**${user.username} (${user.id}) | ${Donator[i].get('comment')}**\n`;}
else {donatorMessage += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`;} else {donatorMessage += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`;}
} }

View file

@ -1,10 +1,17 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js';
import { MessageEmbed, MessageActionRow, MessageSelectMenu } from 'discord.js'; import { execFile } from 'node:child_process';
import { exec } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import utils from '../../utils/videos.js'; import utils from '../../utils/videos.js';
let client;
let maxFileSize;
let { ytdlpMaxResolution } = process.env;
const { proxy } = process.env;
// Convert to number as process.env is always a string
ytdlpMaxResolution = Number(ytdlpMaxResolution);
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('download') .setName('download')
@ -19,21 +26,47 @@ export default {
.setRequired(false)) .setRequired(false))
.addBooleanOption(option => .addBooleanOption(option =>
option.setName('compress') option.setName('compress')
.setDescription('Compress the video?') .setDescription('Compress the video.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('autocrop')
.setDescription('Autocrop borders on videos. Ignored when using compress option.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('description')
.setDescription('Include the video description.')
.setRequired(false)), .setRequired(false)),
category: 'utility',
alias: ['dl'],
integration_types: [0, 1],
async execute(interaction, args, c) {
client = c;
const url = args.url;
const format = args.format;
maxFileSize = await utils.getMaxFileSize(interaction.guild);
interaction.doCompress = args.compress;
interaction.doAutocrop = args.autocrop;
async execute(interaction) {
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
const url = interaction.options.getString('url');
if (interaction.isMessage) {
interaction.delete();
}
if (!await utils.stringIsAValidurl(url)) { if (!await utils.stringIsAValidurl(url)) {
console.error(`Not a url!!! ${url}`); console.error(`Not a url!!! ${url}`);
return interaction.editReply({ content: '❌ This does not look like a valid url!', ephemeral: true }); return interaction.editReply({ content: '❌ This does not look like a valid url!', ephemeral: true });
} }
if (interaction.options.getBoolean('format')) { if (format) {
let qualitys = await new Promise((resolve, reject) => { let qualitys = await new Promise((resolve, reject) => {
exec(`./bin/yt-dlp "${url}" --print "%()j"`, (err, stdout, stderr) => { const options = [url, '--print', '%()j'];
if (proxy) {
options.push('--proxy');
options.push(proxy);
};
execFile('./bin/yt-dlp', options, (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -43,11 +76,12 @@ export default {
resolve(stdout); resolve(stdout);
}); });
}); });
qualitys = JSON.parse(qualitys);
qualitys = JSON.parse(qualitys);
const options = []; const options = [];
qualitys.formats.forEach(f => { qualitys.formats.forEach(f => {
if (f.format.includes('storyboard')) return;
options.push({ options.push({
label: f.resolution ? f.resolution : 'Unknown format', label: f.resolution ? f.resolution : 'Unknown format',
description: `${f.format} V: ${f.vcodec} A: ${f.acodec}`, description: `${f.format} V: ${f.vcodec} A: ${f.acodec}`,
@ -71,10 +105,10 @@ export default {
options.reverse(); options.reverse();
} }
const row = new MessageActionRow() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new MessageSelectMenu() new StringSelectMenuBuilder()
.setCustomId('downloadQuality') .setCustomId(`downloadQuality${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected') .setPlaceholder('Nothing selected')
.setMinValues(1) .setMinValues(1)
.setMaxValues(2) .setMaxValues(2)
@ -84,27 +118,40 @@ export default {
await interaction.deleteReply(); await interaction.deleteReply();
await interaction.followUp({ content: 'Which quality do you want?', ephemeral: true, components: [row] }); await interaction.followUp({ content: 'Which quality do you want?', ephemeral: true, components: [row] });
interaction.client.once('interactionCreate', async (interactionMenu) => { client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return; if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === 'downloadQuality') { if (interactionMenu.customId === `downloadQuality${interaction.user.id}${interaction.id}`) {
await interactionMenu.deferReply({ ephemeral: false }); await interactionMenu.deferReply({ ephemeral: false });
download(url, interactionMenu, interaction);
await checkSize(url, interactionMenu.values[0], args, interaction);
return download(url, interactionMenu, interaction, undefined, true);
} }
}); });
return; return;
} }
download(url, interaction); const newFormat = await checkSize(url, undefined, args, interaction);
return download(url, interaction, interaction, newFormat, args.description);
}, },
}; };
async function download(url, interaction, originalInteraction) { async function download(url, interaction, originalInteraction, format = undefined, description = false) {
let format = 'bestvideo*+bestaudio/best'; let embedColour = 'Navy';
const Embed = new MessageEmbed() if (interaction.member) {
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY') if (interaction.member.displayHexColor) {
.setAuthor({ name: `Downloaded by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL(), url: url }) embedColour = interaction.member.displayHexColor;
.setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.tag}" message!` }); }
}
const Embed = new EmbedBuilder()
.setColor(embedColour)
.setAuthor({ name: `Downloaded by ${interaction.user.username}`, iconURL: interaction.user.displayAvatarURL(), url: url })
.setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.username}" message!` });
if (interaction.customId === 'downloadQuality') { if (description) {
Embed.setDescription(await getVideoDescription(url));
}
if (interaction.customId === `downloadQuality${interaction.user.id}${originalInteraction.id}` && !format) {
format = interaction.values[0]; format = interaction.values[0];
if (interaction.values[1]) format += '+' + interaction.values[1]; if (interaction.values[1]) format += '+' + interaction.values[1];
} }
@ -112,13 +159,11 @@ async function download(url, interaction, originalInteraction) {
utils.downloadVideo(url, interaction.id, format) utils.downloadVideo(url, interaction.id, format)
.then(async () => { .then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id)); const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
const output = `${os.tmpdir()}/${file}`; let output = `${os.tmpdir()}/${file}`;
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
const compressInteraction = originalInteraction ? originalInteraction : interaction; const compressInteraction = originalInteraction ? originalInteraction : interaction;
if (compressInteraction.options.getBoolean('compress')) { if (compressInteraction.doCompress) {
const presets = [ 'Discord Tiny 5 Minutes 240p30', 'Discord Small 2 Minutes 360p30', 'Discord Nitro Small 10-20 Minutes 480p30', 'Discord Nitro Medium 5-10 Minutes 720p30', 'Discord Nitro Large 3-6 Minutes 1080p30' ]; const presets = [ 'Social 25 MB 5 Minutes 360p60', 'Social 25 MB 2 Minutes 540p60', 'Social 25 MB 1 Minute 720p60', 'Social 25 MB 30 Seconds 1080p60' ];
const options = []; const options = [];
presets.forEach(p => { presets.forEach(p => {
@ -128,41 +173,88 @@ async function download(url, interaction, originalInteraction) {
}); });
}); });
const row = new MessageActionRow() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new MessageSelectMenu() new StringSelectMenuBuilder()
.setCustomId('preset') .setCustomId(`preset${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected') .setPlaceholder('Nothing selected')
.addOptions(options), .addOptions(options),
); );
await interaction.deleteReply(); await interaction.deleteReply();
await interaction.followUp({ content: 'Which compression preset do you want?', ephemeral: true, components: [row] }); await interaction.followUp({ content: 'Which compression preset do you want?', ephemeral: true, components: [row] });
interaction.client.once('interactionCreate', async (interactionMenu) => { client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return; if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === 'preset') { if (interactionMenu.customId === `preset${interaction.user.id}${interaction.id}`) {
await interactionMenu.deferReply({ ephemeral: false }); await interactionMenu.deferReply({ ephemeral: false });
compress(file, interactionMenu, Embed); compress(file, interactionMenu, Embed);
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
} }
}); });
return; return;
} }
// If the video format is not one compatible with Discord, reencode it unless autocrop is choosen in which case it gets reencoded anyway.
if (!interaction.doAutocrop) {
const bannedFormats = ['av1'];
const codec = await utils.getVideoCodec(output);
if (bannedFormats.includes(codec)) {
const oldOutput = output;
output = `${os.tmpdir()}/264${file}`;
await utils.ffmpeg(['-i', oldOutput, '-vcodec', 'libx264', '-acodec', 'aac', output]);
}
}
else if (interaction.doAutocrop && !compressInteraction.doCompress) {
const oldOutput = output;
output = `${os.tmpdir()}/autocrop${file}`;
await utils.autoCrop(oldOutput, output);
}
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
Embed.setAuthor({ name: `${Embed.data.author.name} (${fileSize.toFixed(2)} MB)`, iconURL: Embed.data.author.icon_url, url: Embed.data.author.url });
let message = null;
if (interaction.isMessage && interaction.reference !== null) {
const channel = client.channels.resolve(interaction.reference.channelId);
message = await channel.messages.fetch(interaction.reference.messageId);
}
if (fileSize > 100) { if (fileSize > 100) {
await interaction.deleteReply(); await interaction.deleteReply();
await interaction.followUp('Uh oh! The video you tried to download is too big!', { ephemeral: true }); await interaction.followUp('Uh oh! The video you tried to download is too big!', { ephemeral: true });
} }
else if (fileSize > 8) { else if (fileSize > maxFileSize) {
const fileurl = await utils.upload(output) const fileurl = await utils.upload(output)
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
await interaction.editReply({ content: 'File was bigger than 8 mb. It has been uploaded to an external site.', embeds: [Embed], ephemeral: false });
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, embeds: [Embed], ephemeral: false });
if (interaction.isMessage && message) {
await message.reply({ content: fileurl });
}
else {
await interaction.followUp({ content: fileurl, ephemeral: false }); await interaction.followUp({ content: fileurl, ephemeral: false });
} }
}
else if (interaction.isMessage && message) {
await message.reply({ embeds: [Embed], files: [output] });
}
else { else {
await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false }); await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false });
} }
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
}) })
.catch(async err => { .catch(async err => {
console.error(err); console.error(err);
@ -174,16 +266,69 @@ async function download(url, interaction, originalInteraction) {
async function compress(input, interaction, embed) { async function compress(input, interaction, embed) {
const output = `compressed${input}.mp4`; const output = `compressed${input}.mp4`;
// Delete the file as it apparently don't overwrite?
if (fs.existsSync(output)) {
fs.rmSync(output);
}
utils.compressVideo(`${os.tmpdir()}/${input}`, output, interaction.values[0]) utils.compressVideo(`${os.tmpdir()}/${input}`, output, interaction.values[0])
.then(async () => { .then(async () => {
const fileStat = fs.statSync(`${os.tmpdir()}/${output}`); const fileStat = fs.statSync(`${os.tmpdir()}/${output}`);
const fileSize = fileStat.size / 1000000.0; const fileSize = fileStat.size / 1000000.0;
if (fileSize > 8) { embed.setAuthor({ name: `${embed.data.author.name} (${fileSize.toFixed(2)} MB)`, iconURL: embed.data.author.icon_url, url: embed.data.author.url });
await interaction.editReply({ content: 'File was bigger than 8 mb. but due to the compression it is not being uploaded externally.', ephemeral: true });
if (fileSize > maxFileSize) {
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, ephemeral: false });
} }
else { else {
await interaction.editReply({ embeds: [embed], files: [`${os.tmpdir()}/${output}`], ephemeral: false }); await interaction.editReply({ embeds: [embed], files: [`${os.tmpdir()}/${output}`], ephemeral: false });
} }
}); });
} }
async function checkSize(url, format, args, interaction, tries = 0) {
const resolutions = [144, 240, 360, 480, 720, 1080, 1440, 2160];
while (tries < 4) {
format = `bestvideo[height<=?${resolutions[resolutions.indexOf(ytdlpMaxResolution) - tries]}]+bestaudio/best`;
const aproxFileSize = await utils.getVideoSize(url, format);
if (isNaN(aproxFileSize)) return format;
if (format || tries >= 4) {
if (aproxFileSize > 100 && !args.compress && tries > 4) {
return await interaction.followUp(`Uh oh! The video you tried to download is larger than 100 mb (is ${aproxFileSize} mb)! Try again with a lower resolution format.`);
}
else if (aproxFileSize > 500 && tries > 4) {
return await interaction.followUp(`Uh oh! The video you tried to download is larger than 500 mb (is ${aproxFileSize} mb)! Try again with a lower resolution format.`);
}
}
if (aproxFileSize < 100) {
return format;
}
if (tries < 4 && aproxFileSize > 100) {
tries++;
}
}
}
async function getVideoDescription(urlArg) {
return await new Promise((resolve, reject) => {
const options = [urlArg, '--no-warnings', '-O', '%(description)s'];
if (proxy) {
options.push('--proxy');
options.push(proxy);
};
execFile('./bin/yt-dlp', options, (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout.slice(0, 240));
});
});
}

View file

@ -1,5 +1,5 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
const { feedbackChannelId } = process.env; const { feedbackChannelId } = process.env;
@ -10,14 +10,19 @@ export default {
.addStringOption(option => .addStringOption(option =>
option.setName('feedback') option.setName('feedback')
.setDescription('The message you want to send me.') .setDescription('The message you want to send me.')
.setRequired(true)), .setRequired(true))
async execute(interaction) { .addAttachmentOption(option =>
const Embed = new MessageEmbed() option.setName('image')
.setAuthor({ name: `${interaction.user.tag} (${interaction.user.id})`, iconURL: interaction.user.avatarURL() }) .setDescription('Optional attachment.')
.setRequired(false)),
category: 'utility',
async execute(interaction, args) {
const Embed = new EmbedBuilder()
.setAuthor({ name: `${interaction.user.username} (${interaction.user.id})`, iconURL: interaction.user.avatarURL() })
.setTimestamp(); .setTimestamp();
if (interaction.guild) Embed.addField('Guild', `${interaction.guild.name} (${interaction.guild.id})`, true); if (interaction.guild) Embed.addFields({ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true });
Embed.addField('Feedback', interaction.options.getString('feedback')); Embed.addFields({ name: 'Feedback', value: args.feedback, inline: true });
// Don't let new account use this command to prevent spam // Don't let new account use this command to prevent spam
const date = new Date(); const date = new Date();
@ -26,7 +31,12 @@ export default {
} }
const channel = interaction.client.channels.resolve(feedbackChannelId); const channel = interaction.client.channels.resolve(feedbackChannelId);
if (args.image) {
channel.send({ embeds: [Embed], files: [args.image] });
}
else {
channel.send({ embeds: [Embed] }); channel.send({ embeds: [Embed] });
}
await interaction.reply({ content: 'Your feedback has been sent! Don\'t forget to have dm open if you want to get an answer from the dev!', ephemeral: true }); await interaction.reply({ content: 'Your feedback has been sent! Don\'t forget to have dm open if you want to get an answer from the dev!', ephemeral: true });
}, },
}; };

164
commands/utility/help.js Normal file
View file

@ -0,0 +1,164 @@
import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, PermissionsBitField } from 'discord.js';
import fs from 'node:fs';
import ratelimiter from '../../utils/ratelimiter.js';
const { ownerId, prefix } = process.env;
const prefixs = prefix.split(',');
export default {
data: new SlashCommandBuilder()
.setName('help')
.setDescription('Displays a list of commands or information about a command.')
.addStringOption(option =>
option.setName('command')
.setDescription('The command you want more details about.')),
category: 'utility',
async execute(interaction, args, client) {
if (args.command) {
const command = client.commands.get(args.command);
if (!command) return interaction.reply(`Did not found any command named \`\`${args.command}\`\`. Please make sure it is a valid command and not an alias.`);
const description = Object.assign({
content: 'No description available.',
usage: '',
examples: [],
fields: [],
}, command.data);
const usage = command.data.options.map(cmd => {
let type = 'String';
const constructorName = cmd.constructor.name.toLowerCase();
if (constructorName.includes('boolean')) {
if (interaction.isMessage) {
type = `--${cmd.name}`;
}
else {
type = 'True/False';
}
}
else if (constructorName.includes('mentionable')) {
type = 'User';
}
else if (constructorName.includes('attachment')) {
type = 'Attachment';
}
return `[${cmd.name}: ${type}]`;
});
let p = '/';
if (interaction.isMessage) {
p = prefixs[0];
}
const embed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle(`\`${p}${command.data.name} ${usage.join(' ')}\``)
.addFields(
{ name: 'Description', value: description.description },
)
.setFooter({ text: `All the available prefix: ${prefixs.join(' | ')}` });
for (const field of description.fields) embed.addFields({ name: field.name, value: field.value });
if (description.examples.length) {
const text = `${prefixs[0]}${command.alias[0]}`;
embed.addFields({ name: 'Examples', value: `\`${text} ${description.examples.join(`\`\n\`${text} `)}\``, inline: true });
}
else {
const example = command.data.options.map(cmd => {
let string = '"lorem ipsum"';
const constructorName = cmd.constructor.name.toLowerCase();
if (constructorName.includes('boolean')) {
if (interaction.isMessage) {
string = `--${cmd.name}`;
}
else {
string = 'True/False';
}
}
else if (constructorName.includes('mentionable')) {
string = `@${interaction.user.username}`;
}
else if (constructorName.includes('attachment')) {
string = 'Attachment';
}
if (!interaction.isMessage) {
string = `\`\`${cmd.name}:${string}\`\``;
}
return string;
});
embed.addFields({ name: 'Example', value: `${p}${command.data.name} ${example.join(' ')}`, inline: true });
}
if (command.alias) {
if (command.alias.length >= 1) {
embed.addFields({ name: 'Aliases', value: `\`${command.alias.join('` `')}\``, inline: true });
}
}
if (command.userPermissions) {
const perm = [];
command.userPermissions.forEach(permission => {
perm.push(new PermissionsBitField(permission).toArray());
});
embed.addFields({ name: 'User permission', value: `\`${perm.join('` `')}\``, inline: true });
}
if (command.clientPermissions) {
const perm = [];
command.clientPermissions.forEach(permission => {
perm.push(new PermissionsBitField(permission).toArray());
});
embed.addFields({ name: 'Bot permission', value: `\`${perm.join('` `')}\``, inline: true });
}
if (command.parallelLimit) {
const paralellimit = ratelimiter.checkParallel(interaction.user, command.data.name, command);
embed.addFields({ name: 'Current number of executions', value: `\`${paralellimit.current}\``, inline: false });
embed.addFields({ name: 'Maximum number of executions', value: `\`${command.parallelLimit}\``, inline: true });
}
if (fs.existsSync(`./asset/img/command/${command.category}/${command.data.name}.png`)) {
const file = new AttachmentBuilder(`./asset/img/command/${command.category}/${command.data.name}.png`);
embed.setImage(`attachment://${command.data.name}.png`);
return interaction.reply({ embeds: [embed], files: [file] });
}
return interaction.reply({ embeds: [embed] });
}
else {
const embed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.addFields({ name: 'Command List', value: `This is a list of commands.\nTo view details for a command, do \`${prefixs[0]}help <command>\`.` })
.setFooter({ text: `All the available prefix: ${prefixs.join('| ')}` });
const object = { };
for (const command of client.commands.values()) {
if (command.category === 'secret') continue;
if (interaction.user.id !== ownerId && command.category === 'owner') continue;
if (object[command.category]) {
object[command.category].push(command.data.name);
}
else {
object[command.category] = [ command.data.name ];
}
}
for (const category in object) {
const title = {
fun: '🎉\u2000Fun',
utility: '🔩\u2000Utility',
admin: '⚡\u2000Admin',
owner: '🛠️\u2000Owner',
voice: '🗣️\u2000Voice',
AI: '🦾\u2000AI',
}[category];
embed.addFields({ name: title, value: `\`${object[category].join('` `')}\`` });
}
return interaction.reply({ embeds: [embed] });
}
},
};

View file

@ -0,0 +1,27 @@
import { SlashCommandBuilder } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('invite')
.setDescription('Generate invite link for the bot or another')
.addMentionableOption(option =>
option.setName('bot')
.setDescription('The bot you want to make an invite link for.')
.setRequired(false)),
category: 'utility',
integration_types: [0, 1],
async execute(interaction, args, client) {
if (args.bot) {
if (args.bot.user.bot) {
return interaction.reply(`You can add the bot you mentioned with this link: https://discordapp.com/oauth2/authorize?client_id=${args.bot.id}&permissions=2684406848&scope=bot%20applications.commands\n\`Note: The invite will not work if the bot is not public\``);
}
else {
return interaction.reply('I\'m sorry but the user you mentioned is not a bot!');
}
}
else {
return interaction.reply(`
You can add me for your server from here: https://discord.com/oauth2/authorize?client_id=${client.user.id}&permissions=2684406848&scope=bot%20applications.commands` +
`\nIf you want to use my commands no matter the server you can install me as a user applications from here: https://discord.com/oauth2/authorize?client_id=${client.user.id}`);
}
},
};

View file

@ -0,0 +1,68 @@
import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import db from '../../models/index.js';
export default {
data: new SlashCommandBuilder()
.setName('optout')
.setDescription('Opt out of the non commands features and arguments logging (for debugging purposes)'),
category: 'utility',
integration_types: [0, 1],
async execute(interaction, args, client) {
const isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } });
if (!isOptOut) {
const body = { userID: interaction.user.id };
await db.optout.create(body);
await interaction.reply({ content: 'You have successfully been opt out.', ephemeral: true });
}
else {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
await interaction.reply({ content: 'You are already opt out, do you wish to opt in?', components: [row], ephemeral: true });
listenButton(client, interaction, interaction.user);
}
return interaction.followUp({
content:
'As a reminder here what opting out does:\n'
+ '- Your user ID will no longer be used for debug logging.\n'
+ '- servers will no longer be shown in added/kicked stats.\n'
+ '- Your messages won\'t be quoted.\n'
+ '- Won\'t show the arguments from commands.',
ephemeral: true,
},
);
},
};
async function listenButton(client, interaction, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}${originalId}`) {
db.optout.destroy({ where: { userID: interaction.user.id } });
return interaction.editReply({ content: 'You have successfully been opt in', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

View file

@ -1,10 +1,22 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js';
const { uptimePage } = process.env;
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('ping') .setName('ping')
.setDescription('Replies with Pong!'), .setDescription('Replies with Pong!'),
category: 'utility',
integration_types: [0, 1],
async execute(interaction) { async execute(interaction) {
await interaction.reply(`Pong! \`${Math.round(interaction.client.ws.ping)} ms\``); const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setLabel('Status page')
.setURL(uptimePage)
.setStyle(ButtonStyle.Link),
);
await interaction.reply({ content: `Pong! \`${Math.round(interaction.client.ws.ping)} ms\``, components: [row] });
}, },
}; };

View file

@ -0,0 +1,33 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('serverinfo')
.setDescription('Show info about the server'),
category: 'utility',
alias: ['server'],
async execute(interaction, args, client) {
await interaction.guild.members.fetch();
const botCount = interaction.guild.members.cache.filter(member => member.user.bot).size;
const guildOwner = await client.users.fetch(interaction.guild.ownerId);
const addEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY')
.setTitle(interaction.guild.name)
.setThumbnail(interaction.guild.iconURL())
.addFields(
{ name: 'Number of users', value: (interaction.guild.memberCount - botCount).toString(), inline: true },
{ name: 'Number of bots', value: botCount.toString(), inline: true },
{ name: 'Total number of members', value: interaction.guild.memberCount.toString(), inline: true },
{ name: 'Number of channels', value: interaction.guild.channels.cache.size.toString(), inline: true },
{ name: '', value:'' },
{ name: 'Date when guild created', value: interaction.guild.createdAt.toString(), inline: true },
{ name: 'Owner', value: guildOwner.toString(), inline: true },
)
.setTimestamp();
interaction.reply({ embeds: [addEmbed] });
},
};

View file

@ -1,11 +1,12 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import { MessageEmbed, version } from 'discord.js'; import { EmbedBuilder, version } from 'discord.js';
import os from 'node:os'; import os from 'node:os';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('stats') .setName('stats')
.setDescription('Show some stats about the bot'), .setDescription('Show some stats about the bot'),
category: 'utility',
async execute(interaction) { async execute(interaction) {
const client = interaction.client; const client = interaction.client;
const uptime = process.uptime(); const uptime = process.uptime();
@ -32,19 +33,21 @@ export default {
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}; };
const statsEmbed = new MessageEmbed() const statsEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY') .setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle('Bot stats') .setTitle('Bot stats')
.setAuthor({ name: client.user.tag, iconURL: client.user.displayAvatarURL(), url: 'https://libtar.de' }) .setAuthor({ name: client.user.username, iconURL: client.user.displayAvatarURL(), url: 'https://libtar.de' })
.addField('Servers', client.guilds.cache.size.toString(), true) .addFields(
.addField('Channels', client.channels.cache.size.toString(), true) { name: 'Servers', value: client.guilds.cache.size.toString(), inline: true },
.addField('Users', client.users.cache.size.toString(), true) { name: 'Channels', value: client.channels.cache.size.toString(), inline: true },
.addField('Ram usage', `${bytesToSize(process.memoryUsage().heapUsed)}/${bytesToSize(os.totalmem)}`, true) { name: 'Users', value: client.users.cache.size.toString(), inline: true },
.addField('CPU', `${os.cpus()[0].model} (${os.cpus().length} core)`, true) { name: 'Ram usage', value: `${bytesToSize(process.memoryUsage().heapUsed)}/${bytesToSize(os.totalmem)}`, inline: true },
.addField('OS', `${os.platform()} ${os.release()}`, true) { name: 'CPU', value: `${os.cpus()[0].model} (${os.cpus().length} core) (${os.arch()})`, inline: true },
.addField('Nodejs version', process.version, true) { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true },
.addField('Discord.js version', version, true) { name: 'Nodejs version', value: process.version, inline: true },
.addField('Uptime', dateString, true) { name: 'Discord.js version', value: version, inline: true },
{ name: 'Uptime', value: dateString, inline: true },
)
.setTimestamp(); .setTimestamp();
return interaction.reply({ embeds: [statsEmbed] }); return interaction.reply({ embeds: [statsEmbed] });

View file

@ -0,0 +1,69 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
export default {
data: new SlashCommandBuilder()
.setName('userinfo')
.setDescription('Show info about a user')
.addMentionableOption(option =>
option.setName('user')
.setDescription('Which user you want to see the info of?')
.setRequired(false)),
category: 'utility',
alias: ['user'],
async execute(interaction, args, client) {
await interaction.guild.members.fetch();
let member = interaction.member;
let user = interaction.user;
if (args.user) {
user = client.users.resolve(args.user);
member = interaction.guild.members.resolve(args.user);
}
const Embed = new EmbedBuilder()
.setColor(member ? member.displayHexColor : 'Navy')
.setAuthor({ name: `${user.username} (${user.id})`, iconURL: user.displayAvatarURL() })
.addFields(
{ name: 'Current rank hex color', value: member ? member.displayHexColor : 'No rank color', inline: true },
{ name: 'Joined guild at', value: member ? member.joinedAt.toString() : 'Not in this guild', inline: true },
{ name: 'Date when account created', value: user.createdAt.toString(), inline: true },
)
.setTimestamp();
Embed.addFields({ name: '', value: '' });
// Show user status
/* Missing presence intent.
if (user.presence.activities[0]) {
Embed.addField('Presence', user.presence.activities[0], true);
if (user.presence.activities[0].details) Embed.addField('', user.presence.activities[0].details, true);
if (user.presence.activities[0].state) Embed.addField('', user.presence.activities[0].state, true);
}
*/
// Is the user a bot?
if (user.bot) Embed.addFields({ name: 'Is a bot?', value: '✅', inline: true });
// Show on which platform they are using discord from if its not a bot
/* Missing presence intent.
if (user.presence.clientStatus && !user.bot) {
Embed.addFields({ name: '', value: '' });
if (user.presence.clientStatus.mobile) Embed.addFields({ name: 'Using discord on', value: '📱 ' + user.presence.clientStatus.mobile, inline: true });
if (user.presence.clientStatus.desktop) Embed.addFields({ name: 'Using discord on', value: '💻 ' + user.presence.clientStatus.desktop, inline: true });
if (user.presence.clientStatus.web) Embed.addFields({ name: 'Using discord on', value: '☁️ ' + user.presence.clientStatus.web, inline: true });
}
*/
if (member) {
// Show since when this user have been boosting the current guild
if (member.premiumSince) Embed.addFields({ name: 'Boosting this guild since', value: member.premiumSince.toString(), inline: true });
// Show guild nickname
if (member.nickname) Embed.addFields({ name: 'Nickname', value: member.nickname, inline: true });
// Show member roles
if (member.roles) {
Embed.addFields({ name: 'Roles', value: `${[...member.roles.cache.values()].join(', ')}` });
Embed.addFields({ name: 'Permissions', value: `\`${member.permissions.toArray().join(', ')}\`` });
}
}
return interaction.reply({ embeds: [Embed] });
},
};

View file

@ -1,10 +1,11 @@
import { SlashCommandBuilder } from '@discordjs/builders'; import { SlashCommandBuilder } from 'discord.js';
import utils from '../../utils/videos.js'; import utils from '../../utils/videos.js';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { exec } from 'node:child_process'; import { execFile } from 'node:child_process';
const { NODE_ENV } = process.env;
const ytdlpFormat = 'bestvideo[height<=?480]/best';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -13,43 +14,101 @@ export default {
.addStringOption(option => .addStringOption(option =>
option.setName('url') option.setName('url')
.setDescription('URL of the video you want to convert') .setDescription('URL of the video you want to convert')
.setRequired(true)), .setRequired(true))
async execute(interaction) { .addIntegerOption(option =>
option.setName('quality')
.setDescription('Quality of the gif conversion. Default 70. Number between 1 and 100')
.setRequired(false))
.addIntegerOption(option =>
option.setName('fps')
.setDescription('Change the speed at which the gif play at. Number between 1 and 100.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('autocrop')
.setDescription('Autocrop borders on gif.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('noloop')
.setDescription('Stop the gif from looping')
.setRequired(false)),
category: 'utility',
alias: ['v2g', 'togif'],
integration_types: [0, 1],
async execute(interaction, args) {
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
const url = interaction.options.getString('url'); const maxFileSize = await utils.getMaxFileSize(interaction.guild);
const url = args.url;
let quality = args.quality;
if (quality) {
if (quality <= 0) {
quality = 1;
}
else if (quality > 100) {
quality = 100;
}
}
if (args.fps) {
if (args.fps <= 0) {
args.fps = 1;
}
else if (args.fps > 100) {
args.fps = 100;
}
}
if (!await utils.stringIsAValidurl(url)) { if (!await utils.stringIsAValidurl(url)) {
console.error(`Not a url!!! ${url}`); console.error(`Not a url!!! ${url}`);
return interaction.editReply({ content: '❌ This does not look like a valid url!', ephemeral: true }); return interaction.editReply({ content: '❌ This does not look like a valid url!', ephemeral: true });
} }
utils.downloadVideo(url, interaction.id) const aproxFileSize = await utils.getVideoSize(url, ytdlpFormat);
console.log(aproxFileSize);
if (aproxFileSize > 4) {
return interaction.editReply('The file you are trying to convert is too big! Limit is 4 MB');
};
utils.downloadVideo(url, interaction.id, ytdlpFormat)
.then(async () => { .then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id)); const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
const output = `${os.tmpdir()}/${file}`; let output = `${os.tmpdir()}/${file}`;
if (args.autocrop) {
const oldOutput = output;
output = `${os.tmpdir()}/autocrop${file}`;
await utils.autoCrop(oldOutput, output);
}
// Get the fps of the original video
if (!args.fps) {
args.fps = getVideoFramerate(output);
}
const gifskiOutput = output.replace(path.extname(output), '.gif'); const gifskiOutput = output.replace(path.extname(output), '.gif');
const gifsicleOutput = output.replace(path.extname(output), 'gifsicle.gif'); const gifsicleOutput = output.replace(path.extname(output), 'gifsicle.gif');
// Extract every frame for gifski // Extract every frame for gifski
await utils.ffmpeg(`-i ${output} ${os.tmpdir()}/frame${interaction.id}%04d.png`); await utils.ffmpeg(['-i', output, `${os.tmpdir()}/frame${interaction.id}%04d.png`]);
// Make it look better // Make it look better
await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`); await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`, quality, await args.fps);
// Optimize it // Optimize it
await gifsicle(gifskiOutput, gifsicleOutput); await gifsicle(gifskiOutput, gifsicleOutput, args.noloop);
const fileStat = fs.statSync(gifsicleOutput); const fileStat = fs.statSync(gifsicleOutput);
const fileSize = fileStat.size / 1000000.0; const fileSize = fileStat.size / 1000000.0;
if (fileSize > 100) { if (fileSize > 25) {
await interaction.deleteReply(); await interaction.deleteReply();
await interaction.followUp('❌ Uh oh! The video once converted is too big!', { ephemeral: true }); await interaction.followUp('❌ Uh oh! The video once converted is too big!', { ephemeral: true });
} }
else if (fileSize > 8) { else if (fileSize > maxFileSize) {
const fileURL = await utils.upload(gifsicleOutput) const fileURL = await utils.upload(gifsicleOutput)
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
await interaction.editReply({ content: ` File was bigger than 8 mb. It has been uploaded to an external site.\n${fileURL}`, ephemeral: false }); await interaction.editReply({ content: ` File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.\n${fileURL}`, ephemeral: false });
} }
else { else {
await interaction.editReply({ files: [gifsicleOutput], ephemeral: false }); await interaction.editReply({ files: [gifsicleOutput], ephemeral: false });
@ -58,30 +117,50 @@ export default {
}, },
}; };
async function gifski(output, input) { async function gifski(output, input, quality, fps) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
exec(`gifski --quality 70 -o ${output} ${input}`, (err, stdout, stderr) => { // Shell: true should be fine as no user input is being passed
execFile('gifski', ['--quality', quality ? quality : 70, '--fps', fps ? fps : 20, '-o', output, input], { shell: true }, (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
if (stderr) { if (stderr) {
console.error(stderr); console.error(stderr);
} }
resolve(stdout); console.log(NODE_ENV === 'development' ? stdout : null);
resolve();
}); });
}); });
} }
async function gifsicle(input, output) { async function gifsicle(input, output, loop = false) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
exec(`gifsicle --colors 256 -i ${input} -o ${output}`, (err, stdout, stderr) => { // Shell: true should be fine as no user input is being passed
execFile('gifsicle', ['--colors', '256', loop ? '--no-loopcount' : '', '-i', input, '-o', output], { shell: true }, (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
if (stderr) { if (stderr) {
console.error(stderr); console.error(stderr);
} }
resolve(stdout); console.log(NODE_ENV === 'development' ? stdout : null);
resolve();
});
});
}
async function getVideoFramerate(input) {
return await new Promise((resolve, reject) => {
execFile('ffprobe', ['-v', 'error', '-of', 'csv=p=0', '-select_streams', 'v:0', '-show_entries', 'stream=r_frame_rate', input], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
const tempfps = stdout.trim().split('/');
const fps = tempfps[0] / tempfps[1];
return resolve(fps);
}); });
}); });
} }

43
commands/voice/dectalk.js Normal file
View file

@ -0,0 +1,43 @@
import { SlashCommandBuilder } from 'discord.js';
import { rand } from '../../utils/rand.js';
import { execFile } from 'node:child_process';
export default {
data: new SlashCommandBuilder()
.setName('dectalk')
.setDescription('Send a .wav sound file of what you wrote with dectalk')
.addStringOption(option =>
option.setName('message')
.setDescription('Write something so I can talk it back with dectalk.')
.setRequired(true)),
category: 'voice',
async execute(interaction, args) {
args.message = rand(args.message, interaction);
const output = `${interaction.id}_dectalk.wav`;
const message = '[:phoneme on]' + args.message;
await interaction.deferReply({ ephemeral: false });
if (process.platform === 'win32') {
// Untested, most likely do not work.
execFile('say.exe', ['-w', output, `${message}`], { cwd: './bin/dectalk/' }, (error, stdout, stderr) => {
sendMessage(output, error, stdout, stderr);
});
}
else if (process.platform === 'linux' || process.platform === 'darwin') {
execFile('wine', ['say.exe', '-w', output, `${message}`], { cwd: './bin/dectalk/' }, (error, stdout, stderr) => {
sendMessage(`./bin/dectalk/${output}`, error, stdout, stderr);
});
}
async function sendMessage(file, error, stdout, stderr) {
if (error) {
console.error(stderr);
console.error(error);
return interaction.editReply('Oh no! an error has occurred!');
}
return interaction.editReply({ files: [file] });
}
},
};

View file

@ -0,0 +1,14 @@
{
"development": {
"dialect": "sqlite",
"storage": "./database.sqlite3"
},
"test": {
"dialect": "sqlite",
"storage": ":memory"
},
"production": {
"dialect": "sqlite",
"storage": "./database.sqlite3"
}
}

101
eslint.config.mjs Normal file
View file

@ -0,0 +1,101 @@
import globals from "globals";
import babelParser from "@babel/eslint-parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [...compat.extends("eslint:recommended"), {
ignores: ["models/*", "migrations/*", "eslint.config.mjs"],
languageOptions: {
globals: {
...globals.node,
},
parser: babelParser,
ecmaVersion: 2022,
sourceType: "module",
parserOptions: {
requireConfigFile: false,
babelOptions: {
plugins: ["@babel/plugin-syntax-import-assertions"],
},
},
},
rules: {
"arrow-spacing": ["warn", {
before: true,
after: true,
}],
"brace-style": ["error", "stroustrup", {
allowSingleLine: true,
}],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
curly: ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
indent: ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", {
max: 4,
}],
"max-statements-per-line": ["error", {
max: 2,
}],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", {
max: 2,
maxEOF: 1,
maxBOF: 0,
}],
"no-shadow": ["error", {
allow: ["err", "resolve", "reject"],
}],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
quotes: ["error", "single"],
semi: ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
anonymous: "never",
named: "never",
asyncArrow: "always",
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
yoda: "error",
},
}];

View file

@ -1,17 +1,26 @@
import db from '../../models/index.js'; import db from '../../models/index.js';
const guildBlacklist = db.guildBlacklist; const guildBlacklist = db.guildBlacklist;
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import dotenv from 'dotenv';
dotenv.config();
const { statusChannel, NODE_ENV } = process.env; const { statusChannel, NODE_ENV } = process.env;
export default { export default {
name: 'guildCreate', name: 'guildCreate',
once: true,
async execute(guild, client) { async execute(guild, client) {
const guildOwner = await client.users.fetch(guild.ownerId); const guildOwner = await client.users.fetch(guild.ownerId);
const isOptOut = await db.optout.findOne({ where: { userID: guildOwner.id } });
if (isOptOut) {
console.log(`A guild\n${guild.memberCount} users`);
if (statusChannel && NODE_ENV !== 'development') {
const channel = client.channels.resolve(statusChannel);
channel.send({ content: `An anonymous guild just added me.\nI'm now in ${client.guilds.cache.size} servers!` });
}
return;
}
console.log(`${guild.name}\n${guild.memberCount} users\nOwner: ${guildOwner.username}\nOwner ID: ${guild.ownerId}`); console.log(`${guild.name}\n${guild.memberCount} users\nOwner: ${guildOwner.username}\nOwner ID: ${guild.ownerId}`);
const blacklist = await guildBlacklist.findOne({ where: { guildID:guild.id } }); const blacklist = await guildBlacklist.findOne({ where: { guildID:guild.id } });
@ -25,16 +34,18 @@ export default {
const channel = client.channels.resolve(statusChannel); const channel = client.channels.resolve(statusChannel);
const botCount = guild.members.cache.filter(member => member.user.bot).size; const botCount = guild.members.cache.filter(member => member.user.bot).size;
console.log(guild.memberCount); console.log(guild.memberCount);
const addEmbed = new MessageEmbed() const addEmbed = new EmbedBuilder()
.setColor('#52e80d') .setColor('#52e80d')
.setTitle('New boiz in town') .setTitle('New boiz in town')
.setURL('https://www.youtube.com/watch?v=6n3pFFPSlW4') .setURL('https://www.youtube.com/watch?v=6n3pFFPSlW4')
.setThumbnail(guild.iconURL()) .setThumbnail(guild.iconURL())
.addField('Guild', `${guild.name} (${guild.id})`) .addFields(
.addField('Total number of members', guild.memberCount.toString(), true) { name: 'Guild', value: `${guild.name} (${guild.id})` },
.addField('Number of users', (guild.memberCount - botCount).toString(), true) { name: 'Total number of members', value: guild.memberCount.toString(), inline: true },
.addField('Number of bots', botCount.toString(), true) { name: 'Number of users', value: (guild.memberCount - botCount).toString(), inline: true },
.addField('Owner', `${guildOwner.username} (${guild.ownerId})`, true) { name: 'Number of bots', value: botCount.toString(), inline: true },
{ name: 'Owner', value: `${guildOwner.username} (${guild.ownerId})`, inline: true },
)
.setFooter({ text: `I'm now in ${client.guilds.cache.size} servers!` }) .setFooter({ text: `I'm now in ${client.guilds.cache.size} servers!` })
.setTimestamp(); .setTimestamp();

View file

@ -1,17 +1,27 @@
import db from '../../models/index.js'; import db from '../../models/index.js';
const guildBlacklist = db.guildBlacklist; const guildBlacklist = db.guildBlacklist;
import { MessageEmbed } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import dotenv from 'dotenv';
dotenv.config();
const { statusChannel, NODE_ENV } = process.env; const { statusChannel, NODE_ENV } = process.env;
export default { export default {
name: 'guildDelete', name: 'guildDelete',
once: true,
async execute(guild, client) { async execute(guild, client) {
if (!guild.available) return;
const guildOwner = await client.users.fetch(guild.ownerId); const guildOwner = await client.users.fetch(guild.ownerId);
const isOptOut = await db.optout.findOne({ where: { userID: guildOwner.id } });
if (isOptOut) {
console.log(`***BOT KICKED***A guild\n${guild.memberCount} users\n***BOT KICKED***`);
if (statusChannel && NODE_ENV !== 'development') {
const channel = client.channels.resolve(statusChannel);
channel.send({ content: `An anonymous guild just removed me.\nI'm now in ${client.guilds.cache.size} servers!` });
}
return;
}
console.log(`***BOT KICKED***\n${guild.name}\n${guild.memberCount} users\nOwner: ${guildOwner.username}\nOwner ID: ${guild.ownerId}\n***BOT KICKED***`); console.log(`***BOT KICKED***\n${guild.name}\n${guild.memberCount} users\nOwner: ${guildOwner.username}\nOwner ID: ${guild.ownerId}\n***BOT KICKED***`);
const blacklist = await guildBlacklist.findOne({ where: { guildID:guild.id } }); const blacklist = await guildBlacklist.findOne({ where: { guildID:guild.id } });
@ -21,16 +31,18 @@ export default {
const channel = client.channels.resolve(statusChannel); const channel = client.channels.resolve(statusChannel);
const botCount = guild.members.cache.filter(member => member.user.bot).size; const botCount = guild.members.cache.filter(member => member.user.bot).size;
console.log(guild.memberCount); console.log(guild.memberCount);
const kickEmbed = new MessageEmbed() const kickEmbed = new EmbedBuilder()
.setColor('#FF0000') .setColor('#FF0000')
.setTitle('Some mofo just removed me from there guild :(') .setTitle('Some mofo just removed me from there guild :(')
.setURL('https://www.youtube.com/watch?v=6n3pFFPSlW4') .setURL('https://www.youtube.com/watch?v=6n3pFFPSlW4')
.setThumbnail(guild.iconURL()) .setThumbnail(guild.iconURL())
.addField('Guild', `${guild.name} (${guild.id})`) .addFields(
.addField('Total number of members', guild.memberCount.toString(), true) { name: 'Guild', value: `${guild.name} (${guild.id})` },
.addField('Number of users', (guild.memberCount - botCount).toString(), true) { name: 'Total number of members', value: guild.memberCount.toString(), inline: true },
.addField('Number of bots', botCount.toString(), true) { name: 'Number of users', value: (guild.memberCount - botCount).toString(), inline: true },
.addField('Owner', `${guildOwner.username} (${guild.ownerId})`, true) { name: 'Number of bots', value: botCount.toString(), inline: true },
{ name: 'Owner', value: `${guildOwner.username} (${guild.ownerId})`, inline: true },
)
.setFooter({ text: `I'm now in ${client.guilds.cache.size} servers!` }) .setFooter({ text: `I'm now in ${client.guilds.cache.size} servers!` })
.setTimestamp(); .setTimestamp();

View file

@ -0,0 +1,56 @@
import db from '../../models/index.js';
import { rand } from '../../utils/rand.js';
export default {
name: 'guildMemberAdd',
async execute(member, client) {
const isOptOut = await db.optout.findOne({ where: { userID: member.user.id } });
if (isOptOut) return;
const join = await db.joinChannel.findOne({ where: { guildID: member.guild.id } });
if (join) {
const channel = client.channels.resolve(join.get('channelID'));
let welcomeMessage = join.get('message');
const invite = new RegExp(/(https?:\/\/)?(www\.)?discord(?:app\.com|\.gg)[/invite/]?(?:(?!.*[Ii10OolL]).[a-zA-Z0-9]{5,6}|[a-zA-Z0-9-]{2,32})/g);
let username = member.user.username;
let user = member.user;
if (username.match(invite)) {
username = username.replace(/(https?:\/\/)?(www\.)?discord(?:app\.com|\.gg)[/invite/]?(?:(?!.*[Ii10OolL]).[a-zA-Z0-9]{5,6}|[a-zA-Z0-9-]{2,32})/g, '[REDACTED]');
user = username;
}
welcomeMessage = welcomeMessage.replace(/\[member\]/g, username);
welcomeMessage = welcomeMessage.replace(/\[memberPing\]/g, user);
welcomeMessage = welcomeMessage.replace(/\[server\]/g, member.guild.name);
// add attachment
let attach;
if (welcomeMessage.includes('[attach:')) {
attach = welcomeMessage.split(/(\[attach:.*?])/);
for (let i = 0, l = attach.length; i < l; i++) {
if (attach[i].includes('[attach:')) {
attach = attach[i].replace('[attach:', '').slice(0, -1);
i = attach.length;
}
}
welcomeMessage = welcomeMessage.replace(/(\[attach:.*?])/, '');
}
welcomeMessage = rand(welcomeMessage);
if (attach) {
return channel.send({ content: welcomeMessage, files: [attach] });
}
else {
return channel.send({ content: welcomeMessage });
}
}
},
};

View file

@ -0,0 +1,56 @@
import db from '../../models/index.js';
import { rand } from '../../utils/rand.js';
export default {
name: 'guildMemberRemove',
async execute(member, client) {
const isOptOut = await db.optout.findOne({ where: { userID: member.user.id } });
if (isOptOut) return;
const leave = await db.leaveChannel.findOne({ where: { guildID: member.guild.id } });
if (leave) {
const channel = client.channels.resolve(leave.get('channelID'));
let welcomeMessage = leave.get('message');
const invite = new RegExp(/(https?:\/\/)?(www\.)?discord(?:app\.com|\.gg)[/invite/]?(?:(?!.*[Ii10OolL]).[a-zA-Z0-9]{5,6}|[a-zA-Z0-9-]{2,32})/g);
let username = member.user.username;
let user = member.user;
if (username.match(invite)) {
username = username.replace(/(https?:\/\/)?(www\.)?discord(?:app\.com|\.gg)[/invite/]?(?:(?!.*[Ii10OolL]).[a-zA-Z0-9]{5,6}|[a-zA-Z0-9-]{2,32})/g, '[REDACTED]');
user = username;
}
welcomeMessage = welcomeMessage.replace(/\[member\]/g, username);
welcomeMessage = welcomeMessage.replace(/\[memberPing\]/g, user);
welcomeMessage = welcomeMessage.replace(/\[server\]/g, member.guild.name);
// add attachment
let attach;
if (welcomeMessage.includes('[attach:')) {
attach = welcomeMessage.split(/(\[attach:.*?])/);
for (let i = 0, l = attach.length; i < l; i++) {
if (attach[i].includes('[attach:')) {
attach = attach[i].replace('[attach:', '').slice(0, -1);
i = attach.length;
}
}
welcomeMessage = welcomeMessage.replace(/(\[attach:.*?])/, '');
}
welcomeMessage = rand(welcomeMessage);
if (attach) {
return channel.send({ content: welcomeMessage, files: [attach] });
}
else {
return channel.send({ content: welcomeMessage });
}
}
},
};

View file

@ -1,19 +1,28 @@
import { Permissions } from 'discord.js'; // TODO: Moving that to a dedicated function that works for both messages and interactions
import db from '../../models/index.js';
const ratelimit = {}; import { PermissionFlagsBits, InteractionType } from 'discord.js';
import db from '../../models/index.js';
import ratelimiter from '../../utils/ratelimiter.js';
import dotenv from 'dotenv';
dotenv.config();
const { ownerId } = process.env; const { ownerId } = process.env;
export default { export default {
name: 'interactionCreate', name: 'interactionCreate',
async execute(interaction) { async execute(interaction) {
const client = interaction.client; const client = interaction.client;
if (!interaction.isCommand()) return; if (interaction.type !== InteractionType.ApplicationCommand) return;
const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:interaction.user.id } }); const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:interaction.user.id } });
const commandBlacklist = await db.Blacklists.findOne({ where: { type:interaction.commandName, uid:interaction.user.id } }); const commandBlacklist = await db.Blacklists.findOne({ where: { type:interaction.commandName, uid:interaction.user.id } });
if (interaction.guild) {
const serverBlacklist = await db.Blacklists.findOne({ where: { type:'guild', uid:interaction.guild.id } });
if (serverBlacklist) {
interaction.reply({ content: `This guild has been blacklisted for the following reason: \`${serverBlacklist.reason}\``, ephemeral: true });
return interaction.guild.leave();
}
}
if (globalBlacklist) { if (globalBlacklist) {
return interaction.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true }); return interaction.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true });
} }
@ -21,7 +30,7 @@ export default {
return interaction.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true }); return interaction.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true });
} }
const userTag = interaction.user.tag; const userTag = interaction.user.username;
const userID = interaction.user.id; const userID = interaction.user.id;
const commandName = interaction.commandName; const commandName = interaction.commandName;
@ -29,56 +38,91 @@ export default {
if (!command) return; if (!command) return;
console.log(`\x1b[33m${userTag} (${userID})\x1b[0m launched command \x1b[33m${commandName}\x1b[0m`); let isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } });
if (commandName === 'optout') {
isOptOut = true;
}
const timestamp = new Date();
console.log(`[${timestamp.toISOString()}] \x1b[33m${ isOptOut ? 'A user' : `${userTag} (${userID})`}\x1b[0m launched command \x1b[33m${commandName}\x1b[0m using slash`);
// Owner only check // Owner only check
if (command.ownerOnly && interaction.user.id !== ownerId) { if (command.ownerOnly && interaction.user.id !== ownerId) {
return interaction.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true }); return interaction.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true });
} }
// Guild only check
if (command.guildOnly && !interaction.guild) {
return interaction.reply({ content: '❌ This command only work in a server!', ephemeral: true });
}
// Check if the bot has the needed permissions // Check if the bot has the needed permissions
if (command.clientPermissions) { if (command.default_permission) {
const clientMember = await interaction.guild.members.fetch(client.user.id); const clientMember = await interaction.guild.members.fetch(client.user.id);
if (!clientMember.permissions.has(command.clientPermissions)) { if (!clientMember.permissions.has(command.clientPermissions)) {
return interaction.reply({ content: `❌ I am missing one of the following permission(s): \`${new Permissions(command.clientPermissions).toArray()}\``, ephemeral: true }); return interaction.reply({ content: `❌ I am missing one of the following permission(s): \`${new PermissionFlagsBits(command.clientPermissions).toArray()}\``, ephemeral: true });
} }
} }
// Check if the user has the needed permissions // Check if the user has the needed permissions
if (command.userPermissions) { /*
if (command.default_member_permissions) {
if (!interaction.member.permissions.has(command.userPermissions)) { if (!interaction.member.permissions.has(command.userPermissions)) {
return interaction.reply({ content: `❌ You are missing one of the following permission(s): \`${new Permissions(command.userPermissions).toArray()}\``, ephemeral: true }); return interaction.reply({ content: `❌ You are missing one of the following permission(s): \`${new PermissionFlagsBits(command.userPermissions).toArray()}\``, ephemeral: true });
} }
} }
*/
// Check if the limit of parallel execution has been reached
if (command.parallelLimit) {
const doParallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (doParallelLimit.limited) {
return await interaction.reply({ content: doParallelLimit.msg, ephemeral: true });
}
ratelimiter.addParallel(commandName);
}
// Check the ratelimit
const doRateLimit = await ratelimiter.check(interaction.user, commandName, command);
if (doRateLimit) {
return interaction.reply({ content: doRateLimit, ephemeral: true });
}
try { try {
const date = new Date(); interaction.prefix = '/';
if (ratelimit[userID]) {
if (ratelimit[userID].cooldown) { const args = {};
if (commandName === ratelimit[userID].command && date > ratelimit[userID].cooldown) { // https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandOptionType
ratelimit[userID].limit = 0; interaction.options.data.forEach(arg => {
ratelimit[userID].cooldown = undefined; let payload = arg.value;
if (arg.type === 9) {
payload = arg.member;
} }
else if (arg.type === 11) {
payload = arg.attachment;
}
args[arg.name] = payload;
});
if (!isOptOut) {
console.log(`[${timestamp.toISOString()}] \x1b[33m⤷\x1b[0m with args ${JSON.stringify(args)}`);
} }
if (commandName === ratelimit[userID].command && command.ratelimit === ratelimit[userID].limit) { await command.execute(interaction, args, client)
return await interaction.reply({ content: `You are being rate limited. You can try again in ${Math.floor((ratelimit[userID].cooldown - date) / 1000)} seconds.`, ephemeral: true }); .then(async () => {
const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
} if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
} });
if (command.ratelimit) {
ratelimit[userID] = { command: commandName, limit: ratelimit[userID] ? ratelimit[userID].limit + 1 : 1 };
if (command.ratelimit === ratelimit[userID].limit) {
date.setSeconds(date.getSeconds() + command.cooldown);
ratelimit[userID] = { command: commandName, limit: ratelimit[userID].limit, cooldown: date };
}
}
await command.execute(interaction);
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
await interaction.followUp({ content: `There was an error while executing this command!\n\`${error}\``, ephemeral: true });
} }
}, },
}; };

View file

@ -0,0 +1,471 @@
/* TODO:
* Make this shit work.
*/
import { ApplicationCommandOptionType, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
import db from '../../models/index.js';
import { rand } from '../../utils/rand.js';
import ratelimiter from '../../utils/ratelimiter.js';
const { ownerId, prefix } = process.env;
const prefixs = prefix.split(',');
export default {
name: 'messageCreate',
async execute(message, client) {
if (message.partials) {
await message.fetch();
}
if (message.author.bot) return;
/* Autoresponse feature & tag
*
* This section contains autoresponse and tag feature
*
*/
// auto responses
if (message.guild) {
const autoresponseStat = await db.autoresponseStat.findOne({ where: { serverID: message.guild.id, stat: 'enable' } });
if (autoresponseStat) {
// Infinite haha very yes
if (message.content.toLowerCase().startsWith('haha very') && message.content.toLowerCase().endsWith('yes')) {
let yes = message.content.toLowerCase().replace('haha', '');
yes = yes.replace('yes', '');
yes += 'very';
return message.channel.send(`haha${yes} yes`);
}
else if (message.content.toLowerCase() == 'haha yes') {
return message.channel.send('haha very yes');
}
// Reply with images as attachement
const autoresponse = await db.autoresponse.findOne({ where: { trigger: message.content.toLowerCase() } });
if (autoresponse) {
db.autoresponse.findOne({ where: { trigger: message.content.toLowerCase() } });
const trigger = autoresponse.get('trigger');
const type = autoresponse.get('type');
const content = autoresponse.get('response');
if (trigger == message.content.toLowerCase() && type == 'text') {
return message.channel.send(content);
}
else if (trigger == message.content.toLowerCase() && type == 'react') {
return message.react(content);
}
else if (trigger == message.content.toLowerCase() && type == 'image') {
return message.channel.send({ files: [content] });
}
}
}
// User autoresponse
const tag = await db.Tag.findOne({ where: { trigger: message.content.toLowerCase(), serverID: message.guild.id } });
if (tag) {
db.Tag.findOne({ where: { trigger: message.content.toLowerCase(), serverID: message.guild.id } });
let text = tag.get('response');
/*
if (text.includes('[ban]')) {
message.member.ban('Tag ban :^)');
}
else if (text.includes('[kick]')) {
message.member.kick('Tag kick :^)');
}
else
*/
if (text.includes('[delete]')) {
message.delete();
}
text = rand(text, message);
let attach = '';
if (text.includes('[attach:')) {
attach = text.split(/(\[attach:.*?])/);
for (let i = 0, l = attach.length; i < l; i++) {
if (attach[i].includes('[attach:')) {
attach = attach[i].replace('[attach:', '').slice(0, -1);
i = attach.length;
}
}
text = text.replace(/(\[attach:.*?])/, '');
}
// THIS SECTION IS VERY VERY BAD MUST CHANGE
if (text.includes('[embed]')) {
text = text.replace(/\[embed\]/, ' ');
let title = '';
let desc = '';
let image;
let thumbnail;
let footer = '';
let color;
if (text.includes('[embedImage:')) {
image = text.split(/(\[embedImage:.*?])/);
for (let i = 0, l = image.length; i < l; i++) {
if (image[i].includes('[embedImage:')) {
image = image[i].replace('[embedImage:', '').slice(0, -1);
text = text.replace(/(\[embedimage:.*?])/g, '');
i = image.length;
}
}
}
if (text.includes('[embedThumbnail:')) {
thumbnail = text.split(/(\[embedThumbnail:.*?])/);
for (let i = 0, l = thumbnail.length; i < l; i++) {
if (thumbnail[i].includes('[embedThumbnail:')) {
thumbnail = thumbnail[i].replace('[embedThumbnail:', '').slice(0, -1);
text = text.replace(/(\[embedThumbnail:.*?])/g, '');
i = thumbnail.length;
}
}
}
if (text.includes('[embedColor:')) {
color = text.split(/(\[embedColor:.*?])/);
for (let i = 0, l = color.length; i < l; i++) {
if (color[i].includes('[embedColor:')) {
color = color[i].replace('[embedColor:', '').slice(0, -1);
text = text.replace(/(\[embedColor:.*?])/g, '');
i = color.length;
}
}
}
if (text.includes('[embedTitle:')) {
title = text.split(/(\[embedTitle:.*?])/);
for (let i = 0, l = title.length; i < l; i++) {
if (title[i].includes('[embedTitle:')) {
title = title[i].replace('[embedTitle:', '').slice(0, -1);
text = text.replace(/(\[embedTitle:.*?])/g, '');
i = title.length;
}
}
}
if (text.includes('[embedFooter:')) {
footer = text.split(/(\[embedFooter:.*?])/);
for (let i = 0, l = footer.length; i < l; i++) {
if (footer[i].includes('[embedFooter:')) {
footer = footer[i].replace('[embedFooter:', '').slice(0, -1);
text = text.replace(/(\[embedFooter:.*?])/g, '');
i = footer.length;
}
}
}
if (text.includes('[embedDesc:')) {
desc = text.split(/(\[embedDesc:.*?])/);
for (let i = 0, l = desc.length; i < l; i++) {
if (desc[i].includes('[embedDesc:')) {
desc = desc[i].replace('[embedDesc:', '').slice(0, -1);
i = desc.length;
}
}
}
const embed = new EmbedBuilder()
.setColor(color)
.setTitle(title)
.setImage(image)
.setThumbnail(thumbnail)
.setDescription(desc)
.setFooter(footer)
.setTimestamp();
if (attach) {
return message.channel.send(embed, { files: [attach] });
}
else {
return message.channel.send(embed);
}
}
if (attach) {
return message.channel.send(text, { files: [attach] });
}
else {
return message.channel.send(text);
}
}
/* Quotation feature
*
* This section will contain the code for the quotation feature, it will detect link for it and send it as embed
*
*/
const isOptOut = await db.optout.findOne({ where: { userID: message.author.id } });
if (!isOptOut) {
const quotationstat = await db.quotationStat.findOne({ where: { serverID: message.guild.id, stat: 'enable' } });
if (quotationstat && (message.content.includes('discordapp.com/channels/') || message.content.includes('discord.com/channels/'))) {
const url = message.content.split('/');
const guildID = url[4];
const channelID = url[5];
const messageID = url[6].split(' ')[0];
// Verify if the guild, channel and message exist
const guild = client.guilds.resolve(guildID);
if (!guild) return;
const channel = client.channels.resolve(channelID);
if (!channel) return;
const quote = await channel.messages.fetch(messageID)
.catch(() => {
return;
});
if (!quote) return;
const Embed = new EmbedBuilder()
.setAuthor({ name: quote.author.username, iconURL: quote.author.displayAvatarURL() })
.setColor(message.member ? message.member.displayHexColor : 'Navy')
.addFields(
{ name: 'Jump to', value: `[message](https://discordapp.com/channels/${message.guild.id}/${channelID}/${messageID})`, inline: true },
{ name: 'In channel', value: quote.channel.name.toString(), inline: true },
{ name: 'Quoted by', value: message.author.toString(), inline: true },
)
.setDescription(quote.content)
.setTimestamp(quote.createdTimestamp);
if (quote.member) Embed.setAuthor({ name: `${quote.author.username}#${quote.author.discriminator}`, iconURL: quote.author.displayAvatarURL() });
if (quote.author.bot) Embed.setAuthor({ name: `${quote.author.username}#${quote.author.discriminator} (BOT)`, iconURL: quote.author.displayAvatarURL() });
if (guild.id != message.guild.id) Embed.addFields({ name: 'In guild', value: guild.name, inline: true });
const Attachment = Array.from(message.attachments.values());
if (Attachment[0]) Embed.setImage(Attachment[0].url);
return message.channel.send({ embeds: [Embed] });
}
}
}
// Command handling from message
// TODO: Moving that to a dedicated function that works for both messages and interactions
let hasPrefix = false;
prefixs.forEach(p => {
if (message.content.toLowerCase().startsWith(p)) {
hasPrefix = true;
}
});
if (!hasPrefix) return;
const messageArray = message.content.match(/"[^"]*"|\S+/g).map(m => m.slice(0, 1) === '"' ? m.slice(1, -1) : m);
let commandName = messageArray[1].toLowerCase();
const messageArgs = messageArray.splice(2, messageArray.length);
// Search for alias
client.commands.find(c => {
if (c.alias) {
if (c.alias.includes(commandName)) {
commandName = c.data.name;
}
}
});
const command = client.commands.get(commandName);
if (!command) return;
const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:message.author.id } });
const commandBlacklist = await db.Blacklists.findOne({ where: { type:commandName, uid:message.author.id } });
if (message.guild) {
const serverBlacklist = await db.Blacklists.findOne({ where: { type:'guild', uid:message.guild.id } });
if (serverBlacklist) {
message.reply({ content: `This guild has been blacklisted for the following reason: \`${serverBlacklist.reason}\``, ephemeral: true });
return message.guild.leave();
}
}
if (globalBlacklist) {
return message.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true });
}
else if (commandBlacklist) {
return message.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true });
}
const userTag = message.author.username;
const userID = message.author.id;
let isOptOut = await db.optout.findOne({ where: { userID: message.author.id } });
if (commandName === 'optout') {
isOptOut = true;
}
const timestamp = new Date();
console.log(`[${timestamp.toISOString()}] \x1b[33m${ isOptOut ? 'A user' : `${userTag} (${userID})`}\x1b[0m launched command \x1b[33m${commandName}\x1b[0m using prefix`);
// Owner only check
if (command.ownerOnly && message.author.id !== ownerId) {
return message.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true });
}
// Guild only check
if (command.guildOnly && !message.guild) {
return message.reply({ content: '❌ This command only work in a server!', ephemeral: true });
}
// Check if the bot has the needed permissions
if (command.clientPermissions) {
const clientMember = await message.guild.members.fetch(client.user.id);
if (!clientMember.permissions.has(command.clientPermissions)) {
return message.reply({ content: `❌ I am missing one of the following permission(s): \`${new PermissionFlagsBits(command.clientPermissions).toArray()}\``, ephemeral: true });
}
}
// Check if the user has the needed permissions
if (command.default_member_permissions) {
if (!message.member.permissions.has(command.default_member_permissions)) {
return message.reply({ content: `❌ You are missing one of the following permission(s): \`${new PermissionFlagsBits(command.userPermissions).toArray()}\``, ephemeral: true });
}
}
// Check if the limit of parallel execution has been reached
if (command.parallelLimit) {
const doParallelLimit = await ratelimiter.checkParallel(message.author, commandName, command);
if (doParallelLimit.limited) {
return await message.reply({ content: doParallelLimit.msg, ephemeral: true });
}
ratelimiter.addParallel(commandName);
}
// Check the ratelimit
const doRateLimit = await ratelimiter.check(message.author, commandName, command);
if (doRateLimit) {
return message.reply({ content: doRateLimit, ephemeral: true });
}
try {
message.user = message.author;
message.isMessage = true;
message.prefix = `${messageArray[0]} `;
let waitingmsg;
const toDelete = [];
message.deferReply = async function() {
waitingmsg = await message.reply('The bot is thinking...');
};
message.followUp = async function(payload) {
if (payload.ephemeral) {
toDelete.push(await message.channel.send(payload));
}
else {
await message.channel.send(payload);
}
};
message.editReply = async function(payload) {
if (waitingmsg) {
await waitingmsg.delete();
}
await message.channel.send(payload);
};
message.deleteReply = async function() {
if (waitingmsg) {
await waitingmsg.delete()
.catch(() => { return; });
}
};
message.cleanUp = async function() {
toDelete.forEach(async msg => {
msg.delete();
});
};
const args = {};
let argsToDelete = 0;
command.data.options.forEach(obj => {
if (obj.type === ApplicationCommandOptionType.Attachment) {
args[obj.name] = message.attachments.first();
delete command.data.options[command.data.options.indexOf(obj)];
argsToDelete++;
}
});
const argsLength = command.data.options.length - argsToDelete;
const missingRequired = [];
for (let i = 0, j = 0; i < argsLength; i++, j++) {
const arg = command.data.options[j];
if (arg.required && !messageArgs[i]) {
missingRequired.push({ name: arg.name, description: arg.description });
}
if (!messageArgs[i]) continue;
if (arg.type === ApplicationCommandOptionType.Attachment) continue;
let payloadName = arg.name;
let payload = messageArgs[i];
if (i >= argsLength - 1) {
payload = messageArgs.slice(i).join(' ');
}
if (arg.type === ApplicationCommandOptionType.Boolean && !messageArgs[i].startsWith('--')) {
continue;
}
else if (messageArgs[i].startsWith('--')) {
payloadName = payload.substring(2);
payload = true;
j--;
}
if (arg.type === ApplicationCommandOptionType.Mentionable || arg.type === ApplicationCommandOptionType.User) {
await message.guild.members.fetch();
payload = message.mentions.members.first() ? message.mentions.members.first() : message.guild.members.cache.find(u => u.user.username.toLowerCase().includes(payload.toLowerCase()));
}
args[payloadName] = payload;
}
if (!isOptOut && argsLength > 0) {
console.log(`[${timestamp.toISOString()}] \x1b[33m⤷\x1b[0m with args ${JSON.stringify(args)}`);
}
if (missingRequired.length > 0) {
let missingMsg = '';
missingRequired.forEach(arg => {
missingMsg += `${arg.name} | ${arg.description}\n`;
});
return message.reply(`You are missing a required argument!\n\`${missingMsg}\``);
}
await command.execute(message, args, client)
.then(async () => {
const hasPrallelLimit = await ratelimiter.checkParallel(message.author, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
});
}
catch (error) {
console.error(error);
const hasPrallelLimit = await ratelimiter.checkParallel(message.author, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
await message.reply({ content: `There was an error while executing this command!\n\`${error}\`` })
.catch(async () => {
await message.channel.send({ content: `There was an error while executing this command!\n\`${error}\`` });
});
}
},
};

View file

@ -0,0 +1,171 @@
import { EmbedBuilder } from 'discord.js';
import fs from 'node:fs';
import db from '../../models/index.js';
export default {
name: 'messageReactionAdd',
async execute(reaction, users, c) {
if (reaction.partial) {
await reaction.fetch()
.catch(err => {
return console.error(err);
});
}
/* I don't really know why this is causing issues.
if (reaction.message.partial) {
await reaction.message.fetch()
.catch(err => {
return console.error(err);
});
}
*/
const isOptOut = await db.optout.findOne({ where: { userID: reaction.message.author.id } });
if (isOptOut) return;
let starboardChannel, shameboardChannel;
let reactionCount = reaction.count;
// If one of the reaction is the author of the message remove 1 to the reaction count
reaction.users.cache.forEach(user => {
if (reaction.message.author == user) reactionCount--;
});
// Starboard
if (fs.existsSync(`./json/board/star${reaction.message.guild.id}.json`)) {
starboardChannel = JSON.parse(fs.readFileSync(`./json/board/star${reaction.message.guild.id}.json`));
let staremote = starboardChannel.emote;
const starcount = starboardChannel.count;
// Get name of the custom emoji
if (reaction.message.guild.emojis.resolve(staremote.replace(/\D/g, ''))) {
staremote = reaction.message.guild.emojis.resolve(staremote.replace(/\D/g, ''));
}
if (reaction.emoji == staremote || reaction.emoji.name == staremote) {
if (global.boards[reaction.message.id] && reactionCount > starcount) {
return editEmbed('starboard', staremote, global.boards[reaction.message.id]);
}
else if (reactionCount == starcount) {
return sendEmbed('starboard', staremote);
}
}
}
// Shameboard
if (fs.existsSync(`./json/board/shame${reaction.message.guild.id}.json`)) {
shameboardChannel = JSON.parse(fs.readFileSync(`./json/board/shame${reaction.message.guild.id}.json`));
let shameemote = shameboardChannel.emote;
const shamecount = shameboardChannel.count;
// Get name of the custom emoji
if (reaction.message.guild.emojis.resolve(shameemote.replace(/\D/g, ''))) {
shameemote = reaction.message.guild.emojis.resolve(shameemote.replace(/\D/g, ''));
}
if (reaction.emoji == shameemote || reaction.emoji.name == shameemote) {
if (global.boards[reaction.message.id] && reactionCount > shamecount) {
return editEmbed('shameboard', shameemote, global.boards[reaction.message.id]);
}
else if (reactionCount == shamecount) {
return sendEmbed('shameboard', shameemote);
}
}
}
async function editEmbed(name, emote, boardID) {
let channel;
if (name == 'starboard') {
channel = c.channels.resolve(starboardChannel.starboard);
}
else {
channel = c.channels.resolve(shameboardChannel.shameboard);
}
const message = await channel.messages.resolve(boardID);
// If the message doesn't have embeds assume it got deleted so don't do anything
if (!message) return;
// If the original embed description is empty make this embed null ( and not empty )
let description = message.embeds[0].description;
if (!message.embeds[0].description || message.embeds[0].description == undefined) {
description = null;
}
const Embed = new EmbedBuilder()
.setColor(reaction.message.member ? reaction.message.member.displayHexColor : 'Navy')
.setAuthor({ name: reaction.message.author.username, iconURL: reaction.message.author.displayAvatarURL() })
.addFields(
{ name: 'Jump to', value: `[message](https://discordapp.com/channels/${reaction.message.guild.id}/${reaction.message.channel.id}/${reaction.message.id})`, inline: true },
{ name: 'Channel', value: reaction.message.channel.toString(), inline: true },
)
.setDescription(description)
.setFooter({ text: `${emote} ${reactionCount}` })
.setTimestamp();
if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter(reactionCount, reaction.message.guild.emojis.resolve(emote).url);
message.edit({ embeds: [Embed] });
}
async function sendEmbed(name, emote) {
let messageAttachments = reaction.message.attachments.map(u => u.url)[0];
// Should change this so it automatically pick the channel ( I'm lazy right now )
let channel;
if (name == 'starboard') {
channel = c.channels.resolve(starboardChannel.starboard);
}
else {
channel = c.channels.resolve(shameboardChannel.shameboard);
}
const Embed = new EmbedBuilder()
.setColor(reaction.message.member ? reaction.message.member.displayHexColor : 'Navy')
.setAuthor({ name: reaction.message.author.username, iconURL: reaction.message.author.displayAvatarURL() })
.addFields(
{ name: 'Jump to', value: `[message](https://discordapp.com/channels/${reaction.message.guild.id}/${reaction.message.channel.id}/${reaction.message.id})`, inline: true },
{ name: 'Channel', value: reaction.message.channel.toString(), inline: true },
)
.setFooter({ text: `${emote} ${reactionCount}` })
.setTimestamp();
if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter({ text: `${reactionCount}`, iconURL: reaction.message.guild.emojis.resolve(emote).url });
let description = null;
if (reaction.message.embeds[0]) {
if (reaction.message.embeds[0].type == 'image') {
messageAttachments = reaction.message.embeds[0].url;
}
if (reaction.message.embeds[0].description) {
description = reaction.message.embeds[0].description;
}
else if (reaction.message.content) {
description = reaction.message.content;
}
}
else if (reaction.message.content) {
description = reaction.message.content;
}
// if message come from nsfw channel and the star/shameboard channel isn't nsfw put it in spoiler
if (reaction.message.channel.nsfw && !channel.nsfw) {
Embed.setDescription(`||${description}||`);
if (messageAttachments != '') {
const message = await channel.send({ content: `||${messageAttachments}||`, embeds: [Embed] });
global.boards[reaction.message.id] = message.id;
}
else {
const message = await channel.send({ embeds: [Embed] });
global.boards[reaction.message.id] = message.id;
}
}
else {
Embed.setDescription(description);
const message = await channel.send({ files: [messageAttachments], embeds: [Embed] })
.catch(async () => channel.send({ content: messageAttachments, embeds: [Embed] }));
global.boards[reaction.message.id] = message.id;
}
}
},
};

View file

@ -0,0 +1,119 @@
import { EmbedBuilder } from 'discord.js';
import fs from 'node:fs';
import db from '../../models/index.js';
export default {
name: 'messageReactionRemove',
async execute(reaction, users, c) {
if (reaction.partial) {
await reaction.fetch()
.catch(err => {
return console.error(err);
});
}
/* I don't really know why this is causing issues.
if (reaction.message.partial) {
await reaction.message.fetch()
.catch(err => {
return console.error(err);
});
}
*/
const isOptOut = await db.optout.findOne({ where: { userID: reaction.message.author.id } });
if (isOptOut) return;
let starboardChannel, shameboardChannel;
let reactionCount = reaction.count;
// If one of the reaction is the author of the message remove 1 to the reaction count
reaction.users.cache.forEach(user => {
if (reaction.message.author == user) reactionCount--;
});
// Starboard
if (fs.existsSync(`./json/board/star${reaction.message.guild.id}.json`)) {
starboardChannel = JSON.parse(fs.readFileSync(`./json/board/star${reaction.message.guild.id}.json`));
let staremote = starboardChannel.emote;
const starcount = starboardChannel.count;
// Get name of the custom emoji
if (reaction.message.guild.emojis.resolve(staremote.replace(/\D/g, ''))) {
staremote = reaction.message.guild.emojis.resolve(staremote.replace(/\D/g, ''));
}
if (global.boards[reaction.message.id] && (reaction.emoji == staremote || reaction.emoji.name == staremote) && reactionCount < starcount) {
const channel = c.channels.resolve(starboardChannel.starboard);
const message = await channel.messages.resolve(global.boards[reaction.message.id]);
delete global.boards[reaction.message.id];
// If it didn't find any message don't do anything
if (!message) return;
message.delete();
}
else if ((reaction.emoji == staremote || reaction.emoji.name == staremote) && reactionCount >= starcount) {
return editEmbed('starboard', staremote, global.boards[reaction.message.id]);
}
}
// Shameboard
if (fs.existsSync(`./json/board/shame${reaction.message.guild.id}.json`)) {
shameboardChannel = JSON.parse(fs.readFileSync(`./json/board/shame${reaction.message.guild.id}.json`));
let shameemote = shameboardChannel.emote;
const shamecount = shameboardChannel.count;
// Get name of the custom emoji
if (reaction.message.guild.emojis.resolve(shameemote.replace(/\D/g, ''))) {
shameemote = reaction.message.guild.emojis.resolve(shameemote.replace(/\D/g, ''));
}
if (global.boards[reaction.message.id] && (reaction.emoji == shameemote || reaction.emoji.name == shameemote) && reactionCount < shamecount) {
const channel = c.channels.resolve(starboardChannel.starboard);
const message = await channel.messages.resolve(global.boards[reaction.message.id]);
delete global.boards[reaction.message.id];
// If it didn't find any message don't do anything
if (!message) return;
message.delete();
}
else if ((reaction.emoji == shameemote || reaction.emoji.name == shameemote) && reactionCount >= shamecount) {
return editEmbed('shameboard', shameemote, global.boards[reaction.message.id]);
}
}
async function editEmbed(name, emote, boardID) {
let channel;
if (name == 'starboard') {
channel = c.channels.resolve(starboardChannel.starboard);
}
else {
channel = c.channels.resolve(shameboardChannel.shameboard);
}
const message = await channel.messages.resolve(boardID);
// If the message doesn't have embeds assume it got deleted so don't do anything
if (!message) return;
// If the original embed description is empty make this embed null ( and not empty )
let description = message.embeds[0].description;
if (!message.embeds[0].description || message.embeds[0].description == undefined) {
description = null;
}
const Embed = new EmbedBuilder()
.setColor(reaction.message.member ? reaction.message.member.displayHexColor : 'Navy')
.setAuthor({ name: reaction.message.author.username, iconURL: reaction.message.author.displayAvatarURL() })
.addFields(
{ name: 'Jump to', value: `[message](https://discordapp.com/channels/${reaction.message.guild.id}/${reaction.message.channel.id}/${reaction.message.id})`, inline: true },
{ name: 'Channel', value: reaction.message.channel.toString(), inline: true },
)
.setDescription(description)
.setFooter({ text: `${emote} ${reactionCount}` })
.setTimestamp();
if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter({ text: `${reactionCount}`, iconURL: reaction.message.guild.emojis.resolve(emote).url });
message.edit({ embeds: [Embed] });
}
},
};

View file

@ -1,14 +1,29 @@
import { exec } from 'node:child_process'; import { execFile } from 'node:child_process';
import dotenv from 'dotenv';
dotenv.config();
const { statusChannel, NODE_ENV } = process.env; const { statusChannel, NODE_ENV } = process.env;
import { version } from 'discord.js';
export default { export default {
name: 'ready', name: 'ready',
once: true, once: true,
async execute(client) { async execute(client) {
// Init global variables.
global.boards = {};
const commandSize = client.commands.size;
const clientTag = client.user.username;
const guildSize = client.guilds.cache.size;
const channelSize = client.channels.cache.size;
const clientID = client.user.id;
console.log('===========[ READY ]===========');
console.log(`\x1b[32mLogged in as \x1b[34m${clientTag}\x1b[0m! (\x1b[33m${clientID}\x1b[0m)`);
console.log(`Ready to serve in \x1b[33m${channelSize}\x1b[0m channels on \x1b[33m${guildSize}\x1b[0m servers.`);
console.log(`${client.readyAt}`);
console.log(`There is \x1b[33m${commandSize}\x1b[0m command loaded.`);
console.log(`Running Discord.js \x1b[33m${version}\x1b[0m`);
const ytdlpVersion = await new Promise((resolve, reject) => { const ytdlpVersion = await new Promise((resolve, reject) => {
exec('./bin/yt-dlp --version', (err, stdout, stderr) => { execFile('./bin/yt-dlp', ['--version'], (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -19,24 +34,17 @@ export default {
}); });
}); });
const commandSize = client.commands.size;
const clientTag = client.user.tag;
const guildSize = client.guilds.cache.size;
const channelSize = client.channels.cache.size;
const clientID = client.user.id;
console.log('===========[ READY ]===========');
console.log(`\x1b[32mLogged in as \x1b[34m${clientTag}\x1b[0m! (\x1b[33m${clientID}\x1b[0m)`);
console.log(`Ready to serve in \x1b[33m${channelSize}\x1b[0m channels on \x1b[33m${guildSize}\x1b[0m servers.`);
console.log(`${client.readyAt}`);
console.log(`There is \x1b[33m${commandSize}\x1b[0m command loaded.`);
console.log(`Running yt-dlp \x1b[33m${ytdlpVersion.replace('\n', '')}\x1b[0m`); console.log(`Running yt-dlp \x1b[33m${ytdlpVersion.replace('\n', '')}\x1b[0m`);
console.log('===========[ READY ]==========='); console.log('===========[ READY ]===========');
// If stats channel settings exist, send bot stats to it // If stats channel settings exist, send bot stats to it
if (statusChannel && NODE_ENV !== 'development') { if (statusChannel && NODE_ENV !== 'development') {
const channel = client.channels.resolve(statusChannel); const channel = client.channels.resolve(statusChannel);
channel.send(`Ready to serve in ${channelSize} channels on ${guildSize} servers.\nThere is ${commandSize} command loaded.\nRunning yt-dlp ${ytdlpVersion.replace('\n', '')}\n${client.readyAt}`); channel.send(
`Ready to serve in ${channelSize} channels on ${guildSize} servers.\n` +
`There is ${commandSize} command loaded.\n` +
`Running yt-dlp ${ytdlpVersion.replace('\n', '')}\n` +
`${client.readyAt}`);
} }
}, },
}; };

View file

@ -1,38 +1,51 @@
import game from '../../json/playing.json' assert {type: 'json'}; import { ActivityType } from 'discord.js';
import watch from '../../json/watching.json' assert {type: 'json'}; import game from '../../json/playing.json' with {type: 'json'};
import music from '../../json/listening.json' with {type: 'json'};
import watch from '../../json/watching.json' with {type: 'json'};
export default { export default {
name: 'ready', name: 'ready',
once: true, once: true,
async execute(client) { async execute(client) {
// Bot status // Bot status
setStatus(); await setStatus();
// Change status every 30 minutes // Change status every 30 minutes
setInterval(async () => { setInterval(async () => {
setStatus(); await setStatus();
}, 1800000); }, 1800000);
async function setStatus() { async function setStatus() {
const random = Math.floor((Math.random() * 2)); const random = Math.floor((Math.random() * 3));
let types, status;
// Random "Watching" status taken from json // Random "Watching" status taken from json
if (random === 0) { if (random === 0) {
console.log('Status type: \x1b[32mWatching\x1b[0m'); console.log('Status type: \x1b[32mWatching\x1b[0m');
let status = watch[Math.floor((Math.random() * watch.length))]; status = watch[Math.floor((Math.random() * watch.length))];
status = status + ' | Now with slash commands!'; status = status + ' | Now with slash commands!';
console.log(`Setting status to: ${status}`); console.log(`Setting status to: ${status}`);
client.user.setActivity(status, { type: 'WATCHING' }); types = [ ActivityType.Watching ];
} }
// Random "Playing" status taken from json // Random "Playing" status taken from json
else if (random === 1) { else if (random === 1) {
console.log('Status type: \x1b[32mPlaying\x1b[0m'); console.log('Status type: \x1b[32mPlaying\x1b[0m');
let status = game[Math.floor((Math.random() * game.length))]; status = game[Math.floor((Math.random() * game.length))];
status = status + ' | Now with slash commands!'; status = status + ' | Now with slash commands!';
console.log(`Setting status to: ${status}`); console.log(`Setting status to: ${status}`);
client.user.setActivity(status, { type: 'PLAYING' }); types = [ ActivityType.Playing, ActivityType.Competing ];
} }
else if (random === 2) {
console.log('Status type: \x1b[32mPlaying\x1b[0m');
status = music[Math.floor((Math.random() * music.length))];
status = status + ' | Now with slash commands!';
console.log(`Setting status to: ${status}`);
types = [ ActivityType.Listening ];
}
await client.user.setActivity(status, { type: types[Math.floor((Math.random() * types.length))] });
} }
}, },
}; };

View file

@ -1,6 +1,4 @@
import https from 'node:https'; import https from 'node:https';
import dotenv from 'dotenv';
dotenv.config();
const { uptimeURL, uptimeInterval } = process.env; const { uptimeURL, uptimeInterval } = process.env;
export default { export default {

View file

@ -1,26 +1,35 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { Client, Collection, Intents } from 'discord.js'; import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js';
import dotenv from 'dotenv'; const { token, NODE_ENV } = process.env;
dotenv.config();
const { token } = process.env;
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const client = new Client({ intents: [Intents.FLAGS.GUILDS], shards: 'auto' }); const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildMembers],
partials: [Partials.Message, Partials.Reaction, Partials.Channel],
shards: 'auto',
});
// Load commands // Load commands
client.commands = new Collection(); client.commands = new Collection();
await loadCommandFromDir('fun');
await loadCommandFromDir('utility'); fs.readdir(`${__dirname}/commands`, (err, categoryPath) => {
await loadCommandFromDir('admin'); if (err) {
await loadCommandFromDir('owner'); return console.error(err);
}
categoryPath.forEach(category => {
loadCommandFromDir(category);
});
});
// Load events // Load events
await loadEventFromDir('client', client); loadEventFromDir('client', client);
await loadEventFromDir('process', process); if (NODE_ENV !== 'development') {
loadEventFromDir('process', process);
}
client.login(token); client.login(token);
@ -30,10 +39,13 @@ async function loadCommandFromDir(dir) {
for (const file of commandFiles) { for (const file of commandFiles) {
const filePath = path.join(commandsPath, file); const filePath = path.join(commandsPath, file);
let command = await import(filePath); import(pathToFileURL(filePath))
command = command.default; .then(importedCommand => {
const command = importedCommand.default;
client.commands.set(command.data.name, command); client.commands.set(command.data.name, command);
console.log(`Successfully loaded command \x1b[32m${command.category}/${command.data.name}\x1b[0m`);
})
.catch(error => console.error(`Failed to load command for path: ${filePath}`, error));
} }
} }
@ -43,13 +55,16 @@ async function loadEventFromDir(dir, listener) {
for (const file of eventFiles) { for (const file of eventFiles) {
const filePath = path.join(eventsPath, file); const filePath = path.join(eventsPath, file);
let event = await import(filePath); import(pathToFileURL(filePath))
event = event.default; .then(importedEvent => {
const event = importedEvent.default;
if (event.once) { if (event.once) {
listener.once(event.name, (...args) => event.execute(...args, client)); listener.once(event.name, (...args) => event.execute(...args, client));
} }
else { else {
listener.on(event.name, (...args) => event.execute(...args, client)); listener.on(event.name, (...args) => event.execute(...args, client));
} }
})
.catch(error => console.error(`Failed to load event for path: ${filePath}`, error));
} }
} }

View file

@ -1 +1 @@
["1488","14/88","14 88","niggar", "nigger","nigar", "kys", "kill yourself", "faggot", "fag", "kill ur self","n\ni\ng\ng\ne\nr","n i g g e r","we must secure the existance of our people and a future for white children."] ["1488","14/88","14 88","niggar", "niggars", "nigger", "niggers","nigar", "kys", "kill yourself", "faggot", "faggots", "fag", "fags", "kill ur self","n\ni\ng\ng\ne\nr","n i g g e r","we must secure the existance of our people and a future for white children."]

View file

@ -0,0 +1 @@
["baseball", "basketball", "football", "golf", "soccer", "tennis"]

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
["10 years later", "a day ago", "a day later", "a month ago", "a month later", "a week ago", "a week later", "a year ago", "a year later", "absentmindedly", "accidentally", "actively", "adventurously", "again", "all the time", "an hour ago", "an hour later", "anally", "angrily", "animatedly", "anxiously", "appreciatively", "ardently", "arrogantly", "artfully", "at sunrise", "at sunset", "at the full moon", "awkwardly", "barely", "beautifully", "billions of years ago", "bitterly", "biweekly", "blissfully", "boldly", "bravely", "brightly", "briskly", "buoyantly", "busily", "calmly", "carefully", "carelessly", "casually", "cautiously", "centenially", "certainly", "closely", "coaxingly", "coldly", "comfortably", "considerably", "continually", "cooly", "correctly", "courageously", "crossly", "crunchily", "curiously", "daily", "daintily", "deeply", "defiantly", "deliberately", "delicately", "deliciously", "delightedly", "densely", "determinedly", "diligently", "discreetly", "dreamily", "eagerly", "earnestly", "efficiently", "effortlessly", "elegantly", "energetically", "enthusiastically", "enviously", "every Tuesday", "every night", "every now and then", "evilly", "excessively", "excitedly", "expertly", "explosively", "extremely", "faithfully", "fearlessly", "ferociously", "fervently", "fiercely", "firmly", "five minutes later", "five years later", "fleetingly", "fluidly", "for 10 weeks", "for 36 hours", "forcibly", "forgivingly", "formally", "frantically", "frequently", "from now on", "generously", "genuinely", "gladly", "gleefully", "gloriously", "grandly", "gravely", "greatly", "greedily", "grudgingly", "grumpily", "happily", "hardly", "harmonically", "hastily", "hatefully", "heartily", "heavily", "hollowly", "honestly", "hungrily", "hurriedly", "illegally", "immediately", "imperfectly", "inappropriately", "indubitably", "instantly", "intensely", "intensively", "intentionally", "intently", "intuitively", "joyfully", "just 5 minutes ago", "justly", "keenly", "kindly", "laboriously", "later", "lightly", "loudly", "lovingly", "magestically", "magically", "majestically", "meekly", "mercifully", "merrily", "methodically", "mildly", "millions of years ago", "modestly", "mortally", "musically", "mysteriously", "naturally", "neatly", "necessarily", "never", "never again", "nimbly", "noisily", "nonchalantly", "now", "objectively", "obscenely", "occasionally", "often", "on Mondays", "once a month", "once a week", "once again", "once in a while", "outstandingly", "partially", "patiently", "peacefully", "perfectly", "perpetually", "persistently", "pointedly", "presently", "professionally", "profusely", "promptly", "proudly", "purposefully", "quickly", "quietly", "quite", "rapidly", "rashly", "readily", "really", "rebelliously", "recently", "recklessly", "regretfully", "repeatedly", "restlessly", "richly", "romantically", "roughly", "royally", "sadly", "saggingly", "sarcastically", "secretly", "several times", "shamefully", "sharply", "side to side", "silently", "single-handedly", "six months later", "slickly", "slimily", "sloppily", "slowly", "sluggishly", "smartly", "smoothly", "snugly", "so", "solemnly", "sometimes", "speedily", "spiritedly", "spontaneously", "stealthily", "strenuously", "strictly", "strongly", "stubbornly", "suspiciously", "sweetly", "sympathetically", "systematically", "terribly", "terrifyingly", "thickly", "thirstily", "this time", "thoroughly", "thoughtlessly", "ticklishly", "to kingdom come", "today", "tonight", "too", "torturously", "trillions of years ago", "truly", "unbelievably", "unnecessarily", "until further notice", "up and down", "urgently", "usually", "very", "victoriously", "vigilantly", "vigorously", "violently", "warily", "weakly", "wetly", "wholeheartedly", "wildly", "wisely", "with haste", "yesterday"]

View file

@ -0,0 +1 @@
["50 Cent", "Adam Sandler", "Alden Ehrenreich", "Anne Hathaway", "Arnold Schwarzenegger", "Axl Rose"," Ben Shapiro", "Ben Stein", "Ben Stiller", "Bernie Sanders", "Bill Murray", "Brad Pitt", "Channing Tatum", "Christian Bale", "Daniel Radcliffe", "Danny Masterson", "David Lee Roth", "Donald Trump", "Duff McKagan", "Eddie Van Halen", "Edward Norton", "Eminem", "Emma Stone", "Emma Watson", "Geoffery Lewis", "Hillary Clinton", "Hugh Jackson", "Jack Black", "Jake Epstein", "Jesse Eisenberg", "Jessica Alba", "John Cena", "John Travolta", "Johnny Depp", "Julia Garner", "Julia Roberts", "Justin Bieber", "Laura Prepon", "Lena Dunham", "Leonardo DiCaprio", "Marilyn Manson", "Megan Fox", "Michael Cera", "Morgan Freeman", "Natalie Portman", "Nicolas Cage", "Orlando Bloom", "Rihanna", "Robert Downey Jr.", "Robin Willians", "Ryan Seacrest", "Scarlett Johansson", "Serj Tankian", "Shia LaBeouf", "Slash", "Tom Cruise", "Tom Hanks", "Trent Reznor", "Will Ferrell", "Will Smith", "Woody Allen", "Zac Effron"]

View file

@ -0,0 +1 @@
["Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Ashmore and Cartier Islands", "Australia", "Austria", "Azerbaijan", "Bahrain", "Bangladesh", "Barbados", "Bassas da India", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia", "Cameroon", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Clipperton Island", "Cocos Islands", "Colombia", "Comoros", "Cook Islands", "Coral Sea Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Czech Republic", "Democratic Republic of the Congo", "Denmark", "Dhekelia", "Djibouti", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Europa Island", "Falkland Islands", "Faroe Islands", "Federated States of Micronesia", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern and Antarctic Lands", "Gabon", "Gaza Strip", "Georgia", "Germany", "Ghana", "Gibraltar", "Glorioso Islands", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", "Guinea_Bissau", "Guyana", "Haiti", "Heard Island and McDonald Islands", "Holy See", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Jan Mayen", "Japan", "Jersey", "Jordan", "Juan de Nova Island", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Moldova", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Namibia", "Nauru", "Navassa Island", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "North Korea", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paracel Islands", "Paraguay", "Peru", "Philippines", "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar", "Republic of the Congo", "Reunion", "Romania", "Russia", "Rwanda", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Spratly Islands", "Sri Lanka", "Sudan", "Suriname", "Svalbard", "Swaziland", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "The Bahamas", "The Gambia", "Timor_Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tromelin Island", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Virgin Islands", "Wake Island", "Wallis and Futuna", "West Bank", "Western Sahara", "Yemen", "Zambia", "Zimbabwe"]

View file

@ -0,0 +1 @@
["AIDS", "Alzheimer's", "Down syndrome", "Hepatitis A", "Hepatitis B", "Hepatitis C", "acute inclusion body encephalitis", "albinism", "alveolitis", "amnesia", "anal sacculitis", "anaphylaxis", "appendicitis", "arsenic poisoning", "arthritis", "athlete's foot", "autism", "bipolar disorder", "bronchitis", "deep vein thrombosis", "delirium", "dementia", "depression", "diarrhea", "dissociative identity disorder", "ebola", "epilepsy", "fibrosis", "folliculitis", "gastroenteritis", "halitosis", "herpes", "hyperthyroidism", "hypothyroidism", "influenza", "laryngitis", "lead poisoning", "lupus", "meningitis", "mental retardation", "mercury poisoning", "osteoporosis", "pinkeye", "plague", "rheumatoid arthritis", "sacculitis", "schizophrenia", "smallpox", "strep throat", "tetanus", "tonsilitis", "type I diabetes", "type II diabetes", "typhoid fever"]

View file

@ -0,0 +1 @@
["actinium", "aluminum", "americium", "antimony", "argon", "arsenic", "astatine", "barium", "berkelium", "beryllium", "bismusth", "boron", "bromine", "cadmium", "calcium", "californium", "carbon", "cerium", "cesium", "chlorine", "chromium", "cobalt", "copper", "curium", "dysprosium", "einsteinium", "erbium", "europium", "fermium", "fluorine", "francium", "gadolinium", "gallium", "germanium", "gold", "hafnium", "helium", "holmium", "hydrogen", "indium", "iodine", "iridium", "iron", "krypton", "lanthanum", "lawrencium", "lead", "lithium", "lutetium", "magnesium", "manganese", "mendelevium", "mercury", "molybdenum", "neodymium", "neon", "neptunium", "nickel", "niobium", "nitrogen", "nobelium", "osmium", "oxygen", "palladium", "phosphorus", "platinum", "plutonium", "polonium", "potassium", "praseodymium", "promethium", "protactinium", "radium", "radon", "rhenium", "rhodium", "rubidium", "ruthenium", "samarium", "scandium", "selenium", "silicon", "silver", "sodium", "strontium", "sulfur", "tantalum", "technetium", "tellurium", "terbium", "thallium", "thorium", "thulium", "tin", "titanium", "tungsten", "uranium", "vanadium", "xenon", "ytterbium", "yttrium", "zinc", "zircomium"]

View file

@ -0,0 +1 @@
["as if!", "no way!", "boo!", "please!", "oh my!", "that can't be!", "wow!", "oh boy...", "oh boy!", "oh dear", "ew!", "gross!", "ugh!", "big deal!", "whatever!", "I don't care!", "big whoop!", "it doesn't matter!", "get out!", "duh!", "no kidding!", "well, duh!", "well, yeah!", "you don't say?", "oh, no!", "finally!", "at last!", "at long last!", "about time!", "it's about time!", "yuck!", "yucky!", "ick!", "icky!", "rats!", "Jesus Christ!", "mother of god!", "amazing!", "damn it!"]

View file

@ -0,0 +1 @@
["hello", "hi", "hey", "greetings", "good day", "good morning", "good afternoon", "good evening"]

View file

@ -0,0 +1 @@
["PC building", "acting", "antique-shopping", "baking", "bank robbery", "baseball", "basket-weaving", "calligraphy", "camping", "cannibalism", "carving", "cooking", "cooking meth", "crocheting", "dancing", "deep-sea diving", "drawing", "drug dealing", "fantasy football", "farming", "filmmaking", "fishing", "flying", "gaming", "gardening", "glassblowing", "golf", "guitar", "hiking", "human dissection", "hunting", "knitting", "money laundering", "mushroom cultivation", "music composition", "opera singing", "painting", "partying", "piano", "poetry", "politics", "pottery", "programming", "pyrotechnics", "resurrecting the dead", "running", "sailing", "scuba diving", "sculpting", "sewing", "shopping", "singing", "skateboarding", "skydiving", "spearfishing", "taxidermy", "theater", "time travel", "toy collecting", "urban exploration", "web-browsing", "whittling", "writing"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
["a", "anti", "auto", "bi", "circum", "contra", "endo", "exo", "extra", "fore", "homo", "hyper", "in", "intra", "mega", "mid", "mini", "mis", "mono", "non", "octo", "omni", "over", "penta", "post", "pre", "pseudo", "psycho", "pyro", "quad", "semi", "sub", "super", "trans", "tri", "un", "under", "uni"]

View file

@ -0,0 +1 @@
["he", "it", "she", "they"]

View file

@ -0,0 +1 @@
["Alabama", "Alaska", "Arizona", "Arkansas", "Baden-W\u00fcrttemberg", "Bavaria", "Berlin", "Brandenburg", "Bremen", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hamburg", "Hawaii", "Hesse", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Lower Saxony", "Maine", "Maryland", "Massachusetts", "Mecklenburg-Vorpommern", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "North Rhine-Westphalia", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhineland-Palatinate", "Rhode Island", "Saarland", "Saxony", "Saxony-Anhalt", "Schleswig-Holstein", "South Carolina", "South Dakota", "Tennessee", "Texas", "Thuringia", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]

View file

@ -0,0 +1 @@
["Colonel", "Daddy", "Dojo", "Dr.", "Governor", "Granny", "Honorable", "King", "Madam", "Mama", "Master", "Mayor", "Mistress", "Moist", "Mr.", "Mrs.", "Ms", "Old", "Papa", "Prince", "Professor", "Queen", "Sensei", "Sergeant", "Sir"]

View file

@ -0,0 +1 @@
["bucket", "centimeter", "century", "cubic centimeter", "cup", "day", "foot", "gallon", "gram", "handful", "hour", "inch", "joule", "kilogram", "kilojoule", "kilometer", "kilovolt", "kilowatt", "light-year", "liter", "megaton", "megawatt", "meter", "microfarad", "mile", "milliampere", "milliliter", "millisecond", "millivolt", "milliwatt", "minute", "month", "mouthful", "ounce", "pint", "pound", "quart", "second", "tablespoon", "teaspoon", "ton", "yard", "year"]

View file

@ -0,0 +1 @@
["abduct", "abolish", "align", "amend", "amputate", "annihilate", "announce", "appreciate", "apprehend", "articulate", "assault", "associate", "attack", "authenticate", "bake", "bang", "barbeque", "bathe", "bawl", "beep", "behead", "belly-flop", "bind", "bite", "blacken", "blast", "bleed", "bless", "bloat", "bloom", "blossom", "blow", "bludgeon", "boil", "breastfeed", "breathe", "bubble", "burn", "burp", "cackle", "call", "caress", "chew", "chill", "choke", "chomp", "chop", "chow", "churn", "clean", "click", "clip", "commandeer", "conserve", "convict", "cook", "crack", "cram", "crank", "crash", "crawl", "cremate", "cripple", "croak", "crouch", "crumple", "crunch", "crush", "cry", "cuddle", "cultivate", "customize", "cut", "cut in half", "dangle", "darken", "decapitate", "declare", "decorate", "deep-fry", "defecate", "defy", "deny", "despise", "dice", "dig", "discipline", "dishonor", "dislike", "dismantle", "dissect", "dominate", "donate", "draft", "drain", "dramatize", "drip", "dry-freeze", "dye", "eat", "ejaculate", "eject", "elect", "electrocute", "eliminate", "embrace", "enforce", "examine", "exclaim", "explode", "exploit", "extend", "extrapolate", "extrude", "fabricate", "fall apart", "falsify", "fart", "feed", "fester", "fiddle", "fight", "fistfight", "fizz", "flap", "flash", "flatten", "flick", "floss", "flutter", "fly", "force", "forecast", "freeze", "froth", "fume", "gallop", "gargle", "gleam", "glorify", "glow", "glue", "gnaw", "gouge", "grab", "grate", "grind", "grip", "groom", "grow", "grunt", "gulp", "guzzle", "gyrate", "hack", "hammer", "handle", "hang", "harass", "harden", "headbutt", "hiccup", "hiss", "hog-tie", "hoist", "hoot", "hug", "hunt", "hurl", "hypnotize", "impale", "impeach", "impede", "implode", "inaugurate", "infest", "ingest", "inject", "injure", "invigorate", "iron", "jargogle", "jerk", "jet-spray", "jimmy", "jingle", "joust", "kick", "kidnap", "kill", "kiss", "knead", "knock out", "lather", "laugh", "lay", "lecture", "legalize", "lick", "lighten", "like", "liquefy", "liquidate", "loathe", "loosen", "maim", "make fun of", "mangle", "manhandle", "manipulate", "march", "marinate", "massage", "masticate", "maul", "mist", "misuse", "moan", "moisten", "move", "mumble", "mutilate", "mutter", "nab", "nail", "need", "nip", "obfuscate", "oil", "organize", "paint", "palpitate", "penetrate", "pepper", "petition", "pickle", "pierce", "pillage", "pinch", "piss", "plaster", "pluck", "plunge", "poke", "polish", "pop", "pour", "pray", "preen", "press", "probe", "prod", "prosecute", "prove", "puff", "pull", "pump", "punch", "punish", "purify", "push", "put up with", "quaff", "quantify", "radiate", "ram", "rapture", "ratify", "rattle", "rearrange", "recycle", "report", "ride", "rip", "ripple", "rise", "roar", "rob", "roll", "rot", "rub", "run", "run over", "run after", "rustle", "salt", "sanitize", "savor", "say", "scold", "scorch", "scratch", "scream", "screech", "screw", "scrub", "scrunch", "sculpt", "season", "serve", "set on fire", "shake", "sharpen", "shatter", "shave", "shimmer", "shine", "shiver", "shoot", "shout", "shove", "shower", "shred", "shriek", "shrink", "shudder", "sit", "skewer", "skip", "slam", "slap", "slash", "slay", "sleepwalk", "slip", "slit", "slouch", "slurp", "smack", "smear", "smoke", "smolder", "snap", "snicker", "sniff", "snip", "snoop", "snort", "snuffle", "snuggle", "soak", "soften", "solidify", "spank", "sparkle", "spelunk", "spit", "splash", "splatter", "spray", "sprinkle", "sprint", "spurt", "sputter", "square dance", "squat", "squeal", "squeeze", "squelch", "squirt", "squish", "stab", "stampede", "stand", "staple", "steam", "stew", "stick", "stimulate", "stir", "stomp", "strain", "strangle", "strap", "strategize", "streamline", "stress-test", "strike", "strut", "stuff", "suckle", "sue", "superglue", "swallow", "swear", "swipe", "tap", "tape", "throttle", "throw", "tickle", "tighten", "tinkle", "tiptoe", "toast", "toke", "tongue", "touch", "transcribe", "tremble", "trot", "tune", "twang", "twinkle", "twist", "undermine", "undress", "undulate", "uproot", "urinate", "vaporize", "venerate", "verify", "veto", "vibrate", "vomit", "waddle", "wail", "walk", "want", "waste", "wave", "wedge", "whimper", "whine", "whip", "whisper", "whiten", "wiggle", "wilt", "withdraw", "wrinkle", "yank", "yell", "zip"]

View file

@ -0,0 +1 @@
["yes", "yeah", "sure", "indeed", "affirmative", "absolutely", "yup", "yep", "no", "nope", "nah", "negative", "absolutely not"]

3
json/listening.json Normal file
View file

@ -0,0 +1,3 @@
[
"psychometricBussdown by oddballTheatre"
]

View file

@ -0,0 +1,27 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('optouts', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userID: {
type: Sequelize.BIGINT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('optouts');
}
};

23
models/optout.js Normal file
View file

@ -0,0 +1,23 @@
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class optout extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
optout.init({
userID: DataTypes.BIGINT
}, {
sequelize,
modelName: 'optout',
});
return optout;
};

6340
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node .", "start": "node --env-file .env .",
"deploy": "node deploy-commands.cjs", "deploy": "node --env-file .env scripts/deploy-commands.js",
"deployGlobally": "node deploy-commands.cjs global", "deployGlobally": "node --env-file .env scripts/deploy-commands.js global",
"lint": "eslint .", "lint": "eslint .",
"lintfix": "eslint . --fix", "lintfix": "eslint . --fix",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -17,23 +17,25 @@
"homepage": "https://libtar.de", "homepage": "https://libtar.de",
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@discordjs/builders": "^0.13.0", "@discordjs/rest": "^2.3.0",
"@discordjs/rest": "^0.4.1", "discord-api-types": "^0.37.91",
"discord-api-types": "^0.33.1", "discord.js": "^14.15.3",
"discord.js": "^13.7.0", "mariadb": "^3.3.1",
"dotenv": "^16.0.1", "node-fetch": "^3.3.2",
"mariadb": "^3.0.1", "sequelize": "^6.37.3",
"mysql2": "^2.3.3", "turndown": "^7.2.0",
"node-fetch": "^3.2.6", "twitter-api-v2": "^1.17.1",
"sequelize": "^6.21.3", "ytpplus-node": "github:Supositware/ytpplus-node"
"turndown": "^7.1.1",
"twit": "^2.2.11"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.18.9", "@babel/eslint-parser": "^7.18.9",
"@babel/plugin-syntax-import-assertions": "^7.18.6", "@babel/plugin-syntax-import-assertions": "^7.18.6",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.6.0",
"@types/node": "^18.7.3", "@types/node": "^18.7.3",
"eslint": "^8.16.0", "eslint": "^8.57.0",
"sequelize-cli": "^6.4.1" "globals": "^15.8.0",
"sequelize-cli": "^6.4.1",
"sqlite3": "^5.1.7"
} }
} }

View file

@ -1,29 +0,0 @@
const fs = require('node:fs');
const https = require('node:https');
console.log('Downloading latest version of yt-dlp');
const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
download(downloadUrl);
function download(url) {
https.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`yt-dlp download url: ${res.headers.location}`);
return download(res.headers.location);
}
const path = './bin/yt-dlp';
const filePath = fs.createWriteStream(path);
res.pipe(filePath);
filePath.on('finish', () => {
filePath.close();
fs.chmodSync('./bin/yt-dlp', '755');
console.log('yt-dlp download finished.');
});
filePath.on('error', (err) => {
filePath.close();
console.error(err.message);
});
});
}

View file

@ -11,12 +11,12 @@ These instructions will get you a copy of the project up and running on your loc
You need to install the following You need to install the following
* ffmpeg (Optional but very recommanded: for yt-dlp to merge video/audio formats) * ffmpeg & ffprobe (Optional but very recommanded: for yt-dlp to merge video/audio formats and Handbrake to compress videos.)
* yt-dlp ([a file can download it for you](prereq.cjs)) * yt-dlp ([a file can download it for you](scripts/updateytdlp.js))
* HandBrakeCLI (For [download](commands/utility/download.js)) * HandBrakeCLI (For [download](commands/utility/download.js))
* gifsicle (For [vid2gif](commands/utility/vid2gif.js)) * gifsicle (For [vid2gif](commands/utility/vid2gif.js))
* gifki (For [vid2gif](commands/utility/vid2gif.js)) * gifki (For [vid2gif](commands/utility/vid2gif.js))
* Somewhere to upload files larger than 8 mb (I use a self hosted [XBackBone](https://github.com/SergiX44/XBackBone/) with the upload.sh script made from it, you can use anything else just need to be located in bin/upload.sh) * Somewhere to upload files larger than the file limit, currently 25 mb. (I use a self hosted [XBackBone](https://github.com/SergiX44/XBackBone/) with the upload.sh script made from it, you can use anything else just need to be located in bin/upload.sh)
### Installing ### Installing
``` ```
@ -27,10 +27,10 @@ npm install
``` ```
To run the bot for the first time you need to execute [deploy-commands.js](scripts/deploy-commands.js) so the commands can be registered, don't forget to set your .env accordingly. To run the bot for the first time you need to execute [deploy-commands.js](scripts/deploy-commands.js) so the commands can be registered, don't forget to set your .env accordingly.
``node scripts/deploy-commands.cjs`` ``node --env-file .env scripts/deploy-commands.cjs``
then you can just run it normally. then you can just run it normally.
``node index.js`` ``node --env-file .env index.js``
If you want to run the bot automatically you can use pm2 If you want to run the bot automatically you can use pm2
``` ```
@ -38,7 +38,7 @@ npm install -g pm2
pm2 start index.js --name (insert name) pm2 start index.js --name (insert name)
``` ```
If you are on linux and don't need automatic restart on crash you can just do If you are on linux and don't need automatic restart on crash you can just do
``nohup node index.js &`` ``nohup node --env-file .env index.js &``
## Built With ## Built With

View file

@ -1,134 +0,0 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
require('dotenv').config();
const { clientId, guildId, token } = process.env;
const commands = [
new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with pong!'),
new SlashCommandBuilder()
.setName('download')
.setDescription('Download a video.')
.addStringOption(option =>
option.setName('url')
.setDescription('url of the video you want to download.')
.setRequired(true))
.addBooleanOption(option =>
option.setName('format')
.setDescription('Choose the quality of the video.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('compress')
.setDescription('Compress the video?')
.setRequired(false)),
new SlashCommandBuilder()
.setName('reddit')
.setDescription('Send random images from the subreddit you choose')
.addStringOption(option =>
option.setName('subreddit')
.setDescription('The subreddit you wish to see')
.setRequired(true)),
new SlashCommandBuilder()
.setName('vid2gif')
.setDescription('Convert your video into a gif.')
.addStringOption(option =>
option.setName('url')
.setDescription('URL of the video you want to convert')
.setRequired(true)),
new SlashCommandBuilder()
.setName('feedback')
.setDescription('Send a feedback to the developer.')
.addStringOption(option =>
option.setName('feedback')
.setDescription('The message you want to send me.')
.setRequired(true)),
new SlashCommandBuilder()
.setName('inspirobot')
.setDescription('Get an image from inspirobot'),
new SlashCommandBuilder()
.setName('tweet')
.setDescription('Send tweet from Haha yes twitter account. Please do not use it for advertisement and keep it english')
.addStringOption(option =>
option.setName('content')
.setDescription('The content of the tweet you want to send me.')
.setRequired(false))
.addAttachmentOption(option =>
option.setName('image')
.setDescription('Optional attachment (Image only.)')
.setRequired(false)),
new SlashCommandBuilder()
.setName('4chan')
.setDescription('Send random images from a 4chan board of your choosing!')
.addStringOption(option =>
option.setName('board')
.setDescription('The board you wish to see')
.setRequired(true)),
new SlashCommandBuilder()
.setName('donator')
.setDescription('All the people who donated for this bot <3'),
new SlashCommandBuilder()
.setName('donate')
.setDescription('Show donation link for the bot.'),
new SlashCommandBuilder()
.setName('about')
.setDescription('About me (The bot)'),
new SlashCommandBuilder()
.setName('stats')
.setDescription('Show some stats about the bot'),
new SlashCommandBuilder()
.setName('fakeuser')
.setDescription('Fake a user with webhooks')
.addMentionableOption(option =>
option.setName('user')
.setDescription('Who do you want to fake?')
.setRequired(true))
.addStringOption(option =>
option.setName('message')
.setDescription('What message do you want me to send?')
.setRequired(true))
.addAttachmentOption(option =>
option.setName('image')
.setDescription('Optional attachment (Image only.)')
.setRequired(false)),
new SlashCommandBuilder()
.setName('s')
.setDescription('What could this be 🤫')
.addStringOption(option =>
option.setName('something')
.setDescription('🤫')
.setRequired(true)),
]
.map(command => command.toJSON());
const rest = new REST({ version: '9' }).setToken(token);
if (process.argv[2] === 'global') {
rest.put(Routes.applicationCommands(clientId), { body: commands })
.then(() => console.log('Successfully registered application commands globally.'))
.catch(console.error);
}
else if (process.argv[2] === 'delete') {
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: [] })
.then(() => console.log('Successfully deleted all guild commands.'))
.catch(console.error);
}
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands })
.then(() => console.log(`Successfully registered application commands for the guild ${guildId}.`))
.catch(console.error);

View file

@ -0,0 +1,42 @@
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const { clientId, guildId, token } = process.env;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const commands = [];
const categoryPath = fs.readdirSync(`${__dirname}/../commands`);
for (let i = 0; i < categoryPath.length; i++) {
const commandsPath = path.join(`${__dirname}/../commands`, categoryPath[i]);
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = await import(pathToFileURL(filePath));
if (command.default.integration_types) {
Object.assign(command.default.data, { integration_types: command.default.integration_types });
Object.assign(command.default.data, { contexts: [0, 1, 2] });
}
commands.push(command.default.data.toJSON());
}
}
const rest = new REST({ version: '9' }).setToken(token);
if (process.argv[2] === 'global') {
rest.put(Routes.applicationCommands(clientId), { body: commands })
.then(() => console.log('Successfully registered application commands globally.'))
.catch(console.error);
}
else if (process.argv[2] === 'delete') {
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: [] })
.then(() => console.log('Successfully deleted all guild commands.'))
.catch(console.error);
}
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands })
.then(() => console.log(`Successfully registered application commands for the guild ${guildId}.`))
.catch(console.error);

View file

@ -1,42 +0,0 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
require('dotenv').config();
const { clientId, guildId, token } = process.env;
const commands = [
new SlashCommandBuilder()
.setName('die')
.setDescription('Kill the bot'),
new SlashCommandBuilder()
.setName('ublacklist')
.setDescription('Blacklist a user from the bot')
.addStringOption(option =>
option.setName('command')
.setDescription('Which command do you want to get a user blacklisted from?')
.setRequired(true))
.addStringOption(option =>
option.setName('userid')
.setDescription('Who do you want to blacklist?')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('The reason of the blacklist.')
.setRequired(false)),
new SlashCommandBuilder()
.setName('deletewteet')
.setDescription('Delete a tweet')
.addStringOption(option =>
option.setName('tweetid')
.setDescription('The id of the tweet you wish to delete.')
.setRequired(true)),
]
.map(command => command.toJSON());
const rest = new REST({ version: '9' }).setToken(token);
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands })
.then(() => console.log(`Successfully registered application commands for the guild ${guildId}.`))
.catch(console.error);

34
scripts/downloadutils.js Normal file
View file

@ -0,0 +1,34 @@
import fs from 'node:fs';
import https from 'node:https';
export default {
download,
};
async function download(url, output) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`${output} download url: ${res.headers.location}`);
return download(res.headers.location, output);
}
const path = output;
const tmpPath = `${output}.new`;
const filePath = fs.createWriteStream(tmpPath);
res.pipe(filePath);
filePath.on('finish', () => {
filePath.close();
fs.renameSync(tmpPath, path);
fs.chmodSync(path, '755');
console.log(`${url} download finished.`);
return resolve(true);
});
filePath.on('error', (err) => {
filePath.close();
return reject(err);
});
});
});
}

View file

@ -0,0 +1,27 @@
import fetch from 'node-fetch';
import { Client, GatewayIntentBits } from 'discord.js';
const { botsggToken, botsggEndpoint, token } = process.env;
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
await client.login(token);
const body = {
guildCount: client.guilds.cache.size,
};
console.log(body);
const response = await fetch(`${botsggEndpoint}/bots/${client.user.id}/stats`, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Authorization': botsggToken, 'Content-Type': 'application/json' },
});
const data = await response.json();
console.log(data);
process.exit();

View file

@ -1,41 +1,17 @@
import fs from 'node:fs'; // This is kind of useless since you can just do `./yt-dlp --update-to nightly` which I didn't know about when I wrote that.
import https from 'node:https'; import utils from './downloadutils.js';
if (process.platform !== 'linux' && process.argv[2] !== '-f') { (async () => {
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f'); if (process.platform !== 'linux' && process.argv[2] !== '-f') {
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f or execute ./bin/yt-dlp --update-to nightly');
process.exit(1); process.exit(1);
} }
else if (process.platform !== 'linux' && process.argv[2] === '-f') { else if (process.platform !== 'linux' && process.argv[2] === '-f') {
console.log('Executed with -f. Reminder that this script only download the linux version of yt-dlp.'); console.log('Executed with -f. Reminder that this script only download the linux version of yt-dlp.');
}
const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
download(downloadUrl);
async function download(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
console.log(`yt-dlp download url: ${res.headers.location}`);
return download(res.headers.location);
} }
const tmpPath = './bin/yt-dlp.new'; const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
const path = './bin/yt-dlp';
const filePath = fs.createWriteStream(tmpPath); await utils.download(downloadUrl, './bin/yt-dlp');
res.pipe(filePath);
filePath.on('finish', () => { });
filePath.close();
fs.renameSync(tmpPath, path);
fs.chmodSync('./bin/yt-dlp', '755');
console.log('yt-dlp download finished.');
resolve(true);
});
filePath.on('error', (err) => {
filePath.close();
reject(err);
});
});
});
}

Some files were not shown because too many files have changed in this diff Show more