forked from Supositware/Haha-Yes
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