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
guildId=DevGuildId
ownerId=OwnerUserId
statusChannel=CHannelIdForStatus
statusChannel=
uptimeURL=UptimeKumaOrWhateverStatusThingYouUseOrJustLeaveEmpty
uptimeInterval=60
twiConsumer=TwitterConsumerToken
@ -15,6 +15,3 @@ botsggToken=APITokenForBots.gg
botsggEndpoint=https://discord.bots.gg/api/v1
stableHordeApi=0000000000
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?**
- [ ] 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

11
.gitignore vendored
View file

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

View file

View file

@ -27,7 +27,6 @@ export default {
.setDescription('What do you want the AI to generate?')
.setRequired(true)),
category: 'AI',
alias: ['i2i'],
async execute(interaction, args, client) {
await interaction.deferReply();
@ -78,12 +77,6 @@ async function generate(i, prompt, client, b64Img) {
let response = await fetch('https://stablehorde.net/api/v2/generate/async', fetchParameters);
response = await response.json();
if (!response.id) {
console.log(response);
return i.editReply({ content: `An error has occured, please try again later. \`${response.message}\`` });
}
let wait_time = 5000;
let checkURL = `https://stablehorde.net/api/v2/generate/check/${response.id}`;
const checking = setInterval(async () => {
@ -91,13 +84,8 @@ async function generate(i, prompt, client, b64Img) {
if (checkResult === undefined) return;
if (!checkResult.done) {
if (checkResult.wait_time === -1) {
console.log(checkResult.raw);
return i.editReply({ content: `An error has occured, please try again later. \`${checkResult.raw.message}\`` });
}
if (checkResult.wait_time < 0) {
console.log(checkResult.raw);
console.log(checkResult);
clearInterval(checking);
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()
.addComponents(
new ButtonBuilder()
.setCustomId(`regenerate${i.user.id}${i.id}`)
.setCustomId(`regenerate${i.user.id}`)
.setLabel('🔄 Regenerate')
.setStyle(ButtonStyle.Primary),
);
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);
}
@ -145,27 +140,14 @@ async function checkGeneration(url) {
check = await check.json();
if (!check.is_possible) {
return { done: false, wait_time: -1, raw: check };
return { done: false, wait_time: -1 };
}
if (check.done) {
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?')
.setRequired(true)),
category: 'AI',
alias: ['t2i'],
async execute(interaction, args, client) {
await interaction.deferReply();
generate(interaction, args.prompt, client);
@ -102,14 +101,21 @@ async function generate(i, prompt, client) {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`regenerate${i.user.id}${i.id}`)
.setCustomId(`regenerate${i.user.id}`)
.setLabel('🔄 Regenerate')
.setStyle(ButtonStyle.Primary),
);
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);
}
@ -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 };
}
}
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()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setCustomId(`yes${interaction.user.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setCustomId(`no${interaction.user.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
@ -39,24 +39,18 @@ export default {
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()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setCustomId(`edit${interaction.user.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setCustomId(`remove${interaction.user.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setCustomId(`nothing${interaction.user.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user);
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()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setCustomId(`yes${interaction.user.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setCustomId(`no${interaction.user.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
@ -40,23 +40,18 @@ export default {
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

@ -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 fs from 'node:fs';
@ -51,12 +51,12 @@ export default {
if (args.remove) {
if (tag) {
if (tag.get('ownerID') == interaction.user.id || interaction.member.permissionsIn(interaction.channel).has(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 } });
return interaction.editReply('successfully deleted the following tag: ' + args.trigger);
}
else {
return interaction.editReply(`You are not the owner of this tag, if you think it is problematic ask a user with the 'Administrator' permission to remove it by doing ${this.client.commandHandler.prefix[0]}tag ${args.trigger} --remove`);
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 {
@ -77,51 +77,56 @@ export default {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setCustomId(`edit${interaction.user.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setCustomId(`remove${interaction.user.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setCustomId(`nothing${interaction.user.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.editReply({ content: 'This tag already exist, do you want to update it, remove it or do nothing?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user);
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 {
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()
.addComponents(
new ButtonBuilder()
.setCustomId(`edit${interaction.user.id}${interaction.id}`)
.setCustomId(`edit${interaction.user.id}`)
.setLabel('Edit')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`remove${interaction.user.id}${interaction.id}`)
.setCustomId(`remove${interaction.user.id}`)
.setLabel('Remove')
.setStyle(ButtonStyle.Danger),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`nothing${interaction.user.id}${interaction.id}`)
.setCustomId(`nothing${interaction.user.id}`)
.setLabel('Do nothing')
.setStyle(ButtonStyle.Secondary),
);
await interaction.reply({ content: 'The server already has a message set, do you want to edit it or remove it?', components: [row], ephemeral: true });
return listenButton(client, interaction, args, interaction.user);
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();
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 {
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 { EmbedBuilder } from 'discord.js';
import { TwitterApi } from 'twitter-api-v2';
import Twit from 'twit';
import fetch from 'node-fetch';
import os from 'node:os';
import fs from 'node:fs';
@ -8,7 +8,7 @@ import util from 'node:util';
import stream from 'node:stream';
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 Blacklists = db.Blacklists;
@ -16,10 +16,10 @@ const Blacklists = db.Blacklists;
export default {
data: new SlashCommandBuilder()
.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 =>
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))
.addAttachmentOption(option =>
option.setName('image')
@ -28,7 +28,6 @@ export default {
category: 'fun',
ratelimit: 3,
cooldown: 86400,
guildOnly: true,
async execute(interaction, args, client) {
const content = args.content;
const attachment = args.image;
@ -40,31 +39,13 @@ export default {
await interaction.deferReply({ ephemeral: false });
let tweet = content;
const date = new Date();
// If guild is less than 1 month old don't accept the tweet
if (interaction.guild.createdAt > date.setMonth(date.getMonth() - 1)) {
await interaction.editReply({ content: 'The server need to be 1 month old to be able to use this command!' });
return;
}
// Reset the date for the next check
date.setTime(Date.now());
// If the bot has been in the guild for less than 1 week don't accept the tweet.
if (interaction.guild.createdAt > date.setDate(date.getDate() - 7)) {
await interaction.editReply({ content: 'I need to be in this server for a week to be able to use this command!' });
}
// Reset the date for the next check
date.setTime(Date.now());
// If account is less than 6 months old don't accept the tweet ( alt prevention )
if (interaction.user.createdAt > date.setMonth(date.getMonth() - 6)) {
await interaction.editReply({ content: 'Your account is too new to be able to use this command!' });
return;
}
// Reset the date for the next check
// Reset the current date so it checks correctly for the 1 year requirement.
date.setTime(Date.now());
// If account is less than 1 year old don't accept attachment
@ -73,25 +54,13 @@ export default {
return;
}
// remove zero width space
if (tweet) {
// remove zero width space
tweet = tweet.replace('', '');
// This should only happen if someone tweets a zero width space
if (tweet.length === 0) {
return interaction.reply({ content: 'Uh oh! You are missing any content for me to tweet!', ephemeral: true });
}
}
wordToCensor.forEach(async word => {
if (tweet.toLowerCase().includes(word.toLowerCase())) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body);
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return;
}
});
if (tweet) {
// Detect banned word (Blacklist the user directly)
/* No worky (I don't remember what the fuck I wrote here)
if (wordToCensor.includes(tweet) || wordToCensor.includes(tweet.substring(0, tweet.length - 1)) || wordToCensor.includes(tweet.substring(1, tweet.length))) {
const body = { type:'tweet', uid: interaction.user.id, reason: 'Automatic ban from banned word.' };
Blacklists.create(body);
@ -99,7 +68,6 @@ export default {
await interaction.editReply({ content: 'Sike, you just posted cringe! Enjoy the blacklist :)' });
return;
}
*/
// Very simple link detection
if (new RegExp('([a-zA-Z0-9]+://)?([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?').test(tweet) && !tweet.includes('twitter.com')) {
@ -113,12 +81,11 @@ export default {
}
}
const userClient = new TwitterApi({
appKey: twiConsumer,
appSecret: twiConsumerSecret,
accessToken: twiToken,
accessSecret: twiTokenSecret,
const T = new Twit({
consumer_key: twiConsumer,
consumer_secret: twiConsumerSecret,
access_token: twiToken,
access_token_secret: twiTokenSecret,
});
try {
@ -140,8 +107,17 @@ export default {
return interaction.editReply({ content: 'Gifs can\'t be larger than 15 MB!' });
}
const image = await userClient.v1.uploadMedia(`${os.tmpdir()}/${attachment.name}`);
Tweet(image);
const b64Image = fs.readFileSync(`${os.tmpdir()}/${attachment.name}`, { encoding: 'base64' });
T.post('media/upload', { media_data: b64Image }, function(err, data) {
if (err) {
console.log('OH NO AN ERROR!!!!!!!');
console.error(err);
return interaction.editReply({ content: 'OH NO!!! AN ERROR HAS occurred!!! please hold on while i find what\'s causing this issue! ' });
}
else {
Tweet(data);
}
});
}
else {
await interaction.editReply({ content: 'File type not supported, you can only send jpg/png/gif' });
@ -158,47 +134,74 @@ export default {
return;
}
async function Tweet(img) {
let options = null;
if (img) {
options = { media: { media_ids: new Array(img) } };
function Tweet(data) {
let options = {
status: tweet,
};
if (data && tweet) {
options = {
status: tweet,
media_ids: new Array(data.media_id_string),
};
}
const tweeted = await userClient.v2.tweet(tweet, options);
const tweetid = tweeted.data.id;
const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
const TweetLink = `https://vxtwitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`;
let channel = await client.channels.resolve(twiChannel);
channel.send(TweetLink);
const Embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setDescription(tweet ? tweet : 'No content.')
.addFields(
{ name: 'Link', value: TweetLink, inline: true },
{ name: 'Tweet ID', value: tweetid, inline: true },
{ name: 'Channel ID', value: interaction.channel.id, inline: true },
{ name: 'Message ID', value: interaction.id, inline: true },
{ name: 'Author', value: `${interaction.user.username} (${interaction.user.id})`, inline: true },
)
.setTimestamp();
if (interaction.guild) {
Embed.addFields(
{ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true },
{ name: 'message link', value: `https://discord.com/channels/${interaction.guild.id}/${interaction.channel.id}/${interaction.id}`, inline: true },
);
}
else {
Embed.addFields({ name: 'message link', value: `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}` });
else if (data) {
options = {
media_ids: new Array(data.media_id_string),
};
}
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);
channel.send({ embeds: [Embed] });
return interaction.editReply({ content: `Go see ur epic tweet ${TweetLink}` });
const tweetid = response.id_str;
const FunnyWords = ['oppaGangnamStyle', '69', '420', 'cum', 'funnyMan', 'GUCCISmartToilet', 'TwitterForClowns', 'fart', 'ok', 'hi', 'howAreYou', 'WhatsNinePlusTen', '21'];
const TweetLink = `https://twitter.com/${FunnyWords[Math.floor((Math.random() * FunnyWords.length))]}/status/${tweetid}`;
// Im too lazy for now to make an entry in config.json
let channel = client.channels.resolve(twiChannel);
channel.send(TweetLink);
const Embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.username, iconURL: interaction.user.displayAvatarURL() })
.setDescription(tweet ? tweet : 'No content.')
.addFields(
{ name: 'Link', value: TweetLink, inline: true },
{ name: 'Tweet ID', value: tweetid, inline: true },
{ name: 'Channel ID', value: interaction.channel.id, inline: true },
{ name: 'Message ID', value: interaction.id, inline: true },
{ name: 'Author', value: `${interaction.user.username} (${interaction.user.id})`, inline: true },
)
.setTimestamp();
if (interaction.guild) {
Embed.addFields(
{ name: 'Guild', value: `${interaction.guild.name} (${interaction.guild.id})`, inline: true },
{ name: 'message link', value: `https://discord.com/channels/${interaction.guild.id}/${interaction.channel.id}/${interaction.id}`, inline: true },
);
}
else {
Embed.addFields({ name: 'message link', value: `https://discord.com/channels/@me/${interaction.channel.id}/${interaction.id}` });
}
if (attachment) Embed.setImage(attachment.url);
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.')
.setRequired(false)),
category: 'fun',
ratelimit: 2,
cooldown: 60,
parallelLimit: 30,
async execute(interaction, args) {
if (!interaction.channel.nsfw && !args.force) return interaction.reply(`Please execute this command in an NSFW channel ( Content might not be NSFW but since the video are user submitted better safe than sorry ) OR do \`\`${interaction.prefix}ytp --force\`\` to make the command work outside of nsfw channel BE AWARE THAT IT WON'T CHANGE THE FINAL RESULT SO NSFW CAN STILL HAPPEN`);
@ -72,19 +69,19 @@ export default {
},
};
await new YTPGenerator().configurateAndGo(options)
new YTPGenerator().configurateAndGo(options)
.then(() => {
loadingmsg.delete();
return interaction.followUp({ content: 'Here is your YTP! Remember, it might contain nsfw, so be careful!', files: [`${os.tmpdir()}/${interaction.id}_YTP.mp4`] })
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 => {
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 => {
console.error(err);
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 Twit from 'twit';

View file

@ -43,7 +43,7 @@ export default {
.setTimestamp();
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();
if (Attachment[0]) {
@ -58,10 +58,10 @@ export default {
else {
client.users.resolve(user).send(Embed)
.then(() => {
return interaction.reply(`DM sent to ${user.username}`);
return interaction.reply(`DM sent to ${user.tag}`);
})
.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) {
const body = { type:command, uid: userid, reason: reason };
Blacklists.create(body);
if (command === 'guild') {
const guildid = userid;
await client.guilds.fetch(guildid);
const guild = client.guilds.resolve(guildid).name;
return interaction.editReply(`The guild ${guild} (${guildid}) has been blacklisted with the following reason \`${reason}\``);
}
else {
let user = userid;
await client.users.fetch(userid);
user = client.users.resolve(userid).username;
let user = userid;
await client.users.fetch(userid);
user = client.users.resolve(userid).tag;
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 {
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(`yes${interaction.user.id}${interaction.id}`)
.setCustomId(`yes${interaction.user.id}`)
.setLabel('Yes')
.setStyle(ButtonStyle.Primary),
)
.addComponents(
new ButtonBuilder()
.setCustomId(`no${interaction.user.id}${interaction.id}`)
.setCustomId(`no${interaction.user.id}`)
.setLabel('No')
.setStyle(ButtonStyle.Danger),
);
await interaction.editReply({ content: 'This user is already blacklisted, do you want to unblacklist him?', ephemeral: true, components: [row] });
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)},
category: '${client.commands.get(args.commandname).category}',
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 { EmbedBuilder } from 'discord.js';
import { execFile } from 'node:child_process';
import { exec } from 'node:child_process';
import db from '../../models/index.js';
const donator = db.donator;
@ -15,7 +15,7 @@ export default {
const Donator = await donator.findAll({ order: ['id'] });
const client = interaction.client;
const tina = await client.users.fetch('336492042299637771');
const creator = await client.users.fetch('267065637183029248');
const owner = await client.users.fetch('267065637183029248');
const maintainer = await client.users.fetch(ownerId);
let description = 'I\'m a fun multipurpose bot made using [discord.js](https://github.com/discordjs/discord.js)'
@ -25,7 +25,7 @@ export default {
for (let i = 0; i < Donator.length; i++) {
const user = await client.users.fetch(Donator[i].get('userID').toString());
if (user !== null) {
description += `**${user.username} (${user.id}) | ${Donator[i].get('comment')}**\n`;
description += `**${user.tag} (${user.id}) | ${Donator[i].get('comment')}**\n`;
}
else {
description += `**A user of discord (${user.id}) | ${Donator[i].get('comment')} (This user no longer share a server with the bot)**\n`;
@ -36,26 +36,26 @@ export default {
description += 'No one :(\n';
}
description += `\nThanks to ${tina.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!';
execFile('git', ['rev-parse', '--short', 'HEAD'], (err, stdout) => {
exec('git rev-parse --short HEAD', (err, stdout) => {
const aboutEmbed = new EmbedBuilder()
.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')
.setDescription(description)
.addFields(
{ 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: 'Github (Mirror)', value: 'https://github.com/Supositware/Haha-yes', inline: true },
{ name: 'Privacy Policy', value: 'https://libtar.de/discordprivacy.txt', inline: true },
{ name: 'Status page', value: uptimePage.toString(), inline: true },
)
.setFooter({ text: `Original bot made by ${creator.username} (267065637183029248)` });
.setFooter({ text: `Original bot made by ${owner.tag} (267065637183029248)` });
interaction.reply({ embeds: [aboutEmbed] });
});

View file

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

View file

@ -1,6 +1,6 @@
import { SlashCommandBuilder } 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 {
data: new SlashCommandBuilder()

View file

@ -17,7 +17,7 @@ export default {
if (Donator[0]) {
for (let i = 0; i < Donator.length; i++) {
const user = await client.users.fetch(Donator[i].get('userID').toString());
if (user !== null) {donatorMessage += `**${user.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`;}
}

View file

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

View file

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

View file

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

View file

@ -36,13 +36,13 @@ export default {
const statsEmbed = new EmbedBuilder()
.setColor(interaction.member ? interaction.member.displayHexColor : 'Navy')
.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(
{ name: 'Servers', value: client.guilds.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: '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: 'Nodejs version', value: process.version, inline: true },
{ name: 'Discord.js version', value: version, inline: true },

View file

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

View file

@ -3,9 +3,9 @@ import utils from '../../utils/videos.js';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { exec } from 'node:child_process';
const { NODE_ENV } = process.env;
const ytdlpFormat = 'bestvideo[height<=?480]/best';
export default {
data: new SlashCommandBuilder()
@ -14,101 +14,44 @@ export default {
.addStringOption(option =>
option.setName('url')
.setDescription('URL of the video you want to convert')
.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)),
.setRequired(true)),
category: 'utility',
alias: ['v2g', 'togif'],
integration_types: [0, 1],
async execute(interaction, args) {
await interaction.deferReply({ ephemeral: false });
const maxFileSize = await utils.getMaxFileSize(interaction.guild);
const url = args.url;
let quality = args.quality;
if (quality) {
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)) {
console.error(`Not a url!!! ${url}`);
return interaction.editReply({ content: '❌ This does not look like a valid url!', ephemeral: true });
}
const aproxFileSize = await utils.getVideoSize(url, ytdlpFormat);
console.log(aproxFileSize);
if (aproxFileSize > 4) {
return interaction.editReply('The file you are trying to convert is too big! Limit is 4 MB');
};
utils.downloadVideo(url, interaction.id, ytdlpFormat)
utils.downloadVideo(url, interaction.id)
.then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
let output = `${os.tmpdir()}/${file}`;
if (args.autocrop) {
const oldOutput = output;
output = `${os.tmpdir()}/autocrop${file}`;
await utils.autoCrop(oldOutput, output);
}
// Get the fps of the original video
if (!args.fps) {
args.fps = getVideoFramerate(output);
}
const output = `${os.tmpdir()}/${file}`;
const gifskiOutput = output.replace(path.extname(output), '.gif');
const gifsicleOutput = output.replace(path.extname(output), 'gifsicle.gif');
// Extract every frame for gifski
await utils.ffmpeg(['-i', output, `${os.tmpdir()}/frame${interaction.id}%04d.png`]);
await utils.ffmpeg(`-i ${output} ${os.tmpdir()}/frame${interaction.id}%04d.png`);
// Make it look better
await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`, quality, await args.fps);
await gifski(gifskiOutput, `${os.tmpdir()}/frame${interaction.id}*`);
// Optimize it
await gifsicle(gifskiOutput, gifsicleOutput, args.noloop);
await gifsicle(gifskiOutput, gifsicleOutput);
const fileStat = fs.statSync(gifsicleOutput);
const fileSize = fileStat.size / 1000000.0;
if (fileSize > 25) {
if (fileSize > 100) {
await interaction.deleteReply();
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)
.catch(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 {
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) => {
// Shell: true should be fine as no user input is being passed
execFile('gifski', ['--quality', quality ? quality : 70, '--fps', fps ? fps : 20, '-o', output, input], { shell: true }, (err, stdout, stderr) => {
exec(`gifski --quality 70 -o ${output} ${input}`, (err, stdout, stderr) => {
if (err) {
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) => {
// Shell: true should be fine as no user input is being passed
execFile('gifsicle', ['--colors', '256', loop ? '--no-loopcount' : '', '-i', input, '-o', output], { shell: true }, (err, stdout, stderr) => {
exec(`gifsicle --colors 256 -i ${input} -o ${output}`, (err, stdout, stderr) => {
if (err) {
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 {
name: 'guildDelete',
async execute(guild, client) {
if (!guild.available) return;
const guildOwner = await client.users.fetch(guild.ownerId);
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 db from '../../models/index.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 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) {
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 });
}
const userTag = interaction.user.username;
const userTag = interaction.user.tag;
const userID = interaction.user.id;
const commandName = interaction.commandName;
@ -38,14 +27,14 @@ export default {
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') {
isOptOut = true;
if (isOptOut) {
console.log(`A user launched command \x1b[33m${commandName}\x1b[0m with slash`);
}
else {
console.log(`\x1b[33m${userTag} (${userID})\x1b[0m launched command \x1b[33m${commandName}\x1b[0m with slash`);
}
const timestamp = new Date();
console.log(`[${timestamp.toISOString()}] \x1b[33m${ isOptOut ? 'A user' : `${userTag} (${userID})`}\x1b[0m launched command \x1b[33m${commandName}\x1b[0m using slash`);
// Owner only check
@ -53,11 +42,6 @@ export default {
return interaction.reply({ content: '❌ This command is reserved for the owner!', ephemeral: true });
}
// Guild only check
if (command.guildOnly && !interaction.guild) {
return interaction.reply({ content: '❌ This command only work in a server!', ephemeral: true });
}
// Check if the bot has the needed permissions
if (command.default_permission) {
const clientMember = await interaction.guild.members.fetch(client.user.id);
@ -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
const doRateLimit = await ratelimiter.check(interaction.user, commandName, command);
const doRateLimit = ratelimiter.check(interaction.user, commandName, command);
if (doRateLimit) {
return interaction.reply({ content: doRateLimit, ephemeral: true });
@ -109,20 +83,14 @@ export default {
});
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)
.then(async () => {
const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
});
await command.execute(interaction, args, client);
}
catch (error) {
console.error(error);
const hasPrallelLimit = await ratelimiter.checkParallel(interaction.user, commandName, command);
if (hasPrallelLimit) ratelimiter.removeParallel(commandName);
await interaction.followUp({ content: `There was an error while executing this command!\n\`${error}\``, ephemeral: true });
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
}
},
};

View file

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

View file

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

View file

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

View file

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

View file

@ -1,51 +1,38 @@
import { ActivityType } from 'discord.js';
import game from '../../json/playing.json' with {type: 'json'};
import music from '../../json/listening.json' with {type: 'json'};
import watch from '../../json/watching.json' with {type: 'json'};
import game from '../../json/playing.json' assert {type: 'json'};
import watch from '../../json/watching.json' assert {type: 'json'};
export default {
name: 'ready',
once: true,
async execute(client) {
// Bot status
await setStatus();
setStatus();
// Change status every 30 minutes
setInterval(async () => {
await setStatus();
setStatus();
}, 1800000);
async function setStatus() {
const random = Math.floor((Math.random() * 3));
let types, status;
const random = Math.floor((Math.random() * 2));
// Random "Watching" status taken from json
if (random === 0) {
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!';
console.log(`Setting status to: ${status}`);
types = [ ActivityType.Watching ];
client.user.setActivity(status, { type: 'WATCHING' });
}
// Random "Playing" status taken from json
else if (random === 1) {
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!';
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 path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js';
import dotenv from 'dotenv';
dotenv.config();
const { token, NODE_ENV } = process.env;
const __filename = fileURLToPath(import.meta.url);
@ -39,7 +41,7 @@ async function loadCommandFromDir(dir) {
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
import(pathToFileURL(filePath))
import(filePath)
.then(importedCommand => {
const command = importedCommand.default;
client.commands.set(command.data.name, command);
@ -55,7 +57,7 @@ async function loadEventFromDir(dir, listener) {
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
import(pathToFileURL(filePath))
import(filePath)
.then(importedEvent => {
const event = importedEvent.default;
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": "",
"main": "index.js",
"scripts": {
"start": "node --env-file .env .",
"deploy": "node --env-file .env scripts/deploy-commands.js",
"deployGlobally": "node --env-file .env scripts/deploy-commands.js global",
"start": "node .",
"deploy": "node deploy-commands.cjs",
"deployGlobally": "node deploy-commands.cjs global",
"lint": "eslint .",
"lintfix": "eslint . --fix",
"test": "echo \"Error: no test specified\" && exit 1"
@ -17,25 +17,25 @@
"homepage": "https://libtar.de",
"license": "AGPL",
"dependencies": {
"@discordjs/rest": "^2.3.0",
"discord-api-types": "^0.37.91",
"discord.js": "^14.15.3",
"mariadb": "^3.3.1",
"node-fetch": "^3.3.2",
"sequelize": "^6.37.3",
"turndown": "^7.2.0",
"twitter-api-v2": "^1.17.1",
"@discordjs/rest": "^0.4.1",
"discord-api-types": "^0.33.5",
"discord.js": "^14.9.0",
"dotenv": "^16.0.1",
"mariadb": "^3.0.1",
"mysql2": "^2.3.3",
"node-fetch": "^3.2.6",
"safe-regex": "github:davisjam/safe-regex",
"sequelize": "^6.21.3",
"turndown": "^7.1.1",
"twit": "^1.1.20",
"ytpplus-node": "github:Supositware/ytpplus-node"
},
"devDependencies": {
"@babel/eslint-parser": "^7.18.9",
"@babel/plugin-syntax-import-assertions": "^7.18.6",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.6.0",
"@types/node": "^18.7.3",
"eslint": "^8.57.0",
"globals": "^15.8.0",
"eslint": "^8.16.0",
"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
* 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))
* HandBrakeCLI (For [download](commands/utility/download.js))
* gifsicle (For [vid2gif](commands/utility/vid2gif.js))
* gifki (For [vid2gif](commands/utility/vid2gif.js))
* Somewhere to upload files larger than 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
```
@ -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.
``node --env-file .env scripts/deploy-commands.cjs``
``node scripts/deploy-commands.cjs``
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
```
@ -38,7 +38,7 @@ npm install -g pm2
pm2 start index.js --name (insert name)
```
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

View file

@ -2,7 +2,9 @@ import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
dotenv.config();
const { clientId, guildId, token } = process.env;
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'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = await import(pathToFileURL(filePath));
if (command.default.integration_types) {
Object.assign(command.default.data, { integration_types: command.default.integration_types });
Object.assign(command.default.data, { contexts: [0, 1, 2] });
}
const command = await import(filePath);
commands.push(command.default.data.toJSON());
}
}

View file

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

View file

@ -1,6 +1,9 @@
import dotenv from 'dotenv';
import fetch from 'node-fetch';
import { Client, GatewayIntentBits } from 'discord.js';
dotenv.config();
const { botsggToken, botsggEndpoint, token } = process.env;
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';
(async () => {
if (process.platform !== 'linux' && process.argv[2] !== '-f') {
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f or execute ./bin/yt-dlp --update-to nightly');
process.exit(1);
}
else if (process.platform !== 'linux' && process.argv[2] === '-f') {
console.log('Executed with -f. Reminder that this script only download the linux version of yt-dlp.');
}
if (process.platform !== 'linux' && process.argv[2] !== '-f') {
console.error('This script only download the linux version of yt-dlp. If you want to download anyway try again with -f');
process.exit(1);
}
else if (process.platform !== 'linux' && process.argv[2] === '-f') {
console.log('Executed with -f. Reminder that this script only download the linux version of yt-dlp.');
}
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);
for (const match of matches) {
if (search(match[0].trim(), variables)) {
text = text.replace(match[0].trim(), search(match[0].trim(), variables).value);
}
if (search(match[0].trim(), variables)) { text = text.replace(match[0].trim(), search(match[0].trim(), variables).value); }
}
return text;

View file

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

View file

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