chore: merge branch dev to main #200

Merged
Anri merged 19 commits from dev into main 2024-11-02 17:36:38 +01:00
27 changed files with 729 additions and 509 deletions

2
.gitignore vendored
View file

@ -11,7 +11,7 @@ docker-compose.yml
dist/ dist/
# Databse # Databse
*.sqlite3 *.sqlite3*
# Debug file # Debug file
src/events/player/debug.ts src/events/player/debug.ts

609
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,21 +21,19 @@
"dependencies": { "dependencies": {
"@discord-player/extractor": "^4.5.1", "@discord-player/extractor": "^4.5.1",
"@discordjs/rest": "^2.4.0", "@discordjs/rest": "^2.4.0",
"@types/sqlite3": "^3.1.11",
"@types/uuid": "^10.0.0",
"discord-player": "^6.7.1", "discord-player": "^6.7.1",
"discord-player-youtubei": "^1.3.2", "discord-player-youtubei": "^1.3.4",
"discord.js": "^14.16.3", "discord.js": "^14.16.3",
"mediaplex": "^0.0.9", "mediaplex": "^0.0.9",
"moment-timezone": "^0.5.46", "moment-timezone": "^0.5.46",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"uuid": "^10.0.0" "uuid": "^11.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "~29.5.13", "@types/jest": "~29.5.14",
"@typescript-eslint/eslint-plugin": "~8.8.1", "@typescript-eslint/eslint-plugin": "~8.12.2",
"@typescript-eslint/parser": "~8.8.1", "@typescript-eslint/parser": "~8.12.2",
"dotenv": "~16.4.5", "dotenv": "~16.4.5",
"jest": "~29.7.0", "jest": "~29.7.0",
"prettier-eslint": "~16.3.0", "prettier-eslint": "~16.3.0",

View file

@ -1,8 +1,20 @@
import { SlashCommandBuilder } from "@discordjs/builders"; import { SlashCommandBuilder } from "@discordjs/builders";
import { ChatInputCommandInteraction, Client, Colors, EmbedBuilder } from "discord.js"; import {
ApplicationCommandOptionType,
ChatInputCommandInteraction,
Client,
Colors,
EmbedBuilder,
} from "discord.js";
import "../../modules/string"; import "../../modules/string";
import { getLocale, getLocalizations } from "../../utils/locales"; import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc"; import { getFilename } from "../../utils/misc";
import {
goodDescription,
goodName,
NameNotLocalized,
SubnameNotLocalized,
} from "../../utils/commands/help";
export default { export default {
scope: () => [], scope: () => [],
@ -47,16 +59,35 @@ export default {
}[] = []; }[] = [];
// Load all the command per categories // Load all the command per categories
// TODO: Check if the command exist in the context (guild)
// TODO: List subcommands too
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/47
client.commands.categories.forEach((commands_name, category) => { client.commands.categories.forEach((commands_name, category) => {
const commands = commands_name.reduce((data, command_name) => { // Check if the command exist in the context (guild)
return data + `\`/${command_name}\`, `; commands_name = commands_name.filter((command) => {
}, ""); const scope = client.commands.list.get(command)?.scope();
return scope!.length === 0 || scope?.find((v) => v === interaction.guildId) !== undefined;
});
// Add subcommands
const all_commands: string[] = [];
commands_name.forEach((command) => {
const data = client.commands.list.get(command)?.data;
const name = goodName(data!, interaction.locale);
all_commands.push(name);
data
?.toJSON()
.options?.filter((option) => option.type === ApplicationCommandOptionType.Subcommand)
.forEach((subcommand) =>
all_commands.push(name + " " + goodName(subcommand, interaction.locale)),
);
});
const commands = all_commands.reduce(
(data, command_name) => data + `\`/${command_name}\`, `,
"",
);
fields.push({ fields.push({
name: category.capitalize() + ` (${commands_name.length})`, name: category.capitalize() + ` (${all_commands.length})`,
value: commands.slice(0, -2), value: commands.slice(0, -2),
}); });
}); });
@ -73,30 +104,49 @@ export default {
}); });
} }
// If a command is specified const error = `${loc.get("c_help3")} \`${desired_command}\``;
// TODO: Check if the command exist in the context (guild)
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/187 const [possible_command, possible_subcommand] = desired_command.split(" ");
const command = client.commands.list.get(desired_command);
const command = NameNotLocalized(client, possible_command);
if (!command) { if (!command) {
// Command don't exist return interaction.reply({ content: error, ephemeral: true });
return interaction.reply({
content: `${loc.get("c_help3")} \`${desired_command}\``,
ephemeral: true,
});
} }
const scope = client.commands.list.get(command.name)?.scope();
if (scope!.length > 0 && scope?.find((id) => id === interaction.guildId) === undefined) {
// Command not available for the current guild
return interaction.reply({ content: error, ephemeral: true });
}
let subcommand = undefined;
if (possible_subcommand) {
subcommand = SubnameNotLocalized(command, possible_subcommand);
} else {
subcommand = null;
}
if (!command || subcommand === undefined) {
// Sub/Command don't exist
return interaction.reply({ content: error, ephemeral: true });
}
// Loads the data according to the user's locals
const requestedName =
goodName(command, interaction.locale) +
(subcommand !== null ? " " + goodName(subcommand, interaction.locale) : "");
const requestedDesc = goodDescription(
subcommand !== null ? subcommand : command,
interaction.locale,
);
// Send information about the command // Send information about the command
return interaction.reply({ return interaction.reply({
embeds: [ embeds: [
new EmbedBuilder() new EmbedBuilder()
.setColor(Colors.Blurple) .setColor(Colors.Blurple)
.setTitle("`/" + command.data.name + "`") .setTitle("`/" + requestedName + "`")
.setDescription( .setDescription(requestedDesc),
// Loads the description
// according to the user's locals
command.data.description_localizations?.[interaction.locale] ??
command.data.description,
),
], ],
}); });
}, },

View file

@ -1,8 +1,9 @@
import { SlashCommandBuilder } from "@discordjs/builders"; import { SlashCommandBuilder } from "@discordjs/builders";
import { useMainPlayer, useQueue } from "discord-player"; import { useMainPlayer, useQueue } from "discord-player";
import { ChatInputCommandInteraction, Client, EmbedBuilder } from "discord.js"; import { ChatInputCommandInteraction, Client, EmbedBuilder, Message } from "discord.js";
import { getLocale, getLocalizations } from "../../utils/locales"; import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc"; import { getFilename } from "../../utils/misc";
import { discord_limit_message } from "../../utils/constants";
export default { export default {
scope: () => [], scope: () => [],
@ -57,7 +58,7 @@ export default {
), ),
) )
// Synced // Synced start
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand subcommand
.setName(loc_default.get(`c_${filename}_sub3_name`)!.toLowerCase()) .setName(loc_default.get(`c_${filename}_sub3_name`)!.toLowerCase())
@ -65,6 +66,15 @@ export default {
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub3_name`, true)) .setNameLocalizations(getLocalizations(client, `c_${filename}_sub3_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub3_desc`)), .setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub3_desc`)),
) )
// Synced stop
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub4_name`)!.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_sub4_desc`)!)
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub4_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub4_desc`)),
)
); );
}, },
@ -123,30 +133,69 @@ export default {
} }
// Load lyrics // Load lyrics
const syncedLyrics = queue.syncedLyrics(data[0]); if (queue.syncedLyricsMemory !== undefined) {
return await interaction.followUp(loc.get("c_lyrics9"));
}
const syncedLyrics = queue.syncedLyrics(data[0]);
queue.syncedLyricsMemory = syncedLyrics;
let message: Message;
syncedLyrics?.onChange(async (lyrics) => { syncedLyrics?.onChange(async (lyrics) => {
const content = `[${data[0].trackName}]: ${lyrics}`;
if (interaction.channel?.isSendable()) { if (interaction.channel?.isSendable()) {
await interaction.channel?.send({ if (message) {
content, const payload = message.cleanContent + "\n" + lyrics;
}); if (payload.length < discord_limit_message) {
message.edit(payload);
return;
}
}
message = await interaction.channel?.send(
(message ? loc.get("c_lyrics6") + " " : "") +
`${data[0].artistName} : **${data[0].trackName}**\n\n` +
lyrics,
);
} else { } else {
await interaction.followUp({ await interaction.followUp(loc.get("c_lyrics5"));
content,
});
} }
}); });
// Live update // Live update
syncedLyrics.subscribe(); syncedLyrics.subscribe();
syncedLyrics.onUnsubscribe(() => {
queue.syncedLyricsMemory = undefined;
});
return await interaction.followUp({ return await interaction.followUp({
content: `🎤 | ${loc.get("c_lyrics4")}`, content: `🎤 | ${loc.get("c_lyrics4")}`,
ephemeral: true, ephemeral: true,
}); });
} }
if (
interaction.options.getSubcommand() ===
loc_default?.get(`c_${filename}_sub4_name`)?.toLowerCase()
) {
if (queue === null) {
return await interaction.followUp(`❌ | ${loc.get("c_lyrics1")}`);
}
if (data === null || !data[0] || !data[0].syncedLyrics) {
return await interaction.followUp(
`❌ | ${loc.get("c_lyrics3")} \`${queue.currentTrack?.cleanTitle}\``,
);
}
// Load lyrics
if (queue.syncedLyricsMemory !== undefined && queue.syncedLyricsMemory.isSubscribed()) {
queue.syncedLyricsMemory.unsubscribe();
return await interaction.followUp(loc.get("c_lyrics7"));
}
return await interaction.followUp(loc.get("c_lyrics8"));
}
if (data && data.length > 0 && data[0].plainLyrics !== null) { if (data && data.length > 0 && data[0].plainLyrics !== null) {
const title = data[0]; const title = data[0];
const limit_desc = 4096; const limit_desc = 4096;

View file

@ -9,6 +9,11 @@ import {
import { getLocale, getLocalizations } from "../../utils/locales"; import { getLocale, getLocalizations } from "../../utils/locales";
import { Metadata } from "../../utils/metadata"; import { Metadata } from "../../utils/metadata";
import { getFilename } from "../../utils/misc"; import { getFilename } from "../../utils/misc";
import {
discord_limit_autocompletion_list_length,
discord_limit_autocompletion_value_length,
} from "../../utils/constants";
import { timeToString } from "../../utils/time";
export default { export default {
scope: () => [], scope: () => [],
@ -175,12 +180,29 @@ export default {
queue.node.play(); queue.node.play();
} }
// TODO: When added to an existing queue (size of queue > 0): const positionEstimation = () => {
// - Add position in queue const pos = queue.node.getTrackPosition(result.tracks[0]) + 1;
// - Add estimated time until playing
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/184 if (pos === 0) {
return loc.get("c_play_sub2_name");
}
const estimation = timeToString(
[queue.currentTrack, ...queue.tracks.toArray()]
.filter((t) => t !== null)
.slice(0, pos)
.reduce((total, t) => {
if (total === 0) {
return queue.dispatcher ? t.durationMS - queue.dispatcher.streamTime : t.durationMS;
}
return total + t.durationMS;
}, 0),
);
return `${loc.get("c_play10")} ${pos} (${loc.get("c_play11")}${estimation})`;
};
return await interaction.followUp({ return await interaction.followUp({
content: `⏱️ | \`${title}\` ${loc.get("c_play5")}.`, content: `⏱️ | \`${title}\` ${loc.get("c_play5")}, ${loc.get("c_play12")} ${positionEstimation()}.`,
}); });
}, },
@ -192,7 +214,10 @@ export default {
const player = useMainPlayer(); const player = useMainPlayer();
const query = interaction.options.getString(loc_default!.get(`c_${filename}_opt1_name`)!, true); const query = interaction.options.getString(loc_default!.get(`c_${filename}_opt1_name`)!, true);
const limit_value_discord = 100; const limit_value_discord = discord_limit_autocompletion_value_length;
const limit_element_discord = discord_limit_autocompletion_list_length;
const query_discord = query.slice(0, limit_value_discord);
if (query) { if (query) {
/* Since Discord wanna receive a response within 3 secs and results is async /* Since Discord wanna receive a response within 3 secs and results is async
@ -208,7 +233,7 @@ export default {
/* Create a race between a timeout and the search /* Create a race between a timeout and the search
* At the end, Discord will always receive a response */ * At the end, Discord will always receive a response */
let tracks = await Promise.race([ const tracks = await Promise.race([
delay, delay,
player.search(query, { player.search(query, {
requestedBy: interaction.user, requestedBy: interaction.user,
@ -225,23 +250,18 @@ export default {
// If tracks found // If tracks found
if (tracks.length > 0) { if (tracks.length > 0) {
if (tracks.length > 25) { const payload = tracks
tracks = tracks
// Assure that URL is under the limit of Discord // Assure that URL is under the limit of Discord
.filter((v) => v.url.length < limit_value_discord) .filter((v) => v.url.length < limit_value_discord)
// Slice the list if needed to the 25 first results // Slice the list to respect the limit of Discord
.slice(0, 25); .slice(0, limit_element_discord - 1)
} .map((t) => {
// Returns a list of songs with their title and author
return interaction.respond(
tracks.map((t) => {
let title = t.title; let title = t.title;
let author = t.author; let author = t.author;
let name = `${title}${author}`; let name = `${title}${author}`;
// Slice returned data if needed to not exceed the length limit (100) // Slice returned data if needed to not exceed the length limit
if (name.length > 100) { if (name.length > limit_value_discord) {
const newTitle = title.substring(0, 40); const newTitle = title.substring(0, 40);
if (title.length != newTitle.length) { if (title.length != newTitle.length) {
title = `${newTitle}...`; title = `${newTitle}...`;
@ -257,12 +277,18 @@ export default {
name, name,
value: t.url, value: t.url,
}; };
}), });
);
payload.unshift({
name: query_discord,
value: query_discord,
});
// Returns a list of songs with their title and author
return interaction.respond(payload);
} }
} }
return interaction.respond([
{ name: loc.get("c_play9"), value: query.slice(0, limit_value_discord) }, return interaction.respond([{ name: loc.get("c_play9"), value: query_discord }]);
]);
}, },
}; };

View file

@ -9,6 +9,7 @@ import {
setTimeoutReminder, setTimeoutReminder,
updateReminder, updateReminder,
} from "../../utils/commands/reminder"; } from "../../utils/commands/reminder";
import { readSQL } from "../../utils/db";
export const once = true; export const once = true;
@ -19,7 +20,7 @@ export default async (client: Client) => {
// Restart all the timeout about reminders here // Restart all the timeout about reminders here
new Promise((ok, ko) => { new Promise((ok, ko) => {
// Fetch all reminders // Fetch all reminders
client.db.all("SELECT * FROM reminder", [], (err, row) => { client.db.all(readSQL("reminder/select"), [], (err, row) => {
if (err) { if (err) {
ko(err); ko(err);
} }

View file

@ -61,11 +61,11 @@ export const run = async (isDev: boolean) => {
console.log(logStart(client_name, true)); console.log(logStart(client_name, true));
console.log(`Botanique "${client.user?.username}" v${client.config.version} started!`); console.log(`Botanique "${client.user?.username}" v${client.config.version} started!`);
// ^C // Handle quit
process.on("SIGINT", () => quit(client)); process.on("exit", () => quit(client));
process.on("SIGHUP", () => process.exit(128 + 1));
// Container force closed process.on("SIGINT", () => process.exit(128 + 2));
process.on("SIGTERM", () => quit(client)); process.on("SIGTERM", () => process.exit(128 + 15));
}) })
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);

View file

@ -96,6 +96,9 @@
"c_play7": "Currently playing", "c_play7": "Currently playing",
"c_play8": "Asked by", "c_play8": "Asked by",
"c_play9": "No results were found", "c_play9": "No results were found",
"c_play10": "in position",
"c_play11": "estimation",
"c_play12": "play",
"c_stop_name": "stop", "c_stop_name": "stop",
"c_stop_desc": "Stop the music", "c_stop_desc": "Stop the music",
@ -148,12 +151,19 @@
"c_lyrics_sub2_desc": "Search for romanized lyrics (e.g., hangul → Latin)", "c_lyrics_sub2_desc": "Search for romanized lyrics (e.g., hangul → Latin)",
"c_lyrics_sub3_name": "synced", "c_lyrics_sub3_name": "synced",
"c_lyrics_sub3_desc": "Synchronized lyrics search (updates in live)", "c_lyrics_sub3_desc": "Synchronized lyrics search (updates in live)",
"c_lyrics_sub4_name": "stop-synced",
"c_lyrics_sub4_desc": "Stop Synchronized lyrics",
"c_lyrics_opt1_name": "song", "c_lyrics_opt1_name": "song",
"c_lyrics_opt1_desc": "Wanted song", "c_lyrics_opt1_desc": "Wanted song",
"c_lyrics1": "The bot is not playing anything at the moment, and no songs are specified.", "c_lyrics1": "The bot is not playing anything at the moment, and no songs are specified.",
"c_lyrics2": "Unable to find the lyrics for", "c_lyrics2": "Unable to find the lyrics for",
"c_lyrics3": "Unable to find synchronized lyrics for", "c_lyrics3": "Unable to find synchronized lyrics for",
"c_lyrics4": "It's karaoke time!", "c_lyrics4": "It's karaoke time!",
"c_lyrics5": "Unable to post the lyrics here.",
"c_lyrics6": "More of :",
"c_lyrics7": "Stop synchronized lyrics.",
"c_lyrics8": "No synchronized lyrics currently posted.",
"c_lyrics9": "Synchronized lyrics currently posted.",
"c_repeat_name": "repeat", "c_repeat_name": "repeat",
"c_repeat_desc": "Command for the type of music repetition", "c_repeat_desc": "Command for the type of music repetition",

View file

@ -96,6 +96,9 @@
"c_play7": "Joue actuellement", "c_play7": "Joue actuellement",
"c_play8": "Demandé par", "c_play8": "Demandé par",
"c_play9": "Aucun résultat trouvé", "c_play9": "Aucun résultat trouvé",
"c_play10": "en position",
"c_play11": "estimation",
"c_play12": "joue",
"c_stop_name": "stop", "c_stop_name": "stop",
"c_stop_desc": "Stop la musique", "c_stop_desc": "Stop la musique",
@ -148,12 +151,19 @@
"c_lyrics_sub2_desc": "Recherche de paroles romanisées (ex: hangul → latin)", "c_lyrics_sub2_desc": "Recherche de paroles romanisées (ex: hangul → latin)",
"c_lyrics_sub3_name": "synced", "c_lyrics_sub3_name": "synced",
"c_lyrics_sub3_desc": "Recherche de paroles synchronisées (se mettent à jour avec la chanson en direct)", "c_lyrics_sub3_desc": "Recherche de paroles synchronisées (se mettent à jour avec la chanson en direct)",
"c_lyrics_sub4_name": "stop-synced",
"c_lyrics_sub4_desc": "Arrête les paroles synchronisées",
"c_lyrics_opt1_name": "chanson", "c_lyrics_opt1_name": "chanson",
"c_lyrics_opt1_desc": "Chanson recherchée", "c_lyrics_opt1_desc": "Chanson recherchée",
"c_lyrics1": "Le bot ne joue rien en ce moment et aucune chanson n'est renseignée.", "c_lyrics1": "Le bot ne joue rien en ce moment et aucune chanson n'est renseignée.",
"c_lyrics2": "Impossible de trouver les paroles pour", "c_lyrics2": "Impossible de trouver les paroles pour",
"c_lyrics3": "Impossible de trouver les paroles synchronisées pour", "c_lyrics3": "Impossible de trouver les paroles synchronisées pour",
"c_lyrics4": "C'est parti !", "c_lyrics4": "C'est parti !",
"c_lyrics5": "Impossible de poster les paroles ici.",
"c_lyrics6": "Suite de :",
"c_lyrics7": "Arrêt des paroles synchronisées.",
"c_lyrics8": "Pas de paroles synchronisées en cours.",
"c_lyrics9": "Paroles synchronisées déjà en cours.",
"c_repeat_name": "repeat", "c_repeat_name": "repeat",
"c_repeat_desc": "Commande relative à la répétition des musiques", "c_repeat_desc": "Commande relative à la répétition des musiques",

View file

@ -75,6 +75,8 @@ declare module "discord.js" {
string, string,
/** Command itself */ /** Command itself */
{ {
/** Guilds where the command is active */
scope: () => string[];
/** Data about the command */ /** Data about the command */
data: SlashCommandBuilder; data: SlashCommandBuilder;
/** How the command interact */ /** How the command interact */

12
src/modules/player.ts Normal file
View file

@ -0,0 +1,12 @@
export {};
declare module "discord-player" {
export interface GuildQueue {
syncedLyricsMemory:
| {
isSubscribed: () => unknown;
unsubscribe: () => unknown;
}
| undefined;
}
}

13
src/sql/init.sql Normal file
View file

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS
reminder (
id INTEGER PRIMARY KEY,
data TEXT,
expiration_date TEXT,
option_id INTEGER,
channel_id TEXT,
creation_date TEXT,
user_id TEXT,
guild_id TEXT,
locale TEXT,
timeout_id TEXT
);

14
src/sql/reminder/add.sql Normal file
View file

@ -0,0 +1,14 @@
INSERT INTO
reminder (
data,
expiration_date,
option_id,
channel_id,
creation_date,
user_id,
guild_id,
locale,
timeout_id
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?);

13
src/sql/reminder/find.sql Normal file
View file

@ -0,0 +1,13 @@
SELECT
data,
creation_date,
expiration_date,
id
FROM
reminder
WHERE
user_id = ?
AND (
guild_id = ?
OR guild_id = 0
)

View file

@ -0,0 +1,6 @@
SELECT
*
FROM
reminder
WHERE
id = ?

View file

@ -0,0 +1,14 @@
SELECT
EXISTS (
SELECT
1
FROM
reminder
WHERE
id = ?
AND user_id = ?
AND (
guild_id = ?
OR guild_id = 0
)
)

View file

@ -0,0 +1,4 @@
DELETE FROM reminder
WHERE
creation_date = ?
AND user_id = ?

View file

@ -0,0 +1,4 @@
SELECT
*
FROM
reminder

View file

@ -0,0 +1,13 @@
UPDATE reminder
SET
data = ?,
expiration_date = ?,
option_id = ?,
channel_id = ?,
creation_date = ?,
user_id = ?,
guild_id = ?,
locale = ?,
timeout_id = ?
WHERE
ID = ?

View file

@ -5,6 +5,7 @@ import { Database } from "sqlite3";
import "../modules/client"; import "../modules/client";
import { loadLocales } from "./locales"; import { loadLocales } from "./locales";
import { YoutubeiExtractor } from "discord-player-youtubei"; import { YoutubeiExtractor } from "discord-player-youtubei";
import { readSQL } from "./db";
/** Creation of the client and definition of its properties */ /** Creation of the client and definition of its properties */
export default async (isDev: boolean) => { export default async (isDev: boolean) => {
@ -59,7 +60,7 @@ export default async (isDev: boolean) => {
client.db = new Database(`${process.env.DOCKERIZED === "1" ? "/config" : "./config"}/db.sqlite3`); client.db = new Database(`${process.env.DOCKERIZED === "1" ? "/config" : "./config"}/db.sqlite3`);
initDatabase(client.db); client.db.run(readSQL("init"));
return client; return client;
}; };
@ -75,25 +76,3 @@ export const quit = (client: Client) => {
// Close client // Close client
client.destroy(); client.destroy();
}; };
/**
* Initalize the database
* @param db Database
*/
const initDatabase = (db: Database) => {
// Table for reminders
db.run(
"CREATE TABLE IF NOT EXISTS reminder ( \
id INTEGER PRIMARY KEY, \
data TEXT, \
expiration_date TEXT, \
option_id INTEGER, \
channel_id TEXT, \
creation_date TEXT, \
user_id TEXT, \
guild_id TEXT, \
locale TEXT, \
timeout_id TEXT \
);",
);
};

View file

@ -0,0 +1,73 @@
import {
APIApplicationCommandSubcommandOption,
ApplicationCommandOptionType,
Client,
Locale,
SlashCommandBuilder,
} from "discord.js";
type Data = SlashCommandBuilder | APIApplicationCommandSubcommandOption;
/**
* Find the name of the command, trying to get the correct locale
* @param data Command data
* @param locale Locale wanted
* @returns Command's name
*/
export const goodName = (data: Data, locale: Locale) =>
data.name_localizations?.[locale] ?? data.name;
/**
* Find the description of the command, trying to get the correct locale
* @param data Command data
* @param locale Locale wanted
* @returns Command's description
*/
export const goodDescription = (data: Data, locale: Locale) =>
data.description_localizations?.[locale] ?? data.description;
/**
* Aux function for Sub/NameNotLocalized
* @param cmd data
* @param command command researched
* @returns if we found or not the researched command
*/
const filterLocalizations = (cmd: Data, command: string) => {
let res = false;
for (const key in cmd?.name_localizations) {
res = res || cmd.name_localizations?.[key as Locale] === command;
}
return res;
};
/**
* Find a command based on any string, localized or not
* @param command string
* @returns the not localized corresponding string's command name
*/
export const NameNotLocalized = (client: Client, command: string): SlashCommandBuilder | null => {
const list = client.commands.list.map((cmd) => cmd.data);
return (
list.find((cmd) => cmd.name === command) ||
list.filter((cmd) => filterLocalizations(cmd, command))[0]
);
};
/**
* Find a subcommand of a command based on any string, localized or not
* @param parent command of the subcommand
* @param command string
* @returns the not localized corresponding string's subcommand name
*/
export const SubnameNotLocalized = (parent: SlashCommandBuilder, command: string) => {
const list = parent
?.toJSON()
.options?.filter((option) => option.type === ApplicationCommandOptionType.Subcommand);
return (
list?.find((cmd) => cmd?.name === command) ||
list?.filter((cmd) => filterLocalizations(cmd, command))[0]
);
};

View file

@ -3,6 +3,7 @@ import { GuildQueue, QueueRepeatMode } from "discord-player";
import { Client } from "discord.js"; import { Client } from "discord.js";
import { getLocale } from "../locales"; import { getLocale } from "../locales";
import { blank } from "../misc"; import { blank } from "../misc";
import { discord_limit_embed_field } from "../constants";
export const embedListQueue = ( export const embedListQueue = (
client: Client, client: Client,
@ -17,8 +18,7 @@ export const embedListQueue = (
// Add the current song at the top of the list // Add the current song at the top of the list
tracks.unshift(queue.history.currentTrack!); tracks.unshift(queue.history.currentTrack!);
// Limit of discord is 25 const limit_fields = discord_limit_embed_field;
const limit_fields = 25;
const pageMax = Math.ceil(tracks.length / limit_fields); const pageMax = Math.ceil(tracks.length / limit_fields);

View file

@ -3,6 +3,7 @@ import { getLocale } from "../locales";
import { blank, cleanCodeBlock } from "../misc"; import { blank, cleanCodeBlock } from "../misc";
import { showDate, strToSeconds, timeDeltaToString } from "../time"; import { showDate, strToSeconds, timeDeltaToString } from "../time";
import { RegexC, RegExpFlags } from "../regex"; import { RegexC, RegExpFlags } from "../regex";
import { readSQL } from "../db";
/** /**
* Option possible for reminders * Option possible for reminders
@ -92,9 +93,7 @@ export const newReminder = async (client: Client, time: string, info: infoRemind
// Add the remind to the db // Add the remind to the db
client.db.run( client.db.run(
"INSERT INTO reminder ( \ readSQL("reminder/add"),
data, expiration_date, option_id, channel_id, creation_date, user_id, guild_id, locale, timeout_id \
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? );",
[ [
info.message, info.message,
`${expiration_date}`, `${expiration_date}`,
@ -127,19 +126,15 @@ export const newReminder = async (client: Client, time: string, info: infoRemind
export const deleteReminder = (client: Client, createdAt: string, userId: string) => { export const deleteReminder = (client: Client, createdAt: string, userId: string) => {
// Delete the reminder for the database // Delete the reminder for the database
return new Promise((ok, ko) => { return new Promise((ok, ko) => {
// Add the remind to the db // Remove the remind to the db
client.db.run( client.db.run(readSQL("reminder/remove"), [createdAt, userId], (err) => {
"DELETE FROM reminder WHERE creation_date = ? AND user_id = ?",
[createdAt, userId],
(err) => {
if (err) { if (err) {
ko(err); ko(err);
} }
// Send confirmation to user // Send confirmation to user
ok(true); ok(true);
}, });
);
}); });
}; };
@ -199,14 +194,18 @@ const sendReminderAux = (client: Client, info: infoReminder, option: OptionRemin
// Channel // Channel
client.channels.fetch(info.channelId!).then((channel) => { client.channels.fetch(info.channelId!).then((channel) => {
if (channel?.isSendable()) { if (channel?.isSendable()) {
let content = `<@${info.userId}>`; const author_mention = `<@${info.userId}>`;
let content = author_mention;
embed.setFooter({ embed.setFooter({
text: `${loc.get("c_reminder17")} ${timeDeltaToString(info.createdAt)}`, text: `${loc.get("c_reminder17")} ${timeDeltaToString(info.createdAt)}`,
}); });
// Mention everybody if needed // Mention everybody if needed
if (option === OptionReminder.Mention) { if (option === OptionReminder.Mention) {
(info.message?.match(/<@\d+>/g) ?? []).forEach((mention) => { [...new Set(info.message?.match(/<@\d+>/g) ?? [])]
.filter((mention) => mention !== author_mention)
.forEach((mention: string) => {
content += " " + mention; content += " " + mention;
}); });
} }
@ -276,12 +275,7 @@ export const checkOwnershipReminder = async (
const data = (await new Promise((ok, ko) => { const data = (await new Promise((ok, ko) => {
// Check the ownership // Check the ownership
client.db.all<returnData>( client.db.all<returnData>(
"SELECT EXISTS ( \ readSQL("reminder/ownership_check"),
SELECT 1 FROM reminder \
WHERE id = ? \
AND user_id = ? \
AND (guild_id = ? OR guild_id = 0) \
)",
[id, userId, guildId], [id, userId, guildId],
(err, row) => { (err, row) => {
if (err) { if (err) {
@ -304,19 +298,14 @@ export const checkOwnershipReminder = async (
export const getReminderInfo = async (client: Client, id: number) => { export const getReminderInfo = async (client: Client, id: number) => {
return (await new Promise((ok, ko) => { return (await new Promise((ok, ko) => {
// Check the ownership // Check the ownership
client.db.all<dbReminder>( client.db.all<dbReminder>(readSQL("reminder/findById"), [id], (err, row) => {
"SELECT * FROM reminder \
WHERE id = ?",
[id],
(err, row) => {
if (err) { if (err) {
ko(err); ko(err);
} }
// Send all the current reminders // Send all the current reminders
ok(row[0]); ok(row[0]);
}, });
);
})) as dbReminder; })) as dbReminder;
}; };
@ -330,17 +319,7 @@ export const updateReminder = (client: Client, data: dbReminder) => {
return new Promise((ok, ko) => { return new Promise((ok, ko) => {
// Update the db // Update the db
client.db.run( client.db.run(
"UPDATE reminder \ readSQL("reminder/update"),
SET data = ?, \
expiration_date = ?, \
option_id = ?, \
channel_id = ?, \
creation_date = ?, \
user_id = ?, \
guild_id = ?, \
locale = ?, \
timeout_id = ? \
WHERE ID = ?",
[ [
data.data, data.data,
data.expiration_date, data.expiration_date,
@ -374,19 +353,14 @@ export const updateReminder = (client: Client, data: dbReminder) => {
const listReminders = async (client: Client, userId: string, guildId: string | null) => { const listReminders = async (client: Client, userId: string, guildId: string | null) => {
return (await new Promise((ok, ko) => { return (await new Promise((ok, ko) => {
// Check the ownership // Check the ownership
client.db.all<dbReminder>( client.db.all<dbReminder>(readSQL("reminder/find"), [userId, guildId ?? 0], (err, row) => {
"SELECT data, creation_date, expiration_date, id FROM reminder \
WHERE user_id = ? AND (guild_id = ? OR guild_id = 0)",
[userId, guildId ?? 0],
(err, row) => {
if (err) { if (err) {
ko(err); ko(err);
} }
// Send all the current reminders // Send all the current reminders
ok(row); ok(row);
}, });
);
})) as dbReminder[]; })) as dbReminder[];
}; };

11
src/utils/constants.ts Normal file
View file

@ -0,0 +1,11 @@
/** Max message length */
export const discord_limit_message = 2000;
/** Max embed field an embed can have */
export const discord_limit_embed_field = 25;
/** Max element the autocompletion of slash commands can have */
export const discord_limit_autocompletion_list_length = 25;
/** Max length of an element in autocompletion of slash commands */
export const discord_limit_autocompletion_value_length = 100;

15
src/utils/db.ts Normal file
View file

@ -0,0 +1,15 @@
import fs from "node:fs";
export const readSQL = (path: string) => {
const dir = "./src/sql/";
if (!path.startsWith(dir)) {
path = dir + path;
}
const ext = ".sql";
if (!path.endsWith(ext)) {
path += ext;
}
return fs.readFileSync(path, "utf8");
};

View file

@ -94,14 +94,12 @@ export const strToSeconds = (time: string) => {
}; };
/** /**
* Calculating the difference between a date and now * Returns the time in a readable way
* @param lang Locale * @param seconds Time in milliseconds
* @param time Time * @returns Time as string
* @returns Delta between the time and now
*/ */
export const timeDeltaToString = (time: number) => { export const timeToString = (time: number) => {
const now = Date.now(); let secondsDifference = Math.abs(Math.ceil(time / 1000));
let secondsDifference = Math.abs(Math.ceil((time - now) / 1000));
if (secondsDifference === 0) { if (secondsDifference === 0) {
return "0s"; return "0s";
@ -123,3 +121,13 @@ export const timeDeltaToString = (time: number) => {
.filter(Boolean) .filter(Boolean)
.join(" "); .join(" ");
}; };
/**
* Calculating the difference between a date and now
* @param time Time in milliseconds
* @returns Delta between the time and now
*/
export const timeDeltaToString = (time: number) => {
const now = Date.now();
return timeToString(time - now);
};