Refactoring, comments & overall improvements
This commit is contained in:
60 files changed
+1106
-708
No files matched your search
+5
-2
@@ -5,12 +5,15 @@
|
||||
!Dockerfile
|
||||
|
||||
# Source
|
||||
!config.ts
|
||||
!src/
|
||||
!prisma/
|
||||
!modules/
|
||||
|
||||
# Dependencies
|
||||
!package.json
|
||||
!pnpm-lock.yaml
|
||||
|
||||
# Typescript compiller config
|
||||
# Build
|
||||
!build.sh
|
||||
!config.tsconfig.json
|
||||
!tsconfig.json
|
||||
+21
-28
@@ -1,6 +1,7 @@
|
||||
FROM node:18-alpine as builder
|
||||
FROM node:20-alpine as builder
|
||||
|
||||
RUN apk add --no-cache python3
|
||||
RUN apk add --no-cache curl
|
||||
RUN apk add --no-cache make
|
||||
RUN apk add --no-cache gcc
|
||||
RUN apk add --no-cache build-base
|
||||
@@ -17,35 +18,27 @@ COPY . .
|
||||
|
||||
RUN pnpm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
RUN apk update
|
||||
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
|
||||
|
||||
ENV NODE_ENV production
|
||||
USER node
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN mkdir bin
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/src/app/bin/yt-dlp
|
||||
RUN chmod a+rx /usr/src/app/bin/yt-dlp
|
||||
|
||||
COPY prisma/ .
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN mkdir ./dist/bin/
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/src/app/dist/bin/yt-dlp
|
||||
RUN chmod a+rx /usr/src/app/dist/bin/yt-dlp
|
||||
|
||||
RUN pnpm install --prod
|
||||
|
||||
RUN pnpx prisma generate
|
||||
|
||||
COPY --from=builder /usr/src/app/dist ./dist
|
||||
FROM node:20-alpine
|
||||
|
||||
CMD [ "pnpm", "start" ]
|
||||
RUN apk update
|
||||
RUN apk add --no-cache ffmpeg
|
||||
RUN apk add --no-cache python3
|
||||
|
||||
USER node
|
||||
ENV NODE_ENV production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json ./
|
||||
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/app/dist ./
|
||||
|
||||
CMD [ "node", "./src/index.js" ]
|
||||
@@ -35,7 +35,8 @@ modules/example/
|
||||
interface Module {
|
||||
name: string; // Name of the module.
|
||||
description?: string; // Description of the module.
|
||||
controller?: unknown; // The service orchestrating the module. (use it in commands or events)
|
||||
controller?: unknown;
|
||||
// The service orchestrating the module. (passed as third argument to command or event executor)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -49,7 +50,7 @@ Each **command** has to be defined in its own file, preferably named after **the
|
||||
```ts
|
||||
interface Command {
|
||||
data: ApplicationCommandData; // Definition of command specifically for the Discord API.
|
||||
execute: (client: Client, interaction: CommandInteraction) => Promise<void>;
|
||||
execute: (client: Client, interaction: CommandInteraction, controller?: unknown) => Promise<void>;
|
||||
// Function that will be called upon interaction invocation.
|
||||
}
|
||||
```
|
||||
@@ -66,7 +67,7 @@ bound to **them** by **modules**, despite being declared similarly to **commands
|
||||
```ts
|
||||
export interface Event {
|
||||
name: string;
|
||||
execute: (client: Client, eventee: unknown[]) => Promise<void>;
|
||||
execute: (client: Client, eventee: unknown[], controller?: unknown) => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -97,7 +98,6 @@ that shall be located at _bin/_ directory or be accessible from _the environment
|
||||
|
||||
| **Base/Module** | **Dependency** |
|
||||
| --------------- | ------------------------------------------ |
|
||||
| Base | [postgres](https://www.postgresql.org/) |
|
||||
| Music | [yt-dlp](https://github.com/yt-dlp/yt-dlp) |
|
||||
|
||||
### Shipping
|
||||
@@ -113,7 +113,6 @@ provided by the respective
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-e DATABASE_URL=*as required by prisma - postgres connection url* \
|
||||
-e DISCORD_TOKEN=*aquired via Discord Developer Portal* \
|
||||
--name quetza -d \
|
||||
unknowableshade/quetza-bot:latest
|
||||
@@ -123,12 +122,6 @@ unknowableshade/quetza-bot:latest
|
||||
|
||||
Although it is possible to run it directly from the source, some additional steps are required.
|
||||
|
||||
- Push _SQL migrations_ using _prisma_ CLI (that will also generate _client_)
|
||||
|
||||
```bash
|
||||
pnpx prisma migrate deploy
|
||||
```
|
||||
|
||||
- Compile _TypeScript_
|
||||
|
||||
```bash
|
||||
@@ -148,9 +141,6 @@ In any case, you have to provide **environmental variables** for **bot** to run.
|
||||
- **DISCORD_TOKEN**: generated token issued by **Discord** itself at
|
||||
**[Developer Portal](https://discord.com/developers/applications)**.
|
||||
|
||||
- **DATABASE_URL**: _postgres_ connection URL,
|
||||
if _prisma_ database provider has not been changed.
|
||||
|
||||
## Licence
|
||||
|
||||
**Quetza's** source code is licensed under **[the MIT Licence](,/LICENSE)**.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Build typescript
|
||||
pnpm exec tsc --build
|
||||
|
||||
# Map path aliases from tsconfigs
|
||||
for TS_CONFIG in ./config.tsconfig.json ./src/tsconfig.json ./modules/*/tsconfig.json; do
|
||||
pnpm exec tsc-alias -p "$TS_CONFIG"
|
||||
done
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ActivityOptions, ActivityType, ColorResolvable } from "discord.js";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
/** Quetza project root directory. */
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
/** Quetza's modules directory. */
|
||||
const modulesDir = join(rootDir, "/modules/");
|
||||
/** External binaries directory. */
|
||||
const binariesDir = join(rootDir, "/bin/");
|
||||
/** Logger files directory. */
|
||||
const loggerDir = join(rootDir, "/log/");
|
||||
|
||||
/** Locations that might be useful in application. */
|
||||
const path = {
|
||||
root: rootDir,
|
||||
modules: modulesDir,
|
||||
binaries: binariesDir,
|
||||
log: loggerDir
|
||||
};
|
||||
|
||||
/** Discord bot token. */
|
||||
const appToken = process.env.DISCORD_TOKEN;
|
||||
/** Discord bot user activity status */
|
||||
const appActivity = { type: ActivityType.Watching, name: "over you" };
|
||||
|
||||
/** Bot application configuration variables. */
|
||||
const application = {
|
||||
token: appToken,
|
||||
activity: appActivity as ActivityOptions
|
||||
};
|
||||
|
||||
/** Colors Quetza uses in responses. */
|
||||
const colors = {
|
||||
default: "#ff6e9b" as ColorResolvable,
|
||||
error: "#b94e59" as ColorResolvable,
|
||||
warning: "#e5925f" as ColorResolvable,
|
||||
info: "#367df6" as ColorResolvable,
|
||||
success: "#83d18e" as ColorResolvable
|
||||
};
|
||||
|
||||
/** Guild where testing occures. */
|
||||
const testGuild = "912401672939139142";
|
||||
|
||||
/** Developing options. */
|
||||
const dev = {
|
||||
guild: testGuild
|
||||
};
|
||||
|
||||
/** Exported bundled configuration object. */
|
||||
const config = {
|
||||
application,
|
||||
colors,
|
||||
path,
|
||||
dev
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.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> {
|
||||
@@ -1,13 +1,11 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"CR1001": {
|
||||
"id": 652627557,
|
||||
"title": "Imported Modules",
|
||||
"color": 6412972,
|
||||
"fields": [],
|
||||
"footer": {
|
||||
"text": "If there is a bug - submit it"
|
||||
},
|
||||
"timestamp": "2024-01-18T21:00:00.000Z"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Events, Interaction } from "discord.js";
|
||||
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
/**
|
||||
* Resolves creation of an interaction by users.
|
||||
*
|
||||
* @remark Only {@link ChatInputCommand} currently being resolved.
|
||||
*
|
||||
* @param client - Quetza client.
|
||||
* @param eventee - List containing invoked interaction.
|
||||
*/
|
||||
async function execute(client: Client, eventee: [Interaction]): Promise<void> {
|
||||
const [interaction] = eventee;
|
||||
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
logger.notice("Interaction is not a ChatInputCommand.");
|
||||
return;
|
||||
}
|
||||
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
logger.notice("Interaction does not exist.", interaction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("Interaction was created.", interaction);
|
||||
|
||||
await command.execute(client, interaction, command.module.controller);
|
||||
} catch (error) {
|
||||
logger.error("Interaction has and error occured.", error, interaction);
|
||||
}
|
||||
}
|
||||
|
||||
/** Event name. */
|
||||
const name = Events.InteractionCreate;
|
||||
|
||||
export { execute, name };
|
||||
@@ -0,0 +1,48 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* Function that will execute upon emitting Client's Ready event.
|
||||
*
|
||||
* @remarks Will push slash commands, set activity status,
|
||||
* and log bot's initials.
|
||||
*
|
||||
* @param client - Quetza client.
|
||||
*/
|
||||
async function execute(client: Client<true>): Promise<void> {
|
||||
const commandData = client.commands.map((value) => value.data);
|
||||
|
||||
// To not reload bot's global slash commands at every dev build.
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
logger.info(
|
||||
"Development mode: Commands will be pushed to the _test guild_!",
|
||||
client.generateApplicationStatus()
|
||||
);
|
||||
logger.info("Dependency report by '@discordjs/voice'.", {
|
||||
report: generateDependencyReport()
|
||||
});
|
||||
|
||||
const guild = await client.guilds.fetch(config.dev.guild);
|
||||
guild.commands.set(commandData);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
logger.info(
|
||||
"Production mode: Commands will be pushed to the _application_!",
|
||||
client.generateApplicationStatus()
|
||||
);
|
||||
|
||||
await client.application.commands.set(commandData);
|
||||
}
|
||||
|
||||
client.user.setActivity(config.application.activity);
|
||||
}
|
||||
|
||||
/** Event name. */
|
||||
const name = Events.ClientReady;
|
||||
|
||||
export { execute, name };
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseMessageOptions, Collection, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import { Module } from "../../../lib/types.js";
|
||||
import config from "$config.js";
|
||||
import { Module } from "$lib/types.js";
|
||||
|
||||
const replies = {
|
||||
modules: (modules: Collection<string, Module>): BaseMessageOptions => {
|
||||
@@ -1,4 +1,6 @@
|
||||
/** Module's name. */
|
||||
const name = "core";
|
||||
/** Module's description */
|
||||
const description = "Quetza's core functionality";
|
||||
|
||||
export { description, name };
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.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";
|
||||
@@ -35,11 +36,7 @@ const data = new SlashCommandBuilder()
|
||||
option
|
||||
.setName("filter")
|
||||
.setDescription("Filter to apply.")
|
||||
.setChoices(
|
||||
...Object.keys(Filter.filters).map((key) => {
|
||||
return { name: key, value: key };
|
||||
})
|
||||
)
|
||||
.setChoices(...Object.values(Filter.FILTERS))
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.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";
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
SlashCommandBuilder
|
||||
} from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "../../../lib/client.js";
|
||||
import logger from "../../../lib/logger.js";
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import replies from "../lib/replies.js";
|
||||
import { controller } from "../module.js";
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { BaseGuildVoiceChannel, User, VoiceState } from "discord.js";
|
||||
|
||||
import Client from "$lib/client.js";
|
||||
import Music from "$mlib/music.js";
|
||||
|
||||
import { asleep } from "../lib/misc.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
/** Amount of time Quetza can be alone in voice channel. */
|
||||
const TIME_TILL_DISCONNECT = 30_000;
|
||||
|
||||
/**
|
||||
* Deduct {@link BaseGuildVoiceChannel} user is a member of.
|
||||
*
|
||||
* @returns BaseGuildVoiceChannel user is a member of, null if not a member of any.
|
||||
*
|
||||
* @param user - User that might be in one of a channels.
|
||||
* @param channels - List of channels that might have user as it member.
|
||||
*/
|
||||
function resolveVoiceChannel(
|
||||
user: User,
|
||||
...channels: (BaseGuildVoiceChannel | null)[]
|
||||
): BaseGuildVoiceChannel | null {
|
||||
return (
|
||||
channels.find((channel) => channel?.members.some((member) => member.user === user)) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks voice channel Quetza is connected to.
|
||||
*
|
||||
* @remark Destroys player if Quetza appears to be alone.
|
||||
*
|
||||
* @param client - Quetza client.
|
||||
* @param eventee - Old and new {@link VoiceState}.
|
||||
* @param controller - Module's controller.
|
||||
*/
|
||||
async function execute(
|
||||
client: Client<true>,
|
||||
eventee: [VoiceState, VoiceState],
|
||||
controller: Music
|
||||
): Promise<void> {
|
||||
const [oldState, newState] = eventee;
|
||||
|
||||
const player = controller.get(oldState.guild);
|
||||
|
||||
const channel = resolveVoiceChannel(client.user, oldState.channel, newState.channel);
|
||||
|
||||
if (!player || !channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel.members.size === 1) {
|
||||
await asleep(TIME_TILL_DISCONNECT);
|
||||
if (channel.members.size === 1 && channel.members.first()?.user === client.user) {
|
||||
await player.channel.send(replies.alone());
|
||||
player.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Event name. */
|
||||
const name = "voiceStateUpdate";
|
||||
|
||||
export { execute, name };
|
||||
@@ -0,0 +1,9 @@
|
||||
import QuetzaError from "$lib/error.js";
|
||||
|
||||
export default class MusicError extends QuetzaError {
|
||||
public static readonly PREFIX = "MZ";
|
||||
|
||||
constructor(msg: string, code: number) {
|
||||
super(msg, code, MusicError.PREFIX);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DlpDump, dlpExtractorKey, DlpInfo } from "../types";
|
||||
import { DlpDump, dlpExtractorKey, DlpInfo } from "$mlib/types.js";
|
||||
|
||||
export const dlpExtractorsValues = ["Youtube", "Soundcloud"] as const;
|
||||
|
||||
@@ -7,14 +7,10 @@ export const dlpExtractorsValues = ["Youtube", "Soundcloud"] as const;
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* @see {@link youtubeExtractor | the youtubeExtractor(...) function }
|
||||
* @see the {@link youtubeExtractor | youtubeExtractor(...) } function
|
||||
*/
|
||||
type dlpExtractor = (arg0: DlpDump, arg1: (arg0: string) => Promise<DlpInfo>) => Promise<DlpInfo>;
|
||||
|
||||
export function isDlpExtractorKey(that: string): that is dlpExtractorKey {
|
||||
return dlpExtractorsValues.includes(that as dlpExtractorKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves data that was extracted by dlp (depending on extractor_key).
|
||||
*
|
||||
@@ -33,7 +29,7 @@ const youtubeExtractor: dlpExtractor = async (dump, dlpInfo) => {
|
||||
duration: dump.duration
|
||||
};
|
||||
|
||||
return [[result], []];
|
||||
return [result];
|
||||
}
|
||||
|
||||
return await dlpInfo(dump.original_url);
|
||||
@@ -51,12 +47,15 @@ const soundcloudExtractor: dlpExtractor = async (dump, dlpInfo) => {
|
||||
duration: dump.duration
|
||||
};
|
||||
|
||||
return [[result], []];
|
||||
return [result];
|
||||
}
|
||||
|
||||
return await dlpInfo(dump.original_url);
|
||||
};
|
||||
|
||||
/**
|
||||
* DlpDump extractor_key values mapped to corresponding extractor.
|
||||
*/
|
||||
const extractors: Record<dlpExtractorKey, dlpExtractor> = {
|
||||
Youtube: youtubeExtractor,
|
||||
Soundcloud: soundcloudExtractor
|
||||
@@ -4,10 +4,12 @@ import prismMedia from "prism-media";
|
||||
import { Readable } from "stream";
|
||||
import { promisify } from "util";
|
||||
|
||||
import config from "../../../../config.js";
|
||||
import logger from "../../../../lib/logger.js";
|
||||
import config from "$config.js";
|
||||
import logger from "$lib/logger.js";
|
||||
import MusicError from "$mlib/MusicError.js";
|
||||
|
||||
import { DlpDump, DlpInfo, dlpStreamOptions } from "../types.js";
|
||||
import extractors, { isDlpExtractorKey } from "./extractors.js";
|
||||
import extractors from "./extractors.js";
|
||||
|
||||
const { FFmpeg } = prismMedia;
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -15,7 +17,30 @@ const execFileAsync = promisify(execFile);
|
||||
/**
|
||||
* Maximum length of dlp search stack.
|
||||
*/
|
||||
const DLPINFO_DEPTH = 5;
|
||||
const DLPINFO_DEPTH = 3;
|
||||
|
||||
/**
|
||||
* Path to the Dlp binary.
|
||||
*/
|
||||
const DLP_BINARY = path.join(config.path.binaries, "/yt-dlp");
|
||||
|
||||
/**
|
||||
* Base arguments for Ffmpeg transcoder.
|
||||
*/
|
||||
const FFMPEG_BASE_ARGS = [
|
||||
"-analyzeduration",
|
||||
"0",
|
||||
"-loglevel",
|
||||
"0",
|
||||
"-acodec",
|
||||
"libopus",
|
||||
"-f",
|
||||
"opus",
|
||||
"-ar",
|
||||
"48000",
|
||||
"-ac",
|
||||
"2"
|
||||
];
|
||||
|
||||
/**
|
||||
* Predicate to check whether is string a valid URL.
|
||||
@@ -43,14 +68,14 @@ function isURL(str: string): boolean {
|
||||
async function dlpInfoRecursive(query: string, depth = DLPINFO_DEPTH): Promise<DlpInfo> {
|
||||
const error = (message: string, reason: number, ...meta: unknown[]): DlpInfo => {
|
||||
logger.warn(message, { reason, query, ...meta });
|
||||
return [[], [{ query, reason }]];
|
||||
throw new MusicError(message, reason);
|
||||
};
|
||||
|
||||
if (!depth) {
|
||||
return error("Dlp: recursion depth reached", 408);
|
||||
return error("Dlp: recursion depth reached", 1010);
|
||||
}
|
||||
|
||||
const subprocess = await execFileAsync(path.join(config.binariesDir, "/yt-dlp"), [
|
||||
const subprocess = await execFileAsync(DLP_BINARY, [
|
||||
query,
|
||||
"-O",
|
||||
"%(.{id,original_url,title,uploader,duration,extractor_key,thumbnail})j",
|
||||
@@ -59,20 +84,23 @@ async function dlpInfoRecursive(query: string, depth = DLPINFO_DEPTH): Promise<D
|
||||
]);
|
||||
|
||||
if (!subprocess.stdout) {
|
||||
return error("Dlp: no stdout", 400);
|
||||
return error("Dlp: no stdout", 1011);
|
||||
}
|
||||
|
||||
if (subprocess.stderr) {
|
||||
return error("Dlp: stderr", 500, {
|
||||
return error("Dlp: stderr", 1012, {
|
||||
stderr: subprocess.stderr
|
||||
});
|
||||
}
|
||||
|
||||
const dumps: DlpDump[] = JSON.parse("[" + subprocess.stdout.replace(/\n(?=.+)/g, ",") + "]");
|
||||
const dumps: DlpDump[] = subprocess.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((out) => JSON.parse(out));
|
||||
|
||||
const resolveDump = async (dump: DlpDump): Promise<DlpInfo> => {
|
||||
if (!isDlpExtractorKey(dump.extractor_key)) {
|
||||
throw new Error("Dlp: illegal extractor");
|
||||
if (!Object.hasOwn(extractors, dump.extractor_key)) {
|
||||
error("Dlp: illegal extractor", 1013);
|
||||
}
|
||||
|
||||
return extractors[dump.extractor_key](dump, (subquery: string) =>
|
||||
@@ -80,14 +108,9 @@ async function dlpInfoRecursive(query: string, depth = DLPINFO_DEPTH): Promise<D
|
||||
);
|
||||
};
|
||||
|
||||
const result = await Promise.all(dumps.map(resolveDump)).catch((err: Error): DlpInfo[] => {
|
||||
return [error(err.message, 417, { query })];
|
||||
});
|
||||
const result = await Promise.all(dumps.map(resolveDump));
|
||||
|
||||
return result.reduce(
|
||||
(prev, curr) => [prev[0].concat(curr[0]), prev[1].concat(curr[1])],
|
||||
[[], []]
|
||||
);
|
||||
return result.flat();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,21 +132,8 @@ export async function dlpInfo(query: string): Promise<DlpInfo> {
|
||||
* @param url - Dlp compatible URL to the requested resource
|
||||
* @param options - Transcoders and dlp options
|
||||
*/
|
||||
export function dlpStream(url: string, options?: dlpStreamOptions) {
|
||||
let ffmpegArgs: string[] = [
|
||||
"-analyzeduration",
|
||||
"0",
|
||||
"-loglevel",
|
||||
"0",
|
||||
"-acodec",
|
||||
"libopus",
|
||||
"-f",
|
||||
"opus",
|
||||
"-ar",
|
||||
"48000",
|
||||
"-ac",
|
||||
"2"
|
||||
];
|
||||
export function dlpStream(url: string, options?: dlpStreamOptions): Promise<Readable> {
|
||||
let ffmpegArgs = FFMPEG_BASE_ARGS;
|
||||
|
||||
if (options?.seek) {
|
||||
ffmpegArgs.unshift("-ss", options.seek.toString());
|
||||
@@ -135,7 +145,7 @@ export function dlpStream(url: string, options?: dlpStreamOptions) {
|
||||
|
||||
return new Promise<Readable>((resolve, reject) => {
|
||||
const subprocess = execFile(
|
||||
path.join(config.binariesDir, "/yt-dlp"),
|
||||
DLP_BINARY,
|
||||
[
|
||||
url,
|
||||
"-o",
|
||||
@@ -0,0 +1,55 @@
|
||||
type filterValue = {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const FILTERS: Record<string, filterValue> = {
|
||||
bassboost1x: { name: "Bassboost 1x", value: "bass=g=15:f=110:w=0.3" },
|
||||
bassboost2x: { name: "Bassboost 2x", value: "bass=g=30:f=110:w=0.3" },
|
||||
daycore: { name: "Daycore", value: "aresample=48000,asetrate=48000*0.75" },
|
||||
nightcore: { name: "Nightcore", value: "aresample=48000,asetrate=48000*1.25" },
|
||||
treble: {
|
||||
name: "Treble Boost",
|
||||
value: "firequalizer=gain_entry='entry(2500,3);entry(6300,6);entry(16000,10)'"
|
||||
}
|
||||
} as const;
|
||||
|
||||
type filterPreset = keyof typeof FILTERS;
|
||||
|
||||
export default class Filter {
|
||||
public static readonly FILTERS = FILTERS;
|
||||
|
||||
private switches_ = new Map<filterPreset, filterValue>();
|
||||
|
||||
public toggled(): filterValue[] {
|
||||
return [...this.switches_.values()];
|
||||
}
|
||||
|
||||
public toggle(query?: filterPreset): boolean {
|
||||
if (!query) {
|
||||
this.switches_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switches_.delete(query)) {
|
||||
this.switches_.set(query, FILTERS[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.value)
|
||||
.join(",");
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,6 +1,7 @@
|
||||
import { Collection, Guild, GuildTextBasedChannel } from "discord.js";
|
||||
|
||||
import logger from "../../../lib/logger.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import Player from "./player.js";
|
||||
|
||||
export default class Music {
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "@discordjs/voice";
|
||||
import { Guild, GuildTextBasedChannel, Message, User, VoiceBasedChannel } from "discord.js";
|
||||
|
||||
import { clamp } from "$lib/misc.js";
|
||||
|
||||
import { dlpInfo, dlpStream } from "./fetch/fetch.js";
|
||||
import Filter from "./filter.js";
|
||||
import { largestCommonSequence } from "./misc.js";
|
||||
@@ -18,10 +20,33 @@ import Queue from "./queue.js";
|
||||
import replies from "./replies.js";
|
||||
import { Track } from "./types.js";
|
||||
|
||||
/**
|
||||
* Defines guild music player connected to a voice channel by wrapping AudioPlayer, Queue, VoiceConnection, etc.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default class Player {
|
||||
private music: Music;
|
||||
private guild: Guild;
|
||||
/**
|
||||
* Volume limiter clamp.
|
||||
*/
|
||||
public static readonly VOLUME_CLAMP = clamp(0, 150);
|
||||
|
||||
/**
|
||||
* Module controller orchistrate guild-player mapping.
|
||||
*/
|
||||
private music_: Music;
|
||||
|
||||
/**
|
||||
* {@link Player} owner guild.
|
||||
*
|
||||
* @remarks No different {@link Player} instances can have same {@link Guild}.
|
||||
*/
|
||||
private guild_: Guild;
|
||||
|
||||
/**
|
||||
* {@link Guild | Guild's} text based channel for player notifications,
|
||||
* such as Current Playing Track.
|
||||
*/
|
||||
private channel_: GuildTextBasedChannel;
|
||||
public get channel(): GuildTextBasedChannel {
|
||||
return this.channel_;
|
||||
@@ -30,44 +55,82 @@ export default class Player {
|
||||
this.channel_ = x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Previous notification message.
|
||||
*/
|
||||
private message_?: Message;
|
||||
|
||||
/**
|
||||
* A connection to the voice channel of a {@link Guild}.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
private connection_?: VoiceConnection;
|
||||
public get connection(): VoiceConnection | undefined {
|
||||
return this.connection_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player for {@link AudioResource} that is subscribed to the most recent {@link VoiceConnection}.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
private player_: AudioPlayer;
|
||||
public get player(): AudioPlayer {
|
||||
return this.player_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an Audio Resource that can be played by {@link AudioPlayer}.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
private resource_: AudioResource<Track> | undefined;
|
||||
public get resource(): AudioResource<Track> | undefined {
|
||||
return this.resource_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline volume for an {@link AudioResource}.
|
||||
* Must be clamped by {@link VOLUME_CLAMP}.
|
||||
*/
|
||||
private volume_ = 100;
|
||||
public get volume(): number {
|
||||
return this.volume_;
|
||||
}
|
||||
public set volume(x: number) {
|
||||
this.volume_ = Math.max(0, Math.min(150, x));
|
||||
this.volume_ = Player.VOLUME_CLAMP(x);
|
||||
|
||||
if (this.resource_ && this.resource_.volume) {
|
||||
if (this.resource_?.volume) {
|
||||
this.resource_.volume.setVolumeLogarithmic(this.volume_ / 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue manager of the {@link Player}.
|
||||
*/
|
||||
public readonly queue = new Queue();
|
||||
|
||||
/**
|
||||
* Filter manager of the {@link Player}.
|
||||
*/
|
||||
public readonly filter = new Filter();
|
||||
|
||||
/**
|
||||
* Timestamp when {@link Player} was created.
|
||||
*/
|
||||
public readonly createdAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Player constructor.
|
||||
*
|
||||
* @param guild - {@link Guild} for which player will be created for
|
||||
* @param music - Module controller for managing player own existence
|
||||
* @param channel - Initial text based channed for player notifications
|
||||
*/
|
||||
public constructor(guild: Guild, music: Music, channel: GuildTextBasedChannel) {
|
||||
this.music = music;
|
||||
this.guild = guild;
|
||||
this.music_ = music;
|
||||
this.guild_ = guild;
|
||||
this.channel_ = channel;
|
||||
|
||||
this.player_ = createAudioPlayer();
|
||||
@@ -75,7 +138,15 @@ export default class Player {
|
||||
this.player_.on(AudioPlayerStatus.Idle, async () => this.resourceEndResolvable());
|
||||
}
|
||||
|
||||
private async resourceGenerator(track: Track, seek?: number) {
|
||||
/**
|
||||
* Creates {@link AudioResource | an Audio Resource} for {@link an AudioPlayer}.
|
||||
*
|
||||
* @returns Configured audio resource ready to be played
|
||||
*
|
||||
* @param track - Metadata to generate resource from and embed into it
|
||||
* @param seek - Amount of seconds to start playback from
|
||||
*/
|
||||
private async resourceGenerator(track: Track, seek?: number): Promise<AudioResource<Track>> {
|
||||
const probe = await dlpStream(track.url, {
|
||||
seek: seek ? seek / 1000 : undefined,
|
||||
ffmpeg: !this.filter.empty() ? ["-af", this.filter.toString()] : undefined
|
||||
@@ -97,6 +168,13 @@ export default class Player {
|
||||
return this.resource_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves player behavior after {@link AudioResource} ends.
|
||||
*
|
||||
* @remarks Force destroys ended resource,
|
||||
* and either plays next one or stops,
|
||||
* depending on {@link Queue#next | Queue.next() method}
|
||||
*/
|
||||
private resourceEndResolvable(): void {
|
||||
if (this.resource_) {
|
||||
this.resource_.encoder?.destroy();
|
||||
@@ -110,6 +188,12 @@ export default class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves notification emittion.
|
||||
*
|
||||
* @remarks Tries to delete previous notification message,
|
||||
* and send next one
|
||||
*/
|
||||
private async messageResolvable(track: Track) {
|
||||
if (this.message_?.deletable) {
|
||||
await this.message_.delete().catch();
|
||||
@@ -118,18 +202,27 @@ export default class Player {
|
||||
this.message_ = await this.channel_.send(replies.nowPlaying(track));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches {@link DlpInfo} and converts it to {@link Track | Track metadata}.
|
||||
*
|
||||
* @returns Promise to Track list or undefined, if found nothing.
|
||||
*
|
||||
* @param query - String that Dlp will try to resolve
|
||||
* @param requester - {@link User} that requested Tracks returned to complete convertion
|
||||
*/
|
||||
private async search(query: string, requester: User): Promise<Track[] | undefined> {
|
||||
const [info] = await dlpInfo(query);
|
||||
|
||||
if (info.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const info = await dlpInfo(query);
|
||||
|
||||
return info.map((value) => {
|
||||
return { ...value, requester: requester };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays current {@link queue} track from position specified.
|
||||
*
|
||||
* @param seek - Amount of seconds to start playback from
|
||||
*/
|
||||
public async play(seek?: number): Promise<void> {
|
||||
const track = this.queue.current();
|
||||
|
||||
@@ -144,6 +237,12 @@ export default class Player {
|
||||
await this.messageResolvable(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates connection to guild's voice based channel,
|
||||
* previously destroying existing connection.
|
||||
*
|
||||
* @param channel - {@link VoiceBasedChannel} to connect to
|
||||
*/
|
||||
public connect(channel: VoiceBasedChannel): void {
|
||||
if (
|
||||
this.connection_?.joinConfig.channelId === channel.id &&
|
||||
@@ -165,6 +264,17 @@ export default class Player {
|
||||
this.connection_.subscribe(this.player_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link Track | Track(s)} found by {@link search | the search method} to the {@link queue}.
|
||||
*
|
||||
* @returns Track metadata if found, undefined otherwise
|
||||
*
|
||||
* @param query - String that will be passed to the search method
|
||||
* @param requester - {@link User} that requested the query
|
||||
* @param index - Position to insert track to
|
||||
* Value < 0 resolves to 0
|
||||
* Value >= (queue size) resolves to (queue end)
|
||||
*/
|
||||
public async add(query: string, requester: User, index?: number): Promise<Track | undefined> {
|
||||
const tracks = await this.search(query, requester);
|
||||
|
||||
@@ -177,6 +287,16 @@ export default class Player {
|
||||
return tracks[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Jumps across {@link queue} to the position determined by the query.
|
||||
*
|
||||
* @remarks If string passed, then position will be determined by
|
||||
* the largest common sequence across all the {@link queue} titles.
|
||||
*
|
||||
* @return Position to which jump was perfomed
|
||||
*
|
||||
* @param query - String of {@link Track} title or position to jump to
|
||||
*/
|
||||
public jump(query: number | string): number | undefined {
|
||||
let position = typeof query === "number" ? query : -1;
|
||||
let diff = 0;
|
||||
@@ -199,6 +319,16 @@ export default class Player {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes {@link Track} from {@link queue | the queue} by the position determined by the query.
|
||||
*
|
||||
* @remarks If string passed, then position will be determined by
|
||||
* the largest common sequence across all the {@link queue} titles.
|
||||
*
|
||||
* @return Metadata of removed track, or undefined if nothing happened
|
||||
*
|
||||
* @param query - String of {@link Track} title or position to perfome removal at
|
||||
*/
|
||||
public remove(query?: number | string): Track | undefined {
|
||||
const oldPosition = this.queue.position;
|
||||
|
||||
@@ -227,10 +357,18 @@ export default class Player {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts playback from timecode specified.
|
||||
*
|
||||
* @param time - Amount of seconds to start playback from
|
||||
*/
|
||||
public seek(time: number): void {
|
||||
this.play(time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears {@link queue} completely - not destroying {@link voiceConnection_}.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.queue.clear();
|
||||
this.resource_ = undefined;
|
||||
@@ -240,15 +378,23 @@ export default class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this instance of {@link Player} and deletes it from controller mapping.
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this.connection_) {
|
||||
this.connection_.destroy();
|
||||
}
|
||||
|
||||
this.player_.stop();
|
||||
this.music.delete(this.guild.id);
|
||||
this.music_.delete(this.guild_.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses/unpauses {@link AudioPlayer} playback.
|
||||
*
|
||||
* @returns True if paused, or False if unpaused
|
||||
*/
|
||||
public pause(): boolean {
|
||||
if (this.player_.state.status === AudioPlayerStatus.Paused) {
|
||||
this.player_.unpause();
|
||||
@@ -259,6 +405,9 @@ export default class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reshuffles {@link queue} order - will start new playback from the same position in the queue.
|
||||
*/
|
||||
public shuffle(): void {
|
||||
this.queue.shuffle();
|
||||
|
||||
@@ -268,10 +417,23 @@ export default class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops {@link player_} that will emit {@link resourceEndResolvable}.
|
||||
*
|
||||
* @see {@link resourceEndResolvable} for understanding full behavior
|
||||
*/
|
||||
public skip(): void {
|
||||
this.player_.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@link Filter} preset to the current playback.
|
||||
*
|
||||
* @returns True if applied, false if revoked
|
||||
*
|
||||
* @param filter - Name of a filter to apply
|
||||
* Must exist in Filter class
|
||||
*/
|
||||
public setFilter(filter?: string): boolean {
|
||||
const status = this.filter.toggle(filter);
|
||||
|
||||
File renamed without changes.
@@ -1,7 +1,8 @@
|
||||
import { AudioResource } from "@discordjs/voice";
|
||||
import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "../../../config.js";
|
||||
import config from "$config.js";
|
||||
|
||||
import { loopMoji, statusBarGenerator, toISOTime, volumeMoji } from "./misc.js";
|
||||
import Player from "./player.js";
|
||||
import Queue from "./queue.js";
|
||||
@@ -338,7 +339,7 @@ const replies = {
|
||||
value:
|
||||
player.filter
|
||||
.toggled()
|
||||
.map((value) => value[0])
|
||||
.map((value) => value.name)
|
||||
.join("\n") + "\ndefault",
|
||||
inline: true
|
||||
},
|
||||
@@ -0,0 +1,81 @@
|
||||
import { User } from "discord.js";
|
||||
|
||||
import { dlpExtractorsValues } from "./fetch/extractors.js";
|
||||
|
||||
/**
|
||||
* Track information fetched by Dlp.
|
||||
*/
|
||||
export interface TrackBase {
|
||||
/** URL of track. */
|
||||
url: string;
|
||||
|
||||
/** Track title. */
|
||||
title: string;
|
||||
|
||||
/** Link to track thumbnail. */
|
||||
thumbnail: string;
|
||||
|
||||
/** Track duration in seconds. */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track information with {@link User} that requested it.
|
||||
*/
|
||||
export interface Track extends TrackBase {
|
||||
/** User requested this track. */
|
||||
requester: User;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player loop options.
|
||||
*
|
||||
* @remarks LOOP - Looping queue;
|
||||
* SONG - Looping single song;
|
||||
* NONE - Stops after queue end; (default)
|
||||
* AUTO - Random order;
|
||||
*/
|
||||
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
|
||||
|
||||
/** Dlp fetched track list. */
|
||||
export type DlpInfo = TrackBase[];
|
||||
|
||||
/** Dlp supported extractors as types. */
|
||||
export type dlpExtractorKey = (typeof dlpExtractorsValues)[number];
|
||||
|
||||
/**
|
||||
* Actual Dlp dump that later translates to {@link DlpInfo}.
|
||||
* */
|
||||
export interface DlpDump {
|
||||
/** Id of resource. */
|
||||
id: string;
|
||||
|
||||
/** URL passed to Dlp or link to resource found. */
|
||||
original_url: string;
|
||||
|
||||
/** Link to webpage of resource requested. */
|
||||
webpage_url?: string;
|
||||
|
||||
/** Resource title. */
|
||||
title?: string;
|
||||
|
||||
/** Resource duration in seconds. */
|
||||
duration?: number;
|
||||
|
||||
/** Link to resource thumbnail. */
|
||||
thumbnail?: string;
|
||||
|
||||
/** Resource's extractor identifier. */
|
||||
extractor_key: dlpExtractorKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that shall be passed to DlpStream function.
|
||||
*/
|
||||
export interface dlpStreamOptions {
|
||||
/** Amount of seconds to start playback from. */
|
||||
seek?: number;
|
||||
|
||||
/** Args for FFMPEG transcoder. */
|
||||
ffmpeg?: string[];
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import Music from "./lib/music.js";
|
||||
|
||||
/** Module's name. */
|
||||
const name = "music";
|
||||
/** Module's description. */
|
||||
const description = "Music module";
|
||||
|
||||
/** Module's controller. */
|
||||
const controller = new Music();
|
||||
|
||||
export { controller, description, name };
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "quetza",
|
||||
"version": "2023.1.0-pre",
|
||||
"version": "2023.1.1-pre",
|
||||
"description": "Awesome Discord bot that performs various duties",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"main": "./dist/src/index.js",
|
||||
"scripts": {
|
||||
"start": "node ./dist/index.js",
|
||||
"build": "pnpm exec tsc --build",
|
||||
"start": "node ./dist/src/index.js",
|
||||
"build": "./build.sh",
|
||||
"clean": "rm -rf ./dist",
|
||||
"lint": "pnpm exec eslint . && pnpm exec prettier --check .",
|
||||
"format": "pnpm exec eslint --fix . && pnpm exec prettier --write .",
|
||||
@@ -49,7 +49,7 @@
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prisma": "^5.7.0",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
Generated
+54
-60
@@ -13,7 +13,7 @@ dependencies:
|
||||
version: 0.16.1(@discordjs/opus@0.9.0)
|
||||
"@prisma/client":
|
||||
specifier: 5.7.0
|
||||
version: 5.7.0(prisma@5.7.0)
|
||||
version: 5.7.0
|
||||
discord.js:
|
||||
specifier: ^14.14.1
|
||||
version: 14.14.1
|
||||
@@ -55,9 +55,9 @@ devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
prisma:
|
||||
specifier: ^5.7.0
|
||||
version: 5.7.0
|
||||
tsc-alias:
|
||||
specifier: ^1.8.8
|
||||
version: 1.8.8
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
@@ -352,7 +352,7 @@ packages:
|
||||
fastq: 1.15.0
|
||||
dev: true
|
||||
|
||||
/@prisma/client@5.7.0(prisma@5.7.0):
|
||||
/@prisma/client@5.7.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==
|
||||
@@ -364,52 +364,8 @@ packages:
|
||||
peerDependenciesMeta:
|
||||
prisma:
|
||||
optional: true
|
||||
dependencies:
|
||||
prisma: 5.7.0
|
||||
dev: false
|
||||
|
||||
/@prisma/debug@5.7.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==
|
||||
}
|
||||
|
||||
/@prisma/engines-version@5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==
|
||||
}
|
||||
|
||||
/@prisma/engines@5.7.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==
|
||||
}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
"@prisma/debug": 5.7.0
|
||||
"@prisma/engines-version": 5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9
|
||||
"@prisma/fetch-engine": 5.7.0
|
||||
"@prisma/get-platform": 5.7.0
|
||||
|
||||
/@prisma/fetch-engine@5.7.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==
|
||||
}
|
||||
dependencies:
|
||||
"@prisma/debug": 5.7.0
|
||||
"@prisma/engines-version": 5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9
|
||||
"@prisma/get-platform": 5.7.0
|
||||
|
||||
/@prisma/get-platform@5.7.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==
|
||||
}
|
||||
dependencies:
|
||||
"@prisma/debug": 5.7.0
|
||||
|
||||
/@sapphire/async-queue@1.5.1:
|
||||
resolution:
|
||||
{
|
||||
@@ -1070,6 +1026,14 @@ packages:
|
||||
text-hex: 1.0.0
|
||||
dev: false
|
||||
|
||||
/commander@9.5.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
|
||||
}
|
||||
engines: { node: ^12.20.0 || >=14 }
|
||||
dev: true
|
||||
|
||||
/concat-map@0.0.1:
|
||||
resolution:
|
||||
{
|
||||
@@ -2555,6 +2519,14 @@ packages:
|
||||
integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
}
|
||||
|
||||
/mylas@2.1.13:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==
|
||||
}
|
||||
engines: { node: ">=12.0.0" }
|
||||
dev: true
|
||||
|
||||
/nanoid@3.3.3:
|
||||
resolution:
|
||||
{
|
||||
@@ -2804,6 +2776,16 @@ packages:
|
||||
engines: { node: ">=8.6" }
|
||||
dev: true
|
||||
|
||||
/plimit-lit@1.6.1:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==
|
||||
}
|
||||
engines: { node: ">=12" }
|
||||
dependencies:
|
||||
queue-lit: 1.5.2
|
||||
dev: true
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution:
|
||||
{
|
||||
@@ -2844,17 +2826,6 @@ packages:
|
||||
"@discordjs/opus": 0.9.0
|
||||
dev: false
|
||||
|
||||
/prisma@5.7.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==
|
||||
}
|
||||
engines: { node: ">=16.13" }
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
"@prisma/engines": 5.7.0
|
||||
|
||||
/punycode@2.3.1:
|
||||
resolution:
|
||||
{
|
||||
@@ -2863,6 +2834,14 @@ packages:
|
||||
engines: { node: ">=6" }
|
||||
dev: true
|
||||
|
||||
/queue-lit@1.5.2:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==
|
||||
}
|
||||
engines: { node: ">=12" }
|
||||
dev: true
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution:
|
||||
{
|
||||
@@ -3302,6 +3281,21 @@ packages:
|
||||
}
|
||||
dev: false
|
||||
|
||||
/tsc-alias@1.8.8:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==
|
||||
}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chokidar: 3.5.3
|
||||
commander: 9.5.0
|
||||
globby: 11.1.0
|
||||
mylas: 2.1.13
|
||||
normalize-path: 3.0.0
|
||||
plimit-lit: 1.6.1
|
||||
dev: true
|
||||
|
||||
/tsconfig-paths@3.14.2:
|
||||
resolution:
|
||||
{
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Guild" (
|
||||
"id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Guild_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Event" (
|
||||
"objectId" SERIAL NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"Roles" TEXT[],
|
||||
"guildId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Event_pkey" PRIMARY KEY ("objectId")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Event_guildId_title_key" ON "Event"("guildId", "title");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Event" ADD CONSTRAINT "Event_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `Event` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- You are about to drop the column `Roles` on the `Event` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `objectId` on the `Event` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[title,action,guildId]` on the table `Event` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `action` to the `Event` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `metadata` to the `Event` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PenaltyType" AS ENUM ('BAN', 'TIMEOUT', 'WARN');
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Event_guildId_title_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Event" DROP CONSTRAINT "Event_pkey",
|
||||
DROP COLUMN "Roles",
|
||||
DROP COLUMN "objectId",
|
||||
ADD COLUMN "action" TEXT NOT NULL,
|
||||
ADD COLUMN "metadata" JSONB NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Queue" (
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"Tracks" JSONB NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"guildId" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Member" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"level" INTEGER NOT NULL DEFAULT 0,
|
||||
"guildId" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Penalty" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" "PenaltyType" NOT NULL,
|
||||
"reason" TEXT,
|
||||
"expires" TIMESTAMP(3),
|
||||
"guildId" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Queue_title_guildId_key" ON "Queue"("title", "guildId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Member_userId_guildId_key" ON "Member"("userId", "guildId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Penalty_type_userId_guildId_key" ON "Penalty"("type", "userId", "guildId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Event_title_action_guildId_key" ON "Event"("title", "action", "guildId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Queue" ADD CONSTRAINT "Queue_userId_guildId_fkey" FOREIGN KEY ("userId", "guildId") REFERENCES "Member"("userId", "guildId") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Queue" ADD CONSTRAINT "Queue_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Member" ADD CONSTRAINT "Member_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Penalty" ADD CONSTRAINT "Penalty_userId_guildId_fkey" FOREIGN KEY ("userId", "guildId") REFERENCES "Member"("userId", "guildId") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Penalty" ADD CONSTRAINT "Penalty_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,68 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Guild {
|
||||
id String @id
|
||||
events Event[]
|
||||
members Member[]
|
||||
penalties Penalty[]
|
||||
queues Queue[]
|
||||
}
|
||||
|
||||
model Queue {
|
||||
title String
|
||||
description String?
|
||||
updatedAt DateTime @updatedAt
|
||||
Tracks Json
|
||||
Member Member @relation(fields: [userId, guildId], references: [userId, guildId])
|
||||
userId String
|
||||
authorId String
|
||||
Guild Guild @relation(fields: [guildId], references: [id])
|
||||
guildId String
|
||||
@@unique([title, guildId], name: "queueIdentifier")
|
||||
}
|
||||
|
||||
model Member {
|
||||
userId String
|
||||
level Int @default(0)
|
||||
queues Queue[]
|
||||
Guild Guild @relation(fields: [guildId], references: [id])
|
||||
guildId String
|
||||
penalties Penalty[]
|
||||
@@unique([userId, guildId], name: "memberIdentifier")
|
||||
}
|
||||
|
||||
enum PenaltyType {
|
||||
BAN
|
||||
TIMEOUT
|
||||
WARN
|
||||
}
|
||||
|
||||
model Penalty {
|
||||
Member Member @relation(fields: [userId, guildId], references: [userId, guildId])
|
||||
userId String
|
||||
type PenaltyType
|
||||
reason String?
|
||||
expires DateTime?
|
||||
Guild Guild @relation(fields: [guildId], references: [id])
|
||||
guildId String
|
||||
@@unique([type, userId, guildId], name: "penaltyIdentifier")
|
||||
}
|
||||
|
||||
model Event {
|
||||
title String
|
||||
action String
|
||||
metadata Json
|
||||
Guild Guild @relation(fields: [guildId], references: [id])
|
||||
guildId String
|
||||
@@unique([title, action, guildId], name: "eventIdentifier")
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
+8
-3
@@ -1,8 +1,13 @@
|
||||
import { GatewayIntentBits } from "discord.js";
|
||||
|
||||
import config from "./config.js";
|
||||
import Client from "./lib/client.js";
|
||||
import config from "$config.js";
|
||||
import Client from "$lib/client.js";
|
||||
|
||||
/**
|
||||
* Ready to go bot instance.
|
||||
*
|
||||
* @remarks Quetza itself.
|
||||
*/
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
@@ -11,4 +16,4 @@ const client = new Client({
|
||||
]
|
||||
});
|
||||
|
||||
client.login(config.discordToken);
|
||||
client.login(config.application.token);
|
||||
+110
-62
@@ -1,84 +1,132 @@
|
||||
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 { readdirSync } from "fs";
|
||||
|
||||
import config from "../config.js";
|
||||
import { ApplicationStatus, Command, Event, Module } from "./types.js";
|
||||
import config from "$config.js";
|
||||
import { importDir, pathThrough } from "$lib/misc.js";
|
||||
import {
|
||||
ApplicationStatus,
|
||||
Command,
|
||||
CommandBase,
|
||||
Event,
|
||||
EventBase,
|
||||
Module,
|
||||
ModuleBase
|
||||
} from "$lib/types.js";
|
||||
|
||||
const MODULE_ENTRY = "module.js";
|
||||
const MODULE_COMMANDS = "commands";
|
||||
const MODULE_EVENTS = "events";
|
||||
/**
|
||||
* Creating path to {@link Module | Module's} declaration file.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const toModuleDeclaration = pathThrough([config.path.modules], ["module.js"]);
|
||||
|
||||
export default class Client extends DiscordClient {
|
||||
/**
|
||||
* Creating path to {@link Module | Module's} commands directory.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const toModuleCommands = pathThrough([config.path.modules], ["commands"]);
|
||||
|
||||
/**
|
||||
* Creating path to {@link Module | Module's} events directory.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const toModuleEvents = pathThrough([config.path.modules], ["events"]);
|
||||
|
||||
/**
|
||||
* Bot client enchanced with module handling and database access.
|
||||
*
|
||||
* @typeparam T - True if client ready, false otherwise.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default class Client<T extends boolean = boolean> extends DiscordClient<T> {
|
||||
/**
|
||||
* Mapping command name to its {@link Command | instance}.
|
||||
*/
|
||||
public readonly commands = new Collection<string, Command>();
|
||||
|
||||
/**
|
||||
* Mapping event name to its {@link Event | instance}.
|
||||
*/
|
||||
public readonly events = new Collection<string, Event>();
|
||||
|
||||
/**
|
||||
* Mapping module name to its {@link Module | instance}.
|
||||
*/
|
||||
public readonly modules = new Collection<string, Module>();
|
||||
|
||||
public readonly db = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Constructs Quetza Client.
|
||||
*
|
||||
* @param options - Discord.js client options
|
||||
*/
|
||||
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, modulePath)
|
||||
);
|
||||
}
|
||||
readdirSync(config.path.modules).forEach((module) =>
|
||||
this.importModule(
|
||||
toModuleDeclaration(module),
|
||||
toModuleCommands(module),
|
||||
toModuleEvents(module)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private loadModule(data: Module, rootDir: string): void {
|
||||
function importModule(dir: string, importThen: (data: Command & Event) => void) {
|
||||
if (existsSync(dir)) {
|
||||
for (const source of readdirSync(dir)) {
|
||||
const file = path.join(dir, source);
|
||||
/**
|
||||
* Generates client's status by providing Bot User and Loaded Modules info.
|
||||
*
|
||||
* @returns Status of application.
|
||||
* Additional info such as Application ID provided if {@link Client} is ready.
|
||||
*/
|
||||
public generateApplicationStatus(): ApplicationStatus {
|
||||
const modules = Array.from(this.modules.values());
|
||||
|
||||
if (!lstatSync(file).isFile() || !file.endsWith(".js")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
import(pathToFileURL(file).toString()).then(importThen);
|
||||
}
|
||||
}
|
||||
if (!this.isReady()) {
|
||||
return {
|
||||
applicationId: null,
|
||||
tag: null,
|
||||
modules
|
||||
};
|
||||
}
|
||||
|
||||
importModule(path.join(rootDir, MODULE_COMMANDS), (command: Command) => {
|
||||
this.commands.set(command.data.name, command);
|
||||
});
|
||||
|
||||
importModule(path.join(rootDir, MODULE_EVENTS), (event: Event) => {
|
||||
this.on(
|
||||
event.name,
|
||||
async (...eventee: unknown[]) => await event.execute(this, eventee)
|
||||
);
|
||||
});
|
||||
|
||||
this.modules.set(data.name, data);
|
||||
}
|
||||
|
||||
public generateApplicationStatus(): ApplicationStatus<"OK" | "UNAUTHORIZED"> {
|
||||
const modules = Array.from(this.modules.keys());
|
||||
const commands = Array.from(this.commands.keys());
|
||||
const events = Array.from(this.events.keys());
|
||||
|
||||
const status: "OK" | "UNAUTHORIZED" = this.user && this.application ? "OK" : "UNAUTHORIZED";
|
||||
|
||||
return {
|
||||
status,
|
||||
applicationId: this.application?.id,
|
||||
userTag: this.user?.tag,
|
||||
modules,
|
||||
commands,
|
||||
events
|
||||
applicationId: this.application.id,
|
||||
tag: this.user.tag,
|
||||
modules
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports module metadata, commands and events to the client.
|
||||
*
|
||||
* @param declaration - File declaring module information.
|
||||
* @param commands - Directory containing module's commands.
|
||||
* @param events - Directory containing module's events.
|
||||
*/
|
||||
private async importModule(
|
||||
declaration: string,
|
||||
commands: string,
|
||||
events: string
|
||||
): Promise<void> {
|
||||
const module = await import(declaration).then((module: ModuleBase) =>
|
||||
this.modules.ensure(module.name, () => ({ ...module, commands: [], events: [] }))
|
||||
);
|
||||
|
||||
importDir<CommandBase>(commands, (command) => {
|
||||
const saved = this.commands.ensure(command.data.name, () => ({ ...command, module }));
|
||||
|
||||
module.commands.push(saved);
|
||||
});
|
||||
|
||||
importDir<EventBase>(events, (event) => {
|
||||
const saved = this.events.ensure(event.name, () => ({ ...event, module }));
|
||||
|
||||
module.events.push(saved);
|
||||
this.on(event.name, (...eventee: unknown[]) =>
|
||||
saved.execute(this, eventee, module.controller)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Base class for all quetza's errors.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default class QuetzaError extends Error {
|
||||
/**
|
||||
* Default prefix value stands for quetza's base features.
|
||||
*/
|
||||
public static readonly DEFAULT_PREFIX = "QZ";
|
||||
|
||||
/**
|
||||
* Error code is an identifier for specific Error in the same module.
|
||||
*/
|
||||
public readonly code: number;
|
||||
|
||||
/**
|
||||
* Prefix defines module where Error has occured.
|
||||
*
|
||||
* @defaultValue `QuetzaError.DEFAULT_PREFIX`
|
||||
*/
|
||||
public readonly prefix: string;
|
||||
|
||||
/**
|
||||
* Constructs QuetzaError class.
|
||||
*
|
||||
* @param msg - Error message (that shall not be displayed to end user)
|
||||
* @param code - {@link code | Code} unique to error in module
|
||||
* @param prefix - Indentifier of module where error has occured
|
||||
*/
|
||||
public constructor(msg: string, code: number, prefix = QuetzaError.DEFAULT_PREFIX) {
|
||||
super(msg);
|
||||
|
||||
Object.setPrototypeOf(this, QuetzaError.prototype);
|
||||
|
||||
this.code = code;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must create a unique error ID if no collision bugs were written.
|
||||
*
|
||||
* @returns error ID as concatination of {@link prefix} and {@link code}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const error = new QuetzaError("Some error.", 404, "NF");
|
||||
*
|
||||
* error.getErrorId(); // "NF404"
|
||||
* ```
|
||||
*/
|
||||
public getErrorId(): string {
|
||||
return this.prefix + this.code.toString();
|
||||
}
|
||||
}
|
||||
+17
-3
@@ -1,12 +1,26 @@
|
||||
import path from "path";
|
||||
import winston from "winston";
|
||||
|
||||
import config from "$config.js";
|
||||
|
||||
/** Error logging file path. */
|
||||
const errorLog = path.join(config.path.log, "error.log");
|
||||
|
||||
/** Combined logging file path. */
|
||||
const combinedLog = path.join(config.path.log, "combined.log");
|
||||
|
||||
/**
|
||||
* Logger instance. Use it for logging.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
const logger = winston.createLogger({
|
||||
level: "info",
|
||||
levels: winston.config.syslog.levels,
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
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/combined.log" })
|
||||
new winston.transports.File({ filename: errorLog, level: "error" }),
|
||||
new winston.transports.File({ filename: combinedLog })
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Definitions for miscellaneous functions used in this project.
|
||||
*/
|
||||
|
||||
import assert from "assert";
|
||||
import { existsSync, readdirSync } from "fs";
|
||||
import path from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
/**
|
||||
* Clamps value between min and max params.
|
||||
*
|
||||
* @returns Function with defined min and max, expecting a value to clamp.
|
||||
*
|
||||
* @param min - value less than min will resolve to min.
|
||||
* @param max - value greater than max will resolve to max.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function clamp(min: number, max: number) {
|
||||
assert(min <= max);
|
||||
|
||||
return (value: number) => Math.max(min, Math.max(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Path to File URI Scheme String.
|
||||
*
|
||||
* @returns URI.toString() made from joined paths.
|
||||
*
|
||||
* @param paths - Strings that should be joined and converted to URI.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function pathToURI(...paths: string[]): string {
|
||||
return pathToFileURL(path.join(...paths)).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports every module withing provided folder with callback.
|
||||
*
|
||||
* @remarks Will import nothing if directory not exists.
|
||||
*
|
||||
* @typeparam T - Type of whatever you're importing.
|
||||
* If not 'any', expecting every import to have the same semantics.
|
||||
*
|
||||
* @param dir - Directory where importing modules resides.
|
||||
* @param callback - Function to run on every import.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function importDir<T>(dir: string, callback: (module: T) => void) {
|
||||
if (!existsSync(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
readdirSync(dir)
|
||||
.filter((source) => source.endsWith(".js"))
|
||||
.forEach((source) => import(pathToURI(dir, source)).then(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates function that joins paths with predefined origin and target.
|
||||
*
|
||||
* @returns String with following structure: '...origins/...through/...target'.
|
||||
*
|
||||
* @param origins - Paths that will be joined and prepended to 'through'.
|
||||
* @param targets - Paths that will be joined and appended to 'through'.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function pathThrough(
|
||||
origins: string[],
|
||||
targets: string[]
|
||||
): (...through: string[]) => string {
|
||||
return (...through: string[]) => path.join(...origins, ...through, ...targets);
|
||||
}
|
||||
+76
-15
@@ -1,28 +1,89 @@
|
||||
import { ApplicationCommandData, CommandInteraction } from "discord.js";
|
||||
/**
|
||||
* Quetza's base type declarations.
|
||||
*
|
||||
* @see Architecture documentation for more understanding of its purpose.
|
||||
*/
|
||||
import { ApplicationCommandData, CommandInteraction, If } from "discord.js";
|
||||
|
||||
import Client from "./client.js";
|
||||
import Client from "$lib/client.js";
|
||||
|
||||
export interface Module {
|
||||
/**
|
||||
* Quetza's module declaration.
|
||||
*/
|
||||
export interface ModuleBase {
|
||||
/** Name of a module. */
|
||||
name: string;
|
||||
description?: string;
|
||||
|
||||
/** Description of a module. */
|
||||
description: string;
|
||||
|
||||
/** Module controller. */
|
||||
controller?: unknown;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
/**
|
||||
* Complete {@link ModuleBase | Module} declaration with commands and events.
|
||||
*/
|
||||
export interface Module extends ModuleBase {
|
||||
/** Module's commands. */
|
||||
commands: CommandBase[];
|
||||
|
||||
/** Module's events. */
|
||||
events: EventBase[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quetza's command interaction declaration.
|
||||
*/
|
||||
export interface CommandBase {
|
||||
/** Command metadata that will be passed to the Discord API. */
|
||||
data: ApplicationCommandData;
|
||||
execute: (client: Client, interaction: CommandInteraction) => Promise<void>;
|
||||
|
||||
/** Function that will be executed upon invoking this interaction. */
|
||||
execute: (
|
||||
client: Client,
|
||||
interaction: CommandInteraction,
|
||||
controller?: unknown
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
/**
|
||||
* Command with module included.
|
||||
*/
|
||||
export interface Command extends CommandBase {
|
||||
/** Module containing this Command. */
|
||||
module: Module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quetza's event declaration.
|
||||
*/
|
||||
export interface EventBase {
|
||||
/** Name of an event to listen. */
|
||||
name: string;
|
||||
execute: (client: Client, eventee: unknown[]) => Promise<void>;
|
||||
|
||||
/** Function that will be executed upon event emitting. */
|
||||
execute: (client: Client, eventee: unknown[], controller?: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ApplicationStatus<T extends "OK" | "UNAUTHORIZED"> {
|
||||
status: T;
|
||||
applicationId: T extends "OK" ? string : undefined;
|
||||
userTag: T extends "OK" ? string : undefined;
|
||||
modules: string[];
|
||||
commands: string[];
|
||||
events: string[];
|
||||
/**
|
||||
* {@link EventBase | Event} with {@link Module} included.
|
||||
*/
|
||||
export interface Event extends EventBase {
|
||||
/** Module containing this Event. */
|
||||
module: ModuleBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application status provides information that may be useful at initialization.
|
||||
*/
|
||||
export interface ApplicationStatus<Ready extends boolean = boolean> {
|
||||
/** Discord's ID of an application. */
|
||||
applicationId: If<Ready, string>;
|
||||
|
||||
/** Tag of a bot user. */
|
||||
tag: If<Ready, string>;
|
||||
|
||||
/** List of each module imported. */
|
||||
modules: Module[];
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
logger.info("Ready.", client.generateApplicationStatus());
|
||||
|
||||
client.user.setActivity(config.activityStatus);
|
||||
}
|
||||
|
||||
const name = Events.ClientReady;
|
||||
|
||||
export { execute, name };
|
||||
@@ -1,36 +0,0 @@
|
||||
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 };
|
||||
@@ -1,66 +0,0 @@
|
||||
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,44 +0,0 @@
|
||||
import { User } from "discord.js";
|
||||
|
||||
import { dlpExtractorsValues } from "./fetch/extractors";
|
||||
|
||||
export interface LostTrack {
|
||||
query: string;
|
||||
reason: number;
|
||||
}
|
||||
|
||||
export interface TrackBase {
|
||||
url: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface Track extends TrackBase {
|
||||
url: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
requester: User;
|
||||
}
|
||||
|
||||
export type DlpInfo = [TrackBase[], LostTrack[]];
|
||||
|
||||
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
|
||||
|
||||
export type dlpExtractorKey = (typeof dlpExtractorsValues)[number];
|
||||
|
||||
export interface DlpDump {
|
||||
id: string;
|
||||
original_url: string;
|
||||
webpage_url?: string;
|
||||
title?: string;
|
||||
duration?: number;
|
||||
thumbnail?: string;
|
||||
extractor_key: dlpExtractorKey;
|
||||
}
|
||||
|
||||
export interface dlpStreamOptions {
|
||||
seek?: number;
|
||||
ffmpeg?: string[];
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"$config": ["../config.js"],
|
||||
"$config.js": ["../config.js"],
|
||||
"$lib/*": ["./lib/*"]
|
||||
},
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
|
||||
"esModuleInterop": true,
|
||||
|
||||
Reference in new issue
Block a user