12 Commits
51 changed files with 6388 additions and 6098 deletions

No files matched your search

+21 -21
View File
@@ -1,21 +1,21 @@
{ {
"root": true, "root": true,
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"warnOnUnsupportedTypeScriptVersion": false, "warnOnUnsupportedTypeScriptVersion": false,
"sourceType": "module" "sourceType": "module"
}, },
"env": { "env": {
"browser": false, "browser": false,
"node": true, "node": true,
"es6": true "es6": true
}, },
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"plugins": ["@typescript-eslint", "import", "simple-import-sort"], "plugins": ["@typescript-eslint", "import", "simple-import-sort"],
"ignorePatterns": ["**/node_modules/**", "/dist/**", "/tests/**"], "ignorePatterns": ["**/node_modules/**", "/dist/**", "/tests/**"],
"rules": { "rules": {
"curly": ["error", "all"], "curly": ["error", "all"],
"simple-import-sort/imports": "error", "simple-import-sort/imports": "error",
"simple-import-sort/exports": "error" "simple-import-sort/exports": "error"
} }
} }
+68 -68
View File
@@ -1,68 +1,68 @@
name: actions name: actions
on: on:
push: push:
tags: tags:
- "*" - "*"
jobs: jobs:
mocha: mocha:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [18.x] node-version: [18.x]
pnpm-version: [8] pnpm-version: [8]
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: ${{ matrix.pnpm-version }} version: ${{ matrix.pnpm-version }}
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: ESLint & Prettier - name: ESLint & Prettier
run: pnpm run lint run: pnpm run lint
- name: Build - name: Build
run: pnpm run build run: pnpm run build
- name: Test - name: Test
run: pnpm run --if-present test run: pnpm run --if-present test
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: mocha needs: mocha
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with: with:
images: unknowableshade/quetza-bot images: unknowableshade/quetza-bot
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with: with:
context: . context: .
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+23 -23
View File
@@ -1,23 +1,23 @@
{ {
"semi": true, "semi": true,
"trailingComma": "none", "trailingComma": "none",
"singleQuote": false, "singleQuote": false,
"printWidth": 100, "printWidth": 100,
"arrowParens": "always", "arrowParens": "always",
"bracketSpacing": true, "bracketSpacing": true,
"endOfLine": "crlf", "endOfLine": "lf",
"overrides": [ "overrides": [
{ {
"files": ["**/*.json, **/*.yml"], "files": ["**/*.json, **/*.yml"],
"options": { "options": {
"tabWidth": 2 "tabWidth": 2
} }
}, },
{ {
"files": ["**/*.ts"], "files": ["**/*.ts"],
"options": { "options": {
"tabWidth": 4 "tabWidth": 4
} }
} }
] ]
} }
+8
View File
@@ -1,5 +1,10 @@
FROM node:18-alpine as builder FROM node:18-alpine as builder
RUN apk add --no-cache python3
RUN apk add --no-cache make
RUN apk add --no-cache gcc
RUN apk add --no-cache build-base
RUN npm install -g pnpm RUN npm install -g pnpm
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -19,6 +24,9 @@ RUN apk add --no-cache bash
RUN apk add --no-cache curl RUN apk add --no-cache curl
RUN apk add --no-cache ffmpeg RUN apk add --no-cache ffmpeg
RUN apk add --no-cache python3 RUN apk add --no-cache python3
RUN apk add --no-cache make
RUN apk add --no-cache gcc
RUN apk add --no-cache build-base
RUN npm install -g pnpm RUN npm install -g pnpm
-1
View File
@@ -1 +0,0 @@
worker: npm start
+16 -16
View File
@@ -1,16 +1,16 @@
# Quetza 4 # Quetza 4
_Neat typescript discord bot._ _Neat typescript discord bot._
## About ## About
- Base player features. - Base player features.
- Play music from YouTube and many other sites. - Play music from YouTube and many other sites.
- Lots of different filters _(nightcore, earrape etc.)_. - Lots of different filters _(nightcore, earrape etc.)_.
- Slash commands. - Slash commands.
## Links ## Links
- **[Dependencies](https://github.com/unknowableshade/quetza-bot/blob/master/package.json)** - **[Dependencies](https://github.com/unknowableshade/quetza-bot/blob/master/package.json)**
- **[Docker](https://hub.docker.com/repository/docker/unknowableshade/quetza-bot)** - **[Docker](https://hub.docker.com/repository/docker/unknowableshade/quetza-bot)**
- **[Licence](https://github.com/unknowableshade/5a0b-bot/blob/master/LICENSE)** - **[Licence](https://github.com/unknowableshade/5a0b-bot/blob/master/LICENSE)**
+61 -54
View File
@@ -1,54 +1,61 @@
{ {
"name": "quetza-bot", "name": "quetza-bot",
"engines": { "version": "4.2.2",
"node": "18.x.x", "description": "Discord multipurpose bot",
"npm": "*" "type": "module",
}, "main": "./dist/index.js",
"type": "module", "scripts": {
"version": "4.0.0", "start": "node ./dist/index.js",
"description": "Discord multifunctional bot", "build": "pnpm exec tsc --build",
"author": { "clean": "rm -rf ./dist && pnpm run build",
"name": "unknowableshade", "lint": "pnpm exec eslint . && pnpm exec prettier --check .",
"email": "60275123+unknowableshade@users.noreply.github.com", "format": "pnpm exec eslint --fix . && pnpm exec prettier --write .",
"url": "https://unknowableshade.ru" "preinstall": "pnpm dlx only-allow pnpm"
}, },
"repository": { "license": "MIT",
"type": "git", "keywords": [
"url": "git+https://github.com/unknowableshade/quetza-bot.git" "discord",
}, "bot",
"bugs": { "music"
"url": "https://github.com/unknowableshade/quetza-bot/issues" ],
}, "repository": {
"readme": "https://github.com/unknowableshade/quetza-bot#readme", "type": "git",
"license": "MIT", "url": "git+https://github.com/unknowableshade/quetza-bot.git"
"main": "dist/index.js", },
"scripts": { "author": {
"start": "node dist/index.js", "name": "unknowableshade",
"build": "pnpm exec tsc --build", "email": "60275123+unknowableshade@users.noreply.github.com",
"clean": "pnpm exec tsc --build --clean", "url": "https://unknowableshade.ru"
"lint": "pnpm exec eslint --fix . && pnpm exec prettier --write .", },
"preinstall": "pnpx only-allow pnpm" "bugs": {
}, "url": "https://github.com/unknowableshade/quetza-bot/issues"
"dependencies": { },
"@discordjs/opus": "^0.9.0", "readme": "https://github.com/unknowableshade/quetza-bot#readme",
"@discordjs/voice": "^0.16.0", "engines": {
"@prisma/client": "4.13.0", "node": "18.x.x",
"discord-ytdl-core": "^5.0.4", "npm": "*"
"discord.js": "^14.11.0", },
"tweetnacl": "^1.0.3", "dependencies": {
"winston": "^3.8.2", "@discordjs/opus": "^0.9.0",
"ytdl-core": "^4.11.4" "@discordjs/voice": "^0.16.0",
}, "@prisma/client": "5.0.0",
"devDependencies": { "discord.js": "^14.11.0",
"@types/node": "^18.16.6", "prism-media": "^1.3.5",
"@typescript-eslint/eslint-plugin": "^5.59.5", "tweetnacl": "^1.0.3",
"@typescript-eslint/parser": "^5.59.5", "winston": "^3.10.0",
"eslint": "^8.40.0", "ytdl-core": "^4.11.4"
"eslint-config-prettier": "^8.8.0", },
"eslint-plugin-import": "^2.27.5", "devDependencies": {
"eslint-plugin-simple-import-sort": "^10.0.0", "@types/node": "^20.4.1",
"prettier": "^2.8.8", "@typescript-eslint/eslint-plugin": "^6.0.0",
"prisma": "^4.13.0", "@typescript-eslint/parser": "^6.0.0",
"typescript": "^4.9.5" "eslint": "^8.44.0",
} "eslint-config-prettier": "^8.8.0",
} "eslint-plugin-import": "^2.27.5",
"eslint-plugin-simple-import-sort": "^10.0.0",
"mocha": "^10.2.0",
"prettier": "^3.0.0",
"prisma": "^5.0.0",
"typescript": "^5.1.6"
}
}
+3557 -3218
View File
File diff suppressed because it is too large. Load diff
+26 -26
View File
@@ -1,26 +1,26 @@
import { ActivityOptions, ActivityType, ColorResolvable } from "discord.js"; import { ActivityOptions, ActivityType, ColorResolvable } from "discord.js";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const ROOT = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.dirname(fileURLToPath(import.meta.url));
const discordToken = process.env.DISCORD_TOKEN; const discordToken = process.env.DISCORD_TOKEN;
const modulesDir = path.join(ROOT, "/modules/"); const modulesDir = path.join(ROOT, "/modules/");
const binariesDir = path.join(ROOT, "/../bin/"); const binariesDir = path.join(ROOT, "/../bin/");
const activityStatus: ActivityOptions = { type: ActivityType.Watching, name: "over you" }; const activityStatus: ActivityOptions = { type: ActivityType.Watching, name: "over you" };
const colors = { const colors = {
default: "#ff6e9b" as ColorResolvable, default: "#ff6e9b" as ColorResolvable,
error: "#b94e59" as ColorResolvable, error: "#b94e59" as ColorResolvable,
warning: "#e5925f" as ColorResolvable, warning: "#e5925f" as ColorResolvable,
info: "#367df6" as ColorResolvable, info: "#367df6" as ColorResolvable,
success: "#83d18e" as ColorResolvable success: "#83d18e" as ColorResolvable
}; };
const config = { const config = {
discordToken, discordToken,
modulesDir, modulesDir,
binariesDir, binariesDir,
activityStatus, activityStatus,
colors colors
}; };
export default config; export default config;
+14 -14
View File
@@ -1,14 +1,14 @@
import { GatewayIntentBits } from "discord.js"; import { GatewayIntentBits } from "discord.js";
import config from "./config.js"; import config from "./config.js";
import Client from "./lib/client.js"; import Client from "./lib/client.js";
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMembers GatewayIntentBits.GuildMembers
] ]
}); });
client.login(config.discordToken); client.login(config.discordToken);
+91 -88
View File
@@ -1,88 +1,91 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { Client as DiscordClient, ClientOptions, Collection } from "discord.js"; import { Client as DiscordClient, ClientOptions, Collection } from "discord.js";
import { existsSync, lstatSync, readdirSync } from "fs"; import { existsSync, lstatSync, readdirSync } from "fs";
import path from "path"; import path from "path";
import { pathToFileURL } from "url"; import { pathToFileURL } from "url";
import config from "../config.js"; import config from "../config.js";
import { Command, Event, Module, ModuleMetadata } from "./types.js"; import { Command, Event, Module, ModuleMetadata } from "./types.js";
const MODULE_ENTRY = "module.js"; const MODULE_ENTRY = "module.js";
const MODULE_COMMANDS = "commands"; const MODULE_COMMANDS = "commands";
const MODULE_EVENTS = "events"; const MODULE_EVENTS = "events";
export default class Client extends DiscordClient { export default class Client extends DiscordClient {
public readonly commands = new Collection<string, Command>(); public readonly commands = new Collection<string, Command>();
public readonly modules = new Collection<string, ModuleMetadata>(); public readonly modules = new Collection<string, ModuleMetadata>();
public readonly db = new PrismaClient(); public readonly db = new PrismaClient();
public constructor(options: ClientOptions) { public constructor(options: ClientOptions) {
super(options); super(options);
for (const module of readdirSync(config.modulesDir)) { for (const module of readdirSync(config.modulesDir)) {
const modulePath = path.join(config.modulesDir, module); const modulePath = path.join(config.modulesDir, module);
if (!lstatSync(modulePath).isDirectory()) { if (!lstatSync(modulePath).isDirectory()) {
continue; continue;
} }
import(pathToFileURL(path.join(modulePath, MODULE_ENTRY)).toString()).then( import(pathToFileURL(path.join(modulePath, MODULE_ENTRY)).toString()).then(
(data: Module) => this.loadModule(data) (data: Module) => this.loadModule(data)
); );
} }
} }
private loadModule(data: Module): void { private loadModule(data: Module): void {
const commands: string[] = []; const commands: string[] = [];
const events: string[] = []; const events: string[] = [];
function importModule(dir: string, importThen: (data: Command & Event) => void) { function importModule(dir: string, importThen: (data: Command & Event) => void) {
if (existsSync(dir)) { if (existsSync(dir)) {
for (const source of readdirSync(dir)) { for (const source of readdirSync(dir)) {
const file = path.join(dir, source); const file = path.join(dir, source);
if (!lstatSync(file).isFile() || !file.endsWith(".js")) { if (!lstatSync(file).isFile() || !file.endsWith(".js")) {
continue; continue;
} }
import(pathToFileURL(file).toString()).then(importThen); import(pathToFileURL(file).toString()).then(importThen);
} }
} }
} }
importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => { importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => {
this.commands.set(command.data.name, command); this.commands.set(command.data.name, command);
commands.push(command.data.name); commands.push(command.data.name);
}); });
importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => { importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
this.on(event.name, (...eventee: unknown[]) => event.execute(this, eventee)); this.on(
events.push(event.name); event.name,
}); async (...eventee: unknown[]) => await event.execute(this, eventee)
);
this.modules.set(data.name, { events.push(event.name);
...data, });
commands,
events this.modules.set(data.name, {
}); ...data,
} commands,
events
public generateApplicationStatus(): string[] { });
const modulesStatus = this.modules.map((module) => { }
const tag = `✓ ${module.name}@${module.tag} module is loaded.`;
const commands = module.commands.map((name) => " - " + name).join("\n"); public generateApplicationStatus(): string[] {
const events = module.events.map((name) => " - " + name).join("\n"); const modulesStatus = this.modules.map((module) => {
const tag = `✓ ${module.name}@${module.tag} module is loaded.`;
return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n"); const commands = module.commands.map((name) => " - " + name).join("\n");
}); const events = module.events.map((name) => " - " + name).join("\n");
const clientStatus = return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n");
this.user && this.application });
? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
: "✖ Application is not availiable."; const clientStatus =
this.user && this.application
return modulesStatus.concat(clientStatus); ? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
} : "✖ Application is not availiable.";
}
return modulesStatus.concat(clientStatus);
}
}
+13 -20
View File
@@ -1,20 +1,13 @@
import winston from "winston"; import winston from "winston";
const logger = winston.createLogger({ const logger = winston.createLogger({
level: "info", level: "info",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()), format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [ transports: [
new winston.transports.File({ filename: "logs/error.log", level: "error" }), new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: "logs/combined.log" }) new winston.transports.File({ filename: "logs/error.log", level: "error" }),
] new winston.transports.File({ filename: "logs/combined.log" })
}); ]
});
if (process.env.NODE_ENV !== "production") {
logger.add( export default logger;
new winston.transports.Console({
format: winston.format.simple()
})
);
}
export default logger;
+27 -27
View File
@@ -1,27 +1,27 @@
import { ApplicationCommandData, CommandInteraction } from "discord.js"; import { ApplicationCommandData, CommandInteraction } from "discord.js";
import Client from "./client.js"; import Client from "./client.js";
export interface Module { export interface Module {
name: string; name: string;
description?: string; description?: string;
author: string; author: string;
tag: string; tag: string;
controller?: unknown; controller?: unknown;
rootDir: string; rootDir: string;
} }
export interface ModuleMetadata extends Module { export interface ModuleMetadata extends Module {
commands: string[]; commands: string[];
events: string[]; events: string[];
} }
export interface Command { export interface Command {
data: ApplicationCommandData; data: ApplicationCommandData;
execute: (client: Client, interaction: CommandInteraction) => Promise<void>; execute: (client: Client, interaction: CommandInteraction) => Promise<void>;
} }
export interface Event { export interface Event {
name: string; name: string;
execute: (client: Client, eventee: unknown[]) => Promise<void>; execute: (client: Client, eventee: unknown[]) => Promise<void>;
} }
+21 -21
View File
@@ -1,21 +1,21 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand()) { if (!interaction.isChatInputCommand()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
await interaction.reply(replies.modules(client.modules)); await interaction.reply(replies.modules(client.modules));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("modules") .setName("modules")
.setDescription("List of availiable modules."); .setDescription("List of availiable modules.");
export { data, execute }; export { data, execute };
+23 -23
View File
@@ -1,23 +1,23 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand()) { if (!interaction.isChatInputCommand()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const reply = await interaction.reply({ ...replies.ping(client.ws.ping), fetchReply: true }); const reply = await interaction.reply({ ...replies.ping(client.ws.ping), fetchReply: true });
await interaction.editReply( await interaction.editReply(
replies.ping(client.ws.ping, reply.createdTimestamp - interaction.createdTimestamp) replies.ping(client.ws.ping, reply.createdTimestamp - interaction.createdTimestamp)
); );
} }
const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me."); const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me.");
export { data, execute }; export { data, execute };
+32 -22
View File
@@ -1,22 +1,32 @@
import { Events, Interaction } from "discord.js"; import { Events, Interaction } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
async function execute(client: Client, eventee: [Interaction]): Promise<void> { async function execute(client: Client, eventee: [Interaction]): Promise<void> {
const [interaction] = eventee; const [interaction] = eventee;
logger.info("Interaction created.", interaction); if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
if (interaction.isCommand()) {
const command = client.commands.get(interaction.commandName); if (!command) {
logger.error(`No command matching ${interaction.commandName} was found.`, {
if (command) { interaction
command.execute(client, interaction); });
}
} return;
} }
const name = Events.InteractionCreate; try {
logger.info(`Interaction ${interaction.commandName} was created.`, interaction);
export { execute, name };
await command.execute(client, interaction);
} catch (error) {
logger.error("Interaction error occured. ", { error, interaction });
}
}
}
const name = Events.InteractionCreate;
export { execute, name };
+37 -37
View File
@@ -1,37 +1,37 @@
import { generateDependencyReport } from "@discordjs/voice"; import { generateDependencyReport } from "@discordjs/voice";
import { Events } from "discord.js"; import { Events } from "discord.js";
import config from "../../../config.js"; import config from "../../../config.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
async function execute(client: Client): Promise<void> { async function execute(client: Client): Promise<void> {
if (!client.user || !client.application) { if (!client.user || !client.application) {
throw new Error("No application or user for it is provided."); throw new Error("No application or user for it is provided.");
} }
const commandData = Array.from(client.commands.values()).map((value) => value.data); const commandData = Array.from(client.commands.values()).map((value) => value.data);
if (process.env.NODE_ENV === "development" && process.env.TEST_GUILD) { if (process.env.NODE_ENV === "development" && process.env.TEST_GUILD) {
logger.info("Development mode: Commands will be pushed to the test guild!"); logger.info("Development mode: Commands will be pushed to the test guild!");
logger.info(generateDependencyReport()); logger.info(generateDependencyReport());
const guild = await client.guilds.fetch(process.env.TEST_GUILD); const guild = await client.guilds.fetch(process.env.TEST_GUILD);
guild.commands.set(commandData); guild.commands.set(commandData);
} }
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
logger.info("Production mode: Commands will be pushed to the application!"); logger.info("Production mode: Commands will be pushed to the application!");
await client.application.commands.set(commandData); await client.application.commands.set(commandData);
} }
client.generateApplicationStatus().forEach((report) => logger.info(report)); client.generateApplicationStatus().forEach((report) => logger.info(report));
client.user.setActivity(config.activityStatus); client.user.setActivity(config.activityStatus);
} }
const name = Events.ClientReady; const name = Events.ClientReady;
export { execute, name }; export { execute, name };
+36 -36
View File
@@ -1,36 +1,36 @@
import { BaseMessageOptions, Collection, EmbedBuilder, italic } from "discord.js"; import { BaseMessageOptions, Collection, EmbedBuilder, italic } from "discord.js";
import config from "../../../config.js"; import config from "../../../config.js";
import { ModuleMetadata } from "../../../lib/types.js"; import { ModuleMetadata } from "../../../lib/types.js";
const replies = { const replies = {
modules: (modules: Collection<string, ModuleMetadata>): BaseMessageOptions => { modules: (modules: Collection<string, ModuleMetadata>): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setTitle("Applied modules") .setTitle("Applied modules")
.addFields( .addFields(
modules.map((value) => { modules.map((value) => {
return { return {
name: value.name + "@" + value.tag, name: value.name + "@" + value.tag,
value: italic(value.description ?? "") value: italic(value.description ?? "")
}; };
}) })
); );
return { embeds: [embed] }; return { embeds: [embed] };
}, },
ping: (websocket: number, roundtrip?: number): BaseMessageOptions => { ping: (websocket: number, roundtrip?: number): BaseMessageOptions => {
const title = roundtrip ? `I've pinged ${roundtrip} ms. late` : "Pinging..."; const title = roundtrip ? `I've pinged ${roundtrip} ms. late` : "Pinging...";
const comment = `Websocket ping is ${websocket} ms.`; const comment = `Websocket ping is ${websocket} ms.`;
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setTitle(title) .setTitle(title)
.setDescription(italic(comment)); .setDescription(italic(comment));
return { embeds: [embed] }; return { embeds: [embed] };
} }
}; };
export default replies; export default replies;
+11 -11
View File
@@ -1,11 +1,11 @@
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const name = "core"; const name = "core";
const description = "Quetza core functionality"; const description = "Quetza core functionality";
const author = "unknowableshade"; const author = "unknowableshade";
const tag = "4.2.0"; const tag = "4.2.2";
const rootDir = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.dirname(fileURLToPath(import.meta.url));
export { author, description, name, rootDir, tag }; export { author, description, name, rootDir, tag };
+35 -35
View File
@@ -1,35 +1,35 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
const amount = player.queue.tracks.length; const amount = player.queue.tracks.length;
player.clear(); player.clear();
await interaction.reply(replies.cleared(amount)); await interaction.reply(replies.cleared(amount));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("clear") .setName("clear")
.setDescription("Clear the player's queue.") .setDescription("Clear the player's queue.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+43 -43
View File
@@ -1,43 +1,43 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
await interaction.deferReply(); await interaction.deferReply();
const player = const player =
controller.get(interaction.guild, interaction.channel) ?? controller.get(interaction.guild, interaction.channel) ??
controller.set(interaction.guild, interaction.channel); controller.set(interaction.guild, interaction.channel);
const channel = interaction.member.voice.channel; const channel = interaction.member.voice.channel;
if (!channel) { if (!channel) {
controller.delete(interaction.guildId); controller.delete(interaction.guildId);
await interaction.editReply(replies.notConnected()); await interaction.editReply(replies.notConnected());
return; return;
} }
player.connect(channel); player.connect(channel);
await interaction.editReply(replies.okConnected(channel.name)); await interaction.editReply(replies.okConnected(channel.name));
return; return;
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("connect") .setName("connect")
.setDescription("Connect me explicitly to the voice channel.") .setDescription("Connect me explicitly to the voice channel.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+46 -46
View File
@@ -1,46 +1,46 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import Filter from "../lib/filter.js"; import Filter from "../lib/filter.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const filter = interaction.options.getString("filter") ?? undefined; const filter = interaction.options.getString("filter") ?? undefined;
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
const status = player.setFilter(filter); const status = player.setFilter(filter);
await interaction.reply(replies.filtered(filter, status)); await interaction.reply(replies.filtered(filter, status));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("filter") .setName("filter")
.setDescription("Apply unique filters to the playback.") .setDescription("Apply unique filters to the playback.")
.addStringOption((option) => .addStringOption((option) =>
option option
.setName("filter") .setName("filter")
.setDescription("Filter to apply.") .setDescription("Filter to apply.")
.setChoices( .setChoices(
...Object.keys(Filter.filters).map((key) => { ...Object.keys(Filter.filters).map((key) => {
return { name: key, value: key }; return { name: key, value: key };
}) })
) )
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+31 -31
View File
@@ -1,31 +1,31 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
await interaction.reply(replies.info(player)); await interaction.reply(replies.info(player));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("info") .setName("info")
.setDescription("Verbose information about player and stats.") .setDescription("Verbose information about player and stats.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+44 -44
View File
@@ -1,44 +1,44 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const pos = interaction.options.getNumber("position", true); const query = interaction.options.getString("query", true);
const query = interaction.options.getString("query", true); const pos = interaction.options.getInteger("position", true);
await interaction.deferReply(); await interaction.deferReply();
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.editReply(replies.notExists()); await interaction.editReply(replies.notExists());
return; return;
} }
const track = await player.add(query, interaction.user, pos - 1); const track = await player.add(query, interaction.user, pos - 1);
await interaction.editReply(replies.appended(track)); await interaction.editReply(replies.appended(track));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("insert") .setName("insert")
.setDescription("Insert a new track to specific position in the queue.") .setDescription("Insert a new track to specific position in the queue.")
.addStringOption((option) => .addStringOption((option) =>
option.setName("query").setDescription("Title or URL of the song.").setRequired(true) option.setName("query").setDescription("Title or URL of the song.").setRequired(true)
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option.setName("position").setDescription("Position to insert.").setRequired(true) option.setName("position").setDescription("Position to insert.").setRequired(true)
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+43 -43
View File
@@ -1,43 +1,43 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const query = interaction.options.getString("query", true); const query = interaction.options.getString("query", true);
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
const previous = player.resource?.metadata; const previous = player.resource?.metadata;
const position = const position =
!isNaN(+query) && isFinite(+query) && !/e/i.test(query) !isNaN(+query) && isFinite(+query) && !/e/i.test(query)
? player.jump(parseInt(query) - 1) ? player.jump(parseInt(query) - 1)
: player.jump(query); : player.jump(query);
await interaction.reply(replies.jumped(previous, position)); await interaction.reply(replies.jumped(previous, position));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("jump") .setName("jump")
.setDescription("Jump to specific track in the queue.") .setDescription("Jump to specific track in the queue.")
.addStringOption((option) => .addStringOption((option) =>
option.setName("query").setDescription("Position or title to jump to.").setRequired(true) option.setName("query").setDescription("Position or title to jump to.").setRequired(true)
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+48 -48
View File
@@ -1,48 +1,48 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { LoopOption } from "../lib/types.js"; import { LoopOption } from "../lib/types.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const option = interaction.options.getString("option", true) as LoopOption; const option = interaction.options.getString("option", true) as LoopOption;
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
player.queue.loop = option; player.queue.loop = option;
await interaction.reply(replies.looped(option)); await interaction.reply(replies.looped(option));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("loop") .setName("loop")
.setDescription("Select player's loop mode.") .setDescription("Select player's loop mode.")
.addStringOption((option) => .addStringOption((option) =>
option option
.setName("option") .setName("option")
.setDescription("Loop option.") .setDescription("Loop option.")
.setChoices( .setChoices(
{ name: "Loop queue", value: "LOOP" }, { name: "Loop queue", value: "LOOP" },
{ name: "Loop single track", value: "SONG" }, { name: "Loop single track", value: "SONG" },
{ name: "Drop on final", value: "NONE" }, { name: "Drop on final", value: "NONE" },
{ name: "Random", value: "AUTO" } { name: "Random", value: "AUTO" }
) )
.setRequired(true) .setRequired(true)
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+33 -33
View File
@@ -1,33 +1,33 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
player.skip(); player.skip();
await interaction.reply(replies.skipped()); await interaction.reply(replies.skipped());
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("next") .setName("next")
.setDescription("Skip current song.") .setDescription("Skip current song.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+33 -33
View File
@@ -1,33 +1,33 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
const state = player.pause(); const state = player.pause();
await interaction.reply(replies.paused(state)); await interaction.reply(replies.paused(state));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("pause") .setName("pause")
.setDescription("Toggle pause on playback.") .setDescription("Toggle pause on playback.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+58 -58
View File
@@ -1,58 +1,58 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const query = interaction.options.getString("query"); const query = interaction.options.getString("query");
if (query) { if (query) {
await interaction.reply(replies.searching()); await interaction.reply(replies.searching());
} else { } else {
await interaction.deferReply(); await interaction.deferReply();
} }
const player = const player =
controller.get(interaction.guild, interaction.channel) ?? controller.get(interaction.guild, interaction.channel) ??
controller.set(interaction.guild, interaction.channel); controller.set(interaction.guild, interaction.channel);
const channel = interaction.member.voice.channel; const channel = interaction.member.voice.channel;
if (!channel) { if (!channel) {
controller.delete(interaction.guildId); controller.delete(interaction.guildId);
await interaction.editReply(replies.notConnected()); await interaction.editReply(replies.notConnected());
return; return;
} }
player.connect(channel); player.connect(channel);
if (query) { if (query) {
const track = await player.add(query, interaction.user); const track = await player.add(query, interaction.user);
await interaction.editReply(replies.appended(track)); await interaction.editReply(replies.appended(track));
} }
if (!player.resource) { if (!player.resource) {
player.play(); await player.play();
} }
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("play") .setName("play")
.setDescription("Launch player and add songs to the queue.") .setDescription("Launch player and add songs to the queue.")
.addStringOption((option) => .addStringOption((option) =>
option.setName("query").setDescription("Title or URL of the song.") option.setName("query").setDescription("Title or URL of the song.")
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+86 -86
View File
@@ -1,86 +1,86 @@
import { import {
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
ButtonInteraction, ButtonInteraction,
ButtonStyle, ButtonStyle,
Interaction, ChatInputCommandInteraction,
MessageActionRowComponentBuilder, MessageActionRowComponentBuilder,
SlashCommandBuilder SlashCommandBuilder
} from "discord.js"; } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
await interaction.deferReply(); await interaction.deferReply();
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.editReply(replies.notExists()); await interaction.editReply(replies.notExists());
return; return;
} }
if (player.queue.empty()) { if (player.queue.empty()) {
await interaction.editReply(replies.queue(player.queue)); await interaction.editReply(replies.queue(player.queue));
return; return;
} }
let position = 1; let position = 1;
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents( const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
new ButtonBuilder().setCustomId("Top").setLabel("🔼").setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId("Top").setLabel("🔼").setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId("Up").setLabel("🔺").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId("Up").setLabel("🔺").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("Down").setLabel("🔻").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId("Down").setLabel("🔻").setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId("End").setLabel("🔽").setStyle(ButtonStyle.Primary) new ButtonBuilder().setCustomId("End").setLabel("🔽").setStyle(ButtonStyle.Primary)
); );
const message = await interaction.editReply({ const message = await interaction.editReply({
...replies.queue(player.queue, 0, player.resource), ...replies.queue(player.queue, 0, player.resource),
components: [row] components: [row]
}); });
const collector = message.createMessageComponentCollector({ time: 30000 }); const collector = message.createMessageComponentCollector({ time: 30000 });
collector.on("collect", async (btn: ButtonInteraction) => { collector.on("collect", async (btn: ButtonInteraction) => {
switch (btn.customId) { switch (btn.customId) {
case "Top": { case "Top": {
position = 1; position = 1;
break; break;
} }
case "Up": { case "Up": {
position = Math.max(position - 10, 1); position = Math.max(position - 10, 1);
break; break;
} }
case "Down": { case "Down": {
position = Math.min( position = Math.min(
position + 10, position + 10,
Math.floor(player.queue.tracks.length / 10) * 10 Math.floor(player.queue.tracks.length / 10) * 10
); );
break; break;
} }
case "End": { case "End": {
position = Math.floor(player.queue.tracks.length / 10) * 10; position = Math.floor(player.queue.tracks.length / 10) * 10;
} }
} }
await btn.update(replies.queue(player.queue, position - 1, player.resource)); await btn.update(replies.queue(player.queue, position - 1, player.resource));
}); });
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("queue") .setName("queue")
.setDescription("Print interactive queue pointing to current track.") .setDescription("Print interactive queue pointing to current track.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+41 -41
View File
@@ -1,41 +1,41 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const query = interaction.options.getString("query", true); const query = interaction.options.getString("query", true);
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
const track = const track =
!isNaN(+query) && isFinite(+query) && !/e/i.test(query) !isNaN(+query) && isFinite(+query) && !/e/i.test(query)
? player.remove(parseInt(query) - 1) ? player.remove(parseInt(query) - 1)
: player.remove(query); : player.remove(query);
await interaction.reply(replies.removed(track)); await interaction.reply(replies.removed(track));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("remove") .setName("remove")
.setDescription("Remove specific track in the queue.") .setDescription("Remove specific track in the queue.")
.addStringOption((option) => .addStringOption((option) =>
option.setName("query").setDescription("Position or title to remove.").setRequired(true) option.setName("query").setDescription("Position or title to remove.").setRequired(true)
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+33 -33
View File
@@ -1,33 +1,33 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
player.shuffle(); player.shuffle();
await interaction.reply(replies.reshuffle()); await interaction.reply(replies.reshuffle());
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("reshuffle") .setName("reshuffle")
.setDescription("Reshuffle the queue. (Will change queue order)") .setDescription("Reshuffle the queue. (Will change queue order)")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+50 -50
View File
@@ -1,50 +1,50 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const hours = interaction.options.getInteger("hrs") ?? 0; const hours = interaction.options.getInteger("hrs") ?? 0;
const minutes = interaction.options.getInteger("mins") ?? 0; const minutes = interaction.options.getInteger("mins") ?? 0;
const seconds = interaction.options.getInteger("secs") ?? 0; const seconds = interaction.options.getInteger("secs") ?? 0;
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
if (!player.resource) { if (!player.resource) {
await interaction.reply(replies.notPlaying()); await interaction.reply(replies.notPlaying());
return; return;
} }
const prevTime = player.resource.playbackDuration / 1000; const prevTime = player.resource.playbackDuration / 1000;
const nextTime = hours * 3600 + minutes * 60 + seconds; const nextTime = hours * 3600 + minutes * 60 + seconds;
player.seek(nextTime * 1000); player.seek(nextTime * 1000);
await interaction.reply(replies.seeked(prevTime, nextTime)); await interaction.reply(replies.seeked(prevTime, nextTime));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("seek") .setName("seek")
.setDescription("Fast-forward/Fast-reverse by the given time.") .setDescription("Fast-forward/Fast-reverse by the given time.")
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek.")) .addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek.")) .addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek.")) .addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."))
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+33 -33
View File
@@ -1,33 +1,33 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
player.destroy(); player.destroy();
await interaction.reply(replies.destroyed()); await interaction.reply(replies.destroyed());
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("stop") .setName("stop")
.setDescription("Destroy the player.") .setDescription("Destroy the player.")
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+38 -38
View File
@@ -1,38 +1,38 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const volume = interaction.options.getInteger("volume") ?? 100; const volume = interaction.options.getInteger("volume") ?? 100;
const player = controller.get(interaction.guild, interaction.channel); const player = controller.get(interaction.guild, interaction.channel);
if (!player) { if (!player) {
await interaction.reply(replies.notExists()); await interaction.reply(replies.notExists());
return; return;
} }
player.volume = volume; player.volume = volume;
await interaction.reply(replies.volumeSet(player.volume)); await interaction.reply(replies.volumeSet(player.volume));
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("volume") .setName("volume")
.setDescription("Change player's volume amount.") .setDescription("Change player's volume amount.")
.addIntegerOption((option) => .addIntegerOption((option) =>
option.setName("volume").setDescription("Volume in percents.").setRequired(true) option.setName("volume").setDescription("Volume in percents.").setRequired(true)
) )
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+36 -36
View File
@@ -1,36 +1,36 @@
import { VoiceState } from "discord.js"; import { VoiceState } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import { asleep } from "../lib/misc.js"; import { asleep } from "../lib/misc.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
const WAIT_TIME = 30_000; const WAIT_TIME = 30_000;
async function execute(client: Client, eventee: [VoiceState]): Promise<void> { async function execute(client: Client, eventee: [VoiceState]): Promise<void> {
const [oldState] = eventee; const [oldState] = eventee;
const player = controller.get(oldState.guild); const player = controller.get(oldState.guild);
if (!player || !oldState.channel || !client.user) { if (!player || !oldState.channel || !client.user) {
return; return;
} }
if ( if (
oldState.channel.members.size === 1 && oldState.channel.members.size === 1 &&
oldState.channel.members.first()?.user === client.user oldState.channel.members.first()?.user === client.user
) { ) {
await asleep(WAIT_TIME); await asleep(WAIT_TIME);
if ( if (
oldState.channel.members.size === 1 && oldState.channel.members.size === 1 &&
oldState.channel.members.first()?.user === client.user oldState.channel.members.first()?.user === client.user
) { ) {
await player.channel.send(replies.alone()); await player.channel.send(replies.alone());
player.destroy(); player.destroy();
} }
} }
} }
const name = "voiceStateUpdate"; const name = "voiceStateUpdate";
export { execute, name }; export { execute, name };
+245 -102
View File
@@ -1,102 +1,245 @@
import { execFile } from "child_process"; import { execFile } from "child_process";
import path from "path"; import path from "path";
import { promisify } from "util"; import prismMedia from "prism-media";
import { Duplex, Readable } from "stream";
import config from "../../../config.js"; import { promisify } from "util";
import logger from "../../../lib/logger.js"; import ytdl from "ytdl-core";
const execFileAsync = promisify(execFile); import config from "../../../config.js";
import logger from "../../../lib/logger.js";
const SEARCH_DEPTH = 5; import { StreamOptions, YtdlDump, YtdlResult, YtdlStreamOptions } from "./types.js";
function isURL(url: string): boolean { const { opus: Opus, FFmpeg } = prismMedia;
return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test( const execFileAsync = promisify(execFile);
url
); const SEARCH_DEPTH = 5;
} const YTDL_EVENTS = [
"info",
interface YtdlDump { "progress",
id: string; "abort",
original_url: string; "request",
title?: string; "response",
uploader?: string; "error",
duration?: number; "redirect",
extractor_key: string; "retry",
thumbnail?: string; "reconnect"
} ];
interface YtdlResult { function isURL(url: string): boolean {
url: string; return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test(
title: string; url
thumbnail: string; );
duration: number; }
}
async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> {
async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> { if (depth === SEARCH_DEPTH) {
if (depth === SEARCH_DEPTH) { logger.warn("yt-dlp failed to find anything.", { query });
logger.warn("yt-dlp failed to find anything.", { query });
return [];
return []; }
}
const executable = await execFileAsync(path.join(config.binariesDir, "/yt-dlp"), [
const executable = await execFileAsync(path.join(config.binariesDir, "/yt-dlp"), [ query,
query, "-O %(.{id,original_url,title,uploader,duration,extractor_key,thumbnail})j",
"-O %(.{id,original_url,title,uploader,duration,extractor_key,thumbnail})j", "-s",
"-s", "--flat-playlist"
"--flat-playlist" ]);
]);
const dump: YtdlDump[] = JSON.parse("[" + executable.stdout.replace(/\n(?=.+)/g, ",") + "]");
const dump: YtdlDump[] = JSON.parse("[" + executable.stdout.replace(/\n(?=.+)/g, ",") + "]");
const result: YtdlResult[] = [];
const result: YtdlResult[] = [];
for (const entry of dump) {
for (const entry of dump) { if (entry.title && entry.thumbnail && entry.duration && entry.extractor_key === "Youtube") {
if (entry.title && entry.thumbnail && entry.duration && entry.extractor_key === "Youtube") { result.push({
result.push({ url: entry.original_url,
url: entry.original_url, title: entry.title,
title: entry.title, thumbnail: entry.thumbnail,
thumbnail: entry.thumbnail, duration: entry.duration
duration: entry.duration });
});
logger.info("yt-dlp has found corresponding youtube video.", {
logger.info("yt-dlp has found corresponding youtube video.", { result: result[result.length - 1]
result: result[result.length - 1] });
});
continue;
continue; }
}
let unwind: YtdlResult[];
let unwind: YtdlResult[];
if (entry.extractor_key !== "Youtube" && entry.title) {
if (entry.extractor_key !== "Youtube" && entry.title) { unwind = await fetchInfoRecursive(
unwind = await fetchInfoRecursive( `ytsearch1:${entry.uploader ? entry.uploader + " - " : ""} ${entry.title}`,
`ytsearch1:${entry.uploader ? entry.uploader + " - " : ""} ${entry.title}`, depth + 1
depth + 1 );
); } else {
} else { unwind = await fetchInfoRecursive(entry.original_url, depth + 1);
unwind = await fetchInfoRecursive(entry.original_url, depth + 1); }
}
if (unwind.length !== 0) {
if (unwind.length !== 0) { result.push(unwind[0]);
result.push(unwind[0]); }
} }
}
return result;
return result; }
}
export async function fetchInfo(query: string): Promise<YtdlResult[]> {
export async function fetchInfo(query: string): Promise<YtdlResult[]> { logger.info("yt-dlp was invoked.", { query });
logger.info("yt-dlp was invoked.", { query });
try {
try { if (!isURL(query)) {
if (!isURL(query)) { return await fetchInfoRecursive("ytsearch1:" + query);
return await fetchInfoRecursive("ytsearch1:" + query); }
}
return await fetchInfoRecursive(query);
return await fetchInfoRecursive(query); } catch (error) {
} catch (error) { logger.error("yt-dlp execution invoked error.", { error });
logger.error("yt-dlp execution invoked error.", { error });
return [];
return []; }
} }
}
export function ytdlStream(url: string, options?: YtdlStreamOptions) {
logger.info("Created ytdl stream.", { url, options });
let ffmpegArgs: string[] = [
"-analyzeduration",
"0",
"-loglevel",
"0",
"-f",
`${options?.fmt ? options.fmt : "s16le"}`,
"-ar",
"48000",
"-ac",
"2"
];
if (options?.seek) {
ffmpegArgs.unshift("-ss", options.seek.toString());
}
if (options?.encoderArgs) {
ffmpegArgs = ffmpegArgs.concat(options.encoderArgs);
}
const transcoder = new FFmpeg({
args: ffmpegArgs
});
const inputStream = ytdl(url, options);
const transcoderStream = inputStream.pipe(transcoder);
if (options && !options.opusEncoded) {
YTDL_EVENTS.forEach((event) =>
inputStream.on(event, (...args) => transcoderStream.emit(event, ...args))
);
inputStream.on("error", () => transcoder.destroy());
transcoderStream.on("close", () => transcoder.destroy());
return transcoderStream;
}
const opus = new Opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960
});
const outputStream = transcoderStream.pipe(opus);
transcoderStream.on("error", (e) => outputStream.emit("error", e));
YTDL_EVENTS.forEach((event) =>
inputStream.on(event, (...args) => outputStream.emit(event, ...args))
);
outputStream.on("close", () => {
transcoder.destroy();
opus.destroy();
});
return outputStream;
}
export function arbitraryStream(stream: string | Readable | Duplex, options?: StreamOptions) {
logger.info("Created arbitrary stream.", { stream, options });
let ffmpegArgs: string[];
if (typeof stream === "string") {
ffmpegArgs = [
"-reconnect",
"1",
"-reconnect_streamed",
"1",
"-reconnect_delay_max",
"5",
"-i",
stream,
"-analyzeduration",
"0",
"-loglevel",
"0",
"-f",
`${options?.fmt ? options.fmt : "s16le"}`,
"-ar",
"48000",
"-ac",
"2"
];
} else {
ffmpegArgs = [
"-analyzeduration",
"0",
"-loglevel",
"0",
"-f",
`${options?.fmt ? options.fmt : "s16le"}`,
"-ar",
"48000",
"-ac",
"2"
];
}
if (options?.seek) {
ffmpegArgs.unshift("-ss", options.seek.toString());
}
if (options?.encoderArgs) {
ffmpegArgs = ffmpegArgs.concat(options.encoderArgs);
}
let transcoder = new FFmpeg({
args: ffmpegArgs
});
if (typeof stream !== "string") {
transcoder = stream.pipe(transcoder);
stream.on("error", () => transcoder.destroy());
}
if (options && !options.opusEncoded) {
transcoder.on("close", () => transcoder.destroy());
return transcoder;
}
const opus = new Opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960
});
const outputStream = transcoder.pipe(opus);
outputStream.on("close", () => {
transcoder.destroy();
opus.destroy();
});
return outputStream;
}
+66 -66
View File
@@ -1,66 +1,66 @@
export default class Filter { export default class Filter {
public static readonly filters = { public static readonly filters = {
"Bassboost 1.5": "bass=g=15:f=110:w=0.3", "Bassboost 1.5": "bass=g=15:f=110:w=0.3",
"bassboost 2.0": "bass=g=20:f=110:w=0.3", "bassboost 2.0": "bass=g=20:f=110:w=0.3",
"bassboost 3.0": "bass=g=30:f=110:w=0.3", "bassboost 3.0": "bass=g=30:f=110:w=0.3",
"8D": "apulsator=hz=0.09", "8D": "apulsator=hz=0.09",
Daycore: "aresample=48000,asetrate=48000*0.75", Daycore: "aresample=48000,asetrate=48000*0.75",
Nightcore: "aresample=48000,asetrate=48000*1.25", Nightcore: "aresample=48000,asetrate=48000*1.25",
Phaser: "aphaser=in_gain=0.4", Phaser: "aphaser=in_gain=0.4",
Tremolo: "tremolo", Tremolo: "tremolo",
Vibrato: "vibrato=f=6.5", Vibrato: "vibrato=f=6.5",
Reverse: "areverse", Reverse: "areverse",
Treble: "treble=g=5", Treble: "treble=g=5",
"Normalizer 1": "dynaudnorm=g=101", "Normalizer 1": "dynaudnorm=g=101",
"Normalizer 2": "acompressor", "Normalizer 2": "acompressor",
"Surround Sound": "surround", "Surround Sound": "surround",
Pulsator: "apulsator=hz=1", Pulsator: "apulsator=hz=1",
Subboost: "asubboost", Subboost: "asubboost",
Karaoke: "stereotools=mlev=0.03", Karaoke: "stereotools=mlev=0.03",
Compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6", Compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6",
Expander: Expander:
"compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3", "compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3",
"Soft Limiter": "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8", "Soft Limiter": "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8",
Chorus: "chorus=0.7:0.9:55:0.4:0.25:2", Chorus: "chorus=0.7:0.9:55:0.4:0.25:2",
"Fade In": "afade=t=in:ss=0:d=10", "Fade In": "afade=t=in:ss=0:d=10",
Dimming: Dimming:
"afftfilt=\"'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'\"", "afftfilt=\"'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'\"",
Earrape: "channelsplit,sidechaingate=level_in=64" Earrape: "channelsplit,sidechaingate=level_in=64"
}; };
private switches_ = new Set<string>(); private switches_ = new Set<string>();
public toggled(): [string, string][] { public toggled(): [string, string][] {
return Object.entries(Filter.filters) return Object.entries(Filter.filters)
.filter((value) => this.switches_.has(value[0])) .filter((value) => this.switches_.has(value[0]))
.map((value) => [value[0], value[1]]); .map((value) => [value[0], value[1]]);
} }
public toggle(query?: string): boolean { public toggle(query?: string): boolean {
if (!query) { if (!query) {
this.switches_.clear(); this.switches_.clear();
return false; return false;
} }
if (!this.switches_.delete(query) && Object.hasOwn(Filter.filters, query)) { if (!this.switches_.delete(query) && Object.hasOwn(Filter.filters, query)) {
this.switches_.add(query); this.switches_.add(query);
return true; return true;
} }
return false; return false;
} }
public size(): number { public size(): number {
return this.switches_.size; return this.switches_.size;
} }
public empty(): boolean { public empty(): boolean {
return this.size() === 0; return this.size() === 0;
} }
public toString(): string { public toString(): string {
return this.toggled() return this.toggled()
.map((value) => value[1]) .map((value) => value[1])
.join(","); .join(",");
} }
} }
+59 -59
View File
@@ -1,59 +1,59 @@
import { AudioResource } from "@discordjs/voice"; import { AudioResource } from "@discordjs/voice";
import { LoopOption, Track } from "./types.js"; import { LoopOption, Track } from "./types.js";
const STATUSBAR_LENGTH = 15; const STATUSBAR_LENGTH = 15;
export function statusBarGenerator(resource?: AudioResource<Track>): string { export function statusBarGenerator(resource?: AudioResource<Track>): string {
const duration = resource?.metadata.duration || 1; const duration = resource?.metadata.duration || 1;
const playback = Math.min(resource?.playbackDuration || 0 / 1000, duration); const playback = Math.min((resource?.playbackDuration || 0) / 1000, duration);
const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹"); const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹");
const empty = Array<string>(STATUSBAR_LENGTH - filled.length).fill("▫️"); const empty = Array<string>(STATUSBAR_LENGTH - filled.length).fill("▫️");
return `[${filled.join("")}${empty.join("")}] ${toISOTime(playback)}/${toISOTime(duration)}`; return `[${filled.join("")}${empty.join("")}] ${toISOTime(playback)}/${toISOTime(duration)}`;
} }
export function toISOTime(amount: number): string { export function toISOTime(amount: number): string {
const h = Math.floor(amount / 3600); const h = Math.floor(amount / 3600);
const m = Math.floor((amount % 3600) / 60); const m = Math.floor((amount % 3600) / 60);
const s = Math.floor((amount % 3600) % 60); const s = Math.floor((amount % 3600) % 60);
const hDisplay = h > 0 ? `${h}:` : ""; const hDisplay = h > 0 ? `${h}:` : "";
const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`; const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`;
const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`; const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`;
return hDisplay + mDisplay + sDisplay; return hDisplay + mDisplay + sDisplay;
} }
export async function asleep(amount: number): Promise<unknown> { export async function asleep(amount: number): Promise<unknown> {
return new Promise((r) => setTimeout(r, amount)); return new Promise((r) => setTimeout(r, amount));
} }
export function volumeMoji(amount: number): string { export function volumeMoji(amount: number): string {
return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇"; return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇";
} }
export function loopMoji(loop: LoopOption): string { export function loopMoji(loop: LoopOption): string {
return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂"; return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂";
} }
export function largestCommonSequence(s1: string, s2: string) { export function largestCommonSequence(s1: string, s2: string) {
s1 = s1.toLowerCase().slice(0, 100); s1 = s1.toLowerCase().slice(0, 100);
s2 = s2.toLowerCase().slice(0, 100); s2 = s2.toLowerCase().slice(0, 100);
const buffer = Array.from(Array(s1.length + 1), () => new Array<number>(s2.length + 1).fill(0)); const buffer = Array.from(Array(s1.length + 1), () => new Array<number>(s2.length + 1).fill(0));
for (let i = 1; i <= s1.length; i++) { for (let i = 1; i <= s1.length; i++) {
for (let j = 1; j <= s2.length; j++) { for (let j = 1; j <= s2.length; j++) {
if (s1[i - 1] === s2[j - 1]) { if (s1[i - 1] === s2[j - 1]) {
buffer[i][j] = buffer[i - 1][j - 1] + 1; buffer[i][j] = buffer[i - 1][j - 1] + 1;
} else { } else {
buffer[i][j] = Math.max(buffer[i - 1][j], buffer[i][j - 1]); buffer[i][j] = Math.max(buffer[i - 1][j], buffer[i][j - 1]);
} }
} }
} }
return buffer[s1.length][s2.length]; return buffer[s1.length][s2.length];
} }
+36 -36
View File
@@ -1,36 +1,36 @@
import { Collection, Guild, GuildTextBasedChannel } from "discord.js"; import { Collection, Guild, GuildTextBasedChannel } from "discord.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import Player from "./player.js"; import Player from "./player.js";
export default class Music { export default class Music {
private players_ = new Collection<string, Player>(); private players_ = new Collection<string, Player>();
public get(guild: Guild, channel?: GuildTextBasedChannel): Player | undefined { public get(guild: Guild, channel?: GuildTextBasedChannel): Player | undefined {
const player = this.players_.get(guild.id); const player = this.players_.get(guild.id);
if (player) { if (player) {
if (channel) { if (channel) {
player.channel = channel; player.channel = channel;
} }
return player; return player;
} }
} }
public set(guild: Guild, channel: GuildTextBasedChannel): Player { public set(guild: Guild, channel: GuildTextBasedChannel): Player {
const player = new Player(guild, this, channel); const player = new Player(guild, this, channel);
this.players_.set(guild.id, player); this.players_.set(guild.id, player);
logger.info("Player was created.", { player }); logger.info("Player was created.", { player });
return player; return player;
} }
public delete(guildId: string): void { public delete(guildId: string): void {
logger.info("Player was deleted.", { guildId }); logger.info("Player was deleted.", { guildId });
this.players_.delete(guildId); this.players_.delete(guildId);
} }
} }
+287 -288
View File
@@ -1,288 +1,287 @@
import { import {
AudioPlayer, AudioPlayer,
AudioPlayerStatus, AudioPlayerStatus,
AudioResource, AudioResource,
createAudioPlayer, createAudioPlayer,
createAudioResource, createAudioResource,
DiscordGatewayAdapterCreator, DiscordGatewayAdapterCreator,
joinVoiceChannel, joinVoiceChannel,
VoiceConnection VoiceConnection
} from "@discordjs/voice"; } from "@discordjs/voice";
import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js"; import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js";
import ytdl from "discord-ytdl-core";
import { fetchInfo, ytdlStream } from "./fetch.js";
import { fetchInfo } from "./fetch.js"; import Filter from "./filter.js";
import Filter from "./filter.js"; import { largestCommonSequence } from "./misc.js";
import { largestCommonSequence } from "./misc.js"; import Music from "./music.js";
import Music from "./music.js"; import Queue from "./queue.js";
import Queue from "./queue.js"; import replies from "./replies.js";
import replies from "./replies.js"; import { Track } from "./types.js";
import { Track } from "./types.js";
export default class Player {
export default class Player { private music: Music;
private music: Music; private guild: Guild;
private guild: Guild;
private channel_: GuildTextBasedChannel;
private channel_: GuildTextBasedChannel; public get channel(): GuildTextBasedChannel {
public get channel(): GuildTextBasedChannel { return this.channel_;
return this.channel_; }
} public set channel(x: GuildTextBasedChannel) {
public set channel(x: GuildTextBasedChannel) { this.channel_ = x;
this.channel_ = x; }
}
private message_?: Message;
private message_?: Message;
private connection_?: VoiceConnection;
private connection_?: VoiceConnection; public get connection(): VoiceConnection | undefined {
public get connection(): VoiceConnection | undefined { return this.connection_;
return this.connection_; }
}
private player_: AudioPlayer;
private player_: AudioPlayer; public get player(): AudioPlayer {
public get player(): AudioPlayer { return this.player_;
return this.player_; }
}
private resource_: AudioResource<Track> | undefined;
private resource_: AudioResource<Track> | undefined; public get resource(): AudioResource<Track> | undefined {
public get resource(): AudioResource<Track> | undefined { return this.resource_;
return this.resource_; }
}
private volume_ = 100;
private volume_ = 100; public get volume(): number {
public get volume(): number { return this.volume_;
return this.volume_; }
} public set volume(x: number) {
public set volume(x: number) { this.volume_ = Math.max(0, Math.min(150, x));
this.volume_ = Math.max(0, Math.min(150, x));
if (this.resource_ && this.resource_.volume) {
if (this.resource_ && this.resource_.volume) { this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100); }
} }
}
public readonly queue = new Queue();
public readonly queue = new Queue();
public readonly filter = new Filter();
public readonly filter = new Filter();
public readonly createdAt: Date = new Date();
public readonly createdAt: Date = new Date();
public constructor(guild: Guild, music: Music, channel: GuildTextBasedChannel) {
public constructor(guild: Guild, music: Music, channel: GuildTextBasedChannel) { this.music = music;
this.music = music; this.guild = guild;
this.guild = guild; this.channel_ = channel;
this.channel_ = channel;
this.player_ = createAudioPlayer();
this.player_ = createAudioPlayer();
this.player_.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
this.player_.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable()); }
}
private resourceGenerator(track: Track, seek?: number) {
private resourceGenerator(track: Track, seek?: number) { const stream = ytdlStream(track.url, {
const stream = ytdl(track.url, { opusEncoded: true,
opusEncoded: true, filter: "audioonly",
filter: "audioonly", highWaterMark: 1 << 25,
highWaterMark: 1 << 25, seek: seek ? seek / 1000 : undefined,
seek: seek ? seek / 1000 : undefined, encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined });
});
this.resource_ = createAudioResource(stream, {
this.resource_ = createAudioResource(stream, { inlineVolume: true,
inlineVolume: true, metadata: track
metadata: track });
});
if (seek) {
if (seek) { this.resource_.playbackDuration = seek;
this.resource_.playbackDuration = seek; }
}
if (this.resource_.volume) {
if (this.resource_.volume) { this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100); }
}
return this.resource_;
return this.resource_; }
}
private resourceEndResolvable(): void {
private resourceEndResolvable(): void { if (this.resource_) {
if (this.resource_) { this.resource_.encoder?.destroy();
this.resource_.encoder?.destroy(); }
}
if (this.queue.next()) {
if (this.queue.next()) { this.play();
this.play(); } else {
} else { this.player_.stop(true);
this.player_.stop(true); this.resource_ = undefined;
this.resource_ = undefined; }
} }
}
private async messageResolvable(track: Track) {
private async messageResolvable(track: Track) { if (this.message_?.deletable) {
if (this.message_?.deletable) { await this.message_.delete().catch();
await this.message_.delete().catch(); }
}
this.message_ = await this.channel_.send(replies.nowPlaying(track));
this.message_ = await this.channel_.send(replies.nowPlaying(track)); }
}
private async search(query: string, requester: User): Promise<Track[] | undefined> {
private async search(query: string, requester: User): Promise<Track[] | undefined> { const info = await fetchInfo(query);
const info = await fetchInfo(query);
if (info.length === 0) {
if (info.length === 0) { return undefined;
return undefined; }
}
return info.map((value) => {
return info.map((value) => { return { ...value, requester: requester };
return { ...value, requester: requester }; });
}); }
}
public async play(seek?: number): Promise<void> {
public play(seek?: number): void { const track = this.queue.current();
const track = this.queue.current();
if (!this.connection_ || !track) {
if (!this.connection_ || !track) { return;
return; }
}
this.player_.play(this.resourceGenerator(track, seek));
this.player_.play(this.resourceGenerator(track, seek));
this.connection_.subscribe(this.player_);
this.connection_.subscribe(this.player_);
await this.messageResolvable(track);
this.messageResolvable(track); }
}
public connect(channel: VoiceBasedChannel): void {
public connect(channel: VoiceBasedChannel): void { if (
if ( this.connection_?.joinConfig.channelId === channel.id &&
this.connection_?.joinConfig.channelId === channel.id && this.connection_.joinConfig.guildId === channel.guildId
this.connection_.joinConfig.guildId === channel.guildId ) {
) { return;
return; }
}
if (this.connection_) {
if (this.connection_) { this.connection_.destroy();
this.connection_.destroy(); }
}
this.connection_ = joinVoiceChannel({
this.connection_ = joinVoiceChannel({ channelId: channel.id,
channelId: channel.id, guildId: channel.guildId,
guildId: channel.guildId, adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator });
});
this.connection_.subscribe(this.player_);
this.connection_.subscribe(this.player_); }
}
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> { const tracks = await this.search(query, requester);
const tracks = await this.search(query, requester);
if (!tracks) {
if (!tracks) { return;
return; }
}
this.queue.push(tracks, index);
this.queue.push(tracks, index);
return tracks[0];
return tracks[0]; }
}
public jump(query: number | string): number | undefined {
public jump(query: number | string): number | undefined { let position = typeof query === "number" ? query : -1;
let position = typeof query === "number" ? query : -1; let diff = 0;
let diff = 0;
if (typeof query === "string") {
if (typeof query === "string") { this.queue.tracks.forEach((track, index) => {
this.queue.tracks.forEach((track, index) => { const tempDiff = largestCommonSequence(track.title, query);
const tempDiff = largestCommonSequence(track.title, query); if (diff < tempDiff) {
if (diff < tempDiff) { diff = tempDiff;
diff = tempDiff; position = index;
position = index; }
} });
}); }
}
if (position !== -1 && this.queue.jump(position)) {
if (position !== -1 && this.queue.jump(position)) { this.play();
this.play(); return position;
return position; }
}
return undefined;
return undefined; }
}
public remove(query?: number | string): Track | undefined {
public remove(query?: number | string): Track | undefined { const oldPosition = this.queue.position;
const oldPosition = this.queue.position;
let newPosition = typeof query === "number" ? query : this.queue.position;
let newPosition = typeof query === "number" ? query : this.queue.position; let diff = 0;
let diff = 0;
if (typeof query === "string") {
if (typeof query === "string") { this.queue.tracks.forEach((track, index) => {
this.queue.tracks.forEach((track, index) => { const tempDiff = largestCommonSequence(track.title, query);
const tempDiff = largestCommonSequence(track.title, query); if (diff < tempDiff) {
if (diff < tempDiff) { diff = tempDiff;
diff = tempDiff; newPosition = index;
newPosition = index; }
} });
}); }
}
const track = this.queue.pop(newPosition);
const track = this.queue.pop(newPosition);
if (track) {
if (track) { if (oldPosition === newPosition) {
if (oldPosition === newPosition) { this.play();
this.play(); }
} return track;
return track; }
}
return undefined;
return undefined; }
}
public seek(time: number): void {
public seek(time: number): void { this.play(time);
this.play(time); }
}
public clear(): void {
public clear(): void { this.queue.clear();
this.queue.clear(); this.resource_ = undefined;
this.resource_ = undefined;
if (this.player_) {
if (this.player_) { this.player_.stop();
this.player_.stop(); }
} }
}
public destroy(): void {
public destroy(): void { if (this.connection_) {
if (this.connection_) { this.connection_.destroy();
this.connection_.destroy(); }
}
this.player_.stop();
this.player_.stop(); this.music.delete(this.guild.id);
this.music.delete(this.guild.id); }
}
public pause(): boolean {
public pause(): boolean { if (this.player_.state.status === AudioPlayerStatus.Paused) {
if (this.player_.state.status === AudioPlayerStatus.Paused) { this.player_.unpause();
this.player_.unpause(); return false;
return false; } else {
} else { this.player_.pause();
this.player_.pause(); return true;
return true; }
} }
}
public shuffle(): void {
public shuffle(): void { this.queue.shuffle();
this.queue.shuffle();
if (this.resource_) {
if (this.resource_) { this.player_.stop();
this.player_.stop(); this.play();
this.play(); }
} }
}
public skip(): void {
public skip(): void { this.player_.stop();
this.player_.stop(); }
}
public setFilter(filter?: string): boolean {
public setFilter(filter?: string): boolean { const status = this.filter.toggle(filter);
const status = this.filter.toggle(filter);
if (this.resource_) {
if (this.resource_) { this.play(this.resource_.playbackDuration);
this.play(this.resource_.playbackDuration); }
}
return status;
return status; }
} }
}
+119 -117
View File
@@ -1,117 +1,119 @@
import { LoopOption, Track } from "./types.js"; import { LoopOption, Track } from "./types.js";
export default class Queue { export default class Queue {
private tracks_: Track[] = []; private tracks_: Track[] = [];
public get tracks(): Track[] { public get tracks(): Track[] {
return this.tracks_; return this.tracks_;
} }
private position_ = 0; private position_ = 0;
public get position(): number { public get position(): number {
return this.position_; return this.position_;
} }
private loop_: LoopOption = "NONE"; private loop_: LoopOption = "NONE";
public get loop(): LoopOption { public get loop(): LoopOption {
return this.loop_; return this.loop_;
} }
public set loop(x: LoopOption) { public set loop(x: LoopOption) {
this.loop_ = x; this.loop_ = x;
} }
private duration_ = 0; private duration_ = 0;
public get duration(): number { public get duration(): number {
return this.duration_; return this.duration_;
} }
public next(): Track | undefined { public next(): Track | undefined {
if (this.loop_ === "SONG") { if (this.loop_ === "SONG") {
return this.current(); return this.current();
} }
if (this.loop_ === "AUTO") { if (this.loop_ === "AUTO") {
this.position_ = Math.floor(Math.random() * this.tracks_.length); this.position_ = Math.floor(Math.random() * this.tracks_.length);
return this.current(); return this.current();
} }
if (this.position_ + 1 < this.tracks_.length) { if (this.position_ + 1 < this.tracks_.length) {
this.position_++; this.position_++;
return this.current(); return this.current();
} else if (this.loop_ === "LOOP") { } else if (this.loop_ === "LOOP") {
this.position_ = 0; this.position_ = 0;
return this.current(); return this.current();
} else if (this.loop_ === "NONE") { } else if (this.loop_ === "NONE") {
this.position_ = 0; this.position_ = 0;
return undefined; return undefined;
} }
} }
public shuffle(): void { public shuffle(): void {
let currentIndex = this.tracks_.length; let currentIndex = this.tracks_.length;
while (currentIndex != 0) { while (currentIndex != 0) {
const randomIndex = Math.floor(Math.random() * currentIndex); const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--; currentIndex--;
[this.tracks_[currentIndex], this.tracks_[randomIndex]] = [ [this.tracks_[currentIndex], this.tracks_[randomIndex]] = [
this.tracks_[randomIndex], this.tracks_[randomIndex],
this.tracks_[currentIndex] this.tracks_[currentIndex]
]; ];
} }
this.position_ = 0; this.position_ = 0;
} }
public push(tracks: Track[], index?: number): Track { public push(tracks: Track[], index?: number): Track {
if (!index || index >= this.tracks_.length) { for (const track of tracks) {
this.tracks_ = this.tracks_.concat(tracks); this.duration_ += track.duration;
} else { }
this.tracks_.splice(Math.max(0, index), 0, ...tracks);
if (this.position_ >= index) { if (index === undefined || index >= this.tracks_.length) {
this.position_ += tracks.length; this.tracks_ = this.tracks_.concat(tracks);
}
} return this.tracks_[this.tracks_.length - 1];
}
for (const track of tracks) {
this.duration_ += track.duration; this.tracks_.splice(Math.max(0, index), 0, ...tracks);
} if (this.position_ >= index) {
this.position_ += tracks.length;
return this.tracks_[index || this.tracks_.length - 1]; }
}
return this.tracks_[Math.max(0, index)];
public pop(query: number): Track | undefined { }
if (query >= 0 && query < this.tracks_.length) {
const track = this.tracks_[query]; public pop(query: number): Track | undefined {
if (query >= 0 && query < this.tracks_.length) {
this.tracks_.splice(query, 1); const track = this.tracks_[query];
this.position_ = Math.max(0, this.position_ - 1);
this.duration_ -= track.duration; this.tracks_.splice(query, 1);
return track; this.position_ = Math.max(0, this.position_ - 1);
} this.duration_ -= track.duration;
return track;
return undefined; }
}
return undefined;
public jump(query: number): Track | undefined { }
if (query >= 0 && query < this.tracks_.length) {
const track = this.tracks_[query]; public jump(query: number): Track | undefined {
this.position_ = query; if (query >= 0 && query < this.tracks_.length) {
return track; const track = this.tracks_[query];
} this.position_ = query;
return track;
return undefined; }
}
return undefined;
public current(): Track { }
return this.tracks_[this.position_];
} public current(): Track {
return this.tracks_[this.position_];
public empty(): boolean { }
return this.tracks_.length === 0;
} public empty(): boolean {
return this.tracks_.length === 0;
public clear(): void { }
this.tracks_ = [];
this.position_ = 0; public clear(): void {
} this.tracks_ = [];
} this.position_ = 0;
}
}
+376 -376
View File
@@ -1,376 +1,376 @@
import { AudioResource } from "@discordjs/voice"; import { AudioResource } from "@discordjs/voice";
import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js"; import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js";
import config from "../../../config.js"; import config from "../../../config.js";
import { loopMoji, statusBarGenerator, toISOTime, volumeMoji } from "./misc.js"; import { loopMoji, statusBarGenerator, toISOTime, volumeMoji } from "./misc.js";
import Player from "./player.js"; import Player from "./player.js";
import Queue from "./queue.js"; import Queue from "./queue.js";
import { LoopOption, Track } from "./types.js"; import { LoopOption, Track } from "./types.js";
const replies = { const replies = {
notExists: (): BaseMessageOptions => { notExists: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.warning) .setColor(config.colors.warning)
.setTitle("⚠️ Command ignored - player does not exist") .setTitle("⚠️ Command ignored - player does not exist")
.setDescription("Use '/play' to create a new one."); .setDescription("Use '/play' to create a new one.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
searching: (): BaseMessageOptions => { searching: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setTitle("🔍 Searching...") .setTitle("🔍 Searching...")
.setDescription("It will take longer if playlist or non-youtube URL was queried."); .setDescription("It will take longer if playlist or non-youtube URL was queried.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
notPlaying: (): BaseMessageOptions => { notPlaying: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setTitle("💿 Insert the disk...") .setTitle("💿 Insert the disk...")
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ") .setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.setDescription("Use '/play' to start the playback."); .setDescription("Use '/play' to start the playback.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
nowPlaying: (track: Track): BaseMessageOptions => { nowPlaying: (track: Track): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setAuthor({ .setAuthor({
iconURL: track.requester.avatarURL() ?? undefined, iconURL: track.requester.avatarURL() ?? undefined,
name: track.requester.tag name: track.requester.username
}) })
.setTitle(track.title) .setTitle(track.title)
.setURL(track.url) .setURL(track.url)
.setDescription("playing now.") .setDescription("playing now.")
.setThumbnail(track.thumbnail); .setThumbnail(track.thumbnail);
return { embeds: [embed] }; return { embeds: [embed] };
}, },
okConnected: (channel: string): BaseMessageOptions => { okConnected: (channel: string): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`✅ Successfuly connected to: ${italic(channel)}`) .setTitle(`✅ Successfuly connected to: ${italic(channel)}`)
.setDescription("Will leave after some time, if left alone."); .setDescription("Will leave after some time, if left alone.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
notConnected: (): BaseMessageOptions => { notConnected: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle("❗ Cannot create new player.") .setTitle("❗ Cannot create new player.")
.setDescription("Make yourself connected to a voice channel beforehand."); .setDescription("Make yourself connected to a voice channel beforehand.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
destroyed: (): BaseMessageOptions => { destroyed: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle("💀 Destroying the player") .setTitle("💀 Destroying the player")
.setDescription("Everything is lost now. Use '/play' to create a new one."); .setDescription("Everything is lost now. Use '/play' to create a new one.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
alone: (): BaseMessageOptions => { alone: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setTitle("🥬 I have decided to leave to conserve my energy") .setTitle("🥬 I have decided to leave to conserve my energy")
.setDescription("Since there was nobody in the voice channel."); .setDescription("Since there was nobody in the voice channel.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
paused: (state: boolean): BaseMessageOptions => { paused: (state: boolean): BaseMessageOptions => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (state) { if (state) {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle("▶️ The playback was resumed"); .setTitle("▶️ The playback was resumed");
} else { } else {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.warning) .setColor(config.colors.warning)
.setTitle("⏸️ The playback was paused") .setTitle("⏸️ The playback was paused")
.setDescription("Invoke this command again to resume."); .setDescription("Invoke this command again to resume.");
} }
return { embeds: [embed] }; return { embeds: [embed] };
}, },
seeked: (from: number, to: number): BaseMessageOptions => { seeked: (from: number, to: number): BaseMessageOptions => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (from < to) { if (from < to) {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.info) .setColor(config.colors.info)
.setTitle(`⏩ Fast-forwarding by ${toISOTime(to - from)}`); .setTitle(`⏩ Fast-forwarding by ${toISOTime(to - from)}`);
} else { } else {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.info) .setColor(config.colors.info)
.setTitle(`⏪ Fast-reversing by ${toISOTime(from - to)}`); .setTitle(`⏪ Fast-reversing by ${toISOTime(from - to)}`);
} }
embed.setDescription( embed.setDescription(
`Moving playback from ${italic(toISOTime(from))} to ${italic(toISOTime(to))}.` `Moving playback from ${italic(toISOTime(from))} to ${italic(toISOTime(to))}.`
); );
return { embeds: [embed] }; return { embeds: [embed] };
}, },
looped: (option: LoopOption) => { looped: (option: LoopOption) => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
switch (option) { switch (option) {
case "AUTO": { case "AUTO": {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.default) .setColor(config.colors.default)
.setTitle(`${loopMoji(option)} Shuffle mode.`) .setTitle(`${loopMoji(option)} Shuffle mode.`)
.setDescription("Does not affect the queue order. Endless playback."); .setDescription("Does not affect the queue order. Endless playback.");
break; break;
} }
case "LOOP": { case "LOOP": {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.info) .setColor(config.colors.info)
.setTitle(`${loopMoji(option)} Looping over the queue.`) .setTitle(`${loopMoji(option)} Looping over the queue.`)
.setDescription("Will start over after final track. Endless playback."); .setDescription("Will start over after final track. Endless playback.");
break; break;
} }
case "SONG": { case "SONG": {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.warning) .setColor(config.colors.warning)
.setTitle(`${loopMoji(option)} Looping a single track.`) .setTitle(`${loopMoji(option)} Looping a single track.`)
.setDescription("Endless playback of a single track. Over and over again..."); .setDescription("Endless playback of a single track. Over and over again...");
break; break;
} }
case "NONE": { case "NONE": {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle(`${loopMoji(option)} Drop on final.`) .setTitle(`${loopMoji(option)} Drop on final.`)
.setDescription( .setDescription(
"Will end playback after final track. Action will be needed to continue." "Will end playback after final track. Action will be needed to continue."
); );
break; break;
} }
} }
return { embeds: [embed] }; return { embeds: [embed] };
}, },
reshuffle: (): BaseMessageOptions => { reshuffle: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle("✅ Successfuly reshuffled queue") .setTitle("✅ Successfuly reshuffled queue")
.setDescription("Use 'random' mode, if you want shuffle without reshuffle."); .setDescription("Use 'random' mode, if you want shuffle without reshuffle.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
skipped: (): BaseMessageOptions => { skipped: (): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.info) .setColor(config.colors.info)
.setTitle("⏭️ Skipping current track") .setTitle("⏭️ Skipping current track")
.setDescription("Behavior depends on the loop option."); .setDescription("Behavior depends on the loop option.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
jumped: (previous?: Track, to?: number): BaseMessageOptions => { jumped: (previous?: Track, to?: number): BaseMessageOptions => {
if (!to) { if (!to) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle("❗ Cannot perfome jump over the queue") .setTitle("❗ Cannot perfome jump over the queue")
.setDescription("Either not found by query, or index is not in range."); .setDescription("Either not found by query, or index is not in range.");
return { embeds: [embed] }; return { embeds: [embed] };
} }
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`✅ Jumping over the queue`); .setTitle(`✅ Jumping over the queue`);
if (previous) { if (previous) {
embed.setDescription(`Switching from ${italic(previous.title)}.`); embed.setDescription(`Switching from ${italic(previous.title)}.`);
} else { } else {
embed.setDescription("Playback will start again."); embed.setDescription("Playback will start again.");
} }
return { embeds: [embed] }; return { embeds: [embed] };
}, },
appended: (track?: Track): BaseMessageOptions => { appended: (track?: Track): BaseMessageOptions => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (track) { if (track) {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setAuthor({ .setAuthor({
iconURL: track.requester.avatarURL() ?? undefined, iconURL: track.requester.avatarURL() ?? undefined,
name: track.requester.tag name: track.requester.username
}) })
.setTitle(track.title) .setTitle(track.title)
.setURL(track.url) .setURL(track.url)
.setDescription("has been added to the queue.") .setDescription("has been added to the queue.")
.setThumbnail(track.thumbnail) .setThumbnail(track.thumbnail)
.setTimestamp(); .setTimestamp();
} else { } else {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle("❗ Cannot add track to the queue") .setTitle("❗ Cannot add track to the queue")
.setDescription("Either not found by query, or URL is broken."); .setDescription("Either not found by query, or URL is broken.");
} }
return { embeds: [embed] }; return { embeds: [embed] };
}, },
removed: (track?: Track): BaseMessageOptions => { removed: (track?: Track): BaseMessageOptions => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (track) { if (track) {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(track.title) .setTitle(track.title)
.setURL(track.url) .setURL(track.url)
.setDescription("was removed from the queue.") .setDescription("was removed from the queue.")
.setThumbnail(track.thumbnail) .setThumbnail(track.thumbnail)
.setTimestamp(); .setTimestamp();
} else { } else {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle("❗ Cannot delete track from queue") .setTitle("❗ Cannot delete track from queue")
.setDescription("Either not found by query, or index is not in range."); .setDescription("Either not found by query, or index is not in range.");
} }
return { embeds: [embed] }; return { embeds: [embed] };
}, },
cleared: (amount: number): BaseMessageOptions => { cleared: (amount: number): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`♻️ Queue has been cleared`) .setTitle(`♻️ Queue has been cleared`)
.setDescription( .setDescription(
`${italic(amount.toString())} track${amount > 1 ? "s were" : " was"} deleted.` `${italic(amount.toString())} track${amount > 1 ? "s were" : " was"} deleted.`
); );
return { embeds: [embed] }; return { embeds: [embed] };
}, },
filtered: (filter?: string, status = true): BaseMessageOptions => { filtered: (filter?: string, status = true): BaseMessageOptions => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (filter) { if (filter) {
embed = new EmbedBuilder().setTitle( embed = new EmbedBuilder().setTitle(
`✅ ${italic(filter)} was ${status ? "applied to the playback" : "declined"}.` `✅ ${italic(filter)} was ${status ? "applied to the playback" : "declined"}.`
); );
} else { } else {
embed = new EmbedBuilder().setTitle("♻️ All filters were declined"); embed = new EmbedBuilder().setTitle("♻️ All filters were declined");
} }
embed embed
.setColor(config.colors.success) .setColor(config.colors.success)
.setDescription("Playback can experience some lags during intial transformation."); .setDescription("Playback can experience some lags during initial transformation.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
volumeSet: (amount: number): BaseMessageOptions => { volumeSet: (amount: number): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`${volumeMoji(amount)} Volume has been set to ${amount}%`) .setTitle(`${volumeMoji(amount)} Volume has been set to ${amount}%`)
.setDescription("This will affect everyone listening."); .setDescription("This will affect everyone listening.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
queue: (queue: Queue, position = 0, resource?: AudioResource<Track>): BaseMessageOptions => { queue: (queue: Queue, position = 0, resource?: AudioResource<Track>): BaseMessageOptions => {
if (queue.empty()) { if (queue.empty()) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.warning) .setColor(config.colors.warning)
.setTitle("⚠️ Queue is empty") .setTitle("⚠️ Queue is empty")
.setDescription("Use '/play' to add some tracks."); .setDescription("Use '/play' to add some tracks.");
return { embeds: [embed] }; return { embeds: [embed] };
} }
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (resource) { if (resource) {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setTitle(`${queue.position + 1}. ${resource.metadata.title}`) .setTitle(`${queue.position + 1}. ${resource.metadata.title}`)
.setAuthor({ .setAuthor({
iconURL: resource.metadata.requester.avatarURL() ?? undefined, iconURL: resource.metadata.requester.avatarURL() ?? undefined,
name: resource.metadata.requester.tag name: resource.metadata.requester.username
}) })
.setURL(resource.metadata.url) .setURL(resource.metadata.url)
.setDescription(statusBarGenerator(resource)) .setDescription(statusBarGenerator(resource))
.setThumbnail(resource.metadata.thumbnail); .setThumbnail(resource.metadata.thumbnail);
} else { } else {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setTitle("💿 Insert the disk...") .setTitle("💿 Insert the disk...")
.setURL("https://www.youtube.com/watch?v=5mGuCdlCcNM&ab_channel=Ra%C3%BAlBlanco") .setURL("https://www.youtube.com/watch?v=5mGuCdlCcNM&ab_channel=Ra%C3%BAlBlanco")
.setDescription(statusBarGenerator()) .setDescription(statusBarGenerator())
.setThumbnail("https://img.youtube.com/vi/5mGuCdlCcNM/default.jpg"); .setThumbnail("https://img.youtube.com/vi/5mGuCdlCcNM/default.jpg");
} }
embed.setColor(config.colors.info).setFields( embed.setColor(config.colors.info).setFields(
queue.tracks.slice(position, position + 10).map((value, index) => ({ queue.tracks.slice(position, position + 10).map((value, index) => ({
name: `${position + index + 1}. ${value.title.padEnd(35)}`, name: `${position + index + 1}. ${value.title.padEnd(35)}`,
value: `[${toISOTime(value.duration)}] | ${value.requester.tag}` value: `[${toISOTime(value.duration)}] | ${value.requester.username}`
})) }))
); );
return { embeds: [embed] }; return { embeds: [embed] };
}, },
info: (player: Player): BaseMessageOptions => { info: (player: Player): BaseMessageOptions => {
let embed: EmbedBuilder; let embed: EmbedBuilder;
if (player.resource) { if (player.resource) {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`) .setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`)
.setAuthor({ .setAuthor({
iconURL: player.resource.metadata.requester.avatarURL() ?? undefined, iconURL: player.resource.metadata.requester.avatarURL() ?? undefined,
name: player.resource.metadata.requester.tag name: player.resource.metadata.requester.username
}) })
.setURL(player.resource.metadata.url) .setURL(player.resource.metadata.url)
.setDescription(statusBarGenerator(player.resource)) .setDescription(statusBarGenerator(player.resource))
.setThumbnail(player.resource.metadata.thumbnail); .setThumbnail(player.resource.metadata.thumbnail);
} else { } else {
embed = new EmbedBuilder() embed = new EmbedBuilder()
.setTitle("💿 Insert the disk...") .setTitle("💿 Insert the disk...")
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ") .setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
.setDescription(statusBarGenerator()); .setDescription(statusBarGenerator());
} }
embed.setColor(config.colors.info).addFields([ embed.setColor(config.colors.info).addFields([
{ {
name: "Applied Filters", name: "Applied Filters",
value: value:
player.filter player.filter
.toggled() .toggled()
.map((value) => value[0]) .map((value) => value[0])
.join("\n") + "\ndefault", .join("\n") + "\ndefault",
inline: true inline: true
}, },
{ {
name: "Queue Length", name: "Queue Length",
value: `🔢 ${player.queue.tracks.length}`, value: `🔢 ${player.queue.tracks.length}`,
inline: true inline: true
}, },
{ {
name: "Queue Duration", name: "Queue Duration",
value: `💿 ${toISOTime(player.queue.duration)}`, value: `💿 ${toISOTime(player.queue.duration)}`,
inline: true inline: true
}, },
{ {
name: "Queue Looping", name: "Queue Looping",
value: `${loopMoji(player.queue.loop)} ${player.queue.loop}`, value: `${loopMoji(player.queue.loop)} ${player.queue.loop}`,
inline: true inline: true
}, },
{ {
name: "Volume Amount", name: "Volume Amount",
value: `${volumeMoji(player.volume)} ${player.volume}/150`, value: `${volumeMoji(player.volume)} ${player.volume}/150`,
inline: true inline: true
}, },
{ {
name: "Player Lifetime", name: "Player Lifetime",
value: `🕓 ${toISOTime((Date.now() - player.createdAt.getTime()) / 1000)}`, value: `🕓 ${toISOTime((Date.now() - player.createdAt.getTime()) / 1000)}`,
inline: true inline: true
} }
]); ]);
return { embeds: [embed] }; return { embeds: [embed] };
} }
}; };
export default replies; export default replies;
+43 -261
View File
@@ -1,261 +1,43 @@
import { User } from "discord.js"; import { User } from "discord.js";
import { downloadOptions } from "ytdl-core";
export interface Track {
url: string; export interface Track {
title: string; url: string;
thumbnail: string; title: string;
duration: number; thumbnail: string;
requester: User; duration: number;
} requester: User;
}
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
export interface YtdlArgs {
help?: boolean; export interface YtdlDump {
version?: boolean; id: string;
update?: boolean; original_url: string;
ignoreErrors?: boolean; title?: string;
abortOnError?: boolean; uploader?: string;
dumpUserAgent?: boolean; duration?: number;
listExtractors?: boolean; extractor_key: string;
extractorDescriptions?: boolean; thumbnail?: string;
forceGenericExtractor?: boolean; }
defaultSearch?: string;
igonreConfig?: boolean; export interface YtdlResult {
configLocation?: string; url: string;
flatPlaylist?: boolean; title: string;
markWatched?: boolean; thumbnail: string;
noColor?: boolean; duration: number;
proxy?: string; }
socketTimeout?: number;
sourceAddress?: string; export interface YtdlStreamOptions extends downloadOptions {
forceIpv4?: boolean; seek?: number;
forceIpv6?: boolean; encoderArgs?: string[];
geoVerificationProxy?: string; fmt?: string;
geoBypass?: boolean; opusEncoded?: boolean;
geoBypassCountry?: string; }
geoBypassIpBlock?: string;
playlistStart?: number; export interface StreamOptions {
playlistEnd?: number | "last"; seek?: number;
playlistItems?: string; encoderArgs?: string[];
matchTitle?: string; fmt?: string;
rejectTitle?: string; opusEncoded?: boolean;
maxDownloads?: number; }
minFilesize?: string;
maxFilesize?: string;
date?: string;
datebefore?: string;
dateafter?: string;
minViews?: number;
maxViews?: number;
matchFilter?: string;
noPlaylist?: boolean;
yesPlaylist?: boolean;
ageLimit?: number;
downloadArchive?: string;
includeAds?: boolean;
limitRate?: string;
retries?: number | "infinite";
skipUnavailableFragments?: boolean;
abortOnUnavailableFragment?: boolean;
keepFragments?: boolean;
bufferSize?: string;
noResizeBuffer?: boolean;
httpChunkSize?: string;
playlistReverse?: boolean;
playlistRandom?: boolean;
xattrSetFilesize?: boolean;
hlsPreferNative?: boolean;
hlsPreferFfmpeg?: boolean;
hlsUseMpegts?: boolean;
externalDownloader?: string;
externalDownloaderArgs?: string;
batchFile?: string;
id?: boolean;
output?: string;
outputNaPlaceholder?: string;
autonumberStart?: number;
restrictFilenames?: boolean;
noOverwrites?: boolean;
continue?: boolean;
noPart?: boolean;
noMtime?: boolean;
writeDescription?: boolean;
writeInfoJson?: boolean;
writeAnnotations?: boolean;
loadInfoJson?: string;
cookies?: string;
cacheDir?: string;
noCacheDir?: boolean;
rmCacheDir?: boolean;
writeThumbnail?: boolean;
writeAllThumbnails?: boolean;
listThumbnails?: boolean;
quiet?: boolean;
noWarnings?: boolean;
simulate?: boolean;
skipDownload?: boolean;
getUrl?: boolean;
getTitle?: boolean;
getId?: boolean;
getThumbnail?: boolean;
getDuration?: boolean;
getFilename?: boolean;
getFormat?: boolean;
dumpJson?: boolean;
dumpSingleJson?: boolean;
printJson?: boolean;
newline?: boolean;
noProgress?: boolean;
consoleTitle?: boolean;
verbose?: boolean;
dumpPages?: boolean;
writePages?: boolean;
printTraffic?: boolean;
callHome?: boolean;
encoding?: string;
noCheckCertificate?: boolean;
preferInsecure?: boolean;
userAgent?: string;
referer?: string;
addHeader?: string;
bidiWorkaround?: boolean;
sleepInterval?: number;
maxSleepInterval?: number;
format?: string;
allFormats?: boolean;
preferFreeFormats?: boolean;
listFormats?: boolean;
youtubeSkipDashManifest?: boolean;
mergeOutputFormat?: string;
writeSub?: boolean;
writeAutoSub?: boolean;
allSubs?: boolean;
listSubs?: boolean;
subFormat?: string;
subLang?: string;
username?: string;
password?: string;
twofactor?: string;
netrc?: boolean;
videoPassword?: string;
apMso?: string;
apUsername?: string;
apPassword?: string;
apListMso?: boolean;
extractAudio?: boolean;
audioFormat?: string;
audioQuality?: number;
recodeVideo?: string;
postprocessorArgs?: string;
keepVideo?: boolean;
noPostOverwrites?: boolean;
embedSubs?: boolean;
embedThumbnail?: boolean;
addMetadata?: boolean;
metadataFromFile?: string;
xattrs?: boolean;
fixup?: string;
preferAvconv?: boolean;
preferFfmpeg?: boolean;
ffmpegLocation?: string;
exec?: string;
convertSubs?: string;
}
interface YtdlFormat {
asr: number;
filesize: number;
format_id: string;
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: unknown;
container: string;
format: string;
protocol: string;
http_headers: unknown;
}
interface YtdlThumbnail {
height: number;
url: string;
width: number;
resolution: string;
id: string;
}
interface YtdlEntry {
uploader: string;
duration: number;
id: string;
title: string;
url: string;
}
export interface YtdlResponse {
id: string;
title: string;
formats: YtdlFormat[];
thumbnails: YtdlThumbnail[];
description: string;
upload_date: string;
uploader: string;
uploader_id: string;
uploader_url: string;
channel_id: string;
channel_url: string;
duration: number;
view_count: number;
average_rating: number;
age_limit: number;
webpage_url: string;
categories: string[];
tags: string[];
is_live: boolean;
like_count: number;
dislike_count: number;
channel: string;
track: string;
artist: string;
creator: string;
alt_title: string;
extractor: string;
webpage_url_basename: string;
extractor_key: string;
playlist: string;
playlist_index: number;
thumbnail: string;
display_id: string;
requested_subtitles: unknown;
asr: number;
filesize: number;
format_id: string;
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: { http_chunk_size: number };
container: string;
format: string;
protocol: string;
http_headers: unknown;
fulltitle: string;
_filename: string;
entries: YtdlEntry[];
}
+15 -15
View File
@@ -1,15 +1,15 @@
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import Music from "./lib/music.js"; import Music from "./lib/music.js";
const name = "music"; const name = "music";
const description = "Music module"; const description = "Music module";
const author = "unknowableshade"; const author = "unknowableshade";
const tag = "2.0.0"; const tag = "2.0.0";
const controller = new Music(); const controller = new Music();
const rootDir = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.dirname(fileURLToPath(import.meta.url));
export { author, controller, description, name, rootDir, tag }; export { author, controller, description, name, rootDir, tag };
+95 -90
View File
@@ -1,90 +1,95 @@
import { Events, Interaction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; import {
ChatInputCommandInteraction,
import Client from "../../../lib/client.js"; Events,
import logger from "../../../lib/logger.js"; PermissionFlagsBits,
import replies from "../lib/replies.js"; SlashCommandBuilder
} from "discord.js";
async function execute(client: Client, interaction: Interaction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) { import Client from "../../../lib/client.js";
logger.warn("Interaction rejected.", { interaction }); import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js";
return;
} async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.inCachedGuild()) {
const subcommand = interaction.options.getSubcommand(); logger.warn("Interaction rejected.", { interaction });
const event = interaction.options.getString("event", true);
return;
switch (subcommand) { }
case "role": {
const role = interaction.options.getRole("role", true); const subcommand = interaction.options.getSubcommand();
const event = interaction.options.getString("event", true);
await interaction.deferReply();
switch (subcommand) {
try { case "role": {
await client.db.event.upsert({ const role = interaction.options.getRole("role", true);
where: {
eventIdentifier: { await interaction.deferReply();
title: event,
action: subcommand, try {
guildId: interaction.guildId await client.db.event.upsert({
} where: {
}, eventIdentifier: {
update: { metadata: [role.id] }, title: event,
create: { action: subcommand,
Guild: { guildId: interaction.guildId
connectOrCreate: { }
where: { },
id: interaction.guildId update: { metadata: [role.id] },
}, create: {
create: { Guild: {
id: interaction.guildId connectOrCreate: {
} where: {
} id: interaction.guildId
}, },
title: event, create: {
action: subcommand, id: interaction.guildId
metadata: [role.id] }
} }
}); },
title: event,
await interaction.editReply(replies.auto("add role to a member", event)); action: subcommand,
} catch (error) { metadata: [role.id]
logger.error("Error occured while quering db.", { interaction }); }
} finally { });
await client.db.$disconnect();
} await interaction.editReply(replies.auto("add role to a member", event));
} catch (error) {
return; logger.error("Error occured while quering db.", { error });
} } finally {
} await client.db.$disconnect();
} }
const AUTO_EVENT_CHOICES = [ return;
{ }
name: "Member Joins", }
value: Events.GuildMemberAdd }
}
]; const AUTO_EVENT_CHOICES = [
{
const data = new SlashCommandBuilder() name: "Member Joins",
.setName("auto") value: Events.GuildMemberAdd
.setDescription("Quetza's automatization engine.") }
.addSubcommand((subcommand) => ];
subcommand
.setName("role") const data = new SlashCommandBuilder()
.setDescription("Auto: give role.") .setName("auto")
.addStringOption((option) => .setDescription("Quetza's automatization engine.")
option .addSubcommand((subcommand) =>
.setName("event") subcommand
.setDescription("Event when perfome an action.") .setName("role")
.setChoices(...AUTO_EVENT_CHOICES) .setDescription("Auto: give role.")
.setRequired(true) .addStringOption((option) =>
) option
.addRoleOption((option) => .setName("event")
option.setName("role").setDescription("Role to grant on event.").setRequired(true) .setDescription("Event when perfome an action.")
) .setChoices(...AUTO_EVENT_CHOICES)
) .setRequired(true)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) )
.setDMPermission(false); .addRoleOption((option) =>
option.setName("role").setDescription("Role to grant on event.").setRequired(true)
export { data, execute }; )
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false);
export { data, execute };
+98 -98
View File
@@ -1,98 +1,98 @@
import { Interaction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
switch (subcommand) { switch (subcommand) {
case "check": { case "check": {
const user = interaction.options.getUser("member", true); const user = interaction.options.getUser("member", true);
const member = await interaction.guild.members.fetch(user).catch(() => undefined); const member = await interaction.guild.members.fetch(user).catch(() => undefined);
if (member) { if (member) {
await interaction.reply(replies.roleList(user.tag, member.roles.cache)); await interaction.reply(replies.roleList(user.username, member.roles.cache));
} else { } else {
await interaction.reply(replies.notMember("edited", user)); await interaction.reply(replies.notMember("edited", user));
} }
return; return;
} }
case "edit": { case "edit": {
const user = interaction.options.getUser("member", true); const user = interaction.options.getUser("member", true);
const role = interaction.options.getRole("role", true); const role = interaction.options.getRole("role", true);
const reason = interaction.options.getString("reason") ?? undefined; const reason = interaction.options.getString("reason") ?? undefined;
const member = await interaction.guild.members.fetch(user).catch(() => undefined); const member = await interaction.guild.members.fetch(user).catch(() => undefined);
if (member) { if (member) {
if (member.roles.cache.some((memberRole) => role.id === memberRole.id)) { if (member.roles.cache.some((memberRole) => role.id === memberRole.id)) {
await member.roles.remove(role.id, reason); await member.roles.remove(role.id, reason);
await interaction.reply( await interaction.reply(
replies.roleAction("revoked from", interaction.user, role.name, reason) replies.roleAction("revoked from", interaction.user, role.name, reason)
); );
} else { } else {
await member.roles.add(role.id, reason); await member.roles.add(role.id, reason);
await interaction.reply( await interaction.reply(
replies.roleAction("granted to", interaction.user, role.name, reason) replies.roleAction("granted to", interaction.user, role.name, reason)
); );
} }
} else { } else {
await interaction.reply(replies.notMember("edited", user)); await interaction.reply(replies.notMember("edited", user));
} }
return; return;
} }
case "list": { case "list": {
const roles = await interaction.guild.roles.fetch(); const roles = await interaction.guild.roles.fetch();
await interaction.reply(replies.roleList(interaction.guild.name, roles)); await interaction.reply(replies.roleList(interaction.guild.name, roles));
return; return;
} }
} }
} }
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("role") .setName("role")
.setDescription("Quetza's role manager.") .setDescription("Quetza's role manager.")
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand.setName("list").setDescription("Role: list guild's roles.") subcommand.setName("list").setDescription("Role: list guild's roles.")
) )
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand subcommand
.setName("check") .setName("check")
.setDescription("Role: check user roles.") .setDescription("Role: check user roles.")
.addUserOption((option) => .addUserOption((option) =>
option.setName("member").setDescription("Member to check.").setRequired(true) option.setName("member").setDescription("Member to check.").setRequired(true)
) )
) )
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand subcommand
.setName("edit") .setName("edit")
.setDescription("Role: edit role for a specific user.") .setDescription("Role: edit role for a specific user.")
.addUserOption((option) => .addUserOption((option) =>
option.setName("member").setDescription("Member to edit.").setRequired(true) option.setName("member").setDescription("Member to edit.").setRequired(true)
) )
.addRoleOption((option) => .addRoleOption((option) =>
option.setName("role").setDescription("Member's role to edit.").setRequired(true) option.setName("role").setDescription("Member's role to edit.").setRequired(true)
) )
.addStringOption((option) => .addStringOption((option) =>
option.setName("reason").setDescription("Reason for an action.") option.setName("reason").setDescription("Reason for an action.")
) )
) )
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles) .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
.setDMPermission(false); .setDMPermission(false);
export { data, execute }; export { data, execute };
+23 -23
View File
@@ -1,23 +1,23 @@
import { Events, GuildMember } from "discord.js"; import { Events, GuildMember } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
async function execute(client: Client, eventee: [GuildMember]): Promise<void> { async function execute(client: Client, eventee: [GuildMember]): Promise<void> {
const [member] = eventee; const [member] = eventee;
try { try {
const event = await client.db.event.findFirst({ const event = await client.db.event.findFirst({
where: { guildId: member.guild.id, title: name } where: { guildId: member.guild.id, title: name }
}); });
if (event) { if (event) {
await member.roles.add(event.metadata as string[]); await member.roles.add(event.metadata as string[]);
} }
} finally { } finally {
await client.db.$disconnect(); await client.db.$disconnect();
} }
} }
const name = Events.GuildMemberAdd; const name = Events.GuildMemberAdd;
export { execute, name }; export { execute, name };
+80 -80
View File
@@ -1,80 +1,80 @@
import { import {
BaseMessageOptions, BaseMessageOptions,
bold, bold,
Collection, Collection,
EmbedBuilder, EmbedBuilder,
InteractionReplyOptions, InteractionReplyOptions,
italic, italic,
Role, Role,
underscore, underscore,
User User
} from "discord.js"; } from "discord.js";
import config from "../../../config.js"; import config from "../../../config.js";
const replies = { const replies = {
error: (): InteractionReplyOptions => { error: (): InteractionReplyOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.error) .setColor(config.colors.error)
.setTitle("Internal error occured while resolving interaction.") .setTitle("Internal error occured while resolving interaction.")
.setDescription("Submit an issue (bio). Logs were saved.") .setDescription("Submit an issue (bio). Logs were saved.")
.setTimestamp(); .setTimestamp();
return { embeds: [embed], ephemeral: true }; return { embeds: [embed], ephemeral: true };
}, },
notMember: (action: string, user: User): InteractionReplyOptions => { notMember: (action: string, user: User): InteractionReplyOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor("Blue") .setColor("Blue")
.setTitle(`${user.tag} cannot be ${action}`) .setTitle(`${user.username} cannot be ${action}`)
.setDescription("They're not a guild member.") .setDescription("They're not a guild member.")
.setTimestamp(); .setTimestamp();
return { embeds: [embed], ephemeral: true }; return { embeds: [embed], ephemeral: true };
}, },
roleAction: ( roleAction: (
action: string, action: string,
user: User, user: User,
roleName: string, roleName: string,
reason?: string reason?: string
): InteractionReplyOptions => { ): InteractionReplyOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`Role ${roleName} was ${action} user ${user.tag}`) .setTitle(`Role ${roleName} was ${action} user ${user.username}`)
.setDescription(reason ?? "No reason provided.") .setDescription(reason ?? "No reason provided.")
.setTimestamp(); .setTimestamp();
return { embeds: [embed], ephemeral: true }; return { embeds: [embed], ephemeral: true };
}, },
roleList: (observed: string, roles: Collection<string, Role>): InteractionReplyOptions => { roleList: (observed: string, roles: Collection<string, Role>): InteractionReplyOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`${observed} roles:`) .setTitle(`${observed} roles:`)
.addFields( .addFields(
roles.map((role) => { roles.map((role) => {
return { return {
name: role.name, name: role.name,
value: italic(role.id) value: italic(role.id)
}; };
}) })
) )
.setTimestamp(); .setTimestamp();
return { embeds: [embed], ephemeral: true }; return { embeds: [embed], ephemeral: true };
}, },
auto: (action: string, event: string): BaseMessageOptions => { auto: (action: string, event: string): BaseMessageOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle( .setTitle(
bold( bold(
`From now I will ${underscore(italic(action))} when ${underscore( `From now I will ${underscore(italic(action))} when ${underscore(
italic(event) italic(event)
)} event happens` )} event happens`
) )
) )
.setTimestamp(); .setTimestamp();
return { embeds: [embed] }; return { embeds: [embed] };
} }
}; };
export default replies; export default replies;
+11 -11
View File
@@ -1,11 +1,11 @@
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const name = "server"; const name = "server";
const description = "Server utilities module"; const description = "Server utilities module";
const author = "unknowableshade"; const author = "unknowableshade";
const tag = "0.0.1"; const tag = "0.0.1";
const rootDir = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.dirname(fileURLToPath(import.meta.url));
export { author, description, name, rootDir, tag }; export { author, description, name, rootDir, tag };
+19 -19
View File
@@ -1,19 +1,19 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "dist", "outDir": "dist",
"removeComments": false, "removeComments": false,
"sourceMap": true, "sourceMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"target": "ESNext", "target": "ESNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"types": ["node"], "types": ["node"],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/commands", "src/events"] "exclude": ["node_modules", "dist", "src/commands", "src/events"]
} }