Documentation of Music & Core (almost done)

This commit is contained in:
deytenit committed 2024-03-02 02:04:00 +03:00
1 parent 3144dd69b7
commit afc6fce555
22 files changed
+886 -459

No files matched your search

+10
View File
@@ -1,3 +1,13 @@
/**
* /modules
*
* Replies module listing as well as description of each.
*
* Just for the information I guess...
*
* @packageDocumentation
*/
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "$lib/client.js";
+10
View File
@@ -1,3 +1,13 @@
/**
* /ping
*
* Replies websocket ping (ping between bot and discord) and real ping (ping to fetch + process).
*
* @remarks Probably should double their value as two ways, but nvm.
*
* @packageDocumentation
*/
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import Client from "$lib/client.js";
-12
View File
@@ -1,12 +0,0 @@
{
"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"
}
}
+9 -9
View File
@@ -1,16 +1,17 @@
/**
* InteractionCreate
*
* Resolves creation of an interaction by users.
*
* @remark Only {@link ChatInputCommand} currently being resolved.
*
* @packageDocumentation
*/
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;
@@ -36,7 +37,6 @@ async function execute(client: Client, eventee: [Interaction]): Promise<void> {
}
}
/** Event name. */
const name = Events.InteractionCreate;
export { execute, name };
+11 -9
View File
@@ -1,3 +1,14 @@
/**
* Ready
*
* Will execute upon emitting Client's Ready event.
*
* @remarks Will push slash commands, set activity status,
* and log bot's initials.
*
* @packageDocumentation
*/
import { generateDependencyReport } from "@discordjs/voice";
import { Events } from "discord.js";
@@ -5,14 +16,6 @@ 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);
@@ -42,7 +45,6 @@ async function execute(client: Client<true>): Promise<void> {
client.user.setActivity(config.application.activity);
}
/** Event name. */
const name = Events.ClientReady;
export { execute, name };
+16 -2
View File
@@ -1,6 +1,20 @@
/** Module's name. */
/**
* Core Quetza Module definition.
*
* This module provides default capabilities.
* - Comands are being resolved here at InteractionCreated event.
* - Aplication status being generated as well as commands pushed to Discord at Ready event.
*
* Also this module provides basic commands:
* - ping
* - modules
*
* There a lot of speaking because it's as important as $lib.
*
* @packageDocumentation
*/
const name = "core";
/** Module's description */
const description = "Quetza's core functionality";
export { description, name };
+11 -10
View File
@@ -1,3 +1,12 @@
/**
* voiceStateUpdate
*
* Monitors voice states of attended guilds.
* If Quetza happens to be alone in the Voice Channel - destroys guilds player and disconnects.
*
* @packageDocumentation
*/
import { BaseGuildVoiceChannel, User, VoiceState } from "discord.js";
import Client from "$lib/client.js";
@@ -16,6 +25,8 @@ const TIME_TILL_DISCONNECT = 30_000;
*
* @param user - User that might be in one of a channels.
* @param channels - List of channels that might have user as it member.
*
* @internal
*/
function resolveVoiceChannel(
user: User,
@@ -26,15 +37,6 @@ function resolveVoiceChannel(
);
}
/**
* 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],
@@ -59,7 +61,6 @@ async function execute(
}
}
/** Event name. */
const name = "voiceStateUpdate";
export { execute, name };
+6
View File
@@ -1,3 +1,9 @@
/**
* @see ../../../src/lib/error.ts
*
* @packageDocumentation
*/
import QuetzaError from "$lib/error.js";
export default class MusicError extends QuetzaError {
+11
View File
@@ -1,5 +1,10 @@
import { DlpDump, dlpExtractorKey, DlpInfo } from "$mlib/types.js";
/**
* Avaliable extractors.
*
* @internal
*/
export const dlpExtractorsValues = ["Youtube", "Soundcloud"] as const;
/**
@@ -8,6 +13,8 @@ export const dlpExtractorsValues = ["Youtube", "Soundcloud"] as const;
* For example:
*
* @see the {@link youtubeExtractor | youtubeExtractor(...) } function
*
* @internal
*/
type dlpExtractor = (arg0: DlpDump, arg1: (arg0: string) => Promise<DlpInfo>) => Promise<DlpInfo>;
@@ -19,6 +26,8 @@ type dlpExtractor = (arg0: DlpDump, arg1: (arg0: string) => Promise<DlpInfo>) =>
* @param dump - Parsed dlp output
* @param dlpInfo - Function to call if any other information needed
* (will recurcively call next extractor after its run)
*
* @public
*/
const youtubeExtractor: dlpExtractor = async (dump, dlpInfo) => {
if (dump.id && dump.title && dump.duration) {
@@ -55,6 +64,8 @@ const soundcloudExtractor: dlpExtractor = async (dump, dlpInfo) => {
/**
* DlpDump extractor_key values mapped to corresponding extractor.
*
* @public
*/
const extractors: Record<dlpExtractorKey, dlpExtractor> = {
Youtube: youtubeExtractor,
+14
View File
@@ -16,16 +16,22 @@ const execFileAsync = promisify(execFile);
/**
* Maximum length of dlp search stack.
*
* @internal
*/
const DLPINFO_DEPTH = 3;
/**
* Path to the Dlp binary.
*
* @internal
*/
const DLP_BINARY = path.join(config.path.binaries, "/yt-dlp");
/**
* Base arguments for Ffmpeg transcoder.
*
* @internal
*/
const FFMPEG_BASE_ARGS = [
"-analyzeduration",
@@ -48,6 +54,8 @@ const FFMPEG_BASE_ARGS = [
* @returns true if given string is a valid URL, false otherwise
*
* @param str - Any string
*
* @internal
*/
function isURL(str: string): boolean {
return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/.test(
@@ -64,6 +72,8 @@ function isURL(str: string): boolean {
*
* @param query - Dlp search request or compatible URL
* @param depth - Recursion depth (force stops at 0)
*
* @internal
*/
async function dlpInfoRecursive(query: string, depth = DLPINFO_DEPTH): Promise<DlpInfo> {
const error = (message: string, reason: number, ...meta: unknown[]): DlpInfo => {
@@ -119,6 +129,8 @@ async function dlpInfoRecursive(query: string, depth = DLPINFO_DEPTH): Promise<D
* @returns Metadata to a youtube video that related to query by title or URL of findings
*
* @param query - Dlp search request or compatible URL
*
* @public
*/
export async function dlpInfo(query: string): Promise<DlpInfo> {
return await dlpInfoRecursive(isURL(query) ? query : "ytsearch1:" + query);
@@ -131,6 +143,8 @@ export async function dlpInfo(query: string): Promise<DlpInfo> {
*
* @param url - Dlp compatible URL to the requested resource
* @param options - Transcoders and dlp options
*
* @public
*/
export function dlpStream(url: string, options?: dlpStreamOptions): Promise<Readable> {
let ffmpegArgs = FFMPEG_BASE_ARGS;
+58
View File
@@ -1,8 +1,21 @@
/**
* Filter entry.
*
* @internal
*/
type filterValue = {
/** Public well formatted name of the filter. */
name: string;
/** FFmpeg argument of this filter. */
value: string;
};
/**
* Record mapping id of filter with its {@link filterValue}.
*
* @internal
*/
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" },
@@ -14,17 +27,47 @@ const FILTERS: Record<string, filterValue> = {
}
} as const;
/**
* Type build of {@link FILTERS} keys.
*
* @internal
*/
type filterPreset = keyof typeof FILTERS;
/**
* {@link ./player#Player | Player's } filters options.
*
* Resolves FFMpeg transcoder arguments that tweak playback stream.
*
* @public
*/
export default class Filter {
/**
* Filter list
*/
public static readonly FILTERS = FILTERS;
/**
* Active filters. Mock of {@link FILTERS} object.
*/
private switches_ = new Map<filterPreset, filterValue>();
/**
* Converts active {@link filterValue | filterValues} to array.
*
* @returns Array of active filter values.
*/
public toggled(): filterValue[] {
return [...this.switches_.values()];
}
/**
* Toggles filter specified. Clears them all if no arguments provided.
*
* @returns True if filter was toggled, False otherwise.
*
* @param query - filter id to toggle. Undefined to clear all.
*/
public toggle(query?: filterPreset): boolean {
if (!query) {
this.switches_.clear();
@@ -39,14 +82,29 @@ export default class Filter {
return false;
}
/**
* Calculates of active filter map.
*
* @returns Size of active filter map.
*/
public size(): number {
return this.switches_.size;
}
/**
* Checks if there are any active filter.
*
* @returns size === 0 ? True : False.
*/
public empty(): boolean {
return this.size() === 0;
}
/**
* Converts active filters to FFmpeg compatable argument.
*
* @returns FFmpeg compatable argument representing active filters.
*/
public toString(): string {
return this.toggled()
.map((value) => value.value)
+74 -1
View File
@@ -2,8 +2,26 @@ import { AudioResource } from "@discordjs/voice";
import { LoopOption, Track } from "./types.js";
/**
* Length of playback status bar in emoji symbols.
*
* @intenal
*/
const STATUSBAR_LENGTH = 15;
/**
* Generates pretty playback status bar out of {@link AudioResource}.
*
* @remarks Takes duration and current playback duration to 'draw' bar.
*
* @returns String representing generated status bar.
*
* @param resource - AudioResource with {@link Track} metadata, so duration and playback duration is available.
*
* @privateRemarks I don't know why resource can be undefined - I forgor.
*
* @public
*/
export function statusBarGenerator(resource?: AudioResource<Track>): string {
const duration = resource?.metadata.duration || 1;
const playback = Math.min((resource?.playbackDuration || 0) / 1000, duration);
@@ -15,6 +33,20 @@ export function statusBarGenerator(resource?: AudioResource<Track>): string {
return `[${filled.join("")}${empty.join("")}] ${toISOTime(playback)}/${toISOTime(duration)}`;
}
/**
* Converts seconds to ISO time string.
*
* @returns String representing given seconds in ISO format.
*
* @param amount - Seconds to convert.
*
* @example
* ```
* toISOTime(1337228); // "371:27:08"
* ```
*
* @public
*/
export function toISOTime(amount: number): string {
const h = Math.floor(amount / 3600);
const m = Math.floor((amount % 3600) / 60);
@@ -27,19 +59,60 @@ export function toISOTime(amount: number): string {
return hDisplay + mDisplay + sDisplay;
}
/**
* Asynchronous timeout.
*
* @returns Promise that will be settled after timeout pass.
*
* @param amount - Milliseconds to wait before settle.
*
* @public
*/
export async function asleep(amount: number): Promise<unknown> {
return new Promise((r) => setTimeout(r, amount));
}
/**
* Converts volume value to emoji representation.
*
* @remarks In span of 0 ... 100 - emoji from muted to loud.
*
* @returns String with single emoji representing specified volume.
*
* @param amount - volume to convert.
*
* @public
*/
export function volumeMoji(amount: number): string {
return amount > 100 ? "🔊" : amount > 50 ? "🔉" : amount > 0 ? "🔈" : "🔇";
}
/**
* Converts {@link LoopOption} to emoji representation..
*
* @returns String with single emoji representing specified loop option.
*
* @param loop - Loop option that will be converted.
*
* @public
*/
export function loopMoji(loop: LoopOption): string {
return loop === "AUTO" ? "🔀" : loop === "LOOP" ? "🔁" : loop === "NONE" ? "⤵️" : "🔂";
}
export function largestCommonSequence(s1: string, s2: string) {
/**
* Finds larges common sequence (LCS) of characters withing provided strings.
*
* @remarks Used to fuzzy search query title of Track.
*
* @returns Length of the LCS found.
*
* @param s1 - First string.
* @param s2 - Second string.
*
* @public
*/
export function largestCommonSequence(s1: string, s2: string): number {
s1 = s1.toLowerCase().slice(0, 100);
s2 = s2.toLowerCase().slice(0, 100);
+30
View File
@@ -4,9 +4,26 @@ import logger from "$lib/logger.js";
import Player from "./player.js";
/**
* Module's controller.
*
* Binds guilds to player.
* Creates and destroys them.
*/
export default class Music {
/**
* GuildId to {@link Player} mapping.
*/
private players_ = new Collection<string, Player>();
/**
* Retrieves {@link Player} by {@link Guild}.
*
* @returns Player if such mapping exists or undefined otherwise.
*
* @param guild - Discord guild that might has a Player binded.
* @param channel - Text channel that should replace Player's one.
*/
public get(guild: Guild, channel?: GuildTextBasedChannel): Player | undefined {
const player = this.players_.get(guild.id);
@@ -19,6 +36,14 @@ export default class Music {
}
}
/**
* Creates {@link Player} and maps it to GuildId.
*
* @returns Newly created Player.
*
* @param guild - {@link Guild} that created Player will be binded to.
* @param channel - Text channel that used by Player constructor.
*/
public set(guild: Guild, channel: GuildTextBasedChannel): Player {
const player = new Player(guild, this, channel);
@@ -29,6 +54,11 @@ export default class Music {
return player;
}
/**
* Deleted {@link Player} and its mapping by GuildId.
*
* @param guildId - Id of {@link Guild} that must be removed from mapping.
*/
public delete(guildId: string): void {
logger.info("Player was deleted.", { guildId });
+1 -1
View File
@@ -219,7 +219,7 @@ export default class Player {
}
/**
* Plays current {@link queue} track from position specified.
* Plays current {@link Queue} track from position specified.
*
* @param seek - Amount of seconds to start playback from
*/
+64 -2
View File
@@ -1,16 +1,32 @@
import { LoopOption, Track } from "./types.js";
/**
* Class of {@link ./player#Player | Player's} queue.
*
* @public
*/
export default class Queue {
/**
* Array of {@link Track | Tracks} - content of queue.
*/
private tracks_: Track[] = [];
public get tracks(): Track[] {
return this.tracks_;
}
/**
* Position of current playing track.
*/
private position_ = 0;
public get position(): number {
return this.position_;
}
/**
* Defines behavior of queue.
*
* @see ./types.ts for more info about values.
*/
private loop_: LoopOption = "NONE";
public get loop(): LoopOption {
return this.loop_;
@@ -19,11 +35,21 @@ export default class Queue {
this.loop_ = x;
}
/**
* Queue overall duration in seconds.
*/
private duration_ = 0;
public get duration(): number {
return this.duration_;
}
/**
* Skips current track.
*
* @remarks Behavior depends on loop option.
*
* @returns next {@link Track} or undefined if end was reached.
*/
public next(): Track | undefined {
if (this.loop_ === "SONG") {
return this.current();
@@ -46,6 +72,9 @@ export default class Queue {
}
}
/**
* Shuffles queue by using simple Math.random.
*/
public shuffle(): void {
let currentIndex = this.tracks_.length;
@@ -62,15 +91,23 @@ export default class Queue {
this.position_ = 0;
}
/**
* Inserts tracks from provided array at specific index.
*
* @returns first {@link Track} of inserted Tracks.
*
* @param tracks - List of Tracks to insert to the queue.
* @param index - Position at which insertion will be occurred.
*/
public push(tracks: Track[], index?: number): Track {
for (const track of tracks) {
this.duration_ += track.duration;
}
if (index === undefined || index >= this.tracks_.length) {
if (!index || index >= this.tracks_.length) {
this.tracks_ = this.tracks_.concat(tracks);
return this.tracks_[this.tracks_.length - 1];
return this.tracks_[this.tracks_.length - tracks.length];
}
this.tracks_.splice(Math.max(0, index), 0, ...tracks);
@@ -81,6 +118,13 @@ export default class Queue {
return this.tracks_[Math.max(0, index)];
}
/**
* Removes {@link Track} from the queue at position specified.
*
* @returns Track that was deleted or undefined if not.
*
* @param query - Index of element that must be deleted.
*/
public pop(query: number): Track | undefined {
if (query >= 0 && query < this.tracks_.length) {
const track = this.tracks_[query];
@@ -94,6 +138,11 @@ export default class Queue {
return undefined;
}
/**
* Assigns position new value.
*
* @returns {@link Track} at position specified or undefined if index out of bounds.
*/
public jump(query: number): Track | undefined {
if (query >= 0 && query < this.tracks_.length) {
const track = this.tracks_[query];
@@ -104,14 +153,27 @@ export default class Queue {
return undefined;
}
/**
* Returns current {@link Track} (at position).
*
* @returns Track at internal position.
*/
public current(): Track {
return this.tracks_[this.position_];
}
/**
* Checks if queue is empty.
*
* @returns True is queue is empty, False otherwise.
*/
public empty(): boolean {
return this.tracks_.length === 0;
}
/**
* Clears entire queue and sets position to 0.
*/
public clear(): void {
this.tracks_ = [];
this.position_ = 0;
+24 -6
View File
@@ -4,6 +4,8 @@ import { dlpExtractorsValues } from "./fetch/extractors.js";
/**
* Track information fetched by Dlp.
*
* @public
*/
export interface TrackBase {
/** URL of track. */
@@ -21,6 +23,8 @@ export interface TrackBase {
/**
* Track information with {@link User} that requested it.
*
* @public
*/
export interface Track extends TrackBase {
/** User requested this track. */
@@ -31,21 +35,33 @@ export interface Track extends TrackBase {
* Player loop options.
*
* @remarks LOOP - Looping queue;
* SONG - Looping single song;
* NONE - Stops after queue end; (default)
* AUTO - Random order;
* SONG - Looping single song;
* NONE - Stops after queue end; (default)
* AUTO - Random order;
*
* @public
*/
export type LoopOption = "LOOP" | "SONG" | "NONE" | "AUTO";
/** Dlp fetched track list. */
/**
* Dlp fetched track list.
*
* @public
*/
export type DlpInfo = TrackBase[];
/** Dlp supported extractors as types. */
/**
* Dlp supported extractors as types.
*
* @public
*/
export type dlpExtractorKey = (typeof dlpExtractorsValues)[number];
/**
* Actual Dlp dump that later translates to {@link DlpInfo}.
* */
*
* @public
*/
export interface DlpDump {
/** Id of resource. */
id: string;
@@ -71,6 +87,8 @@ export interface DlpDump {
/**
* Options that shall be passed to DlpStream function.
*
* @public
*/
export interface dlpStreamOptions {
/** Amount of seconds to start playback from. */
+8 -3
View File
@@ -1,11 +1,16 @@
/**
* Music module definition.
*
* Module to play media content from various sites.
* @see ./lib/fetch/extractors.ts for site extractors.
*
* @packageDocumentation
*/
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 };
+9 -10
View File
@@ -1,6 +1,6 @@
{
"name": "quetza",
"version": "2023.1.1-pre",
"version": "2024.3.0",
"description": "Awesome Discord bot that performs various duties",
"type": "module",
"main": "./dist/src/index.js",
@@ -33,22 +33,21 @@
"dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.16.1",
"@prisma/client": "5.7.0",
"discord.js": "^14.14.1",
"prism-media": "^1.3.5",
"tweetnacl": "^1.0.3",
"winston": "^3.11.0"
},
"devDependencies": {
"@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"eslint": "^8.55.0",
"@types/node": "^20.11.24",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"mocha": "^10.2.0",
"prettier": "^3.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-simple-import-sort": "^12.0.0",
"mocha": "^10.3.0",
"prettier": "^3.2.5",
"tsc-alias": "^1.8.8",
"typescript": "^5.3.3"
}
+493 -384
View File
File diff suppressed because it is too large. Load diff
+2 -5
View File
@@ -26,13 +26,10 @@ export default class QuetzaError extends Error {
*
* @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
* @param prefix - Identifier of module where error has occurred
*/
public constructor(msg: string, code: number, prefix = QuetzaError.DEFAULT_PREFIX) {
super(msg);
Object.setPrototypeOf(this, QuetzaError.prototype);
this.code = code;
this.prefix = prefix;
}
@@ -40,7 +37,7 @@ export default class QuetzaError extends Error {
/**
* Must create a unique error ID if no collision bugs were written.
*
* @returns error ID as concatination of {@link prefix} and {@link code}
* @returns error ID as concatenation of {@link prefix} and {@link code}
*
* @example
* ```ts
+8 -2
View File
@@ -3,10 +3,16 @@ import winston from "winston";
import config from "$config.js";
/** Error logging file path. */
/** Error logging file path.
*
* @internal
*/
const errorLog = path.join(config.path.log, "error.log");
/** Combined logging file path. */
/** Combined logging file path.
*
* @internal
*/
const combinedLog = path.join(config.path.log, "combined.log");
/**
+17 -3
View File
@@ -9,6 +9,8 @@ import Client from "$lib/client.js";
/**
* Quetza's module declaration.
*
* @public
*/
export interface ModuleBase {
/** Name of a module. */
@@ -22,7 +24,9 @@ export interface ModuleBase {
}
/**
* Complete {@link ModuleBase | Module} declaration with commands and events.
* Complete {@link ModuleBase} declaration with commands and events.
*
* @public
*/
export interface Module extends ModuleBase {
/** Module's commands. */
@@ -34,6 +38,8 @@ export interface Module extends ModuleBase {
/**
* Quetza's command interaction declaration.
*
* @public
*/
export interface CommandBase {
/** Command metadata that will be passed to the Discord API. */
@@ -48,7 +54,9 @@ export interface CommandBase {
}
/**
* Command with module included.
* {@link ModuleBase} with {@link Module} included.
*
* @public
*/
export interface Command extends CommandBase {
/** Module containing this Command. */
@@ -57,6 +65,8 @@ export interface Command extends CommandBase {
/**
* Quetza's event declaration.
*
* @public
*/
export interface EventBase {
/** Name of an event to listen. */
@@ -67,7 +77,9 @@ export interface EventBase {
}
/**
* {@link EventBase | Event} with {@link Module} included.
* {@link EventBase} with {@link Module} included.
*
* @public
*/
export interface Event extends EventBase {
/** Module containing this Event. */
@@ -76,6 +88,8 @@ export interface Event extends EventBase {
/**
* Application status provides information that may be useful at initialization.
*
* @public
*/
export interface ApplicationStatus<Ready extends boolean = boolean> {
/** Discord's ID of an application. */