12 Commits
37 changed files with 964 additions and 674 deletions

No files matched your search

+3 -3
View File
@@ -48,19 +48,19 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with: with:
images: unknowableshade/quetza-bot images: unknowableshade/quetza-bot
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with: with:
context: . context: .
push: true push: true
+1 -1
View File
@@ -5,7 +5,7 @@
"printWidth": 100, "printWidth": 100,
"arrowParens": "always", "arrowParens": "always",
"bracketSpacing": true, "bracketSpacing": true,
"endOfLine": "crlf", "endOfLine": "lf",
"overrides": [ "overrides": [
{ {
"files": ["**/*.json, **/*.yml"], "files": ["**/*.json, **/*.yml"],
+8
View File
@@ -1,5 +1,10 @@
FROM node:18-alpine as builder FROM node:18-alpine as builder
RUN apk add --no-cache python3
RUN apk add --no-cache make
RUN apk add --no-cache gcc
RUN apk add --no-cache build-base
RUN npm install -g pnpm RUN npm install -g pnpm
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -19,6 +24,9 @@ RUN apk add --no-cache bash
RUN apk add --no-cache curl RUN apk add --no-cache curl
RUN apk add --no-cache ffmpeg RUN apk add --no-cache ffmpeg
RUN apk add --no-cache python3 RUN apk add --no-cache python3
RUN apk add --no-cache make
RUN apk add --no-cache gcc
RUN apk add --no-cache build-base
RUN npm install -g pnpm RUN npm install -g pnpm
-1
View File
@@ -1 +0,0 @@
worker: npm start
+35 -28
View File
@@ -1,54 +1,61 @@
{ {
"name": "quetza-bot", "name": "quetza-bot",
"engines": { "version": "4.2.2",
"node": "18.x.x", "description": "Discord multipurpose bot",
"npm": "*"
},
"type": "module", "type": "module",
"version": "4.0.0", "main": "./dist/index.js",
"description": "Discord multifunctional bot", "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": { "author": {
"name": "unknowableshade", "name": "unknowableshade",
"email": "60275123+unknowableshade@users.noreply.github.com", "email": "60275123+unknowableshade@users.noreply.github.com",
"url": "https://unknowableshade.ru" "url": "https://unknowableshade.ru"
}, },
"repository": {
"type": "git",
"url": "git+https://github.com/unknowableshade/quetza-bot.git"
},
"bugs": { "bugs": {
"url": "https://github.com/unknowableshade/quetza-bot/issues" "url": "https://github.com/unknowableshade/quetza-bot/issues"
}, },
"readme": "https://github.com/unknowableshade/quetza-bot#readme", "readme": "https://github.com/unknowableshade/quetza-bot#readme",
"license": "MIT", "engines": {
"main": "dist/index.js", "node": "18.x.x",
"scripts": { "npm": "*"
"start": "node dist/index.js",
"build": "pnpm exec tsc --build",
"clean": "pnpm exec tsc --build --clean",
"lint": "pnpm exec eslint --fix . && pnpm exec prettier --write .",
"preinstall": "pnpx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.9.0", "@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.16.0", "@discordjs/voice": "^0.16.0",
"@prisma/client": "4.13.0", "@prisma/client": "5.0.0",
"discord-ytdl-core": "^5.0.4",
"discord.js": "^14.11.0", "discord.js": "^14.11.0",
"prism-media": "^1.3.5",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"winston": "^3.8.2", "winston": "^3.10.0",
"ytdl-core": "^4.11.4" "ytdl-core": "^4.11.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.16.6", "@types/node": "^20.4.1",
"@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^5.59.5", "@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.40.0", "eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.8.8", "mocha": "^10.2.0",
"prisma": "^4.13.0", "prettier": "^3.0.0",
"typescript": "^4.9.5" "prisma": "^5.0.0",
"typescript": "^5.1.6"
} }
} }
+634 -295
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -58,7 +58,10 @@ export default class Client extends DiscordClient {
}); });
importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => { importModule(path.join(data.rootDir, MODULE_EVENTS), (event: Event) => {
this.on(event.name, (...eventee: unknown[]) => event.execute(this, eventee)); this.on(
event.name,
async (...eventee: unknown[]) => await event.execute(this, eventee)
);
events.push(event.name); events.push(event.name);
}); });
+1 -8
View File
@@ -4,17 +4,10 @@ const logger = winston.createLogger({
level: "info", level: "info",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()), format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [ transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: "logs/error.log", level: "error" }), new winston.transports.File({ filename: "logs/error.log", level: "error" }),
new winston.transports.File({ filename: "logs/combined.log" }) 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; export default logger;
+2 -2
View File
@@ -1,10 +1,10 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand()) { if (!interaction.isChatInputCommand()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,10 +1,10 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand()) { if (!interaction.isChatInputCommand()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+15 -5
View File
@@ -6,13 +6,23 @@ import logger from "../../../lib/logger.js";
async function execute(client: Client, eventee: [Interaction]): Promise<void> { async function execute(client: Client, eventee: [Interaction]): Promise<void> {
const [interaction] = eventee; const [interaction] = eventee;
logger.info("Interaction created.", interaction); if (interaction.isChatInputCommand()) {
if (interaction.isCommand()) {
const command = client.commands.get(interaction.commandName); const command = client.commands.get(interaction.commandName);
if (command) { if (!command) {
command.execute(client, interaction); 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 });
} }
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { fileURLToPath } from "url";
const name = "core"; const name = "core";
const description = "Quetza core functionality"; const description = "Quetza core functionality";
const author = "unknowableshade"; const author = "unknowableshade";
const tag = "4.2.0"; const tag = "4.2.2";
const rootDir = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.dirname(fileURLToPath(import.meta.url));
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,4 +1,4 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
@@ -6,7 +6,7 @@ import Filter from "../lib/filter.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+3 -3
View File
@@ -1,19 +1,19 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
} }
const pos = interaction.options.getNumber("position", true);
const query = interaction.options.getString("query", true); const query = interaction.options.getString("query", true);
const pos = interaction.options.getInteger("position", true);
await interaction.deferReply(); await interaction.deferReply();
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,4 +1,4 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
@@ -6,7 +6,7 @@ import replies from "../lib/replies.js";
import { LoopOption } from "../lib/types.js"; import { LoopOption } from "../lib/types.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+3 -3
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
@@ -43,7 +43,7 @@ async function execute(client: Client, interaction: Interaction) {
} }
if (!player.resource) { if (!player.resource) {
player.play(); await player.play();
} }
} }
+2 -2
View File
@@ -3,7 +3,7 @@ import {
ButtonBuilder, ButtonBuilder,
ButtonInteraction, ButtonInteraction,
ButtonStyle, ButtonStyle,
Interaction, ChatInputCommandInteraction,
MessageActionRowComponentBuilder, MessageActionRowComponentBuilder,
SlashCommandBuilder SlashCommandBuilder
} from "discord.js"; } from "discord.js";
@@ -13,7 +13,7 @@ import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+2 -2
View File
@@ -1,11 +1,11 @@
import { Interaction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
import { controller } from "../module.js"; import { controller } from "../module.js";
async function execute(client: Client, interaction: Interaction) { async function execute(client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild() || !interaction.channel) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
+160 -17
View File
@@ -1,13 +1,29 @@
import { execFile } from "child_process"; import { execFile } from "child_process";
import path from "path"; import path from "path";
import prismMedia from "prism-media";
import { Duplex, Readable } from "stream";
import { promisify } from "util"; import { promisify } from "util";
import ytdl from "ytdl-core";
import config from "../../../config.js"; import config from "../../../config.js";
import logger from "../../../lib/logger.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 execFileAsync = promisify(execFile);
const SEARCH_DEPTH = 5; const SEARCH_DEPTH = 5;
const YTDL_EVENTS = [
"info",
"progress",
"abort",
"request",
"response",
"error",
"redirect",
"retry",
"reconnect"
];
function isURL(url: string): boolean { 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( return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test(
@@ -15,23 +31,6 @@ function isURL(url: string): boolean {
); );
} }
interface YtdlDump {
id: string;
original_url: string;
title?: string;
uploader?: string;
duration?: number;
extractor_key: string;
thumbnail?: string;
}
interface YtdlResult {
url: string;
title: string;
thumbnail: string;
duration: number;
}
async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> { async function fetchInfoRecursive(query: string, depth = 0): Promise<YtdlResult[]> {
if (depth === SEARCH_DEPTH) { if (depth === SEARCH_DEPTH) {
logger.warn("yt-dlp failed to find anything.", { query }); logger.warn("yt-dlp failed to find anything.", { query });
@@ -100,3 +99,147 @@ export async function fetchInfo(query: string): Promise<YtdlResult[]> {
return []; return [];
} }
} }
export function ytdlStream(url: string, options?: YtdlStreamOptions) {
logger.info("Created ytdl stream.", { url, options });
let ffmpegArgs: string[] = [
"-analyzeduration",
"0",
"-loglevel",
"0",
"-f",
`${options?.fmt ? options.fmt : "s16le"}`,
"-ar",
"48000",
"-ac",
"2"
];
if (options?.seek) {
ffmpegArgs.unshift("-ss", options.seek.toString());
}
if (options?.encoderArgs) {
ffmpegArgs = ffmpegArgs.concat(options.encoderArgs);
}
const transcoder = new FFmpeg({
args: ffmpegArgs
});
const inputStream = ytdl(url, options);
const transcoderStream = inputStream.pipe(transcoder);
if (options && !options.opusEncoded) {
YTDL_EVENTS.forEach((event) =>
inputStream.on(event, (...args) => transcoderStream.emit(event, ...args))
);
inputStream.on("error", () => transcoder.destroy());
transcoderStream.on("close", () => transcoder.destroy());
return transcoderStream;
}
const opus = new Opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960
});
const outputStream = transcoderStream.pipe(opus);
transcoderStream.on("error", (e) => outputStream.emit("error", e));
YTDL_EVENTS.forEach((event) =>
inputStream.on(event, (...args) => outputStream.emit(event, ...args))
);
outputStream.on("close", () => {
transcoder.destroy();
opus.destroy();
});
return outputStream;
}
export function arbitraryStream(stream: string | Readable | Duplex, options?: StreamOptions) {
logger.info("Created arbitrary stream.", { stream, options });
let ffmpegArgs: string[];
if (typeof stream === "string") {
ffmpegArgs = [
"-reconnect",
"1",
"-reconnect_streamed",
"1",
"-reconnect_delay_max",
"5",
"-i",
stream,
"-analyzeduration",
"0",
"-loglevel",
"0",
"-f",
`${options?.fmt ? options.fmt : "s16le"}`,
"-ar",
"48000",
"-ac",
"2"
];
} else {
ffmpegArgs = [
"-analyzeduration",
"0",
"-loglevel",
"0",
"-f",
`${options?.fmt ? options.fmt : "s16le"}`,
"-ar",
"48000",
"-ac",
"2"
];
}
if (options?.seek) {
ffmpegArgs.unshift("-ss", options.seek.toString());
}
if (options?.encoderArgs) {
ffmpegArgs = ffmpegArgs.concat(options.encoderArgs);
}
let transcoder = new FFmpeg({
args: ffmpegArgs
});
if (typeof stream !== "string") {
transcoder = stream.pipe(transcoder);
stream.on("error", () => transcoder.destroy());
}
if (options && !options.opusEncoded) {
transcoder.on("close", () => transcoder.destroy());
return transcoder;
}
const opus = new Opus.Encoder({
rate: 48000,
channels: 2,
frameSize: 960
});
const outputStream = transcoder.pipe(opus);
outputStream.on("close", () => {
transcoder.destroy();
opus.destroy();
});
return outputStream;
}
+1 -1
View File
@@ -6,7 +6,7 @@ const STATUSBAR_LENGTH = 15;
export function statusBarGenerator(resource?: AudioResource<Track>): string { export function statusBarGenerator(resource?: AudioResource<Track>): string {
const duration = resource?.metadata.duration || 1; const duration = resource?.metadata.duration || 1;
const playback = Math.min(resource?.playbackDuration || 0 / 1000, duration); const playback = Math.min((resource?.playbackDuration || 0) / 1000, duration);
const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹"); const filled = Array<string>(Math.floor((STATUSBAR_LENGTH * playback) / duration)).fill("🔹");
+4 -5
View File
@@ -9,9 +9,8 @@ import {
VoiceConnection VoiceConnection
} from "@discordjs/voice"; } from "@discordjs/voice";
import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js"; import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js";
import ytdl from "discord-ytdl-core";
import { fetchInfo } from "./fetch.js"; import { fetchInfo, ytdlStream } from "./fetch.js";
import Filter from "./filter.js"; import Filter from "./filter.js";
import { largestCommonSequence } from "./misc.js"; import { largestCommonSequence } from "./misc.js";
import Music from "./music.js"; import Music from "./music.js";
@@ -77,7 +76,7 @@ export default class Player {
} }
private resourceGenerator(track: Track, seek?: number) { private resourceGenerator(track: Track, seek?: number) {
const stream = ytdl(track.url, { const stream = ytdlStream(track.url, {
opusEncoded: true, opusEncoded: true,
filter: "audioonly", filter: "audioonly",
highWaterMark: 1 << 25, highWaterMark: 1 << 25,
@@ -134,7 +133,7 @@ export default class Player {
}); });
} }
public play(seek?: number): void { public async play(seek?: number): Promise<void> {
const track = this.queue.current(); const track = this.queue.current();
if (!this.connection_ || !track) { if (!this.connection_ || !track) {
@@ -145,7 +144,7 @@ export default class Player {
this.connection_.subscribe(this.player_); this.connection_.subscribe(this.player_);
this.messageResolvable(track); await this.messageResolvable(track);
} }
public connect(channel: VoiceBasedChannel): void { public connect(channel: VoiceBasedChannel): void {
+12 -10
View File
@@ -63,20 +63,22 @@ export default class Queue {
} }
public push(tracks: Track[], index?: number): Track { public push(tracks: Track[], index?: number): Track {
if (!index || index >= this.tracks_.length) {
this.tracks_ = this.tracks_.concat(tracks);
} else {
this.tracks_.splice(Math.max(0, index), 0, ...tracks);
if (this.position_ >= index) {
this.position_ += tracks.length;
}
}
for (const track of tracks) { for (const track of tracks) {
this.duration_ += track.duration; this.duration_ += track.duration;
} }
return this.tracks_[index || this.tracks_.length - 1]; 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 { public pop(query: number): Track | undefined {
+6 -6
View File
@@ -38,7 +38,7 @@ const replies = {
.setColor(config.colors.default) .setColor(config.colors.default)
.setAuthor({ .setAuthor({
iconURL: track.requester.avatarURL() ?? undefined, iconURL: track.requester.avatarURL() ?? undefined,
name: track.requester.tag name: track.requester.username
}) })
.setTitle(track.title) .setTitle(track.title)
.setURL(track.url) .setURL(track.url)
@@ -202,7 +202,7 @@ const replies = {
.setColor(config.colors.success) .setColor(config.colors.success)
.setAuthor({ .setAuthor({
iconURL: track.requester.avatarURL() ?? undefined, iconURL: track.requester.avatarURL() ?? undefined,
name: track.requester.tag name: track.requester.username
}) })
.setTitle(track.title) .setTitle(track.title)
.setURL(track.url) .setURL(track.url)
@@ -261,7 +261,7 @@ const replies = {
embed embed
.setColor(config.colors.success) .setColor(config.colors.success)
.setDescription("Playback can experience some lags during intial transformation."); .setDescription("Playback can experience some lags during initial transformation.");
return { embeds: [embed] }; return { embeds: [embed] };
}, },
@@ -290,7 +290,7 @@ const replies = {
.setTitle(`${queue.position + 1}. ${resource.metadata.title}`) .setTitle(`${queue.position + 1}. ${resource.metadata.title}`)
.setAuthor({ .setAuthor({
iconURL: resource.metadata.requester.avatarURL() ?? undefined, iconURL: resource.metadata.requester.avatarURL() ?? undefined,
name: resource.metadata.requester.tag name: resource.metadata.requester.username
}) })
.setURL(resource.metadata.url) .setURL(resource.metadata.url)
.setDescription(statusBarGenerator(resource)) .setDescription(statusBarGenerator(resource))
@@ -306,7 +306,7 @@ const replies = {
embed.setColor(config.colors.info).setFields( embed.setColor(config.colors.info).setFields(
queue.tracks.slice(position, position + 10).map((value, index) => ({ queue.tracks.slice(position, position + 10).map((value, index) => ({
name: `${position + index + 1}. ${value.title.padEnd(35)}`, name: `${position + index + 1}. ${value.title.padEnd(35)}`,
value: `[${toISOTime(value.duration)}] | ${value.requester.tag}` value: `[${toISOTime(value.duration)}] | ${value.requester.username}`
})) }))
); );
@@ -320,7 +320,7 @@ const replies = {
.setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`) .setTitle(`${player.queue.position + 1}. ${player.resource.metadata.title}`)
.setAuthor({ .setAuthor({
iconURL: player.resource.metadata.requester.avatarURL() ?? undefined, iconURL: player.resource.metadata.requester.avatarURL() ?? undefined,
name: player.resource.metadata.requester.tag name: player.resource.metadata.requester.username
}) })
.setURL(player.resource.metadata.url) .setURL(player.resource.metadata.url)
.setDescription(statusBarGenerator(player.resource)) .setDescription(statusBarGenerator(player.resource))
+27 -245
View File
@@ -1,4 +1,5 @@
import { User } from "discord.js"; import { User } from "discord.js";
import { downloadOptions } from "ytdl-core";
export interface Track { export interface Track {
url: string; url: string;
@@ -10,252 +11,33 @@ export interface Track {
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO"; export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
export interface YtdlArgs { export interface YtdlDump {
help?: boolean;
version?: boolean;
update?: boolean;
ignoreErrors?: boolean;
abortOnError?: boolean;
dumpUserAgent?: boolean;
listExtractors?: boolean;
extractorDescriptions?: boolean;
forceGenericExtractor?: boolean;
defaultSearch?: string;
igonreConfig?: boolean;
configLocation?: string;
flatPlaylist?: boolean;
markWatched?: boolean;
noColor?: boolean;
proxy?: string;
socketTimeout?: number;
sourceAddress?: string;
forceIpv4?: boolean;
forceIpv6?: boolean;
geoVerificationProxy?: string;
geoBypass?: boolean;
geoBypassCountry?: string;
geoBypassIpBlock?: string;
playlistStart?: number;
playlistEnd?: number | "last";
playlistItems?: string;
matchTitle?: string;
rejectTitle?: string;
maxDownloads?: number;
minFilesize?: string;
maxFilesize?: string;
date?: string;
datebefore?: string;
dateafter?: string;
minViews?: number;
maxViews?: number;
matchFilter?: string;
noPlaylist?: boolean;
yesPlaylist?: boolean;
ageLimit?: number;
downloadArchive?: string;
includeAds?: boolean;
limitRate?: string;
retries?: number | "infinite";
skipUnavailableFragments?: boolean;
abortOnUnavailableFragment?: boolean;
keepFragments?: boolean;
bufferSize?: string;
noResizeBuffer?: boolean;
httpChunkSize?: string;
playlistReverse?: boolean;
playlistRandom?: boolean;
xattrSetFilesize?: boolean;
hlsPreferNative?: boolean;
hlsPreferFfmpeg?: boolean;
hlsUseMpegts?: boolean;
externalDownloader?: string;
externalDownloaderArgs?: string;
batchFile?: string;
id?: boolean;
output?: string;
outputNaPlaceholder?: string;
autonumberStart?: number;
restrictFilenames?: boolean;
noOverwrites?: boolean;
continue?: boolean;
noPart?: boolean;
noMtime?: boolean;
writeDescription?: boolean;
writeInfoJson?: boolean;
writeAnnotations?: boolean;
loadInfoJson?: string;
cookies?: string;
cacheDir?: string;
noCacheDir?: boolean;
rmCacheDir?: boolean;
writeThumbnail?: boolean;
writeAllThumbnails?: boolean;
listThumbnails?: boolean;
quiet?: boolean;
noWarnings?: boolean;
simulate?: boolean;
skipDownload?: boolean;
getUrl?: boolean;
getTitle?: boolean;
getId?: boolean;
getThumbnail?: boolean;
getDuration?: boolean;
getFilename?: boolean;
getFormat?: boolean;
dumpJson?: boolean;
dumpSingleJson?: boolean;
printJson?: boolean;
newline?: boolean;
noProgress?: boolean;
consoleTitle?: boolean;
verbose?: boolean;
dumpPages?: boolean;
writePages?: boolean;
printTraffic?: boolean;
callHome?: boolean;
encoding?: string;
noCheckCertificate?: boolean;
preferInsecure?: boolean;
userAgent?: string;
referer?: string;
addHeader?: string;
bidiWorkaround?: boolean;
sleepInterval?: number;
maxSleepInterval?: number;
format?: string;
allFormats?: boolean;
preferFreeFormats?: boolean;
listFormats?: boolean;
youtubeSkipDashManifest?: boolean;
mergeOutputFormat?: string;
writeSub?: boolean;
writeAutoSub?: boolean;
allSubs?: boolean;
listSubs?: boolean;
subFormat?: string;
subLang?: string;
username?: string;
password?: string;
twofactor?: string;
netrc?: boolean;
videoPassword?: string;
apMso?: string;
apUsername?: string;
apPassword?: string;
apListMso?: boolean;
extractAudio?: boolean;
audioFormat?: string;
audioQuality?: number;
recodeVideo?: string;
postprocessorArgs?: string;
keepVideo?: boolean;
noPostOverwrites?: boolean;
embedSubs?: boolean;
embedThumbnail?: boolean;
addMetadata?: boolean;
metadataFromFile?: string;
xattrs?: boolean;
fixup?: string;
preferAvconv?: boolean;
preferFfmpeg?: boolean;
ffmpegLocation?: string;
exec?: string;
convertSubs?: string;
}
interface YtdlFormat {
asr: number;
filesize: number;
format_id: string;
format_note: string;
fps: number;
height: number;
quality: number;
tbr: number;
url: string;
width: number;
ext: string;
vcodec: string;
acodec: string;
abr: number;
downloader_options: unknown;
container: string;
format: string;
protocol: string;
http_headers: unknown;
}
interface YtdlThumbnail {
height: number;
url: string;
width: number;
resolution: string;
id: string; id: string;
} original_url: string;
title?: string;
interface YtdlEntry { uploader?: string;
uploader: string; duration?: number;
duration: number;
id: string;
title: string;
url: string;
}
export interface YtdlResponse {
id: string;
title: string;
formats: YtdlFormat[];
thumbnails: YtdlThumbnail[];
description: string;
upload_date: string;
uploader: string;
uploader_id: string;
uploader_url: string;
channel_id: string;
channel_url: string;
duration: number;
view_count: number;
average_rating: number;
age_limit: number;
webpage_url: string;
categories: string[];
tags: string[];
is_live: boolean;
like_count: number;
dislike_count: number;
channel: string;
track: string;
artist: string;
creator: string;
alt_title: string;
extractor: string;
webpage_url_basename: string;
extractor_key: string; extractor_key: string;
playlist: string; thumbnail?: string;
playlist_index: number; }
export interface YtdlResult {
url: string;
title: string;
thumbnail: string; thumbnail: string;
display_id: string; duration: number;
requested_subtitles: unknown; }
asr: number;
filesize: number; export interface YtdlStreamOptions extends downloadOptions {
format_id: string; seek?: number;
format_note: string; encoderArgs?: string[];
fps: number; fmt?: string;
height: number; opusEncoded?: boolean;
quality: number; }
tbr: number;
url: string; export interface StreamOptions {
width: number; seek?: number;
ext: string; encoderArgs?: string[];
vcodec: string; fmt?: string;
acodec: string; opusEncoded?: boolean;
abr: number;
downloader_options: { http_chunk_size: number };
container: string;
format: string;
protocol: string;
http_headers: unknown;
fulltitle: string;
_filename: string;
entries: YtdlEntry[];
} }
+9 -4
View File
@@ -1,11 +1,16 @@
import { Events, Interaction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; import {
ChatInputCommandInteraction,
Events,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) { if (!interaction.inCachedGuild()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
return; return;
@@ -49,7 +54,7 @@ async function execute(client: Client, interaction: Interaction): Promise<void>
await interaction.editReply(replies.auto("add role to a member", event)); await interaction.editReply(replies.auto("add role to a member", event));
} catch (error) { } catch (error) {
logger.error("Error occured while quering db.", { interaction }); logger.error("Error occured while quering db.", { error });
} finally { } finally {
await client.db.$disconnect(); await client.db.$disconnect();
} }
+3 -3
View File
@@ -1,10 +1,10 @@
import { Interaction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import Client from "../../../lib/client.js"; import Client from "../../../lib/client.js";
import logger from "../../../lib/logger.js"; import logger from "../../../lib/logger.js";
import replies from "../lib/replies.js"; import replies from "../lib/replies.js";
async function execute(client: Client, interaction: Interaction): Promise<void> { async function execute(client: Client, interaction: ChatInputCommandInteraction): Promise<void> {
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) { if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
logger.warn("Interaction rejected.", { interaction }); logger.warn("Interaction rejected.", { interaction });
@@ -20,7 +20,7 @@ async function execute(client: Client, interaction: Interaction): Promise<void>
const member = await interaction.guild.members.fetch(user).catch(() => undefined); const member = await interaction.guild.members.fetch(user).catch(() => undefined);
if (member) { if (member) {
await interaction.reply(replies.roleList(user.tag, member.roles.cache)); await interaction.reply(replies.roleList(user.username, member.roles.cache));
} else { } else {
await interaction.reply(replies.notMember("edited", user)); await interaction.reply(replies.notMember("edited", user));
} }
+2 -2
View File
@@ -25,7 +25,7 @@ const replies = {
notMember: (action: string, user: User): InteractionReplyOptions => { notMember: (action: string, user: User): InteractionReplyOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor("Blue") .setColor("Blue")
.setTitle(`${user.tag} cannot be ${action}`) .setTitle(`${user.username} cannot be ${action}`)
.setDescription("They're not a guild member.") .setDescription("They're not a guild member.")
.setTimestamp(); .setTimestamp();
@@ -39,7 +39,7 @@ const replies = {
): InteractionReplyOptions => { ): InteractionReplyOptions => {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(config.colors.success) .setColor(config.colors.success)
.setTitle(`Role ${roleName} was ${action} user ${user.tag}`) .setTitle(`Role ${roleName} was ${action} user ${user.username}`)
.setDescription(reason ?? "No reason provided.") .setDescription(reason ?? "No reason provided.")
.setTimestamp(); .setTimestamp();