[chore]: Initial commit

This commit is contained in:
corner 2020-03-27 13:47:29 +01:00
commit b23e51cfdc
10 changed files with 2409 additions and 0 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
*.js

40
.eslintrc.js Normal file
View File

@ -0,0 +1,40 @@
module.exports = {
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
config.json

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# Breakn't-bot
Welcome to the repository for breakn't-bot. Breakn't-bot is supposed to be a reliable music player for discord servers.
## Try it yourself!
Firstly, clone this repo and install all dependencies (discord.js does not work on devices other than x86 processors):
```bash
git clone <git link> && cd breakn\'t-bot
npm i
```
Then create `config.json` in the root of the breakn't-bot folder. It should contain this:
```json
{
"prefix": "}",
"token": "Discord token",
"api_key": "Youtube data api v3 token"
}
```
To start the bot run: `npm start`;

1786
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "breaknt-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "eslint src/* && tsc src/index.ts && node src/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/opus": "^0.1.0",
"discord.js": "^12.0.2",
"ffmpeg": "0.0.4",
"fluent-ffmpeg": "^2.1.2",
"youtube-search": "^1.1.4",
"ytdl-core": "^2.0.1"
},
"devDependencies": {
"@types/node": "^12.12.31",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"eslint": "^6.8.0",
"typescript": "^3.8.3"
}
}

264
src/index.js Normal file
View File

@ -0,0 +1,264 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
exports.__esModule = true;
var Discord = require("discord.js");
var _a = require("../config.json"), prefix = _a.prefix, token = _a.token, api_key = _a.api_key, debug_channel = _a.debug_channel;
var ytdl = require("ytdl-core");
var search = require("youtube-search");
function debug(message, something) {
return __awaiter(this, void 0, void 0, function () {
var channel;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, client.channels.fetch(debug_channel)];
case 1:
channel = _a.sent();
channel.send("[" + new Date().toUTCString() + "] Reply on " + message.content + ":\n" + something);
return [2 /*return*/];
}
});
});
}
var opts = {
maxResults: 1,
key: api_key,
type: "video"
};
var client = new Discord.Client();
var queue = new Map();
client.once("ready", function () {
console.log("Ready!");
});
client.once("reconnecting", function () {
console.log("Reconnecting!");
});
client.once("disconnect", function () {
console.log("Disconnect!");
});
client.on("message", function (message) { return __awaiter(void 0, void 0, void 0, function () {
var serverQueue, toBeSent_1, toBeSent, currentSong, songInfo;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (message.author.bot)
return [2 /*return*/];
if (!message.content.startsWith(prefix))
return [2 /*return*/];
debug(message, "Received message: " + message.content);
serverQueue = queue.get(message.guild.id);
if (!message.content.startsWith(prefix + "play")) return [3 /*break*/, 1];
execute(message, serverQueue);
return [2 /*return*/];
case 1:
if (!message.content.startsWith(prefix + "skip")) return [3 /*break*/, 2];
skip(message, serverQueue);
return [2 /*return*/];
case 2:
if (!message.content.startsWith(prefix + "stop")) return [3 /*break*/, 3];
stop(message, serverQueue);
return [2 /*return*/];
case 3:
if (!message.content.startsWith(prefix + "queue")) return [3 /*break*/, 4];
toBeSent_1 = ">>> **Queue for breakn't-bot:**\n";
serverQueue.songs.forEach(function (song, index) {
toBeSent_1 += index === 0 ? "_Currently playing:_ " + song.title + "\n" : index + ". " + song.title + "\n";
});
message.channel.send(toBeSent_1);
return [2 /*return*/];
case 4:
if (!message.content.startsWith(prefix + "help")) return [3 /*break*/, 5];
toBeSent = ">>> **Help for breakn't-bot:**\n - `}play <song name>`: Searches for a song on youtube and plays it or adds it to the queue.\n - `}skip`: Skips the currently playing song.\n - `}stop`: Forces the breakn't-bot to leave the channel. Poor bot.\n - `}queue`: Shows the queue.\n - `}remove <index>`: Removes the song with the specified index from the queue.\n - `}pause`: Pause the playback.\n - `}resume`: Resume the playback.\n - `}np`: Shows info about the currently playing song.";
message.channel.send(toBeSent);
return [2 /*return*/];
case 5:
if (!message.content.startsWith(prefix + "remove")) return [3 /*break*/, 6];
remove(message, serverQueue);
return [2 /*return*/];
case 6:
if (!message.content.startsWith(prefix + "np")) return [3 /*break*/, 8];
currentSong = serverQueue.songs[0];
return [4 /*yield*/, ytdl.getInfo(currentSong.url)];
case 7:
songInfo = _a.sent();
message.channel.send(">>> _Currently playing:_ " + currentSong.title + "\n " + (serverQueue.connection.dispatcher.streamTime / 1000).toFixed(0) + "/" + songInfo.length_seconds);
return [2 /*return*/];
case 8:
if (message.content.startsWith(prefix + "pause")) {
serverQueue.connection.dispatcher.pause();
return [2 /*return*/];
}
else if (message.content.startsWith(prefix + "resume")) {
serverQueue.connection.dispatcher.resume();
return [2 /*return*/];
}
else {
message.channel.send("command validn't!");
}
_a.label = 9;
case 9: return [2 /*return*/];
}
});
}); });
function execute(message, serverQueue) {
return __awaiter(this, void 0, void 0, function () {
var _a, args, voiceChannel, permissions;
var _this = this;
return __generator(this, function (_b) {
_a = message.content.split(" "), args = _a.slice(1);
debug(message, "Trying to find song: " + args.join(" "));
voiceChannel = message.member.voice.channel;
if (!voiceChannel)
return [2 /*return*/, message.channel.send("You need to be in a voice channel to play music!")];
permissions = voiceChannel.permissionsFor(message.client.user);
if (!permissions.has("CONNECT") || !permissions.has("SPEAK"))
return [2 /*return*/, message.channel.send("I need the permissions to join and speak in your voice channel!")];
search(args.join(" "), opts, function (err, results) { return __awaiter(_this, void 0, void 0, function () {
var song, queueContruct, connection, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (err)
return [2 /*return*/, debug(message, "Error! " + err.name + ": " + err.message + ". Stacktrace: " + err.stack)];
song = {
title: results[0].title,
url: results[0].link
};
if (!!serverQueue) return [3 /*break*/, 5];
queueContruct = {
textChannel: message.channel,
voiceChannel: voiceChannel,
connection: null,
songs: [],
volume: 5,
playing: true
};
queue.set(message.guild.id, queueContruct);
queueContruct.songs.push(song);
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, voiceChannel.join()];
case 2:
connection = _a.sent();
queueContruct.connection = connection;
play(message.guild, queueContruct.songs[0], message);
return [3 /*break*/, 4];
case 3:
err_1 = _a.sent();
debug(message, "Error! " + err_1.name + ": " + err_1.message + ". Stacktrace: " + err_1.stack);
queue["delete"](message.guild.id);
return [2 /*return*/, message.channel.send(err_1)];
case 4: return [3 /*break*/, 6];
case 5:
debug(message, "Playing song " + song.title);
serverQueue.songs.push(song);
return [2 /*return*/, message.channel.send("**" + song.title + "** has been added to the queue!")];
case 6: return [2 /*return*/];
}
});
}); });
return [2 /*return*/];
});
});
}
function remove(message, serverQueue) {
var _a = message.content.split(" "), args = _a.slice(1);
if (/^\d+$/.test(args[0])) {
var index = Number(args[0]);
if (index < serverQueue.songs.length) {
var deleted = serverQueue.songs.splice(index, 1)[0];
message.channel.send("Removed **" + deleted.title + "** from the queue");
}
else {
debug(message, "The entered value is outside the queue's range.");
}
}
else {
debug(message, "The entered value is not a valid number!");
}
}
function skip(message, serverQueue) {
if (!message.member.voice.channel) {
debug(message, "No song to skip!");
message.channel.send("You have to be in a voice channel to stop the music!");
return;
}
if (!serverQueue) {
message.channel.send("There is no song that I could skip!");
debug(message, "No song to skip!");
return;
}
serverQueue.connection.dispatcher.end();
}
function stop(message, serverQueue) {
if (!message.member.voice.channel)
return message.channel.send("You have to be in a voice channel to stop the music!");
if (serverQueue.songs.length) {
serverQueue.songs = [];
}
serverQueue.connection.dispatcher.end();
serverQueue.voiceChannel.leave();
}
function play(guild, song, message) {
var serverQueue = queue.get(guild.id);
if (!song) {
serverQueue.voiceChannel.leave();
queue["delete"](guild.id);
return;
}
var dispatcher;
function attemptPlay() {
debug(message, "Attempting to play song!");
dispatcher = serverQueue.connection
.play(ytdl(song.url))
.on("finish", function () {
serverQueue.songs.shift();
play(guild, serverQueue.songs[0], message);
})
.on("error", function (err) {
debug(message, "Error playing song!");
debug(message, "Error! " + err.name + ": " + err.message + ". Stacktrace: " + err.stack);
attemptPlay();
});
debug(message, "Successfully avoided errors!");
dispatcher.setVolumeLogarithmic(serverQueue.volume / 5);
serverQueue.textChannel.send("Started playing: **" + song.title + "**");
}
attemptPlay();
}
client.login(token);

252
src/index.ts Normal file
View File

@ -0,0 +1,252 @@
import * as Discord from "discord.js";
const { prefix, token, api_key, debug_channel } = require("../config.json");
import * as ytdl from "ytdl-core";
import * as search from "youtube-search";
// eslint-disable-next-line no-unused-vars
import { Song, QueueConstruct } from "./model";
async function debug(message: Discord.Message | Discord.PartialMessage, something: string) {
const channel = await client.channels.fetch(debug_channel) as Discord.TextChannel;
channel.send(`[${new Date().toUTCString()}] Reply on ${message.content}:\n${something}`);
}
const opts: search.YouTubeSearchOptions = {
maxResults: 1,
key: api_key,
type: "video"
};
const client = new Discord.Client();
const queue = new Map();
client.once("ready", () => {
console.log("Ready!");
});
client.once("reconnecting", () => {
console.log("Reconnecting!");
});
client.once("disconnect", () => {
console.log("Disconnect!");
});
client.on("message", async message => {
if (message.author.bot) return;
if (!message.content.startsWith(prefix)) return;
debug(message, `Received message: ${message.content}`);
const serverQueue: QueueConstruct = queue.get(message.guild.id);
if (message.content.startsWith(`${prefix}play`)) {
execute(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}skip`)) {
skip(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}stop`)) {
stop(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}queue`)) {
let toBeSent = ">>> **Queue for breakn't-bot:**\n";
serverQueue.songs.forEach((song, index) => {
toBeSent += index === 0 ? `_Currently playing:_ ${song.title}\n` : `${index}. ${song.title}\n`;
});
message.channel.send(toBeSent);
return;
} else if (message.content.startsWith(`${prefix}help`)) {
let toBeSent = `>>> **Help for breakn't-bot:**
- \`}play <song name>\`: Searches for a song on youtube and plays it or adds it to the queue.
- \`}skip\`: Skips the currently playing song.
- \`}stop\`: Forces the breakn't-bot to leave the channel. Poor bot.
- \`}queue\`: Shows the queue.
- \`}remove <index>\`: Removes the song with the specified index from the queue.
- \`}pause\`: Pause the playback.
- \`}resume\`: Resume the playback.
- \`}np\`: Shows info about the currently playing song.`;
message.channel.send(toBeSent);
return;
} else if (message.content.startsWith(`${prefix}remove`)) {
remove(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}np`)) {
const currentSong = serverQueue.songs[0];
const songInfo = await ytdl.getInfo(currentSong.url);
message.channel.send(`>>> _Currently playing:_ ${currentSong.title}
${(serverQueue.connection.dispatcher.streamTime/1000).toFixed(0)}/${songInfo.length_seconds}`);
return;
} else if (message.content.startsWith(`${prefix}pause`)) {
serverQueue.connection.dispatcher.pause();
return;
} else if (message.content.startsWith(`${prefix}resume`)) {
serverQueue.connection.dispatcher.resume();
return;
} else {
message.channel.send("command validn't!");
}
});
async function execute(message: Discord.Message | Discord.PartialMessage, serverQueue: QueueConstruct) {
const [, ...args] = message.content.split(" ");
debug(message, `Trying to find song: ${args.join(" ")}`);
const voiceChannel = message.member.voice.channel;
if (!voiceChannel)
return message.channel.send(
"You need to be in a voice channel to play music!"
);
const permissions = voiceChannel.permissionsFor(message.client.user);
if (!permissions.has("CONNECT") || !permissions.has("SPEAK"))
return message.channel.send(
"I need the permissions to join and speak in your voice channel!"
);
search(args.join(" "), opts, async (err, results) => {
if(err) return debug(message, "Error! " + err.name + ": " + err.message + ". Stacktrace: " + err.stack);
const song: Song = {
title: results[0].title,
url: results[0].link
};
if (!serverQueue) {
const queueContruct: QueueConstruct = {
textChannel: message.channel,
voiceChannel: voiceChannel,
connection: null,
songs: [],
volume: 5,
playing: true
};
queue.set(message.guild.id, queueContruct);
queueContruct.songs.push(song);
try {
var connection = await voiceChannel.join();
queueContruct.connection = connection;
play(message.guild, queueContruct.songs[0], message);
} catch (err) {
debug(message, "Error! " + err.name + ": " + err.message + ". Stacktrace: " + err.stack);
queue.delete(message.guild.id);
return message.channel.send(err);
}
} else {
debug(message, `Playing song ${song.title}`);
serverQueue.songs.push(song);
return message.channel.send(`**${song.title}** has been added to the queue!`);
}
});
}
function remove(message: Discord.Message | Discord.PartialMessage, serverQueue: QueueConstruct) {
const [, ...args] = message.content.split(" ");
if (/^\d+$/.test(args[0])) {
const index = Number(args[0]);
if (index < serverQueue.songs.length) {
const deleted = serverQueue.songs.splice(index, 1)[0];
message.channel.send(`Removed **${deleted.title}** from the queue`);
} else {
debug(message, "The entered value is outside the queue's range.");
}
} else {
debug(message, "The entered value is not a valid number!");
}
}
function skip(message: Discord.Message | Discord.PartialMessage, serverQueue: QueueConstruct) {
if (!message.member.voice.channel) {
debug(message, "No song to skip!");
message.channel.send("You have to be in a voice channel to stop the music!");
return;
}
if (!serverQueue) {
message.channel.send("There is no song that I could skip!");
debug(message, "No song to skip!");
return;
}
serverQueue.connection.dispatcher.end();
}
function stop(message: Discord.Message | Discord.PartialMessage, serverQueue: QueueConstruct) {
if (!message.member.voice.channel)
return message.channel.send(
"You have to be in a voice channel to stop the music!"
);
if (serverQueue.songs.length) {
serverQueue.songs = [];
}
serverQueue.connection.dispatcher.end();
serverQueue.voiceChannel.leave();
}
function play(guild: Discord.Guild, song: Song, message: Discord.Message | Discord.PartialMessage) {
const serverQueue: QueueConstruct = queue.get(guild.id);
if (!song) {
serverQueue.voiceChannel.leave();
queue.delete(guild.id);
return;
}
let dispatcher: Discord.StreamDispatcher;
function attemptPlay() {
debug(message, "Attempting to play song!");
dispatcher = serverQueue.connection
.play(ytdl(song.url))
.on("finish", () => {
serverQueue.songs.shift();
play(guild, serverQueue.songs[0], message);
})
.on("error", err => {
debug(message, "Error playing song!");
debug(message, "Error! " + err.name + ": " + err.message + ". Stacktrace: " + err.stack);
attemptPlay();
});
debug(message, "Successfully avoided errors!");
dispatcher.setVolumeLogarithmic(serverQueue.volume / 5);
serverQueue.textChannel.send(`Started playing: **${song.title}**`);
}
attemptPlay();
}
client.login(token);

2
src/model.js Normal file
View File

@ -0,0 +1,2 @@
"use strict";
exports.__esModule = true;

13
src/model.ts Normal file
View File

@ -0,0 +1,13 @@
// eslint-disable-next-line no-unused-vars
import * as Discord from "discord.js";
export interface Song {title: string, url: string}
export interface QueueConstruct {
textChannel: Discord.TextChannel | Discord.DMChannel,
voiceChannel: Discord.VoiceChannel,
connection: null | Discord.VoiceConnection,
songs: Song[],
volume: number,
playing: boolean
}