Prettier & CI/CD Fixes
This commit is contained in:
1 parent
e8cd52d315
commit
a045be3f2f
51 files changed
+6395
-6393
No files matched your search
+21
-21
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"warnOnUnsupportedTypeScriptVersion": false,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": false,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"plugins": ["@typescript-eslint", "import", "simple-import-sort"],
|
||||
"ignorePatterns": ["**/node_modules/**", "/dist/**", "/tests/**"],
|
||||
"rules": {
|
||||
"curly": ["error", "all"],
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
}
|
||||
}
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"warnOnUnsupportedTypeScriptVersion": false,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": false,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"plugins": ["@typescript-eslint", "import", "simple-import-sort"],
|
||||
"ignorePatterns": ["**/node_modules/**", "/dist/**", "/tests/**"],
|
||||
"rules": {
|
||||
"curly": ["error", "all"],
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,68 @@
|
||||
name: actions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
mocha:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
pnpm-version: [8]
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ matrix.pnpm-version }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ESLint & Prettier
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Test
|
||||
run: pnpm run --if-present test
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: mocha
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: unknowableshade/quetza-bot
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
name: actions
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
mocha:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
pnpm-version: [8]
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ matrix.pnpm-version }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ESLint & Prettier
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Test
|
||||
run: pnpm run --if-present test
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: mocha
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: unknowableshade/quetza-bot
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "crlf",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.json, **/*.yml"],
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.json, **/*.yml"],
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
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
|
||||
|
||||
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 ffmpeg
|
||||
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
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Quetza 4
|
||||
|
||||
_Neat typescript discord bot._
|
||||
|
||||
## About
|
||||
|
||||
- Base player features.
|
||||
- Play music from YouTube and many other sites.
|
||||
- Lots of different filters _(nightcore, earrape etc.)_.
|
||||
- Slash commands.
|
||||
|
||||
## Links
|
||||
|
||||
- **[Dependencies](https://github.com/unknowableshade/quetza-bot/blob/master/package.json)**
|
||||
- **[Docker](https://hub.docker.com/repository/docker/unknowableshade/quetza-bot)**
|
||||
- **[Licence](https://github.com/unknowableshade/5a0b-bot/blob/master/LICENSE)**
|
||||
# Quetza 4
|
||||
|
||||
_Neat typescript discord bot._
|
||||
|
||||
## About
|
||||
|
||||
- Base player features.
|
||||
- Play music from YouTube and many other sites.
|
||||
- Lots of different filters _(nightcore, earrape etc.)_.
|
||||
- Slash commands.
|
||||
|
||||
## Links
|
||||
|
||||
- **[Dependencies](https://github.com/unknowableshade/quetza-bot/blob/master/package.json)**
|
||||
- **[Docker](https://hub.docker.com/repository/docker/unknowableshade/quetza-bot)**
|
||||
- **[Licence](https://github.com/unknowableshade/5a0b-bot/blob/master/LICENSE)**
|
||||
+61
-61
@@ -1,61 +1,61 @@
|
||||
{
|
||||
"name": "quetza-bot",
|
||||
"version": "4.2.0",
|
||||
"description": "Discord multipurpose bot",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node ./dist/index.js",
|
||||
"build": "pnpm exec tsc --build",
|
||||
"clean": "rm -rf ./dist && pnpm run build",
|
||||
"lint": "pnpm exec eslint . && pnpm exec prettier --check .",
|
||||
"format": "pnpm exec eslint --fix . && pnpm exec prettier --write .",
|
||||
"preinstall": "pnpm dlx only-allow pnpm"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"bot",
|
||||
"music"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/unknowableshade/quetza-bot.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "unknowableshade",
|
||||
"email": "60275123+unknowableshade@users.noreply.github.com",
|
||||
"url": "https://unknowableshade.ru"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/unknowableshade/quetza-bot/issues"
|
||||
},
|
||||
"readme": "https://github.com/unknowableshade/quetza-bot#readme",
|
||||
"engines": {
|
||||
"node": "18.x.x",
|
||||
"npm": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/voice": "^0.16.0",
|
||||
"@prisma/client": "4.13.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"prism-media": "^1.3.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"winston": "^3.8.2",
|
||||
"ytdl-core": "^4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
"@typescript-eslint/parser": "^5.59.5",
|
||||
"eslint": "^8.40.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": "^2.8.8",
|
||||
"prisma": "^4.13.0",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "quetza-bot",
|
||||
"version": "4.2.1",
|
||||
"description": "Discord multipurpose bot",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node ./dist/index.js",
|
||||
"build": "pnpm exec tsc --build",
|
||||
"clean": "rm -rf ./dist && pnpm run build",
|
||||
"lint": "pnpm exec eslint . && pnpm exec prettier --check .",
|
||||
"format": "pnpm exec eslint --fix . && pnpm exec prettier --write .",
|
||||
"preinstall": "pnpm dlx only-allow pnpm"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"bot",
|
||||
"music"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/unknowableshade/quetza-bot.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "unknowableshade",
|
||||
"email": "60275123+unknowableshade@users.noreply.github.com",
|
||||
"url": "https://unknowableshade.ru"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/unknowableshade/quetza-bot/issues"
|
||||
},
|
||||
"readme": "https://github.com/unknowableshade/quetza-bot#readme",
|
||||
"engines": {
|
||||
"node": "18.x.x",
|
||||
"npm": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/voice": "^0.16.0",
|
||||
"@prisma/client": "5.0.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"prism-media": "^1.3.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"winston": "^3.10.0",
|
||||
"ytdl-core": "^4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Generated
+3557
-3560
File diff suppressed because it is too large.
Load diff
+26
-26
@@ -1,26 +1,26 @@
|
||||
import { ActivityOptions, ActivityType, ColorResolvable } from "discord.js";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||||
const discordToken = process.env.DISCORD_TOKEN;
|
||||
const modulesDir = path.join(ROOT, "/modules/");
|
||||
const binariesDir = path.join(ROOT, "/../bin/");
|
||||
const activityStatus: ActivityOptions = { type: ActivityType.Watching, name: "over you" };
|
||||
const colors = {
|
||||
default: "#ff6e9b" as ColorResolvable,
|
||||
error: "#b94e59" as ColorResolvable,
|
||||
warning: "#e5925f" as ColorResolvable,
|
||||
info: "#367df6" as ColorResolvable,
|
||||
success: "#83d18e" as ColorResolvable
|
||||
};
|
||||
|
||||
const config = {
|
||||
discordToken,
|
||||
modulesDir,
|
||||
binariesDir,
|
||||
activityStatus,
|
||||
colors
|
||||
};
|
||||
|
||||
export default config;
|
||||
import { ActivityOptions, ActivityType, ColorResolvable } from "discord.js";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||||
const discordToken = process.env.DISCORD_TOKEN;
|
||||
const modulesDir = path.join(ROOT, "/modules/");
|
||||
const binariesDir = path.join(ROOT, "/../bin/");
|
||||
const activityStatus: ActivityOptions = { type: ActivityType.Watching, name: "over you" };
|
||||
const colors = {
|
||||
default: "#ff6e9b" as ColorResolvable,
|
||||
error: "#b94e59" as ColorResolvable,
|
||||
warning: "#e5925f" as ColorResolvable,
|
||||
info: "#367df6" as ColorResolvable,
|
||||
success: "#83d18e" as ColorResolvable
|
||||
};
|
||||
|
||||
const config = {
|
||||
discordToken,
|
||||
modulesDir,
|
||||
binariesDir,
|
||||
activityStatus,
|
||||
colors
|
||||
};
|
||||
|
||||
export default config;
|
||||
+14
-14
@@ -1,14 +1,14 @@
|
||||
import { GatewayIntentBits } from "discord.js";
|
||||
|
||||
import config from "./config.js";
|
||||
import Client from "./lib/client.js";
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildVoiceStates,
|
||||
GatewayIntentBits.GuildMembers
|
||||
]
|
||||
});
|
||||
|
||||
client.login(config.discordToken);
|
||||
import { GatewayIntentBits } from "discord.js";
|
||||
|
||||
import config from "./config.js";
|
||||
import Client from "./lib/client.js";
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildVoiceStates,
|
||||
GatewayIntentBits.GuildMembers
|
||||
]
|
||||
});
|
||||
|
||||
client.login(config.discordToken);
|
||||
+91
-91
@@ -1,91 +1,91 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { Client as DiscordClient, ClientOptions, Collection } from "discord.js";
|
||||
import { existsSync, lstatSync, readdirSync } from "fs";
|
||||
import path from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import config from "../config.js";
|
||||
import { Command, Event, Module, ModuleMetadata } from "./types.js";
|
||||
|
||||
const MODULE_ENTRY = "module.js";
|
||||
const MODULE_COMMANDS = "commands";
|
||||
const MODULE_EVENTS = "events";
|
||||
|
||||
export default class Client extends DiscordClient {
|
||||
public readonly commands = new Collection<string, Command>();
|
||||
|
||||
public readonly modules = new Collection<string, ModuleMetadata>();
|
||||
|
||||
public readonly db = new PrismaClient();
|
||||
|
||||
public constructor(options: ClientOptions) {
|
||||
super(options);
|
||||
|
||||
for (const module of readdirSync(config.modulesDir)) {
|
||||
const modulePath = path.join(config.modulesDir, module);
|
||||
|
||||
if (!lstatSync(modulePath).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
import(pathToFileURL(path.join(modulePath, MODULE_ENTRY)).toString()).then(
|
||||
(data: Module) => this.loadModule(data)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private loadModule(data: Module): void {
|
||||
const commands: string[] = [];
|
||||
const events: string[] = [];
|
||||
|
||||
function importModule(dir: string, importThen: (data: Command & Event) => void) {
|
||||
if (existsSync(dir)) {
|
||||
for (const source of readdirSync(dir)) {
|
||||
const file = path.join(dir, source);
|
||||
|
||||
if (!lstatSync(file).isFile() || !file.endsWith(".js")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
import(pathToFileURL(file).toString()).then(importThen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => {
|
||||
this.commands.set(command.data.name, command);
|
||||
commands.push(command.data.name);
|
||||
});
|
||||
|
||||
importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
|
||||
this.on(
|
||||
event.name,
|
||||
async (...eventee: unknown[]) => await event.execute(this, eventee)
|
||||
);
|
||||
events.push(event.name);
|
||||
});
|
||||
|
||||
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");
|
||||
const events = module.events.map((name) => " - " + name).join("\n");
|
||||
|
||||
return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n");
|
||||
});
|
||||
|
||||
const clientStatus =
|
||||
this.user && this.application
|
||||
? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
|
||||
: "✖ Application is not availiable.";
|
||||
|
||||
return modulesStatus.concat(clientStatus);
|
||||
}
|
||||
}
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { Client as DiscordClient, ClientOptions, Collection } from "discord.js";
|
||||
import { existsSync, lstatSync, readdirSync } from "fs";
|
||||
import path from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import config from "../config.js";
|
||||
import { Command, Event, Module, ModuleMetadata } from "./types.js";
|
||||
|
||||
const MODULE_ENTRY = "module.js";
|
||||
const MODULE_COMMANDS = "commands";
|
||||
const MODULE_EVENTS = "events";
|
||||
|
||||
export default class Client extends DiscordClient {
|
||||
public readonly commands = new Collection<string, Command>();
|
||||
|
||||
public readonly modules = new Collection<string, ModuleMetadata>();
|
||||
|
||||
public readonly db = new PrismaClient();
|
||||
|
||||
public constructor(options: ClientOptions) {
|
||||
super(options);
|
||||
|
||||
for (const module of readdirSync(config.modulesDir)) {
|
||||
const modulePath = path.join(config.modulesDir, module);
|
||||
|
||||
if (!lstatSync(modulePath).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
import(pathToFileURL(path.join(modulePath, MODULE_ENTRY)).toString()).then(
|
||||
(data: Module) => this.loadModule(data)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private loadModule(data: Module): void {
|
||||
const commands: string[] = [];
|
||||
const events: string[] = [];
|
||||
|
||||
function importModule(dir: string, importThen: (data: Command & Event) => void) {
|
||||
if (existsSync(dir)) {
|
||||
for (const source of readdirSync(dir)) {
|
||||
const file = path.join(dir, source);
|
||||
|
||||
if (!lstatSync(file).isFile() || !file.endsWith(".js")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
import(pathToFileURL(file).toString()).then(importThen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importModule(path.join(data.rootDir, MODULE_COMMANDS), (command: Command) => {
|
||||
this.commands.set(command.data.name, command);
|
||||
commands.push(command.data.name);
|
||||
});
|
||||
|
||||
importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
|
||||
this.on(
|
||||
event.name,
|
||||
async (...eventee: unknown[]) => await event.execute(this, eventee)
|
||||
);
|
||||
events.push(event.name);
|
||||
});
|
||||
|
||||
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");
|
||||
const events = module.events.map((name) => " - " + name).join("\n");
|
||||
|
||||
return [`${tag}\n`, "commands:", `${commands}`, "events:", `${events}`].join("\n");
|
||||
});
|
||||
|
||||
const clientStatus =
|
||||
this.user && this.application
|
||||
? `✓ ${this.application.id} successfully logged in as ${this.user.tag}.`
|
||||
: "✖ Application is not availiable.";
|
||||
|
||||
return modulesStatus.concat(clientStatus);
|
||||
}
|
||||
}
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
import winston from "winston";
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
transports: [
|
||||
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(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default logger;
|
||||
import winston from "winston";
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
transports: [
|
||||
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(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default logger;
|
||||
+27
-27
@@ -1,27 +1,27 @@
|
||||
import { ApplicationCommandData, CommandInteraction } from "discord.js";
|
||||
|
||||
import Client from "./client.js";
|
||||
|
||||
export interface Module {
|
||||
name: string;
|
||||
description?: string;
|
||||
author: string;
|
||||
tag: string;
|
||||
controller?: unknown;
|
||||
rootDir: string;
|
||||
}
|
||||
|
||||
export interface ModuleMetadata extends Module {
|
||||
commands: string[];
|
||||
events: string[];
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
data: ApplicationCommandData;
|
||||
execute: (client: Client, interaction: CommandInteraction) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
name: string;
|
||||
execute: (client: Client, eventee: unknown[]) => Promise<void>;
|
||||
}
|
||||
import { ApplicationCommandData, CommandInteraction } from "discord.js";
|
||||
|
||||
import Client from "./client.js";
|
||||
|
||||
export interface Module {
|
||||
name: string;
|
||||
description?: string;
|
||||
author: string;
|
||||
tag: string;
|
||||
controller?: unknown;
|
||||
rootDir: string;
|
||||
}
|
||||
|
||||
export interface ModuleMetadata extends Module {
|
||||
commands: string[];
|
||||
events: string[];
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
data: ApplicationCommandData;
|
||||
execute: (client: Client, interaction: CommandInteraction) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
name: string;
|
||||
execute: (client: Client, eventee: unknown[]) => Promise<void>;
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply(replies.modules(client.modules));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("modules")
|
||||
.setDescription("List of availiable modules.");
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply(replies.modules(client.modules));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("modules")
|
||||
.setDescription("List of availiable modules.");
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,23 +1,23 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const reply = await interaction.reply({ ...replies.ping(client.ws.ping), fetchReply: true });
|
||||
|
||||
await interaction.editReply(
|
||||
replies.ping(client.ws.ping, reply.createdTimestamp - interaction.createdTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me.");
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const reply = await interaction.reply({ ...replies.ping(client.ws.ping), fetchReply: true });
|
||||
|
||||
await interaction.editReply(
|
||||
replies.ping(client.ws.ping, reply.createdTimestamp - interaction.createdTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder().setName("ping").setDescription("Try to ping me.");
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,32 +1,32 @@
|
||||
import { Events, Interaction } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
|
||||
async function execute(client: Client, eventee: [Interaction]): Promise<void> {
|
||||
const [interaction] = eventee;
|
||||
|
||||
if (interaction.isChatInputCommand()) {
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
logger.error(`No command matching ${interaction.commandName} was found.`, {
|
||||
interaction
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Interaction ${interaction.commandName} was created.`, interaction);
|
||||
|
||||
await command.execute(client, interaction);
|
||||
} catch (error) {
|
||||
logger.error("Interaction error occured. ", { error, interaction });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const name = Events.InteractionCreate;
|
||||
|
||||
export { execute, name };
|
||||
import { Events, Interaction } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
|
||||
async function execute(client: Client, eventee: [Interaction]): Promise<void> {
|
||||
const [interaction] = eventee;
|
||||
|
||||
if (interaction.isChatInputCommand()) {
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
logger.error(`No command matching ${interaction.commandName} was found.`, {
|
||||
interaction
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Interaction ${interaction.commandName} was created.`, interaction);
|
||||
|
||||
await command.execute(client, interaction);
|
||||
} catch (error) {
|
||||
logger.error("Interaction error occured. ", { error, interaction });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const name = Events.InteractionCreate;
|
||||
|
||||
export { execute, name };
|
||||
@@ -1,37 +1,37 @@
|
||||
import { generateDependencyReport } from "@discordjs/voice";
|
||||
import { Events } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
|
||||
async function execute(client: Client): Promise<void> {
|
||||
if (!client.user || !client.application) {
|
||||
throw new Error("No application or user for it is provided.");
|
||||
}
|
||||
|
||||
const commandData = Array.from(client.commands.values()).map((value) => value.data);
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.TEST_GUILD) {
|
||||
logger.info("Development mode: Commands will be pushed to the test guild!");
|
||||
|
||||
logger.info(generateDependencyReport());
|
||||
|
||||
const guild = await client.guilds.fetch(process.env.TEST_GUILD);
|
||||
guild.commands.set(commandData);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
logger.info("Production mode: Commands will be pushed to the application!");
|
||||
|
||||
await client.application.commands.set(commandData);
|
||||
}
|
||||
|
||||
client.generateApplicationStatus().forEach((report) => logger.info(report));
|
||||
|
||||
client.user.setActivity(config.activityStatus);
|
||||
}
|
||||
|
||||
const name = Events.ClientReady;
|
||||
|
||||
export { execute, name };
|
||||
import { generateDependencyReport } from "@discordjs/voice";
|
||||
import { Events } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
|
||||
async function execute(client: Client): Promise<void> {
|
||||
if (!client.user || !client.application) {
|
||||
throw new Error("No application or user for it is provided.");
|
||||
}
|
||||
|
||||
const commandData = Array.from(client.commands.values()).map((value) => value.data);
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.TEST_GUILD) {
|
||||
logger.info("Development mode: Commands will be pushed to the test guild!");
|
||||
|
||||
logger.info(generateDependencyReport());
|
||||
|
||||
const guild = await client.guilds.fetch(process.env.TEST_GUILD);
|
||||
guild.commands.set(commandData);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
logger.info("Production mode: Commands will be pushed to the application!");
|
||||
|
||||
await client.application.commands.set(commandData);
|
||||
}
|
||||
|
||||
client.generateApplicationStatus().forEach((report) => logger.info(report));
|
||||
|
||||
client.user.setActivity(config.activityStatus);
|
||||
}
|
||||
|
||||
const name = Events.ClientReady;
|
||||
|
||||
export { execute, name };
|
||||
@@ -1,36 +1,36 @@
|
||||
import { BaseMessageOptions, Collection, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import { ModuleMetadata } from "../../../lib/types.js";
|
||||
|
||||
const replies = {
|
||||
modules: (modules: Collection<string, ModuleMetadata>): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("Applied modules")
|
||||
.addFields(
|
||||
modules.map((value) => {
|
||||
return {
|
||||
name: value.name + "@" + value.tag,
|
||||
value: italic(value.description ?? "")
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
ping: (websocket: number, roundtrip?: number): BaseMessageOptions => {
|
||||
const title = roundtrip ? `I've pinged ${roundtrip} ms. late` : "Pinging...";
|
||||
|
||||
const comment = `Websocket ping is ${websocket} ms.`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle(title)
|
||||
.setDescription(italic(comment));
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
import { BaseMessageOptions, Collection, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import { ModuleMetadata } from "../../../lib/types.js";
|
||||
|
||||
const replies = {
|
||||
modules: (modules: Collection<string, ModuleMetadata>): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("Applied modules")
|
||||
.addFields(
|
||||
modules.map((value) => {
|
||||
return {
|
||||
name: value.name + "@" + value.tag,
|
||||
value: italic(value.description ?? "")
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
ping: (websocket: number, roundtrip?: number): BaseMessageOptions => {
|
||||
const title = roundtrip ? `I've pinged ${roundtrip} ms. late` : "Pinging...";
|
||||
|
||||
const comment = `Websocket ping is ${websocket} ms.`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle(title)
|
||||
.setDescription(italic(comment));
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
+11
-11
@@ -1,11 +1,11 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const name = "core";
|
||||
const description = "Quetza core functionality";
|
||||
const author = "unknowableshade";
|
||||
const tag = "4.2.0";
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { author, description, name, rootDir, tag };
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const name = "core";
|
||||
const description = "Quetza core functionality";
|
||||
const author = "unknowableshade";
|
||||
const tag = "4.2.0";
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { author, description, name, rootDir, tag };
|
||||
@@ -1,35 +1,35 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = player.queue.tracks.length;
|
||||
|
||||
player.clear();
|
||||
|
||||
await interaction.reply(replies.cleared(amount));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("clear")
|
||||
.setDescription("Clear the player's queue.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = player.queue.tracks.length;
|
||||
|
||||
player.clear();
|
||||
|
||||
await interaction.reply(replies.cleared(amount));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("clear")
|
||||
.setDescription("Clear the player's queue.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,43 +1,43 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const player =
|
||||
controller.get(interaction.guild, interaction.channel) ??
|
||||
controller.set(interaction.guild, interaction.channel);
|
||||
|
||||
const channel = interaction.member.voice.channel;
|
||||
|
||||
if (!channel) {
|
||||
controller.delete(interaction.guildId);
|
||||
|
||||
await interaction.editReply(replies.notConnected());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.connect(channel);
|
||||
|
||||
await interaction.editReply(replies.okConnected(channel.name));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("connect")
|
||||
.setDescription("Connect me explicitly to the voice channel.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const player =
|
||||
controller.get(interaction.guild, interaction.channel) ??
|
||||
controller.set(interaction.guild, interaction.channel);
|
||||
|
||||
const channel = interaction.member.voice.channel;
|
||||
|
||||
if (!channel) {
|
||||
controller.delete(interaction.guildId);
|
||||
|
||||
await interaction.editReply(replies.notConnected());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.connect(channel);
|
||||
|
||||
await interaction.editReply(replies.okConnected(channel.name));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("connect")
|
||||
.setDescription("Connect me explicitly to the voice channel.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,46 +1,46 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Filter from "../lib/filter.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = interaction.options.getString("filter") ?? undefined;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const status = player.setFilter(filter);
|
||||
|
||||
await interaction.reply(replies.filtered(filter, status));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("filter")
|
||||
.setDescription("Apply unique filters to the playback.")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("filter")
|
||||
.setDescription("Filter to apply.")
|
||||
.setChoices(
|
||||
...Object.keys(Filter.filters).map((key) => {
|
||||
return { name: key, value: key };
|
||||
})
|
||||
)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Filter from "../lib/filter.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = interaction.options.getString("filter") ?? undefined;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const status = player.setFilter(filter);
|
||||
|
||||
await interaction.reply(replies.filtered(filter, status));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("filter")
|
||||
.setDescription("Apply unique filters to the playback.")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("filter")
|
||||
.setDescription("Filter to apply.")
|
||||
.setChoices(
|
||||
...Object.keys(Filter.filters).map((key) => {
|
||||
return { name: key, value: key };
|
||||
})
|
||||
)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,31 +1,31 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply(replies.info(player));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("info")
|
||||
.setDescription("Verbose information about player and stats.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply(replies.info(player));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("info")
|
||||
.setDescription("Verbose information about player and stats.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,44 +1,44 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query", true);
|
||||
const pos = interaction.options.getInteger("position", true);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.editReply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const track = await player.add(query, interaction.user, pos - 1);
|
||||
|
||||
await interaction.editReply(replies.appended(track));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("insert")
|
||||
.setDescription("Insert a new track to specific position in the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Title or URL of the song.").setRequired(true)
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option.setName("position").setDescription("Position to insert.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query", true);
|
||||
const pos = interaction.options.getInteger("position", true);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.editReply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const track = await player.add(query, interaction.user, pos - 1);
|
||||
|
||||
await interaction.editReply(replies.appended(track));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("insert")
|
||||
.setDescription("Insert a new track to specific position in the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Title or URL of the song.").setRequired(true)
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option.setName("position").setDescription("Position to insert.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,43 +1,43 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query", true);
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = player.resource?.metadata;
|
||||
|
||||
const position =
|
||||
!isNaN(+query) && isFinite(+query) && !/e/i.test(query)
|
||||
? player.jump(parseInt(query) - 1)
|
||||
: player.jump(query);
|
||||
|
||||
await interaction.reply(replies.jumped(previous, position));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("jump")
|
||||
.setDescription("Jump to specific track in the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Position or title to jump to.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query", true);
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = player.resource?.metadata;
|
||||
|
||||
const position =
|
||||
!isNaN(+query) && isFinite(+query) && !/e/i.test(query)
|
||||
? player.jump(parseInt(query) - 1)
|
||||
: player.jump(query);
|
||||
|
||||
await interaction.reply(replies.jumped(previous, position));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("jump")
|
||||
.setDescription("Jump to specific track in the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Position or title to jump to.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,48 +1,48 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { LoopOption } from "../lib/types.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const option = interaction.options.getString("option", true) as LoopOption;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.queue.loop = option;
|
||||
|
||||
await interaction.reply(replies.looped(option));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("loop")
|
||||
.setDescription("Select player's loop mode.")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("option")
|
||||
.setDescription("Loop option.")
|
||||
.setChoices(
|
||||
{ name: "Loop queue", value: "LOOP" },
|
||||
{ name: "Loop single track", value: "SONG" },
|
||||
{ name: "Drop on final", value: "NONE" },
|
||||
{ name: "Random", value: "AUTO" }
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { LoopOption } from "../lib/types.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const option = interaction.options.getString("option", true) as LoopOption;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.queue.loop = option;
|
||||
|
||||
await interaction.reply(replies.looped(option));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("loop")
|
||||
.setDescription("Select player's loop mode.")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("option")
|
||||
.setDescription("Loop option.")
|
||||
.setChoices(
|
||||
{ name: "Loop queue", value: "LOOP" },
|
||||
{ name: "Loop single track", value: "SONG" },
|
||||
{ name: "Drop on final", value: "NONE" },
|
||||
{ name: "Random", value: "AUTO" }
|
||||
)
|
||||
.setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.skip();
|
||||
|
||||
await interaction.reply(replies.skipped());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("next")
|
||||
.setDescription("Skip current song.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.skip();
|
||||
|
||||
await interaction.reply(replies.skipped());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("next")
|
||||
.setDescription("Skip current song.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const state = player.pause();
|
||||
|
||||
await interaction.reply(replies.paused(state));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("pause")
|
||||
.setDescription("Toggle pause on playback.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const state = player.pause();
|
||||
|
||||
await interaction.reply(replies.paused(state));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("pause")
|
||||
.setDescription("Toggle pause on playback.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,58 +1,58 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query");
|
||||
|
||||
if (query) {
|
||||
await interaction.reply(replies.searching());
|
||||
} else {
|
||||
await interaction.deferReply();
|
||||
}
|
||||
|
||||
const player =
|
||||
controller.get(interaction.guild, interaction.channel) ??
|
||||
controller.set(interaction.guild, interaction.channel);
|
||||
|
||||
const channel = interaction.member.voice.channel;
|
||||
|
||||
if (!channel) {
|
||||
controller.delete(interaction.guildId);
|
||||
|
||||
await interaction.editReply(replies.notConnected());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.connect(channel);
|
||||
|
||||
if (query) {
|
||||
const track = await player.add(query, interaction.user);
|
||||
|
||||
await interaction.editReply(replies.appended(track));
|
||||
}
|
||||
|
||||
if (!player.resource) {
|
||||
await player.play();
|
||||
}
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("play")
|
||||
.setDescription("Launch player and add songs to the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Title or URL of the song.")
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query");
|
||||
|
||||
if (query) {
|
||||
await interaction.reply(replies.searching());
|
||||
} else {
|
||||
await interaction.deferReply();
|
||||
}
|
||||
|
||||
const player =
|
||||
controller.get(interaction.guild, interaction.channel) ??
|
||||
controller.set(interaction.guild, interaction.channel);
|
||||
|
||||
const channel = interaction.member.voice.channel;
|
||||
|
||||
if (!channel) {
|
||||
controller.delete(interaction.guildId);
|
||||
|
||||
await interaction.editReply(replies.notConnected());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.connect(channel);
|
||||
|
||||
if (query) {
|
||||
const track = await player.add(query, interaction.user);
|
||||
|
||||
await interaction.editReply(replies.appended(track));
|
||||
}
|
||||
|
||||
if (!player.resource) {
|
||||
await player.play();
|
||||
}
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("play")
|
||||
.setDescription("Launch player and add songs to the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Title or URL of the song.")
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,86 +1,86 @@
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonInteraction,
|
||||
ButtonStyle,
|
||||
ChatInputCommandInteraction,
|
||||
MessageActionRowComponentBuilder,
|
||||
SlashCommandBuilder
|
||||
} from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.editReply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.queue.empty()) {
|
||||
await interaction.editReply(replies.queue(player.queue));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let position = 1;
|
||||
|
||||
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
|
||||
new ButtonBuilder().setCustomId("Top").setLabel("🔼").setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder().setCustomId("Up").setLabel("🔺").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("Down").setLabel("🔻").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("End").setLabel("🔽").setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
|
||||
const message = await interaction.editReply({
|
||||
...replies.queue(player.queue, 0, player.resource),
|
||||
components: [row]
|
||||
});
|
||||
|
||||
const collector = message.createMessageComponentCollector({ time: 30000 });
|
||||
|
||||
collector.on("collect", async (btn: ButtonInteraction) => {
|
||||
switch (btn.customId) {
|
||||
case "Top": {
|
||||
position = 1;
|
||||
break;
|
||||
}
|
||||
case "Up": {
|
||||
position = Math.max(position - 10, 1);
|
||||
break;
|
||||
}
|
||||
case "Down": {
|
||||
position = Math.min(
|
||||
position + 10,
|
||||
Math.floor(player.queue.tracks.length / 10) * 10
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "End": {
|
||||
position = Math.floor(player.queue.tracks.length / 10) * 10;
|
||||
}
|
||||
}
|
||||
|
||||
await btn.update(replies.queue(player.queue, position - 1, player.resource));
|
||||
});
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("queue")
|
||||
.setDescription("Print interactive queue pointing to current track.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonInteraction,
|
||||
ButtonStyle,
|
||||
ChatInputCommandInteraction,
|
||||
MessageActionRowComponentBuilder,
|
||||
SlashCommandBuilder
|
||||
} from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.editReply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.queue.empty()) {
|
||||
await interaction.editReply(replies.queue(player.queue));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let position = 1;
|
||||
|
||||
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
|
||||
new ButtonBuilder().setCustomId("Top").setLabel("🔼").setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder().setCustomId("Up").setLabel("🔺").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("Down").setLabel("🔻").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("End").setLabel("🔽").setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
|
||||
const message = await interaction.editReply({
|
||||
...replies.queue(player.queue, 0, player.resource),
|
||||
components: [row]
|
||||
});
|
||||
|
||||
const collector = message.createMessageComponentCollector({ time: 30000 });
|
||||
|
||||
collector.on("collect", async (btn: ButtonInteraction) => {
|
||||
switch (btn.customId) {
|
||||
case "Top": {
|
||||
position = 1;
|
||||
break;
|
||||
}
|
||||
case "Up": {
|
||||
position = Math.max(position - 10, 1);
|
||||
break;
|
||||
}
|
||||
case "Down": {
|
||||
position = Math.min(
|
||||
position + 10,
|
||||
Math.floor(player.queue.tracks.length / 10) * 10
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "End": {
|
||||
position = Math.floor(player.queue.tracks.length / 10) * 10;
|
||||
}
|
||||
}
|
||||
|
||||
await btn.update(replies.queue(player.queue, position - 1, player.resource));
|
||||
});
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("queue")
|
||||
.setDescription("Print interactive queue pointing to current track.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,41 +1,41 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query", true);
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const track =
|
||||
!isNaN(+query) && isFinite(+query) && !/e/i.test(query)
|
||||
? player.remove(parseInt(query) - 1)
|
||||
: player.remove(query);
|
||||
|
||||
await interaction.reply(replies.removed(track));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("remove")
|
||||
.setDescription("Remove specific track in the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Position or title to remove.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const query = interaction.options.getString("query", true);
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const track =
|
||||
!isNaN(+query) && isFinite(+query) && !/e/i.test(query)
|
||||
? player.remove(parseInt(query) - 1)
|
||||
: player.remove(query);
|
||||
|
||||
await interaction.reply(replies.removed(track));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("remove")
|
||||
.setDescription("Remove specific track in the queue.")
|
||||
.addStringOption((option) =>
|
||||
option.setName("query").setDescription("Position or title to remove.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.shuffle();
|
||||
|
||||
await interaction.reply(replies.reshuffle());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("reshuffle")
|
||||
.setDescription("Reshuffle the queue. (Will change queue order)")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.shuffle();
|
||||
|
||||
await interaction.reply(replies.reshuffle());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("reshuffle")
|
||||
.setDescription("Reshuffle the queue. (Will change queue order)")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,50 +1,50 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = interaction.options.getInteger("hrs") ?? 0;
|
||||
const minutes = interaction.options.getInteger("mins") ?? 0;
|
||||
const seconds = interaction.options.getInteger("secs") ?? 0;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.resource) {
|
||||
await interaction.reply(replies.notPlaying());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const prevTime = player.resource.playbackDuration / 1000;
|
||||
|
||||
const nextTime = hours * 3600 + minutes * 60 + seconds;
|
||||
|
||||
player.seek(nextTime * 1000);
|
||||
|
||||
await interaction.reply(replies.seeked(prevTime, nextTime));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("seek")
|
||||
.setDescription("Fast-forward/Fast-reverse by the given time.")
|
||||
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
|
||||
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
|
||||
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."))
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = interaction.options.getInteger("hrs") ?? 0;
|
||||
const minutes = interaction.options.getInteger("mins") ?? 0;
|
||||
const seconds = interaction.options.getInteger("secs") ?? 0;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.resource) {
|
||||
await interaction.reply(replies.notPlaying());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const prevTime = player.resource.playbackDuration / 1000;
|
||||
|
||||
const nextTime = hours * 3600 + minutes * 60 + seconds;
|
||||
|
||||
player.seek(nextTime * 1000);
|
||||
|
||||
await interaction.reply(replies.seeked(prevTime, nextTime));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("seek")
|
||||
.setDescription("Fast-forward/Fast-reverse by the given time.")
|
||||
.addIntegerOption((option) => option.setName("hrs").setDescription("Hours to seek."))
|
||||
.addIntegerOption((option) => option.setName("mins").setDescription("Minutes to seek."))
|
||||
.addIntegerOption((option) => option.setName("secs").setDescription("Seconds to seek."))
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.destroy();
|
||||
|
||||
await interaction.reply(replies.destroyed());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("stop")
|
||||
.setDescription("Destroy the player.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.destroy();
|
||||
|
||||
await interaction.reply(replies.destroyed());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("stop")
|
||||
.setDescription("Destroy the player.")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,38 +1,38 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = interaction.options.getInteger("volume") ?? 100;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.volume = volume;
|
||||
|
||||
await interaction.reply(replies.volumeSet(player.volume));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("volume")
|
||||
.setDescription("Change player's volume amount.")
|
||||
.addIntegerOption((option) =>
|
||||
option.setName("volume").setDescription("Volume in percents.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction) {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = interaction.options.getInteger("volume") ?? 100;
|
||||
|
||||
const player = controller.get(interaction.guild, interaction.channel);
|
||||
|
||||
if (!player) {
|
||||
await interaction.reply(replies.notExists());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
player.volume = volume;
|
||||
|
||||
await interaction.reply(replies.volumeSet(player.volume));
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("volume")
|
||||
.setDescription("Change player's volume amount.")
|
||||
.addIntegerOption((option) =>
|
||||
option.setName("volume").setDescription("Volume in percents.").setRequired(true)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,36 +1,36 @@
|
||||
import { VoiceState } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import { asleep } from "../lib/misc.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
const WAIT_TIME = 30_000;
|
||||
|
||||
async function execute(client: Client, eventee: [VoiceState]): Promise<void> {
|
||||
const [oldState] = eventee;
|
||||
|
||||
const player = controller.get(oldState.guild);
|
||||
|
||||
if (!player || !oldState.channel || !client.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
oldState.channel.members.size === 1 &&
|
||||
oldState.channel.members.first()?.user === client.user
|
||||
) {
|
||||
await asleep(WAIT_TIME);
|
||||
if (
|
||||
oldState.channel.members.size === 1 &&
|
||||
oldState.channel.members.first()?.user === client.user
|
||||
) {
|
||||
await player.channel.send(replies.alone());
|
||||
player.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const name = "voiceStateUpdate";
|
||||
|
||||
export { execute, name };
|
||||
import { VoiceState } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import { asleep } from "../lib/misc.js";
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
const WAIT_TIME = 30_000;
|
||||
|
||||
async function execute(client: Client, eventee: [VoiceState]): Promise<void> {
|
||||
const [oldState] = eventee;
|
||||
|
||||
const player = controller.get(oldState.guild);
|
||||
|
||||
if (!player || !oldState.channel || !client.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
oldState.channel.members.size === 1 &&
|
||||
oldState.channel.members.first()?.user === client.user
|
||||
) {
|
||||
await asleep(WAIT_TIME);
|
||||
if (
|
||||
oldState.channel.members.size === 1 &&
|
||||
oldState.channel.members.first()?.user === client.user
|
||||
) {
|
||||
await player.channel.send(replies.alone());
|
||||
player.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const name = "voiceStateUpdate";
|
||||
|
||||
export { execute, name };
|
||||
+245
-245
@@ -1,245 +1,245 @@
|
||||
import { execFile } from "child_process";
|
||||
import path from "path";
|
||||
import prismMedia from "prism-media";
|
||||
import { Duplex, Readable } from "stream";
|
||||
import { promisify } from "util";
|
||||
import ytdl from "ytdl-core";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import { StreamOptions, YtdlDump, YtdlResult, YtdlStreamOptions } from "./types.js";
|
||||
|
||||
const { opus: Opus, FFmpeg } = prismMedia;
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SEARCH_DEPTH = 5;
|
||||
const YTDL_EVENTS = [
|
||||
"info",
|
||||
"progress",
|
||||
"abort",
|
||||
"request",
|
||||
"response",
|
||||
"error",
|
||||
"redirect",
|
||||
"retry",
|
||||
"reconnect"
|
||||
];
|
||||
|
||||
function isURL(url: string): boolean {
|
||||
return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test(
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> {
|
||||
if (depth === SEARCH_DEPTH) {
|
||||
logger.warn("yt-dlp failed to find anything.", { query });
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const executable = await execFileAsync(path.join(config.binariesDir, "/yt-dlp"), [
|
||||
query,
|
||||
"-O %(.{id,original_url,title,uploader,duration,extractor_key,thumbnail})j",
|
||||
"-s",
|
||||
"--flat-playlist"
|
||||
]);
|
||||
|
||||
const dump: YtdlDump[] = JSON.parse("[" + executable.stdout.replace(/\n(?=.+)/g, ",") + "]");
|
||||
|
||||
const result: YtdlResult[] = [];
|
||||
|
||||
for (const entry of dump) {
|
||||
if (entry.title && entry.thumbnail && entry.duration && entry.extractor_key === "Youtube") {
|
||||
result.push({
|
||||
url: entry.original_url,
|
||||
title: entry.title,
|
||||
thumbnail: entry.thumbnail,
|
||||
duration: entry.duration
|
||||
});
|
||||
|
||||
logger.info("yt-dlp has found corresponding youtube video.", {
|
||||
result: result[result.length - 1]
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let unwind: YtdlResult[];
|
||||
|
||||
if (entry.extractor_key !== "Youtube" && entry.title) {
|
||||
unwind = await fetchInfoRecursive(
|
||||
`ytsearch1:${entry.uploader ? entry.uploader + " - " : ""} ${entry.title}`,
|
||||
depth + 1
|
||||
);
|
||||
} else {
|
||||
unwind = await fetchInfoRecursive(entry.original_url, depth + 1);
|
||||
}
|
||||
|
||||
if (unwind.length !== 0) {
|
||||
result.push(unwind[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchInfo(query: string): Promise<YtdlResult[]> {
|
||||
logger.info("yt-dlp was invoked.", { query });
|
||||
|
||||
try {
|
||||
if (!isURL(query)) {
|
||||
return await fetchInfoRecursive("ytsearch1:" + query);
|
||||
}
|
||||
|
||||
return await fetchInfoRecursive(query);
|
||||
} catch (error) {
|
||||
logger.error("yt-dlp execution invoked error.", { error });
|
||||
|
||||
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;
|
||||
}
|
||||
import { execFile } from "child_process";
|
||||
import path from "path";
|
||||
import prismMedia from "prism-media";
|
||||
import { Duplex, Readable } from "stream";
|
||||
import { promisify } from "util";
|
||||
import ytdl from "ytdl-core";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import { StreamOptions, YtdlDump, YtdlResult, YtdlStreamOptions } from "./types.js";
|
||||
|
||||
const { opus: Opus, FFmpeg } = prismMedia;
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SEARCH_DEPTH = 5;
|
||||
const YTDL_EVENTS = [
|
||||
"info",
|
||||
"progress",
|
||||
"abort",
|
||||
"request",
|
||||
"response",
|
||||
"error",
|
||||
"redirect",
|
||||
"retry",
|
||||
"reconnect"
|
||||
];
|
||||
|
||||
function isURL(url: string): boolean {
|
||||
return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test(
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> {
|
||||
if (depth === SEARCH_DEPTH) {
|
||||
logger.warn("yt-dlp failed to find anything.", { query });
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const executable = await execFileAsync(path.join(config.binariesDir, "/yt-dlp"), [
|
||||
query,
|
||||
"-O %(.{id,original_url,title,uploader,duration,extractor_key,thumbnail})j",
|
||||
"-s",
|
||||
"--flat-playlist"
|
||||
]);
|
||||
|
||||
const dump: YtdlDump[] = JSON.parse("[" + executable.stdout.replace(/\n(?=.+)/g, ",") + "]");
|
||||
|
||||
const result: YtdlResult[] = [];
|
||||
|
||||
for (const entry of dump) {
|
||||
if (entry.title && entry.thumbnail && entry.duration && entry.extractor_key === "Youtube") {
|
||||
result.push({
|
||||
url: entry.original_url,
|
||||
title: entry.title,
|
||||
thumbnail: entry.thumbnail,
|
||||
duration: entry.duration
|
||||
});
|
||||
|
||||
logger.info("yt-dlp has found corresponding youtube video.", {
|
||||
result: result[result.length - 1]
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let unwind: YtdlResult[];
|
||||
|
||||
if (entry.extractor_key !== "Youtube" && entry.title) {
|
||||
unwind = await fetchInfoRecursive(
|
||||
`ytsearch1:${entry.uploader ? entry.uploader + " - " : ""} ${entry.title}`,
|
||||
depth + 1
|
||||
);
|
||||
} else {
|
||||
unwind = await fetchInfoRecursive(entry.original_url, depth + 1);
|
||||
}
|
||||
|
||||
if (unwind.length !== 0) {
|
||||
result.push(unwind[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchInfo(query: string): Promise<YtdlResult[]> {
|
||||
logger.info("yt-dlp was invoked.", { query });
|
||||
|
||||
try {
|
||||
if (!isURL(query)) {
|
||||
return await fetchInfoRecursive("ytsearch1:" + query);
|
||||
}
|
||||
|
||||
return await fetchInfoRecursive(query);
|
||||
} catch (error) {
|
||||
logger.error("yt-dlp execution invoked error.", { error });
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,66 +1,66 @@
|
||||
export default class Filter {
|
||||
public static readonly filters = {
|
||||
"Bassboost 1.5": "bass=g=15: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",
|
||||
"8D": "apulsator=hz=0.09",
|
||||
Daycore: "aresample=48000,asetrate=48000*0.75",
|
||||
Nightcore: "aresample=48000,asetrate=48000*1.25",
|
||||
Phaser: "aphaser=in_gain=0.4",
|
||||
Tremolo: "tremolo",
|
||||
Vibrato: "vibrato=f=6.5",
|
||||
Reverse: "areverse",
|
||||
Treble: "treble=g=5",
|
||||
"Normalizer 1": "dynaudnorm=g=101",
|
||||
"Normalizer 2": "acompressor",
|
||||
"Surround Sound": "surround",
|
||||
Pulsator: "apulsator=hz=1",
|
||||
Subboost: "asubboost",
|
||||
Karaoke: "stereotools=mlev=0.03",
|
||||
Compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6",
|
||||
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",
|
||||
"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",
|
||||
"Fade In": "afade=t=in:ss=0:d=10",
|
||||
Dimming:
|
||||
"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"
|
||||
};
|
||||
|
||||
private switches_ = new Set<string>();
|
||||
|
||||
public toggled(): [string, string][] {
|
||||
return Object.entries(Filter.filters)
|
||||
.filter((value) => this.switches_.has(value[0]))
|
||||
.map((value) => [value[0], value[1]]);
|
||||
}
|
||||
|
||||
public toggle(query?: string): boolean {
|
||||
if (!query) {
|
||||
this.switches_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switches_.delete(query) && Object.hasOwn(Filter.filters, query)) {
|
||||
this.switches_.add(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public size(): number {
|
||||
return this.switches_.size;
|
||||
}
|
||||
|
||||
public empty(): boolean {
|
||||
return this.size() === 0;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.toggled()
|
||||
.map((value) => value[1])
|
||||
.join(",");
|
||||
}
|
||||
}
|
||||
export default class Filter {
|
||||
public static readonly filters = {
|
||||
"Bassboost 1.5": "bass=g=15: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",
|
||||
"8D": "apulsator=hz=0.09",
|
||||
Daycore: "aresample=48000,asetrate=48000*0.75",
|
||||
Nightcore: "aresample=48000,asetrate=48000*1.25",
|
||||
Phaser: "aphaser=in_gain=0.4",
|
||||
Tremolo: "tremolo",
|
||||
Vibrato: "vibrato=f=6.5",
|
||||
Reverse: "areverse",
|
||||
Treble: "treble=g=5",
|
||||
"Normalizer 1": "dynaudnorm=g=101",
|
||||
"Normalizer 2": "acompressor",
|
||||
"Surround Sound": "surround",
|
||||
Pulsator: "apulsator=hz=1",
|
||||
Subboost: "asubboost",
|
||||
Karaoke: "stereotools=mlev=0.03",
|
||||
Compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6",
|
||||
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",
|
||||
"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",
|
||||
"Fade In": "afade=t=in:ss=0:d=10",
|
||||
Dimming:
|
||||
"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"
|
||||
};
|
||||
|
||||
private switches_ = new Set<string>();
|
||||
|
||||
public toggled(): [string, string][] {
|
||||
return Object.entries(Filter.filters)
|
||||
.filter((value) => this.switches_.has(value[0]))
|
||||
.map((value) => [value[0], value[1]]);
|
||||
}
|
||||
|
||||
public toggle(query?: string): boolean {
|
||||
if (!query) {
|
||||
this.switches_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switches_.delete(query) && Object.hasOwn(Filter.filters, query)) {
|
||||
this.switches_.add(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public size(): number {
|
||||
return this.switches_.size;
|
||||
}
|
||||
|
||||
public empty(): boolean {
|
||||
return this.size() === 0;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.toggled()
|
||||
.map((value) => value[1])
|
||||
.join(",");
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,59 @@
|
||||
import { AudioResource } from "@discordjs/voice";
|
||||
|
||||
import { LoopOption, Track } from "./types.js";
|
||||
|
||||
const STATUSBAR_LENGTH = 15;
|
||||
|
||||
export function statusBarGenerator(resource?: AudioResource<Track>): string {
|
||||
const duration = resource?.metadata.duration || 1;
|
||||
const playback = Math.min((resource?.playbackDuration || 0) / 1000, duration);
|
||||
|
||||
const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹");
|
||||
|
||||
const empty = Array<string>(STATUSBAR_LENGTH - filled.length).fill("▫️");
|
||||
|
||||
return `[${filled.join("")}${empty.join("")}] ${toISOTime(playback)}/${toISOTime(duration)}`;
|
||||
}
|
||||
|
||||
export function toISOTime(amount: number): string {
|
||||
const h = Math.floor(amount / 3600);
|
||||
const m = Math.floor((amount % 3600) / 60);
|
||||
const s = Math.floor((amount % 3600) % 60);
|
||||
|
||||
const hDisplay = h > 0 ? `${h}:` : "";
|
||||
const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`;
|
||||
const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`;
|
||||
|
||||
return hDisplay + mDisplay + sDisplay;
|
||||
}
|
||||
|
||||
export async function asleep(amount: number): Promise<unknown> {
|
||||
return new Promise((r) => setTimeout(r, amount));
|
||||
}
|
||||
|
||||
export function volumeMoji(amount: number): string {
|
||||
return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇";
|
||||
}
|
||||
|
||||
export function loopMoji(loop: LoopOption): string {
|
||||
return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂";
|
||||
}
|
||||
|
||||
export function largestCommonSequence(s1: string, s2: string) {
|
||||
s1 = s1.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));
|
||||
|
||||
for (let i = 1; i <= s1.length; i++) {
|
||||
for (let j = 1; j <= s2.length; j++) {
|
||||
if (s1[i - 1] === s2[j - 1]) {
|
||||
buffer[i][j] = buffer[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
buffer[i][j] = Math.max(buffer[i - 1][j], buffer[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer[s1.length][s2.length];
|
||||
}
|
||||
import { AudioResource } from "@discordjs/voice";
|
||||
|
||||
import { LoopOption, Track } from "./types.js";
|
||||
|
||||
const STATUSBAR_LENGTH = 15;
|
||||
|
||||
export function statusBarGenerator(resource?: AudioResource<Track>): string {
|
||||
const duration = resource?.metadata.duration || 1;
|
||||
const playback = Math.min((resource?.playbackDuration || 0) / 1000, duration);
|
||||
|
||||
const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹");
|
||||
|
||||
const empty = Array<string>(STATUSBAR_LENGTH - filled.length).fill("▫️");
|
||||
|
||||
return `[${filled.join("")}${empty.join("")}] ${toISOTime(playback)}/${toISOTime(duration)}`;
|
||||
}
|
||||
|
||||
export function toISOTime(amount: number): string {
|
||||
const h = Math.floor(amount / 3600);
|
||||
const m = Math.floor((amount % 3600) / 60);
|
||||
const s = Math.floor((amount % 3600) % 60);
|
||||
|
||||
const hDisplay = h > 0 ? `${h}:` : "";
|
||||
const mDisplay = `${Math.floor(m / 10)}${Math.floor(m % 10)}:`;
|
||||
const sDisplay = `${Math.floor(s / 10)}${Math.floor(s % 10)}`;
|
||||
|
||||
return hDisplay + mDisplay + sDisplay;
|
||||
}
|
||||
|
||||
export async function asleep(amount: number): Promise<unknown> {
|
||||
return new Promise((r) => setTimeout(r, amount));
|
||||
}
|
||||
|
||||
export function volumeMoji(amount: number): string {
|
||||
return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇";
|
||||
}
|
||||
|
||||
export function loopMoji(loop: LoopOption): string {
|
||||
return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂";
|
||||
}
|
||||
|
||||
export function largestCommonSequence(s1: string, s2: string) {
|
||||
s1 = s1.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));
|
||||
|
||||
for (let i = 1; i <= s1.length; i++) {
|
||||
for (let j = 1; j <= s2.length; j++) {
|
||||
if (s1[i - 1] === s2[j - 1]) {
|
||||
buffer[i][j] = buffer[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
buffer[i][j] = Math.max(buffer[i - 1][j], buffer[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer[s1.length][s2.length];
|
||||
}
|
||||
@@ -1,36 +1,36 @@
|
||||
import { Collection, Guild, GuildTextBasedChannel } from "discord.js";
|
||||
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Player from "./player.js";
|
||||
|
||||
export default class Music {
|
||||
private players_ = new Collection<string, Player>();
|
||||
|
||||
public get(guild: Guild, channel?: GuildTextBasedChannel): Player | undefined {
|
||||
const player = this.players_.get(guild.id);
|
||||
|
||||
if (player) {
|
||||
if (channel) {
|
||||
player.channel = channel;
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
public set(guild: Guild, channel: GuildTextBasedChannel): Player {
|
||||
const player = new Player(guild, this, channel);
|
||||
|
||||
this.players_.set(guild.id, player);
|
||||
|
||||
logger.info("Player was created.", { player });
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public delete(guildId: string): void {
|
||||
logger.info("Player was deleted.", { guildId });
|
||||
|
||||
this.players_.delete(guildId);
|
||||
}
|
||||
}
|
||||
import { Collection, Guild, GuildTextBasedChannel } from "discord.js";
|
||||
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Player from "./player.js";
|
||||
|
||||
export default class Music {
|
||||
private players_ = new Collection<string, Player>();
|
||||
|
||||
public get(guild: Guild, channel?: GuildTextBasedChannel): Player | undefined {
|
||||
const player = this.players_.get(guild.id);
|
||||
|
||||
if (player) {
|
||||
if (channel) {
|
||||
player.channel = channel;
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
public set(guild: Guild, channel: GuildTextBasedChannel): Player {
|
||||
const player = new Player(guild, this, channel);
|
||||
|
||||
this.players_.set(guild.id, player);
|
||||
|
||||
logger.info("Player was created.", { player });
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public delete(guildId: string): void {
|
||||
logger.info("Player was deleted.", { guildId });
|
||||
|
||||
this.players_.delete(guildId);
|
||||
}
|
||||
}
|
||||
+287
-287
@@ -1,287 +1,287 @@
|
||||
import {
|
||||
AudioPlayer,
|
||||
AudioPlayerStatus,
|
||||
AudioResource,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
DiscordGatewayAdapterCreator,
|
||||
joinVoiceChannel,
|
||||
VoiceConnection
|
||||
} from "@discordjs/voice";
|
||||
import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js";
|
||||
|
||||
import { fetchInfo, ytdlStream } from "./fetch.js";
|
||||
import Filter from "./filter.js";
|
||||
import { largestCommonSequence } from "./misc.js";
|
||||
import Music from "./music.js";
|
||||
import Queue from "./queue.js";
|
||||
import replies from "./replies.js";
|
||||
import { Track } from "./types.js";
|
||||
|
||||
export default class Player {
|
||||
private music: Music;
|
||||
private guild: Guild;
|
||||
|
||||
private channel_: GuildTextBasedChannel;
|
||||
public get channel(): GuildTextBasedChannel {
|
||||
return this.channel_;
|
||||
}
|
||||
public set channel(x: GuildTextBasedChannel) {
|
||||
this.channel_ = x;
|
||||
}
|
||||
|
||||
private message_?: Message;
|
||||
|
||||
private connection_?: VoiceConnection;
|
||||
public get connection(): VoiceConnection | undefined {
|
||||
return this.connection_;
|
||||
}
|
||||
|
||||
private player_: AudioPlayer;
|
||||
public get player(): AudioPlayer {
|
||||
return this.player_;
|
||||
}
|
||||
|
||||
private resource_: AudioResource<Track> | undefined;
|
||||
public get resource(): AudioResource<Track> | undefined {
|
||||
return this.resource_;
|
||||
}
|
||||
|
||||
private volume_ = 100;
|
||||
public get volume(): number {
|
||||
return this.volume_;
|
||||
}
|
||||
public set volume(x: number) {
|
||||
this.volume_ = Math.max(0, Math.min(150, x));
|
||||
|
||||
if (this.resource_ && this.resource_.volume) {
|
||||
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly queue = new Queue();
|
||||
|
||||
public readonly filter = new Filter();
|
||||
|
||||
public readonly createdAt: Date = new Date();
|
||||
|
||||
public constructor(guild: Guild, music: Music, channel: GuildTextBasedChannel) {
|
||||
this.music = music;
|
||||
this.guild = guild;
|
||||
this.channel_ = channel;
|
||||
|
||||
this.player_ = createAudioPlayer();
|
||||
|
||||
this.player_.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
|
||||
}
|
||||
|
||||
private resourceGenerator(track: Track, seek?: number) {
|
||||
const stream = ytdlStream(track.url, {
|
||||
opusEncoded: true,
|
||||
filter: "audioonly",
|
||||
highWaterMark: 1 << 25,
|
||||
seek: seek ? seek / 1000 : undefined,
|
||||
encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
|
||||
});
|
||||
|
||||
this.resource_ = createAudioResource(stream, {
|
||||
inlineVolume: true,
|
||||
metadata: track
|
||||
});
|
||||
|
||||
if (seek) {
|
||||
this.resource_.playbackDuration = seek;
|
||||
}
|
||||
|
||||
if (this.resource_.volume) {
|
||||
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||
}
|
||||
|
||||
return this.resource_;
|
||||
}
|
||||
|
||||
private resourceEndResolvable(): void {
|
||||
if (this.resource_) {
|
||||
this.resource_.encoder?.destroy();
|
||||
}
|
||||
|
||||
if (this.queue.next()) {
|
||||
this.play();
|
||||
} else {
|
||||
this.player_.stop(true);
|
||||
this.resource_ = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async messageResolvable(track: Track) {
|
||||
if (this.message_?.deletable) {
|
||||
await this.message_.delete().catch();
|
||||
}
|
||||
|
||||
this.message_ = await this.channel_.send(replies.nowPlaying(track));
|
||||
}
|
||||
|
||||
private async search(query: string, requester: User): Promise<Track[] | undefined> {
|
||||
const info = await fetchInfo(query);
|
||||
|
||||
if (info.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return info.map((value) => {
|
||||
return { ...value, requester: requester };
|
||||
});
|
||||
}
|
||||
|
||||
public async play(seek?: number): Promise<void> {
|
||||
const track = this.queue.current();
|
||||
|
||||
if (!this.connection_ || !track) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.player_.play(this.resourceGenerator(track, seek));
|
||||
|
||||
this.connection_.subscribe(this.player_);
|
||||
|
||||
await this.messageResolvable(track);
|
||||
}
|
||||
|
||||
public connect(channel: VoiceBasedChannel): void {
|
||||
if (
|
||||
this.connection_?.joinConfig.channelId === channel.id &&
|
||||
this.connection_.joinConfig.guildId === channel.guildId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connection_) {
|
||||
this.connection_.destroy();
|
||||
}
|
||||
|
||||
this.connection_ = joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guildId,
|
||||
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
|
||||
});
|
||||
|
||||
this.connection_.subscribe(this.player_);
|
||||
}
|
||||
|
||||
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
|
||||
const tracks = await this.search(query, requester);
|
||||
|
||||
if (!tracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.queue.push(tracks, index);
|
||||
|
||||
return tracks[0];
|
||||
}
|
||||
|
||||
public jump(query: number | string): number | undefined {
|
||||
let position = typeof query === "number" ? query : -1;
|
||||
let diff = 0;
|
||||
|
||||
if (typeof query === "string") {
|
||||
this.queue.tracks.forEach((track, index) => {
|
||||
const tempDiff = largestCommonSequence(track.title, query);
|
||||
if (diff < tempDiff) {
|
||||
diff = tempDiff;
|
||||
position = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (position !== -1 && this.queue.jump(position)) {
|
||||
this.play();
|
||||
return position;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public remove(query?: number | string): Track | undefined {
|
||||
const oldPosition = this.queue.position;
|
||||
|
||||
let newPosition = typeof query === "number" ? query : this.queue.position;
|
||||
let diff = 0;
|
||||
|
||||
if (typeof query === "string") {
|
||||
this.queue.tracks.forEach((track, index) => {
|
||||
const tempDiff = largestCommonSequence(track.title, query);
|
||||
if (diff < tempDiff) {
|
||||
diff = tempDiff;
|
||||
newPosition = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const track = this.queue.pop(newPosition);
|
||||
|
||||
if (track) {
|
||||
if (oldPosition === newPosition) {
|
||||
this.play();
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public seek(time: number): void {
|
||||
this.play(time);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.queue.clear();
|
||||
this.resource_ = undefined;
|
||||
|
||||
if (this.player_) {
|
||||
this.player_.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.connection_) {
|
||||
this.connection_.destroy();
|
||||
}
|
||||
|
||||
this.player_.stop();
|
||||
this.music.delete(this.guild.id);
|
||||
}
|
||||
|
||||
public pause(): boolean {
|
||||
if (this.player_.state.status === AudioPlayerStatus.Paused) {
|
||||
this.player_.unpause();
|
||||
return false;
|
||||
} else {
|
||||
this.player_.pause();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public shuffle(): void {
|
||||
this.queue.shuffle();
|
||||
|
||||
if (this.resource_) {
|
||||
this.player_.stop();
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
public skip(): void {
|
||||
this.player_.stop();
|
||||
}
|
||||
|
||||
public setFilter(filter?: string): boolean {
|
||||
const status = this.filter.toggle(filter);
|
||||
|
||||
if (this.resource_) {
|
||||
this.play(this.resource_.playbackDuration);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
import {
|
||||
AudioPlayer,
|
||||
AudioPlayerStatus,
|
||||
AudioResource,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
DiscordGatewayAdapterCreator,
|
||||
joinVoiceChannel,
|
||||
VoiceConnection
|
||||
} from "@discordjs/voice";
|
||||
import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js";
|
||||
|
||||
import { fetchInfo, ytdlStream } from "./fetch.js";
|
||||
import Filter from "./filter.js";
|
||||
import { largestCommonSequence } from "./misc.js";
|
||||
import Music from "./music.js";
|
||||
import Queue from "./queue.js";
|
||||
import replies from "./replies.js";
|
||||
import { Track } from "./types.js";
|
||||
|
||||
export default class Player {
|
||||
private music: Music;
|
||||
private guild: Guild;
|
||||
|
||||
private channel_: GuildTextBasedChannel;
|
||||
public get channel(): GuildTextBasedChannel {
|
||||
return this.channel_;
|
||||
}
|
||||
public set channel(x: GuildTextBasedChannel) {
|
||||
this.channel_ = x;
|
||||
}
|
||||
|
||||
private message_?: Message;
|
||||
|
||||
private connection_?: VoiceConnection;
|
||||
public get connection(): VoiceConnection | undefined {
|
||||
return this.connection_;
|
||||
}
|
||||
|
||||
private player_: AudioPlayer;
|
||||
public get player(): AudioPlayer {
|
||||
return this.player_;
|
||||
}
|
||||
|
||||
private resource_: AudioResource<Track> | undefined;
|
||||
public get resource(): AudioResource<Track> | undefined {
|
||||
return this.resource_;
|
||||
}
|
||||
|
||||
private volume_ = 100;
|
||||
public get volume(): number {
|
||||
return this.volume_;
|
||||
}
|
||||
public set volume(x: number) {
|
||||
this.volume_ = Math.max(0, Math.min(150, x));
|
||||
|
||||
if (this.resource_ && this.resource_.volume) {
|
||||
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly queue = new Queue();
|
||||
|
||||
public readonly filter = new Filter();
|
||||
|
||||
public readonly createdAt: Date = new Date();
|
||||
|
||||
public constructor(guild: Guild, music: Music, channel: GuildTextBasedChannel) {
|
||||
this.music = music;
|
||||
this.guild = guild;
|
||||
this.channel_ = channel;
|
||||
|
||||
this.player_ = createAudioPlayer();
|
||||
|
||||
this.player_.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
|
||||
}
|
||||
|
||||
private resourceGenerator(track: Track, seek?: number) {
|
||||
const stream = ytdlStream(track.url, {
|
||||
opusEncoded: true,
|
||||
filter: "audioonly",
|
||||
highWaterMark: 1 << 25,
|
||||
seek: seek ? seek / 1000 : undefined,
|
||||
encoderArgs: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
|
||||
});
|
||||
|
||||
this.resource_ = createAudioResource(stream, {
|
||||
inlineVolume: true,
|
||||
metadata: track
|
||||
});
|
||||
|
||||
if (seek) {
|
||||
this.resource_.playbackDuration = seek;
|
||||
}
|
||||
|
||||
if (this.resource_.volume) {
|
||||
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||
}
|
||||
|
||||
return this.resource_;
|
||||
}
|
||||
|
||||
private resourceEndResolvable(): void {
|
||||
if (this.resource_) {
|
||||
this.resource_.encoder?.destroy();
|
||||
}
|
||||
|
||||
if (this.queue.next()) {
|
||||
this.play();
|
||||
} else {
|
||||
this.player_.stop(true);
|
||||
this.resource_ = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async messageResolvable(track: Track) {
|
||||
if (this.message_?.deletable) {
|
||||
await this.message_.delete().catch();
|
||||
}
|
||||
|
||||
this.message_ = await this.channel_.send(replies.nowPlaying(track));
|
||||
}
|
||||
|
||||
private async search(query: string, requester: User): Promise<Track[] | undefined> {
|
||||
const info = await fetchInfo(query);
|
||||
|
||||
if (info.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return info.map((value) => {
|
||||
return { ...value, requester: requester };
|
||||
});
|
||||
}
|
||||
|
||||
public async play(seek?: number): Promise<void> {
|
||||
const track = this.queue.current();
|
||||
|
||||
if (!this.connection_ || !track) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.player_.play(this.resourceGenerator(track, seek));
|
||||
|
||||
this.connection_.subscribe(this.player_);
|
||||
|
||||
await this.messageResolvable(track);
|
||||
}
|
||||
|
||||
public connect(channel: VoiceBasedChannel): void {
|
||||
if (
|
||||
this.connection_?.joinConfig.channelId === channel.id &&
|
||||
this.connection_.joinConfig.guildId === channel.guildId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connection_) {
|
||||
this.connection_.destroy();
|
||||
}
|
||||
|
||||
this.connection_ = joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guildId,
|
||||
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator
|
||||
});
|
||||
|
||||
this.connection_.subscribe(this.player_);
|
||||
}
|
||||
|
||||
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
|
||||
const tracks = await this.search(query, requester);
|
||||
|
||||
if (!tracks) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.queue.push(tracks, index);
|
||||
|
||||
return tracks[0];
|
||||
}
|
||||
|
||||
public jump(query: number | string): number | undefined {
|
||||
let position = typeof query === "number" ? query : -1;
|
||||
let diff = 0;
|
||||
|
||||
if (typeof query === "string") {
|
||||
this.queue.tracks.forEach((track, index) => {
|
||||
const tempDiff = largestCommonSequence(track.title, query);
|
||||
if (diff < tempDiff) {
|
||||
diff = tempDiff;
|
||||
position = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (position !== -1 && this.queue.jump(position)) {
|
||||
this.play();
|
||||
return position;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public remove(query?: number | string): Track | undefined {
|
||||
const oldPosition = this.queue.position;
|
||||
|
||||
let newPosition = typeof query === "number" ? query : this.queue.position;
|
||||
let diff = 0;
|
||||
|
||||
if (typeof query === "string") {
|
||||
this.queue.tracks.forEach((track, index) => {
|
||||
const tempDiff = largestCommonSequence(track.title, query);
|
||||
if (diff < tempDiff) {
|
||||
diff = tempDiff;
|
||||
newPosition = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const track = this.queue.pop(newPosition);
|
||||
|
||||
if (track) {
|
||||
if (oldPosition === newPosition) {
|
||||
this.play();
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public seek(time: number): void {
|
||||
this.play(time);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.queue.clear();
|
||||
this.resource_ = undefined;
|
||||
|
||||
if (this.player_) {
|
||||
this.player_.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.connection_) {
|
||||
this.connection_.destroy();
|
||||
}
|
||||
|
||||
this.player_.stop();
|
||||
this.music.delete(this.guild.id);
|
||||
}
|
||||
|
||||
public pause(): boolean {
|
||||
if (this.player_.state.status === AudioPlayerStatus.Paused) {
|
||||
this.player_.unpause();
|
||||
return false;
|
||||
} else {
|
||||
this.player_.pause();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public shuffle(): void {
|
||||
this.queue.shuffle();
|
||||
|
||||
if (this.resource_) {
|
||||
this.player_.stop();
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
public skip(): void {
|
||||
this.player_.stop();
|
||||
}
|
||||
|
||||
public setFilter(filter?: string): boolean {
|
||||
const status = this.filter.toggle(filter);
|
||||
|
||||
if (this.resource_) {
|
||||
this.play(this.resource_.playbackDuration);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
+119
-119
@@ -1,119 +1,119 @@
|
||||
import { LoopOption, Track } from "./types.js";
|
||||
|
||||
export default class Queue {
|
||||
private tracks_: Track[] = [];
|
||||
public get tracks(): Track[] {
|
||||
return this.tracks_;
|
||||
}
|
||||
|
||||
private position_ = 0;
|
||||
public get position(): number {
|
||||
return this.position_;
|
||||
}
|
||||
|
||||
private loop_: LoopOption = "NONE";
|
||||
public get loop(): LoopOption {
|
||||
return this.loop_;
|
||||
}
|
||||
public set loop(x: LoopOption) {
|
||||
this.loop_ = x;
|
||||
}
|
||||
|
||||
private duration_ = 0;
|
||||
public get duration(): number {
|
||||
return this.duration_;
|
||||
}
|
||||
|
||||
public next(): Track | undefined {
|
||||
if (this.loop_ === "SONG") {
|
||||
return this.current();
|
||||
}
|
||||
|
||||
if (this.loop_ === "AUTO") {
|
||||
this.position_ = Math.floor(Math.random() * this.tracks_.length);
|
||||
return this.current();
|
||||
}
|
||||
|
||||
if (this.position_ + 1 < this.tracks_.length) {
|
||||
this.position_++;
|
||||
return this.current();
|
||||
} else if (this.loop_ === "LOOP") {
|
||||
this.position_ = 0;
|
||||
return this.current();
|
||||
} else if (this.loop_ === "NONE") {
|
||||
this.position_ = 0;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public shuffle(): void {
|
||||
let currentIndex = this.tracks_.length;
|
||||
|
||||
while (currentIndex != 0) {
|
||||
const randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
|
||||
[this.tracks_[currentIndex], this.tracks_[randomIndex]] = [
|
||||
this.tracks_[randomIndex],
|
||||
this.tracks_[currentIndex]
|
||||
];
|
||||
}
|
||||
|
||||
this.position_ = 0;
|
||||
}
|
||||
|
||||
public push(tracks: Track[], index?: number): Track {
|
||||
for (const track of tracks) {
|
||||
this.duration_ += track.duration;
|
||||
}
|
||||
|
||||
if (index === undefined || index >= this.tracks_.length) {
|
||||
this.tracks_ = this.tracks_.concat(tracks);
|
||||
|
||||
return this.tracks_[this.tracks_.length - 1];
|
||||
}
|
||||
|
||||
this.tracks_.splice(Math.max(0, index), 0, ...tracks);
|
||||
if (this.position_ >= index) {
|
||||
this.position_ += tracks.length;
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
this.tracks_.splice(query, 1);
|
||||
this.position_ = Math.max(0, this.position_ - 1);
|
||||
this.duration_ -= track.duration;
|
||||
return track;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public jump(query: number): Track | undefined {
|
||||
if (query >= 0 && query < this.tracks_.length) {
|
||||
const track = this.tracks_[query];
|
||||
this.position_ = query;
|
||||
return track;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public current(): Track {
|
||||
return this.tracks_[this.position_];
|
||||
}
|
||||
|
||||
public empty(): boolean {
|
||||
return this.tracks_.length === 0;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.tracks_ = [];
|
||||
this.position_ = 0;
|
||||
}
|
||||
}
|
||||
import { LoopOption, Track } from "./types.js";
|
||||
|
||||
export default class Queue {
|
||||
private tracks_: Track[] = [];
|
||||
public get tracks(): Track[] {
|
||||
return this.tracks_;
|
||||
}
|
||||
|
||||
private position_ = 0;
|
||||
public get position(): number {
|
||||
return this.position_;
|
||||
}
|
||||
|
||||
private loop_: LoopOption = "NONE";
|
||||
public get loop(): LoopOption {
|
||||
return this.loop_;
|
||||
}
|
||||
public set loop(x: LoopOption) {
|
||||
this.loop_ = x;
|
||||
}
|
||||
|
||||
private duration_ = 0;
|
||||
public get duration(): number {
|
||||
return this.duration_;
|
||||
}
|
||||
|
||||
public next(): Track | undefined {
|
||||
if (this.loop_ === "SONG") {
|
||||
return this.current();
|
||||
}
|
||||
|
||||
if (this.loop_ === "AUTO") {
|
||||
this.position_ = Math.floor(Math.random() * this.tracks_.length);
|
||||
return this.current();
|
||||
}
|
||||
|
||||
if (this.position_ + 1 < this.tracks_.length) {
|
||||
this.position_++;
|
||||
return this.current();
|
||||
} else if (this.loop_ === "LOOP") {
|
||||
this.position_ = 0;
|
||||
return this.current();
|
||||
} else if (this.loop_ === "NONE") {
|
||||
this.position_ = 0;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public shuffle(): void {
|
||||
let currentIndex = this.tracks_.length;
|
||||
|
||||
while (currentIndex != 0) {
|
||||
const randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
|
||||
[this.tracks_[currentIndex], this.tracks_[randomIndex]] = [
|
||||
this.tracks_[randomIndex],
|
||||
this.tracks_[currentIndex]
|
||||
];
|
||||
}
|
||||
|
||||
this.position_ = 0;
|
||||
}
|
||||
|
||||
public push(tracks: Track[], index?: number): Track {
|
||||
for (const track of tracks) {
|
||||
this.duration_ += track.duration;
|
||||
}
|
||||
|
||||
if (index === undefined || index >= this.tracks_.length) {
|
||||
this.tracks_ = this.tracks_.concat(tracks);
|
||||
|
||||
return this.tracks_[this.tracks_.length - 1];
|
||||
}
|
||||
|
||||
this.tracks_.splice(Math.max(0, index), 0, ...tracks);
|
||||
if (this.position_ >= index) {
|
||||
this.position_ += tracks.length;
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
this.tracks_.splice(query, 1);
|
||||
this.position_ = Math.max(0, this.position_ - 1);
|
||||
this.duration_ -= track.duration;
|
||||
return track;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public jump(query: number): Track | undefined {
|
||||
if (query >= 0 && query < this.tracks_.length) {
|
||||
const track = this.tracks_[query];
|
||||
this.position_ = query;
|
||||
return track;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public current(): Track {
|
||||
return this.tracks_[this.position_];
|
||||
}
|
||||
|
||||
public empty(): boolean {
|
||||
return this.tracks_.length === 0;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.tracks_ = [];
|
||||
this.position_ = 0;
|
||||
}
|
||||
}
|
||||
+376
-376
@@ -1,376 +1,376 @@
|
||||
import { AudioResource } from "@discordjs/voice";
|
||||
import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import { loopMoji, statusBarGenerator, toISOTime, volumeMoji } from "./misc.js";
|
||||
import Player from "./player.js";
|
||||
import Queue from "./queue.js";
|
||||
import { LoopOption, Track } from "./types.js";
|
||||
|
||||
const replies = {
|
||||
notExists: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle("⚠️ Command ignored - player does not exist")
|
||||
.setDescription("Use '/play' to create a new one.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
searching: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("🔍 Searching...")
|
||||
.setDescription("It will take longer if playlist or non-youtube URL was queried.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
notPlaying: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("💿 Insert the disk...")
|
||||
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
.setDescription("Use '/play' to start the playback.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
nowPlaying: (track: Track): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setAuthor({
|
||||
iconURL: track.requester.avatarURL() ?? undefined,
|
||||
name: track.requester.tag
|
||||
})
|
||||
.setTitle(track.title)
|
||||
.setURL(track.url)
|
||||
.setDescription("playing now.")
|
||||
.setThumbnail(track.thumbnail);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
okConnected: (channel: string): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`✅ Successfuly connected to: ${italic(channel)}`)
|
||||
.setDescription("Will leave after some time, if left alone.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
notConnected: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot create new player.")
|
||||
.setDescription("Make yourself connected to a voice channel beforehand.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
destroyed: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("💀 Destroying the player")
|
||||
.setDescription("Everything is lost now. Use '/play' to create a new one.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
alone: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("🥬 I have decided to leave to conserve my energy")
|
||||
.setDescription("Since there was nobody in the voice channel.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
paused: (state: boolean): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (state) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle("▶️ The playback was resumed");
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle("⏸️ The playback was paused")
|
||||
.setDescription("Invoke this command again to resume.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
seeked: (from: number, to: number): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (from < to) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle(`⏩ Fast-forwarding by ${toISOTime(to - from)}`);
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle(`⏪ Fast-reversing by ${toISOTime(from - to)}`);
|
||||
}
|
||||
|
||||
embed.setDescription(
|
||||
`Moving playback from ${italic(toISOTime(from))} to ${italic(toISOTime(to))}.`
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
looped: (option: LoopOption) => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
switch (option) {
|
||||
case "AUTO": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle(`${loopMoji(option)} Shuffle mode.`)
|
||||
.setDescription("Does not affect the queue order. Endless playback.");
|
||||
|
||||
break;
|
||||
}
|
||||
case "LOOP": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle(`${loopMoji(option)} Looping over the queue.`)
|
||||
.setDescription("Will start over after final track. Endless playback.");
|
||||
|
||||
break;
|
||||
}
|
||||
case "SONG": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle(`${loopMoji(option)} Looping a single track.`)
|
||||
.setDescription("Endless playback of a single track. Over and over again...");
|
||||
|
||||
break;
|
||||
}
|
||||
case "NONE": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle(`${loopMoji(option)} Drop on final.`)
|
||||
.setDescription(
|
||||
"Will end playback after final track. Action will be needed to continue."
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
reshuffle: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle("✅ Successfuly reshuffled queue")
|
||||
.setDescription("Use 'random' mode, if you want shuffle without reshuffle.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
skipped: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle("⏭️ Skipping current track")
|
||||
.setDescription("Behavior depends on the loop option.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
jumped: (previous?: Track, to?: number): BaseMessageOptions => {
|
||||
if (!to) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot perfome jump over the queue")
|
||||
.setDescription("Either not found by query, or index is not in range.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`✅ Jumping over the queue`);
|
||||
|
||||
if (previous) {
|
||||
embed.setDescription(`Switching from ${italic(previous.title)}.`);
|
||||
} else {
|
||||
embed.setDescription("Playback will start again.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
appended: (track?: Track): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (track) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setAuthor({
|
||||
iconURL: track.requester.avatarURL() ?? undefined,
|
||||
name: track.requester.tag
|
||||
})
|
||||
.setTitle(track.title)
|
||||
.setURL(track.url)
|
||||
.setDescription("has been added to the queue.")
|
||||
.setThumbnail(track.thumbnail)
|
||||
.setTimestamp();
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot add track to the queue")
|
||||
.setDescription("Either not found by query, or URL is broken.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
removed: (track?: Track): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (track) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(track.title)
|
||||
.setURL(track.url)
|
||||
.setDescription("was removed from the queue.")
|
||||
.setThumbnail(track.thumbnail)
|
||||
.setTimestamp();
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot delete track from queue")
|
||||
.setDescription("Either not found by query, or index is not in range.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
cleared: (amount: number): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`♻️ Queue has been cleared`)
|
||||
.setDescription(
|
||||
`${italic(amount.toString())} track${amount > 1 ? "s were" : " was"} deleted.`
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
filtered: (filter?: string, status = true): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (filter) {
|
||||
embed = new EmbedBuilder().setTitle(
|
||||
`✅ ${italic(filter)} was ${status ? "applied to the playback" : "declined"}.`
|
||||
);
|
||||
} else {
|
||||
embed = new EmbedBuilder().setTitle("♻️ All filters were declined");
|
||||
}
|
||||
|
||||
embed
|
||||
.setColor(config.colors.success)
|
||||
.setDescription("Playback can experience some lags during initial transformation.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
volumeSet: (amount: number): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`${volumeMoji(amount)} Volume has been set to ${amount}%`)
|
||||
.setDescription("This will affect everyone listening.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
queue: (queue: Queue, position = 0, resource?: AudioResource<Track>): BaseMessageOptions => {
|
||||
if (queue.empty()) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle("⚠️ Queue is empty")
|
||||
.setDescription("Use '/play' to add some tracks.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (resource) {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle(`${queue.position + 1}. ${resource.metadata.title}`)
|
||||
.setAuthor({
|
||||
iconURL: resource.metadata.requester.avatarURL() ?? undefined,
|
||||
name: resource.metadata.requester.tag
|
||||
})
|
||||
.setURL(resource.metadata.url)
|
||||
.setDescription(statusBarGenerator(resource))
|
||||
.setThumbnail(resource.metadata.thumbnail);
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle("💿 Insert the disk...")
|
||||
.setURL("https://www.youtube.com/watch?v=5mGuCdlCcNM&ab_channel=Ra%C3%BAlBlanco")
|
||||
.setDescription(statusBarGenerator())
|
||||
.setThumbnail("https://img.youtube.com/vi/5mGuCdlCcNM/default.jpg");
|
||||
}
|
||||
|
||||
embed.setColor(config.colors.info).setFields(
|
||||
queue.tracks.slice(position, position + 10).map((value, index) => ({
|
||||
name: `${position + index + 1}. ${value.title.padEnd(35)}`,
|
||||
value: `[${toISOTime(value.duration)}] | ${value.requester.tag}`
|
||||
}))
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
info: (player: Player): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (player.resource) {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`)
|
||||
.setAuthor({
|
||||
iconURL: player.resource.metadata.requester.avatarURL() ?? undefined,
|
||||
name: player.resource.metadata.requester.tag
|
||||
})
|
||||
.setURL(player.resource.metadata.url)
|
||||
.setDescription(statusBarGenerator(player.resource))
|
||||
.setThumbnail(player.resource.metadata.thumbnail);
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle("💿 Insert the disk...")
|
||||
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
.setDescription(statusBarGenerator());
|
||||
}
|
||||
|
||||
embed.setColor(config.colors.info).addFields([
|
||||
{
|
||||
name: "Applied Filters",
|
||||
value:
|
||||
player.filter
|
||||
.toggled()
|
||||
.map((value) => value[0])
|
||||
.join("\n") + "\ndefault",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Queue Length",
|
||||
value: `🔢 ${player.queue.tracks.length}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Queue Duration",
|
||||
value: `💿 ${toISOTime(player.queue.duration)}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Queue Looping",
|
||||
value: `${loopMoji(player.queue.loop)} ${player.queue.loop}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Volume Amount",
|
||||
value: `${volumeMoji(player.volume)} ${player.volume}/150`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Player Lifetime",
|
||||
value: `🕓 ${toISOTime((Date.now() - player.createdAt.getTime()) / 1000)}`,
|
||||
inline: true
|
||||
}
|
||||
]);
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
import { AudioResource } from "@discordjs/voice";
|
||||
import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import { loopMoji, statusBarGenerator, toISOTime, volumeMoji } from "./misc.js";
|
||||
import Player from "./player.js";
|
||||
import Queue from "./queue.js";
|
||||
import { LoopOption, Track } from "./types.js";
|
||||
|
||||
const replies = {
|
||||
notExists: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle("⚠️ Command ignored - player does not exist")
|
||||
.setDescription("Use '/play' to create a new one.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
searching: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("🔍 Searching...")
|
||||
.setDescription("It will take longer if playlist or non-youtube URL was queried.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
notPlaying: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("💿 Insert the disk...")
|
||||
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
.setDescription("Use '/play' to start the playback.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
nowPlaying: (track: Track): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setAuthor({
|
||||
iconURL: track.requester.avatarURL() ?? undefined,
|
||||
name: track.requester.username
|
||||
})
|
||||
.setTitle(track.title)
|
||||
.setURL(track.url)
|
||||
.setDescription("playing now.")
|
||||
.setThumbnail(track.thumbnail);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
okConnected: (channel: string): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`✅ Successfuly connected to: ${italic(channel)}`)
|
||||
.setDescription("Will leave after some time, if left alone.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
notConnected: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot create new player.")
|
||||
.setDescription("Make yourself connected to a voice channel beforehand.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
destroyed: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("💀 Destroying the player")
|
||||
.setDescription("Everything is lost now. Use '/play' to create a new one.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
alone: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle("🥬 I have decided to leave to conserve my energy")
|
||||
.setDescription("Since there was nobody in the voice channel.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
paused: (state: boolean): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (state) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle("▶️ The playback was resumed");
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle("⏸️ The playback was paused")
|
||||
.setDescription("Invoke this command again to resume.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
seeked: (from: number, to: number): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (from < to) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle(`⏩ Fast-forwarding by ${toISOTime(to - from)}`);
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle(`⏪ Fast-reversing by ${toISOTime(from - to)}`);
|
||||
}
|
||||
|
||||
embed.setDescription(
|
||||
`Moving playback from ${italic(toISOTime(from))} to ${italic(toISOTime(to))}.`
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
looped: (option: LoopOption) => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
switch (option) {
|
||||
case "AUTO": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.default)
|
||||
.setTitle(`${loopMoji(option)} Shuffle mode.`)
|
||||
.setDescription("Does not affect the queue order. Endless playback.");
|
||||
|
||||
break;
|
||||
}
|
||||
case "LOOP": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle(`${loopMoji(option)} Looping over the queue.`)
|
||||
.setDescription("Will start over after final track. Endless playback.");
|
||||
|
||||
break;
|
||||
}
|
||||
case "SONG": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle(`${loopMoji(option)} Looping a single track.`)
|
||||
.setDescription("Endless playback of a single track. Over and over again...");
|
||||
|
||||
break;
|
||||
}
|
||||
case "NONE": {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle(`${loopMoji(option)} Drop on final.`)
|
||||
.setDescription(
|
||||
"Will end playback after final track. Action will be needed to continue."
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
reshuffle: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle("✅ Successfuly reshuffled queue")
|
||||
.setDescription("Use 'random' mode, if you want shuffle without reshuffle.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
skipped: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.info)
|
||||
.setTitle("⏭️ Skipping current track")
|
||||
.setDescription("Behavior depends on the loop option.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
jumped: (previous?: Track, to?: number): BaseMessageOptions => {
|
||||
if (!to) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot perfome jump over the queue")
|
||||
.setDescription("Either not found by query, or index is not in range.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`✅ Jumping over the queue`);
|
||||
|
||||
if (previous) {
|
||||
embed.setDescription(`Switching from ${italic(previous.title)}.`);
|
||||
} else {
|
||||
embed.setDescription("Playback will start again.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
appended: (track?: Track): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (track) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setAuthor({
|
||||
iconURL: track.requester.avatarURL() ?? undefined,
|
||||
name: track.requester.username
|
||||
})
|
||||
.setTitle(track.title)
|
||||
.setURL(track.url)
|
||||
.setDescription("has been added to the queue.")
|
||||
.setThumbnail(track.thumbnail)
|
||||
.setTimestamp();
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot add track to the queue")
|
||||
.setDescription("Either not found by query, or URL is broken.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
removed: (track?: Track): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (track) {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(track.title)
|
||||
.setURL(track.url)
|
||||
.setDescription("was removed from the queue.")
|
||||
.setThumbnail(track.thumbnail)
|
||||
.setTimestamp();
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("❗ Cannot delete track from queue")
|
||||
.setDescription("Either not found by query, or index is not in range.");
|
||||
}
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
cleared: (amount: number): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`♻️ Queue has been cleared`)
|
||||
.setDescription(
|
||||
`${italic(amount.toString())} track${amount > 1 ? "s were" : " was"} deleted.`
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
filtered: (filter?: string, status = true): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (filter) {
|
||||
embed = new EmbedBuilder().setTitle(
|
||||
`✅ ${italic(filter)} was ${status ? "applied to the playback" : "declined"}.`
|
||||
);
|
||||
} else {
|
||||
embed = new EmbedBuilder().setTitle("♻️ All filters were declined");
|
||||
}
|
||||
|
||||
embed
|
||||
.setColor(config.colors.success)
|
||||
.setDescription("Playback can experience some lags during initial transformation.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
volumeSet: (amount: number): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`${volumeMoji(amount)} Volume has been set to ${amount}%`)
|
||||
.setDescription("This will affect everyone listening.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
queue: (queue: Queue, position = 0, resource?: AudioResource<Track>): BaseMessageOptions => {
|
||||
if (queue.empty()) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning)
|
||||
.setTitle("⚠️ Queue is empty")
|
||||
.setDescription("Use '/play' to add some tracks.");
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (resource) {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle(`${queue.position + 1}. ${resource.metadata.title}`)
|
||||
.setAuthor({
|
||||
iconURL: resource.metadata.requester.avatarURL() ?? undefined,
|
||||
name: resource.metadata.requester.username
|
||||
})
|
||||
.setURL(resource.metadata.url)
|
||||
.setDescription(statusBarGenerator(resource))
|
||||
.setThumbnail(resource.metadata.thumbnail);
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle("💿 Insert the disk...")
|
||||
.setURL("https://www.youtube.com/watch?v=5mGuCdlCcNM&ab_channel=Ra%C3%BAlBlanco")
|
||||
.setDescription(statusBarGenerator())
|
||||
.setThumbnail("https://img.youtube.com/vi/5mGuCdlCcNM/default.jpg");
|
||||
}
|
||||
|
||||
embed.setColor(config.colors.info).setFields(
|
||||
queue.tracks.slice(position, position + 10).map((value, index) => ({
|
||||
name: `${position + index + 1}. ${value.title.padEnd(35)}`,
|
||||
value: `[${toISOTime(value.duration)}] | ${value.requester.username}`
|
||||
}))
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
info: (player: Player): BaseMessageOptions => {
|
||||
let embed: EmbedBuilder;
|
||||
|
||||
if (player.resource) {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`)
|
||||
.setAuthor({
|
||||
iconURL: player.resource.metadata.requester.avatarURL() ?? undefined,
|
||||
name: player.resource.metadata.requester.username
|
||||
})
|
||||
.setURL(player.resource.metadata.url)
|
||||
.setDescription(statusBarGenerator(player.resource))
|
||||
.setThumbnail(player.resource.metadata.thumbnail);
|
||||
} else {
|
||||
embed = new EmbedBuilder()
|
||||
.setTitle("💿 Insert the disk...")
|
||||
.setURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
.setDescription(statusBarGenerator());
|
||||
}
|
||||
|
||||
embed.setColor(config.colors.info).addFields([
|
||||
{
|
||||
name: "Applied Filters",
|
||||
value:
|
||||
player.filter
|
||||
.toggled()
|
||||
.map((value) => value[0])
|
||||
.join("\n") + "\ndefault",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Queue Length",
|
||||
value: `🔢 ${player.queue.tracks.length}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Queue Duration",
|
||||
value: `💿 ${toISOTime(player.queue.duration)}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Queue Looping",
|
||||
value: `${loopMoji(player.queue.loop)} ${player.queue.loop}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Volume Amount",
|
||||
value: `${volumeMoji(player.volume)} ${player.volume}/150`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Player Lifetime",
|
||||
value: `🕓 ${toISOTime((Date.now() - player.createdAt.getTime()) / 1000)}`,
|
||||
inline: true
|
||||
}
|
||||
]);
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
@@ -1,43 +1,43 @@
|
||||
import { User } from "discord.js";
|
||||
import { downloadOptions } from "ytdl-core";
|
||||
|
||||
export interface Track {
|
||||
url: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
requester: User;
|
||||
}
|
||||
|
||||
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
|
||||
|
||||
export interface YtdlDump {
|
||||
id: string;
|
||||
original_url: string;
|
||||
title?: string;
|
||||
uploader?: string;
|
||||
duration?: number;
|
||||
extractor_key: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
export interface YtdlResult {
|
||||
url: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface YtdlStreamOptions extends downloadOptions {
|
||||
seek?: number;
|
||||
encoderArgs?: string[];
|
||||
fmt?: string;
|
||||
opusEncoded?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamOptions {
|
||||
seek?: number;
|
||||
encoderArgs?: string[];
|
||||
fmt?: string;
|
||||
opusEncoded?: boolean;
|
||||
}
|
||||
import { User } from "discord.js";
|
||||
import { downloadOptions } from "ytdl-core";
|
||||
|
||||
export interface Track {
|
||||
url: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
requester: User;
|
||||
}
|
||||
|
||||
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
|
||||
|
||||
export interface YtdlDump {
|
||||
id: string;
|
||||
original_url: string;
|
||||
title?: string;
|
||||
uploader?: string;
|
||||
duration?: number;
|
||||
extractor_key: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
export interface YtdlResult {
|
||||
url: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface YtdlStreamOptions extends downloadOptions {
|
||||
seek?: number;
|
||||
encoderArgs?: string[];
|
||||
fmt?: string;
|
||||
opusEncoded?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamOptions {
|
||||
seek?: number;
|
||||
encoderArgs?: string[];
|
||||
fmt?: string;
|
||||
opusEncoded?: boolean;
|
||||
}
|
||||
+15
-15
@@ -1,15 +1,15 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import Music from "./lib/music.js";
|
||||
|
||||
const name = "music";
|
||||
const description = "Music module";
|
||||
const author = "unknowableshade";
|
||||
const tag = "2.0.0";
|
||||
|
||||
const controller = new Music();
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { author, controller, description, name, rootDir, tag };
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import Music from "./lib/music.js";
|
||||
|
||||
const name = "music";
|
||||
const description = "Music module";
|
||||
const author = "unknowableshade";
|
||||
const tag = "2.0.0";
|
||||
|
||||
const controller = new Music();
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { author, controller, description, name, rootDir, tag };
|
||||
@@ -1,95 +1,95 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
Events,
|
||||
PermissionFlagsBits,
|
||||
SlashCommandBuilder
|
||||
} from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.inCachedGuild()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const event = interaction.options.getString("event", true);
|
||||
|
||||
switch (subcommand) {
|
||||
case "role": {
|
||||
const role = interaction.options.getRole("role", true);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
await client.db.event.upsert({
|
||||
where: {
|
||||
eventIdentifier: {
|
||||
title: event,
|
||||
action: subcommand,
|
||||
guildId: interaction.guildId
|
||||
}
|
||||
},
|
||||
update: { metadata: [role.id] },
|
||||
create: {
|
||||
Guild: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: interaction.guildId
|
||||
},
|
||||
create: {
|
||||
id: interaction.guildId
|
||||
}
|
||||
}
|
||||
},
|
||||
title: event,
|
||||
action: subcommand,
|
||||
metadata: [role.id]
|
||||
}
|
||||
});
|
||||
|
||||
await interaction.editReply(replies.auto("add role to a member", event));
|
||||
} catch (error) {
|
||||
logger.error("Error occured while quering db.", { interaction });
|
||||
} finally {
|
||||
await client.db.$disconnect();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AUTO_EVENT_CHOICES = [
|
||||
{
|
||||
name: "Member Joins",
|
||||
value: Events.GuildMemberAdd
|
||||
}
|
||||
];
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("auto")
|
||||
.setDescription("Quetza's automatization engine.")
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName("role")
|
||||
.setDescription("Auto: give role.")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("event")
|
||||
.setDescription("Event when perfome an action.")
|
||||
.setChoices(...AUTO_EVENT_CHOICES)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addRoleOption((option) =>
|
||||
option.setName("role").setDescription("Role to grant on event.").setRequired(true)
|
||||
)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
Events,
|
||||
PermissionFlagsBits,
|
||||
SlashCommandBuilder
|
||||
} from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.inCachedGuild()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const event = interaction.options.getString("event", true);
|
||||
|
||||
switch (subcommand) {
|
||||
case "role": {
|
||||
const role = interaction.options.getRole("role", true);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
await client.db.event.upsert({
|
||||
where: {
|
||||
eventIdentifier: {
|
||||
title: event,
|
||||
action: subcommand,
|
||||
guildId: interaction.guildId
|
||||
}
|
||||
},
|
||||
update: { metadata: [role.id] },
|
||||
create: {
|
||||
Guild: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: interaction.guildId
|
||||
},
|
||||
create: {
|
||||
id: interaction.guildId
|
||||
}
|
||||
}
|
||||
},
|
||||
title: event,
|
||||
action: subcommand,
|
||||
metadata: [role.id]
|
||||
}
|
||||
});
|
||||
|
||||
await interaction.editReply(replies.auto("add role to a member", event));
|
||||
} catch (error) {
|
||||
logger.error("Error occured while quering db.", { interaction });
|
||||
} finally {
|
||||
await client.db.$disconnect();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AUTO_EVENT_CHOICES = [
|
||||
{
|
||||
name: "Member Joins",
|
||||
value: Events.GuildMemberAdd
|
||||
}
|
||||
];
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("auto")
|
||||
.setDescription("Quetza's automatization engine.")
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName("role")
|
||||
.setDescription("Auto: give role.")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("event")
|
||||
.setDescription("Event when perfome an action.")
|
||||
.setChoices(...AUTO_EVENT_CHOICES)
|
||||
.setRequired(true)
|
||||
)
|
||||
.addRoleOption((option) =>
|
||||
option.setName("role").setDescription("Role to grant on event.").setRequired(true)
|
||||
)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,98 +1,98 @@
|
||||
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case "check": {
|
||||
const user = interaction.options.getUser("member", true);
|
||||
|
||||
const member = await interaction.guild.members.fetch(user).catch(() => undefined);
|
||||
|
||||
if (member) {
|
||||
await interaction.reply(replies.roleList(user.tag, member.roles.cache));
|
||||
} else {
|
||||
await interaction.reply(replies.notMember("edited", user));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case "edit": {
|
||||
const user = interaction.options.getUser("member", true);
|
||||
const role = interaction.options.getRole("role", true);
|
||||
const reason = interaction.options.getString("reason") ?? undefined;
|
||||
|
||||
const member = await interaction.guild.members.fetch(user).catch(() => undefined);
|
||||
|
||||
if (member) {
|
||||
if (member.roles.cache.some((memberRole) => role.id === memberRole.id)) {
|
||||
await member.roles.remove(role.id, reason);
|
||||
await interaction.reply(
|
||||
replies.roleAction("revoked from", interaction.user, role.name, reason)
|
||||
);
|
||||
} else {
|
||||
await member.roles.add(role.id, reason);
|
||||
await interaction.reply(
|
||||
replies.roleAction("granted to", interaction.user, role.name, reason)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await interaction.reply(replies.notMember("edited", user));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case "list": {
|
||||
const roles = await interaction.guild.roles.fetch();
|
||||
|
||||
await interaction.reply(replies.roleList(interaction.guild.name, roles));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("role")
|
||||
.setDescription("Quetza's role manager.")
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand.setName("list").setDescription("Role: list guild's roles.")
|
||||
)
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName("check")
|
||||
.setDescription("Role: check user roles.")
|
||||
.addUserOption((option) =>
|
||||
option.setName("member").setDescription("Member to check.").setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName("edit")
|
||||
.setDescription("Role: edit role for a specific user.")
|
||||
.addUserOption((option) =>
|
||||
option.setName("member").setDescription("Member to edit.").setRequired(true)
|
||||
)
|
||||
.addRoleOption((option) =>
|
||||
option.setName("role").setDescription("Member's role to edit.").setRequired(true)
|
||||
)
|
||||
.addStringOption((option) =>
|
||||
option.setName("reason").setDescription("Reason for an action.")
|
||||
)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case "check": {
|
||||
const user = interaction.options.getUser("member", true);
|
||||
|
||||
const member = await interaction.guild.members.fetch(user).catch(() => undefined);
|
||||
|
||||
if (member) {
|
||||
await interaction.reply(replies.roleList(user.username, member.roles.cache));
|
||||
} else {
|
||||
await interaction.reply(replies.notMember("edited", user));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case "edit": {
|
||||
const user = interaction.options.getUser("member", true);
|
||||
const role = interaction.options.getRole("role", true);
|
||||
const reason = interaction.options.getString("reason") ?? undefined;
|
||||
|
||||
const member = await interaction.guild.members.fetch(user).catch(() => undefined);
|
||||
|
||||
if (member) {
|
||||
if (member.roles.cache.some((memberRole) => role.id === memberRole.id)) {
|
||||
await member.roles.remove(role.id, reason);
|
||||
await interaction.reply(
|
||||
replies.roleAction("revoked from", interaction.user, role.name, reason)
|
||||
);
|
||||
} else {
|
||||
await member.roles.add(role.id, reason);
|
||||
await interaction.reply(
|
||||
replies.roleAction("granted to", interaction.user, role.name, reason)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await interaction.reply(replies.notMember("edited", user));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case "list": {
|
||||
const roles = await interaction.guild.roles.fetch();
|
||||
|
||||
await interaction.reply(replies.roleList(interaction.guild.name, roles));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("role")
|
||||
.setDescription("Quetza's role manager.")
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand.setName("list").setDescription("Role: list guild's roles.")
|
||||
)
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName("check")
|
||||
.setDescription("Role: check user roles.")
|
||||
.addUserOption((option) =>
|
||||
option.setName("member").setDescription("Member to check.").setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName("edit")
|
||||
.setDescription("Role: edit role for a specific user.")
|
||||
.addUserOption((option) =>
|
||||
option.setName("member").setDescription("Member to edit.").setRequired(true)
|
||||
)
|
||||
.addRoleOption((option) =>
|
||||
option.setName("role").setDescription("Member's role to edit.").setRequired(true)
|
||||
)
|
||||
.addStringOption((option) =>
|
||||
option.setName("reason").setDescription("Reason for an action.")
|
||||
)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Events, GuildMember } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
|
||||
async function execute(client: Client, eventee: [GuildMember]): Promise<void> {
|
||||
const [member] = eventee;
|
||||
|
||||
try {
|
||||
const event = await client.db.event.findFirst({
|
||||
where: { guildId: member.guild.id, title: name }
|
||||
});
|
||||
|
||||
if (event) {
|
||||
await member.roles.add(event.metadata as string[]);
|
||||
}
|
||||
} finally {
|
||||
await client.db.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
const name = Events.GuildMemberAdd;
|
||||
|
||||
export { execute, name };
|
||||
import { Events, GuildMember } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
|
||||
async function execute(client: Client, eventee: [GuildMember]): Promise<void> {
|
||||
const [member] = eventee;
|
||||
|
||||
try {
|
||||
const event = await client.db.event.findFirst({
|
||||
where: { guildId: member.guild.id, title: name }
|
||||
});
|
||||
|
||||
if (event) {
|
||||
await member.roles.add(event.metadata as string[]);
|
||||
}
|
||||
} finally {
|
||||
await client.db.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
const name = Events.GuildMemberAdd;
|
||||
|
||||
export { execute, name };
|
||||
@@ -1,80 +1,80 @@
|
||||
import {
|
||||
BaseMessageOptions,
|
||||
bold,
|
||||
Collection,
|
||||
EmbedBuilder,
|
||||
InteractionReplyOptions,
|
||||
italic,
|
||||
Role,
|
||||
underscore,
|
||||
User
|
||||
} from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
|
||||
const replies = {
|
||||
error: (): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("Internal error occured while resolving interaction.")
|
||||
.setDescription("Submit an issue (bio). Logs were saved.")
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
notMember: (action: string, user: User): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor("Blue")
|
||||
.setTitle(`${user.tag} cannot be ${action}`)
|
||||
.setDescription("They're not a guild member.")
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
roleAction: (
|
||||
action: string,
|
||||
user: User,
|
||||
roleName: string,
|
||||
reason?: string
|
||||
): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`Role ${roleName} was ${action} user ${user.tag}`)
|
||||
.setDescription(reason ?? "No reason provided.")
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
roleList: (observed: string, roles: Collection<string, Role>): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`${observed} roles:`)
|
||||
.addFields(
|
||||
roles.map((role) => {
|
||||
return {
|
||||
name: role.name,
|
||||
value: italic(role.id)
|
||||
};
|
||||
})
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
auto: (action: string, event: string): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(
|
||||
bold(
|
||||
`From now I will ${underscore(italic(action))} when ${underscore(
|
||||
italic(event)
|
||||
)} event happens`
|
||||
)
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
import {
|
||||
BaseMessageOptions,
|
||||
bold,
|
||||
Collection,
|
||||
EmbedBuilder,
|
||||
InteractionReplyOptions,
|
||||
italic,
|
||||
Role,
|
||||
underscore,
|
||||
User
|
||||
} from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
|
||||
const replies = {
|
||||
error: (): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error)
|
||||
.setTitle("Internal error occured while resolving interaction.")
|
||||
.setDescription("Submit an issue (bio). Logs were saved.")
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
notMember: (action: string, user: User): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor("Blue")
|
||||
.setTitle(`${user.username} cannot be ${action}`)
|
||||
.setDescription("They're not a guild member.")
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
roleAction: (
|
||||
action: string,
|
||||
user: User,
|
||||
roleName: string,
|
||||
reason?: string
|
||||
): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`Role ${roleName} was ${action} user ${user.username}`)
|
||||
.setDescription(reason ?? "No reason provided.")
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
roleList: (observed: string, roles: Collection<string, Role>): InteractionReplyOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(`${observed} roles:`)
|
||||
.addFields(
|
||||
roles.map((role) => {
|
||||
return {
|
||||
name: role.name,
|
||||
value: italic(role.id)
|
||||
};
|
||||
})
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed], ephemeral: true };
|
||||
},
|
||||
auto: (action: string, event: string): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success)
|
||||
.setTitle(
|
||||
bold(
|
||||
`From now I will ${underscore(italic(action))} when ${underscore(
|
||||
italic(event)
|
||||
)} event happens`
|
||||
)
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
@@ -1,11 +1,11 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const name = "server";
|
||||
const description = "Server utilities module";
|
||||
const author = "unknowableshade";
|
||||
const tag = "0.0.1";
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { author, description, name, rootDir, tag };
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const name = "server";
|
||||
const description = "Server utilities module";
|
||||
const author = "unknowableshade";
|
||||
const tag = "0.0.1";
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export { author, description, name, rootDir, tag };
|
||||
+19
-19
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/commands", "src/events"]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/commands", "src/events"]
|
||||
}
|
||||
Reference in new issue
Block a user