1
0
Fork 0

Compare commits

...

76 Commits

Author SHA1 Message Date
Supositware 2a8356d219 option to add description 3 months ago
Supositware e4b441e5f5 Fix "thinking" message not getting deleted 4 months ago
Supositware 89f00a2fcf use ytdlpMaxResolution instead of the temporary hardcoded value 4 months ago
Supositware e6dca692ed make ratelimiter async (also actually fix message) 4 months ago
Supositware 12ba7621b6 remove console.log 4 months ago
Supositware bf9c87f3b8 Fix lazy copy paste to say prefix instead of slash 4 months ago
Supositware 2c25890f5b Fix rate limit message not working correctly with optout setting 4 months ago
Supositware ceafee8287 Undo preset change 4 months ago
Supositware 76f7e40d8f replace exec with execFIle
(Should've done this long ago)
4 months ago
Supositware 35b57c219f Hide warnings 4 months ago
Supositware d96d32f008 Show error message 4 months ago
Supositware fe014ca7d7 Limit to 30 ytp at the same time and 2 per users 4 months ago
Supositware 995634a4b2 Limit videos to 480p 4 months ago
Supositware 3464736d85 Fix anything triggering boolean options in commands 4 months ago
Supositware fd7ca30e1c Fix cleanup 4 months ago
Supositware 38b8f80c43 update .env 4 months ago
Supositware ff8c6c29b0 Fix the stupid huge mistake i made 4 months ago
Supositware 44a629c7fc Better UX and fixed format option 4 months ago
Supositware 28ff4f518e Show current discord.js version 4 months ago
Supositware e8fc57394f Return when the video is too big 4 months ago
Supositware 3b9d2dc556 Limit videos to 720p 4 months ago
Supositware b095d5ce3a use vxtwitter 5 months ago
Supositware da3e0185e1 Quality option 5 months ago
Supositware 50e49db47c show how many execution in parallel are currently running in help 5 months ago
Supositware 591652f33f update tag to username 5 months ago
Supositware 49e13885fe Added timestamp 5 months ago
Supositware 0bde6afdce Remove console.log and ignore execution limit if bot owner 5 months ago
Supositware c782708fa6 Update some more strings 5 months ago
Supositware 1cd6a6009d Adding a limit to how many time a command can be executed at the same time 5 months ago
Supositware 520ca95b29 added some ytp files 5 months ago
Supositware ff98b259e7 Updated some strings 5 months ago
Supositware fa4b5165e8 Show user id in the feedback 8 months ago
Supositware 162a91ca48 Fix admin permission check 8 months ago
Supositware 5d6746a233 Replace tag with username 8 months ago
Supositware 16842d7127 Don't accept playlist (They didn't work already before) 9 months ago
Supositware 0c38de9ea2 Non-ephemeral so I can keep a record of it 9 months ago
Supositware e235f064d8 Fix custom emotes in starboard/shameboard 10 months ago
Supositware 8546fb30f5 removed comments and console.log 12 months ago
Supositware ba42ef6f37 Plugging friend sound 12 months ago
Supositware b559edcd10 Use twitter-api-v2 instead of twit 12 months ago
Supositware 1269403787 Handle some error and send error message 1 year ago
Supositware de6e0dd3c7 fix getVideoSize 1 year ago
Supositware 4e5324155d Fix some occasional error 1 year ago
Supositware 65eb5b997f Tweet is not feedback 1 year ago
Supositware fb4db75f09 Don't apply rate limit to bot owner 1 year ago
Supositware 59bf0b9430 Update file size limit 1 year ago
Supositware 543ab35c9e up file limit to 25 1 year ago
Supositware 88ff7390cd adding more ban words 1 year ago
Supositware ccf9dc5785 add alias and ability to not loop 1 year ago
Supositware 5cc94e54a3 Added some aliases 1 year ago
Supositware d925e62004 "using slash" instead of "with slash" 1 year ago
Supositware 633f0a6fec Added more restrictions.
Only work in guilds.
Server need to be 1 month old.
Bot need to be in server for 1 week.
1 year ago
Supositware 780aef27c5 guildOnly check 1 year ago
Supositware 408176cc9d Fix stupid censor 1 year ago
Supositware cd4ffa8b53 fix getVideoSize 1 year ago
Supositware aacd7aa9fa Fix compression, add file size to author 1 year ago
Supositware f294e8cee1 getVideoSize 1 year ago
Supositware 3780fad9ae await getMaxFIleSize 1 year ago
Supositware d4e3693be6 Use the guild max file size 1 year ago
Supositware d926931e37 Exclude some more files 1 year ago
Supositware 6a9425eccc get the max file size from a guild 1 year ago
Supositware 1991925213 Download and load a command 1 year ago
Supositware 0f72e8c180 Keep some more folders 1 year ago
Supositware 64988e340f Update text to say bug report instead of feature request 1 year ago
Supositware bd4dcd087e rename owner to creator 1 year ago
Supositware c68d4fca00 Update to StringSelectMenu 1 year ago
Supositware 56d06cedc4 remux-video instead of merge-output-format 1 year ago
Supositware 1585941e8a Ported audio2image and image2audio 1 year ago
Supositware 646421df8f ffmpeg hide_banner 1 year ago
Supositware cb13a55c0c Play error video 1 year ago
Supositware 2f34b9fcc8 Tell what opting out does 1 year ago
Supositware dceb4fbbb8 replace arg command with a nice arrow 1 year ago
Supositware 39ff404deb Minor style change 1 year ago
Supositware 2cc13e8328 Update a bunch of buttons 1 year ago
Supositware fce229e73a Fix status 1 year ago
Supositware 32fb7bc005 Really did a sloppy job on that one, remember kids, copy pasting is bad! 1 year ago

@ -2,7 +2,7 @@ token=YourToken
clientId=BotClientId
guildId=DevGuildId
ownerId=OwnerUserId
statusChannel=
statusChannel=CHannelIdForStatus
uptimeURL=UptimeKumaOrWhateverStatusThingYouUseOrJustLeaveEmpty
uptimeInterval=60
twiConsumer=TwitterConsumerToken
@ -14,4 +14,6 @@ twiLogChannel=ChannelWhereTheDetailedInfoOfTheCommandIsSent
botsggToken=APITokenForBots.gg
botsggEndpoint=https://discord.bots.gg/api/v1
stableHordeApi=0000000000
stableHordeID=0000
stableHordeID=0000
NODE_ENV=development
ytdlpMaxResolution=720

@ -33,5 +33,5 @@ labels:
**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

13
.gitignore vendored

@ -1,7 +1,16 @@
.env
node_modules/
bin/
config/config.json
json/board/
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

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

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

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

@ -25,44 +25,50 @@ export default {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}`)
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}`)
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}`)
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
client.on('interactionCreate', async (interactionMenu) => {
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 });
}
});
return listenButton(client, interaction, args, interaction.user);
},
};
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}${originalId}`) {
if (!args.message) {
return interaction.reply({ content: 'You need to input a message for me to edit!', ephemeral: true });
}
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.leaveChannel.update(body, { where: { guildID: interaction.guild.id } });
return interaction.editReply({ content: `The leave message has been set to ${args.message}`, ephemeral: true });
}
else if (interactionMenu.customId === `remove${interaction.user.id}${originalId}`) {
db.leaveChannel.destroy({ where: { guildID: interaction.guild.id, channelID: interaction.channel.id } });
return interaction.editReply({ content: 'The leave message has been deleted.', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

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

@ -6,7 +6,7 @@ export default {
.setName('shameboard')
.setDescription('Set shameboard to the current channel.')
.addStringOption(option =>
option.setName('emote')
option.setName('emote')
.setDescription('The emote that should be used to enter the shameboard.'))
.addStringOption(option =>
option.setName('count')

@ -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 fs from 'node:fs';
@ -51,12 +51,12 @@ export default {
if (args.remove) {
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 } });
return interaction.editReply('successfully deleted the following tag: ' + args.trigger);
}
else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask 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 {
@ -77,56 +77,51 @@ export default {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}`)
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}`)
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}`)
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.editReply({ content: 'This tag already exist, do you want to update it, remove it or do nothing?', components: [row], ephemeral: true });
client.on('interactionCreate', async (interactionMenu) => {
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 });
}
});
return listenButton(client, interaction, args, interaction.user);
}
else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask an admin to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`);
}
},
};
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
const join = await db.joinChannel.findOne({ where: { guildID: interaction.guild.id } });
await interactionMenu.update({ components: [] });
if (!join && !args.message) {
return interaction.editReply({ content: 'You need a message for me to say anything!', ephemeral: true });
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 (!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 });
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 });
}
});
}

@ -26,44 +26,50 @@ export default {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}`)
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}`)
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}`)
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
client.on('interactionCreate', async (interactionMenu) => {
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 });
}
});
return listenButton(client, interaction, args, interaction.user);
},
};
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}${originalId}`) {
if (!args.message) {
return interaction.reply({ content: 'You need to input a message for me to edit!', ephemeral: true });
}
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.joinChannel.update(body, { where: { guildID: interaction.guild.id } });
return interaction.editReply({ content: `The join message has been set to ${args.message}`, ephemeral: true });
}
else if (interactionMenu.customId === `remove${interaction.user.id}${originalId}`) {
db.joinChannel.destroy({ where: { guildID: interaction.guild.id, channelID: interaction.channel.id } });
return interaction.editReply({ content: 'The join message has been deleted.', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

@ -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;
}
});
}
}

@ -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;
}
});
}
}

@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js';
import { EmbedBuilder } from 'discord.js';
import Twit from 'twit';
import { TwitterApi } from 'twitter-api-v2';
import fetch from 'node-fetch';
import os from 'node:os';
import fs from 'node:fs';
@ -16,10 +16,10 @@ const Blacklists = db.Blacklists;
export default {
data: new SlashCommandBuilder()
.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 =>
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))
.addAttachmentOption(option =>
option.setName('image')
@ -28,6 +28,7 @@ export default {
category: 'fun',
ratelimit: 3,
cooldown: 86400,
guildOnly: true,
async execute(interaction, args, client) {
const content = args.content;
const attachment = args.image;
@ -39,13 +40,31 @@ export default {
await interaction.deferReply({ ephemeral: false });
let tweet = content;
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 (interaction.user.createdAt > date.setMonth(date.getMonth() - 6)) {
await interaction.editReply({ content: 'Your account is too new to be able to use this command!' });
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());
// If account is less than 1 year old don't accept attachment
@ -54,13 +73,25 @@ export default {
return;
}
// remove zero width space
if (tweet) {
// remove zero width space
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)
/* No worky (I don't remember what the fuck I wrote here)
if (wordToCensor.includes(tweet) || wordToCensor.includes(tweet.substring(0, tweet.length - 1)) || wordToCensor.includes(tweet.substring(1, tweet.length))) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body);
@ -68,6 +99,7 @@ export default {
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return;
}
*/
// 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')) {
@ -81,11 +113,12 @@ export default {
}
}
const T = new Twit({
consumer_key: twiConsumer,
consumer_secret: twiConsumerSecret,
access_token: twiToken,
access_token_secret: twiTokenSecret,
const userClient = new TwitterApi({
appKey: twiConsumer,
appSecret: twiConsumerSecret,
accessToken: twiToken,
accessSecret: twiTokenSecret,
});
try {
@ -107,17 +140,8 @@ export default {
return interaction.editReply({ content: 'Gifs can\'t be larger than 15 MB!' });
}
const b64Image = fs.readFileSync(`${os.tmpdir()}/${attachment.name}`, { encoding: 'base64' });
T.post('media/upload', { media_data: b64Image }, function(err, data) {
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);
}
});
const image = await userClient.v1.uploadMedia(`${os.tmpdir()}/${attachment.name}`);
Tweet(image);
}
else {
await interaction.editReply({ content: 'File type not supported, you can only send jpg/png/gif' });
@ -134,74 +158,47 @@ export default {
return;
}
function Tweet(data) {
let options = {
status: tweet,
};
if (data && tweet) {
options = {
status: tweet,
media_ids: new Array(data.media_id_string),
};
async function Tweet(img) {
let options = null;
if (img) {
options = { media: { media_ids: new Array(img) } };
}
else if (data) {
options = {
media_ids: new Array(data.media_id_string),
};
const tweeted = await userClient.v2.tweet(tweet, options);
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 (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;
const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
const TweetLink = `https://twitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`;
// Im too lazy for now to make an entry in config.json
let channel = 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);
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}` });
});
channel = await client.channels.resolve(twiLogChannel);
channel.send({ embeds: [Embed] });
return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` });
}
},
};

@ -12,6 +12,9 @@ export default {
.setDescription('Force the generation of the video in non-nsfw channel.')
.setRequired(false)),
category: 'fun',
ratelimit: 2,
cooldown: 60,
parallelLimit: 30,
async execute(interaction, args) {
if (!interaction.channel.nsfw && !args.force) return interaction.reply(`Please execute this command in an NSFW channel ( Content might not be NSFW but since the video are user submitted better safe than sorry ) OR do \`\`${interaction.prefix}ytp --force\`\` to make the command work outside of nsfw channel BE AWARE THAT IT WON'T CHANGE THE FINAL RESULT SO NSFW CAN STILL HAPPEN`);
@ -69,19 +72,19 @@ export default {
},
};
new YTPGenerator().configurateAndGo(options)
await new YTPGenerator().configurateAndGo(options)
.then(() => {
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 => {
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 => {
console.error(err);
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`] });
});
},
};

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

@ -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.`);
},
};

@ -34,40 +34,46 @@ export default {
Blacklists.create(body);
let user = userid;
await client.users.fetch(userid);
user = client.users.resolve(userid).tag;
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 {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}`)
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}`)
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
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) => {
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.');
}
});
return listenButton(client, interaction, command, userid, interaction.user);
}
},
};
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.');
}
});
}

@ -1,6 +1,6 @@
import { SlashCommandBuilder } 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';
const donator = db.donator;
@ -15,7 +15,7 @@ export default {
const Donator = await donator.findAll({ order: ['id'] });
const client = interaction.client;
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);
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++) {
const user = await client.users.fetch(Donator[i].get('userID').toString());
if (user !== null) {
description += `**${user.tag} (${user.id}) | ${Donator[i].get('comment')}**\n`;
description += `**${user.username} (${user.id}) | ${Donator[i].get('comment')}**\n`;
}
else {
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 += `\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!';
exec('git rev-parse --short HEAD', (err, stdout) => {
execFile('git', ['rev-parse', '--short', 'HEAD'], (err, stdout) => {
const aboutEmbed = new EmbedBuilder()
.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')
.setDescription(description)
.addFields(
{ 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: 'Github (Mirror)', value: 'https://github.com/Supositware/Haha-yes', inline: true },
{ name: 'Privacy Policy', value: 'https://libtar.de/discordprivacy.txt', inline: true },
{ name: 'Status page', value: uptimePage.toString(), inline: true },
)
.setFooter({ text: `Original bot made by ${owner.tag} (267065637183029248)` });
.setFooter({ text: `Original bot made by ${creator.username} (267065637183029248)` });
interaction.reply({ embeds: [aboutEmbed] });
});

@ -18,6 +18,11 @@ export default {
cooldown: 86400,
async execute(interaction, args) {
const url = args.url;
// This is rather rudementary, a proper way would be using yt-dlp to know if it is a playlist
if (url.includes('list=')) {
return interaction.reply({ content: '❌ Playlists are not allowed!', ephemeral: true });
}
if (!await utils.stringIsAValidurl(url)) {
console.error(`Not a url!!! ${url}`);
return interaction.reply({ content: '❌ This does not look like a valid url!', ephemeral: true });
@ -25,7 +30,7 @@ export default {
await interaction.deferReply({ ephemeral: true });
utils.downloadVideo(url, interaction.id, 'mp4')
utils.downloadVideo(url, interaction.id, 'bestvideo[height<=?480]+bestaudio/best')
.then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
const output = `${os.tmpdir()}/${file}`;

@ -17,7 +17,7 @@ export default {
if (Donator[0]) {
for (let i = 0; i < Donator.length; i++) {
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`;}
}

@ -1,11 +1,15 @@
import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, SelectMenuBuilder } from 'discord.js';
import { exec } from 'node:child_process';
import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js';
import { execFile } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import utils from '../../utils/videos.js';
let client;
let cleanUp;
let maxFileSize;
let { ytdlpMaxResolution } = process.env;
// Convert to number as process.env is always a string
ytdlpMaxResolution = Number(ytdlpMaxResolution);
export default {
data: new SlashCommandBuilder()
@ -22,6 +26,10 @@ export default {
.addBooleanOption(option =>
option.setName('compress')
.setDescription('Compress the video?')
.setRequired(false))
.addBooleanOption(option =>
option.setName('description')
.setDescription('Include the video description?')
.setRequired(false)),
category: 'utility',
alias: ['dl'],
@ -30,10 +38,8 @@ export default {
client = c;
const url = args.url;
const format = args.format;
maxFileSize = await utils.getMaxFileSize(interaction.guild);
interaction.doCompress = args.compress;
if (interaction.cleanUp) {
cleanUp = interaction.cleanUp;
}
await interaction.deferReply({ ephemeral: false });
@ -48,7 +54,7 @@ export default {
if (format) {
let qualitys = await new Promise((resolve, reject) => {
exec(`./bin/yt-dlp "${url}" --print "%()j"`, (err, stdout, stderr) => {
execFile('./bin/yt-dlp', [url, '--print', '%()j'], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
@ -58,8 +64,8 @@ export default {
resolve(stdout);
});
});
qualitys = JSON.parse(qualitys);
qualitys = JSON.parse(qualitys);
const options = [];
qualitys.formats.forEach(f => {
@ -89,8 +95,8 @@ export default {
const row = new ActionRowBuilder()
.addComponents(
new SelectMenuBuilder()
.setCustomId(`downloadQuality${interaction.user.id}`)
new StringSelectMenuBuilder()
.setCustomId(`downloadQuality${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected')
.setMinValues(1)
.setMaxValues(2)
@ -103,25 +109,31 @@ export default {
client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) 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 });
download(url, interactionMenu, interaction);
await checkSize(url, interactionMenu.values[0], args, interaction);
return download(url, interactionMenu, interaction, undefined, true);
}
});
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) {
let format = 'bestvideo*+bestaudio/best';
async function download(url, interaction, originalInteraction, format = undefined, description = false) {
const Embed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setAuthor({ name: `Downloaded by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL(), url: url })
.setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.tag}" message!` });
.setAuthor({ name: `Downloaded by ${interaction.user.username}`, iconURL: interaction.user.displayAvatarURL(), url: url })
.setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.username}" message!` });
if (description) {
Embed.setDescription(await getVideoDescription(url));
}
if (interaction.customId === `downloadQuality${interaction.user.id}`) {
if (interaction.customId === `downloadQuality${interaction.user.id}${originalInteraction.id}` && !format) {
format = interaction.values[0];
if (interaction.values[1]) format += '+' + interaction.values[1];
}
@ -131,8 +143,6 @@ async function download(url, interaction, originalInteraction) {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
let output = `${os.tmpdir()}/${file}`;
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
const compressInteraction = originalInteraction ? originalInteraction : interaction;
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' ];
@ -147,8 +157,8 @@ async function download(url, interaction, originalInteraction) {
const row = new ActionRowBuilder()
.addComponents(
new SelectMenuBuilder()
.setCustomId(`preset${interaction.user.id}`)
new StringSelectMenuBuilder()
.setCustomId(`preset${interaction.user.id}${interaction.id}`)
.setPlaceholder('Nothing selected')
.addOptions(options),
);
@ -158,10 +168,13 @@ async function download(url, interaction, originalInteraction) {
client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) 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 });
compress(file, interactionMenu, Embed);
if (interaction.isMessage) cleanUp();
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
}
});
return;
@ -172,28 +185,51 @@ async function download(url, interaction, originalInteraction) {
const codec = await utils.getVideoCodec(output);
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}`);
await utils.ffmpeg(['-i', oldOutput, '-vcodec', 'libx264', '-acodec', 'aac', 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) {
await interaction.deleteReply();
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)
.catch(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 {
await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false });
}
if (interaction.isMessage) cleanUp();
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
})
.catch(async err => {
console.error(err);
@ -206,17 +242,63 @@ async function download(url, interaction, originalInteraction) {
async function compress(input, interaction, embed) {
const output = `compressed${input}.mp4`;
// 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])
.then(async () => {
const fileStat = fs.statSync(`${os.tmpdir()}/${output}`);
const fileSize = fileStat.size / 1000000.0;
if (fileSize > 8) {
await interaction.editReply({ content: 'File was bigger than 8 mb. but due to the compression it is not being uploaded externally.', ephemeral: true });
embed.setAuthor({ name: `${embed.data.author.name} (${fileSize.toFixed(2)} MB)`, iconURL: embed.data.author.icon_url, url: embed.data.author.url });
if (fileSize > maxFileSize) {
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, ephemeral: false });
}
else {
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) => {
execFile('./bin/yt-dlp', [urlArg, '--no-warnings', '-O', '%(description)s'], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout.slice(0, 240));
});
});
}

@ -18,7 +18,7 @@ export default {
category: 'utility',
async execute(interaction, args) {
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();
if (interaction.guild) Embed.addFields({ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true });

@ -1,5 +1,6 @@
import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, PermissionsBitField } from 'discord.js';
import fs from 'node:fs';
import ratelimiter from '../../utils/ratelimiter.js';
const { ownerId, prefix } = process.env;
const prefixs = prefix.split(',');
@ -113,6 +114,13 @@ export default {
embed.addFields({ name: 'Bot permission', value: `\`${perm.join('` `')}\``, inline: true });
}
if (command.parallelLimit) {
const paralellimit = ratelimiter.checkParallel(interaction.user, command.data.name, command);
embed.addFields({ name: 'Current number of executions', value: `\`${paralellimit.current}\``, inline: false });
embed.addFields({ name: 'Maximum number of executions', value: `\`${command.parallelLimit}\``, inline: true });
}
if (fs.existsSync(`./asset/img/command/${command.category}/${command.data.name}.png`)) {
const file = new AttachmentBuilder(`./asset/img/command/${command.category}/${command.data.name}.png`);
embed.setImage(`attachment://${command.data.name}.png`);

@ -12,36 +12,55 @@ export default {
if (!isOptOut) {
const body = { userID: interaction.user.id };
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),
);
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
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.');
}
});
await interaction.reply({ content: 'You are already opt out, do you wish to opt in?', components: [row], ephemeral: true });
listenButton(client, interaction, interaction.user);
}
return interaction.followUp({
content:
'As a reminder here what opting out does:\n'
+ '- Your user ID will no longer be used for debug logging.\n'
+ '- servers will no longer be shown in added/kicked stats.\n'
+ '- Your messages won\'t be quoted.\n'
+ '- Won\'t show the arguments from commands.',
ephemeral: true,
},
);
},
};
async function listenButton(client, interaction, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}${originalId}`) {
db.optout.destroy({ where: { userID: interaction.user.id } });
return interaction.editReply({ content: 'You have successfully been opt in', ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

@ -36,7 +36,7 @@ export default {
const statsEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.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(
{ name: 'Servers', value: client.guilds.cache.size.toString(), inline: true },
{ name: 'Channels', value: client.channels.cache.size.toString(), inline: true },

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

@ -3,7 +3,7 @@ import utils from '../../utils/videos.js';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { exec } from 'node:child_process';
import { execFile } from 'node:child_process';
const { NODE_ENV } = process.env;
@ -14,11 +14,28 @@ export default {
.addStringOption(option =>
option.setName('url')
.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))
.addBooleanOption(option =>
option.setName('noloop')
.setDescription('Stop the gif from looping')
.setRequired(false)),
category: 'utility',
alias: ['v2g'],
async execute(interaction, args) {
await interaction.deferReply({ ephemeral: false });
const maxFileSize = await utils.getMaxFileSize(interaction.guild);
const url = args.url;
let quality = args.quality;
if (quality <= 0) {
quality = 1;
}
else if (quality > 100) {
quality = 100;
}
if (!await utils.stringIsAValidurl(url)) {
console.error(`Not a url!!! ${url}`);
@ -33,11 +50,11 @@ export default {
const gifsicleOutput = output.replace(path.extname(output), 'gifsicle.gif');
// 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
await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`);
await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`, quality);
// Optimize it
await gifsicle(gifskiOutput, gifsicleOutput);
await gifsicle(gifskiOutput, gifsicleOutput, args.noloop);
const fileStat = fs.statSync(gifsicleOutput);
const fileSize = fileStat.size / 1000000.0;
@ -46,12 +63,12 @@ export default {
await interaction.deleteReply();
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)
.catch(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 {
await interaction.editReply({ files: [gifsicleOutput], ephemeral: false });
@ -60,9 +77,10 @@ export default {
},
};
async function gifski(output, input) {
async function gifski(output, input, quality) {
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, '-o', output, input], { shell: true }, (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
@ -75,9 +93,10 @@ async function gifski(output, input) {
});
}
async function gifsicle(input, output) {
async function gifsicle(input, output, loop = false) {
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) {
reject(stderr);
}

@ -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 db from '../../models/index.js';
import ratelimiter from '../../utils/ratelimiter.js';
@ -11,15 +13,22 @@ export default {
if (interaction.type !== InteractionType.ApplicationCommand) return;
const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:interaction.user.id } });
// const serverBlacklist = await db.Blacklists.findOne({ where: { type:'guild', uid:interaction.guild.id } });
const commandBlacklist = await db.Blacklists.findOne({ where: { type:interaction.commandName, uid:interaction.user.id } });
if (globalBlacklist) {
return interaction.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true });
}
/* Server blacklist is untested
else if (serverBlacklist) {
return interaction.reply({ content: `This guild has been blacklisted for the following reason: \`${serverBlacklist.reason}\``, ephemeral: true });
}
*/
else if (commandBlacklist) {
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 commandName = interaction.commandName;
@ -29,12 +38,8 @@ export default {
const isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } });
if (isOptOut) {
console.log(`A user launched command \x1b[33m${commandName}\x1b[0m with slash`);
}
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
@ -42,6 +47,11 @@ export default {
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
if (command.default_permission) {
const clientMember = await interaction.guild.members.fetch(client.user.id);
@ -59,8 +69,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
const doRateLimit = ratelimiter.check(interaction.user, commandName, command);
const doRateLimit = await ratelimiter.check(interaction.user, commandName, command);
if (doRateLimit) {
return interaction.reply({ content: doRateLimit, ephemeral: true });
@ -83,14 +103,18 @@ export default {
});
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) {
console.error(error);
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
await interaction.followUp({ content: `There was an error while executing this command!\n\`${error}\``, ephemeral: true });
}
},
};

@ -253,6 +253,7 @@ export default {
}
// Command handling from message
// TODO: Moving that to a dedicated function that works for both messages and interactions
let hasPrefix = false;
prefixs.forEach(p => {
@ -281,32 +282,40 @@ export default {
if (!command) return;
const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:message.author.id } });
// const serverBlacklist = await db.Blacklists.findOne({ where: { type:'guild', uid:message.guild.id } });
const commandBlacklist = await db.Blacklists.findOne({ where: { type:commandName, uid:message.author.id } });
if (globalBlacklist) {
return message.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true });
}
/* Server blacklist is untested
else if (serverBlacklist) {
return message.reply({ content: `This guild has been blacklisted for the following reason: \`${serverBlacklist.reason}\``, ephemeral: true });
}
*/
else if (commandBlacklist) {
return message.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true });
}
const userTag = message.author.tag;
const userTag = message.author.username;
const userID = message.author.id;
const isOptOut = await db.optout.findOne({ where: { userID: message.author.id } });
if (isOptOut) {
console.log(`A user launched command \x1b[33m${commandName}\x1b[0m with prefix`);
}
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
if (command.ownerOnly && message.author.id !== ownerId) {
return message.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true });
}
// Guild only check
if (command.guildOnly && !message.guild) {
return message.reply({ content: '❌ This command only work in a server!', ephemeral: true });
}
// Check if the bot has the needed permissions
if (command.clientPermissions) {
const clientMember = await message.guild.members.fetch(client.user.id);
@ -322,8 +331,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
const doRateLimit = ratelimiter.check(message.author, commandName, command);
const doRateLimit = await ratelimiter.check(message.author, commandName, command);
if (doRateLimit) {
return message.reply({ content: doRateLimit, ephemeral: true });
@ -391,13 +410,16 @@ export default {
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);
payload = true;
j--;
}
if (arg.type === ApplicationCommandOptionType.Mentionable) {
if (arg.type === ApplicationCommandOptionType.Mentionable || arg.type === ApplicationCommandOptionType.User) {
await message.guild.members.fetch();
payload = message.mentions.members.first() ? message.mentions.members.first() : message.guild.members.cache.find(u => u.user.username.toLowerCase().includes(payload.toLowerCase()));
}
@ -405,15 +427,23 @@ export default {
args[payloadName] = payload;
}
if (!isOptOut) {
console.log(`\x1b[33m${commandName}\x1b[0m with args ${JSON.stringify(args)}`);
if (!isOptOut && argsLength > 0) {
console.log(`[${timestamp.toISOString()}] \x1b[33m⤷\x1b[0m with args ${JSON.stringify(args)}`);
}
await command.execute(message, args, client);
await command.execute(message, args, client)
.then(async () => {
const hasPrallelLimit = await ratelimiter.checkParallel(message.author, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
});
}
catch (error) {
console.error(error);
await message.reply({ content: 'There was an error while executing this command!', ephemeral: true });
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}\`` });
});
}
},
};

@ -128,7 +128,7 @@ export default {
.setFooter({ text: `${emote} ${reactionCount}` })
.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;

@ -111,7 +111,7 @@ export default {
.setFooter({ text: `${emote} ${reactionCount}` })
.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] });
}

@ -1,5 +1,6 @@
import { exec } from 'node:child_process';
import { execFile } from 'node:child_process';
const { statusChannel, NODE_ENV } = process.env;
import { version } from 'discord.js';
export default {
name: 'ready',
@ -9,7 +10,7 @@ export default {
global.boards = {};
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) {
reject(stderr);
}
@ -21,7 +22,7 @@ export default {
});
const commandSize = client.commands.size;
const clientTag = client.user.tag;
const clientTag = client.user.username;
const guildSize = client.guilds.cache.size;
const channelSize = client.channels.cache.size;
const clientID = client.user.id;
@ -32,12 +33,17 @@ export default {
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 Discord.js \x1b[33m${version}\x1b[0m`);
console.log('===========[ READY ]===========');
// If stats channel settings exist, send bot stats to it
if (statusChannel && NODE_ENV !== 'development') {
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}`);
}
},
};

@ -1,4 +1,6 @@
import { ActivityType } from 'discord.js';
import game from '../../json/playing.json' assert {type: 'json'};
import music from '../../json/listening.json' assert {type: 'json'};
import watch from '../../json/watching.json' assert {type: 'json'};
export default {
@ -6,33 +8,44 @@ export default {
once: true,
async execute(client) {
// Bot status
setStatus();
await setStatus();
// Change status every 30 minutes
setInterval(async () => {
setStatus();
await setStatus();
}, 1800000);
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
if (random === 0) {
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!';
console.log(`Setting status to: ${status}`);
client.user.setActivity(status, { type: 'WATCHING' });
types = [ ActivityType.Watching ];
}
// Random "Playing" status taken from json
else if (random === 1) {
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!';
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))] });
}
},
};

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

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

21
package-lock.json generated

@ -19,7 +19,7 @@
"safe-regex": "github:davisjam/safe-regex",
"sequelize": "^6.21.3",
"turndown": "^7.1.1",
"twit": "^1.1.20",
"twitter-api-v2": "^1.15.0",
"ytpplus-node": "github:Supositware/ytpplus-node"
},
"devDependencies": {
@ -5159,11 +5159,6 @@
"set-blocking": "^2.0.0"
}
},
"node_modules/oauth": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.9.tgz",
"integrity": "sha512-kjdfbdtIFcfvHTdvh+pbR5RMOxrihCmMtDjOCAieetr4NoUOuRjl1q9gyGnvpPNDOalH8moHqur150XYGPQFeA=="
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -6822,16 +6817,10 @@
"domino": "^2.1.6"
}
},
"node_modules/twit": {
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/twit/-/twit-1.1.20.tgz",
"integrity": "sha512-5Wb4b+1mqSxOXGrTe5O4GIB6/IQnh5anAub34M+VfAdA1g+9QfUuod4C94xf9NNcaz+rNZ/3LRUSTpn8wLMnYA==",
"dependencies": {
"oauth": "0.9.9"
},
"engines": {
"node": ">=0.6.0"
}
"node_modules/twitter-api-v2": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.15.0.tgz",
"integrity": "sha512-Cqg3pIGhSwPyFBndpBrucdeNXecNFnYcXy3ixQ4brJHd/3k1CAtBVcX0e3s6jRYl/QIx5BmyGXS/SHEGtYZ3gw=="
},
"node_modules/type": {
"version": "1.2.0",

@ -27,7 +27,7 @@
"safe-regex": "github:davisjam/safe-regex",
"sequelize": "^6.21.3",
"turndown": "^7.1.1",
"twit": "^1.1.20",
"twitter-api-v2": "^1.15.0",
"ytpplus-node": "github:Supositware/ytpplus-node"
},
"devDependencies": {

@ -16,7 +16,7 @@ You need to install the following
* HandBrakeCLI (For [download](commands/utility/download.js))
* gifsicle (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
```

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

@ -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';
if (process.platform !== 'linux' && process.argv[2] !== '-f') {
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f');
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.');
}
(async () => {
if (process.platform !== 'linux' && process.argv[2] !== '-f') {
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f or execute ./bin/yt-dlp --update-to nightly');
process.exit(1);
}
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');
});

@ -65,7 +65,9 @@ export function rand(text, interaction) {
const matches = text.matchAll(/\[.*?\]\s?/g);
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;

@ -1,12 +1,25 @@
const ratelimit = {};
const parallelLimit = {};
const { ownerId, NODE_ENV } = process.env;
import db from '../models/index.js';
export default {
check,
addParallel,
removeParallel,
checkParallel,
};
function check(user, commandName, commands) {
async function check(user, commandName, commands) {
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]) {
ratelimit[userID] = {};
@ -27,13 +40,11 @@ function check(user, commandName, commands) {
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 isOptOut = db.optout.findOne({ where: { userID: userID } });
if (isOptOut) {
console.log(`A user 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}.`);
}
const isOptOut = await db.optout.findOne({ where: { userID: userID } });
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}.`);
return `You are being rate limited. You can try again in${dateString}.`;
}
}
@ -45,4 +56,48 @@ function check(user, commandName, commands) {
}
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 };
}

@ -1,6 +1,6 @@
import os from 'node:os';
import { exec } from 'node:child_process';
const { NODE_ENV } = process.env;
import { execFile } from 'node:child_process';
const { NODE_ENV, ytdlpMaxResolution } = process.env;
export default {
downloadVideo,
@ -9,25 +9,29 @@ export default {
stringIsAValidurl,
compressVideo,
getVideoCodec,
getVideoSize,
getMaxFileSize,
};
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) => {
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) => {
execFile('./bin/yt-dlp', ['-f', format, urlArg, '-o', `${os.tmpdir()}/${output}.%(ext)s`, '--force-overwrites', '--no-playlist', '--remux-video=mp4/webm/mov', '--no-warnings'], (err, stdout, stderr) => {
if (err) {
reject(stderr);
return reject(stderr);
}
if (stderr) {
console.error(stderr);
// Should already be rejected at that points
return reject(stderr);
}
console.log(NODE_ENV === 'development' ? stdout : null);
resolve();
return resolve();
});
});
}
async function upload(file) {
return await new Promise((resolve, reject) => {
exec(`./bin/upload.sh ${file}`, (err, stdout, stderr) => {
execFile('./bin/upload.sh', [file], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
@ -41,7 +45,7 @@ async function upload(file) {
async function ffmpeg(command) {
return await new Promise((resolve, reject) => {
exec(`ffmpeg ${command}`, (err, stdout, stderr) => {
execFile('ffmpeg', ['-hide_banner', ...command], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
@ -66,7 +70,7 @@ async function stringIsAValidurl(s) {
async function compressVideo(input, output, preset) {
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, '-o', `${os.tmpdir()}/${output}`], (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
@ -80,7 +84,7 @@ async function compressVideo(input, output, preset) {
}
async function getVideoCodec(input) {
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) {
reject(stderr);
}
@ -90,4 +94,39 @@ async function getVideoCodec(input) {
resolve(stdout.trim());
});
});
}
async function getVideoSize(urlArg, format = `bestvideo[height<=?${ytdlpMaxResolution}]+bestaudio/best`) {
return await new Promise((resolve, reject) => {
execFile('./bin/yt-dlp', [urlArg, '-f', format, '--no-warnings', '-O', '%(filesize,filesize_approx)s'], (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) => {
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;
}
});
}
Loading…
Cancel
Save