Initial slash bot (Only download and ping)

This commit is contained in:
Supositware 2022-06-16 09:18:39 +02:00
parent dbec2afc42
commit f7108763bb
11 changed files with 2547 additions and 0 deletions

49
.eslintrc.json Normal file
View file

@ -0,0 +1,49 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021
},
"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"
}
}

151
commands/download.js Normal file
View file

@ -0,0 +1,151 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const { MessageEmbed, MessageActionRow, MessageSelectMenu } = require('discord.js');
const { exec } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
module.exports = {
data: new SlashCommandBuilder()
.setName('download')
.setDescription('Download a video.')
.addStringOption(option =>
option.setName('url')
.setDescription('URL of the video you want to download.')
.setRequired(true))
.addBooleanOption(option =>
option.setName('advanced')
.setDescription('Choose the quality of the video.')
.setRequired(false)),
async execute(interaction) {
await interaction.deferReply({ ephemeral: false });
const url = interaction.options.getString('url');
if (interaction.options.getBoolean('advanced')) {
let qualitys = await new Promise((resolve, reject) => {
exec(`./bin/yt-dlp ${url} --print "%()j"`, (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout);
});
});
qualitys = JSON.parse(qualitys);
const options = [];
qualitys.formats.forEach(f => {
options.push({
label: f.resolution,
description: f.format,
value: f.format_id,
});
});
if (options.length > 25) {
// Reverse so the higher quality formats are first
options.reverse();
while (options.length > 25) {
// Remove the lower quality formats
options.pop();
}
// Reverse again so the lower quality appears first
options.reverse();
}
const row = new MessageActionRow()
.addComponents(
new MessageSelectMenu()
.setCustomId('downloadQuality')
.setPlaceholder('Nothing selected')
.addOptions(options),
);
await interaction.deleteReply();
await interaction.followUp({ content: 'Which quality do you want?', ephemeral: true, components: [row] });
interaction.client.on('interactionCreate', async (interactionMenu) => {
if (!interactionMenu.isSelectMenu()) return;
if (interactionMenu.customId === 'downloadQuality') {
await interactionMenu.deferReply({ ephemeral: false });
download(url, interactionMenu);
}
});
return;
}
download(url, interaction);
},
};
async function download(url, interaction) {
let format = 'bestvideo*+bestaudio/best';
const Embed = new MessageEmbed()
.setColor(interaction.member ? interaction.member.displayHexColor : 'NAVY')
.setAuthor(`Downloaded by ${interaction.member.displayName}`, interaction.member.displayAvatarURL(), url)
.setDescription(url);
if (interaction.customId === 'downloadQuality') {
format = interaction.values[0];
}
downloadVideo(url, interaction.id, format)
.then(async () => {
const file = fs.readdirSync(os.tmpdir()).filter(fn => fn.startsWith(interaction.id));
const output = `${os.tmpdir()}/${file}`;
const fileStat = fs.statSync(output);
const fileSize = fileStat.size / 1000000.0;
if (fileSize > 100) {
await interaction.deleteReply();
await interaction.followUp('Uh oh! The video you tried to download is too big!', { ephemeral: true });
}
else if (fileSize > 8) {
const fileURL = await upload(output)
.catch(err => {
console.error(err);
});
await interaction.editReply({ content: 'File was bigger than 8 mb. It has been uploaded to an external site.', embeds: [Embed], ephemeral: false });
await interaction.followUp({ content: fileURL, ephemeral: false });
}
else {
await interaction.editReply({ embeds: [Embed], files: [output], ephemeral: false });
}
})
.catch(async err => {
console.error(err);
await interaction.deleteReply();
await interaction.followUp({ content: 'Uh oh! An error has occured!', ephemeral: true });
});
}
async function downloadVideo(url, output, format) {
await new Promise((resolve, reject) => {
exec(`./bin/yt-dlp -f ${format} ${url} -o "${os.tmpdir()}/${output}.%(ext)s" --force-overwrites`, (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout);
});
});
}
async function upload(file) {
return await new Promise((resolve, reject) => {
exec(`./bin/upload.sh ${file}`, (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout);
});
});
}

10
commands/ping.js Normal file
View file

@ -0,0 +1,10 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction) {
await interaction.reply('Pong!');
},
};

30
deploy-commands.js Normal file
View file

@ -0,0 +1,30 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
require('dotenv').config();
const { clientId, guildId, token } = process.env;
const commands = [
new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with pong!'),
new SlashCommandBuilder()
.setName('download')
.setDescription('Download a video. (100 mb max)')
.addStringOption(option =>
option.setName('url')
.setDescription('URL of the video you want to download.')
.setRequired(true))
.addBooleanOption(option =>
option.setName('advanced')
.setDescription('Choose the quality of the video.')
.setRequired(false)),
]
.map(command => command.toJSON());
const rest = new REST({ version: '9' }).setToken(token);
rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands })
.then(() => console.log('Successfully registered application commands.'))
.catch(console.error);

View file

@ -0,0 +1,19 @@
module.exports = {
name: 'interactionCreate',
async execute(interaction) {
const client = interaction.client;
if (!interaction.isCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
}
catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
},
};

32
events/ready.js Normal file
View file

@ -0,0 +1,32 @@
const { exec } = require('node:child_process');
module.exports = {
name: 'ready',
once: true,
async execute(client) {
const ytdlpVersion = await new Promise((resolve, reject) => {
exec('./bin/yt-dlp --version', (err, stdout, stderr) => {
if (err) {
reject(stderr);
}
if (stderr) {
console.error(stderr);
}
resolve(stdout);
});
});
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}\x1b[0m`);
console.log('===========[ READY ]===========');
},
};

41
index.js Normal file
View file

@ -0,0 +1,41 @@
// Require the necessary discord.js classes
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, Intents } = require('discord.js');
require('dotenv').config();
const { token } = process.env;
// Create a new client instance
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
// Load commands from the commands folder
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
client.commands.set(command.data.name, command);
}
// Load events from the events folder
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
}
else {
client.on(event.name, (...args) => event.execute(...args));
}
}
// Login to Discord with your client's token
client.login(token);

2108
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "haha-yes-new",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/builders": "^0.13.0",
"@discordjs/rest": "^0.4.1",
"discord-api-types": "^0.33.1",
"discord.js": "^13.7.0",
"dotenv": "^16.0.1"
},
"devDependencies": {
"eslint": "^8.16.0"
}
}

29
prereq.js Normal file
View file

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

56
readme.md Normal file
View file

@ -0,0 +1,56 @@
# Haha Yes
A multi function discord bot.
## Getting Started
These instructions will get you a copy of the project up and running on your local machine
### Prerequisites
You need to install the following
* ffmpeg (Optional but very recommanded: for yt-dlp to merge video/audio formats)
* yt-dlp ([a file can download it for you](prereq.js))
### Installing
```
git clone https://git.namejeff.xyz/Supositware/Haha-Yes
cd haha-Yes
git checkout slash
npm install
```
To run the bot either use pm2
```
npm install -g pm2
pm2 start index.js --name(insert name)
```
or with node ``node index.js``
If on linux you can also do
``nohup node index.js &``
## Built With
* [Discord.JS](https://github.com/discordjs/discord.js)
* [yt-dlp](https://github.com/yt-dlp/yt-dlp)
## Authors
* **Loïc Bersier**
## Donation link
[![Paypal](https://www.paypalobjects.com/en_US/CH/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/paypalme2/supositware/)
## License
This project is licensed under the **GNU Affero General Public License v3.0** License - see the [LICENSE](LICENSE) file for details
## Acknowledgments
* [discord.JS team](https://github.com/discordjs/discord.js)
* Tina the Cyclops girl#0064 for inspiring me for starting the making of this bot