Initial slash bot (Only download and ping)
This commit is contained in:
parent
dbec2afc42
commit
f7108763bb
11 changed files with 2547 additions and 0 deletions
49
.eslintrc.json
Normal file
49
.eslintrc.json
Normal 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
151
commands/download.js
Normal 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
10
commands/ping.js
Normal 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
30
deploy-commands.js
Normal 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);
|
19
events/interactionCreate.js
Normal file
19
events/interactionCreate.js
Normal 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
32
events/ready.js
Normal 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
41
index.js
Normal 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
2108
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal 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
29
prereq.js
Normal 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
56
readme.md
Normal 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
|
Loading…
Reference in a new issue