feat: Reminders (#44)
Checklist: - [x] Suivre les indications de `CONTRIBUTING.md` - [x] Référence aux tickets (par exemple `Closes #xyz`) Closes #1 Additional changes: - use of ChatInputCommandInteraction (part of v14) - fixed locales progress bars - updates dependencies - better handle of errors - support of modals - support of buttons Co-authored-by: Mylloon <kennel.anri@tutanota.com> Reviewed-on: https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/pulls/44
This commit is contained in:
parent
9d5c65bf9d
commit
8f096b9589
27 changed files with 3655 additions and 584 deletions
|
@ -31,7 +31,8 @@
|
||||||
"no-lonely-if": "error",
|
"no-lonely-if": "error",
|
||||||
"no-multi-spaces": "error",
|
"no-multi-spaces": "error",
|
||||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
|
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
|
||||||
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
|
"no-shadow": "off",
|
||||||
|
"@typescript-eslint/no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
|
||||||
"no-trailing-spaces": ["error"],
|
"no-trailing-spaces": ["error"],
|
||||||
"no-var": "error",
|
"no-var": "error",
|
||||||
"object-curly-spacing": ["error", "always"],
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -9,3 +9,6 @@ docker-compose.yml
|
||||||
|
|
||||||
# JS generated files
|
# JS generated files
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# Databse
|
||||||
|
*.sqlite3
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# Comment contribuer ? <!-- omit in toc -->
|
# Comment contribuer ? <!-- omit in toc -->
|
||||||
|
|
||||||
Ce guide contient méthodes et conseils sur comment aider le projet.
|
Ce guide contient méthodes et conseils sur comment aider le projet.
|
||||||
Lisez attentivement si vous êtes un nouveau contributeur.
|
Lisez attentivement si vous êtes un nouveau contributeur.
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ d'un [ticket](https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/issues) ou
|
||||||
une [Pull Request](https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/pulls).
|
une [Pull Request](https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/pulls).
|
||||||
|
|
||||||
## Sommaire <!-- omit in toc -->
|
## Sommaire <!-- omit in toc -->
|
||||||
|
|
||||||
- [Recevoir de l'aide](#recevoir-de-laide)
|
- [Recevoir de l'aide](#recevoir-de-laide)
|
||||||
- [Langues](#langues)
|
- [Langues](#langues)
|
||||||
- [Ajouter une langue](#ajouter-une-langue)
|
- [Ajouter une langue](#ajouter-une-langue)
|
||||||
|
@ -16,15 +18,19 @@ une [Pull Request](https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/pulls).
|
||||||
- [Projet](#projet)
|
- [Projet](#projet)
|
||||||
- [Ajouter une commande](#ajouter-une-commande)
|
- [Ajouter une commande](#ajouter-une-commande)
|
||||||
- [Ajouter un évènement](#ajouter-un-évènement)
|
- [Ajouter un évènement](#ajouter-un-évènement)
|
||||||
|
- [Modèles](#modèles)
|
||||||
|
- [Boutons](#boutons)
|
||||||
- [Modifier du code](#modifier-du-code)
|
- [Modifier du code](#modifier-du-code)
|
||||||
- [Soumettre ses modifications](#soumettre-ses-modifications)
|
- [Soumettre ses modifications](#soumettre-ses-modifications)
|
||||||
- [Gestion du dépôt](#gestion-du-dépôt)
|
- [Gestion du dépôt](#gestion-du-dépôt)
|
||||||
|
|
||||||
## Recevoir de l'aide
|
## Recevoir de l'aide
|
||||||
|
|
||||||
Si tu as besoin d'aide, tu peux poser ta question sur
|
Si tu as besoin d'aide, tu peux poser ta question sur
|
||||||
le [Discord](https://discord.gg/Z5ePxH4).
|
le [Discord](https://discord.gg/Z5ePxH4).
|
||||||
|
|
||||||
## Langues
|
## Langues
|
||||||
|
|
||||||
La langue par défaut est définie dans
|
La langue par défaut est définie dans
|
||||||
[`src/utils/client.ts`](src/utils/client.ts) dans `client.config.default_lang`.
|
[`src/utils/client.ts`](src/utils/client.ts) dans `client.config.default_lang`.
|
||||||
|
|
||||||
|
@ -33,13 +39,21 @@ traduction est incomplète. On part donc du postulat que la langue par défaut
|
||||||
contient toujours toutes les chaînes de caractère dont le bot a besoin.
|
contient toujours toutes les chaînes de caractère dont le bot a besoin.
|
||||||
|
|
||||||
La norme pour les nom dans les fichiers est la suivante :
|
La norme pour les nom dans les fichiers est la suivante :
|
||||||
|
|
||||||
- Chaîne de charactère des commandes :
|
- Chaîne de charactère des commandes :
|
||||||
`c` est utilisé pour `C`ommande.
|
`c` est utilisé pour `C`ommande.
|
||||||
|
|
||||||
- `c_NOM-COMMANDE_name` : Nom de la commande
|
- `c_NOM-COMMANDE_name` : Nom de la commande
|
||||||
- `c_NOM-COMMANDE_desc` : Description de la commande
|
- `c_NOM-COMMANDE_desc` : Description de la commande
|
||||||
- `c_NOM-COMMANDE_optX_name` : Nom de l'option X
|
- `c_NOM-COMMANDE_optX_name` : Nom de l'option X
|
||||||
- `c_NOM-COMMANDE_optX_desc` : Description de l'option X
|
- `c_NOM-COMMANDE_optX_desc` : Description de l'option X
|
||||||
|
- `c_NOM-COMMANDE_subX_name` : Nom de la sous-commande X
|
||||||
|
- `c_NOM-COMMANDE_subX_desc` : Description de la sous-commande X
|
||||||
- `c_NOM-COMMANDEX` : `X` le numéro de la chaîne de caractère
|
- `c_NOM-COMMANDEX` : `X` le numéro de la chaîne de caractère
|
||||||
|
|
||||||
|
Évidemment ça peut s'additionner,
|
||||||
|
par exemple : `c_NOM-COMMANDE_subX_optX_desc`.
|
||||||
|
|
||||||
- Chaîne de charactère des évènements :
|
- Chaîne de charactère des évènements :
|
||||||
`e` est utilisé pour `E`vènements.
|
`e` est utilisé pour `E`vènements.
|
||||||
- `e_NOM-EVENEMENT_N` : `N` le nom de la chaîne de caractère
|
- `e_NOM-EVENEMENT_N` : `N` le nom de la chaîne de caractère
|
||||||
|
@ -48,6 +62,7 @@ La norme pour les nom dans les fichiers est la suivante :
|
||||||
- `u_NOM-FICHIER-UTILS_N` : `N` le nom de la chaîne de caractère
|
- `u_NOM-FICHIER-UTILS_N` : `N` le nom de la chaîne de caractère
|
||||||
|
|
||||||
### Ajouter une langue
|
### Ajouter une langue
|
||||||
|
|
||||||
1. Créer un nouveau fichier dans [src/locales/](./src/locales/), le fichier
|
1. Créer un nouveau fichier dans [src/locales/](./src/locales/), le fichier
|
||||||
doit être nommé `langue.json` avec `langue` suivant
|
doit être nommé `langue.json` avec `langue` suivant
|
||||||
[cette liste](https://discord.com/developers/docs/reference#locales).
|
[cette liste](https://discord.com/developers/docs/reference#locales).
|
||||||
|
@ -60,14 +75,17 @@ doit être nommé `langue.json` avec `langue` suivant
|
||||||
4. Une fois terminée, [ouvrez une Pull Request](#soumettre-ses-modifications).
|
4. Une fois terminée, [ouvrez une Pull Request](#soumettre-ses-modifications).
|
||||||
|
|
||||||
### Mettre à jour une langue
|
### Mettre à jour une langue
|
||||||
|
|
||||||
1. Rechercher la langue dans le dossier [src/locales/](./src/locales/).
|
1. Rechercher la langue dans le dossier [src/locales/](./src/locales/).
|
||||||
2. Modifier/Ajouter des traductions comme
|
2. Modifier/Ajouter des traductions comme
|
||||||
[expliquer au dessus](#ajouter-une-langue) (à partir du `3.`).
|
[expliquer au dessus](#ajouter-une-langue) (à partir du `3.`).
|
||||||
> Pensez à vérifier si de nouvelles valeurs n'ont pas été ajouté dans
|
> Pensez à vérifier si de nouvelles valeurs n'ont pas été ajouté dans
|
||||||
le fichier langue par défaut, [cf. au dessus](#langues).
|
> le fichier langue par défaut, [cf. au dessus](#langues).
|
||||||
|
|
||||||
## Projet
|
## Projet
|
||||||
|
|
||||||
Le code se trouve dans le dosier [src/](./src/). Dans ce dossier il y a :
|
Le code se trouve dans le dosier [src/](./src/). Dans ce dossier il y a :
|
||||||
|
|
||||||
- [commands/](./src/commands/) qui contient toutes les commandes, rangés par
|
- [commands/](./src/commands/) qui contient toutes les commandes, rangés par
|
||||||
catégories
|
catégories
|
||||||
- [events/](./src/events/) qui contient tous les évènements, rangés par
|
- [events/](./src/events/) qui contient tous les évènements, rangés par
|
||||||
|
@ -84,39 +102,48 @@ contiennent chaquin un fichier `loader.js` qui charge respectivement
|
||||||
les commandes et les évènements dans le bot.
|
les commandes et les évènements dans le bot.
|
||||||
|
|
||||||
## Ajouter une commande
|
## Ajouter une commande
|
||||||
|
|
||||||
Pour ajouter une commande au bot, créez un fichier `nom-de-la-commande.ts` dans
|
Pour ajouter une commande au bot, créez un fichier `nom-de-la-commande.ts` dans
|
||||||
un sous dossier de [`src/commands/`](./src/commands/). Vous pouvez créer une
|
un sous dossier de [`src/commands/`](./src/commands/). Vous pouvez créer une
|
||||||
nouvelle catégorie si votre commande n'entre dans aucune qui existe déjà.
|
nouvelle catégorie si votre commande n'entre dans aucune qui existe déjà.
|
||||||
|
|
||||||
Le contenu du fichier doit commencer comme suit :
|
Le contenu du fichier doit commencer comme suit :
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
import { SlashCommandBuilder } from "@discordjs/builders";
|
||||||
import { Client, CommandInteraction } from 'discord.js';
|
import { Client, ChatInputCommandInteraction } 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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: (client: Client) => {
|
data: (client: Client) => {
|
||||||
const filename = getFilename(__filename);
|
const filename = getFilename(__filename);
|
||||||
return new SlashCommandBuilder()
|
return new SlashCommandBuilder()
|
||||||
.setName(
|
.setName(filename.toLowerCase())
|
||||||
filename.toLowerCase())
|
.setDescription(
|
||||||
.setDescription(client.locales.get(client.config.default_lang)
|
client.locales
|
||||||
?.get(`c_${filename}_desc`) ?? '')
|
.get(client.config.default_lang)
|
||||||
|
?.get(`c_${filename}_desc`) ?? ""
|
||||||
|
)
|
||||||
.setNameLocalizations(
|
.setNameLocalizations(
|
||||||
getLocalizations(client, `c_${filename}_name`, true))
|
getLocalizations(client, `c_${filename}_name`, true)
|
||||||
|
)
|
||||||
.setDescriptionLocalizations(
|
.setDescriptionLocalizations(
|
||||||
getLocalizations(client, `c_${filename}_desc`)
|
getLocalizations(client, `c_${filename}_desc`)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
interaction: async (interaction: CommandInteraction, client: Client) => {
|
interaction: async (
|
||||||
|
interaction: ChatInputCommandInteraction,
|
||||||
|
client: Client
|
||||||
|
) => {
|
||||||
const loc = getLocale(client, interaction.locale);
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
|
||||||
/* Votre code ici */
|
/* Votre code ici */
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Ce template vous permet de commencé rapidement votre commande car il contient
|
Ce template vous permet de commencé rapidement votre commande car il contient
|
||||||
déjà tout ce qu'il faut pour le support des langues. Pensez bien à ne pas écrire
|
déjà tout ce qu'il faut pour le support des langues. Pensez bien à ne pas écrire
|
||||||
directement vos chaînes de caractères ici mais bien dans
|
directement vos chaînes de caractères ici mais bien dans
|
||||||
|
@ -124,18 +151,21 @@ les [fichiers de langues](./src/locales/), c'est à ça que la variable
|
||||||
`loc` sert.
|
`loc` sert.
|
||||||
|
|
||||||
Vous devez aussi ajouter **obligatoirement** :
|
Vous devez aussi ajouter **obligatoirement** :
|
||||||
|
|
||||||
- `"c_COMMANDE_name": "NOM"` au fichier de langue, avec `COMMANDE` le nom de
|
- `"c_COMMANDE_name": "NOM"` au fichier de langue, avec `COMMANDE` le nom de
|
||||||
la commande et `NOM` le nom de votre commande.
|
la commande et `NOM` le nom de votre commande.
|
||||||
- `"c_COMMANDE_desc": "DESCRIPTION"` au fichier de langue, avec `COMMANDE`
|
- `"c_COMMANDE_desc": "DESCRIPTION"` au fichier de langue, avec `COMMANDE`
|
||||||
le nom de la commande et `DESCRIPTION` la description de votre commande.
|
le nom de la commande et `DESCRIPTION` la description de votre commande.
|
||||||
|
|
||||||
## Ajouter un évènement
|
## Ajouter un évènement
|
||||||
|
|
||||||
Pour ajouter le support d'un évènement au bot, créez un fichier
|
Pour ajouter le support d'un évènement au bot, créez un fichier
|
||||||
`nom-evenement.ts` dans un sous dossier de [`src/events/`](./src/events/). Vous
|
`nom-evenement.ts` dans un sous dossier de [`src/events/`](./src/events/). Vous
|
||||||
pouvez créer une nouvelle catégorie si votre commande n'entre dans aucune qui
|
pouvez créer une nouvelle catégorie si votre commande n'entre dans aucune qui
|
||||||
existe déjà.
|
existe déjà.
|
||||||
|
|
||||||
Vous pouvez préciser que l'évènement ne sera déclenché qu'une seule fois avec
|
Vous pouvez préciser que l'évènement ne sera déclenché qu'une seule fois avec
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const once = true;
|
export const once = true;
|
||||||
```
|
```
|
||||||
|
@ -144,7 +174,22 @@ De préférence, merci de mettre un lien en commentaire vers la documentation
|
||||||
de discord.js de l'évènement
|
de discord.js de l'évènement
|
||||||
([exemple ici pour l'évènement `ready`](./src/events/client/ready.ts#L3))
|
([exemple ici pour l'évènement `ready`](./src/events/client/ready.ts#L3))
|
||||||
|
|
||||||
|
## Modèles
|
||||||
|
|
||||||
|
Les modèles sont gérés [en dehors séparément du reste](./src/modals/).
|
||||||
|
|
||||||
|
## Boutons
|
||||||
|
|
||||||
|
Les boutons sont gérés [en dehors séparément du reste](./src/buttons/)
|
||||||
|
|
||||||
|
Chaque bouton à une implémentation séparée des autres, même si ils sont dans le
|
||||||
|
même message.
|
||||||
|
|
||||||
|
Contrairement aux autres éléments, les boutons doivent se faire collecter via
|
||||||
|
la fonction [`collect`](./src/buttons/loader.ts#L46) juste après leur déclaration.
|
||||||
|
|
||||||
## Modifier du code
|
## Modifier du code
|
||||||
|
|
||||||
Quand vous modifiez quelque chose, pensez à mettre-à-jour les langues. Si vous
|
Quand vous modifiez quelque chose, pensez à mettre-à-jour les langues. Si vous
|
||||||
ne savez pas traduire dans une langue, ne vous forcez pas, supprimer simplement
|
ne savez pas traduire dans une langue, ne vous forcez pas, supprimer simplement
|
||||||
la traduction.
|
la traduction.
|
||||||
|
@ -159,6 +204,7 @@ Pour commencer, vous pouvez jeté un oeil aux
|
||||||
en anglais, ainsi que les commits afin que chacun puisse contribuer.
|
en anglais, ainsi que les commits afin que chacun puisse contribuer.
|
||||||
|
|
||||||
## Soumettre ses modifications
|
## Soumettre ses modifications
|
||||||
|
|
||||||
1. Pensez à bien commenter votre code (en anglais) pour que n'importe qui
|
1. Pensez à bien commenter votre code (en anglais) pour que n'importe qui
|
||||||
comprennent vos modifications. Vérifier bien dans tout les fichiers si ce que
|
comprennent vos modifications. Vérifier bien dans tout les fichiers si ce que
|
||||||
vous avez modifié n'est pas référencer ailleurs (exemple : si vous modifier
|
vous avez modifié n'est pas référencer ailleurs (exemple : si vous modifier
|
||||||
|
@ -187,8 +233,9 @@ afin que votre code puisse être revu et fusionné. Vous pouvez suivre cette
|
||||||
> supprimé.
|
> supprimé.
|
||||||
|
|
||||||
## Gestion du dépôt
|
## Gestion du dépôt
|
||||||
|
|
||||||
- On ne push jamais directement sur la branche `main`.
|
- On ne push jamais directement sur la branche `main`.
|
||||||
- Quand on merge des modifications vers `main`, on fait un *squash*,
|
- Quand on merge des modifications vers `main`, on fait un _squash_,
|
||||||
l'historique des modifications reste disponible dans
|
l'historique des modifications reste disponible dans
|
||||||
[le graph](https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/graph).
|
[le graph](https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/graph).
|
||||||
- De préférences, suivre les indications de
|
- De préférences, suivre les indications de
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM node:16.15.0-alpine3.15
|
FROM node:19.4.0-alpine3.16
|
||||||
|
|
||||||
|
ENV DOCKERIZED=1
|
||||||
|
RUN mkdir /config
|
||||||
|
RUN chown node:node /config
|
||||||
|
|
||||||
RUN apk add dumb-init
|
RUN apk add dumb-init
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
@ -13,5 +17,6 @@ RUN npm ci --only=production
|
||||||
RUN npx tsc
|
RUN npx tsc
|
||||||
|
|
||||||
RUN rm -r src/ tsconfig.json
|
RUN rm -r src/ tsconfig.json
|
||||||
|
RUN npm uninstall typescript discord-api-types @types/sqlite3
|
||||||
|
|
||||||
CMD ["dumb-init", "node", "./dist/index.js"]
|
CMD ["dumb-init", "node", "./dist/index.js"]
|
||||||
|
|
|
@ -28,6 +28,8 @@ services:
|
||||||
container_name: Botanique
|
container_name: Botanique
|
||||||
environment:
|
environment:
|
||||||
- TOKEN_DISCORD=ton-token-va-ici
|
- TOKEN_DISCORD=ton-token-va-ici
|
||||||
|
volumes:
|
||||||
|
- /here/your/path:/config
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -37,6 +39,11 @@ services:
|
||||||
| TOKEN_DISCORD | Token Discord | Aucune |
|
| TOKEN_DISCORD | Token Discord | Aucune |
|
||||||
| DEFAULT_LANG | Langue par défaut | `fr` | Expérimental, si la langue par défaut n'est pas complète (càd 100%), le bot pourrait ne pas fonctionner correctement.<br>Liste des traductions disponibles [ici](./src/locales/).
|
| DEFAULT_LANG | Langue par défaut | `fr` | Expérimental, si la langue par défaut n'est pas complète (càd 100%), le bot pourrait ne pas fonctionner correctement.<br>Liste des traductions disponibles [ici](./src/locales/).
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
| Chemin | Description
|
||||||
|
| :-------: | :-:
|
||||||
|
| `/config` | Dossier de configuration, par exemple, c'est ici que la base de donnée est.
|
||||||
|
|
||||||
# Contribuer
|
# Contribuer
|
||||||
Toute contribution est la bienvenue !
|
Toute contribution est la bienvenue !
|
||||||
|
|
||||||
|
|
2733
package-lock.json
generated
2733
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -15,12 +15,16 @@
|
||||||
"author": "La confrérie du Kassoulait",
|
"author": "La confrérie du Kassoulait",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/rest": "^1.0.1",
|
"@discordjs/rest": "^1.1.0",
|
||||||
"discord-api-types": "^0.36.0",
|
"@types/sqlite3": "^3.1.8",
|
||||||
"discord.js": "^14.0.3",
|
"discord-api-types": "^0.36.3",
|
||||||
"typescript": "^4.7.4"
|
"discord.js": "^14.3.0",
|
||||||
|
"sqlite3": "^5.0.11",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||||
"@typescript-eslint/parser": "^5.30.7",
|
"@typescript-eslint/parser": "^5.30.7",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
|
|
68
src/buttons/loader.ts
Normal file
68
src/buttons/loader.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { readdir } from 'fs/promises';
|
||||||
|
import { removeExtension } from '../utils/misc';
|
||||||
|
import { ChatInputCommandInteraction, Client, MessageComponentInteraction } from 'discord.js';
|
||||||
|
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'));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
// For each categorie
|
||||||
|
buttons_categories.map(async buttons_category => {
|
||||||
|
// Retrieve all the commands
|
||||||
|
const button_files = await readdir(`${__dirname}/${buttons_category}`);
|
||||||
|
|
||||||
|
// Add the category to the collection for the help command
|
||||||
|
client.buttons.categories.set(
|
||||||
|
buttons_category,
|
||||||
|
button_files.map(removeExtension),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the button
|
||||||
|
return Promise.all(
|
||||||
|
button_files.map(async button_file => {
|
||||||
|
const button = (
|
||||||
|
await import(`../buttons/${buttons_category}/${button_file}`)
|
||||||
|
).default;
|
||||||
|
|
||||||
|
// Add it to the collection so the interaction will work
|
||||||
|
client.buttons.list.set(button.data.name, button);
|
||||||
|
return button.data;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect interactions for buttons
|
||||||
|
* @param client Client
|
||||||
|
* @param interaction Chat interaction
|
||||||
|
* @param id Button ID
|
||||||
|
* @param deferUpdate defer update in case update take time
|
||||||
|
*/
|
||||||
|
export const collect = (client: Client, interaction: ChatInputCommandInteraction | MessageComponentInteraction, id: string, deferUpdate = false) => {
|
||||||
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
const button = client.buttons.list.get(id.split('_')[0]);
|
||||||
|
|
||||||
|
if (!button) {
|
||||||
|
interaction.reply({
|
||||||
|
content: loc.get('e_interacreate_no_button'),
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = (i: MessageComponentInteraction) => i.customId === id;
|
||||||
|
const collector = interaction.channel?.createMessageComponentCollector({ filter, max: 1 });
|
||||||
|
collector?.on('collect', async (i) => {
|
||||||
|
if (deferUpdate) {
|
||||||
|
await i.deferUpdate();
|
||||||
|
}
|
||||||
|
const msg = await button?.interaction(i, client);
|
||||||
|
if (msg !== undefined) {
|
||||||
|
await i.update(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
56
src/buttons/misc/reminderList-next.ts
Normal file
56
src/buttons/misc/reminderList-next.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, MessageComponentInteraction, User } from 'discord.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { getLocale } from '../../utils/locales';
|
||||||
|
import { getFilename } from '../../utils/misc';
|
||||||
|
import { embedListReminders } from '../../utils/reminder';
|
||||||
|
import { collect } from '../loader';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: {
|
||||||
|
name: getFilename(__filename),
|
||||||
|
},
|
||||||
|
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
|
||||||
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
const embed_desc = interaction.message.embeds.at(0)?.description as string;
|
||||||
|
|
||||||
|
// 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 > pageMax) {
|
||||||
|
page = 1;
|
||||||
|
} else {
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve user
|
||||||
|
const userId = /(?!<@)\d+(?=>)/gm.exec(embed_desc)?.[0] as string;
|
||||||
|
const user = client.users.cache.get(userId) as User;
|
||||||
|
|
||||||
|
// Fetch list
|
||||||
|
const list = await embedListReminders(client, user, interaction.guildId, page, interaction.locale);
|
||||||
|
|
||||||
|
const idPrec = 'reminderList-prec_' + uuidv4();
|
||||||
|
const idNext = 'reminderList-next_' + uuidv4();
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(idPrec)
|
||||||
|
.setLabel(loc.get('c_reminder12'))
|
||||||
|
.setStyle(ButtonStyle.Primary))
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(idNext)
|
||||||
|
.setLabel(loc.get('c_reminder13'))
|
||||||
|
.setStyle(ButtonStyle.Primary),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buttons interactions
|
||||||
|
collect(client, interaction, idPrec);
|
||||||
|
collect(client, interaction, idNext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [list],
|
||||||
|
components: [row],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
56
src/buttons/misc/reminderList-prec.ts
Normal file
56
src/buttons/misc/reminderList-prec.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, MessageComponentInteraction, User } from 'discord.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { getLocale } from '../../utils/locales';
|
||||||
|
import { getFilename } from '../../utils/misc';
|
||||||
|
import { embedListReminders } from '../../utils/reminder';
|
||||||
|
import { collect } from '../loader';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: {
|
||||||
|
name: getFilename(__filename),
|
||||||
|
},
|
||||||
|
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
|
||||||
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
const embed_desc = interaction.message.embeds.at(0)?.description as string;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Fetch list
|
||||||
|
const list = await embedListReminders(client, user, interaction.guildId, page, interaction.locale);
|
||||||
|
|
||||||
|
const idPrec = 'reminderList-prec_' + uuidv4();
|
||||||
|
const idNext = 'reminderList-next_' + uuidv4();
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(idPrec)
|
||||||
|
.setLabel(loc.get('c_reminder12'))
|
||||||
|
.setStyle(ButtonStyle.Primary))
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(idNext)
|
||||||
|
.setLabel(loc.get('c_reminder13'))
|
||||||
|
.setStyle(ButtonStyle.Primary),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buttons interactions
|
||||||
|
collect(client, interaction, idPrec);
|
||||||
|
collect(client, interaction, idNext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [list],
|
||||||
|
components: [row],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||||
import { Locale } from 'discord-api-types/v9';
|
import { Locale } from 'discord-api-types/v9';
|
||||||
import { Client, CommandInteraction, EmbedBuilder } from 'discord.js';
|
import { Client, ChatInputCommandInteraction, EmbedBuilder } 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 '../../modules/string';
|
import '../../modules/string';
|
||||||
|
@ -31,12 +31,12 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
interaction: async (interaction: CommandInteraction, client: Client) => {
|
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
|
||||||
const loc = getLocale(client, interaction.locale);
|
const loc = getLocale(client, interaction.locale);
|
||||||
const desired_command = interaction.options.get(client
|
const desired_command = interaction.options.getString(client
|
||||||
.locales
|
.locales
|
||||||
.get(client.config.default_lang)
|
.get(client.config.default_lang)
|
||||||
?.get(`c_${getFilename(__filename)}_opt1_name`) ?? '')?.value as string;
|
?.get(`c_${getFilename(__filename)}_opt1_name`) ?? '');
|
||||||
|
|
||||||
// If a command isn't specified
|
// If a command isn't specified
|
||||||
if (!desired_command) {
|
if (!desired_command) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||||
import { Client, CommandInteraction, Message } from 'discord.js';
|
import { ChatInputCommandInteraction, Client, 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';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
interaction: async (interaction: CommandInteraction, client: Client) => {
|
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
|
||||||
const loc = getLocale(client, interaction.locale);
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
|
||||||
const sent = await interaction.reply({
|
const sent = await interaction.reply({
|
||||||
|
|
275
src/commands/misc/reminder.ts
Normal file
275
src/commands/misc/reminder.ts
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
import { ModalActionRowComponentBuilder, SlashCommandBuilder } from '@discordjs/builders';
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { collect } from '../../buttons/loader';
|
||||||
|
import { getLocale, getLocalizations } from '../../utils/locales';
|
||||||
|
import { getFilename } from '../../utils/misc';
|
||||||
|
import { checkOwnershipReminder, deleteReminder, embedListReminders, getReminderInfo, newReminder } from '../../utils/reminder';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: (client: Client) => {
|
||||||
|
const filename = getFilename(__filename);
|
||||||
|
const loc_default = client.locales.get(client.config.default_lang);
|
||||||
|
if (!loc_default) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SlashCommandBuilder()
|
||||||
|
// Command
|
||||||
|
.setName(filename.toLowerCase())
|
||||||
|
.setDescription(loc_default.get(`c_${filename}_desc`) ?? '')
|
||||||
|
.setNameLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_name`, true)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_desc`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// New reminder
|
||||||
|
.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`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Specified Time
|
||||||
|
.addStringOption(option => option
|
||||||
|
.setName(
|
||||||
|
loc_default.get(`c_${filename}_sub1_opt1_name`)
|
||||||
|
?.toLowerCase() ?? ''
|
||||||
|
).setDescription(
|
||||||
|
loc_default.get(`c_${filename}_sub1_opt1_desc`) ?? ''
|
||||||
|
).setNameLocalizations(
|
||||||
|
getLocalizations(
|
||||||
|
client,
|
||||||
|
`c_${filename}_sub1_opt1_name`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub1_opt1_desc`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Specified message (not required)
|
||||||
|
.addStringOption(option => option
|
||||||
|
.setName(
|
||||||
|
loc_default.get(`c_${filename}_sub1_opt2_name`)
|
||||||
|
?.toLowerCase() ?? ''
|
||||||
|
).setDescription(
|
||||||
|
loc_default.get(`c_${filename}_sub1_opt2_desc`) ?? ''
|
||||||
|
).setNameLocalizations(
|
||||||
|
getLocalizations(
|
||||||
|
client,
|
||||||
|
`c_${filename}_sub1_opt2_name`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub1_opt2_desc`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// List reminders
|
||||||
|
.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`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// User
|
||||||
|
.addUserOption(option => option
|
||||||
|
.setName(
|
||||||
|
loc_default.get(`c_${filename}_sub2_opt1_name`)
|
||||||
|
?.toLowerCase() ?? ''
|
||||||
|
).setDescription(
|
||||||
|
loc_default.get(`c_${filename}_sub2_opt1_desc`) ?? ''
|
||||||
|
).setNameLocalizations(
|
||||||
|
getLocalizations(
|
||||||
|
client,
|
||||||
|
`c_${filename}_sub2_opt1_name`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub2_opt1_desc`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Page
|
||||||
|
.addIntegerOption(option => option
|
||||||
|
.setName(
|
||||||
|
loc_default.get(`c_${filename}_sub2_opt2_name`)
|
||||||
|
?.toLowerCase() ?? ''
|
||||||
|
).setDescription(
|
||||||
|
loc_default.get(`c_${filename}_sub2_opt2_desc`) ?? ''
|
||||||
|
).setNameLocalizations(
|
||||||
|
getLocalizations(
|
||||||
|
client,
|
||||||
|
`c_${filename}_sub2_opt2_name`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub2_opt2_desc`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete a reminder
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName(
|
||||||
|
loc_default.get(`c_${filename}_sub3_name`)
|
||||||
|
?.toLowerCase() ?? ''
|
||||||
|
).setDescription(
|
||||||
|
loc_default.get(`c_${filename}_sub3_desc`) ?? ''
|
||||||
|
).setNameLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub3_name`, true)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub3_desc`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ID
|
||||||
|
.addIntegerOption(option => option
|
||||||
|
.setName(
|
||||||
|
loc_default.get(`c_${filename}_sub3_opt1_name`)
|
||||||
|
?.toLowerCase() ?? ''
|
||||||
|
).setDescription(
|
||||||
|
loc_default.get(`c_${filename}_sub3_opt1_desc`) ?? ''
|
||||||
|
).setNameLocalizations(
|
||||||
|
getLocalizations(
|
||||||
|
client,
|
||||||
|
`c_${filename}_sub3_opt1_name`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
).setDescriptionLocalizations(
|
||||||
|
getLocalizations(client, `c_${filename}_sub3_opt1_desc`)
|
||||||
|
).setRequired(true)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
|
||||||
|
const loc_default = client.locales.get(client.config.default_lang);
|
||||||
|
const filename = getFilename(__filename);
|
||||||
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
switch (subcommand) {
|
||||||
|
// New reminder
|
||||||
|
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);
|
||||||
|
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),
|
||||||
|
createdAt: interaction.createdAt.getTime(),
|
||||||
|
channelId: interaction.channelId,
|
||||||
|
userId: interaction.user.id,
|
||||||
|
guildId: interaction.guildId,
|
||||||
|
}).then((msg) => interaction.reply({
|
||||||
|
content: msg as string,
|
||||||
|
ephemeral: true,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Show modal to user to get at least the time
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId('reminderGUI')
|
||||||
|
.setTitle(loc.get(`c_${filename}_name`).capitalize());
|
||||||
|
|
||||||
|
const timeGUI = new TextInputBuilder()
|
||||||
|
.setCustomId('reminderGUI-time')
|
||||||
|
.setLabel(loc.get(`c_${filename}_sub1_opt1_name`).capitalize())
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('1h')
|
||||||
|
.setRequired(true);
|
||||||
|
|
||||||
|
const messageGUI = new TextInputBuilder()
|
||||||
|
.setCustomId('reminderGUI-message')
|
||||||
|
.setLabel(loc.get(`c_${filename}_sub1_opt2_name`).capitalize())
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setPlaceholder(loc.get(`c_${filename}_sub1_opt2_desc`))
|
||||||
|
.setRequired(false);
|
||||||
|
|
||||||
|
modal.addComponents(
|
||||||
|
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(timeGUI),
|
||||||
|
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(messageGUI)
|
||||||
|
);
|
||||||
|
|
||||||
|
return interaction.showModal(modal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
|
user = interaction.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = interaction.options.getInteger(loc_default?.get(`c_${filename}_sub2_opt2_name`) as string) ?? 1;
|
||||||
|
const list = await embedListReminders(client, user, interaction.guildId, page, interaction.locale);
|
||||||
|
|
||||||
|
const idPrec = 'reminderList-prec_' + uuidv4();
|
||||||
|
const idNext = 'reminderList-next_' + uuidv4();
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(idPrec)
|
||||||
|
.setLabel(loc.get(`c_${filename}12`))
|
||||||
|
.setStyle(ButtonStyle.Primary))
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(idNext)
|
||||||
|
.setLabel(loc.get(`c_${filename}13`))
|
||||||
|
.setStyle(ButtonStyle.Primary),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Buttons interactions
|
||||||
|
collect(client, interaction, idPrec);
|
||||||
|
collect(client, interaction, idNext);
|
||||||
|
|
||||||
|
return await interaction.reply({ ephemeral: true, embeds: [list], components: [row] });
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
if (id === null) {
|
||||||
|
return interaction.reply({ content: loc.get(`c_${filename}2`), ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the ID exists and belongs to the user
|
||||||
|
if (await checkOwnershipReminder(client, id, interaction.user.id, interaction.guildId ?? '0')) {
|
||||||
|
return interaction.reply({ content: loc.get(`c_${filename}3`), ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop timeout
|
||||||
|
const reminderInfo = await getReminderInfo(client, id);
|
||||||
|
clearTimeout(reminderInfo.timeout_id);
|
||||||
|
|
||||||
|
// Delete from database
|
||||||
|
return deleteReminder(client, reminderInfo.creation_date, reminderInfo.user_id)
|
||||||
|
.then(() => interaction.reply({ content: `Reminder **#${id}** supprimé !`, ephemeral: true }));
|
||||||
|
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.error(`${__filename}: unknown subcommand (${subcommand})`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,6 +1,59 @@
|
||||||
|
import { Client } from 'discord.js';
|
||||||
|
import { logStart } from '../../utils/misc';
|
||||||
|
import { dbReminder, deleteReminder, infoReminder, OptionReminder, sendReminder, setTimeoutReminder, updateReminder } from '../../utils/reminder';
|
||||||
|
|
||||||
export const once = true;
|
export const once = true;
|
||||||
|
|
||||||
/** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-ready */
|
/** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-ready */
|
||||||
export default async () => {
|
export default async (client: Client) => {
|
||||||
console.log('Connected to Discord!');
|
console.log(logStart('Connection', true));
|
||||||
|
|
||||||
|
// Restart all the timeout about reminders here
|
||||||
|
new Promise((ok, ko) => {
|
||||||
|
// Fetch all reminders
|
||||||
|
client.db.all('SELECT * FROM reminder', [], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all the current reminders
|
||||||
|
ok(row);
|
||||||
|
});
|
||||||
|
}).then((data) => {
|
||||||
|
const now = Date.now();
|
||||||
|
(data as dbReminder[]).forEach((element) => {
|
||||||
|
const info = {
|
||||||
|
locale: element.locale,
|
||||||
|
message: element.data,
|
||||||
|
createdAt: Number(element.creation_date),
|
||||||
|
channelId: `${element.channel_id}`,
|
||||||
|
userId: `${element.user_id}`,
|
||||||
|
guildId: `${element.guild_id}`,
|
||||||
|
} as infoReminder;
|
||||||
|
|
||||||
|
if (element.expiration_date <= now) {
|
||||||
|
// Reminder expired
|
||||||
|
deleteReminder(client, element.creation_date, `${element.user_id}`).then((res) => {
|
||||||
|
if (res != true) {
|
||||||
|
throw res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sendReminder(client, info, element.option_id as OptionReminder);
|
||||||
|
} else {
|
||||||
|
// Restart timeout
|
||||||
|
const timeoutId = setTimeoutReminder(client, info, element.option_id, (element.expiration_date - now) / 1000);
|
||||||
|
|
||||||
|
// Update timeout in database
|
||||||
|
element.timeout_id = String(timeoutId);
|
||||||
|
updateReminder(client, element).then((res) => {
|
||||||
|
if (res != true) {
|
||||||
|
throw res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,11 @@ import { getLocale } from '../../utils/locales';
|
||||||
|
|
||||||
/** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-interactionCreate */
|
/** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-interactionCreate */
|
||||||
export default (interaction: Interaction, client: Client) => {
|
export default (interaction: Interaction, client: Client) => {
|
||||||
if (interaction.type === InteractionType.ApplicationCommand) {
|
const loc = getLocale(client, interaction.locale);
|
||||||
|
switch (interaction.type) {
|
||||||
|
case InteractionType.ApplicationCommand: {
|
||||||
const command = client.commands.list.get(interaction.commandName);
|
const command = client.commands.list.get(interaction.commandName);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
const loc = getLocale(client, interaction.locale);
|
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: loc.get('e_interacreate_no_command'),
|
content: loc.get('e_interacreate_no_command'),
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
|
@ -15,4 +16,20 @@ export default (interaction: Interaction, client: Client) => {
|
||||||
|
|
||||||
return command.interaction(interaction, client);
|
return command.interaction(interaction, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case InteractionType.ModalSubmit: {
|
||||||
|
const modal = client.modals.list.get(interaction.customId);
|
||||||
|
if (!modal) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: loc.get('e_interacreate_no_modal'),
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return modal.interaction(interaction, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default async (client: Client) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove extension
|
// Remove extension
|
||||||
|
// TODO: use utils functions
|
||||||
const event_type_ext = event_file.split('.');
|
const event_type_ext = event_file.split('.');
|
||||||
const ext = event_type_ext.pop();
|
const ext = event_type_ext.pop();
|
||||||
if (!(ext === 'js' || ext === 'ts')) {
|
if (!(ext === 'js' || ext === 'ts')) {
|
||||||
|
|
|
@ -105,6 +105,7 @@ export default async (message: Message, client: Client) => {
|
||||||
);
|
);
|
||||||
embed.addFields({
|
embed.addFields({
|
||||||
// TODO: Don't pluralize when there is only one file.
|
// TODO: Don't pluralize when there is only one file.
|
||||||
|
// TODO: Locales
|
||||||
name: 'Fichiers joints',
|
name: 'Fichiers joints',
|
||||||
// TODO: Check if don't exceed char limit, if yes, split
|
// TODO: Check if don't exceed char limit, if yes, split
|
||||||
// files into multiples field.
|
// files into multiples field.
|
||||||
|
|
40
src/index.ts
40
src/index.ts
|
@ -1,5 +1,7 @@
|
||||||
import loadClient from './utils/client';
|
import loadClient, { quit } from './utils/client';
|
||||||
import loadEvents from './events/loader';
|
import loadEvents from './events/loader';
|
||||||
|
import loadModals from './modals/loader';
|
||||||
|
import loadButtons from './buttons/loader';
|
||||||
import loadCommands from './commands/loader';
|
import loadCommands from './commands/loader';
|
||||||
|
|
||||||
import { logStart } from './utils/misc';
|
import { logStart } from './utils/misc';
|
||||||
|
@ -19,30 +21,56 @@ const run = async () => {
|
||||||
const client_name = 'Client';
|
const client_name = 'Client';
|
||||||
await loadClient()
|
await loadClient()
|
||||||
.then(async client => {
|
.then(async client => {
|
||||||
console.log(logStart(client_name, true));
|
|
||||||
|
|
||||||
// Events Discord.JS
|
// Events Discord.JS
|
||||||
const events_name = 'Events';
|
const events_name = 'Events';
|
||||||
await loadEvents(client)
|
await loadEvents(client)
|
||||||
.then(() => console.log(logStart(events_name, true)))
|
.then(() => console.log(logStart(events_name, true)))
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
throw logStart(events_name, false);
|
throw logStart(events_name, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect the bot to Discord.com
|
// Connect the bot to Discord.com
|
||||||
await client.login(client.config.token_discord);
|
await client.login(client.config.token_discord);
|
||||||
|
|
||||||
|
// Modals Discord.JS
|
||||||
|
const modals_name = 'Modals';
|
||||||
|
await loadModals(client)
|
||||||
|
.then(() => console.log(logStart(modals_name, true)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
throw logStart(modals_name, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Buttons Discord.JS
|
||||||
|
const buttons_name = 'Buttons';
|
||||||
|
await loadButtons(client)
|
||||||
|
.then(() => console.log(logStart(buttons_name, true)))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
throw logStart(buttons_name, false);
|
||||||
|
});
|
||||||
|
|
||||||
// Commands Slash Discord.JS
|
// Commands Slash Discord.JS
|
||||||
const commands_name = 'Commands';
|
const commands_name = 'Commands';
|
||||||
await loadCommands(client)
|
await loadCommands(client)
|
||||||
.then(() => console.log(logStart(commands_name, true)))
|
.then(() => console.log(logStart(commands_name, true)))
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
throw logStart(commands_name, false);
|
throw logStart(commands_name, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
process.on('SIGINT', () => quit(client));
|
||||||
|
|
||||||
|
// Container force closed
|
||||||
|
process.on('SIGTERM', () => quit(client));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
throw logStart(client_name, false);
|
throw logStart(client_name, false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"e_interacreate_no_command": "Désolé, la commande n'existe plus...",
|
"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...",
|
||||||
|
|
||||||
"c_ping_name": "Ping",
|
"c_ping_name": "Ping",
|
||||||
"c_ping_desc": "Pong!",
|
"c_ping_desc": "Pong!",
|
||||||
|
@ -14,5 +16,42 @@
|
||||||
"c_help2": "`/help <commande>` pour obtenir plus d'informations sur une commande.",
|
"c_help2": "`/help <commande>` pour obtenir plus d'informations sur une commande.",
|
||||||
"c_help3": "Impossible de trouver :",
|
"c_help3": "Impossible de trouver :",
|
||||||
|
|
||||||
"u_time_at": "à"
|
"u_time_at": "à",
|
||||||
|
|
||||||
|
"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_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_sub2_name": "liste",
|
||||||
|
"c_reminder_sub2_desc": "Affiche la liste des rappels d'un utilisateur",
|
||||||
|
"c_reminder_sub2_opt1_name": "utilisateur",
|
||||||
|
"c_reminder_sub2_opt1_desc": "Affiche la liste de l'utilisateur en question",
|
||||||
|
"c_reminder_sub2_opt2_name": "page",
|
||||||
|
"c_reminder_sub2_opt2_desc": "Page à afficher",
|
||||||
|
"c_reminder_sub3_name": "efface",
|
||||||
|
"c_reminder_sub3_desc": "Supprime un rappel",
|
||||||
|
"c_reminder_sub3_opt1_name": "id",
|
||||||
|
"c_reminder_sub3_opt1_desc": "Rappel à supprimé",
|
||||||
|
"c_reminder1": "Un rappel a été configuré pour dans",
|
||||||
|
"c_reminder2": "L'ID renseigné n'est pas valide.",
|
||||||
|
"c_reminder3": "Rappel non trouvé, pas sur le bon serveur ou qui ne vous appartiens pas.",
|
||||||
|
"c_reminder4": "Utilisateur inconnu.",
|
||||||
|
"c_reminder5": "Rappels de",
|
||||||
|
"c_reminder6": "Page",
|
||||||
|
"c_reminder7": "Pas de message",
|
||||||
|
"c_reminder8": "Expire dans",
|
||||||
|
"c_reminder9": "Fais le",
|
||||||
|
"c_reminder10": "L'utilisateur n'a aucun rappel en attente ou page n°",
|
||||||
|
"c_reminder11": "vide",
|
||||||
|
"c_reminder12": "Précédent",
|
||||||
|
"c_reminder13": "Suivant",
|
||||||
|
"c_reminder14": "Message envoyé en DM car le salon n'est plus disponible.",
|
||||||
|
"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": "Pas de message"
|
||||||
}
|
}
|
||||||
|
|
36
src/modals/loader.ts
Normal file
36
src/modals/loader.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { readdir } from 'fs/promises';
|
||||||
|
import { removeExtension } from '../utils/misc';
|
||||||
|
import { Client } from 'discord.js';
|
||||||
|
|
||||||
|
export default async (client: Client) => {
|
||||||
|
// Dossier des modals
|
||||||
|
const modals_categories = (await readdir(__dirname))
|
||||||
|
.filter(element => !element.endsWith('.js') && !element.endsWith('.ts'));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
// For each categorie
|
||||||
|
modals_categories.map(async modals_category => {
|
||||||
|
// Retrieve all the commands
|
||||||
|
const modal_files = await readdir(`${__dirname}/${modals_category}`);
|
||||||
|
|
||||||
|
// Add the category to the collection for the help command
|
||||||
|
client.modals.categories.set(
|
||||||
|
modals_category,
|
||||||
|
modal_files.map(removeExtension),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the modal
|
||||||
|
return Promise.all(
|
||||||
|
modal_files.map(async modal_file => {
|
||||||
|
const modal = (
|
||||||
|
await import(`../modals/${modals_category}/${modal_file}`)
|
||||||
|
).default;
|
||||||
|
|
||||||
|
// Add it to the collection so the interaction will work
|
||||||
|
client.modals.list.set(modal.data.name, modal);
|
||||||
|
return modal.data;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
21
src/modals/misc/reminderGUI.ts
Normal file
21
src/modals/misc/reminderGUI.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Client, ModalSubmitInteraction } from 'discord.js';
|
||||||
|
import { getFilename } from '../../utils/misc';
|
||||||
|
import { newReminder } from '../../utils/reminder';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: {
|
||||||
|
name: getFilename(__filename),
|
||||||
|
},
|
||||||
|
interaction: async (interaction: ModalSubmitInteraction, client: Client) =>
|
||||||
|
newReminder(client, interaction.fields.fields.get('reminderGUI-time')?.value as string, {
|
||||||
|
locale: interaction.locale,
|
||||||
|
message: interaction.fields.fields.get('reminderGUI-message')?.value ?? null,
|
||||||
|
createdAt: interaction.createdAt.getTime(),
|
||||||
|
channelId: interaction.channelId,
|
||||||
|
userId: interaction.user.id,
|
||||||
|
guildId: interaction.guildId,
|
||||||
|
}).then((msg) => interaction.reply({
|
||||||
|
content: msg as string,
|
||||||
|
ephemeral: true,
|
||||||
|
})),
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import { Collection } from 'discord.js';
|
import { Collection } from 'discord.js';
|
||||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||||
|
import { Database } from 'sqlite3';
|
||||||
|
|
||||||
export { };
|
export { };
|
||||||
|
|
||||||
|
@ -15,6 +16,50 @@ declare module 'discord.js' {
|
||||||
/** Default lang used */
|
/** Default lang used */
|
||||||
default_lang: string
|
default_lang: string
|
||||||
},
|
},
|
||||||
|
/** Store all the modals */
|
||||||
|
modals: {
|
||||||
|
categories: Collection<
|
||||||
|
/** Category name */
|
||||||
|
string,
|
||||||
|
/** Name of the modals in the category */
|
||||||
|
string[]
|
||||||
|
>,
|
||||||
|
list: Collection<
|
||||||
|
/** Modal name */
|
||||||
|
string,
|
||||||
|
/** Modal itself */
|
||||||
|
{
|
||||||
|
/** Data about the modal */
|
||||||
|
data: {
|
||||||
|
name: string
|
||||||
|
},
|
||||||
|
/** How the modal interact */
|
||||||
|
interaction: (interaction: ModalSubmitInteraction, client: Client) => unknown
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
/** Store all the buttons */
|
||||||
|
buttons: {
|
||||||
|
categories: Collection<
|
||||||
|
/** Category name */
|
||||||
|
string,
|
||||||
|
/** Name of the buttons in the category */
|
||||||
|
string[]
|
||||||
|
>,
|
||||||
|
list: Collection<
|
||||||
|
/** Button name */
|
||||||
|
string,
|
||||||
|
/** Button itself */
|
||||||
|
{
|
||||||
|
/** Data about the button */
|
||||||
|
data: {
|
||||||
|
name: string
|
||||||
|
},
|
||||||
|
/** How the button interact */
|
||||||
|
interaction: (interaction: MessageComponentInteraction, client: Client) => Promise<InteractionUpdateOptions>
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
},
|
||||||
/** Store all the slash commands */
|
/** Store all the slash commands */
|
||||||
commands: {
|
commands: {
|
||||||
categories: Collection<
|
categories: Collection<
|
||||||
|
@ -34,8 +79,9 @@ declare module 'discord.js' {
|
||||||
interaction: (interaction: CommandInteraction, client: Client) => unknown
|
interaction: (interaction: CommandInteraction, client: Client) => unknown
|
||||||
}
|
}
|
||||||
>,
|
>,
|
||||||
}
|
},
|
||||||
/** Store all the localizations */
|
/** Store all the localizations */
|
||||||
locales: Map<string, Map<string, string>>
|
locales: Map<string, Map<string, string>>,
|
||||||
|
db: Database,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Client, Collection, GatewayIntentBits } from 'discord.js';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { loadLocales } from './locales';
|
import { loadLocales } from './locales';
|
||||||
import '../modules/client';
|
import '../modules/client';
|
||||||
|
import { Database } from 'sqlite3';
|
||||||
|
|
||||||
/** Creation of the client and definition of its properties. */
|
/** Creation of the client and definition of its properties. */
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
@ -18,6 +19,16 @@ export default async () => {
|
||||||
default_lang: process.env.DEFAULT_LANG ?? 'fr',
|
default_lang: process.env.DEFAULT_LANG ?? 'fr',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
client.modals = {
|
||||||
|
categories: new Collection(),
|
||||||
|
list: new Collection(),
|
||||||
|
};
|
||||||
|
|
||||||
|
client.buttons = {
|
||||||
|
categories: new Collection(),
|
||||||
|
list: new Collection(),
|
||||||
|
};
|
||||||
|
|
||||||
client.commands = {
|
client.commands = {
|
||||||
categories: new Collection(),
|
categories: new Collection(),
|
||||||
list: new Collection(),
|
list: new Collection(),
|
||||||
|
@ -26,5 +37,41 @@ export default async () => {
|
||||||
console.log('Translations progression :');
|
console.log('Translations progression :');
|
||||||
client.locales = await loadLocales(client.config.default_lang);
|
client.locales = await loadLocales(client.config.default_lang);
|
||||||
|
|
||||||
|
client.db = new Database(`${process.env.DOCKERIZED === '1' ? '/config' : './config'}/db.sqlite3`);
|
||||||
|
|
||||||
|
initDatabase(client.db);
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quit gracefully the client
|
||||||
|
* @param client Client
|
||||||
|
*/
|
||||||
|
export const quit = (client: Client) => {
|
||||||
|
// Close DB
|
||||||
|
client.db.close();
|
||||||
|
|
||||||
|
// Close client
|
||||||
|
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 \
|
||||||
|
);');
|
||||||
|
};
|
||||||
|
|
|
@ -146,9 +146,9 @@ async (locales: Map<string, Map<string, string>>, default_lang: string) => {
|
||||||
locales_size.forEach((size, lang) => {
|
locales_size.forEach((size, lang) => {
|
||||||
const percentage = (size / max_size) * 100;
|
const percentage = (size / max_size) * 100;
|
||||||
// Colored bar part
|
// Colored bar part
|
||||||
const blocks = ' '.repeat(percentage / bar_size);
|
const blocks = ' '.repeat(Math.floor(percentage / bar_size));
|
||||||
// Blank bar part
|
// Blank bar part
|
||||||
const blank = ' '.repeat((100 - percentage) / bar_size);
|
const blank = ' '.repeat(Math.ceil((100 - percentage) / bar_size));
|
||||||
const color = () => {
|
const color = () => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case percentage <= 25:
|
case percentage <= 25:
|
||||||
|
@ -167,7 +167,8 @@ async (locales: Map<string, Map<string, string>>, default_lang: string) => {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const padding = ' '.repeat(lang.length === 5 ? 1 : 4);
|
||||||
|
|
||||||
console.log(`${lang} | ${color()}${blocks}\x1b[0m${blank} | ${percentage.toPrecision(3)}%`);
|
console.log(`${padding}${lang} | ${color()}${blocks}\x1b[0m${blank} | ${percentage.toPrecision(3)}%`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { GuildMember } from 'discord.js';
|
||||||
* @returns String
|
* @returns String
|
||||||
*/
|
*/
|
||||||
export const logStart = (name: string, status: boolean) => {
|
export const logStart = (name: string, status: boolean) => {
|
||||||
return `> ${name} ${status === true ? '✅' : '❌'}`;
|
// TODO Handle precision about the error if status is false
|
||||||
|
return `> ${name}\t${status === true ? '✅' : '❌'}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,3 +77,27 @@ export const userWithNickname = (member: GuildMember) => {
|
||||||
return member.user.tag;
|
return member.user.tag;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the text into backtick text, preserving mentions and links
|
||||||
|
* @param text Text
|
||||||
|
* @returns Formatted text
|
||||||
|
*/
|
||||||
|
export const cleanCodeBlock = (text: string) => {
|
||||||
|
text = `\`${text.trim()}\``;
|
||||||
|
|
||||||
|
// Keep mentions
|
||||||
|
text = text.replace(/(<@\d+>)/g, function(mention: string) {
|
||||||
|
return `\`${mention}\``;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep links
|
||||||
|
text = text.replace(/(http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)/g, function(url: string) {
|
||||||
|
return `\`${url}\``;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix issues
|
||||||
|
text = text.replace('``', '');
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
363
src/utils/reminder.ts
Normal file
363
src/utils/reminder.ts
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
import { Client, Colors, EmbedBuilder, User } from 'discord.js';
|
||||||
|
import { getLocale } from './locales';
|
||||||
|
import { cleanCodeBlock } from './misc';
|
||||||
|
import { showDate, strToSeconds, timeDeltaToString } from './time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option possible for reminders
|
||||||
|
*/
|
||||||
|
export enum OptionReminder {
|
||||||
|
/** No parameters */
|
||||||
|
Nothing,
|
||||||
|
/** @ */
|
||||||
|
Mention,
|
||||||
|
/** p */
|
||||||
|
DirectMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store data about the remidner
|
||||||
|
*/
|
||||||
|
export type infoReminder = {
|
||||||
|
locale: string,
|
||||||
|
message: string | null,
|
||||||
|
createdAt: number,
|
||||||
|
channelId: string | null,
|
||||||
|
userId: string,
|
||||||
|
guildId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type dbReminder = {
|
||||||
|
id: number,
|
||||||
|
data: string | null,
|
||||||
|
expiration_date: number,
|
||||||
|
option_id: OptionReminder,
|
||||||
|
channel_id: string | null,
|
||||||
|
creation_date: string,
|
||||||
|
user_id: string,
|
||||||
|
guild_id: string | null,
|
||||||
|
locale: string,
|
||||||
|
timeout_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the time and the extra args `p` and `@`
|
||||||
|
* @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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { time: time, option: OptionReminder.Nothing };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new reminder
|
||||||
|
* @param client Client
|
||||||
|
* @param time raw text from user about the time wanted
|
||||||
|
* @param info data about the context of the reminder
|
||||||
|
* @returns Promise resolution of the sql request
|
||||||
|
*/
|
||||||
|
export const newReminder = async (client: Client, time: string, info: infoReminder) =>
|
||||||
|
new Promise((ok, ko) => {
|
||||||
|
const data = splitTime(time);
|
||||||
|
const timeout = strToSeconds(data.time);
|
||||||
|
const timeoutId = setTimeoutReminder(client, info, data.option, timeout);
|
||||||
|
|
||||||
|
// Add the remind to the db
|
||||||
|
client.db.run('INSERT INTO reminder ( \
|
||||||
|
data, expiration_date, option_id, channel_id, creation_date, user_id, guild_id, locale, timeout_id \
|
||||||
|
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? );', [
|
||||||
|
info.message,
|
||||||
|
`${info.createdAt + (timeout * 1000)}`,
|
||||||
|
data.option.valueOf(),
|
||||||
|
info.channelId,
|
||||||
|
`${info.createdAt}`,
|
||||||
|
info.userId,
|
||||||
|
info.guildId,
|
||||||
|
info.locale,
|
||||||
|
timeoutId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send confirmation to user
|
||||||
|
const loc = getLocale(client, info.locale);
|
||||||
|
ok(`${loc.get('c_reminder1')} ${data.time}.`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a reminder from the database
|
||||||
|
* @param client Client
|
||||||
|
* @param createdAt Creation of the reminder
|
||||||
|
* @param userId User ID who created the reminder
|
||||||
|
* @returns what the SQlite request sended
|
||||||
|
*/
|
||||||
|
export const deleteReminder = (client: Client, createdAt: string, userId: string) => {
|
||||||
|
// Delete the reminder for the database
|
||||||
|
return new Promise((ok, ko) => {
|
||||||
|
// Add the remind to the db
|
||||||
|
client.db.run('DELETE FROM reminder WHERE creation_date = ? AND user_id = ?', [createdAt, userId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send confirmation to user
|
||||||
|
ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendReminder = (client: Client, info: infoReminder, option: OptionReminder) => {
|
||||||
|
const loc = getLocale(client, info.locale);
|
||||||
|
// Send the message in the appropriate channel
|
||||||
|
// TODO: Embed
|
||||||
|
let message: string;
|
||||||
|
if (info.message === null) {
|
||||||
|
message = loc.get('c_reminder18');
|
||||||
|
} else {
|
||||||
|
message = cleanCodeBlock(info.message);
|
||||||
|
}
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor('Random')
|
||||||
|
.setDescription(message)
|
||||||
|
.setTimestamp(info.createdAt);
|
||||||
|
|
||||||
|
let channelOk = false;
|
||||||
|
if (info.channelId !== null) {
|
||||||
|
if (client.channels.cache.get(info.channelId) !== undefined) {
|
||||||
|
channelOk = true;
|
||||||
|
} else {
|
||||||
|
embed.setFooter({ text: loc.get('c_reminder14') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let guildOk = false;
|
||||||
|
if (info.guildId !== null) {
|
||||||
|
const guild = client.guilds.cache.get(info.guildId);
|
||||||
|
if (guild !== undefined) {
|
||||||
|
if (guild.members.cache.get(info.userId) !== undefined) {
|
||||||
|
guildOk = true;
|
||||||
|
} else {
|
||||||
|
embed.setFooter({ text: `${loc.get('c_reminder15')} ${guild.name}.` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
embed.setFooter({ text: loc.get('c_reminder16') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == OptionReminder.DirectMessage || !channelOk || !guildOk) {
|
||||||
|
// Direct message
|
||||||
|
const user = client.users.cache.get(info.userId);
|
||||||
|
if (user !== undefined) {
|
||||||
|
user.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Channel
|
||||||
|
client.channels.fetch(info.channelId ?? '').then((channel) => {
|
||||||
|
if (channel?.isTextBased()) {
|
||||||
|
let content = `<@${info.userId}>`;
|
||||||
|
embed.setFooter({ text: `${loc.get('c_reminder17')} ${timeDeltaToString(info.createdAt)}` });
|
||||||
|
|
||||||
|
// Mention everybody if needed
|
||||||
|
if (option == OptionReminder.Mention) {
|
||||||
|
(info.message?.match(/<@\d+>/g) ?? []).forEach(mention => {
|
||||||
|
content += ' ' + mention;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.send({ content, embeds: [embed] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a timeout for a reminder
|
||||||
|
* @param client Client
|
||||||
|
* @param info info about the reminder
|
||||||
|
* @param option option used for this reminder (aka location of the response)
|
||||||
|
* @param timeout Amout of time before the reminder ends
|
||||||
|
* @returns Timeout's ID
|
||||||
|
*/
|
||||||
|
export const setTimeoutReminder = (client: Client, info: infoReminder, option: OptionReminder, timeout: number) => {
|
||||||
|
return Number(setTimeout(() => {
|
||||||
|
deleteReminder(client, String(info.createdAt), info.userId).then((val) => {
|
||||||
|
if (val != true) {
|
||||||
|
throw val;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReminder(client, info, option);
|
||||||
|
});
|
||||||
|
}, timeout * 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the owernship of a reminder by a user
|
||||||
|
* @param client Client
|
||||||
|
* @param id ID of the reminder
|
||||||
|
* @param userId user ID to check
|
||||||
|
* @param guildId guild ID where the ownership request as been send, 0 if DM
|
||||||
|
*/
|
||||||
|
export const checkOwnershipReminder = async (client: Client, id: number, userId: string, guildId: string) => {
|
||||||
|
const data = await new Promise((ok, ko) => {
|
||||||
|
// Check the ownership
|
||||||
|
client.db.all('SELECT EXISTS ( \
|
||||||
|
SELECT 1 FROM reminder \
|
||||||
|
WHERE id = ? \
|
||||||
|
AND user_id = ? \
|
||||||
|
AND (guild_id = ? OR guild_id = 0) \
|
||||||
|
)', [
|
||||||
|
id, userId, guildId,
|
||||||
|
], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all the current reminders
|
||||||
|
ok(row[0]);
|
||||||
|
});
|
||||||
|
}) as { [key: string]: number };
|
||||||
|
return Object.keys(data).map((key) => data[key])[0] === 0 ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve info about a reminder
|
||||||
|
* @param client Client
|
||||||
|
* @param id Reminder's ID
|
||||||
|
*/
|
||||||
|
export const getReminderInfo = async (client: Client, id: number) => {
|
||||||
|
return await new Promise((ok, ko) => {
|
||||||
|
// Check the ownership
|
||||||
|
client.db.all('SELECT * FROM reminder \
|
||||||
|
WHERE id = ?', [
|
||||||
|
id], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all the current reminders
|
||||||
|
ok(row[0]);
|
||||||
|
});
|
||||||
|
}) as dbReminder;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an entry of the database
|
||||||
|
* @param client Client
|
||||||
|
* @param data Data who will override the data in database
|
||||||
|
*/
|
||||||
|
export const updateReminder = (client: Client, data: dbReminder) => {
|
||||||
|
// Delete the reminder for the database
|
||||||
|
return new Promise((ok, ko) => {
|
||||||
|
// Update the db
|
||||||
|
client.db.run('UPDATE reminder \
|
||||||
|
SET data = ?, \
|
||||||
|
expiration_date = ?, \
|
||||||
|
option_id = ?, \
|
||||||
|
channel_id = ?, \
|
||||||
|
creation_date = ?, \
|
||||||
|
user_id = ?, \
|
||||||
|
guild_id = ?, \
|
||||||
|
locale = ?, \
|
||||||
|
timeout_id = ? \
|
||||||
|
WHERE ID = ?', [
|
||||||
|
data.data,
|
||||||
|
data.expiration_date,
|
||||||
|
data.option_id,
|
||||||
|
data.channel_id,
|
||||||
|
data.creation_date,
|
||||||
|
data.user_id,
|
||||||
|
data.guild_id,
|
||||||
|
data.locale,
|
||||||
|
data.timeout_id,
|
||||||
|
data.id], (err) => {
|
||||||
|
if (err) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of reminders for a user in a specified context
|
||||||
|
* @param client Client
|
||||||
|
* @param userId user ID
|
||||||
|
* @param guildId guild ID
|
||||||
|
* @returns List of reminders of a user in a guild
|
||||||
|
*/
|
||||||
|
const listReminders = async (client: Client, userId: string, guildId: string | null) => {
|
||||||
|
return await new Promise((ok, ko) => {
|
||||||
|
// Check the ownership
|
||||||
|
client.db.all('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) {
|
||||||
|
ko(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all the current reminders
|
||||||
|
ok(row);
|
||||||
|
});
|
||||||
|
}) as dbReminder[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the embed of the reminders
|
||||||
|
* @param client Client
|
||||||
|
* @param user User
|
||||||
|
* @param guildId Guild ID
|
||||||
|
* @param page Page requested
|
||||||
|
* @param local Lang
|
||||||
|
* @returns Pretty embed who list reminders
|
||||||
|
*/
|
||||||
|
export const embedListReminders = async (client: Client, user: User, guildId: string | null, page: number, local: string) => {
|
||||||
|
const loc = getLocale(client, local);
|
||||||
|
const reminders = await listReminders(client, user.id, guildId);
|
||||||
|
|
||||||
|
const elementPerPage = 5;
|
||||||
|
const pageMax = Math.ceil(reminders.length / elementPerPage);
|
||||||
|
|
||||||
|
if (pageMax <= 1) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
// TODO: Use Random color or force a color from args
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(Colors.DarkGrey)
|
||||||
|
.setDescription(`${loc.get('c_reminder5')} ${user} • ${loc.get('c_reminder6')} ${page}/${pageMax}`)
|
||||||
|
.setThumbnail(user.displayAvatarURL());
|
||||||
|
|
||||||
|
const limit = elementPerPage * page;
|
||||||
|
|
||||||
|
if (reminders.length > 0 && page <= pageMax) {
|
||||||
|
let curseur = limit - elementPerPage;
|
||||||
|
reminders.splice(0, limit - elementPerPage);
|
||||||
|
reminders.forEach((remind) => {
|
||||||
|
if (curseur < limit) {
|
||||||
|
let text = remind.data ?? loc.get('c_reminder7');
|
||||||
|
if (text.length > 1024) {
|
||||||
|
text = `${text.substring(0, 1021)}...`;
|
||||||
|
}
|
||||||
|
const expiration = `${loc.get('c_reminder8')} ${timeDeltaToString(remind.expiration_date)}`;
|
||||||
|
embed.addFields({
|
||||||
|
name: `#${remind.id} • ${loc.get('c_reminder9')} ${showDate(local, loc, new Date(Number(remind.creation_date)))}\n${expiration}`,
|
||||||
|
value: text,
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
curseur++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
embed.addFields({ name: '\u200b', value: `${loc.get('c_reminder10')}${page} ${loc.get('c_reminder11')}.` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
};
|
|
@ -10,7 +10,45 @@ export const showDate = (
|
||||||
locale: Map<string, unknown>,
|
locale: Map<string, unknown>,
|
||||||
date: Date
|
date: Date
|
||||||
) => {
|
) => {
|
||||||
return date.toLocaleString(tz).replace(' ', ` ${
|
return date.toLocaleString(tz).replace(' ', ` ${locale.get('u_time_at')} `);
|
||||||
locale.get('u_time_at')
|
};
|
||||||
} `);
|
|
||||||
|
enum TimeSecond {
|
||||||
|
Year = 31536000,
|
||||||
|
Week = 604800,
|
||||||
|
Day = 86400,
|
||||||
|
Hour = 3600,
|
||||||
|
Minute = 60,
|
||||||
|
Second = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]?))`);
|
||||||
|
|
||||||
|
const data = Object.assign({}, regex.exec(time)?.groups);
|
||||||
|
|
||||||
|
let res = 0;
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (value) {
|
||||||
|
res += +value * TimeSecond[key as keyof typeof TimeSecond];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculating the difference between a date and now
|
||||||
|
* @param time Time
|
||||||
|
* @returns Delta between the time and now
|
||||||
|
*/
|
||||||
|
export const timeDeltaToString = (time: number) => {
|
||||||
|
const now = Date.now();
|
||||||
|
// TODO adapt the output and not always parse the time as seconds
|
||||||
|
return `${strToSeconds(`${(now - time) / 1000}`)} secs`;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue