Compare commits

..

No commits in common. "Slash-V14" and "Slash-V14" have entirely different histories.

58 changed files with 4181 additions and 2369 deletions

View file

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

57
.eslintrc.json Normal file
View file

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

View file

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

13
.gitignore vendored
View file

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

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits, PermissionsBitField } from 'discord.js'; import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionFlagsBits } from 'discord.js';
import os from 'node:os'; import os from 'node:os';
import fs from 'node:fs'; import fs from 'node:fs';
@ -51,12 +51,12 @@ export default {
if (args.remove) { if (args.remove) {
if (tag) { if (tag) {
if (tag.get('ownerID') == interaction.user.id || interaction.member.permissionsIn(interaction.channel).has(PermissionsBitField.Flags.Administrator) || interaction.user.id == ownerId) { if (tag.get('ownerID') == interaction.user.id || interaction.member.permissionsIn(interaction.channel).has('ADMINISTRATOR') || interaction.user.id == ownerId) {
db.Tag.destroy({ where: { trigger: args.trigger, serverID: interaction.guild.id } }); db.Tag.destroy({ where: { trigger: args.trigger, serverID: interaction.guild.id } });
return interaction.editReply('successfully deleted the following tag: ' + args.trigger); return interaction.editReply('successfully deleted the following tag: ' + args.trigger);
} }
else { else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask a user with the 'Administrator' permission to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`); return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask an admin to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`);
} }
} }
else { else {
@ -77,51 +77,56 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`) .setCustomId(`edit${interaction.user.id}`)
.setLabel('Edit') .setLabel('Edit')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`) .setCustomId(`remove${interaction.user.id}`)
.setLabel('Remove') .setLabel('Remove')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`) .setCustomId(`nothing${interaction.user.id}`)
.setLabel('Do nothing') .setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary), .setStyle(ButtonStyle.Secondary),
); );
await interaction.editReply({ content: 'This tag already exist, do you want to update it, remove it or do nothing?', components: [row], ephemeral: true }); await interaction.editReply({ content: 'This tag already exist, do you want to update it, remove it or do nothing?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user); 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 });
}
});
} }
else { else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask an admin to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`); return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask an admin to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`);
} }
const join = await db.joinChannel.findOne({ where: { guildID: interaction.guild.id } });
if (!join && !args.message) {
return interaction.editReply({ content: 'You need a message for me to say anything!', ephemeral: true });
}
else if (!join) {
const body = { guildID: interaction.guild.id, channelID: interaction.channel.id, message: args.message };
await db.joinChannel.create(body);
return interaction.editReply({ content: `The join message have been set with ${args.message}`, ephemeral: true });
}
}, },
}; };
async function listenButton(client, interaction, args, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, args, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `edit${interaction.user.id}${originalId}`) {
const body = { trigger: args.trigger, response: args.response, ownerID: interaction.user.id, serverID: interaction.guild.id };
db.Tag.update(body, { where: { serverID: interaction.guild.id } });
return interaction.editReply({ content: `The tag ${args.trigger} has been set to ${args.response}`, ephemeral: true });
}
else if (interactionMenu.customId === `remove${interaction.user.id}${originalId}`) {
db.Tag.destroy({ where: { trigger: args.trigger, serverID: interaction.guild.id } });
return interaction.editReply({ content: `The tag ${args.trigger} has been deleted`, ephemeral: true });
}
else {
return interaction.editReply({ content: 'Nothing has been changed.', ephemeral: true });
}
});
}

View file

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

View file

@ -4,7 +4,7 @@ import TurndownService from 'turndown';
const turndown = new TurndownService(); const turndown = new TurndownService();
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import fourChan from '../../json/4chan.json' with {type: 'json'}; import fourChan from '../../json/4chan.json' assert {type: 'json'};
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import { TwitterApi } from 'twitter-api-v2'; import Twit from 'twit';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import os from 'node:os'; import os from 'node:os';
import fs from 'node:fs'; import fs from 'node:fs';
@ -8,7 +8,7 @@ import util from 'node:util';
import stream from 'node:stream'; import stream from 'node:stream';
import db from '../../models/index.js'; import db from '../../models/index.js';
import wordToCensor from '../../json/censor.json' with {type: 'json'}; import wordToCensor from '../../json/censor.json' assert {type: 'json'};
const { twiConsumer, twiConsumerSecret, twiToken, twiTokenSecret, twiChannel, twiLogChannel } = process.env; const { twiConsumer, twiConsumerSecret, twiToken, twiTokenSecret, twiChannel, twiLogChannel } = process.env;
const Blacklists = db.Blacklists; const Blacklists = db.Blacklists;
@ -16,10 +16,10 @@ const Blacklists = db.Blacklists;
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('tweet') .setName('tweet')
.setDescription('Send tweet from the bot twitter account. Please do not use it for advertisement and keep it english') .setDescription('Send tweet from Haha yes twitter account. Please do not use it for advertisement and keep it english')
.addStringOption(option => .addStringOption(option =>
option.setName('content') option.setName('content')
.setDescription('!THIS IS NOT FEEDBACK! The content of the tweet you want to send me.') .setDescription('The content of the tweet you want to send me.')
.setRequired(false)) .setRequired(false))
.addAttachmentOption(option => .addAttachmentOption(option =>
option.setName('image') option.setName('image')
@ -28,7 +28,6 @@ export default {
category: 'fun', category: 'fun',
ratelimit: 3, ratelimit: 3,
cooldown: 86400, cooldown: 86400,
guildOnly: true,
async execute(interaction, args, client) { async execute(interaction, args, client) {
const content = args.content; const content = args.content;
const attachment = args.image; const attachment = args.image;
@ -40,31 +39,13 @@ export default {
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
let tweet = content; let tweet = content;
const date = new Date(); const date = new Date();
// If guild is less than 1 month old don't accept the tweet
if (interaction.guild.createdAt > date.setMonth(date.getMonth() - 1)) {
await interaction.editReply({ content: 'The server need to be 1 month old to be able to use this command!' });
return;
}
// Reset the date for the next check
date.setTime(Date.now());
// If the bot has been in the guild for less than 1 week don't accept the tweet.
if (interaction.guild.createdAt > date.setDate(date.getDate() - 7)) {
await interaction.editReply({ content: 'I need to be in this server for a week to be able to use this command!' });
}
// Reset the date for the next check
date.setTime(Date.now());
// If account is less than 6 months old don't accept the tweet ( alt prevention ) // If account is less than 6 months old don't accept the tweet ( alt prevention )
if (interaction.user.createdAt > date.setMonth(date.getMonth() - 6)) { if (interaction.user.createdAt > date.setMonth(date.getMonth() - 6)) {
await interaction.editReply({ content: 'Your account is too new to be able to use this command!' }); await interaction.editReply({ content: 'Your account is too new to be able to use this command!' });
return; return;
} }
// Reset the date for the next check // Reset the current date so it checks correctly for the 1 year requirement.
date.setTime(Date.now()); date.setTime(Date.now());
// If account is less than 1 year old don't accept attachment // If account is less than 1 year old don't accept attachment
@ -73,25 +54,13 @@ export default {
return; return;
} }
// remove zero width space
if (tweet) { if (tweet) {
// remove zero width space
tweet = tweet.replace('', ''); tweet = tweet.replace('', '');
// This should only happen if someone tweets a zero width space }
if (tweet.length === 0) {
return interaction.reply({ content: 'Uh oh! You are missing any content for me to tweet!', ephemeral: true });
}
wordToCensor.forEach(async word => { if (tweet) {
if (tweet.toLowerCase().includes(word.toLowerCase())) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body);
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return;
}
});
// Detect banned word (Blacklist the user directly) // Detect banned word (Blacklist the user directly)
/* No worky (I don't remember what the fuck I wrote here)
if (wordToCensor.includes(tweet) || wordToCensor.includes(tweet.substring(0, tweet.length - 1)) || wordToCensor.includes(tweet.substring(1, tweet.length))) { if (wordToCensor.includes(tweet) || wordToCensor.includes(tweet.substring(0, tweet.length - 1)) || wordToCensor.includes(tweet.substring(1, tweet.length))) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' }; const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body); Blacklists.create(body);
@ -99,7 +68,6 @@ export default {
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' }); await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return; return;
} }
*/
// Very simple link detection // Very simple link detection
if (new RegExp('([a-zA-Z0-9]+://)?([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?').test(tweet) && !tweet.includes('twitter.com')) { if (new RegExp('([a-zA-Z0-9]+://)?([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?').test(tweet) && !tweet.includes('twitter.com')) {
@ -113,12 +81,11 @@ export default {
} }
} }
const T = new Twit({
const userClient = new TwitterApi({ consumer_key: twiConsumer,
appKey: twiConsumer, consumer_secret: twiConsumerSecret,
appSecret: twiConsumerSecret, access_token: twiToken,
accessToken: twiToken, access_token_secret: twiTokenSecret,
accessSecret: twiTokenSecret,
}); });
try { try {
@ -140,8 +107,17 @@ export default {
return interaction.editReply({ content: 'Gifs can\'t be larger than 15 MB!' }); return interaction.editReply({ content: 'Gifs can\'t be larger than 15 MB!' });
} }
const image = await userClient.v1.uploadMedia(`${os.tmpdir()}/${attachment.name}`); const b64Image = fs.readFileSync(`${os.tmpdir()}/${attachment.name}`, { encoding: 'base64' });
Tweet(image); 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);
}
});
} }
else { else {
await interaction.editReply({ content: 'File type not supported, you can only send jpg/png/gif' }); await interaction.editReply({ content: 'File type not supported, you can only send jpg/png/gif' });
@ -158,47 +134,74 @@ export default {
return; return;
} }
async function Tweet(img) { function Tweet(data) {
let options = null; let options = {
if (img) { status: tweet,
options = { media: { media_ids: new Array(img) } }; };
if (data && tweet) {
options = {
status: tweet,
media_ids: new Array(data.media_id_string),
};
} }
const tweeted = await userClient.v2.tweet(tweet, options); else if (data) {
options = {
const tweetid = tweeted.data.id; media_ids: new Array(data.media_id_string),
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}` });
} }
if (attachment) Embed.setImage(attachment.url); 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!' });
}
channel = await client.channels.resolve(twiLogChannel); const tweetid = response.id_str;
channel.send({ embeds: [Embed] }); const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` }); 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);
channel = client.channels.resolve(twiLogChannel);
channel.send({ embeds: [Embed] });
return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` });
});
} }
}, },
}; };

View file

@ -12,9 +12,6 @@ export default {
.setDescription('Force the generation of the video in non-nsfw channel.') .setDescription('Force the generation of the video in non-nsfw channel.')
.setRequired(false)), .setRequired(false)),
category: 'fun', category: 'fun',
ratelimit: 2,
cooldown: 60,
parallelLimit: 30,
async execute(interaction, args) { async execute(interaction, args) {
if (!interaction.channel.nsfw && !args.force) return interaction.reply(`Please execute this command in an NSFW channel ( Content might not be NSFW but since the video are user submitted better safe than sorry ) OR do \`\`${interaction.prefix}ytp --force\`\` to make the command work outside of nsfw channel BE AWARE THAT IT WON'T CHANGE THE FINAL RESULT SO NSFW CAN STILL HAPPEN`); if (!interaction.channel.nsfw && !args.force) return interaction.reply(`Please execute this command in an NSFW channel ( Content might not be NSFW but since the video are user submitted better safe than sorry ) OR do \`\`${interaction.prefix}ytp --force\`\` to make the command work outside of nsfw channel BE AWARE THAT IT WON'T CHANGE THE FINAL RESULT SO NSFW CAN STILL HAPPEN`);
@ -72,19 +69,19 @@ export default {
}, },
}; };
await new YTPGenerator().configurateAndGo(options) new YTPGenerator().configurateAndGo(options)
.then(() => { .then(() => {
loadingmsg.delete(); loadingmsg.delete();
return interaction.followUp({ content: 'Here is your YTP! Remember, it might contain nsfw, so be careful!', files: [`${os.tmpdir()}/${interaction.id}_YTP.mp4`] }) return interaction.reply({ content: 'Here is your YTP! Remember, it might contain nsfw, so be careful!', files: [`${os.tmpdir()}/${interaction.id}_YTP.mp4`] })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
return interaction.followUp({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] }); return interaction.reply('Whoops, look like the vid might be too big for discord, my bad, please try again');
}); });
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
loadingmsg.delete(); loadingmsg.delete();
return interaction.followUp({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] }); return interaction.reply({ files: [`./asset/ytp/error${Math.floor(Math.random() * 2) + 1}.mp4`] });
}); });
}, },
}; };

View file

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

View file

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

View file

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

View file

@ -32,58 +32,42 @@ export default {
if (!blacklist) { if (!blacklist) {
const body = { type:command, uid: userid, reason: reason }; const body = { type:command, uid: userid, reason: reason };
Blacklists.create(body); Blacklists.create(body);
if (command === 'guild') { let user = userid;
const guildid = userid; await client.users.fetch(userid);
await client.guilds.fetch(guildid); user = client.users.resolve(userid).tag;
const guild = client.guilds.resolve(guildid).name;
return interaction.editReply(`The guild ${guild} (${guildid}) has been blacklisted with the following reason \`${reason}\``);
}
else {
let user = userid;
await client.users.fetch(userid);
user = client.users.resolve(userid).username;
return interaction.editReply(`${user} (${userid}) has been blacklisted from ${command} with the following reason \`${reason}\``); return interaction.editReply(`${user} has been blacklisted from ${command} with the following reason \`${reason}\``);
}
} }
else { else {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`) .setCustomId(`yes${interaction.user.id}`)
.setLabel('Yes') .setLabel('Yes')
.setStyle(ButtonStyle.Primary), .setStyle(ButtonStyle.Primary),
) )
.addComponents( .addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`) .setCustomId(`no${interaction.user.id}`)
.setLabel('No') .setLabel('No')
.setStyle(ButtonStyle.Danger), .setStyle(ButtonStyle.Danger),
); );
await interaction.editReply({ content: 'This user is already blacklisted, do you want to unblacklist him?', ephemeral: true, components: [row] }); await interaction.editReply({ content: 'This user is already blacklisted, do you want to unblacklist him?', ephemeral: true, components: [row] });
return listenButton(client, interaction, command, userid, interaction.user); 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.');
}
});
} }
}, },
}; };
async function listenButton(client, interaction, command, userid, user = interaction.user, originalId = interaction.id) {
client.once('interactionCreate', async (interactionMenu) => {
if (user !== interactionMenu.user) return listenButton(client, interaction, command, userid, user, originalId);
if (!interactionMenu.isButton()) return;
await interactionMenu.update({ components: [] });
if (interactionMenu.customId === `yes${interaction.user.id}${originalId}`) {
Blacklists.destroy({ where: { type:command, uid:userid } });
return interaction.editReply(`The following ID have been unblacklisted from ${command}: ${userid}`);
}
else {
return interaction.editReply('No one has been unblacklisted.');
}
});
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import { EmbedBuilder } from 'discord.js'; import { EmbedBuilder } from 'discord.js';
import donations from '../../json/donations.json' with {type: 'json'}; import donations from '../../json/donations.json' assert {type: 'json'};
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()

View file

@ -17,7 +17,7 @@ export default {
if (Donator[0]) { if (Donator[0]) {
for (let i = 0; i < Donator.length; i++) { for (let i = 0; i < Donator.length; i++) {
const user = await client.users.fetch(Donator[i].get('userID').toString()); const user = await client.users.fetch(Donator[i].get('userID').toString());
if (user !== null) {donatorMessage += `**${user.username} (${user.id}) | ${Donator[i].get('comment')}**\n`;} if (user !== null) {donatorMessage += `**${user.tag} (${user.id}) | ${Donator[i].get('comment')}**\n`;}
else {donatorMessage += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`;} else {donatorMessage += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`;}
} }

View file

@ -1,16 +1,11 @@
import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js'; import { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, SelectMenuBuilder } from 'discord.js';
import { execFile } from 'node:child_process'; import { exec } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import utils from '../../utils/videos.js'; import utils from '../../utils/videos.js';
let client; let client;
let maxFileSize; let cleanUp;
let { ytdlpMaxResolution } = process.env;
const { proxy } = process.env;
// Convert to number as process.env is always a string
ytdlpMaxResolution = Number(ytdlpMaxResolution);
export default { export default {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@ -26,27 +21,19 @@ export default {
.setRequired(false)) .setRequired(false))
.addBooleanOption(option => .addBooleanOption(option =>
option.setName('compress') option.setName('compress')
.setDescription('Compress the video.') .setDescription('Compress the video?')
.setRequired(false))
.addBooleanOption(option =>
option.setName('autocrop')
.setDescription('Autocrop borders on videos. Ignored when using compress option.')
.setRequired(false))
.addBooleanOption(option =>
option.setName('description')
.setDescription('Include the video description.')
.setRequired(false)), .setRequired(false)),
category: 'utility', category: 'utility',
alias: ['dl'], alias: ['dl'],
integration_types: [0, 1],
async execute(interaction, args, c) { async execute(interaction, args, c) {
client = c; client = c;
const url = args.url; const url = args.url;
const format = args.format; const format = args.format;
maxFileSize = await utils.getMaxFileSize(interaction.guild);
interaction.doCompress = args.compress; interaction.doCompress = args.compress;
interaction.doAutocrop = args.autocrop; if (interaction.cleanUp) {
cleanUp = interaction.cleanUp;
}
await interaction.deferReply({ ephemeral: false }); await interaction.deferReply({ ephemeral: false });
@ -61,12 +48,7 @@ export default {
if (format) { if (format) {
let qualitys = await new Promise((resolve, reject) => { let qualitys = await new Promise((resolve, reject) => {
const options = [url, '--print', '%()j']; exec(`./bin/yt-dlp "${url}" --print "%()j"`, (err, stdout, stderr) => {
if (proxy) {
options.push('--proxy');
options.push(proxy);
};
execFile('./bin/yt-dlp', options, (err, stdout, stderr) => {
if (err) { if (err) {
reject(stderr); reject(stderr);
} }
@ -76,8 +58,8 @@ export default {
resolve(stdout); resolve(stdout);
}); });
}); });
qualitys = JSON.parse(qualitys); qualitys = JSON.parse(qualitys);
const options = []; const options = [];
qualitys.formats.forEach(f => { qualitys.formats.forEach(f => {
@ -107,8 +89,8 @@ export default {
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new StringSelectMenuBuilder() new SelectMenuBuilder()
.setCustomId(`downloadQuality${interaction.user.id}${interaction.id}`) .setCustomId(`downloadQuality${interaction.user.id}`)
.setPlaceholder('Nothing selected') .setPlaceholder('Nothing selected')
.setMinValues(1) .setMinValues(1)
.setMaxValues(2) .setMaxValues(2)
@ -121,37 +103,25 @@ export default {
client.on('interactionCreate', async (interactionMenu) => { client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return; if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return; if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === `downloadQuality${interaction.user.id}${interaction.id}`) { if (interactionMenu.customId === `downloadQuality${interaction.user.id}`) {
await interactionMenu.deferReply({ ephemeral: false }); await interactionMenu.deferReply({ ephemeral: false });
download(url, interactionMenu, interaction);
await checkSize(url, interactionMenu.values[0], args, interaction);
return download(url, interactionMenu, interaction, undefined, true);
} }
}); });
return; return;
} }
const newFormat = await checkSize(url, undefined, args, interaction); download(url, interaction);
return download(url, interaction, interaction, newFormat, args.description);
}, },
}; };
async function download(url, interaction, originalInteraction, format = undefined, description = false) { async function download(url, interaction, originalInteraction) {
let embedColour = 'Navy'; let format = 'bestvideo*+bestaudio/best';
if (interaction.member) {
if (interaction.member.displayHexColor) {
embedColour = interaction.member.displayHexColor;
}
}
const Embed = new EmbedBuilder() const Embed = new EmbedBuilder()
.setColor(embedColour) .setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setAuthor({ name: `Downloaded by ${interaction.user.username}`, iconURL: interaction.user.displayAvatarURL(), url: url }) .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.username}" message!` }); .setFooter({ text: `You can get the original video by clicking on the "Downloaded by ${interaction.user.tag}" message!` });
if (description) { if (interaction.customId === `downloadQuality${interaction.user.id}`) {
Embed.setDescription(await getVideoDescription(url));
}
if (interaction.customId === `downloadQuality${interaction.user.id}${originalInteraction.id}` && !format) {
format = interaction.values[0]; format = interaction.values[0];
if (interaction.values[1]) format += '+' + interaction.values[1]; if (interaction.values[1]) format += '+' + interaction.values[1];
} }
@ -161,9 +131,11 @@ async function download(url, interaction, originalInteraction, format = undefine
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id)); const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
let output = `${os.tmpdir()}/${file}`; let output = `${os.tmpdir()}/${file}`;
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
const compressInteraction = originalInteraction ? originalInteraction : interaction; const compressInteraction = originalInteraction ? originalInteraction : interaction;
if (compressInteraction.doCompress) { if (compressInteraction.doCompress) {
const presets = [ 'Social 25 MB 5 Minutes 360p60', 'Social 25 MB 2 Minutes 540p60', 'Social 25 MB 1 Minute 720p60', 'Social 25 MB 30 Seconds 1080p60' ]; const presets = [ 'Social 8 MB 3 Minutes 360p30', 'Social 50 MB 10 Minutes 480p30', 'Social 50 MB 5 Minutes 720p30', 'Social 100 MB 5 Minutes 1080p30' ];
const options = []; const options = [];
presets.forEach(p => { presets.forEach(p => {
@ -175,8 +147,8 @@ async function download(url, interaction, originalInteraction, format = undefine
const row = new ActionRowBuilder() const row = new ActionRowBuilder()
.addComponents( .addComponents(
new StringSelectMenuBuilder() new SelectMenuBuilder()
.setCustomId(`preset${interaction.user.id}${interaction.id}`) .setCustomId(`preset${interaction.user.id}`)
.setPlaceholder('Nothing selected') .setPlaceholder('Nothing selected')
.addOptions(options), .addOptions(options),
); );
@ -186,75 +158,42 @@ async function download(url, interaction, originalInteraction, format = undefine
client.on('interactionCreate', async (interactionMenu) => { client.on('interactionCreate', async (interactionMenu) => {
if (interaction.user !== interactionMenu.user) return; if (interaction.user !== interactionMenu.user) return;
if (!interactionMenu.isSelectMenu()) return; if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === `preset${interaction.user.id}${interaction.id}`) { if (interactionMenu.customId === `preset${interaction.user.id}`) {
await interactionMenu.deferReply({ ephemeral: false }); await interactionMenu.deferReply({ ephemeral: false });
compress(file, interactionMenu, Embed); compress(file, interactionMenu, Embed);
if (interaction.isMessage) { if (interaction.isMessage) cleanUp();
interaction.deleteReply();
interaction.cleanUp();
}
} }
}); });
return; return;
} }
// If the video format is not one compatible with Discord, reencode it unless autocrop is choosen in which case it gets reencoded anyway. // If the video format is not one compatible with Discord, reencode it.
if (!interaction.doAutocrop) { const bannedFormats = ['hevc'];
const bannedFormats = ['av1']; const codec = await utils.getVideoCodec(output);
const codec = await utils.getVideoCodec(output);
if (bannedFormats.includes(codec)) { if (bannedFormats.includes(codec)) {
const oldOutput = output; console.log('Reencoding video');
output = `${os.tmpdir()}/264${file}`;
await utils.ffmpeg(['-i', oldOutput, '-vcodec', 'libx264', '-acodec', 'aac', output]);
}
}
else if (interaction.doAutocrop && !compressInteraction.doCompress) {
const oldOutput = output; const oldOutput = output;
output = `${os.tmpdir()}/autocrop${file}`; output = `${os.tmpdir()}/264${file}`;
await utils.autoCrop(oldOutput, 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) { if (fileSize > 100) {
await interaction.deleteReply(); await interaction.deleteReply();
await interaction.followUp('Uh oh! The video you tried to download is too big!', { ephemeral: true }); await interaction.followUp('Uh oh! The video you tried to download is too big!', { ephemeral: true });
} }
else if (fileSize > maxFileSize) { else if (fileSize > 8) {
const fileurl = await utils.upload(output) const fileurl = await utils.upload(output)
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
await interaction.editReply({ content: 'File was bigger than 8 mb. It has been uploaded to an external site.', embeds: [Embed], ephemeral: false });
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, embeds: [Embed], ephemeral: false }); await interaction.followUp({ content: fileurl, ephemeral: false });
if (interaction.isMessage && message) {
await message.reply({ content: fileurl });
}
else {
await interaction.followUp({ content: fileurl, ephemeral: false });
}
}
else if (interaction.isMessage && message) {
await message.reply({ embeds: [Embed], files: [output] });
} }
else { else {
await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false }); await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false });
} }
if (interaction.isMessage) cleanUp();
if (interaction.isMessage) {
interaction.deleteReply();
interaction.cleanUp();
}
}) })
.catch(async err => { .catch(async err => {
console.error(err); console.error(err);
@ -267,68 +206,17 @@ async function download(url, interaction, originalInteraction, format = undefine
async function compress(input, interaction, embed) { async function compress(input, interaction, embed) {
const output = `compressed${input}.mp4`; const output = `compressed${input}.mp4`;
// Delete the file as it apparently don't overwrite? // Delete the file as it apparently don't overwrite?
if (fs.existsSync(output)) { fs.rmSync(output);
fs.rmSync(output);
}
utils.compressVideo(`${os.tmpdir()}/${input}`, output, interaction.values[0]) utils.compressVideo(`${os.tmpdir()}/${input}`, output, interaction.values[0])
.then(async () => { .then(async () => {
const fileStat = fs.statSync(`${os.tmpdir()}/${output}`); const fileStat = fs.statSync(`${os.tmpdir()}/${output}`);
const fileSize = fileStat.size / 1000000.0; const fileSize = fileStat.size / 1000000.0;
embed.setAuthor({ name: `${embed.data.author.name} (${fileSize.toFixed(2)} MB)`, iconURL: embed.data.author.icon_url, url: embed.data.author.url }); 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 });
if (fileSize > maxFileSize) {
await interaction.editReply({ content: `File was bigger than ${maxFileSize} mb. It has been uploaded to an external site.`, ephemeral: false });
} }
else { else {
await interaction.editReply({ embeds: [embed], files: [`${os.tmpdir()}/${output}`], ephemeral: false }); await interaction.editReply({ embeds: [embed], files: [`${os.tmpdir()}/${output}`], ephemeral: false });
} }
}); });
}
async function checkSize(url, format, args, interaction, tries = 0) {
const resolutions = [144, 240, 360, 480, 720, 1080, 1440, 2160];
while (tries < 4) {
format = `bestvideo[height<=?${resolutions[resolutions.indexOf(ytdlpMaxResolution) - tries]}]+bestaudio/best`;
const aproxFileSize = await utils.getVideoSize(url, format);
if (isNaN(aproxFileSize)) return format;
if (format || tries >= 4) {
if (aproxFileSize > 100 && !args.compress && tries > 4) {
return await interaction.followUp(`Uh oh! The video you tried to download is larger than 100 mb (is ${aproxFileSize} mb)! Try again with a lower resolution format.`);
}
else if (aproxFileSize > 500 && tries > 4) {
return await interaction.followUp(`Uh oh! The video you tried to download is larger than 500 mb (is ${aproxFileSize} mb)! Try again with a lower resolution format.`);
}
}
if (aproxFileSize < 100) {
return format;
}
if (tries < 4 && aproxFileSize > 100) {
tries++;
}
}
}
async function getVideoDescription(urlArg) {
return await new Promise((resolve, reject) => {
const options = [urlArg, '--no-warnings', '-O', '%(description)s'];
if (proxy) {
options.push('--proxy');
options.push(proxy);
};
execFile('./bin/yt-dlp', options, (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout.slice(0, 240));
});
});
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,13 +36,13 @@ export default {
const statsEmbed = new EmbedBuilder() const statsEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy') .setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.setTitle('Bot stats') .setTitle('Bot stats')
.setAuthor({ name: client.user.username, iconURL: client.user.displayAvatarURL(), url: 'https://libtar.de' }) .setAuthor({ name: client.user.tag, iconURL: client.user.displayAvatarURL(), url: 'https://libtar.de' })
.addFields( .addFields(
{ name: 'Servers', value: client.guilds.cache.size.toString(), inline: true }, { name: 'Servers', value: client.guilds.cache.size.toString(), inline: true },
{ name: 'Channels', value: client.channels.cache.size.toString(), inline: true }, { name: 'Channels', value: client.channels.cache.size.toString(), inline: true },
{ name: 'Users', value: client.users.cache.size.toString(), inline: true }, { name: 'Users', value: client.users.cache.size.toString(), inline: true },
{ name: 'Ram usage', value: `${bytesToSize(process.memoryUsage().heapUsed)}/${bytesToSize(os.totalmem)}`, inline: true }, { name: 'Ram usage', value: `${bytesToSize(process.memoryUsage().heapUsed)}/${bytesToSize(os.totalmem)}`, inline: true },
{ name: 'CPU', value: `${os.cpus()[0].model} (${os.cpus().length} core) (${os.arch()})`, inline: true }, { name: 'CPU', value: `${os.cpus()[0].model} (${os.cpus().length} core)`, inline: true },
{ name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true }, { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true },
{ name: 'Nodejs version', value: process.version, inline: true }, { name: 'Nodejs version', value: process.version, inline: true },
{ name: 'Discord.js version', value: version, inline: true }, { name: 'Discord.js version', value: version, inline: true },

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ const { statusChannel, NODE_ENV } = process.env;
export default { export default {
name: 'guildDelete', name: 'guildDelete',
async execute(guild, client) { async execute(guild, client) {
if (!guild.available) return;
const guildOwner = await client.users.fetch(guild.ownerId); const guildOwner = await client.users.fetch(guild.ownerId);
const isOptOut = await db.optout.findOne({ where: { userID: guildOwner.id } }); const isOptOut = await db.optout.findOne({ where: { userID: guildOwner.id } });

View file

@ -1,5 +1,3 @@
// TODO: Moving that to a dedicated function that works for both messages and interactions
import { PermissionFlagsBits, InteractionType } from 'discord.js'; import { PermissionFlagsBits, InteractionType } from 'discord.js';
import db from '../../models/index.js'; import db from '../../models/index.js';
import ratelimiter from '../../utils/ratelimiter.js'; import ratelimiter from '../../utils/ratelimiter.js';
@ -14,15 +12,6 @@ export default {
const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:interaction.user.id } }); const globalBlacklist = await db.Blacklists.findOne({ where: { type:'global', uid:interaction.user.id } });
const commandBlacklist = await db.Blacklists.findOne({ where: { type:interaction.commandName, uid:interaction.user.id } }); const commandBlacklist = await db.Blacklists.findOne({ where: { type:interaction.commandName, uid:interaction.user.id } });
if (interaction.guild) {
const serverBlacklist = await db.Blacklists.findOne({ where: { type:'guild', uid:interaction.guild.id } });
if (serverBlacklist) {
interaction.reply({ content: `This guild has been blacklisted for the following reason: \`${serverBlacklist.reason}\``, ephemeral: true });
return interaction.guild.leave();
}
}
if (globalBlacklist) { if (globalBlacklist) {
return interaction.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true }); return interaction.reply({ content: `You are globally blacklisted for the following reason: \`${globalBlacklist.reason}\``, ephemeral: true });
} }
@ -30,7 +19,7 @@ export default {
return interaction.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true }); return interaction.reply({ content: `You are blacklisted for the following reason: \`${commandBlacklist.reason}\``, ephemeral: true });
} }
const userTag = interaction.user.username; const userTag = interaction.user.tag;
const userID = interaction.user.id; const userID = interaction.user.id;
const commandName = interaction.commandName; const commandName = interaction.commandName;
@ -38,14 +27,14 @@ export default {
if (!command) return; if (!command) return;
let isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } }); const isOptOut = await db.optout.findOne({ where: { userID: interaction.user.id } });
if (commandName === 'optout') { if (isOptOut) {
isOptOut = true; 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 // Owner only check
@ -53,11 +42,6 @@ export default {
return interaction.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true }); return interaction.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true });
} }
// Guild only check
if (command.guildOnly && !interaction.guild) {
return interaction.reply({ content: '❌ This command only work in a server!', ephemeral: true });
}
// Check if the bot has the needed permissions // Check if the bot has the needed permissions
if (command.default_permission) { if (command.default_permission) {
const clientMember = await interaction.guild.members.fetch(client.user.id); const clientMember = await interaction.guild.members.fetch(client.user.id);
@ -75,18 +59,8 @@ export default {
} }
*/ */
// Check if the limit of parallel execution has been reached
if (command.parallelLimit) {
const doParallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (doParallelLimit.limited) {
return await interaction.reply({ content: doParallelLimit.msg, ephemeral: true });
}
ratelimiter.addParallel(commandName);
}
// Check the ratelimit // Check the ratelimit
const doRateLimit = await ratelimiter.check(interaction.user, commandName, command); const doRateLimit = ratelimiter.check(interaction.user, commandName, command);
if (doRateLimit) { if (doRateLimit) {
return interaction.reply({ content: doRateLimit, ephemeral: true }); return interaction.reply({ content: doRateLimit, ephemeral: true });
@ -109,20 +83,14 @@ export default {
}); });
if (!isOptOut) { if (!isOptOut) {
console.log(`[${timestamp.toISOString()}] \x1b[33m⤷\x1b[0m with args ${JSON.stringify(args)}`); console.log(`\x1b[33m${commandName}\x1b[0m with args ${JSON.stringify(args)}`);
} }
await command.execute(interaction, args, client) await command.execute(interaction, args, client);
.then(async () => {
const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
});
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command); await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
await interaction.followUp({ content: `There was an error while executing this command!\n\`${error}\``, ephemeral: true });
} }
}, },
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url';
import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'; import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js';
import dotenv from 'dotenv';
dotenv.config();
const { token, NODE_ENV } = process.env; const { token, NODE_ENV } = process.env;
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -39,7 +41,7 @@ async function loadCommandFromDir(dir) {
for (const file of commandFiles) { for (const file of commandFiles) {
const filePath = path.join(commandsPath, file); const filePath = path.join(commandsPath, file);
import(pathToFileURL(filePath)) import(filePath)
.then(importedCommand => { .then(importedCommand => {
const command = importedCommand.default; const command = importedCommand.default;
client.commands.set(command.data.name, command); client.commands.set(command.data.name, command);
@ -55,7 +57,7 @@ async function loadEventFromDir(dir, listener) {
for (const file of eventFiles) { for (const file of eventFiles) {
const filePath = path.join(eventsPath, file); const filePath = path.join(eventsPath, file);
import(pathToFileURL(filePath)) import(filePath)
.then(importedEvent => { .then(importedEvent => {
const event = importedEvent.default; const event = importedEvent.default;
if (event.once) { if (event.once) {

View file

@ -1 +1 @@
["1488","14/88","14 88","niggar", "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."] ["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."]

View file

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

4742
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,13 @@
// This is kind of useless since you can just do `./yt-dlp --update-to nightly` which I didn't know about when I wrote that.
import utils from './downloadutils.js'; import utils from './downloadutils.js';
(async () => { if (process.platform !== 'linux' && process.argv[2] !== '-f') {
if (process.platform !== 'linux' && process.argv[2] !== '-f') { console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f');
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f or execute ./bin/yt-dlp --update-to nightly'); process.exit(1);
process.exit(1); }
} else if (process.platform !== 'linux' && process.argv[2] === '-f') {
else if (process.platform !== 'linux' && process.argv[2] === '-f') { console.log('Executed with -f. Reminder that this script only download the linux version of yt-dlp.');
console.log('Executed with -f. Reminder that this script only download the linux version of yt-dlp.'); }
}
const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp'; const downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
await utils.download(downloadUrl, './bin/yt-dlp'); utils.download(downloadUrl, './bin/yt-dlp');
});

View file

View file

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

View file

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

View file

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