Replace Lavalink wrapper, migrate to pnpm, add ko-fi sponsor link

This commit is contained in:
Essem 2022-06-14 00:38:01 -05:00
parent cefafba8fb
commit 10becff3a0
No known key found for this signature in database
GPG key ID: 7D497397CC3A2A8C
19 changed files with 2768 additions and 8025 deletions

1
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
patreon: TheEssem
ko_fi: TheEssem

View file

@ -17,10 +17,12 @@ jobs:
uses: actions/checkout@v1
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.2
- name: Install dependencies
run: sudo apt install libvips-dev libmagick++-dev
- name: Build
run: npm install --legacy-peer-deps && npm run build
run: pnpm install && npm run build
win32:
runs-on: windows-latest
@ -29,10 +31,12 @@ jobs:
uses: actions/checkout@v1
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.2
- name: Install dependencies
run: choco install imagemagick -PackageParameters InstallDevelopmentHeaders=true
- name: Build
run: npm install --legacy-peer-deps && npm run build
run: pnpm install && npm run build
darwin:
runs-on: macos-latest
@ -41,7 +45,9 @@ jobs:
uses: actions/checkout@v1
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.2
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.2
- name: Install dependencies
run: brew install imagemagick vips
- name: Build
run: npm install --legacy-peer-deps && npm run build
run: pnpm install && npm run build

View file

@ -52,8 +52,6 @@ jobs:
#scope: # optional
# Used to pull node distributions from node-versions. Since there's a default, this is typically not supplied by the user.
#token: # optional, default is ${{ github.token }}
- run: sudo apt install libvips-dev libmagick++-dev
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
@ -65,20 +63,5 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild
# uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# npm install --legacy-peer-deps
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View file

@ -20,10 +20,8 @@ class AvatarCommand extends Command {
return self.dynamicAvatarURL(null, 512);
}
} else if (this.args.join(" ") !== "" && this.channel.guild) {
console.log(member);
const searched = await this.channel.guild.searchMembers(this.args.join(" "));
if (searched.length === 0) return self.dynamicAvatarURL(null, 512);
console.log(searched);
const user = await this.client.getRESTUser(searched[0].user.id);
return user ? user.dynamicAvatarURL(null, 512) : self.dynamicAvatarURL(null, 512);
} else {

View file

@ -6,7 +6,7 @@ class CaptionTwoCommand extends ImageCommand {
params(url) {
const newArgs = this.options.text ?? this.args.filter(item => !item.includes(url)).join(" ");
return {
caption: newArgs && newArgs.trim() ? newArgs.replaceAll("&", "&amp;").replaceAll(">", "&gt;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;").replaceAll("%", "%").replaceAll("\\n", "\n") : words.sort(() => 0.5 - Math.random()).slice(0, Math.floor(Math.random() * words.length + 1)).join(" "),
caption: newArgs && newArgs.trim() ? newArgs.replaceAll("&", "&amp;").replaceAll(">", "&gt;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;").replaceAll("\\n", "\n") : words.sort(() => 0.5 - Math.random()).slice(0, Math.floor(Math.random() * words.length + 1)).join(" "),
top: !!this.specialArgs.top,
font: this.specialArgs.font && allowedFonts.includes(this.specialArgs.font.toLowerCase()) ? this.specialArgs.font.toLowerCase() : "helvetica"
};

View file

@ -1,4 +1,3 @@
import { Rest } from "lavacord";
import format from "format-duration";
import MusicCommand from "../../classes/musicCommand.js";
@ -9,8 +8,8 @@ class NowPlayingCommand extends MusicCommand {
if (!this.channel.guild.members.get(this.client.user.id).voiceState.channelID) return "I'm not in a voice channel!";
const player = this.connection.player;
if (!player) return "I'm not playing anything!";
const track = await Rest.decode(player.node, player.track);
const parts = Math.floor((player.state.position / track.length) * 10);
const track = await player.node.rest.decode(player.track);
const parts = Math.floor((player.position / track.length) * 10);
return {
embeds: [{
color: 16711680,
@ -32,7 +31,7 @@ class NowPlayingCommand extends MusicCommand {
},
{
name: `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
value: `${format(player.state.position)}/${track.isStream ? "∞" : format(track.length)}`
value: `${format(player.position)}/${track.isStream ? "∞" : format(track.length)}`
}]
}]
};

View file

@ -1,6 +1,6 @@
//import { Rest } from "lavacord";
import fetch from "node-fetch";
import format from "format-duration";
import { nodes } from "../../utils/soundplayer.js";
import paginator from "../../utils/pagination/pagination.js";
import MusicCommand from "../../classes/musicCommand.js";
@ -11,8 +11,8 @@ class QueueCommand extends MusicCommand {
if (!this.channel.guild.members.get(this.client.user.id).voiceState.channelID) return "I'm not in a voice channel!";
if (!this.channel.permissionsOf(this.client.user.id).has("embedLinks")) return "I don't have the `Embed Links` permission!";
const player = this.connection;
//const tracks = await Rest.decode(player.player.node, queue);
const tracks = await fetch(`http://${player.player.node.host}:${player.player.node.port}/decodetracks`, { method: "POST", body: JSON.stringify(this.queue), headers: { Authorization: player.player.node.password, "Content-Type": "application/json" } }).then(res => res.json());
const node = nodes.filter((val) => { return val.name === player.player.node.name })[0];
const tracks = await fetch(`http://${node.url}/decodetracks`, { method: "POST", body: JSON.stringify(this.queue), headers: { Authorization: node.auth, "Content-Type": "application/json" } }).then(res => res.json());
const trackList = [];
const firstTrack = tracks.shift();
for (const [i, track] of tracks.entries()) {

View file

@ -1,4 +1,3 @@
import { Rest } from "lavacord";
import { queues } from "../../utils/soundplayer.js";
import MusicCommand from "../../classes/musicCommand.js";
@ -11,7 +10,8 @@ class RemoveCommand extends MusicCommand {
const pos = parseInt(this.options.position ?? this.args[0]);
if (isNaN(pos) || pos > this.queue.length || pos < 1) return "That's not a valid position!";
const removed = this.queue.splice(pos, 1);
const track = await Rest.decode(this.connection.player.node, removed[0]);
if (removed.length === 0) return "That's not a valid position!";
const track = await this.connection.player.node.rest.decode(removed[0]);
queues.set(this.channel.guild.id, this.queue);
return `🔊 The song \`${track.title ? track.title : "(blank)"}\` has been removed from the queue.`;
}

View file

@ -1,4 +1,3 @@
import { Rest } from "lavacord";
import MusicCommand from "../../classes/musicCommand.js";
class SeekCommand extends MusicCommand {
@ -8,11 +7,11 @@ class SeekCommand extends MusicCommand {
if (!this.channel.guild.members.get(this.client.user.id).voiceState.channelID) return "I'm not in a voice channel!";
if (this.connection.host !== this.author.id) return "Only the current voice session host can seek the music!";
const player = this.connection.player;
const track = await Rest.decode(player.node, player.track);
const track = await player.node.rest.decode(player.track);
if (!track.isSeekable) return "This track isn't seekable!";
const seconds = parseFloat(this.options.position ?? this.args[0]);
if (isNaN(seconds) || (seconds * 1000) > track.length || (seconds * 1000) < 0) return "That's not a valid position!";
await player.seek(seconds * 1000);
player.seekTo(seconds * 1000);
return `🔊 Seeked track to ${seconds} second(s).`;
}

View file

@ -16,7 +16,7 @@ class SkipCommand extends MusicCommand {
max: votes.max
};
if (votes.count + 1 === votes.max) {
await player.player.stop(this.channel.guild.id);
await player.player.stopTrack(this.channel.guild.id);
skipVotes.set(this.channel.guild.id, { count: 0, ids: [], max: Math.min(3, player.voiceChannel.voiceMembers.filter((i) => i.id !== this.client.user.id && !i.bot).length) });
if (this.type === "application") return "🔊 The current song has been skipped.";
} else {
@ -24,7 +24,7 @@ class SkipCommand extends MusicCommand {
return `🔊 Voted to skip song (${votes.count + 1}/${votes.max} people have voted).`;
}
} else {
await player.player.stop(this.channel.guild.id);
await player.player.stopTrack();
if (this.type === "application") return "🔊 The current song has been skipped.";
}
}

View file

@ -7,13 +7,12 @@ class StopCommand extends MusicCommand {
if (!this.member.voiceState.channelID) return "You need to be in a voice channel first!";
if (!this.channel.guild.members.get(this.client.user.id).voiceState.channelID) return "I'm not in a voice channel!";
if (!this.connection) {
await manager.leave(this.channel.guild.id);
await manager.getNode().leaveChannel(this.channel.guild.id);
return "🔊 The current voice channel session has ended.";
}
if (this.connection.host !== this.author.id && !this.member.permissions.has("manageChannels")) return "Only the current voice session host can stop the music!";
await manager.leave(this.channel.guild.id);
const connection = this.connection.player;
await connection.destroy();
connection.node.leaveChannel(this.channel.guild.id);
players.delete(this.channel.guild.id);
queues.delete(this.channel.guild.id);
return "🔊 The current voice channel session has ended.";

View file

@ -7,7 +7,7 @@ class ToggleCommand extends MusicCommand {
if (!this.channel.guild.members.get(this.client.user.id).voiceState.channelID) return "I'm not in a voice channel!";
if (this.connection.host !== this.author.id && !this.member.permissions.has("manageChannels")) return "Only the current voice session host can pause/resume the music!";
const player = this.connection.player;
await player.pause(!player.paused ? true : false);
player.setPaused(!player.paused ? true : false);
return `🔊 The player has been ${player.paused ? "paused" : "resumed"}.`;
}

View file

@ -1,17 +0,0 @@
import { manager } from "../utils/soundplayer.js";
// run when a raw packet is sent, used for sending data to lavalink
export default async (client, cluster, worker, ipc, packet) => {
if (!manager) return;
switch (packet.t) {
case "VOICE_SERVER_UPDATE":
await manager.voiceServerUpdate(packet.d);
break;
case "VOICE_STATE_UPDATE":
await manager.voiceStateUpdate(packet.d);
break;
case "GUILD_CREATE":
for (const state of packet.d.voice_states) await manager.voiceStateUpdate({ ...state, guild_id: packet.d.id });
break;
}
};

7920
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -33,15 +33,16 @@
"file-type": "^17.1.1",
"format-duration": "^2.0.0",
"jsqr": "^1.3.1",
"lavacord": "^1.1.9",
"node-addon-api": "^5.0.0",
"node-emoji": "^1.10.0",
"node-fetch": "^3.2.0",
"qrcode": "^1.4.4",
"sharp": "^0.30.6",
"shoukaku": "^3.1.0",
"winston": "^3.3.3"
},
"devDependencies": {
"@babel/core": "^7.18.5",
"@babel/eslint-parser": "^7.13.8",
"@babel/eslint-plugin": "^7.13.0",
"@babel/plugin-proposal-class-properties": "^7.13.0",

2685
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"lava": [
{ "id": "1", "host": "localhost", "port": 2333, "password": "youshallnotpass", "local": true }
{ "name": "localhost", "url": "localhost:2333", "auth": "youshallnotpass", "local": true }
],
"image": [
{ "server": "localhost", "auth": "verycoolpass100", "tls": false }

View file

@ -10,7 +10,7 @@ import { log, error } from "./utils/logger.js";
// initialize command loader
import { load, update } from "./utils/handler.js";
// lavalink stuff
import { checkStatus, connect, status, connected } from "./utils/soundplayer.js";
import { checkStatus, connect, reload, status, connected, manager } from "./utils/soundplayer.js";
// database stuff
import database from "./utils/database.js";
// command collections
@ -83,7 +83,7 @@ class Shard extends BaseClusterWorker {
this.ipc.register("soundreload", async () => {
const soundStatus = await checkStatus();
if (!soundStatus) {
const length = await connect(this.bot);
const length = reload();
return this.ipc.broadcast("soundReloadSuccess", { length });
} else {
return this.ipc.broadcast("soundReloadFail");

View file

@ -2,15 +2,14 @@ import * as logger from "./logger.js";
import fetch from "node-fetch";
import fs from "fs";
import format from "format-duration";
import { Manager, Rest } from "lavacord";
let nodes;
import { Shoukaku, Connectors } from "shoukaku";
export const players = new Map();
export const queues = new Map();
export const skipVotes = new Map();
export let manager;
export let nodes;
export let status = false;
export let connected = false;
@ -20,7 +19,7 @@ export async function checkStatus() {
const newNodes = [];
for (const node of nodes) {
try {
const response = await fetch(`http://${node.host}:${node.port}/version`, { headers: { Authorization: node.password } }).then(res => res.text());
const response = await fetch(`http://${node.url}/version`, { headers: { Authorization: node.auth } }).then(res => res.text());
if (response) newNodes.push(node);
} catch {
logger.error(`Failed to get status of Lavalink node ${node.host}.`);
@ -32,22 +31,31 @@ export async function checkStatus() {
}
export async function connect(client) {
manager = new Manager(nodes, {
user: client.user.id,
shards: client.shards.size || 1,
send: (packet) => {
const guild = client.guilds.get(packet.d.guild_id);
if (!guild) return;
return guild.shard.sendWS(packet.op, packet.d);
}
});
const { length } = await manager.connect();
logger.log(`Successfully connected to ${length} Lavalink node(s).`);
connected = true;
manager.on("error", (error, node) => {
manager = new Shoukaku(new Connectors.Eris(client), nodes);
client.emit("ready"); // workaround
manager.on("error", (node, error) => {
logger.error(`An error occurred on Lavalink node ${node}: ${error}`);
});
return length;
manager.once("ready", (name) => {
logger.log(`Successfully connected to ${manager.nodes.size} Lavalink node(s).`);
connected = true;
});
}
export function reload() {
const activeNodes = manager.nodes;
const names = nodes.map((a) => a.name);
for (const name in activeNodes) {
if (!names.includes(name)) {
manager.removeNode(name);
}
}
for (const node of nodes) {
if (!activeNodes.has(node.name)) {
manager.addNode(node);
}
}
return manager.nodes.size;
}
export async function play(client, sound, options, music = false) {
@ -58,41 +66,44 @@ export async function play(client, sound, options, music = false) {
const voiceChannel = options.channel.guild.channels.get(options.member.voiceState.channelID);
if (!voiceChannel.permissionsOf(client.user.id).has("voiceConnect")) return "I don't have permission to join this voice channel!";
const player = players.get(options.channel.guild.id);
if (!music && manager.voiceStates.has(options.channel.guild.id) && (player && player.type === "music")) return "I can't play a sound effect while playing music!";
let node = manager.idealNodes[0];
if (!music && manager.players().has(options.channel.guild.id) && (player && player.type === "music")) return "I can't play a sound effect while playing music!";
let node = manager.getNode();
if (!node) {
const status = await checkStatus();
if (!status) {
await connect(client);
node = manager.idealNodes[0];
node = manager.getNode();
}
}
if (!music && !nodes.filter(obj => obj.host === node.host)[0].local) {
if (!music && !nodes.filter(obj => obj.name === node.name)[0].local) {
sound = sound.replace(/\.\//, "https://raw.githubusercontent.com/esmBot/esmBot/master/");
}
let tracks, playlistInfo;
let response;
try {
({ tracks, playlistInfo } = await Rest.load(node, sound));
response = await node.rest.resolve(sound);
if (!response) return "🔊 I couldn't get a response from the audio server.";
if (response.loadType === "NO_MATCHES" || response.loadType === "LOAD_FAILED") return "I couldn't find that song!";
} catch {
return "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit.";
}
const oldQueue = queues.get(voiceChannel.guild.id);
if (!tracks || tracks.length === 0) return "I couldn't find that song!";
if (!response.tracks || response.tracks.length === 0) return "I couldn't find that song!";
if (music) {
const sortedTracks = tracks.map((val) => { return val.track; });
const playlistTracks = playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]];
const sortedTracks = response.tracks.map((val) => { return val.track; });
const playlistTracks = response.playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]];
queues.set(voiceChannel.guild.id, oldQueue ? [...oldQueue, ...playlistTracks] : playlistTracks);
}
const connection = await manager.join({
guild: voiceChannel.guild.id,
channel: voiceChannel.id,
node: node.id
}, { selfdeaf: true });
const connection = player && player.player ? player.player : await node.joinChannel({
guildId: voiceChannel.guild.id,
channelId: voiceChannel.id,
shardId: voiceChannel.guild.shard.id,
deaf: true
});
if (oldQueue && oldQueue.length !== 0 && music) {
return `Your ${playlistInfo.name ? "playlist" : "tune"} \`${playlistInfo.name ? playlistInfo.name.trim() : (tracks[0].info.title !== "" ? tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`;
return `Your ${response.playlistInfo.name ? "playlist" : "tune"} \`${response.playlistInfo.name ? response.playlistInfo.name.trim() : (response.tracks[0].info.title !== "" ? response.tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`;
} else {
nextSong(client, options, connection, tracks[0].track, tracks[0].info, music, voiceChannel, player ? player.host : options.member.id, player ? player.loop : false, player ? player.shuffle : false);
nextSong(client, options, connection, response.tracks[0].track, response.tracks[0].info, music, voiceChannel, player ? player.host : options.member.id, player ? player.loop : false, player ? player.shuffle : false);
return;
}
}
@ -151,9 +162,9 @@ export async function nextSong(client, options, connection, track, info, music,
}
connection.removeAllListeners("error");
connection.removeAllListeners("end");
await connection.play(track);
connection.playTrack({ track });
players.set(voiceChannel.guild.id, { player: connection, type: music ? "music" : "sound", host: host, voiceChannel: voiceChannel, originalChannel: options.channel, loop: loop, shuffle: shuffle, playMessage: playingMessage });
connection.once("error", async (error) => {
connection.once("exception", async (exception) => {
try {
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
const playMessage = players.get(voiceChannel.guild.id).playMessage;
@ -162,16 +173,15 @@ export async function nextSong(client, options, connection, track, info, music,
// no-op
}
try {
await manager.leave(voiceChannel.guild.id);
await connection.destroy();
connection.node.leaveChannel(voiceChannel.guild.id);
} catch {
// no-op
}
connection.removeAllListeners("end");
players.delete(voiceChannel.guild.id);
queues.delete(voiceChannel.guild.id);
logger.error(error);
const content = `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${error.type}: ${error.error}\`\`\``;
logger.error(exception.error);
const content = `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${exception.type}: ${exception.error}\`\`\``;
if (options.type === "classic") {
await client.createMessage(options.channel.id, content);
} else {
@ -203,7 +213,7 @@ export async function nextSong(client, options, connection, track, info, music,
}
queues.set(voiceChannel.guild.id, newQueue);
if (newQueue.length !== 0) {
const newTrack = await Rest.decode(connection.node, newQueue[0]);
const newTrack = await connection.node.rest.decode(newQueue[0]);
nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track);
try {
if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
@ -212,8 +222,7 @@ export async function nextSong(client, options, connection, track, info, music,
// no-op
}
} else if (process.env.STAYVC !== "true") {
await manager.leave(voiceChannel.guild.id);
await connection.destroy();
connection.node.leaveChannel(voiceChannel.guild.id);
players.delete(voiceChannel.guild.id);
queues.delete(voiceChannel.guild.id);
skipVotes.delete(voiceChannel.guild.id);