Compare commits
13 commits
5b3d6a4489
...
dabdf86d82
Author | SHA1 | Date | |
---|---|---|---|
dabdf86d82 | |||
f9cc154b04 | |||
2cc6c0bd74 | |||
a08d0c0e9b | |||
b1abeefad2 | |||
facf0cd88e | |||
7fed94def8 | |||
088693d2d2 | |||
929312e0ed | |||
e4d1e307df | |||
767612a000 | |||
23d3918459 | |||
fdc081fd6d |
40 changed files with 4387 additions and 408 deletions
|
@ -5,3 +5,5 @@
|
|||
!package-lock.json
|
||||
!LICENSE
|
||||
!tsconfig.json
|
||||
|
||||
src/tests/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: Lint and Format Check
|
||||
name: PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -21,3 +21,6 @@ jobs:
|
|||
|
||||
- name: Run format check
|
||||
run: npm run format-check
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test -- --ci
|
|
@ -2,7 +2,8 @@ name: Publish latest version
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -32,5 +33,6 @@ jobs:
|
|||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -15,3 +15,6 @@ dist/
|
|||
|
||||
# Debug file
|
||||
src/events/player/debug.ts
|
||||
|
||||
# Jest
|
||||
coverage/
|
||||
|
|
|
@ -25,6 +25,7 @@ une [Pull Request](https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/pulls)
|
|||
- [Modifier du code](#modifier-du-code)
|
||||
- [Soumettre ses modifications](#soumettre-ses-modifications)
|
||||
- [Gestion du dépôt](#gestion-du-dépôt)
|
||||
- [Tester son code](#tester-son-code)
|
||||
|
||||
## Recevoir de l'aide
|
||||
|
||||
|
@ -284,3 +285,20 @@ Pour commencer, vous pouvez jeter un œil aux
|
|||
[le graphe](https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/graph).
|
||||
- De préférences, suivre [ces conventions](https://www.conventionalcommits.org/fr/v1.0.0/)
|
||||
(cf. cette [partie précédente](#soumettre-ses-modifications)).
|
||||
|
||||
## Tester son code
|
||||
|
||||
Il est souhaité d'écrire des tests quand cela est possible.
|
||||
|
||||
```ts
|
||||
import { fnReturnsTrue } from "../src/utils/file";
|
||||
|
||||
describe("test name", () => {
|
||||
{
|
||||
const name = "to be tested";
|
||||
test(name, () => {
|
||||
expect(fnReturnsTrue() /* function to test */).toBe(true /* expected result */);
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22.8-bullseye-slim
|
||||
FROM node:22.9-bullseye-slim
|
||||
|
||||
ENV DOCKERIZED=1
|
||||
RUN mkdir /config && \
|
||||
|
|
3611
package-lock.json
generated
3611
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -8,7 +8,8 @@
|
|||
"debug": "npx tsnd --respawn ./src/index.ts",
|
||||
"lint": "npx eslint src",
|
||||
"format-check": "npx prettier --check src",
|
||||
"format-write": "npx prettier --write src"
|
||||
"format-write": "npx prettier --write src",
|
||||
"test": "npx jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -19,21 +20,28 @@
|
|||
"dependencies": {
|
||||
"@discord-player/extractor": "^4.5.1",
|
||||
"@discordjs/rest": "^2.4.0",
|
||||
"@distube/ytdl-core": "^4.14.4",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"discord-player": "^6.7.1",
|
||||
"discord-player-youtubei": "^1.3.1",
|
||||
"discord.js": "^14.16.2",
|
||||
"mediaplex": "^0.0.9",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "~29.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "~8.6.0",
|
||||
"@typescript-eslint/parser": "~8.6.0",
|
||||
"dotenv": "~16.4.5",
|
||||
"jest": "~29.7.0",
|
||||
"prettier-eslint": "~16.3.0",
|
||||
"ts-jest": "~29.2.5",
|
||||
"ts-node-dev": "~2.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ import { getLocale } from "../utils/locales";
|
|||
|
||||
export default async (client: Client) => {
|
||||
// Dossier des buttons
|
||||
const buttons_categories = (await readdir(__dirname)).filter(
|
||||
(element) => !element.endsWith(".js") && !element.endsWith(".ts"),
|
||||
);
|
||||
const buttons_categories = (await readdir(__dirname, { withFileTypes: true }))
|
||||
.filter((element) => element.isDirectory())
|
||||
.map((element) => element.name);
|
||||
|
||||
await Promise.all(
|
||||
// For each categorie
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
ButtonStyle,
|
||||
Client,
|
||||
MessageComponentInteraction,
|
||||
User,
|
||||
} from "discord.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getLocale } from "../../utils/locales";
|
||||
|
@ -18,11 +17,11 @@ export default {
|
|||
},
|
||||
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
|
||||
const loc = getLocale(client, interaction.locale);
|
||||
const embed_desc = interaction.message.embeds.at(0)?.description as string;
|
||||
const embed_desc = interaction.message.embeds.at(0)?.description;
|
||||
|
||||
// Retrieve Pages
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc)?.[0]);
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc!)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc!)?.[0]);
|
||||
if (page + 1 > pageMax) {
|
||||
page = 1;
|
||||
} else {
|
||||
|
@ -30,8 +29,8 @@ export default {
|
|||
}
|
||||
|
||||
// Retrieve user
|
||||
const userId = /(?!<@)\d+(?=>)/gm.exec(embed_desc)?.[0] as string;
|
||||
const user = client.users.cache.get(userId) as User;
|
||||
const userId = /(?!<@)\d+(?=>)/gm.exec(embed_desc!)?.[0];
|
||||
const user = client.users.cache.get(userId!)!;
|
||||
|
||||
// Fetch list
|
||||
const list = await embedListReminders(
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
ButtonStyle,
|
||||
Client,
|
||||
MessageComponentInteraction,
|
||||
User,
|
||||
} from "discord.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getLocale } from "../../utils/locales";
|
||||
|
@ -18,20 +17,20 @@ export default {
|
|||
},
|
||||
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
|
||||
const loc = getLocale(client, interaction.locale);
|
||||
const embed_desc = interaction.message.embeds.at(0)?.description as string;
|
||||
const embed_desc = interaction.message.embeds.at(0)?.description;
|
||||
|
||||
// Retrieve Pages
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc)?.[0]);
|
||||
if (page - 1 == 0) {
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc!)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc!)?.[0]);
|
||||
if (page - 1 === 0) {
|
||||
page = pageMax;
|
||||
} else {
|
||||
page--;
|
||||
}
|
||||
|
||||
// Retrieve user
|
||||
const userId = /(?!<@)\d+(?=>)/gm.exec(embed_desc)?.[0] as string;
|
||||
const user = client.users.cache.get(userId) as User;
|
||||
const userId = /(?!<@)\d+(?=>)/gm.exec(embed_desc!)?.[0];
|
||||
const user = client.users.cache.get(userId!)!;
|
||||
|
||||
// Fetch list
|
||||
const list = await embedListReminders(
|
||||
|
|
|
@ -19,11 +19,11 @@ export default {
|
|||
},
|
||||
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
|
||||
const loc = getLocale(client, interaction.locale);
|
||||
const embed_desc = interaction.message.embeds.at(0)?.author?.name as string;
|
||||
const embed_desc = interaction.message.embeds.at(0)?.author?.name;
|
||||
|
||||
// Retrieve Pages
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc)?.[0]);
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc!)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc!)?.[0]);
|
||||
if (page + 1 > pageMax) {
|
||||
page = 1;
|
||||
} else {
|
||||
|
|
|
@ -19,12 +19,12 @@ export default {
|
|||
},
|
||||
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
|
||||
const loc = getLocale(client, interaction.locale);
|
||||
const embed_desc = interaction.message.embeds.at(0)?.author?.name as string;
|
||||
const embed_desc = interaction.message.embeds.at(0)!.author!.name;
|
||||
|
||||
// Retrieve Pages
|
||||
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc)?.[0]);
|
||||
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc)?.[0]);
|
||||
if (page - 1 == 0) {
|
||||
if (page - 1 === 0) {
|
||||
page = pageMax;
|
||||
} else {
|
||||
page--;
|
||||
|
|
|
@ -8,9 +8,9 @@ import { removeExtension } from "../utils/misc";
|
|||
export default async (client: Client) => {
|
||||
const rest = new REST({ version: "10" }).setToken(client.token!);
|
||||
|
||||
const command_categories = (await readdir(__dirname)).filter(
|
||||
(element) => !element.endsWith(".js") && !element.endsWith(".ts"),
|
||||
);
|
||||
const command_categories = (await readdir(__dirname, { withFileTypes: true }))
|
||||
.filter((element) => element.isDirectory())
|
||||
.map((element) => element.name);
|
||||
|
||||
const commands = (
|
||||
await Promise.all(
|
||||
|
@ -64,14 +64,14 @@ export default async (client: Client) => {
|
|||
|
||||
scopedCommands.forEach(
|
||||
async (command, guild) =>
|
||||
await rest.put(Routes.applicationGuildCommands(client.user?.id as string, guild), {
|
||||
await rest.put(Routes.applicationGuildCommands(client.user!.id, guild), {
|
||||
body: command,
|
||||
}),
|
||||
);
|
||||
|
||||
// Send global commands to Discord
|
||||
const globalCommands = commands.filter((c) => c.scope().length == 0);
|
||||
return await rest.put(Routes.applicationCommands(client.user?.id as string), {
|
||||
const globalCommands = commands.filter((c) => c.scope().length === 0);
|
||||
return await rest.put(Routes.applicationCommands(client.user!.id), {
|
||||
body: globalCommands.map((c) => c.data.toJSON()),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -74,14 +74,14 @@ export default {
|
|||
allChannel?.then(async (channelGuild) => {
|
||||
// Retrieve category to archive
|
||||
const catToArchive = channelGuild
|
||||
.filter((chan) => chan?.type == ChannelType.GuildCategory)
|
||||
.filter((chan) => chan?.name == desiredCat);
|
||||
.filter((chan) => chan?.type === ChannelType.GuildCategory)
|
||||
.filter((chan) => chan?.name === desiredCat);
|
||||
|
||||
// Create/Retrieve the archive category
|
||||
const catArchivedName = "archive - " + desiredCat;
|
||||
const catArchivedMap = channelGuild
|
||||
.filter((chan) => chan?.type == ChannelType.GuildCategory)
|
||||
.filter((chan) => chan?.name == catArchivedName);
|
||||
.filter((chan) => chan?.type === ChannelType.GuildCategory)
|
||||
.filter((chan) => chan?.name === catArchivedName);
|
||||
|
||||
let catArchived: NonThreadGuildBasedChannel | null | undefined;
|
||||
if (catArchivedMap.size > 0) {
|
||||
|
@ -94,11 +94,11 @@ export default {
|
|||
}
|
||||
|
||||
const allChannelDesired = channelGuild
|
||||
.filter((chan) => chan?.type == 0)
|
||||
.filter((chan) => chan?.parentId == catToArchive.map((cat) => cat?.id)[0]);
|
||||
.filter((chan) => chan?.type === 0)
|
||||
.filter((chan) => chan?.parentId === catToArchive.map((cat) => cat?.id)[0]);
|
||||
|
||||
// If no channels in the source category
|
||||
if (allChannelDesired.size == 0) {
|
||||
if (allChannelDesired.size === 0) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { SlashCommandBuilder } from "@discordjs/builders";
|
||||
import { Locale } from "discord-api-types/v9";
|
||||
import { ChatInputCommandInteraction, Client, Colors, EmbedBuilder } from "discord.js";
|
||||
import "../../modules/string";
|
||||
import { getLocale, getLocalizations } from "../../utils/locales";
|
||||
|
@ -50,6 +49,7 @@ export default {
|
|||
// 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) => {
|
||||
const commands = commands_name.reduce((data, command_name) => {
|
||||
return data + `\`/${command_name}\`, `;
|
||||
|
@ -75,6 +75,7 @@ export default {
|
|||
|
||||
// If a command is specified
|
||||
// TODO: Check if the command exist in the context (guild)
|
||||
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/187
|
||||
const command = client.commands.list.get(desired_command);
|
||||
if (!command) {
|
||||
// Command don't exist
|
||||
|
@ -93,7 +94,7 @@ export default {
|
|||
.setDescription(
|
||||
// Loads the description
|
||||
// according to the user's locals
|
||||
command.data.description_localizations?.[interaction.locale as Locale] ??
|
||||
command.data.description_localizations?.[interaction.locale] ??
|
||||
command.data.description,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -67,21 +67,21 @@ export default {
|
|||
const allChannel = interaction.guild?.channels.fetch();
|
||||
allChannel?.then((channel_guild) => {
|
||||
const cat_to_prep = channel_guild
|
||||
.filter((chan) => chan?.type == ChannelType.GuildCategory)
|
||||
.filter((chan) => chan?.name == desired_cat);
|
||||
.filter((chan) => chan?.type === ChannelType.GuildCategory)
|
||||
.filter((chan) => chan?.name === desired_cat);
|
||||
const cat_to_prep_id = cat_to_prep.map((cat) => cat?.id);
|
||||
const cat_to_prep_name = cat_to_prep.map((cat) => cat?.name);
|
||||
|
||||
// console.log(cat_to_prep);
|
||||
const all_channel_desired = channel_guild
|
||||
.filter((chan) => chan?.type == 0)
|
||||
.filter((chan) => chan?.parentId == cat_to_prep_id[0]);
|
||||
.filter((chan) => chan?.type === 0)
|
||||
.filter((chan) => chan?.parentId === cat_to_prep_id[0]);
|
||||
const all_channel_desired_name = all_channel_desired.map((c_d) => c_d?.name);
|
||||
|
||||
let desc = "";
|
||||
|
||||
const general = "général";
|
||||
if (all_channel_desired_name.filter((cdn) => cdn == general).length == 0) {
|
||||
if (all_channel_desired_name.filter((cdn) => cdn === general).length === 0) {
|
||||
interaction.guild?.channels.create({
|
||||
name: general,
|
||||
type: 0,
|
||||
|
@ -91,7 +91,7 @@ export default {
|
|||
}
|
||||
|
||||
const info = "informations";
|
||||
if (all_channel_desired_name.filter((cdn) => cdn == info).length == 0) {
|
||||
if (all_channel_desired_name.filter((cdn) => cdn === info).length === 0) {
|
||||
interaction.guild?.channels.create({
|
||||
name: info,
|
||||
type: 0,
|
||||
|
@ -101,7 +101,7 @@ export default {
|
|||
desc += "`" + info + "` " + loc.get("c_prep5") + "\n";
|
||||
}
|
||||
|
||||
if (desc == "") {
|
||||
if (desc === "") {
|
||||
desc = loc.get("c_prep6");
|
||||
}
|
||||
|
||||
|
|
|
@ -145,25 +145,32 @@ export default {
|
|||
case loc_default?.get(`c_${filename}_sub1_name`)?.toLowerCase(): {
|
||||
// If time is already renseigned
|
||||
const time = interaction.options.getString(
|
||||
loc_default?.get(`c_${filename}_sub1_opt1_name`) as string,
|
||||
loc_default!.get(`c_${filename}_sub1_opt1_name`)!,
|
||||
);
|
||||
if (time != null) {
|
||||
// Use the cli because we already have enough data
|
||||
return newReminder(client, time, {
|
||||
locale: interaction.locale,
|
||||
message: interaction.options.getString(
|
||||
loc_default?.get(`c_${filename}_sub1_opt2_name`) as string,
|
||||
loc_default!.get(`c_${filename}_sub1_opt2_name`)!,
|
||||
),
|
||||
createdAt: interaction.createdAt.getTime(),
|
||||
channelId: interaction.channelId,
|
||||
userId: interaction.user.id,
|
||||
guildId: interaction.guildId,
|
||||
}).then((msg) =>
|
||||
interaction.reply({
|
||||
content: msg as string,
|
||||
ephemeral: true,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.then((msg) =>
|
||||
interaction.reply({
|
||||
content: msg as string,
|
||||
ephemeral: true,
|
||||
}),
|
||||
)
|
||||
.catch((err) => {
|
||||
interaction.reply({
|
||||
content: err,
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Show modal to user to get at least the time
|
||||
const modal = new ModalBuilder()
|
||||
|
@ -195,17 +202,13 @@ export default {
|
|||
// List reminders
|
||||
case loc_default?.get(`c_${filename}_sub2_name`)?.toLowerCase(): {
|
||||
// Which user to show
|
||||
let user = interaction.options.getUser(
|
||||
loc_default?.get(`c_${filename}_sub2_opt1_name`) as string,
|
||||
);
|
||||
if (user == null) {
|
||||
let user = interaction.options.getUser(loc_default!.get(`c_${filename}_sub2_opt1_name`)!);
|
||||
if (user === null) {
|
||||
user = interaction.user;
|
||||
}
|
||||
|
||||
const page =
|
||||
interaction.options.getInteger(
|
||||
loc_default?.get(`c_${filename}_sub2_opt2_name`) as string,
|
||||
) ?? 1;
|
||||
interaction.options.getInteger(loc_default!.get(`c_${filename}_sub2_opt2_name`)!) ?? 1;
|
||||
const list = await embedListReminders(
|
||||
client,
|
||||
user,
|
||||
|
@ -246,7 +249,7 @@ export default {
|
|||
// Delete a reminder
|
||||
case loc_default?.get(`c_${filename}_sub3_name`)?.toLowerCase(): {
|
||||
const id = interaction.options.getInteger(
|
||||
loc_default?.get(`c_${filename}_sub3_opt1_name`) as string,
|
||||
loc_default!.get(`c_${filename}_sub3_opt1_name`)!,
|
||||
);
|
||||
if (id === null) {
|
||||
return interaction.reply({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { SlashCommandBuilder } from "@discordjs/builders";
|
||||
import { Player, useMainPlayer, useQueue } from "discord-player";
|
||||
import { useMainPlayer, useQueue } from "discord-player";
|
||||
import { ChatInputCommandInteraction, Client, EmbedBuilder } from "discord.js";
|
||||
import { getLocale, getLocalizations } from "../../utils/locales";
|
||||
import { getFilename } from "../../utils/misc";
|
||||
|
@ -74,18 +74,16 @@ export default {
|
|||
|
||||
const loc = getLocale(client, interaction.locale);
|
||||
|
||||
let request = interaction.options.getString(
|
||||
loc_default?.get(`c_${filename}_opt1_name`) as string,
|
||||
);
|
||||
let request = interaction.options.getString(loc_default!.get(`c_${filename}_opt1_name`)!);
|
||||
|
||||
let data = null;
|
||||
await interaction.deferReply();
|
||||
|
||||
const player = useMainPlayer() as Player;
|
||||
const player = useMainPlayer();
|
||||
const queue = useQueue(interaction.guildId!);
|
||||
if (request) {
|
||||
if (
|
||||
interaction.options.getSubcommand() ==
|
||||
interaction.options.getSubcommand() ===
|
||||
loc_default?.get(`c_${filename}_sub2_name`)?.toLowerCase()
|
||||
) {
|
||||
// Romanized
|
||||
|
@ -111,7 +109,7 @@ export default {
|
|||
}
|
||||
|
||||
if (
|
||||
interaction.options.getSubcommand() ==
|
||||
interaction.options.getSubcommand() ===
|
||||
loc_default?.get(`c_${filename}_sub3_name`)?.toLowerCase()
|
||||
) {
|
||||
if (queue === null) {
|
||||
|
@ -154,9 +152,10 @@ export default {
|
|||
const limit_desc = 4096;
|
||||
const nb_embed = Math.ceil(title.plainLyrics.length / limit_desc);
|
||||
|
||||
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/186
|
||||
// TODO: If lyrics < 6000, only send one message with multiples embed
|
||||
for (let i = 0, j = 0; i < nb_embed; i++, j += limit_desc) {
|
||||
// TODO: Better cut in lyrics
|
||||
// + Better cut in lyrics
|
||||
const lyrics = title.plainLyrics.slice(j, j + limit_desc);
|
||||
|
||||
let embed;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { SlashCommandBuilder } from "@discordjs/builders";
|
||||
import { Player, SearchResult, useMainPlayer, useQueue } from "discord-player";
|
||||
import { SearchResult, useMainPlayer, useQueue } from "discord-player";
|
||||
import {
|
||||
AutocompleteInteraction,
|
||||
ChatInputCommandInteraction,
|
||||
Client,
|
||||
EmbedBuilder,
|
||||
GuildResolvable,
|
||||
VoiceBasedChannel,
|
||||
} from "discord.js";
|
||||
import { getLocale, getLocalizations } from "../../utils/locales";
|
||||
import { Metadata } from "../../utils/metadata";
|
||||
|
@ -29,14 +27,42 @@ export default {
|
|||
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
|
||||
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`))
|
||||
|
||||
// Command option
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(loc_default.get(`c_${filename}_opt1_name`)!.toLowerCase())
|
||||
.setDescription(loc_default.get(`c_${filename}_opt1_desc`)!)
|
||||
.setNameLocalizations(getLocalizations(client, `c_${filename}_opt1_name`, true))
|
||||
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_opt1_desc`))
|
||||
.setAutocomplete(true),
|
||||
// Normal
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName(loc_default.get(`c_${filename}_sub1_name`)!.toLowerCase())
|
||||
.setDescription(loc_default.get(`c_${filename}_sub1_desc`)!)
|
||||
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub1_name`, true))
|
||||
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub1_desc`))
|
||||
|
||||
// Command option
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(loc_default.get(`c_${filename}_opt1_name`)!.toLowerCase())
|
||||
.setDescription(loc_default.get(`c_${filename}_opt1_desc`)!)
|
||||
.setNameLocalizations(getLocalizations(client, `c_${filename}_opt1_name`, true))
|
||||
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_opt1_desc`))
|
||||
.setAutocomplete(true),
|
||||
),
|
||||
)
|
||||
|
||||
// Play now
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName(loc_default.get(`c_${filename}_sub2_name`)!.toLowerCase())
|
||||
.setDescription(loc_default.get(`c_${filename}_sub2_desc`)!)
|
||||
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub2_name`, true))
|
||||
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub2_desc`))
|
||||
|
||||
// Command option
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(loc_default.get(`c_${filename}_opt1_name`)!.toLowerCase())
|
||||
.setDescription(loc_default.get(`c_${filename}_opt1_desc`)!)
|
||||
.setNameLocalizations(getLocalizations(client, `c_${filename}_opt1_name`, true))
|
||||
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_opt1_desc`))
|
||||
.setAutocomplete(true),
|
||||
),
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -67,11 +93,9 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
const query = interaction.options.getString(
|
||||
loc_default?.get(`c_${filename}_opt1_name`) as string,
|
||||
);
|
||||
const query = interaction.options.getString(loc_default!.get(`c_${filename}_opt1_name`)!);
|
||||
|
||||
const player = useMainPlayer() as Player;
|
||||
const player = useMainPlayer();
|
||||
if (!query) {
|
||||
// Now playing
|
||||
|
||||
|
@ -97,7 +121,8 @@ export default {
|
|||
return await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const queue = player.nodes.create(interaction.guild as GuildResolvable, {
|
||||
const queue = player.nodes.create(interaction.guild!, {
|
||||
volume: 50,
|
||||
defaultFFmpegFilters: ["silenceremove"],
|
||||
metadata: {
|
||||
channel: interaction.channel,
|
||||
|
@ -106,7 +131,7 @@ export default {
|
|||
|
||||
// Verify vc connection
|
||||
try {
|
||||
if (!queue.connection) await queue.connect(member.voice.channel as VoiceBasedChannel);
|
||||
if (!queue.connection) await queue.connect(member.voice.channel!);
|
||||
} catch {
|
||||
queue.delete();
|
||||
return await interaction.reply({
|
||||
|
@ -134,7 +159,15 @@ export default {
|
|||
} else {
|
||||
const track = result.tracks[0];
|
||||
|
||||
queue.addTrack(track);
|
||||
if (
|
||||
interaction.options.getSubcommand() ===
|
||||
loc_default?.get(`c_${filename}_sub2_name`)?.toLowerCase()
|
||||
) {
|
||||
queue.insertTrack(track, 0);
|
||||
} else {
|
||||
queue.addTrack(track);
|
||||
}
|
||||
|
||||
title = track.title;
|
||||
}
|
||||
|
||||
|
@ -145,6 +178,7 @@ export default {
|
|||
// TODO: When added to an existing queue (size of queue > 0):
|
||||
// - Add position in queue
|
||||
// - Add estimated time until playing
|
||||
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/184
|
||||
return await interaction.followUp({
|
||||
content: `⏱️ | \`${title}\` ${loc.get("c_play5")}.`,
|
||||
});
|
||||
|
@ -155,11 +189,8 @@ export default {
|
|||
const loc_default = interaction.client.locales.get(interaction.client.config.default_lang);
|
||||
const filename = getFilename(__filename);
|
||||
|
||||
const player = useMainPlayer() as Player;
|
||||
const query = interaction.options.getString(
|
||||
loc_default?.get(`c_${filename}_opt1_name`) as string,
|
||||
true,
|
||||
);
|
||||
const player = useMainPlayer();
|
||||
const query = interaction.options.getString(loc_default!.get(`c_${filename}_opt1_name`)!, true);
|
||||
|
||||
const limit_value_discord = 100;
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ export default {
|
|||
|
||||
// Specified ID
|
||||
// TODO?: ID range -> as a string: 5-8 remove 5, 6, 7, 8
|
||||
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/185
|
||||
.addNumberOption((option) =>
|
||||
option
|
||||
.setName(loc_default.get(`c_${filename}_sub3_opt1_name`)!.toLowerCase())
|
||||
|
@ -103,9 +104,7 @@ export default {
|
|||
// Show the queue
|
||||
case loc_default?.get(`c_${filename}_sub1_name`)?.toLowerCase(): {
|
||||
const page =
|
||||
interaction.options.getNumber(
|
||||
loc_default?.get(`c_${filename}_sub1_opt1_name`) as string,
|
||||
) ?? 1;
|
||||
interaction.options.getNumber(loc_default!.get(`c_${filename}_sub1_opt1_name`)!) ?? 1;
|
||||
|
||||
embedListQueue(client, embed, queue, page, interaction.locale);
|
||||
|
||||
|
@ -152,8 +151,8 @@ export default {
|
|||
// Remove <ID>
|
||||
case loc_default?.get(`c_${filename}_sub3_name`)?.toLowerCase(): {
|
||||
const id = interaction.options.getNumber(
|
||||
loc_default?.get(`c_${filename}_sub3_opt1_name`) as string,
|
||||
) as number;
|
||||
loc_default!.get(`c_${filename}_sub3_opt1_name`)!,
|
||||
)!;
|
||||
|
||||
const track = queue.removeTrack(id - 1);
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { SlashCommandBuilder } from "@discordjs/builders";
|
||||
import { Player, useMainPlayer } from "discord-player";
|
||||
import { ChatInputCommandInteraction, Client, GuildResolvable } from "discord.js";
|
||||
import { useMainPlayer } from "discord-player";
|
||||
import { ChatInputCommandInteraction, Client } from "discord.js";
|
||||
import { getLocale, getLocalizations } from "../../utils/locales";
|
||||
import { Metadata } from "../../utils/metadata";
|
||||
import { getFilename } from "../../utils/misc";
|
||||
|
||||
export default {
|
||||
|
@ -25,11 +24,11 @@ export default {
|
|||
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
|
||||
const loc = getLocale(client, interaction.locale);
|
||||
|
||||
const player = useMainPlayer() as Player;
|
||||
const queue = player.nodes.create(interaction.guild as GuildResolvable, {
|
||||
const player = useMainPlayer();
|
||||
const queue = player.nodes.create(interaction.guild!, {
|
||||
metadata: {
|
||||
channel: interaction.channel,
|
||||
} as Metadata,
|
||||
},
|
||||
});
|
||||
|
||||
if (!(queue.connection || queue.node.isPlaying())) {
|
||||
|
|
|
@ -1,12 +1,37 @@
|
|||
import { Player, PlayerEvents, useMainPlayer } from "discord-player";
|
||||
import { PlayerEvents, useMainPlayer } from "discord-player";
|
||||
import { Client } from "discord.js";
|
||||
import { readdir } from "fs/promises";
|
||||
import { splitFilenameExtensions } from "../utils/misc";
|
||||
|
||||
/** Load all the events */
|
||||
export default async (client: Client) => {
|
||||
const events_categories = (await readdir(__dirname)).filter(
|
||||
(element) => !element.endsWith(".js") && !element.endsWith(".ts"),
|
||||
);
|
||||
export default async (client: Client, isDev: boolean) => {
|
||||
const events_categories = (await readdir(__dirname, { withFileTypes: true }))
|
||||
.filter((element) => element.isDirectory())
|
||||
.map((element) => element.name);
|
||||
|
||||
const player = useMainPlayer();
|
||||
|
||||
if (isDev) {
|
||||
player.on("debug", async (message) => {
|
||||
console.log(`General player debug event: ${message}`);
|
||||
});
|
||||
|
||||
player.events.on("debug", async (_, message) => {
|
||||
console.log(`Player debug event: ${message}`);
|
||||
});
|
||||
}
|
||||
|
||||
player.events.on("error", (_, error) => {
|
||||
// Emitted when the player queue encounters error
|
||||
console.error(`General player error event: ${error.message}`);
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
player.events.on("playerError", (_, error) => {
|
||||
// Emitted when the audio player errors while streaming audio track
|
||||
console.error(`Player error event: ${error.message}`);
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
events_categories.forEach(async (event_category) => {
|
||||
// Retrieve events
|
||||
|
@ -20,16 +45,12 @@ export default async (client: Client) => {
|
|||
);
|
||||
|
||||
// Remove extension
|
||||
// TODO: use utils functions
|
||||
const event_type_ext = event_file.split(".");
|
||||
const ext = event_type_ext.pop();
|
||||
const { file: event_type, ext } = splitFilenameExtensions(event_file)!;
|
||||
if (!(ext === "js" || ext === "ts")) {
|
||||
throw `Unknown file in ${event_category}: ${event_file}`;
|
||||
}
|
||||
const event_type = event_type_ext.join(".");
|
||||
|
||||
if (event_category == "player") {
|
||||
const player = useMainPlayer() as Player;
|
||||
if (event_category === "player") {
|
||||
if (once) {
|
||||
// eslint-disable-next-line
|
||||
return player.events.once(event_type as keyof PlayerEvents, (...args: any[]) => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Client, EmbedBuilder, GuildMember, Message, TextBasedChannel } from "discord.js";
|
||||
import { Client, EmbedBuilder, Message, TextBasedChannel } from "discord.js";
|
||||
import { getLocale } from "../../utils/locales";
|
||||
import { isImage, userWithNickname } from "../../utils/misc";
|
||||
import { showDate } from "../../utils/time";
|
||||
import { RegexC, RegExpFlags } from "../../utils/regex";
|
||||
|
||||
/** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-messageCreate */
|
||||
export default async (message: Message, client: Client) => {
|
||||
|
@ -24,7 +25,7 @@ export default async (message: Message, client: Client) => {
|
|||
/* Citation */
|
||||
const regex =
|
||||
/https:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19})/g;
|
||||
const urls = message.content.match(new RegExp(regex, "g"));
|
||||
const urls = message.content.match(RegexC(regex, RegExpFlags.Global));
|
||||
|
||||
// Ignore message if there is no URLs
|
||||
if (!urls) {
|
||||
|
@ -42,7 +43,7 @@ export default async (message: Message, client: Client) => {
|
|||
}[] = [],
|
||||
match,
|
||||
) => {
|
||||
const [, guild_id, channel_id, message_id] = new RegExp(regex).exec(
|
||||
const [, guild_id, channel_id, message_id] = RegexC(regex).exec(
|
||||
match,
|
||||
) as RegExpExecArray;
|
||||
|
||||
|
@ -51,10 +52,10 @@ export default async (message: Message, client: Client) => {
|
|||
return data;
|
||||
}
|
||||
|
||||
const channel = message.guild.channels.cache.get(channel_id) as TextBasedChannel;
|
||||
const channel = message.guild.channels.cache.get(channel_id);
|
||||
|
||||
// If channel doesn't exist in the guild and isn't text
|
||||
if (!channel) {
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -65,12 +66,25 @@ export default async (message: Message, client: Client) => {
|
|||
[],
|
||||
)
|
||||
.map(async ({ message_id, channel }) => {
|
||||
const quoted_message = await channel.messages.fetch(message_id).catch(() => undefined);
|
||||
let quoted_message = await channel.messages.fetch(message_id).catch(() => undefined);
|
||||
|
||||
// If it's a reference, we only check for reference once
|
||||
const message_reference = quoted_message?.reference;
|
||||
if (message_reference && message_reference.messageId) {
|
||||
const channel_reference = client.channels.cache.get(message_reference.channelId);
|
||||
if (!channel_reference?.isTextBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
quoted_message = await channel_reference.messages
|
||||
.fetch(message_reference.messageId)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
// If message doesn't exist or empty
|
||||
if (
|
||||
!quoted_message ||
|
||||
(!quoted_message.content && quoted_message.attachments.size == 0)
|
||||
(!quoted_message.content && quoted_message.attachments.size === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -85,97 +99,102 @@ export default async (message: Message, client: Client) => {
|
|||
const loc = getLocale(client, client.config.default_lang);
|
||||
|
||||
// Remove duplicates then map the quoted posts
|
||||
[...new Set(messages)].map((quoted_post) => {
|
||||
const embed = new EmbedBuilder().setColor("#2f3136").setAuthor({
|
||||
name: "Citation",
|
||||
iconURL: quoted_post?.author.displayAvatarURL(),
|
||||
});
|
||||
[...new Set(messages)]
|
||||
.filter((p) => p !== undefined)
|
||||
.map((quoted_post) => {
|
||||
const embed = new EmbedBuilder().setColor("#2f3136").setAuthor({
|
||||
name: "Citation",
|
||||
iconURL: quoted_post.author.displayAvatarURL(),
|
||||
});
|
||||
|
||||
// Handle attachments
|
||||
if (quoted_post?.attachments.size !== 0) {
|
||||
if (
|
||||
quoted_post?.attachments.size === 1 &&
|
||||
isImage(quoted_post.attachments.first()?.name as string)
|
||||
) {
|
||||
// Only contains one image
|
||||
embed.setImage(quoted_post.attachments.first()?.url as string);
|
||||
// Handle attachments
|
||||
if (quoted_post.attachments.size !== 0) {
|
||||
if (quoted_post.attachments.size === 1 && isImage(quoted_post.attachments.first()!.name)) {
|
||||
// Only contains one image
|
||||
embed.setImage(quoted_post.attachments.first()!.url);
|
||||
} else {
|
||||
// Contains more than one image and/or other files
|
||||
let files = "";
|
||||
quoted_post.attachments.forEach((file) => (files += `[${file.name}](${file.url}), `));
|
||||
embed.addFields({
|
||||
// TODO: Don't pluralize when there is only one file.
|
||||
|
||||
// TODO: Locales
|
||||
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/188
|
||||
name: "Fichiers joints",
|
||||
// TODO: Check if don't exceed char limit, if yes, split
|
||||
// files into multiples field.
|
||||
value: `${files.slice(0, -2)}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Description as post content
|
||||
if (quoted_post.content) {
|
||||
// Only if content exists and length > 0
|
||||
embed.setDescription(quoted_post.content);
|
||||
}
|
||||
|
||||
// Footer
|
||||
let footer = `Posté le ${showDate(
|
||||
message.guild?.preferredLocale ?? client.config.default_lang,
|
||||
loc,
|
||||
quoted_post.createdAt,
|
||||
)}`;
|
||||
if (quoted_post.editedAt) {
|
||||
footer += ` et modifié le ${showDate(
|
||||
message.guild?.preferredLocale ?? client.config.default_lang,
|
||||
loc,
|
||||
quoted_post.editedAt,
|
||||
)}`;
|
||||
}
|
||||
|
||||
let author = "Auteur";
|
||||
if (message.author === quoted_post.author) {
|
||||
author += " & Citateur";
|
||||
} else {
|
||||
// Contains more than one image and/or other files
|
||||
let files = "";
|
||||
quoted_post?.attachments.forEach((file) => (files += `[${file.name}](${file.url}), `));
|
||||
embed.addFields({
|
||||
// TODO: Don't pluralize when there is only one file.
|
||||
// TODO: Locales
|
||||
name: "Fichiers joints",
|
||||
// TODO: Check if don't exceed char limit, if yes, split
|
||||
// files into multiples field.
|
||||
value: `${files.slice(0, -2)}.`,
|
||||
footer += `\nCité par ${userWithNickname(message.member!) ?? "?"} le ${showDate(
|
||||
message.guild?.preferredLocale ?? client.config.default_lang,
|
||||
loc,
|
||||
message.createdAt,
|
||||
)}`;
|
||||
}
|
||||
|
||||
embed.setFooter({
|
||||
text: footer,
|
||||
iconURL: message.author.avatarURL() ?? undefined,
|
||||
});
|
||||
|
||||
// Location/author of the quoted post
|
||||
embed.addFields(
|
||||
{
|
||||
name: author,
|
||||
value: `${quoted_post.author}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Message",
|
||||
value: `${quoted_post.channel} - [Lien Message](${quoted_post.url})`,
|
||||
inline: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Delete source message if no content when removing links
|
||||
if (
|
||||
!message.content.replace(RegexC(regex, RegExpFlags.Global), "").trim() &&
|
||||
messages.length === urls.length &&
|
||||
!message.mentions.repliedUser &&
|
||||
message.channel.isSendable()
|
||||
) {
|
||||
message.delete();
|
||||
return message.channel.send({ embeds: [embed] });
|
||||
} else {
|
||||
return message.reply({
|
||||
embeds: [embed],
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Description as post content
|
||||
if (quoted_post?.content) {
|
||||
// Only if content exists and length > 0
|
||||
embed.setDescription(quoted_post?.content);
|
||||
}
|
||||
|
||||
// Footer
|
||||
let footer = `Posté le ${showDate(
|
||||
client.config.default_lang,
|
||||
loc,
|
||||
quoted_post?.createdAt as Date,
|
||||
)}`;
|
||||
if (quoted_post?.editedAt) {
|
||||
footer += ` et modifié le ${showDate(client.config.default_lang, loc, quoted_post.editedAt)}`;
|
||||
}
|
||||
|
||||
let author = "Auteur";
|
||||
if (message.author == quoted_post?.author) {
|
||||
author += " & Citateur";
|
||||
} else {
|
||||
footer += `\nCité par ${userWithNickname(message.member as GuildMember) ?? "?"} le ${showDate(
|
||||
client.config.default_lang,
|
||||
loc,
|
||||
message.createdAt,
|
||||
)}`;
|
||||
}
|
||||
|
||||
embed.setFooter({
|
||||
text: footer,
|
||||
iconURL: message.author.avatarURL() ?? undefined,
|
||||
});
|
||||
|
||||
// Location/author of the quoted post
|
||||
embed.addFields(
|
||||
{
|
||||
name: author,
|
||||
value: `${quoted_post?.author}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Message",
|
||||
value: `${quoted_post?.channel} - [Lien Message](${quoted_post?.url})`,
|
||||
inline: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Delete source message if no content when removing links
|
||||
if (
|
||||
!message.content.replace(new RegExp(regex, "g"), "").trim() &&
|
||||
messages.length === urls.length &&
|
||||
!message.mentions.repliedUser &&
|
||||
message.channel.isSendable()
|
||||
) {
|
||||
message.delete();
|
||||
return message.channel.send({ embeds: [embed] });
|
||||
} else {
|
||||
return message.reply({
|
||||
embeds: [embed],
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
const isDev = process.env.NODE_ENV !== "production";
|
||||
|
||||
/** Load the app */
|
||||
const start_app = () => {
|
||||
import("./load").then((l) => l.run().catch((error) => console.error(error)));
|
||||
import("./load").then((l) => l.run(isDev).catch((error) => console.error(error)));
|
||||
};
|
||||
|
||||
// Load .env if not in prod
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (isDev) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import("dotenv").then((c) => {
|
||||
|
|
11
src/load.ts
11
src/load.ts
|
@ -7,16 +7,21 @@ import loadClient, { quit } from "./utils/client";
|
|||
import { logStart } from "./utils/misc";
|
||||
|
||||
/** Run the bot */
|
||||
export const run = async () => {
|
||||
export const run = async (isDev: boolean) => {
|
||||
console.log("Starting Botanique...");
|
||||
|
||||
// Client Discord.JS
|
||||
const client_name = "Client";
|
||||
await loadClient()
|
||||
await loadClient(isDev)
|
||||
.then(async (client) => {
|
||||
if (isDev) {
|
||||
// Attach debugging listeners
|
||||
client.on("debug", console.log).on("warn", console.warn);
|
||||
}
|
||||
|
||||
// Events Discord.JS and Player
|
||||
const events_name = "Events";
|
||||
await loadEvents(client)
|
||||
await loadEvents(client, isDev)
|
||||
.then(() => console.log(logStart(events_name, true)))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"e_interacreate_no_command": "Sorry, the command probably no longer exists...",
|
||||
"e_interacreate_no_modal": "Sorry, the model no longer exists...",
|
||||
"e_interacreate_no_modal": "Sorry, the template no longer exists...",
|
||||
"e_interacreate_no_button": "Sorry, the button no longer exists...",
|
||||
"e_interacreate_no_autocomplete": "Sorry, no autocomplete is available for this command...",
|
||||
|
||||
"c_ping_name": "Ping",
|
||||
"c_ping_desc": "Pong!",
|
||||
"c_ping1": "Roundtrip latency",
|
||||
"c_ping2": "Websocket heartbeat",
|
||||
"c_ping2": "Web socket heartbeat",
|
||||
|
||||
"c_help_name": "Help",
|
||||
"c_help_desc": "Informations about commands",
|
||||
"c_help_desc": "Information about commands",
|
||||
"c_help_opt1_name": "command",
|
||||
"c_help_opt1_desc": "Command wanted in depth.",
|
||||
"c_help1": "List of categories and associated commands",
|
||||
|
@ -17,7 +18,7 @@
|
|||
"c_help3": "Can't find :",
|
||||
|
||||
"c_archive_name": "clean",
|
||||
"c_archive_desc": "Clean category for new year",
|
||||
"c_archive_desc": "Clean category for the new year",
|
||||
"c_archive_opt1_name": "category",
|
||||
"c_archive_opt1_desc": "Name of the category to be cleaned",
|
||||
"c_archive1": "List of categories subject to cleaning",
|
||||
|
@ -37,7 +38,7 @@
|
|||
"c_prep3": "Unable to find/clean the channel:",
|
||||
"c_prep4": "Lists of prepared channels `",
|
||||
"c_prep5": "created",
|
||||
"c_prep6": "No preparation required",
|
||||
"c_prep6": "No preparation is required",
|
||||
|
||||
"u_time_at": "at",
|
||||
|
||||
|
@ -46,11 +47,11 @@
|
|||
"c_reminder_sub1_name": "new",
|
||||
"c_reminder_sub1_desc": "Sets up a reminder",
|
||||
"c_reminder_sub1_opt1_name": "time",
|
||||
"c_reminder_sub1_opt1_desc": "Desired time before the reminder, append an @ to activate the mention or a p to send in DM",
|
||||
"c_reminder_sub1_opt1_desc": "Desired time before the reminder, append an @ to activate the mention or a p to send in a DM",
|
||||
"c_reminder_sub1_opt2_name": "message",
|
||||
"c_reminder_sub1_opt2_desc": "Reminder message",
|
||||
"c_reminder_sub2_name": "list",
|
||||
"c_reminder_sub2_desc": "Displays the list of reminders of a user",
|
||||
"c_reminder_sub2_desc": "Displays the list of reminders for a user",
|
||||
"c_reminder_sub2_opt1_name": "user",
|
||||
"c_reminder_sub2_opt1_desc": "Displays the list of this user",
|
||||
"c_reminder_sub2_opt2_name": "page",
|
||||
|
@ -61,7 +62,7 @@
|
|||
"c_reminder_sub3_opt1_desc": "Reminder to be deleted",
|
||||
"c_reminder1": "A reminder has been set up for in",
|
||||
"c_reminder2": "The ID entered is not valid.",
|
||||
"c_reminder3": "Reminder not found, not on the right guild or not belonging to you.",
|
||||
"c_reminder3": "Reminder not found, not in the right guild, or not belonging to you.",
|
||||
"c_reminder4": "Unknown user.",
|
||||
"c_reminder5": "Reminders of",
|
||||
"c_reminder6": "Page",
|
||||
|
@ -76,18 +77,25 @@
|
|||
"c_reminder15": "Message sent in DM because you have left",
|
||||
"c_reminder16": "Message sent in DM because the Discord guild is no longer available.",
|
||||
"c_reminder17": "Message from",
|
||||
"c_reminder18": "Invalid time, try again.",
|
||||
|
||||
"c_play_name": "play",
|
||||
"c_play_desc": "Plays a song/playlist, no query displays the now playing song",
|
||||
"c_play_sub1_name": "add",
|
||||
"c_play_sub1_desc": "Adds song/playlist to the queue",
|
||||
"c_play_sub2_name": "now",
|
||||
"c_play_sub2_desc": "Adds the song/playlist to the beginning of the queue",
|
||||
"c_play_opt1_name": "query",
|
||||
"c_play_opt1_desc": "What you want to listen to",
|
||||
"c_play1": "You're not in any vocal channel.",
|
||||
"c_play1": "You're not on any vocal channels.",
|
||||
"c_play2": "You are in the wrong voice channel, I am in",
|
||||
"c_play3": "Unable to join the voice channel.",
|
||||
"c_play4": "not found",
|
||||
"c_play5": "added to the queue",
|
||||
"c_play6": "The bot is not playing anything right now.",
|
||||
"c_play7": "Currently playing",
|
||||
"c_play8": "Asked by",
|
||||
"c_play9": "No results were found",
|
||||
|
||||
"c_stop_name": "stop",
|
||||
"c_stop_desc": "Stop the music",
|
||||
|
@ -134,13 +142,17 @@
|
|||
|
||||
"c_lyrics_name": "lyrics",
|
||||
"c_lyrics_desc": "Displays the lyrics of a song",
|
||||
"c_lyrics_sub1_name": "normal",
|
||||
"c_lyrics_sub1_desc": "Lyrics search",
|
||||
"c_lyrics_sub2_name": "romanized",
|
||||
"c_lyrics_sub2_desc": "Search for romanized lyrics (e.g., hangul → Latin)",
|
||||
"c_lyrics_sub3_name": "synced",
|
||||
"c_lyrics_sub3_desc": "Synchronized lyrics search (updates in live)",
|
||||
"c_lyrics_opt1_name": "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_lyrics3": "Impossible to find synchronized lyrics for",
|
||||
"c_lyrics3": "Unable to find synchronized lyrics for",
|
||||
"c_lyrics4": "It's karaoke time!",
|
||||
|
||||
"c_repeat_name": "repeat",
|
||||
|
@ -158,5 +170,8 @@
|
|||
"c_repeat3": "Repeating the queue",
|
||||
"c_repeat4": "Automatic playback",
|
||||
"c_repeat5": "Repeating the song",
|
||||
"c_repeat6": "enabled"
|
||||
"c_repeat6": "enabled",
|
||||
|
||||
"e_trackstart1": "Asked by",
|
||||
"e_trackstart2": "Duration :"
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
"e_interacreate_no_command": "Désolé, la commande n'existe plus...",
|
||||
"e_interacreate_no_modal": "Désolé, le modèle n'existe plus...",
|
||||
"e_interacreate_no_button": "Désolé, le bouton n'existe plus...",
|
||||
"e_interacreate_no_autocomplete": "Désolé, pas d'autocomplétion existe pour cette commande...",
|
||||
"e_interacreate_no_autocomplete": "Désolé, pas d'autocomplétion n'existe pour cette commande...",
|
||||
|
||||
"c_ping_name": "Ping",
|
||||
"c_ping_desc": "Pong!",
|
||||
"c_ping1": "Latence totale",
|
||||
"c_ping2": "Latence du Websocket",
|
||||
"c_ping2": "Latence du Web socket",
|
||||
|
||||
"c_help_name": "Aide",
|
||||
"c_help_desc": "Informations sur les commandes",
|
||||
"c_help_opt1_name": "commande",
|
||||
"c_help_opt1_desc": "Commande voulu en détail",
|
||||
"c_help_opt1_desc": "Commande voulue en détail",
|
||||
"c_help1": "Liste des catégories et des commandes associées",
|
||||
"c_help2": "`/help <commande>` pour obtenir plus d'informations sur une commande.",
|
||||
"c_help3": "Impossible de trouver :",
|
||||
|
@ -21,7 +21,7 @@
|
|||
"c_archive_desc": "Nettoyage pour le passage à niveau",
|
||||
"c_archive_opt1_name": "catégorie",
|
||||
"c_archive_opt1_desc": "Nom de la catégorie à nettoyer",
|
||||
"c_archive1": "Liste des catégories soumis au nettoyage",
|
||||
"c_archive1": "Liste des catégories soumises au nettoyage",
|
||||
"c_archive2": "`L1`, `L2`, `L3`, `M1`, `M2`",
|
||||
"c_archive3": "Impossible de trouver/nettoyer le salon :",
|
||||
"c_archive4": "Liste des salons archivés de la catégorie",
|
||||
|
@ -33,10 +33,10 @@
|
|||
"c_prep_desc": "Préparation des salons généraux pour la nouvelle année",
|
||||
"c_prep_opt1_name": "année",
|
||||
"c_prep_opt1_desc": "Nom de l'année à préparer",
|
||||
"c_prep1": "Liste des catégories soumis à la préparation",
|
||||
"c_prep1": "Liste des catégories soumises à la préparation",
|
||||
"c_prep2": "`L1`, `L2`, `L3`, `M1`, `M2`",
|
||||
"c_prep3": "Impossible de trouver/nettoyer le salon :",
|
||||
"c_prep4": "Listes des Salons préparés `",
|
||||
"c_prep4": "Listes des salons préparés `",
|
||||
"c_prep5": "créé",
|
||||
"c_prep6": "Pas besoin de préparation",
|
||||
|
||||
|
@ -45,11 +45,11 @@
|
|||
"c_reminder_name": "rappel",
|
||||
"c_reminder_desc": "Commande relative aux rappels",
|
||||
"c_reminder_sub1_name": "nouveau",
|
||||
"c_reminder_sub1_desc": "Met en place un rappel",
|
||||
"c_reminder_sub1_desc": "Mets en place un rappel",
|
||||
"c_reminder_sub1_opt1_name": "temps",
|
||||
"c_reminder_sub1_opt1_desc": "Temps désiré avant le rappel, accolez un @ pour activer la mention ou un p pour envoyer en DM",
|
||||
"c_reminder_sub1_opt2_name": "message",
|
||||
"c_reminder_sub1_opt2_desc": "Message du rappel",
|
||||
"c_reminder_sub1_opt2_desc": "Message de rappel",
|
||||
"c_reminder_sub2_name": "liste",
|
||||
"c_reminder_sub2_desc": "Affiche la liste des rappels d'un utilisateur",
|
||||
"c_reminder_sub2_opt1_name": "utilisateur",
|
||||
|
@ -77,9 +77,14 @@
|
|||
"c_reminder15": "Message envoyé en DM car vous avez quitté",
|
||||
"c_reminder16": "Message envoyé en DM car le serveur Discord n'est plus disponible.",
|
||||
"c_reminder17": "Message d'il y a",
|
||||
"c_reminder18": "Temps invalide, réessayez.",
|
||||
|
||||
"c_play_name": "play",
|
||||
"c_play_desc": "Joue une chanson/playlist, pas de requête affiche la chanson en cours actuellement",
|
||||
"c_play_sub1_name": "ajouter",
|
||||
"c_play_sub1_desc": "Ajoute la chanson/playlist à la file d'attente",
|
||||
"c_play_sub2_name": "maintenant",
|
||||
"c_play_sub2_desc": "Ajoute la chanson/playlist au début de la file",
|
||||
"c_play_opt1_name": "requête",
|
||||
"c_play_opt1_desc": "Ce que vous voulez écouter",
|
||||
"c_play1": "Tu n'es dans aucun salon vocal.",
|
||||
|
@ -114,7 +119,7 @@
|
|||
"c_queue_sub3_name": "retire",
|
||||
"c_queue_sub3_desc": "Retire une chanson de la file d'attente",
|
||||
"c_queue_sub3_opt1_name": "id",
|
||||
"c_queue_sub3_opt1_desc": "ID de la chanson a retirer",
|
||||
"c_queue_sub3_opt1_desc": "ID de la chanson à retirer",
|
||||
"c_queue1": "File d'attente",
|
||||
"c_queue2": "La liste est vide.",
|
||||
"c_queue3": "Liste d'attente mélangée",
|
||||
|
@ -140,9 +145,9 @@
|
|||
"c_lyrics_sub1_name": "normal",
|
||||
"c_lyrics_sub1_desc": "Recherche de paroles",
|
||||
"c_lyrics_sub2_name": "romanized",
|
||||
"c_lyrics_sub2_desc": "Recherche de paroles romanisée (ex: hangul -> latin)",
|
||||
"c_lyrics_sub2_desc": "Recherche de paroles romanisées (ex: hangul → latin)",
|
||||
"c_lyrics_sub3_name": "synced",
|
||||
"c_lyrics_sub3_desc": "Recherche de paroles synchronisée (se met à 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_opt1_name": "chanson",
|
||||
"c_lyrics_opt1_desc": "Chanson recherchée",
|
||||
"c_lyrics1": "Le bot ne joue rien en ce moment et aucune chanson n'est renseignée.",
|
||||
|
@ -161,7 +166,7 @@
|
|||
"c_repeat_sub4_name": "autoplay",
|
||||
"c_repeat_sub4_desc": "Active la lecture automatique",
|
||||
"c_repeat1": "Le bot ne joue rien en ce moment.",
|
||||
"c_repeat2": "Répétition désactivé",
|
||||
"c_repeat2": "Répétition désactivée",
|
||||
"c_repeat3": "Répétition de la file d'attente",
|
||||
"c_repeat4": "Lecture automatique",
|
||||
"c_repeat5": "Répétition de la chanson",
|
||||
|
|
|
@ -7,7 +7,7 @@ export default {
|
|||
name: getFilename(__filename),
|
||||
},
|
||||
interaction: async (interaction: ModalSubmitInteraction, client: Client) =>
|
||||
newReminder(client, interaction.fields.fields.get("reminderGUI-time")?.value as string, {
|
||||
newReminder(client, interaction.fields.fields.get("reminderGUI-time")!.value, {
|
||||
locale: interaction.locale,
|
||||
message: interaction.fields.fields.get("reminderGUI-message")?.value ?? null,
|
||||
createdAt: interaction.createdAt.getTime(),
|
||||
|
|
28
src/tests/modules/string.test.ts
Normal file
28
src/tests/modules/string.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import "../../modules/string";
|
||||
|
||||
describe("Capitalize", () => {
|
||||
{
|
||||
const name = "test";
|
||||
test(name, () => {
|
||||
expect(name.capitalize()).toBe("Test");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "MACHIN";
|
||||
test(name, () => {
|
||||
expect(name.capitalize()).toBe("MACHIN");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "tRUC";
|
||||
test(name, () => {
|
||||
expect(name.capitalize()).toBe("TRUC");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "Super";
|
||||
test(name, () => {
|
||||
expect(name.capitalize()).toBe("Super");
|
||||
});
|
||||
}
|
||||
});
|
151
src/tests/utils/misc.test.ts
Normal file
151
src/tests/utils/misc.test.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import {
|
||||
cleanCodeBlock,
|
||||
emojiPng,
|
||||
isImage,
|
||||
removeExtension,
|
||||
splitFilenameExtensions,
|
||||
} from "../../utils/misc";
|
||||
|
||||
describe("Filename splitter", () => {
|
||||
{
|
||||
const name = "test.js";
|
||||
test(name, () => {
|
||||
expect(splitFilenameExtensions(name)).toStrictEqual({ file: "test", ext: "js" });
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = ".env";
|
||||
test(name, () => {
|
||||
expect(splitFilenameExtensions(name)).toStrictEqual({ file: ".env", ext: undefined });
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = ".env.test";
|
||||
test(name, () => {
|
||||
expect(splitFilenameExtensions(name)).toStrictEqual({
|
||||
file: ".env",
|
||||
ext: "test",
|
||||
});
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "file.test.js";
|
||||
test(name, () => {
|
||||
expect(splitFilenameExtensions(name)).toStrictEqual({ file: "file.test", ext: "js" });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Extension remover", () => {
|
||||
{
|
||||
const name = "test.js";
|
||||
test(name, () => {
|
||||
expect(removeExtension(name)).toBe("test");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = ".env";
|
||||
test(name, () => {
|
||||
expect(removeExtension(name)).toBe(".env");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = ".env.test";
|
||||
test(name, () => {
|
||||
expect(removeExtension(name)).toBe(".env");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "file.test.js";
|
||||
test(name, () => {
|
||||
expect(removeExtension(name)).toBe("file.test");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Image checker", () => {
|
||||
{
|
||||
const name = "image.Png";
|
||||
test(name, () => {
|
||||
expect(isImage(name)).toBe(true);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "image.jpeg";
|
||||
test(name, () => {
|
||||
expect(isImage(name)).toBe(true);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "image.wav";
|
||||
test(name, () => {
|
||||
expect(isImage(name)).toBe(false);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "image.jpg";
|
||||
test(name, () => {
|
||||
expect(isImage(name)).toBe(true);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "image.webP";
|
||||
test(name, () => {
|
||||
expect(isImage(name)).toBe(true);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "image.GIF";
|
||||
test(name, () => {
|
||||
expect(isImage(name)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Code block cleaner", () => {
|
||||
{
|
||||
const name = "salut";
|
||||
test(name, () => {
|
||||
expect(cleanCodeBlock(name)).toBe("`salut`");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "<@158260864623968257> ça va ?";
|
||||
test(name, () => {
|
||||
expect(cleanCodeBlock(name)).toBe("<@158260864623968257>` ça va ?`");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "t'as vu la vidéo ? https://youtu.be/dQw4w9WgXcQ";
|
||||
test(name, () => {
|
||||
expect(cleanCodeBlock(name)).toBe("`t'as vu la vidéo ? `https://youtu.be/dQw4w9WgXcQ");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "t'as vu la vidéo ? https://youtu.be/dQw4w9WgXcQ elle est cool en vrai tqt";
|
||||
test(name, () => {
|
||||
expect(cleanCodeBlock(name)).toBe(
|
||||
"`t'as vu la vidéo ? `https://youtu.be/dQw4w9WgXcQ` elle est cool en vrai tqt`",
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Emoji to link", () => {
|
||||
{
|
||||
const name = "☺️";
|
||||
test(name, () => {
|
||||
expect(emojiPng(name)).toBe(
|
||||
"https://cdn.jsdelivr.net/gh/twitter/twemoji/assets/72x72/263a.png",
|
||||
);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "🍕";
|
||||
test(name, () => {
|
||||
expect(emojiPng(name)).toBe(
|
||||
"https://cdn.jsdelivr.net/gh/twitter/twemoji/assets/72x72/1f355.png",
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
26
src/tests/utils/regex.test.ts
Normal file
26
src/tests/utils/regex.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { RegexC, RegExpFlags } from "../../utils/regex";
|
||||
|
||||
describe("Regex flags", () => {
|
||||
test("One parameter", () => {
|
||||
const regex = RegexC("", RegExpFlags.Global);
|
||||
expect(regex.global).toBeTruthy();
|
||||
});
|
||||
|
||||
test("All parameters", () => {
|
||||
const regex = RegexC(
|
||||
"",
|
||||
RegExpFlags.Global |
|
||||
RegExpFlags.MultiLine |
|
||||
RegExpFlags.Insensitive |
|
||||
RegExpFlags.Sticky |
|
||||
RegExpFlags.Unicode |
|
||||
RegExpFlags.SingleLine,
|
||||
);
|
||||
expect(regex.global).toBeTruthy();
|
||||
expect(regex.multiline).toBeTruthy();
|
||||
expect(regex.ignoreCase).toBeTruthy();
|
||||
expect(regex.sticky).toBeTruthy();
|
||||
expect(regex.unicode).toBeTruthy();
|
||||
expect(regex.dotAll).toBeTruthy();
|
||||
});
|
||||
});
|
28
src/tests/utils/reminder.test.ts
Normal file
28
src/tests/utils/reminder.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { OptionReminder, splitTime } from "../../utils/reminder";
|
||||
|
||||
describe("Time splitter", () => {
|
||||
{
|
||||
const name = "";
|
||||
test(name, () => {
|
||||
expect(splitTime(name)).toStrictEqual({ option: OptionReminder.Nothing, time: "" });
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "2m@p";
|
||||
test(name, () => {
|
||||
expect(splitTime(name)).toStrictEqual({ option: OptionReminder.DirectMessage, time: "2m" });
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "41@";
|
||||
test(name, () => {
|
||||
expect(splitTime(name)).toStrictEqual({ option: OptionReminder.Mention, time: "41" });
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "0P";
|
||||
test(name, () => {
|
||||
expect(splitTime(name)).toStrictEqual({ option: OptionReminder.DirectMessage, time: "0" });
|
||||
});
|
||||
}
|
||||
});
|
86
src/tests/utils/time.test.ts
Normal file
86
src/tests/utils/time.test.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { nextTimeUnit, showDate, strToSeconds, TimeSecond } from "../../utils/time";
|
||||
|
||||
describe("Date with correct timezone", () => {
|
||||
const map = new Map([["u_time_at", "@"]]);
|
||||
const date = new Date(1727434767686);
|
||||
{
|
||||
const name = "fr";
|
||||
test(name, () => {
|
||||
expect(showDate(name, map, date)).toBe("27/09/2024 @ 12:59:27");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "en-US";
|
||||
test(name, () => {
|
||||
expect(showDate(name, map, date)).toBe("9/27/24, @ 1:59:27");
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "unknown";
|
||||
// Depends on the system
|
||||
// The important is that the date is in the correct timezone (UTC)
|
||||
test(name, () => {
|
||||
expect(["27/09/2024 @ 10:59:27", "9/27/24, @ 10:59:27"]).toContain(showDate(name, map, date));
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "zh-CN";
|
||||
test(name, () => {
|
||||
expect(showDate(name, map, date)).toBe("2024/9/27 @ 18:59:27");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("String time to seconds", () => {
|
||||
{
|
||||
const name = "10m30";
|
||||
test(name, () => {
|
||||
expect(strToSeconds(name)).toBe(630);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "12h30";
|
||||
test(name, () => {
|
||||
expect(strToSeconds(name)).toBe(45000);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "12s30";
|
||||
test(name, () => {
|
||||
expect(strToSeconds(name)).toBe(42);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = "1w30h20";
|
||||
test(name, () => {
|
||||
expect(strToSeconds(name)).toBe(714000);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Next time unit", () => {
|
||||
{
|
||||
const name = TimeSecond.Minute;
|
||||
test(name.toString(), () => {
|
||||
expect(nextTimeUnit(name)).toBe(TimeSecond.Second);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = TimeSecond.Hour;
|
||||
test(name.toString(), () => {
|
||||
expect(nextTimeUnit(name)).toBe(TimeSecond.Minute);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = TimeSecond.Second;
|
||||
test(name.toString(), () => {
|
||||
expect(nextTimeUnit(name)).toBe(TimeSecond.Second);
|
||||
});
|
||||
}
|
||||
{
|
||||
const name = TimeSecond.Year;
|
||||
test(name.toString(), () => {
|
||||
expect(nextTimeUnit(name)).toBe(TimeSecond.Week);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,13 +1,14 @@
|
|||
import { Player } from "discord-player";
|
||||
import { Client, Collection, GatewayIntentBits } from "discord.js";
|
||||
import { ActivityType, Client, Collection, GatewayIntentBits } from "discord.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { Database } from "sqlite3";
|
||||
import "../modules/client";
|
||||
import { loadLocales } from "./locales";
|
||||
import { YoutubeiExtractor } from "discord-player-youtubei";
|
||||
|
||||
/** Creation of the client and definition of its properties */
|
||||
export default async () => {
|
||||
export default async (isDev: boolean) => {
|
||||
const activities = isDev ? [] : [{ name: "/help", type: ActivityType.Watching }];
|
||||
|
||||
const client: Client = new Client({
|
||||
shards: "auto",
|
||||
intents: [
|
||||
|
@ -16,6 +17,9 @@ export default async () => {
|
|||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates,
|
||||
],
|
||||
presence: {
|
||||
activities,
|
||||
},
|
||||
});
|
||||
|
||||
client.config = {
|
||||
|
@ -46,8 +50,7 @@ export default async () => {
|
|||
quality: "highestaudio",
|
||||
},
|
||||
});
|
||||
await player.extractors.loadDefault((ext) => ext !== "YouTubeExtractor");
|
||||
await player.extractors.register(YoutubeiExtractor, {});
|
||||
await player.extractors.loadDefault();
|
||||
|
||||
console.log("Translations progression :");
|
||||
client.locales = await loadLocales(client.config.default_lang);
|
||||
|
|
|
@ -28,27 +28,41 @@ export const getFilename = (path: string) => {
|
|||
return removeExtension(filename_with_ext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Split a filename and his extension
|
||||
* @param filename string of the filename
|
||||
* @returns Object with filename and extension splitted
|
||||
*/
|
||||
export const splitFilenameExtensions = (filename: string) => {
|
||||
if (filename.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if the filename starts with a dot and has no other dots
|
||||
if (filename.startsWith(".") && filename.indexOf(".", 1) === -1) {
|
||||
return { file: filename, ext: undefined };
|
||||
}
|
||||
|
||||
const lastDotIndex = filename.lastIndexOf(".");
|
||||
|
||||
// If there's no dot or the dot is at the start, treat the whole string as the filename
|
||||
if (lastDotIndex <= 0) {
|
||||
return { file: filename, ext: undefined };
|
||||
}
|
||||
|
||||
const file = filename.slice(0, lastDotIndex);
|
||||
const ext = filename.slice(lastDotIndex + 1);
|
||||
|
||||
return { file, ext };
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove extension from a filename
|
||||
* @param filename string of the filename with an extension
|
||||
* @returns string of the filename without an extension
|
||||
*/
|
||||
export const removeExtension = (filename: string) => {
|
||||
const array = filename.split(".");
|
||||
array.pop();
|
||||
|
||||
return array.join(".");
|
||||
};
|
||||
|
||||
/**
|
||||
* Get extension from a filename
|
||||
* @param filename string of the filename
|
||||
* @returns string of the extension if it exists
|
||||
*/
|
||||
export const getExtension = (filename: string) => {
|
||||
const array = filename.split(".");
|
||||
|
||||
return array.pop();
|
||||
return splitFilenameExtensions(filename)!.file;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -57,7 +71,11 @@ export const getExtension = (filename: string) => {
|
|||
* @returns true is file is a media
|
||||
*/
|
||||
export const isImage = (filename: string) => {
|
||||
return Boolean(getExtension(filename)?.match(/jpg|jpeg|png|webp|gif/));
|
||||
return Boolean(
|
||||
splitFilenameExtensions(filename)
|
||||
?.ext?.toLowerCase()
|
||||
.match(/jpg|jpeg|png|webp|gif/),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -90,15 +108,16 @@ export const cleanCodeBlock = (text: string) => {
|
|||
});
|
||||
|
||||
// Keep links
|
||||
// Reference: https://stackoverflow.com/a/3809435/15436737
|
||||
text = text.replace(
|
||||
/(http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)/g,
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_+.~#?&//=]*/g,
|
||||
function (url: string) {
|
||||
return `\`${url}\``;
|
||||
},
|
||||
);
|
||||
|
||||
// Fix issues
|
||||
text = text.replace("``", "");
|
||||
text = text.replaceAll("``", "");
|
||||
|
||||
return text;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { EmbedBuilder } from "@discordjs/builders";
|
||||
import { GuildQueue, QueueRepeatMode, Track } from "discord-player";
|
||||
import { GuildQueue, QueueRepeatMode } from "discord-player";
|
||||
import { Client } from "discord.js";
|
||||
import { getLocale } from "./locales";
|
||||
|
||||
|
@ -14,7 +14,7 @@ export const embedListQueue = (
|
|||
const tracks = queue.tracks.toArray();
|
||||
|
||||
// Add the current song at the top of the list
|
||||
tracks.unshift(queue.history.currentTrack as Track);
|
||||
tracks.unshift(queue.history.currentTrack!);
|
||||
|
||||
// Limit of discord is 25
|
||||
const limit_fields = 25;
|
||||
|
@ -25,10 +25,10 @@ export const embedListQueue = (
|
|||
embed.setFooter({ text: `${printRepeatMode(queue.repeatMode, loc)}` });
|
||||
|
||||
tracks.slice((page - 1) * limit_fields, page * limit_fields).forEach((t, idx) => {
|
||||
const now_playing = idx == 0 && page == 1;
|
||||
const now_playing = idx === 0 && page === 1;
|
||||
const name = now_playing
|
||||
? loc.get("c_queue10")
|
||||
: (idx == 1 && page == 1) || (idx == 0 && page > 1)
|
||||
: (idx === 1 && page === 1) || (idx === 0 && page > 1)
|
||||
? loc.get("c_queue11")
|
||||
: "\u200b";
|
||||
const idx_track = now_playing ? "" : `${idx + limit_fields * (page - 1)}. `;
|
||||
|
|
30
src/utils/regex.ts
Normal file
30
src/utils/regex.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
export enum RegExpFlags {
|
||||
// Global
|
||||
Global = 1 << 0,
|
||||
// Multi Line
|
||||
MultiLine = 1 << 1,
|
||||
// Ignore Case
|
||||
Insensitive = 1 << 2,
|
||||
// Sticky
|
||||
Sticky = 1 << 3,
|
||||
// Unicode
|
||||
Unicode = 1 << 4,
|
||||
// Dot All
|
||||
SingleLine = 1 << 6,
|
||||
}
|
||||
|
||||
const flagsToString = (flags: number) => {
|
||||
let result = "";
|
||||
|
||||
if (flags & RegExpFlags.Global) result += "g";
|
||||
if (flags & RegExpFlags.MultiLine) result += "m";
|
||||
if (flags & RegExpFlags.Insensitive) result += "i";
|
||||
if (flags & RegExpFlags.Sticky) result += "y";
|
||||
if (flags & RegExpFlags.Unicode) result += "u";
|
||||
if (flags & RegExpFlags.SingleLine) result += "s";
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const RegexC = (pattern: RegExp | string, flags: number = 0) =>
|
||||
new RegExp(pattern, flagsToString(flags));
|
|
@ -2,6 +2,7 @@ import { Client, Colors, EmbedBuilder, User } from "discord.js";
|
|||
import { getLocale } from "./locales";
|
||||
import { cleanCodeBlock } from "./misc";
|
||||
import { showDate, strToSeconds, timeDeltaToString } from "./time";
|
||||
import { RegexC, RegExpFlags } from "./regex";
|
||||
|
||||
/**
|
||||
* Option possible for reminders
|
||||
|
@ -45,14 +46,26 @@ export type dbReminder = {
|
|||
* @param time raw text from user
|
||||
* @returns An object with the time and the option
|
||||
*/
|
||||
const splitTime = (time: string) => {
|
||||
if (time?.endsWith("@")) {
|
||||
return { time: time.slice(0, -1), option: OptionReminder.Mention };
|
||||
} else if (time?.toLowerCase().endsWith("p")) {
|
||||
return { time: time.slice(0, -1), option: OptionReminder.DirectMessage };
|
||||
}
|
||||
export const splitTime = (time: string) => {
|
||||
const mapping = {
|
||||
[OptionReminder.DirectMessage]: "p",
|
||||
[OptionReminder.Mention]: "@",
|
||||
};
|
||||
|
||||
return { time: time, option: OptionReminder.Nothing };
|
||||
const trimmed = time.replaceAll(
|
||||
RegexC(Object.values(mapping).join("|"), RegExpFlags.Global | RegExpFlags.Insensitive),
|
||||
"",
|
||||
);
|
||||
|
||||
// Depending of the last character of the string
|
||||
switch (time.toLowerCase().slice(-1)) {
|
||||
case mapping[OptionReminder.Mention]:
|
||||
return { time: trimmed, option: OptionReminder.Mention };
|
||||
case mapping[OptionReminder.DirectMessage]:
|
||||
return { time: trimmed, option: OptionReminder.DirectMessage };
|
||||
default:
|
||||
return { time: time, option: OptionReminder.Nothing };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -64,8 +77,15 @@ const splitTime = (time: string) => {
|
|||
*/
|
||||
export const newReminder = async (client: Client, time: string, info: infoReminder) =>
|
||||
new Promise((ok, ko) => {
|
||||
const loc = getLocale(client, info.locale);
|
||||
|
||||
const data = splitTime(time);
|
||||
const timeout = strToSeconds(data.time);
|
||||
if (timeout < 0) {
|
||||
ko(loc.get("c_reminder18"));
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeoutReminder(client, info, data.option, timeout);
|
||||
|
||||
// Add the remind to the db
|
||||
|
@ -90,7 +110,6 @@ export const newReminder = async (client: Client, time: string, info: infoRemind
|
|||
}
|
||||
|
||||
// Send confirmation to user
|
||||
const loc = getLocale(client, info.locale);
|
||||
ok(`${loc.get("c_reminder1")} ${data.time}.`);
|
||||
},
|
||||
);
|
||||
|
@ -160,7 +179,7 @@ export const sendReminder = (client: Client, info: infoReminder, option: OptionR
|
|||
}
|
||||
}
|
||||
|
||||
if (option == OptionReminder.DirectMessage || !channelOk || !guildOk) {
|
||||
if (option === OptionReminder.DirectMessage || !channelOk || !guildOk) {
|
||||
// Direct message
|
||||
const user = client.users.cache.get(info.userId);
|
||||
if (user !== undefined) {
|
||||
|
@ -176,7 +195,7 @@ export const sendReminder = (client: Client, info: infoReminder, option: OptionR
|
|||
});
|
||||
|
||||
// Mention everybody if needed
|
||||
if (option == OptionReminder.Mention) {
|
||||
if (option === OptionReminder.Mention) {
|
||||
(info.message?.match(/<@\d+>/g) ?? []).forEach((mention) => {
|
||||
content += " " + mention;
|
||||
});
|
||||
|
@ -202,8 +221,19 @@ export const setTimeoutReminder = (
|
|||
option: OptionReminder,
|
||||
timeout: number,
|
||||
) => {
|
||||
return Number(
|
||||
setTimeout(() => {
|
||||
const setChunkedTimeout = (remainingTime: number) => {
|
||||
// Maximum for setTimeout is Int32
|
||||
if (remainingTime > 2147483647) {
|
||||
// Schedule a 24-hour delay (24 * 60 * 60 * 1000), then check again
|
||||
const dayChunk = 86400000;
|
||||
|
||||
return setTimeout(() => {
|
||||
setChunkedTimeout(remainingTime - dayChunk);
|
||||
}, dayChunk);
|
||||
}
|
||||
|
||||
// Final timeout when remaining time is within limit
|
||||
return setTimeout(() => {
|
||||
deleteReminder(client, String(info.createdAt), info.userId).then((val) => {
|
||||
if (val != true) {
|
||||
throw val;
|
||||
|
@ -211,8 +241,11 @@ export const setTimeoutReminder = (
|
|||
|
||||
sendReminder(client, info, option);
|
||||
});
|
||||
}, timeout * 1000),
|
||||
);
|
||||
}, remainingTime);
|
||||
};
|
||||
|
||||
// Convert to milliseconds
|
||||
return Number(setChunkedTimeout(timeout * 1000));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import moment from "moment-timezone";
|
||||
import { RegexC, RegExpFlags } from "./regex";
|
||||
|
||||
/**
|
||||
* Parsed string adapted with TZ (locales) and format for the specified lang
|
||||
* @param tz Lang
|
||||
|
@ -6,10 +9,20 @@
|
|||
* @returns String
|
||||
*/
|
||||
export const showDate = (tz: string, locale: Map<string, unknown>, date: Date) => {
|
||||
return date.toLocaleString(tz).replace(" ", ` ${locale.get("u_time_at")} `);
|
||||
const localeInfo = new Intl.Locale(tz);
|
||||
const intlTimezone = moment.tz.zonesForCountry(localeInfo.region ?? localeInfo.baseName);
|
||||
const formattedDate = new Intl.DateTimeFormat(tz, {
|
||||
timeZone: intlTimezone ? intlTimezone[0] : "Factory",
|
||||
dateStyle: "short",
|
||||
timeStyle: "medium",
|
||||
})
|
||||
.format(date)
|
||||
.split(" ");
|
||||
|
||||
return `${formattedDate[0]} ${locale.get("u_time_at")} ${formattedDate[1]}`;
|
||||
};
|
||||
|
||||
enum TimeSecond {
|
||||
export enum TimeSecond {
|
||||
Year = 31536000,
|
||||
Week = 604800,
|
||||
Day = 86400,
|
||||
|
@ -18,29 +31,63 @@ enum TimeSecond {
|
|||
Second = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next time unit. For example the next unit after Hour is Minute
|
||||
* @param currentUnit Current time unit
|
||||
* @returns The next time unit
|
||||
*/
|
||||
export const nextTimeUnit = (currentUnit: number) => {
|
||||
const units = Object.values(TimeSecond) as number[];
|
||||
|
||||
const index = units.indexOf(currentUnit);
|
||||
return units[index + 1] || TimeSecond.Second;
|
||||
};
|
||||
|
||||
/**
|
||||
* Take a cooldown, for example 2min and transform it to seconds, here: 120s
|
||||
* @param time time in human format
|
||||
* @returns time in seconds
|
||||
*/
|
||||
export const strToSeconds = (time: string) => {
|
||||
const regex = new RegExp(
|
||||
`(?<${TimeSecond[TimeSecond.Year]}>[0-9]+(?=[y|a]))|(?<${
|
||||
TimeSecond[TimeSecond.Week]
|
||||
}>[0-9]+(?=[w]))|(?<${TimeSecond[TimeSecond.Day]}>[0-9]+(?=[d|j]))|(?<${
|
||||
TimeSecond[TimeSecond.Hour]
|
||||
}>[0-9]+(?=[h]))|(?<${TimeSecond[TimeSecond.Minute]}>[0-9]+(?=[m]))|(?<${
|
||||
TimeSecond[TimeSecond.Second]
|
||||
}>[0-9]+(?=[s]?))`,
|
||||
if (time.length > 15) {
|
||||
// 15 is a magic value as it's weird to have time this long
|
||||
return -1;
|
||||
}
|
||||
|
||||
const noUnit = "unmarked";
|
||||
const regex = RegexC(
|
||||
`(?<${TimeSecond[TimeSecond.Year]}>[0-9]+(?=[y|a]))|` +
|
||||
`(?<${TimeSecond[TimeSecond.Week]}>[0-9]+(?=[w]))|` +
|
||||
`(?<${TimeSecond[TimeSecond.Day]}>[0-9]+(?=[d|j]))|` +
|
||||
`(?<${TimeSecond[TimeSecond.Hour]}>[0-9]+(?=[h]))|` +
|
||||
`(?<${TimeSecond[TimeSecond.Minute]}>[0-9]+(?=[m]))|` +
|
||||
`(?<${TimeSecond[TimeSecond.Second]}>[0-9]+(?=[s]))|` +
|
||||
`(?<${noUnit}>[0-9]+)`,
|
||||
RegExpFlags.Global | RegExpFlags.Insensitive,
|
||||
);
|
||||
|
||||
const data = Object.assign({}, regex.exec(time)?.groups);
|
||||
const data = Array.from(time.matchAll(regex));
|
||||
if (data.length === 0) {
|
||||
// Regex returned an invalid time
|
||||
return -1;
|
||||
}
|
||||
|
||||
let res = 0;
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
res += +value * TimeSecond[key as keyof typeof TimeSecond];
|
||||
}
|
||||
let lastUnit = TimeSecond.Second;
|
||||
data.forEach((match) => {
|
||||
Object.entries(match.groups!).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
let unit;
|
||||
if (key === noUnit) {
|
||||
unit = nextTimeUnit(lastUnit);
|
||||
res += +value * unit;
|
||||
} else {
|
||||
unit = TimeSecond[key as keyof typeof TimeSecond];
|
||||
res += +value * unit;
|
||||
}
|
||||
lastUnit = unit;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
|
@ -53,6 +100,8 @@ export const strToSeconds = (time: string) => {
|
|||
*/
|
||||
export const timeDeltaToString = (time: number) => {
|
||||
const now = Date.now();
|
||||
// TODO adapt the output and not always parse the time as seconds
|
||||
// TODO: adapt the output and not always parse the time as seconds
|
||||
// https://git.mylloon.fr/ConfrerieDuKassoulait/Botanique/issues/189
|
||||
// Use Intl.RelativeTimeFormat ?
|
||||
return `${strToSeconds(`${(now - time) / 1000}`)} secs`;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue