Compare commits

...

120 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
58 changed files with 2363 additions and 4175 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
@ -15,3 +15,6 @@ botsggToken=APITokenForBots.gg
botsggEndpoint=https://discord.bots.gg/api/v1 botsggEndpoint=https://discord.bots.gg/api/v1
stableHordeApi=0000000000 stableHordeApi=0000000000
stableHordeID=0000 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

11
.gitignore vendored
View file

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

0
bin/.keep Normal file
View file

View file

@ -27,6 +27,7 @@ export default {
.setDescription('What do you want the AI to generate?') .setDescription('What do you want the AI to generate?')
.setRequired(true)), .setRequired(true)),
category: 'AI', category: 'AI',
alias: ['i2i'],
async execute(interaction, args, client) { async execute(interaction, args, client) {
await interaction.deferReply(); await interaction.deferReply();
@ -77,6 +78,12 @@ async function generate(i, prompt, client, b64Img) {
let response = await fetch('https://stablehorde.net/api/v2/generate/async', fetchParameters); let response = await fetch('https://stablehorde.net/api/v2/generate/async', fetchParameters);
response = await response.json(); 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 wait_time = 5000;
let checkURL = `https://stablehorde.net/api/v2/generate/check/${response.id}`; let checkURL = `https://stablehorde.net/api/v2/generate/check/${response.id}`;
const checking = setInterval(async () => { const checking = setInterval(async () => {
@ -84,8 +91,13 @@ async function generate(i, prompt, client, b64Img) {
if (checkResult === undefined) return; if (checkResult === undefined) return;
if (!checkResult.done) { 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) { if (checkResult.wait_time < 0) {
console.log(checkResult); console.log(checkResult.raw);
clearInterval(checking); clearInterval(checking);
return i.editReply({ content: 'No servers are currently available to fulfill your request, please try again later.' }); return i.editReply({ content: 'No servers are currently available to fulfill your request, please try again later.' });
} }
@ -116,21 +128,14 @@ async function generate(i, prompt, client, b64Img) {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`regenerate${i.user.id}`) .setCustomId(`regenerate${i.user.id}${i.id}`)
.setLabel('🔄 Regenerate') .setLabel('🔄 Regenerate')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
); );
await i.editReply({ embeds: [stableEmbed], components: [row], files: [generatedImg] }); await i.editReply({ embeds: [stableEmbed], components: [row], files: [generatedImg] });
client.once('interactionCreate', async (interactionMenu) => { listenButton(client, i, prompt);
if (i.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
if (interactionMenu.customId === `regenerate${interactionMenu.user.id}`) {
await interactionMenu.deferReply();
await generate(interactionMenu, prompt, client);
}
});
} }
}, wait_time); }, wait_time);
} }
@ -140,14 +145,27 @@ async function checkGeneration(url) {
check = await check.json(); check = await check.json();
if (!check.is_possible) { if (!check.is_possible) {
return { done: false, wait_time: -1 }; return { done: false, wait_time: -1, raw: check };
} }
if (check.done) { if (check.done) {
if (!check.generations) { if (!check.generations) {
return { done: false, wait_time: check.wait_time * 1000 }; 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 }; 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);
}
});
}

View file

@ -23,6 +23,7 @@ export default {
.setDescription('What do you want the AI to generate?') .setDescription('What do you want the AI to generate?')
.setRequired(true)), .setRequired(true)),
category: 'AI', category: 'AI',
alias: ['t2i'],
async execute(interaction, args, client) { async execute(interaction, args, client) {
await interaction.deferReply(); await interaction.deferReply();
generate(interaction, args.prompt, client); generate(interaction, args.prompt, client);
@ -101,21 +102,14 @@ async function generate(i, prompt, client) {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`regenerate${i.user.id}`) .setCustomId(`regenerate${i.user.id}${i.id}`)
.setLabel('🔄 Regenerate') .setLabel('🔄 Regenerate')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
); );
await i.editReply({ embeds: [stableEmbed], components: [row], files: [generatedImg] }); await i.editReply({ embeds: [stableEmbed], components: [row], files: [generatedImg] });
client.once('interactionCreate', async (interactionMenu) => { listenButton(client, i, prompt);
if (i.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
if (interactionMenu.customId === `regenerate${interactionMenu.user.id}`) {
await interactionMenu.deferReply();
await generate(interactionMenu, prompt, client);
}
});
} }
}, wait_time); }, wait_time);
} }
@ -136,3 +130,16 @@ async function checkGeneration(url) {
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 }; 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

@ -19,13 +19,13 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}`) .setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes') .setLabel('Yes')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`no${interaction.user.id}`) .setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No') .setLabel('No')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
); );
@ -39,18 +39,24 @@ export default {
return interaction.editReply({ content: 'Auto response has been enabled.', ephemeral: true }); return interaction.editReply({ content: 'Auto response has been enabled.', ephemeral: true });
} }
client.on('interactionCreate', async (interactionMenu) => { return listenButton(client, interaction, interaction.user);
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}`) {
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 });
}
});
}, },
}; };
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 });
}
});
}

View file

@ -25,44 +25,50 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}`) .setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit') .setLabel('Edit')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}`) .setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove') .setLabel('Remove')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}`) .setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing') .setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary), .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 }); await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
client.on('interactionCreate', async (interactionMenu) => { return listenButton(client, interaction, args, interaction.user);
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}`) {
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}`) {
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 });
}
});
}, },
}; };
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

@ -20,13 +20,13 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}`) .setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes') .setLabel('Yes')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`no${interaction.user.id}`) .setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No') .setLabel('No')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
); );
@ -40,18 +40,23 @@ export default {
return interaction.editReply({ content: 'Quotation has been enabled.', ephemeral: true }); return interaction.editReply({ content: 'Quotation has been enabled.', ephemeral: true });
} }
client.on('interactionCreate', async (interactionMenu) => { return listenButton(client, interaction, interaction.user);
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}`) {
const body = { serverID: interaction.guild.id, stat: 'disable' };
await db.quotationStat.update(body, { 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 });
}
});
}, },
}; };
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

@ -1,4 +1,4 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits } from 'discord.js'; import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits, PermissionsBitField } from 'discord.js';
import os from 'node:os'; import os from 'node:os';
import fs from 'node:fs'; import fs from 'node:fs';
@ -51,12 +51,12 @@ export default {
if (args.remove) { if (args.remove) {
if (tag) { if (tag) {
if (tag.get('ownerID') == interaction.user.id || interaction.member.permissionsIn(interaction.channel).has('ADMINISTRATOR') || interaction.user.id == ownerId) { 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 } }); db.Tag.destroy({ where: { trigger: args.trigger, serverID: interaction.guild.id } });
return interaction.editReply('successfully deleted the following tag: ' + args.trigger); return interaction.editReply('successfully deleted the following tag: ' + args.trigger);
} }
else { 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`); 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 { else {
@ -77,56 +77,51 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}`) .setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit') .setLabel('Edit')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}`) .setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove') .setLabel('Remove')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}`) .setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing') .setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary), .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 }); await interaction.editReply({ content: 'This tag already exist, do you want to update it, remove it or do nothing?', components: [row], ephemeral: true });
client.on('interactionCreate', async (interactionMenu) => { return listenButton(client, interaction, args, interaction.user);
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}`) {
const body = { trigger: args.trigger, response: args.response, ownerID: interaction.user.id, serverID: interaction.guild.id };
await db.joinChannel.update(body, { where: { guildID: 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}`) {
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 });
}
});
} }
else { 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`); 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`);
} }
const join = await db.joinChannel.findOne({ where: { guildID: interaction.guild.id } });
if (!join && !args.message) {
return interaction.editReply({ 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.editReply({ content: `The join message have been set with ${args.message}`, ephemeral: true });
}
}, },
}; };
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 });
}
});
}

View file

@ -26,44 +26,50 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}`) .setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit') .setLabel('Edit')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}`) .setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove') .setLabel('Remove')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}`) .setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing') .setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary), .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 }); await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
client.on('interactionCreate', async (interactionMenu) => { return listenButton(client, interaction, args, interaction.user);
if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}`) {
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}`) {
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 });
}
});
}, },
}; };
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

@ -4,7 +4,7 @@ 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()

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

@ -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,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { EmbedBuilder } 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';
@ -8,7 +8,7 @@ import util from 'node:util';
import stream from 'node:stream'; 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'};
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,10 +16,10 @@ 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')
@ -28,6 +28,7 @@ export default {
category: 'fun', category: 'fun',
ratelimit: 3, ratelimit: 3,
cooldown: 86400, cooldown: 86400,
guildOnly: true,
async execute(interaction, args, client) { async execute(interaction, args, client) {
const content = args.content; const content = args.content;
const attachment = args.image; const attachment = args.image;
@ -39,13 +40,31 @@ export default {
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
let tweet = content; let tweet = content;
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
@ -54,13 +73,25 @@ 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 => {
if (tweet.toLowerCase().includes(word.toLowerCase())) {
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;
}
});
// Detect banned word (Blacklist the user directly) // 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))) { 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.' }; const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body); Blacklists.create(body);
@ -68,6 +99,7 @@ export default {
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;
} }
*/
// 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')) {
@ -81,11 +113,12 @@ 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 {
@ -107,17 +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' });
@ -134,74 +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) { const tweeted = await userClient.v2.tweet(tweet, options);
options = {
media_ids: new Array(data.media_id_string), const tweetid = tweeted.data.id;
}; const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
const TweetLink = `https://vxtwitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`;
let channel = await client.channels.resolve(twiChannel);
channel.send(TweetLink);
const Embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setDescription(tweet ? tweet : 'No content.')
.addFields(
{ name: 'Link', value: TweetLink, inline: true },
{ name: 'Tweet ID', value: tweetid, inline: true },
{ name: 'Channel ID', value: interaction.channel.id, inline: true },
{ name: 'Message ID', value: interaction.id, 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}`, inline: true },
);
}
else {
Embed.addFields({ name: 'message link', value: `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}` });
} }
T.post('statuses/update', options, function(err, response) { if (attachment) Embed.setImage(attachment.url);
if (err) {
// Rate limit exceeded
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; channel = await client.channels.resolve(twiLogChannel);
const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21']; channel.send({ embeds: [Embed] });
const TweetLink = `https://twitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`; return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` });
// Im too lazy for now to make an entry in config.json
let channel = client.channels.resolve(twiChannel);
channel.send(TweetLink);
const Embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setDescription(tweet ? tweet : 'No content.')
.addFields(
{ name: 'Link', value: TweetLink, inline: true },
{ name: 'Tweet ID', value: tweetid, inline: true },
{ name: 'Channel ID', value: interaction.channel.id, inline: true },
{ name: 'Message ID', value: interaction.id, 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}`, inline: true },
);
}
else {
Embed.addFields({ name: 'message link', value: `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}` });
}
if (attachment) Embed.setImage(attachment.url);
channel = client.channels.resolve(twiLogChannel);
channel.send({ embeds: [Embed] });
return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` });
});
} }
}, },
}; };

View file

@ -12,6 +12,9 @@ export default {
.setDescription('Force the generation of the video in non-nsfw channel.') .setDescription('Force the generation of the video in non-nsfw channel.')
.setRequired(false)), .setRequired(false)),
category: 'fun', category: 'fun',
ratelimit: 2,
cooldown: 60,
parallelLimit: 30,
async execute(interaction, args) { 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`); 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`);
@ -69,19 +72,19 @@ export default {
}, },
}; };
new YTPGenerator().configurateAndGo(options) await new YTPGenerator().configurateAndGo(options)
.then(() => { .then(() => {
loadingmsg.delete(); loadingmsg.delete();
return interaction.reply({ content: 'Here is your YTP! Remember, it might contain nsfw, so be careful!', files: [`${os.tmpdir()}/${interaction.id}_YTP.mp4`] }) 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 => { .catch(err => {
console.error(err); console.error(err);
return interaction.reply('Whoops, look like the vid might be too big for discord, my bad, please try again'); return interaction.followUp({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] });
}); });
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
loadingmsg.delete(); loadingmsg.delete();
return interaction.reply({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] }); return interaction.followUp({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] });
}); });
}, },
}; };

View file

@ -1,3 +1,5 @@
// TODO
// Switch to 'twitter-api-v2'
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import Twit from 'twit'; import Twit from 'twit';

View file

@ -43,7 +43,7 @@ export default {
.setTimestamp(); .setTimestamp();
user.send({ embeds: [Embed] }); user.send({ embeds: [Embed] });
return interaction.reply({ content: `DM sent to ${user.username}`, ephemeral: true }); return interaction.reply({ content: `DM sent to ${user.username} (${user.id})` });
/* /*
const Attachment = (message.attachments).array(); const Attachment = (message.attachments).array();
if (Attachment[0]) { if (Attachment[0]) {
@ -58,10 +58,10 @@ export default {
else { else {
client.users.resolve(user).send(Embed) client.users.resolve(user).send(Embed)
.then(() => { .then(() => {
return interaction.reply(`DM sent to ${user.tag}`); return interaction.reply(`DM sent to ${user.username}`);
}) })
.catch(() => { .catch(() => {
return interaction.reply(`Could not send a DM to ${user.tag}`); 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.`);
},
};

View file

@ -32,42 +32,58 @@ export default {
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') {
await client.users.fetch(userid); const guildid = userid;
user = client.users.resolve(userid).tag; 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}\``);
}
else {
let user = userid;
await client.users.fetch(userid);
user = client.users.resolve(userid).username;
return interaction.editReply(`${user} has been blacklisted from ${command} with the following reason \`${reason}\``); return interaction.editReply(`${user} (${userid}) has been blacklisted from ${command} with the following reason \`${reason}\``);
}
} }
else { else {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}`) .setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes') .setLabel('Yes')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`no${interaction.user.id}`) .setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No') .setLabel('No')
.setStyle(ButtonStyle.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 (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isButton) return;
interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}`) {
Blacklists.destroy({ where: { type:command, uid:userid } });
return interaction.editReply(`The following ID have been unblacklisted from ${command}: ${userid}`);
}
else {
return interaction.editReply('No one has been unblacklisted.');
}
});
} }
}, },
}; };
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 } });
return interaction.editReply(`The following ID have been unblacklisted from ${command}: ${userid}`);
}
else {
return interaction.editReply('No one has been unblacklisted.');
}
});
}

View file

@ -30,7 +30,7 @@ export default {
data: ${JSON.stringify(client.commands.get(args.commandname).data)}, data: ${JSON.stringify(client.commands.get(args.commandname).data)},
category: '${client.commands.get(args.commandname).category}', category: '${client.commands.get(args.commandname).category}',
async execute(interaction) { async execute(interaction) {
return interaction.reply('${args.placeholder}'); return interaction.reply('${args.placeholder.replace(/'/g, '\\\'')}');
}, },
}; };

View file

@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { EmbedBuilder } 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;
@ -15,7 +15,7 @@ export default {
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 = 'I\'m a fun multipurpose bot made using [discord.js](https://github.com/discordjs/discord.js)' let description = 'I\'m a fun multipurpose bot made using [discord.js](https://github.com/discordjs/discord.js)'
@ -25,7 +25,7 @@ export default {
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,26 +36,26 @@ export default {
description += 'No one :(\n'; description += 'No one :(\n';
} }
description += `\nThanks to ${tina.tag} (336492042299637771) for inspiring me for making this bot!`; 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 EmbedBuilder() 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)
.addFields( .addFields(
{ name: 'Current commit', value: stdout }, { name: 'Current commit', value: stdout },
{ name: 'Current maintainer', value: `${maintainer.tag} (${ownerId})` }, { name: 'Current maintainer', value: `${maintainer.username} (${ownerId})` },
{ name: 'Gitea (Main)', value: 'https://git.namejeff.xyz/Supositware/Haha-Yes', inline: true }, { name: 'Gitea (Main)', value: 'https://git.namejeff.xyz/Supositware/Haha-Yes', inline: true },
{ name: 'Github (Mirror)', value: 'https://github.com/Supositware/Haha-yes', inline: true }, { name: 'Github (Mirror)', value: 'https://github.com/Supositware/Haha-yes', inline: true },
{ name: 'Privacy Policy', value: 'https://libtar.de/discordprivacy.txt', inline: true }, { name: 'Privacy Policy', value: 'https://libtar.de/discordprivacy.txt', inline: true },
{ name: 'Status page', value: uptimePage.toString(), inline: true }, { name: 'Status page', value: uptimePage.toString(), inline: true },
) )
.setFooter({ text: `Original bot made by ${owner.tag} (267065637183029248)` }); .setFooter({ text: `Original bot made by ${creator.username} (267065637183029248)` });
interaction.reply({ embeds: [aboutEmbed] }); interaction.reply({ embeds: [aboutEmbed] });
}); });

View file

@ -18,6 +18,11 @@ export default {
cooldown: 86400, cooldown: 86400,
async execute(interaction, args) { async execute(interaction, args) {
const url = args.url; 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)) { if (!await utils.stringIsAValidurl(url)) {
console.error(`Not a url!!! ${url}`); console.error(`Not a url!!! ${url}`);
return interaction.reply({ content: '❌ This does not look like a valid url!', ephemeral: true }); return interaction.reply({ content: '❌ This does not look like a valid url!', ephemeral: true });
@ -25,7 +30,7 @@ export default {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
utils.downloadVideo(url, interaction.id, 'mp4') utils.downloadVideo(url, interaction.id, 'bestvideo[height<=?480]+bestaudio/best')
.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}`; const output = `${os.tmpdir()}/${file}`;

View file

@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { EmbedBuilder } 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()

View file

@ -17,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,11 +1,16 @@
import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, SelectMenuBuilder } from 'discord.js'; import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js';
import { exec } from 'node:child_process'; import { execFile } 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 client;
let cleanUp; 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()
@ -21,19 +26,27 @@ 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', category: 'utility',
alias: ['dl'], alias: ['dl'],
integration_types: [0, 1],
async execute(interaction, args, c) { async execute(interaction, args, c) {
client = c; client = c;
const url = args.url; const url = args.url;
const format = args.format; const format = args.format;
maxFileSize = await utils.getMaxFileSize(interaction.guild);
interaction.doCompress = args.compress; interaction.doCompress = args.compress;
if (interaction.cleanUp) { interaction.doAutocrop = args.autocrop;
cleanUp = interaction.cleanUp;
}
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
@ -48,7 +61,12 @@ export default {
if (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);
} }
@ -58,8 +76,8 @@ 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 => {
@ -89,8 +107,8 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new SelectMenuBuilder() new StringSelectMenuBuilder()
.setCustomId(`downloadQuality${interaction.user.id}`) .setCustomId(`downloadQuality${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected') .setPlaceholder('Nothing selected')
.setMinValues(1) .setMinValues(1)
.setMaxValues(2) .setMaxValues(2)
@ -103,25 +121,37 @@ export default {
client.on('interactionCreate', async (interactionMenu) => { client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return; if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return; if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === `downloadQuality${interaction.user.id}`) { 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';
if (interaction.member) {
if (interaction.member.displayHexColor) {
embedColour = interaction.member.displayHexColor;
}
}
const Embed = new EmbedBuilder() const Embed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy') .setColor(embedColour)
.setAuthor({ name: `Downloaded by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL(), url: url }) .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.tag}" message!` }); .setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.username}" message!` });
if (interaction.customId === `downloadQuality${interaction.user.id}`) { 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];
} }
@ -131,11 +161,9 @@ async function download(url, interaction, originalInteraction) {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id)); const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
let 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.doCompress) { if (compressInteraction.doCompress) {
const presets = [ 'Social 8 MB 3 Minutes 360p30', 'Social 50 MB 10 Minutes 480p30', 'Social 50 MB 5 Minutes 720p30', 'Social 100 MB 5 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 => {
@ -147,8 +175,8 @@ async function download(url, interaction, originalInteraction) {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new SelectMenuBuilder() new StringSelectMenuBuilder()
.setCustomId(`preset${interaction.user.id}`) .setCustomId(`preset${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected') .setPlaceholder('Nothing selected')
.addOptions(options), .addOptions(options),
); );
@ -158,42 +186,75 @@ async function download(url, interaction, originalInteraction) {
client.on('interactionCreate', async (interactionMenu) => { client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return; if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return; if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === `preset${interaction.user.id}`) { 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) cleanUp(); if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
} }
}); });
return; return;
} }
// If the video format is not one compatible with Discord, reencode it. // If the video format is not one compatible with Discord, reencode it unless autocrop is choosen in which case it gets reencoded anyway.
const bannedFormats = ['hevc']; if (!interaction.doAutocrop) {
const codec = await utils.getVideoCodec(output); const bannedFormats = ['av1'];
const codec = await utils.getVideoCodec(output);
if (bannedFormats.includes(codec)) { if (bannedFormats.includes(codec)) {
console.log('Reencoding video'); 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; const oldOutput = output;
output = `${os.tmpdir()}/264${file}`; output = `${os.tmpdir()}/autocrop${file}`;
await utils.ffmpeg(`-i ${oldOutput} -vcodec libx264 -acodec aac ${output}`); 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.followUp({ content: fileurl, 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 });
}
}
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) cleanUp();
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
}) })
.catch(async err => { .catch(async err => {
console.error(err); console.error(err);
@ -206,17 +267,68 @@ 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? // Delete the file as it apparently don't overwrite?
fs.rmSync(output); 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

@ -18,7 +18,7 @@ export default {
category: 'utility', category: 'utility',
async execute(interaction, args) { async execute(interaction, args) {
const Embed = new EmbedBuilder() const Embed = new EmbedBuilder()
.setAuthor({ name: `${interaction.user.tag} (${interaction.user.id})`, iconURL: interaction.user.avatarURL() }) .setAuthor({ name: `${interaction.user.username} (${interaction.user.id})`, iconURL: interaction.user.avatarURL() })
.setTimestamp(); .setTimestamp();
if (interaction.guild) Embed.addFields({ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true }); if (interaction.guild) Embed.addFields({ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true });

View file

@ -1,5 +1,6 @@
import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, PermissionsBitField } from 'discord.js'; import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, PermissionsBitField } from 'discord.js';
import fs from 'node:fs'; import fs from 'node:fs';
import ratelimiter from '../../utils/ratelimiter.js';
const { ownerId, prefix } = process.env; const { ownerId, prefix } = process.env;
const prefixs = prefix.split(','); const prefixs = prefix.split(',');
@ -113,6 +114,13 @@ export default {
embed.addFields({ name: 'Bot permission', value: `\`${perm.join('` `')}\``, inline: true }); 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`)) { 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`); const file = new AttachmentBuilder(`./asset/img/command/${command.category}/${command.data.name}.png`);
embed.setImage(`attachment://${command.data.name}.png`); embed.setImage(`attachment://${command.data.name}.png`);

View file

@ -8,6 +8,7 @@ export default {
.setDescription('The bot you want to make an invite link for.') .setDescription('The bot you want to make an invite link for.')
.setRequired(false)), .setRequired(false)),
category: 'utility', category: 'utility',
integration_types: [0, 1],
async execute(interaction, args, client) { async execute(interaction, args, client) {
if (args.bot) { if (args.bot) {
if (args.bot.user.bot) { if (args.bot.user.bot) {
@ -18,7 +19,9 @@ export default {
} }
} }
else { else {
return interaction.reply(`You can add me from here: https://discord.com/oauth2/authorize?client_id=${client.user.id}&permissions=2684406848&scope=bot%20applications.commands`); 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

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

@ -5,8 +5,9 @@ export default {
.setName('ping') .setName('ping')
.setDescription('Replies with Pong!'), .setDescription('Replies with Pong!'),
category: 'utility', category: 'utility',
async execute(interaction) { integration_types: [0, 1],
async execute(interaction) {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()

View file

@ -36,13 +36,13 @@ export default {
const statsEmbed = new EmbedBuilder() 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' })
.addFields( .addFields(
{ name: 'Servers', value: client.guilds.cache.size.toString(), inline: true }, { name: 'Servers', value: client.guilds.cache.size.toString(), inline: true },
{ name: 'Channels', value: client.channels.cache.size.toString(), inline: true }, { name: 'Channels', value: client.channels.cache.size.toString(), inline: true },
{ name: 'Users', value: client.users.cache.size.toString(), inline: true }, { name: 'Users', value: client.users.cache.size.toString(), inline: true },
{ name: 'Ram usage', value: `${bytesToSize(process.memoryUsage().heapUsed)}/${bytesToSize(os.totalmem)}`, inline: true }, { name: 'Ram usage', value: `${bytesToSize(process.memoryUsage().heapUsed)}/${bytesToSize(os.totalmem)}`, inline: true },
{ name: 'CPU', value: `${os.cpus()[0].model} (${os.cpus().length} core)`, inline: true }, { name: 'CPU', value: `${os.cpus()[0].model} (${os.cpus().length} core) (${os.arch()})`, inline: true },
{ name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true }, { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true },
{ name: 'Nodejs version', value: process.version, inline: true }, { name: 'Nodejs version', value: process.version, inline: true },
{ name: 'Discord.js version', value: version, inline: true }, { name: 'Discord.js version', value: version, inline: true },

View file

@ -20,7 +20,7 @@ export default {
} }
const Embed = new EmbedBuilder() const Embed = new EmbedBuilder()
.setColor(member ? member.displayHexColor : 'Navy') .setColor(member ? member.displayHexColor : 'Navy')
.setAuthor({ name: `${user.tag} (${user.id})`, iconURL: user.displayAvatarURL() }) .setAuthor({ name: `${user.username} (${user.id})`, iconURL: user.displayAvatarURL() })
.addFields( .addFields(
{ name: 'Current rank hex color', value: member ? member.displayHexColor : 'No rank color', inline: true }, { 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: 'Joined guild at', value: member ? member.joinedAt.toString() : 'Not in this guild', inline: true },

View file

@ -3,9 +3,9 @@ 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 { NODE_ENV } = process.env;
const ytdlpFormat = 'bestvideo[height<=?480]/best';
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -14,44 +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))
.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', category: 'utility',
alias: ['v2g', 'togif'],
integration_types: [0, 1],
async execute(interaction, args) { async execute(interaction, args) {
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
const maxFileSize = await utils.getMaxFileSize(interaction.guild);
const url = args.url; 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 });
@ -60,9 +117,10 @@ 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);
} }
@ -75,9 +133,10 @@ async function gifski(output, input) {
}); });
} }
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);
} }
@ -89,3 +148,19 @@ async function gifsicle(input, output) {
}); });
}); });
} }
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);
});
});
}

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

@ -7,6 +7,7 @@ const { statusChannel, NODE_ENV } = process.env;
export default { export default {
name: 'guildDelete', name: 'guildDelete',
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 } }); const isOptOut = await db.optout.findOne({ where: { userID: guildOwner.id } });

View file

@ -1,3 +1,5 @@
// TODO: Moving that to a dedicated function that works for both messages and interactions
import { PermissionFlagsBits, InteractionType } from 'discord.js'; import { PermissionFlagsBits, InteractionType } from 'discord.js';
import db from '../../models/index.js'; import db from '../../models/index.js';
import ratelimiter from '../../utils/ratelimiter.js'; import ratelimiter from '../../utils/ratelimiter.js';
@ -12,6 +14,15 @@ export default {
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 });
} }
@ -19,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;
@ -27,21 +38,26 @@ export default {
if (!command) return; if (!command) return;
const isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } }); let isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } });
if (isOptOut) { if (commandName === 'optout') {
console.log(`A user launched command \x1b[33m${commandName}\x1b[0m with slash`); isOptOut = true;
}
else {
console.log(`\x1b[33m${userTag} (${userID})\x1b[0m launched command \x1b[33m${commandName}\x1b[0m with slash`);
} }
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.default_permission) { if (command.default_permission) {
const clientMember = await interaction.guild.members.fetch(client.user.id); const clientMember = await interaction.guild.members.fetch(client.user.id);
@ -59,8 +75,18 @@ export default {
} }
*/ */
// 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 // Check the ratelimit
const doRateLimit = ratelimiter.check(interaction.user, commandName, command); const doRateLimit = await ratelimiter.check(interaction.user, commandName, command);
if (doRateLimit) { if (doRateLimit) {
return interaction.reply({ content: doRateLimit, ephemeral: true }); return interaction.reply({ content: doRateLimit, ephemeral: true });
@ -83,14 +109,20 @@ export default {
}); });
if (!isOptOut) { if (!isOptOut) {
console.log(`\x1b[33m${commandName}\x1b[0m with args ${JSON.stringify(args)}`); console.log(`[${timestamp.toISOString()}] \x1b[33m⤷\x1b[0m with args ${JSON.stringify(args)}`);
} }
await command.execute(interaction, args, client); await command.execute(interaction, args, client)
.then(async () => {
const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
});
} }
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

@ -253,6 +253,7 @@ export default {
} }
// Command handling from message // Command handling from message
// TODO: Moving that to a dedicated function that works for both messages and interactions
let hasPrefix = false; let hasPrefix = false;
prefixs.forEach(p => { prefixs.forEach(p => {
@ -283,6 +284,14 @@ export default {
const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:message.author.id } }); 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 } }); 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) { if (globalBlacklist) {
return message.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true }); return message.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true });
} }
@ -290,23 +299,29 @@ export default {
return message.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true }); return message.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true });
} }
const userTag = message.author.tag; const userTag = message.author.username;
const userID = message.author.id; const userID = message.author.id;
const isOptOut = await db.optout.findOne({ where: { userID: message.author.id } }); let isOptOut = await db.optout.findOne({ where: { userID: message.author.id } });
if (isOptOut) { if (commandName === 'optout') {
console.log(`A user launched command \x1b[33m${commandName}\x1b[0m with prefix`); isOptOut = true;
}
else {
console.log(`\x1b[33m${userTag} (${userID})\x1b[0m launched command \x1b[33m${commandName}\x1b[0m with prefix`);
} }
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 // Owner only check
if (command.ownerOnly && message.author.id !== ownerId) { if (command.ownerOnly && message.author.id !== ownerId) {
return message.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true }); 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 // Check if the bot has the needed permissions
if (command.clientPermissions) { if (command.clientPermissions) {
const clientMember = await message.guild.members.fetch(client.user.id); const clientMember = await message.guild.members.fetch(client.user.id);
@ -322,8 +337,18 @@ export default {
} }
} }
// 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 // Check the ratelimit
const doRateLimit = ratelimiter.check(message.author, commandName, command); const doRateLimit = await ratelimiter.check(message.author, commandName, command);
if (doRateLimit) { if (doRateLimit) {
return message.reply({ content: doRateLimit, ephemeral: true }); return message.reply({ content: doRateLimit, ephemeral: true });
@ -377,11 +402,17 @@ export default {
}); });
const argsLength = command.data.options.length - argsToDelete; const argsLength = command.data.options.length - argsToDelete;
const missingRequired = [];
for (let i = 0, j = 0; i < argsLength; i++, j++) { for (let i = 0, j = 0; i < argsLength; i++, j++) {
if (!messageArgs[i]) continue;
const arg = command.data.options[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; if (arg.type === ApplicationCommandOptionType.Attachment) continue;
let payloadName = arg.name; let payloadName = arg.name;
@ -391,13 +422,16 @@ export default {
payload = messageArgs.slice(i).join(' '); payload = messageArgs.slice(i).join(' ');
} }
if (messageArgs[i].startsWith('--')) { if (arg.type === ApplicationCommandOptionType.Boolean && !messageArgs[i].startsWith('--')) {
continue;
}
else if (messageArgs[i].startsWith('--')) {
payloadName = payload.substring(2); payloadName = payload.substring(2);
payload = true; payload = true;
j--; j--;
} }
if (arg.type === ApplicationCommandOptionType.Mentionable) { if (arg.type === ApplicationCommandOptionType.Mentionable || arg.type === ApplicationCommandOptionType.User) {
await message.guild.members.fetch(); 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())); payload = message.mentions.members.first() ? message.mentions.members.first() : message.guild.members.cache.find(u => u.user.username.toLowerCase().includes(payload.toLowerCase()));
} }
@ -405,15 +439,33 @@ export default {
args[payloadName] = payload; args[payloadName] = payload;
} }
if (!isOptOut) { if (!isOptOut && argsLength > 0) {
console.log(`\x1b[33m${commandName}\x1b[0m with args ${JSON.stringify(args)}`); console.log(`[${timestamp.toISOString()}] \x1b[33m⤷\x1b[0m with args ${JSON.stringify(args)}`);
} }
await command.execute(message, args, client); 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) { catch (error) {
console.error(error); console.error(error);
await message.reply({ content: 'There was an error while executing this command!', ephemeral: true }); 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

@ -128,7 +128,7 @@ export default {
.setFooter({ text: `${emote} ${reactionCount}` }) .setFooter({ text: `${emote} ${reactionCount}` })
.setTimestamp(); .setTimestamp();
if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter(reactionCount, reaction.message.guild.emojis.resolve(emote).url); if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter({ text: `${reactionCount}`, iconURL: reaction.message.guild.emojis.resolve(emote).url });
let description = null; let description = null;

View file

@ -111,7 +111,7 @@ export default {
.setFooter({ text: `${emote} ${reactionCount}` }) .setFooter({ text: `${emote} ${reactionCount}` })
.setTimestamp(); .setTimestamp();
if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter(reactionCount, reaction.message.guild.emojis.resolve(emote).url); if (reaction.message.guild.emojis.resolve(emote)) Embed.setFooter({ text: `${reactionCount}`, iconURL: reaction.message.guild.emojis.resolve(emote).url });
message.edit({ embeds: [Embed] }); message.edit({ embeds: [Embed] });
} }

View file

@ -1,5 +1,6 @@
import { exec } from 'node:child_process'; import { execFile } from 'node:child_process';
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',
@ -8,8 +9,21 @@ export default {
// Init global variables. // Init global variables.
global.boards = {}; 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);
} }
@ -20,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,9 +1,7 @@
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, GatewayIntentBits, Partials } from 'discord.js'; import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js';
import dotenv from 'dotenv';
dotenv.config();
const { token, NODE_ENV } = process.env; const { token, NODE_ENV } = process.env;
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -41,7 +39,7 @@ 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);
import(filePath) import(pathToFileURL(filePath))
.then(importedCommand => { .then(importedCommand => {
const command = importedCommand.default; const command = importedCommand.default;
client.commands.set(command.data.name, command); client.commands.set(command.data.name, command);
@ -57,7 +55,7 @@ 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);
import(filePath) import(pathToFileURL(filePath))
.then(importedEvent => { .then(importedEvent => {
const event = importedEvent.default; const event = importedEvent.default;
if (event.once) { if (event.once) {

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."]

3
json/listening.json Normal file
View file

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

4730
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,25 +17,25 @@
"homepage": "https://libtar.de", "homepage": "https://libtar.de",
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@discordjs/rest": "^0.4.1", "@discordjs/rest": "^2.3.0",
"discord-api-types": "^0.33.5", "discord-api-types": "^0.37.91",
"discord.js": "^14.9.0", "discord.js": "^14.15.3",
"dotenv": "^16.0.1", "mariadb": "^3.3.1",
"mariadb": "^3.0.1", "node-fetch": "^3.3.2",
"mysql2": "^2.3.3", "sequelize": "^6.37.3",
"node-fetch": "^3.2.6", "turndown": "^7.2.0",
"safe-regex": "github:davisjam/safe-regex", "twitter-api-v2": "^1.17.1",
"sequelize": "^6.21.3",
"turndown": "^7.1.1",
"twit": "^1.1.20",
"ytpplus-node": "github:Supositware/ytpplus-node" "ytpplus-node": "github:Supositware/ytpplus-node"
}, },
"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",
"globals": "^15.8.0",
"sequelize-cli": "^6.4.1", "sequelize-cli": "^6.4.1",
"sqlite3": "^5.1.6" "sqlite3": "^5.1.7"
} }
} }

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 and Handbrake to compress videos.) * 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](scripts/updateytdlp.js)) * 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

@ -2,9 +2,7 @@ import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9'; import { Routes } from 'discord-api-types/v9';
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 dotenv from 'dotenv';
dotenv.config();
const { clientId, guildId, token } = process.env; const { clientId, guildId, token } = process.env;
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -17,7 +15,12 @@ for (let i = 0; i < categoryPath.length; i++) {
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) { for (const file of commandFiles) {
const filePath = path.join(commandsPath, file); const filePath = path.join(commandsPath, file);
const command = await import(filePath); 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()); commands.push(command.default.data.toJSON());
} }
} }

View file

@ -23,11 +23,11 @@ async function download(url, output) {
fs.renameSync(tmpPath, path); fs.renameSync(tmpPath, path);
fs.chmodSync(path, '755'); fs.chmodSync(path, '755');
console.log(`${url} download finished.`); console.log(`${url} download finished.`);
resolve(true); return resolve(true);
}); });
filePath.on('error', (err) => { filePath.on('error', (err) => {
filePath.close(); filePath.close();
reject(err); return reject(err);
}); });
}); });
}); });

View file

@ -1,9 +1,6 @@
import dotenv from 'dotenv';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { Client, GatewayIntentBits } from 'discord.js'; import { Client, GatewayIntentBits } from 'discord.js';
dotenv.config();
const { botsggToken, botsggEndpoint, token } = process.env; const { botsggToken, botsggEndpoint, token } = process.env;
const client = new Client({ const client = new Client({

View file

@ -1,13 +1,17 @@
// 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 utils from './downloadutils.js'; 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') {
process.exit(1); 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);
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.'); 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.');
}
const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp'; const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
utils.download(downloadUrl, './bin/yt-dlp'); await utils.download(downloadUrl, './bin/yt-dlp');
});

0
tmp/.keep Normal file
View file

View file

@ -65,7 +65,9 @@ export function rand(text, interaction) {
const matches = text.matchAll(/\[.*?\]\s?/g); const matches = text.matchAll(/\[.*?\]\s?/g);
for (const match of matches) { for (const match of matches) {
if (search(match[0].trim(), variables)) { text = text.replace(match[0].trim(), search(match[0].trim(), variables).value); } if (search(match[0].trim(), variables)) {
text = text.replace(match[0].trim(), search(match[0].trim(), variables).value);
}
} }
return text; return text;

View file

@ -1,12 +1,25 @@
const ratelimit = {}; const ratelimit = {};
const parallelLimit = {};
const { ownerId, NODE_ENV } = process.env;
import db from '../models/index.js'; import db from '../models/index.js';
export default { export default {
check, check,
addParallel,
removeParallel,
checkParallel,
}; };
function check(user, commandName, commands) { async function check(user, commandName, commands) {
const userID = user.id; const userID = user.id;
const userTag = user.tag; const userTag = user.username;
// Don't apply the rate limit to bot owner
if (NODE_ENV !== 'development') {
if (user.id === ownerId) {
return false;
}
}
if (!ratelimit[userID]) { if (!ratelimit[userID]) {
ratelimit[userID] = {}; ratelimit[userID] = {};
@ -27,13 +40,11 @@ function check(user, commandName, commands) {
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const dateString = `${hours > 0 ? ` ${Math.floor(hours)} hours` : ''}${minutes > 0 ? ` ${Math.floor(minutes % 60)} minutes` : ''}${seconds > 0 ? ` ${Math.floor(seconds % 60)} seconds` : ''}`; const dateString = `${hours > 0 ? ` ${Math.floor(hours)} hours` : ''}${minutes > 0 ? ` ${Math.floor(minutes % 60)} minutes` : ''}${seconds > 0 ? ` ${Math.floor(seconds % 60)} seconds` : ''}`;
const isOptOut = db.optout.findOne({ where: { userID: userID } }); const isOptOut = await db.optout.findOne({ where: { userID: userID } });
if (isOptOut) {
console.log(`A user is rate limited on \x1b[33m${commandName}\x1b[0m for${dateString}.`); const timestamp = new Date();
} console.log(`[${timestamp.toISOString()}] \x1b[33m${ isOptOut ? 'A user' : `${userTag} (${userID})`}\x1b[0m is rate limited on \x1b[33m${commandName}\x1b[0m for${dateString}.`);
else {
console.log(`\x1b[33m${userTag} (${userID})\x1b[0m is rate limited on \x1b[33m${commandName}\x1b[0m for${dateString}.`);
}
return `You are being rate limited. You can try again in${dateString}.`; return `You are being rate limited. You can try again in${dateString}.`;
} }
} }
@ -46,3 +57,47 @@ function check(user, commandName, commands) {
return false; return false;
} }
async function addParallel(commandName) {
// console.log(`[ADD] Adding parallel to ${commandName}`);
if (!parallelLimit[commandName]) parallelLimit[commandName] = 0;
const prevNumber = parallelLimit[commandName];
// console.log(`[ADD] Previous parallel executions: ${prevNumber}`);
// console.log(`[ADD] Current parallel executions: ${JSON.stringify(parallelLimit)}`);
parallelLimit[commandName] = prevNumber + 1;
}
async function removeParallel(commandName) {
// console.log(`[REMOVE] Removing parallel to ${commandName}`);
// This shouldn't be possible
if (!parallelLimit[commandName]) parallelLimit[commandName] = 0;
const prevNumber = parallelLimit[commandName];
// console.log(`[REMOVE] previous number: ${prevNumber}`);
// console.log(`[REMOVE] previous parallel limit: ${JSON.stringify(parallelLimit)}`);
parallelLimit[commandName] = prevNumber - 1;
// console.log(`[REMOVE] current parallel limit: ${JSON.stringify(parallelLimit)}`);
}
async function checkParallel(user, commandName, command) {
// Don't apply the rate limit to bot owner
if (NODE_ENV !== 'development') {
if (user.id === ownerId) {
return false;
}
}
if (!parallelLimit[commandName]) parallelLimit[commandName] = 0;
// console.log(`[CHECK] command limit: ${command.parallelLimit}`);
// console.log(`[CHECK] current parallel executions: ${parallelLimit[commandName]}`);
if (parallelLimit[commandName] >= command.parallelLimit) {
return { limited: true, current: parallelLimit[commandName], max: command.parallelLimit, msg: `There are currently too many parallel execution of this command, please wait before retrying. (${parallelLimit[commandName]}/${command.parallelLimit})` };
}
return { limited: false, current: parallelLimit[commandName], max: command.parallelLimit };
}

View file

@ -1,6 +1,6 @@
import os from 'node:os'; import os from 'node:os';
import { exec } from 'node:child_process'; import { execFile } from 'node:child_process';
const { NODE_ENV } = process.env; const { NODE_ENV, ytdlpMaxResolution, proxy } = process.env;
export default { export default {
downloadVideo, downloadVideo,
@ -9,25 +9,35 @@ export default {
stringIsAValidurl, stringIsAValidurl,
compressVideo, compressVideo,
getVideoCodec, getVideoCodec,
getVideoSize,
getMaxFileSize,
autoCrop,
}; };
async function downloadVideo(urlArg, output, format = 'bestvideo*+bestaudio/best') { async function downloadVideo(urlArg, output, format = `bestvideo[height<=?${ytdlpMaxResolution}]+bestaudio/best`) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
exec(`./bin/yt-dlp -f ${format} "${urlArg}" -o "${os.tmpdir()}/${output}.%(ext)s" --force-overwrites --no-playlist --merge-output-format=mp4/webm/mov`, (err, stdout, stderr) => { const options = ['-f', format, urlArg, '-o', `${os.tmpdir()}/${output}.%(ext)s`, '--force-overwrites', '--playlist-reverse', '--no-playlist', '--remux-video=mp4/webm/mov', '--no-warnings'];
if (proxy) {
options.push('--proxy');
options.push(proxy);
};
execFile('./bin/yt-dlp', options, (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); return reject(stderr);
} }
if (stderr) { if (stderr) {
console.error(stderr); console.error(stderr);
// Should already be rejected at that points
return reject(stderr);
} }
console.log(NODE_ENV === 'development' ? stdout : null); console.log(NODE_ENV === 'development' ? stdout : null);
resolve(); return resolve();
}); });
}); });
} }
async function upload(file) { async function upload(file) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
exec(`./bin/upload.sh ${file}`, (err, stdout, stderr) => { execFile('./bin/upload.sh', [file], (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -41,7 +51,7 @@ async function upload(file) {
async function ffmpeg(command) { async function ffmpeg(command) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
exec(`ffmpeg ${command}`, (err, stdout, stderr) => { execFile('ffmpeg', ['-hide_banner', ...command], (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -66,7 +76,7 @@ async function stringIsAValidurl(s) {
async function compressVideo(input, output, preset) { async function compressVideo(input, output, preset) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
exec(`./bin/HandBrakeCLI -i '${input}' -Z '${preset}' -o '${os.tmpdir()}/${output}'`, (err, stdout, stderr) => { execFile('./bin/HandBrakeCLI', ['-i', input, '-Z', preset, '--turbo', '--optimize', '-o', `${os.tmpdir()}/${output}`], (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -80,7 +90,7 @@ async function compressVideo(input, output, preset) {
} }
async function getVideoCodec(input) { async function getVideoCodec(input) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
exec(`ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 ${input}`, (err, stdout, stderr) => { execFile('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=codec_name', '-of', 'default=noprint_wrappers=1:nokey=1', input], (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -91,3 +101,83 @@ async function getVideoCodec(input) {
}); });
}); });
} }
async function getVideoSize(urlArg, format = `bestvideo[height<=?${ytdlpMaxResolution}]+bestaudio/best`) {
return await new Promise((resolve, reject) => {
const options = [urlArg, '-f', format, '--no-warnings', '-O', '%(filesize,filesize_approx)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 / 1000000.0);
});
});
}
async function getMaxFileSize(guild) {
return await new Promise((resolve) => {
if (!guild) {
resolve(25);
}
const tier = guild.premiumTier;
switch (tier) {
case 0:
case 1:
resolve(25);
break;
case 2:
resolve(50);
break;
case 3:
resolve(100);
break;
default:
resolve(25);
break;
}
});
}
async function autoCrop(input, output) {
return await new Promise((resolve, reject) => {
let ffprobeInput = input;
if (process.platform === 'win32') {
// ffprobe 'movie=' options does not like windows absolute path
ffprobeInput = input.replace(/\\/g, '/').replace(/\:/g, '\\\\:');
}
execFile('ffprobe',
['-f', 'lavfi', '-i', `movie=${ffprobeInput},cropdetect`, '-show_entries',
'packet_tags=lavfi.cropdetect.w,lavfi.cropdetect.h,lavfi.cropdetect.x,lavfi.cropdetect.y',
'-read_intervals', '%+#10', '-hide_banner', '-print_format', 'json'], async (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
const packets = JSON.parse(stdout).packets;
for (let i = 0; i < packets.length; i++) {
const element = packets[i];
if (element.tags) {
const cropdetect = element.tags;
await ffmpeg(['-i', input, '-vf', `crop=${cropdetect['lavfi.cropdetect.w']}:${cropdetect['lavfi.cropdetect.h']}:${cropdetect['lavfi.cropdetect.x']}:${cropdetect['lavfi.cropdetect.y']}`, '-vcodec', 'libx264', '-acodec', 'aac', output]);
break;
}
}
console.log(NODE_ENV === 'development' ? stdout : null);
resolve();
});
});
}