├── .env.exemple
├── .eslintrc.json
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── proposer-une-fonctionnalit-.md
│ └── signaler-un-bug.md
└── workflows
│ └── qodana_code_quality.yml
├── .gitignore
├── README.md
├── SECURITY.md
├── commands
├── config.js
├── contenu.js
├── cours.js
├── eval.js
├── fichier.js
├── graph.js
├── help.js
├── history.js
├── infos.js
├── logout.js
├── menu.js
├── notes.js
├── ping.js
├── points-bac.js
├── recheck.js
└── reloadcommands.js
├── events
├── autocomplete.js
├── command.js
├── ready.js
└── selectMenu.js
├── index.js
├── package-lock.json
├── package.json
├── qodana.yaml
├── screen-exemple.png
├── start-pronote-bot.bat
└── utils
├── db.js
├── functions.js
├── notifications.js
├── pronoteSynchronization.js
├── subjects.js
├── update.js
└── verif-env.js
/.env.exemple:
--------------------------------------------------------------------------------
1 | PRONOTE_URL="https://demo.index-education.net/pronote/"
2 |
3 | PRONOTE_CAS=""
4 | PRONOTE_USERNAME="TON_IDENTIFIANT"
5 | PRONOTE_PASSWORD="TON_MOT_DE_PASSE"
6 |
7 |
8 | TOKEN="LE_TOKEN_DU_BOT"
9 | DEBUG_MODE="false"
10 | AUTO_UPDATE="true"
11 |
12 | HOMEWORKS_CHANNEL_ID="ID_DU_SALON_DEVOIRS"
13 | MARKS_CHANNEL_ID="ID_DU_SALON_NOTES"
14 | AWAY_CHANNEL_ID="ID_DU_SALON_ANNULATIONS"
15 | INFOS_CHANNEL_ID="ID_DU_SALON_INFORMATIONS"
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "commonjs": true,
4 | "es2020": true,
5 | "node": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "ecmaVersion": 11
10 | },
11 | "rules": {
12 | "indent": [
13 | "error",
14 | 4
15 | ],
16 | "linebreak-style": [
17 | "error",
18 | "unix"
19 | ],
20 | "quotes": [
21 | "error",
22 | "double"
23 | ],
24 | "semi": [
25 | "error",
26 | "always"
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/proposer-une-fonctionnalit-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Proposer une fonctionnalité
3 | about: Suggérer une idée pour ce projet
4 | title: "[SUGGESTION]"
5 | labels: enhancement
6 | assignees: Merlode11
7 |
8 | ---
9 |
10 | **Votre demande de fonctionnalité est-elle liée à un problème ? Veuillez le décrire.**
11 | Une description claire et concise de la nature du problème. Ex. Je suis toujours frustré lorsque [...]
12 |
13 | **Décrivez la solution que vous souhaitez**
14 | Une description claire et concise de ce que vous voulez qu'il se passe.
15 |
16 | **Décrivez les alternatives que vous avez envisagées**
17 | Une description claire et concise de toutes les solutions ou fonctionnalités alternatives que vous avez envisagées.
18 |
19 | **Contexte supplémentaire**
20 | Ajoutez ici tout autre contexte ou capture d'écran concernant la demande de fonctionnalité.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/signaler-un-bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Signaler un bug
3 | about: Signalez un bug pour nous aider à améliorer le code
4 | title: "[BUG]"
5 | labels: bug, help wanted
6 | assignees: Merlode11
7 |
8 | ---
9 |
10 | **Décrivez le bug**
11 | Une description claire et concise de ce qu'est le bug.
12 |
13 | **Reproduire**
14 | Étapes pour reproduire le problème
15 | 1. Aller sur '...'
16 | 2. Cliquer sur '....'
17 | 3. Descendre tout en bas '....'
18 | 4. L'erreur est apparue
19 |
20 | **Comportement attendu**
21 | Une description claire et concise de ce que vous attendiez.
22 |
23 | **Screenshots**
24 | Si c'est possible, joignez des captures d'écran pour que nous comprenions mieux
25 |
26 | **Autres informations**
27 | Ajoutez toutes les autres remarques à propos du bogue ici
28 |
--------------------------------------------------------------------------------
/.github/workflows/qodana_code_quality.yml:
--------------------------------------------------------------------------------
1 | name: Qodana
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | push:
6 | branches: # Specify your branches here
7 | - main # The 'main' branch
8 | - 'releases/*' # The release branches
9 |
10 | jobs:
11 | qodana:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 | checks: write
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
21 | fetch-depth: 0 # a full history is required for pull request analysis
22 | - name: 'Qodana Scan'
23 | uses: JetBrains/qodana-action@v2024.3
24 | with:
25 | pr-mode: false
26 | env:
27 | QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_172613210 }}
28 | QODANA_ENDPOINT: 'https://qodana.cloud'
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node
2 | node_modules
3 | yarn.lock
4 |
5 | # Config
6 | .env
7 | cache*.json
8 |
9 | # IDE files
10 | .idea
11 | .vscode
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pronote Discord Bot
2 |
3 | Un bot Discord très simple qui envoie des notifications dans un salon sur Discord lorsqu'un devoir ou une note est ajouté sur Pronote, ou lorsqu'un enseignant est absent ! 📚
4 |
5 | Si vous êtes plus à l'aise avec Python vous pouvez également utiliser le bot de **[busybox11](https://github.com/busybox11/probote)**, qui sera surement compatible avec la dernière version de Pronote bientôt ! 💫
6 |
7 | 
8 |
9 | ## Information
10 |
11 | Le bot utilise une API de Pronote non officielle. Index Eduction, l'entreprise possédant Pronote n'a pas donné son accord pour l'utilisation de cette API.
12 | Vous pouvez donc risquer des poursuites judiciaires si vous utilisez ce bot. Je ne suis pas responsable de vos actes.
13 |
14 | ## Fonctionnalités
15 |
16 | Ce bot *incroyable* peut vous permettre de réaliser plusieurs actions et d'avoirs des informations sur :
17 |
18 | ### Notifications
19 | - Vos nouvelles notes
20 | * Indique le niveau de la note
21 | + La meilleure note de la classe
22 | + Une note au-dessus de la moyenne de la classe
23 | * Indique **votre note**
24 | * Indique la **moyenne** de la casse
25 | * Indique la note la **plus basse**
26 | * Et indique la note la **plus haute**
27 | * Indique votre **moyenne** dans la **matière**
28 | * Indique la **moyenne** de la **classe*
29 | - Puis pas extension, la moyenne générale
30 | * Moyenne générale de l'**élève**
31 | * Moyenne générale de la **classe**
32 | * L'**ancienne** moyenne de l'**élève**
33 | * L'**ancienne** moyenne de la **classe*
34 | * La *modification* qu'il y a eue pour l'**élève**
35 | * La *modification* qu'il y a eue pour la **classe**
36 | - Pour les devoirs
37 | * La matière
38 | * Le devoir
39 | * La date pour le rendre
40 | - Modification de cours
41 | * Si le cours est annulé ou que le professeur est absent
42 | * La matière
43 | * Le professeur
44 | * La date
45 | - Nouvelles communications
46 | * L'auteur de la communication
47 | * Le titre de la communication
48 | * Le contenu de la communication
49 | - Outil de calcul de la note du bac automatique
50 | * Calcule les notes obtenues au contrôle continu en première et en terminale
51 | * Calcule la note obtenue aux épreuves données
52 | * Applique les coefficients
53 |
54 | ### Commandes
55 | - `/config` pour configurer le bot
56 | - `/contenu` pour afficher le contenu d'un cours
57 | - `/cours` pour avoir l'emploie du temps du jour
58 | - `/fichier` pour afficher un fichier
59 | - `/graph` vous donne un graphique des modifications d'une moyenne (classe/élève pour une matière ou non)
60 | - `/help` pour avoir la liste des commandes
61 | - `/history` vous donne la liste des modifications d'une moyenne (classe/élève pour une matière ou non)
62 | - `/infos` pour avoir les informations sur le compte
63 | - `/logout` pour se déconnecter du compte
64 | - `/menu` pour avoir le menu du jour **[⚠ ATTENTION COMMANDE EN DEV](https://github.com/Merlode11/pronote-bot-discord/issues/4 "Aider à développer la commande")**
65 | - `/notes` pour avoir les notes d'une matière
66 | - `/ping` pour avoir le ping du bot et quelques informations
67 | - `/points-bac` calcule automatiquement le nombre de points obtenu pour le bac
68 | - `/recheck` pour forcer une vérification sur le moment
69 |
70 | ## Installation
71 |
72 | ### Node.js
73 | Node.js est requis pour le bon fonctionnement du bot. Il faut donc aller le [télécharger](https://nodejs.org/en/download/current/) sur son site à la **dernière** version, c'est-à-dire la `16.x.X`
74 |
75 |
76 | ### Windows
77 | * Cloner le repository (`git clone https://github.com/Merlode11/pronote-discord-bot`) ou alors téléchargez la version compressée depuis GitHub
78 | * Renommer le fichier `.env.example` en `.env` et le compléter.
79 | * Lancer le bot avec le ficher bat `start-pronote-bot.bat`
80 | * C'est fini !
81 |
82 | ### Mac/Linux *Ou aussi Windows, ça fonctionne aussi très bien*
83 | *Il y a juste une étape en plus, ne vous inquiétez pas*
84 |
85 | * Cloner le repository (`git clone https://github.com/Merlode11/pronote-discord-bot`) ou alors téléchargez la version compressée depuis GitHub
86 | * Renommer le fichier `.env.example` en `.env` et le compléter.
87 | * Installer les modules (`npm install`) *Via un terminal pointant vers le dossier du bot (`cd EMPLACEMENT_DOSSIER`)*
88 | * Lancer le bot (`node index.js`)
89 | * C'est fini !
90 |
91 | ### Remplir son `.env`
92 | ##### `PRONOTE_URL`
93 | Indiquez ici votre URL sur lequel vous accédez à pronote, sans le `eleve.html` et sa suite. Il faudra s'arrêter à `/pronote/`
94 | > Exemple: `https://0050006e.index-education.net/pronote/`
95 | ##### `PRONOTE_CAS`
96 | **Uniquement dans le cas où vous ne pouvez PAS vous connecter directement par Pronote, mais devez passer par une interface régionale spéciale.**
97 |
98 | **Si vous pouvez vous connecter directement sur l'interface de Pronote, l'API devrait fonctionner PEU IMPORTE VOTRE ACADÉMIE**
99 |
100 | Sinon, l'API propose de se connecter à Pronote avec des comptes des académies suivantes :
101 |
102 |
103 | CAS list
104 |
105 | - Académie d'Orleans-Tours (CAS : ac-orleans-tours, URL : "ent.netocentre.fr")
106 | - Académie de Besançon (CAS : ac-besancon, URL : "cas.eclat-bfc.fr")
107 | - Académie de Bordeaux (CAS : ac-bordeaux, URL : "mon.lyceeconnecte.fr")
108 | - Académie de Bordeaux 2 (CAS : ac-bordeaux2, URL : "ent2d.ac-bordeaux.fr")
109 | - Académie de Caen (CAS : ac-caen, URL : "fip.itslearning.com")
110 | - Académie de Clermont-Ferrand (CAS : ac-clermont, URL : "cas.ent.auvergnerhonealpes.fr")
111 | - Académie de Dijon (CAS : ac-dijon, URL : "cas.eclat-bfc.fr")
112 | - Académie de Grenoble (CAS : ac-grenoble, URL : "cas.ent.auvergnerhonealpes.fr")
113 | - Académie de la Loire (CAS : cybercolleges42, URL : "cas.cybercolleges42.fr")
114 | - Académie de Lille (CAS : ac-lille, URL : "cas.savoirsnumeriques62.fr")
115 | - Académie de Lille (CAS : ac-lille2, URL : "teleservices.ac-lille.fr")
116 | - Académie de Limoges (CAS : ac-limoges, URL : "mon.lyceeconnecte.fr")
117 | - Académie de Lyon (CAS : ac-lyon, URL : "cas.ent.auvergnerhonealpes.fr)
118 | - Académie de Marseille (CAS : atrium-sud, URL : "atrium-sud.fr")
119 | - Académie de Montpellier (CAS : ac-montpellier, URL : "cas.mon-ent-occitanie.fr")
120 | - Académie de Nancy-Metz (CAS : ac-nancy-metz, URL : "cas.monbureaunumerique.fr")
121 | - Académie de Nantes (CAS : ac-nantes, URL : "cas3.e-lyco.fr")
122 | - Académie de Poitiers (CAS : ac-poitiers, URL : "mon.lyceeconnecte.fr")
123 | - Académie de Reims (CAS : ac-reims, URL : "cas.monbureaunumerique.fr")
124 | - Académie de Rouen (Arsene76) (CAS : arsene76, URL : "cas.arsene76.fr")
125 | - Académie de Rouen (CAS : ac-rouen, URL : "nero.l-educdenormandie.fr")
126 | - Académie de Strasbourg (CAS : ac-strasbourg, URL : "cas.monbureaunumerique.fr")
127 | - Académie de Toulouse (CAS : ac-toulouse, URL : "cas.mon-ent-occitanie.fr")
128 | - Académie du Val-d'Oise (CAS : ac-valdoise, URL : "cas.moncollege.valdoise.fr")
129 | - ENT "Agora 06" (Nice) (CAS : agora06, URL : "cas.agora06.fr")
130 | - ENT "Haute-Garonne" (CAS : haute-garonne, URL : "cas.ecollege.haute-garonne.fr")
131 | - ENT "Hauts-de-France" (CAS : hdf, URL : "enthdf.fr")
132 | - ENT "La Classe" (Lyon) (CAS : laclasse, URL : "www.laclasse.com")
133 | - ENT "Lycee Connecte" (Nouvelle-Aquitaine) (CAS : lyceeconnecte, URL : "mon.lyceeconnecte.fr")
134 | - ENT "Seine-et-Marne" (CAS : seine-et-marne, URL : "ent77.seine-et-marne.fr")
135 | - ENT "Somme" (CAS : somme, URL : "college.entsomme.fr")
136 | - ENT "Portail Famille" (Orleans Tours) (CAS : portail-famille, URL : "seshat.ac-orleans-tours.fr:8443")
137 | - ENT "Toutatice" (Rennes) (CAS : toutatice, URL : "www.toutatice.fr")
138 | - ENT "Île de France" (CAS : iledefrance, URL : "ent.iledefrance.fr")
139 | - ENT "Mon collège Essonne" (CAS : moncollege-essonne, URL : "www.moncollege-ent.essonne.fr")
140 | - ENT "Paris Classe Numerique" (CAS : parisclassenumerique, URL : "ent.parisclassenumerique.fr")
141 | - ENT "Lycee Jean Renoir Munich" (CAS : ljr-munich, URL : "cas.kosmoseducation.com")
142 | - ENT "L'Eure en Normandie" (CAS : eure-normandie, URL : "cas.ent27.fr")
143 | - ENT "Mon Bureau Numérique" via EduConnect (CAS: monbureaunumerique-educonnect, URL: "cas.monbureaunumerique.fr")
144 |
145 |
146 |
147 |
148 | ##### `PRONOTE_USERNAME` et `PRONOTE_PASSWORD`
149 | Indiquez ici votre identifiant (`USERNAME`) et votre mot de passe (`PASSWORD`) pour que le bot puisse se connecter à pronote via votre compte
150 |
151 | ⚠ Vous identifiants doivent rester **PRIVÉES** et **personne** ne doit y avoir **accès**. Faite attention à ne donner **aucun** de vos identifiants, ne pas donner **directement** le code. Vous pourriez par oubli donner vos **identifiants**. Faites bien attention de donner le code uniquement via ce repository
152 |
153 | ##### `TOKEN`
154 | Indiquez ici le token de votre bot pour qu'il puisse se connecter à Discord. Allez dans le [portail développeur](https://discord.com/developers/applications/) et récupérez ici le token de votre bot
155 |
156 | [](http://www.youtube.com/watch?v=Y8RcqgmYVU8 "Miniature de la création du bot")
157 |
158 | ##### `CHANNEL`
159 | Complétez toutes les variables finissant par `CHANNEL` par les identifiants des salons où seront envoyées les notifications :
160 | * `HOMEWORKS_CHANNEL_ID` Le salon pour les nouveaux devoirs à la maison
161 | * `MARKS_CHANNEL_ID` Le salon pour les nouvelles notes
162 | * `AWAY_CHANNEL_ID` Le salon où seront envoyées les cours annulés
163 | * `INFOS_CHANNEL_ID` Le salon pour les nouvelles informations (Communication & sondages)
164 |
165 |
166 | ##### `AUTO_UPDATE`
167 | Vous devez indiquer ici si vous voulez que le bot se mette à jour automatiquement ou non. Si vous souhaitez que le bot se mette à jour automatiquement, mettez `true` sinon mettez `false`.
168 | Une confirmation sera demandée si jamais le bot détecte une version plus récente de celle qu'il possède
169 |
170 | ## Crédit
171 |
172 | Le bot est à l'origine créé par [@Androz2091](https://github.com/Androz2091/pronote-bot-discord). Je lui ai apporté une grande part de ma touche personnelle pour l'améliorer et le rendre plus utile que ce qu'il n'était.
173 |
174 |
175 | ## Retours
176 | ### Bugs
177 | En cas de bug ou de problème d'installation vous pouvez ouvrir une [**`Issue`**](https://github.com/Merlode11/pronote-bot-discord/issues/new?assignees=Merlode11&labels=bug%2C+help+wanted&template=signaler-un-bug.md&title=%5BBUG%5D) ou alors contactez-moi sur Discord: `Merlode#8128`
178 | ### Suggestions
179 | Si vous avez la moindre suggestion, proposez là dans les [**`Issue`**](https://github.com/Merlode11/pronote-bot-discord/issues/new?assignees=Merlode11&labels=enhancement&template=proposer-une-fonctionnalit-.md&title=%5BSUGGESTION%5D), elles sont là pour ça
180 |
181 | ### Merci
182 | Merci à vous de me supporter dans cette aventure que je commence tout juste et si vous pouvez laisser une petite star ça ferait vraiment plaisir
183 | N'hésitez pas à partager ce bot à tous ceux qui en ont besoin !
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | Use this section to tell people how to report a vulnerability.
6 |
7 | Tell them where to go, how often they can expect to get an update on a
8 | reported vulnerability, what to expect if the vulnerability is accepted or
9 | declined, etc.
10 |
--------------------------------------------------------------------------------
/commands/config.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, ApplicationCommandOptionType, ChannelType } = require("discord.js");
2 | const fs = require("fs");
3 |
4 | module.exports = {
5 | data: {
6 | description: "Configurer le .env depuis Discord",
7 | options: [
8 | {
9 | type: ApplicationCommandOptionType.Subcommand,
10 | name: "salon-devoirs",
11 | description: "Définir le salon où seront envoyés les devoirs",
12 | options: [
13 | {
14 | type: ApplicationCommandOptionType.Channel,
15 | name: "salon",
16 | description: "Le salon où seront envoyés les devoirs",
17 | required: true,
18 | channelTypes: [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.PublicThread, ChannelType.PrivateThread]
19 | }
20 | ]
21 | },
22 | {
23 | type: ApplicationCommandOptionType.Subcommand,
24 | name: "salon-notes",
25 | description: "Définir le salon où seront envoyés les notes",
26 | options: [
27 | {
28 | type: ApplicationCommandOptionType.Channel,
29 | name: "salon",
30 | description: "Le salon où seront envoyés les notes",
31 | required: true,
32 | channelTypes: [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.PublicThread, ChannelType.PrivateThread]
33 | }
34 | ]
35 | },
36 | {
37 | type: ApplicationCommandOptionType.Subcommand,
38 | name: "salon-modifications",
39 | description: "Définir le salon où seront envoyés les modifications d'emploi du temps",
40 | options: [
41 | {
42 | type: ApplicationCommandOptionType.Channel,
43 | name: "salon",
44 | description: "Le salon où seront envoyés les modifications d'emploi du temps",
45 | required: true,
46 | channelTypes: [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.PublicThread, ChannelType.PrivateThread]
47 | }
48 | ]
49 | },
50 | {
51 | type: ApplicationCommandOptionType.Subcommand,
52 | name: "salon-info",
53 | description: "Définir le salon où seront envoyés les informations",
54 | options: [
55 | {
56 | type: ApplicationCommandOptionType.Channel,
57 | name: "salon",
58 | description: "Le salon où seront envoyés les informations",
59 | required: true,
60 | channelTypes: [ChannelType.GuildText, ChannelType.GuildNews, ChannelType.PublicThread, ChannelType.PrivateThread]
61 | }
62 | ]
63 | },
64 | {
65 | type: ApplicationCommandOptionType.Subcommand,
66 | name: "nom-utilisateur",
67 | description: "Définir le nom d'utilisateur à utiliser pour se connecter à l'ENT",
68 | options: [
69 | {
70 | type: ApplicationCommandOptionType.String,
71 | name: "nom",
72 | description: "Le nom d'utilisateur à utiliser pour se connecter à l'ENT",
73 | required: true,
74 | autocomplete: false,
75 | minLength: 4,
76 | }
77 | ]
78 | },
79 | {
80 | type: ApplicationCommandOptionType.Subcommand,
81 | name: "mot-de-passe",
82 | description: "Définir le mot de passe à utiliser pour se connecter à l'ENT",
83 | options: [
84 | {
85 | type: ApplicationCommandOptionType.String,
86 | name: "mot-de-passe",
87 | description: "Le mot de passe à utiliser pour se connecter à l'ENT",
88 | required: true,
89 | autocomplete: false,
90 | minLength: 4,
91 | }
92 | ]
93 | },
94 | {
95 | type: ApplicationCommandOptionType.Subcommand,
96 | name: "url",
97 | description: "Définir l'URL de l'ENT",
98 | options: [
99 | {
100 | type: ApplicationCommandOptionType.String,
101 | name: "url",
102 | description: "L'URL de l'ENT",
103 | required: true,
104 | autocomplete: false,
105 | minLength: 16,
106 | }
107 | ]
108 | },
109 | {
110 | type: ApplicationCommandOptionType.Subcommand,
111 | name: "cas",
112 | description: "Si l'ENT utilise un CAS, définir le CAS",
113 | options: [
114 | {
115 | type: ApplicationCommandOptionType.String,
116 | name: "cas",
117 | description: "Le CAS de l'ENT",
118 | required: true,
119 | autocomplete: true
120 | }
121 | ]
122 | },
123 | {
124 | type: ApplicationCommandOptionType.Subcommand,
125 | name: "debug",
126 | description: "Activer ou désactiver le mode debug",
127 | options: [
128 | {
129 | type: ApplicationCommandOptionType.Boolean,
130 | name: "debug",
131 | description: "Activer ou désactiver le mode debug",
132 | required: true,
133 | autocomplete: false
134 | }
135 | ]
136 | },
137 | {
138 | type: ApplicationCommandOptionType.Subcommand,
139 | name: "auto-update",
140 | description: "Activer ou désactiver la mise à jour automatique",
141 | options: [
142 | {
143 | type: ApplicationCommandOptionType.Boolean,
144 | name: "auto-update",
145 | description: "Activer ou désactiver la mise à jour automatique",
146 | required: true,
147 | }
148 | ]
149 | },
150 | {
151 | type: ApplicationCommandOptionType.Subcommand,
152 | name: "voir",
153 | description: "Voir la configuration actuelle",
154 | options: [
155 | {
156 | type: ApplicationCommandOptionType.Boolean,
157 | name: "mot-de-passe",
158 | description: "Voir le mot de passe (uniquement pour le créateur du bot)",
159 | required: false,
160 | }
161 | ]
162 | }
163 | ],
164 | },
165 | execute: async (client, interaction) => {
166 | const updateEnv = (key, value) => {
167 | const env = fs.readFileSync("./.env", "utf8");
168 | const regex = new RegExp(`^${key}=(.*)$`, "m");
169 | const newEnv = env.replace(regex, `${key}="${value}"`);
170 | fs.writeFileSync("./.env", newEnv);
171 | require("dotenv").config();
172 | process.env[key] = value;
173 | };
174 |
175 | if (interaction.options.getSubcommand() === "salon-devoirs") {
176 | const channel = interaction.options.getChannel("salon");
177 | if (!channel) {
178 | return interaction.editReply({
179 | content: "Veuillez spécifier un salon valide",
180 | ephemeral: true
181 | });
182 | }
183 | updateEnv("HOMEWORKS_CHANNEL_ID", channel.id);
184 | return await interaction.editReply(`Le salon devoirs a été défini sur ${channel}`);
185 | }
186 | else if (interaction.options.getSubcommand() === "salon-notes") {
187 | const channel = interaction.options.getChannel("salon");
188 | if (!channel) {
189 | return interaction.editReply({
190 | content: "Veuillez spécifier un salon valide",
191 | ephemeral: true
192 | });
193 | }
194 | updateEnv("MARKS_CHANNEL_ID", channel.id);
195 | return await interaction.editReply(`Le salon notes a été défini sur ${channel}`);
196 | }
197 | else if (interaction.options.getSubcommand() === "salon-modifications") {
198 | const channel = interaction.options.getChannel("salon");
199 | if (!channel) {
200 | return interaction.editReply({
201 | content: "Veuillez spécifier un salon valide",
202 | ephemeral: true
203 | });
204 | }
205 | updateEnv("AWAY_CHANNEL_ID", channel.id);
206 | return await interaction.editReply(`Le salon modifications a été défini sur ${channel}`);
207 | }
208 | else if (interaction.options.getSubcommand() === "salon-info") {
209 | const channel = interaction.options.getChannel("salon");
210 | if (!channel) {
211 | return interaction.editReply({
212 | content: "Veuillez spécifier un salon valide",
213 | ephemeral: true
214 | });
215 | }
216 | updateEnv("INFOS_CHANNEL_ID", channel.id);
217 | return await interaction.editReply(`Le salon info a été défini sur ${channel}`);
218 | }
219 | else if (interaction.options.getSubcommand() === "nom-utilisateur") {
220 | const username = interaction.options.getString("nom");
221 | if (!username) {
222 | return interaction.editReply({
223 | content: "Veuillez spécifier un nom d'utilisateur valide",
224 | ephemeral: true
225 | });
226 | }
227 | updateEnv("PRONOTE_USERNAME", username);
228 | return await interaction.editReply(`Le nom d'utilisateur a été défini sur ${username}`);
229 | }
230 | else if (interaction.options.getSubcommand() === "mot-de-passe") {
231 | const password = interaction.options.getString("mot-de-passe");
232 | if (!password) {
233 | return interaction.editReply({
234 | content: "Veuillez spécifier un mot de passe valide",
235 | ephemeral: true
236 | });
237 | }
238 | updateEnv("PRONOTE_PASSWORD", password);
239 | return await interaction.editReply("Le mot de passe a été défini");
240 | }
241 | else if (interaction.options.getSubcommand() === "url") {
242 | const url = interaction.options.getString("url");
243 | if (!url) {
244 | return interaction.editReply({
245 | content: "Veuillez spécifier une URL valide",
246 | ephemeral: true
247 | });
248 | }
249 | updateEnv("PRONOTE_URL", url);
250 | return await interaction.editReply(`L'URL a été définie sur ${url}`);
251 | }
252 | else if (interaction.options.getSubcommand() === "cas") {
253 | const cas = interaction.options.getString("cas");
254 | const { casList } = require("pronote-api-maintained");
255 |
256 | if (!cas || !casList.includes(cas)) {
257 | return interaction.editReply({
258 | content: "Veuillez spécifier un CAS valide",
259 | ephemeral: true
260 | });
261 | }
262 | updateEnv("PRONOTE_CAS", cas);
263 | return await interaction.editReply(`Le CAS a été défini sur ${cas}`);
264 | }
265 | else if (interaction.options.getSubcommand() === "debug") {
266 | const debug = interaction.options.getBoolean("debug");
267 | updateEnv("DEBUG_MODE", debug);
268 | return await interaction.editReply(`Le mode debug a été défini sur ${debug}`);
269 | }
270 | else if (interaction.options.getSubcommand() === "auto-update") {
271 | const autoUpdate = interaction.options.getBoolean("auto-update");
272 | updateEnv("AUTO_UPDATE", autoUpdate);
273 | return await interaction.editReply(`La mise à jour automatique a été définie sur ${autoUpdate}`);
274 | }
275 | else if (interaction.options.getSubcommand() === "voir") {
276 | const env = fs.readFileSync("./.env", "utf8");
277 | const regex = new RegExp("^PRONOTE_PASSWORD=(.*)$", "m");
278 | let password = "\*\*\*\*\*\*\*\*";
279 |
280 | if (interaction.options.getBoolean("mot-de-passe")) {
281 | if ((client.application.owner?.members && client.application.owner?.members.has(interaction.user.id)) || client.application.owner?.id === interaction.user.id) {
282 | password = env.match(regex)[1];
283 | }
284 | }
285 |
286 | const embed = new EmbedBuilder()
287 | .setTitle("Configuration actuelle")
288 | .setColor("#70C7A4")
289 | .addFields([
290 | {
291 | name: "Salon devoirs",
292 | value: (process.env.HOMEWORKS_CHANNEL_ID ? `<#${process.env.HOMEWORKS_CHANNEL_ID}>` : "Non défini") + `\n`,
293 | inline: true
294 | },
295 | {
296 | name: "Salon notes",
297 | value: (process.env.MARKS_CHANNEL_ID ? `<#${process.env.MARKS_CHANNEL_ID}>` : "Non défini") + `\n`,
298 | inline: true
299 | },
300 | {
301 | name: "Salon modifications",
302 | value: (process.env.AWAY_CHANNEL_ID ? `<#${process.env.AWAY_CHANNEL_ID}>` : "Non défini") + `\n`,
303 | inline: true
304 | },
305 | {
306 | name: "Salon info",
307 | value: (process.env.INFOS_CHANNEL_ID ? `<#${process.env.INFOS_CHANNEL_ID}>` : "Non défini") + `\n`,
308 | inline: true
309 | },
310 | {
311 | name: "Nom d'utilisateur",
312 | value: process.env.PRONOTE_USERNAME + `\n`,
313 | inline: true
314 | },
315 | {
316 | name: "Mot de passe",
317 | value: "||" + password + "||" + `\n`,
318 | },
319 | {
320 | name: "URL",
321 | value: process.env.PRONOTE_URL + `\n`,
322 | inline: true
323 | },
324 | {
325 | name: "CAS",
326 | value: process.env.PRONOTE_CAS + `\n`,
327 | inline: true
328 | },
329 | {
330 | name: "Mode debug",
331 | value: process.env.DEBUG_MODE + `\n`,
332 | inline: true
333 | },
334 | {
335 | name: "Mise à jour automatique",
336 | value: process.env.AUTO_UPDATE + `\n`,
337 | inline: true
338 | }
339 | ]);
340 | return await interaction.editReply({
341 | embeds: [embed],
342 | ephemeral: interaction.options.getBoolean("mot-de-passe")
343 | });
344 | }
345 | },
346 | };
--------------------------------------------------------------------------------
/commands/contenu.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, ApplicationCommandOptionType, SelectMenuBuilder, ActionRowBuilder } = require("discord.js");
2 | const { NodeHtmlMarkdown } = require("node-html-markdown");
3 |
4 |
5 | module.exports = {
6 | data: {
7 | description: "Vous fournis le contenu d'un cours",
8 | options: [
9 | {
10 | type: ApplicationCommandOptionType.String,
11 | name: "matière",
12 | description: "Sélectionne la matière",
13 | required: true,
14 | autocomplete: true
15 | },
16 | {
17 | type: ApplicationCommandOptionType.String,
18 | name: "date",
19 | description: "Sélectionne la date du cours",
20 | required: false,
21 | autocomplete: true
22 | }
23 | ],
24 | },
25 | execute: async (client, interaction) => {
26 | await client.session.contents(new Date(new Date().getFullYear(), 8, 1), new Date()).then(async (contents) => {
27 |
28 | const subject = interaction.options.getString("matière");
29 | const date = interaction.options.getString("date");
30 | let data = contents.filter(o => o.subject === subject);
31 | if (!data.length) return interaction.editReply("⚠ | Aucune donnée n'a été trouvée pour cette matière");
32 |
33 | const components = [];
34 | const stringDays = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
35 |
36 | let dateObj = null;
37 | if (date) {
38 | const parsedValue = date.split(/\s+/);
39 | const dateValue = parsedValue[0].split("/");
40 | const timeValue = parsedValue[1].replace("h", "");
41 |
42 | dateObj = new Date(parseInt(dateValue[2]), parseInt(dateValue[1]) - 1, parseInt(dateValue[0]), parseInt(timeValue));
43 | }
44 |
45 | if (data.length > 1) {
46 | const menu = new SelectMenuBuilder()
47 | .setCustomId("content_select")
48 | .setPlaceholder("Sélectionnez une date")
49 | .addOptions(data.map(o => {
50 | return {
51 | label: o.from.toLocaleDateString() + " " + o.from.getHours() + "h",
52 | value: subject + "-" + o.from.toLocaleDateString() + "-" + o.from.getHours(),
53 | description: stringDays[o.from.getDay()] + " " + o.from.toLocaleDateString() + " " + o.from.getHours() + "h",
54 | default: o.from.getTime() === dateObj?.getTime()
55 | };
56 | }).reverse().splice(0, 25))
57 | .setMinValues(1)
58 | .setMaxValues(1);
59 | components.push(new ActionRowBuilder().addComponents(menu));
60 | }
61 |
62 | if (dateObj) {
63 | data = data.filter(o => o.from.getTime() === dateObj.getTime());
64 | }
65 | if (!data.length) return interaction.editReply("⚠ | Aucune donnée n'a été trouvée pour cette date");
66 |
67 | const content = data[0];
68 | const embed = new EmbedBuilder()
69 | .setAuthor({
70 | name: "Contenu du cours",
71 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
72 | url: process.env.PRONOTE_URL,
73 | })
74 | .setColor(content.color)
75 | .addFields([
76 | {
77 | name: content.subject,
78 | value: content.teachers.join(", ") +
79 | "\n le **" + content.from.toLocaleDateString() + "**" +
80 | " de **" + content.from.toLocaleTimeString().split(":")[0] +
81 | "h" + content.from.toLocaleTimeString().split(":")[1] + "**" +
82 | " à **" + content.to.toLocaleTimeString().split(":")[0] +
83 | "h" + content.to.toLocaleTimeString().split(":")[1] + "**",
84 | }
85 | ])
86 | .setFooter({text: "Bot par Merlode#8128"});
87 |
88 | if (content.title) {
89 | embed.setTitle(content.title);
90 | }
91 |
92 | if (content.htmlDescription) {
93 | embed.setDescription(NodeHtmlMarkdown.translate(content.htmlDescription));
94 | } else if (content.description) {
95 | embed.setDescription(content.description);
96 | }
97 |
98 | let attachments = [];
99 | let files = [];
100 | if (content.files.length > 0) {
101 | await client.functions.asyncForEach(content.files, async (file) => {
102 | await client.functions.getFileProperties(file).then(async (properties) => {
103 | if (properties.type === "file") {
104 | attachments.push(properties.attachment);
105 | }
106 | files.push(properties);
107 | });
108 | });
109 | }
110 |
111 | // In the case of more than 10 attachments: send the attachments in more in other messages
112 | const finalAttachments = []
113 | if (attachments.length > 10) {
114 | let i = 10;
115 | while (i < attachments.length) {
116 | interaction.channel.send({files: attachments.slice(i, i + 10)}).then(m => {
117 | finalAttachments.concat(m.attachments);
118 | });
119 | i += 10;
120 | }
121 | }
122 |
123 |
124 |
125 | await interaction.editReply({embeds: [embed], components: components, files: attachments.splice(0, 10), fetchReply: true});
126 |
127 | if (files.length > 0) {
128 | const e = await interaction.fetchReply();
129 | finalAttachments.concat(e.attachments);
130 | let string = "";
131 | if (files.length > 0) {
132 | await client.functions.asyncForEach(files, async (file) => {
133 | if (file.type === "file") {
134 | const name = client.functions.setFileName(file.name);
135 | const attachment = finalAttachments.find(a => a.name === name);
136 | if (attachment) {
137 | string += `[${file.name}](${attachment.url})\n`;
138 | }
139 | } else {
140 | string += `[${file.name ?? file.url}](${file.url} "${file.url}")\n`;
141 | }
142 | });
143 | }
144 | const strings = client.functions.splitMessage(string, {
145 | maxLength: 1024,
146 | });
147 |
148 | if (string.length > 0) {
149 | const lastString = strings.pop();
150 | await client.functions.asyncForEach(strings, async (string) => {
151 | embed.data.fields.unshift({
152 | name: "",
153 | value: string,
154 | inline: false
155 | });
156 | });
157 | embed.data.fields.unshift({
158 | name: "Fichiers joints",
159 | value: lastString,
160 | inline: false
161 | });
162 | await interaction.editReply({embeds: [embed]});
163 | }
164 | }
165 | });
166 | },
167 | };
--------------------------------------------------------------------------------
/commands/cours.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, ApplicationCommandOptionType, SelectMenuBuilder, ActionRowBuilder } = require("discord.js");
2 |
3 | function isLessonInInterval(lesson, from, to) {
4 | return lesson.from >= from && lesson.from <= to;
5 | }
6 |
7 |
8 | module.exports = {
9 | data: {
10 | description: "Vous fournis l'emploi du temps de la journée",
11 | options: [
12 | {
13 | type: ApplicationCommandOptionType.String,
14 | name: "date",
15 | description: "Sélectionnez la date du cours",
16 | required: false,
17 | autocomplete: true
18 | }
19 | ],
20 | },
21 | execute: async (client, interaction) => {
22 | const dateUser = interaction.options.getString("date");
23 | let date = new Date();
24 | if (dateUser) {
25 | let parsed = dateUser.split("/");
26 | date = new Date(parseInt(parsed[2]), parseInt(parsed[1]) - 1, parseInt(parsed[0]));
27 | }
28 |
29 | await client.session.timetable(date).then((cours) => {
30 | let totalDuration = 0;
31 |
32 | let embedCours = cours.map((cour) => {
33 | // Ne pas afficher les cours si jamais ils sont annulés et qu'ils sont remplacés par un autre cours dont les horaires sont inclus par un autre cours
34 | if (cour.isCancelled && cours.find((c) => isLessonInInterval(c, cour.from, cour.to) && !c.isCancelled)) {
35 | return;
36 | }
37 | totalDuration += cour.to.getTime() - cour.from.getTime();
38 |
39 | const subHomeworks = client.cache.homeworks.filter(h => h.subject === cour.subject && cour.from.getDate()+"/"+cour.from.getMonth() === h.for.getDate()+"/"+h.for.getMonth());
40 | const coursIsAway = cour.isAway || cour.isCancelled || cour.status?.match(/(.+)?prof(.+)?absent(.+)?/giu) || cour.status == "Cours annulé";
41 | const embed = new EmbedBuilder()
42 | .setColor(cour.color ?? "#70C7A4")
43 | .setAuthor({
44 | name: cour.subject ?? (cour.status ?? "Non défini"),
45 | })
46 | .setDescription("Professeur: **" + (cour.teacher ?? "*Non précisé*") + "**" +
47 | "\nSalle: `" + (cour.room ?? " ? ") + "`" +
48 | "\nDe **" + cour.from.toLocaleTimeString().split(":")[0] +
49 | "h" + cour.from.toLocaleTimeString().split(":")[1] + "**" +
50 | " à **" + cour.to.toLocaleTimeString().split(":")[0] +
51 | "h" + cour.to.toLocaleTimeString().split(":")[1] + "**" +
52 | " *(" + (cour.to.getTime() - cour.from.getTime()) / 1000 / 60 / 60 + "h)*" +
53 | (subHomeworks.length && !coursIsAway ? `\n⚠**__\`${subHomeworks.length}\` Devoirs__**` : "") +
54 | (coursIsAway ? "\n🚫__**Cour annulé**__" : ""));
55 |
56 | if (cour.status && (!coursIsAway || cour.statut !== "Cours annulé")) {
57 | embed.addFields([
58 | {
59 | name: "Status",
60 | value: "__**" + cour.status + "**__"
61 | }
62 | ]);
63 | }
64 | return embed;
65 | }).filter(emb => !!emb);
66 |
67 | if (embedCours.length >= 9) {
68 | const embed = new EmbedBuilder()
69 | .setColor("#70C7A4")
70 | .addFields(
71 | embedCours.map((emb) => {
72 | return {
73 | name: emb.author.name,
74 | value: emb.description,
75 | inline: false
76 | };
77 | })
78 | );
79 | embedCours = [embed];
80 | }
81 |
82 | totalDuration = Math.abs(totalDuration / 1000 / 60 / 60);
83 | const embed = new EmbedBuilder()
84 | .setColor("#70C7A4")
85 | .setTitle("Vous avez " + embedCours.length + " cours "+ ( dateUser ? `le \`${dateUser}\`` : "aujourd'hui") +" :")
86 | .setDescription("Durée totale : **" + totalDuration + "h**");
87 |
88 | const current = new Date(date.getTime());
89 | const week = [];
90 | for (let i = 1; i <= 7; i++) {
91 | let first = current.getDate() - current.getDay() + i;
92 | let day = new Date(current.setDate(first));
93 | if (day.getDay() !== 0) week.push(day);
94 | }
95 | let weekString = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
96 |
97 | const selectMenu = new SelectMenuBuilder()
98 | .setCustomId("cours_date")
99 | .setPlaceholder("Sélectionnez une date pour voir les cours")
100 | .addOptions(week.map((day) => {
101 | return {
102 | label: day.toLocaleDateString(),
103 | value: day.toLocaleDateString(),
104 | description: weekString[day.getDay()] + " " + day.toLocaleDateString().split("/")[0],
105 | default: day.toLocaleDateString() === date.toLocaleDateString()
106 | };
107 | }))
108 | .setMaxValues(1)
109 | .setMinValues(1);
110 |
111 | return interaction.editReply({ embeds: [embed].concat(embedCours.filter(emb => !!emb)), components: [new ActionRowBuilder().addComponents(selectMenu)] });
112 | });
113 | },
114 | };
--------------------------------------------------------------------------------
/commands/eval.js:
--------------------------------------------------------------------------------
1 | const Discord = require("discord.js");
2 |
3 |
4 | module.exports = {
5 | forDebug: true,
6 | data: {
7 | description: "Teste un code avec le bot",
8 | options: [
9 | {
10 | type: Discord.ApplicationCommandOptionType.String,
11 | name: "code",
12 | description: "Le code a tester",
13 | required: true
14 | }
15 | ],
16 | },
17 | execute: async (client, interaction) => {
18 | // Owner verification
19 | client.application = await client.application.fetch();
20 | let owner = client.application.owner;
21 | if (!owner.tag) {
22 | owner = owner.owner;
23 | }
24 | if (owner.id !== interaction.user.id) {
25 | return interaction.editReply("❌ | Vous n'avez pas la permission de faire cette commande !");
26 | }
27 | let content = interaction.options.getString("code", true);
28 |
29 | if (content.includes("client.token")) {
30 | content = content
31 | .replace("client.token", "'[TOKEN HIDDEN]'");
32 |
33 | }
34 | const result = new Promise(async (resolve) => resolve(await eval(content)));
35 | return result.then(async (output) => {
36 | if (typeof output !== "string") {
37 | output = require("util").inspect(output, { depth: 0 });
38 | }
39 | if (output.includes(client.token)) {
40 | output = output.replace(client.token, "[TOKEN HIDDEN]");
41 | }
42 | return interaction.editReply({
43 | embeds: [new Discord.EmbedBuilder().setColor("#36393F").setDescription("```js\n"+output+"\n```")]
44 | });
45 | }).catch(async (err) => {
46 | err = err.toString();
47 | if (err.includes(client.token)) {
48 | err = err.replace(client.token, "[TOKEN HIDDEN]");
49 | }
50 | return interaction.editReply({
51 | embeds: [new Discord.EmbedBuilder().setColor("#36393F").setDescription("```js\n"+err+"\n```")]
52 | });
53 | });
54 | }
55 | };
--------------------------------------------------------------------------------
/commands/fichier.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, ApplicationCommandOptionType, SelectMenuBuilder, ActionRowBuilder } = require("discord.js");
2 |
3 | module.exports = {
4 | data: {
5 | description: "Vous fournis le contenu d'un cours",
6 | options: [
7 | {
8 | type: ApplicationCommandOptionType.String,
9 | name: "matière",
10 | description: "Sélectionnez la matière",
11 | required: true,
12 | autocomplete: true
13 | },
14 | {
15 | type: ApplicationCommandOptionType.String,
16 | name: "fichier",
17 | description: "Sélectionnez le fichier voulu",
18 | required: false,
19 | autocomplete: true
20 | }
21 | ],
22 | },
23 | execute: async (client, interaction) => {
24 | await client.session.files().then(async (files) => {
25 | const subject = interaction.options.getString("matière");
26 | let data = files.filter(f => f.subject === subject);
27 | const file = interaction.options.getString("fichier");
28 |
29 | if (!data.length) interaction.editReply("⚠ | Aucune donnée n'a été trouvée pour cette matière");
30 |
31 | const components = [];
32 | if (data.length > 1) {
33 | const selectMenu = new SelectMenuBuilder()
34 | .setCustomId("menu_files")
35 | .setPlaceholder("Sélectionnez un fichier")
36 | .setMinValues(1)
37 | .setMaxValues(1)
38 | .addOptions(data.map(f => {
39 | return ({
40 | label: f.name ?? "Lien",
41 | value: f.id + "|" + f.subject,
42 | description: f.time.toLocaleString(),
43 | default: f.id === file
44 | });
45 | }));
46 | components.push(new ActionRowBuilder().addComponents(selectMenu));
47 | }
48 |
49 | let dataToSend = data.find(f => f.id === file);
50 | if (!dataToSend) dataToSend = data[0];
51 |
52 | const properties = await client.functions.getFileProperties(dataToSend);
53 | if (properties.type === "link") {
54 | interaction.editReply({
55 | content: `🔗 | [${properties.name ?? properties.url}](${properties.url} "${properties.url}")`,
56 | components
57 | });
58 | } else {
59 | const embed = new EmbedBuilder()
60 | .setColor("#70C7A4")
61 | .setAuthor({
62 | name: properties.subject,
63 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
64 | url: process.env.PRONOTE_URL,
65 | })
66 | .setTitle(properties.name)
67 | .setDescription(`📅 | **${properties.time.toLocaleString()}**`)
68 | .setFooter({text: "Bot par Merlode#8128"});
69 | // detect if file is an image
70 | if (properties.name.match(/\.(jpeg|jpg|gif|png)$/) != null) {
71 | embed.setImage("attachment://" + properties.name);
72 | }
73 | interaction.editReply({
74 | embeds: [embed],
75 | files: [properties.attachment],
76 | components,
77 | fetchReply: true
78 | }).then(async e => {
79 | if (!e) e = await interaction.fetchReply();
80 | embed.setURL(e.attachments.first().url);
81 | interaction.editReply({embeds: [embed]});
82 | });
83 | }
84 |
85 | });
86 |
87 | },
88 | };
--------------------------------------------------------------------------------
/commands/graph.js:
--------------------------------------------------------------------------------
1 |
2 | const { AttachmentBuilder, EmbedBuilder, ApplicationCommandOptionType, Colors } = require("discord.js");
3 | const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
4 | const width = 800;
5 | const height = 300;
6 | // White color and bold font
7 | const ticksOptions = { ticks: { font: {weight: "bold"}, color: "#fff"} };
8 | const options = {
9 | // Hide legend
10 | plugins: {legend: { /*display: false,*/ labels: {
11 | font: {weight: "bold"}, color: "#fff"
12 | }}},
13 | scales: { yAxes: ticksOptions, xAxes: ticksOptions }
14 | };
15 |
16 | const generateCanvas = async (joinedXDays, lastXDays) => {
17 | const canvasRenderService = new ChartJSNodeCanvas({ width, height });
18 | const image = await canvasRenderService.renderToBuffer({
19 | type: "line",
20 | data: {
21 | labels: lastXDays,
22 | datasets: [
23 | {
24 | label: "Moyenne",
25 | data: joinedXDays,
26 | // The color of the line (the same as the fill color with full opacity)
27 | borderColor: "#70C7A4",
28 | // Fill the line with color
29 | fill: true,
30 | // Blue color and low opacity
31 | backgroundColor: "rgba(112,199,164,0.1)"
32 | }
33 | ]
34 | },
35 | options
36 | });
37 | return new AttachmentBuilder(image, {
38 | name: "graph.png",
39 | description: "Graphique de l'évolution de la moyenne"
40 | });
41 | };
42 |
43 |
44 | module.exports = {
45 | data: {
46 | description: "Génère un graphique de l'évolution des moyennes",
47 | options: [
48 | {
49 | type: ApplicationCommandOptionType.String,
50 | name: "matière",
51 | description: "Sélectionnez l'historique d'une matière voulue",
52 | required: false,
53 | autocomplete: true,
54 | },
55 | {
56 | type: ApplicationCommandOptionType.String,
57 | name: "moyenne",
58 | description: "Sélectionnez l'historique d'une moyenne spécifique",
59 | required: false,
60 | choices: [
61 | {name: "Élève", value: "student"},
62 | {name: "Classe", value: "studentClass"}
63 | ]
64 | },
65 | {
66 | type: ApplicationCommandOptionType.Integer,
67 | name: "nombre",
68 | description: "Donne le nombre de valeur à afficher",
69 | required: false,
70 | }
71 | ],
72 | },
73 | execute: async (client, interaction) => {
74 | const subject = interaction.options.getString("matière", false);
75 | const averageType = interaction.options.getString("moyenne", false) ?? "student";
76 | let number = interaction.options.getInteger("nombre", false) ?? 25;
77 |
78 | if (number < 0) number = -number;
79 | if (number > 25) number = 25;
80 | if (number === 0) number = 1;
81 | let data = [];
82 |
83 | if (subject) {
84 | data = client.cache.marks.subjects.find(s => s.name === subject)?.averagesHistory;
85 | } else {
86 | data = client.cache.marks.averages?.history;
87 | }
88 | if (!data) return interaction.editReply({
89 | embeds: [new EmbedBuilder()
90 | .setTitle("Erreur")
91 | .setDescription("Aucune donnée n'a été trouvée. Réessayez plus tard, une fois que vous aurez des notes")
92 | .setColor(Colors.Red)
93 | ]
94 | });
95 | data.splice(number);
96 |
97 | const graph = await generateCanvas(data.map(o => o[averageType]), data.map(o => {
98 | const timestamp = new Date(o.date);
99 | const day = `${timestamp.getDate()}`.length === 1 ? `0${timestamp.getDate()}` : timestamp.getDate();
100 | const month = `${timestamp.getMonth()+1}`.length === 1 ? `0${timestamp.getMonth()+1}` : (timestamp.getMonth()+1);
101 |
102 | return day +"/"+ month+"/"+ timestamp.getFullYear();
103 | }));
104 | const embed = new EmbedBuilder()
105 | .setColor("#70C7A4")
106 | .setTitle(`Graphique des moyennes ${subject ? `de \`${subject.toUpperCase()}\` ` : ""}pour ${averageType === "student" ? "l'élève": "la classe"}`)
107 | .setImage("attachment://graph.png")
108 | .setFooter({text: "Bot par Merlode#8128"});
109 |
110 |
111 | return interaction.editReply({embeds: [embed], files: [graph]}).catch(console.error);
112 | },
113 | };
--------------------------------------------------------------------------------
/commands/help.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder } = require("discord.js");
2 |
3 |
4 | module.exports = {
5 | data: {
6 | description: "Vous donne la liste des commandes",
7 | options: [],
8 | },
9 | execute: async (client, interaction) => {
10 | let commandString = (await client.application.commands.fetch()).map(c => `${c.name}:${c.id}>: ${c.description}`).join("\n");
11 | let strings = client.functions.splitMessage(commandString, { maxLength: 1024 });
12 | const embed = new EmbedBuilder()
13 | .setColor("#70C7A4")
14 | .setTitle("Liste des commandes")
15 | .setFooter({text: "Bot par Merlode#8128"})
16 | .setDescription(
17 | "Le bot a été développé par Merlode#8128 et est open-source sur [GitHub](https://github.com/Merlode11/pronote-bot-discord)\n"
18 | + "Le bot utilise les commandes slash de Discord, pour les utiliser, il suffit de taper `/` dans un salon textuel.\n")
19 | .addFields(strings.map((s, i) => ({name: i === 0 ? "Commandes" : "\u200b", value: s, inline: false})));
20 | return interaction.editReply({ embeds: [embed] });
21 | },
22 | };
--------------------------------------------------------------------------------
/commands/history.js:
--------------------------------------------------------------------------------
1 |
2 | const { EmbedBuilder, ApplicationCommandOptionType, Colors} = require("discord.js");
3 |
4 | module.exports = {
5 | data: {
6 | description: "Vous donne l'historique des moyennes sur 25 périodes",
7 | options: [
8 | {
9 | type: ApplicationCommandOptionType.String,
10 | name: "matière",
11 | description: "Sélectionne l'historique d'une matière spécifique",
12 | required: false,
13 | autocomplete: true,
14 | },
15 | {
16 | type: ApplicationCommandOptionType.String,
17 | name: "moyenne",
18 | description: "Sélectionne l'historique d'une moyenne spécifique",
19 | required: false,
20 | choices: [
21 | {name: "Élève", value: "student"},
22 | {name: "Classe", value: "studentClass"}
23 | ]
24 | },
25 | {
26 | type: ApplicationCommandOptionType.Integer,
27 | name: "nombre",
28 | description: "Donne le nombre de valeur à afficher",
29 | required: false,
30 | min_value: 1,
31 | max_value:25
32 | }
33 | ],
34 | },
35 | execute: async (client, interaction) => {
36 | const subject = interaction.options.getString("matière", false);
37 | const averageType = interaction.options.getString("moyenne", false) ?? "student";
38 | let number = interaction.options.getInteger("nombre", false) ?? 25;
39 |
40 | if (number < 0) number = -number;
41 | if (number > 25) number = 25;
42 | if (number === 0) number = 1;
43 | let data = [];
44 |
45 | if (subject) {
46 | data = client.cache.marks.subjects.find(s => s.name === subject).averagesHistory;
47 | } else {
48 | data = client.cache.marks.averages.history;
49 | }
50 | if (!data) return interaction.editReply({
51 | embeds: [new EmbedBuilder()
52 | .setTitle("Erreur")
53 | .setDescription("Aucune donnée n'a été trouvée. Réessayez plus tard, une fois que vous aurez des notes")
54 | .setColor(Colors.Red)
55 | ]
56 | });
57 |
58 | const embed = new EmbedBuilder()
59 | .setColor("#70C7A4")
60 | .setTitle(`Historique des moyennes ${subject ? `de \`${subject.toUpperCase()}\` ` : ""}pour ${averageType === "student" ? "l'élève": "la classe"}`)
61 | .setFooter({text: "Bot par Merlode#8128"});
62 |
63 | data.splice(number);
64 | data.forEach((average, index) => {
65 | const timestamp = new Date(average.date);
66 | const day = `${timestamp.getDate()}`.length === 1 ? `0${timestamp.getDate()}` : timestamp.getDate();
67 | const month = `${timestamp.getMonth()+1}`.length === 1 ? `0${timestamp.getMonth()+1}` : (timestamp.getMonth()+1);
68 |
69 | let editStr = "";
70 | if (data[index-1]) {
71 | const edit = Math.round((average[averageType] - data[index - 1][averageType]) * 100) / 100;
72 | editStr = `\n*Modification: ${edit > 0 ? "+"+edit : ""}*`;
73 | }
74 |
75 | embed.addFields([{
76 | name: day +"/" + month + "/" + timestamp.getFullYear(),
77 | value: `**Moyenne: ${average[averageType]}**` + editStr
78 | }]);
79 | });
80 | return interaction.editReply({embeds: [embed]});
81 | },
82 | };
--------------------------------------------------------------------------------
/commands/infos.js:
--------------------------------------------------------------------------------
1 | function getFetchDate(session){
2 | let from = new Date();
3 | if (from < session.params.firstDay) {
4 | from = session.params.firstDay;
5 | }
6 |
7 | const to = new Date(from.getTime());
8 |
9 | return { from, to };
10 | }
11 | const { EmbedBuilder } = require("discord.js");
12 |
13 |
14 | module.exports = {
15 | data: {
16 | description: "Vous fournis les informations sur l'élève",
17 | options: [],
18 | },
19 | execute: async (client, interaction) => {
20 | const session = client.session;
21 |
22 | const { from, to } = getFetchDate(session);
23 | const timetable = await session.timetable(from, to);
24 | const alltimetable = await session.timetable(session.params.firstDay, session.params.lastDay);
25 |
26 | const evaluations = await session.evaluations();
27 | const absences = await session.absences();
28 | const nombreAbsences = absences.absences.length;
29 | const nombreRetards = absences.delays.length;
30 | const allhomeworks = await session.homeworks(session.params.firstDay, session.params.lastDay);
31 | const name = session.user.name;
32 | const classe = session.user.studentClass.name;
33 |
34 | const marks = await session.marks(session.params.firstDay, session.params.lastDay);
35 | const count = marks.subjects.filter(e => e !== "Abs").length;
36 | const moyenne = Math.floor(marks.subjects.map((value) => value.averages.student).reduce((a, b) => a + b) / count *100) / 100;
37 |
38 | const embed = new EmbedBuilder()
39 | .setColor("#70C7A4")
40 | .setAuthor({
41 | name: interaction.user.username,
42 | iconURL: interaction.user.displayAvatarURL({dynamic: true})
43 | })
44 | .setThumbnail(session.user.avatar)
45 | .setTitle(name+", "+classe)
46 | .setDescription("Cours aujourd'hui/Année : "+timetable.length+" | "+alltimetable.length+"\nControles : "+evaluations.length+"\nAbsences/Retards : "+nombreAbsences+"/"+nombreRetards+"\nDevoirs : "+allhomeworks.length+"\nMoyenne : "+moyenne)
47 | .setFooter({text: "Bot par Merlode#8128"});
48 |
49 | return await interaction.editReply({
50 | embeds: [embed]
51 | });
52 | },
53 | };
--------------------------------------------------------------------------------
/commands/logout.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = {
4 | data: {
5 | description: "Se déconecter de Pronote",
6 | options: [],
7 | },
8 | execute: async (client, interaction) => {
9 | client.session.setKeepAlive(false);
10 | await client.session.logout();
11 |
12 | return await interaction.editReply("✅ | Je me suis bien déconnecté de **Pronote** !\n*Note: À la nouvelle vérification, je me reconnecterai*");
13 | },
14 | };
--------------------------------------------------------------------------------
/commands/menu.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, ApplicationCommandOptionType, SelectMenuBuilder, ActionRowBuilder } = require("discord.js");
2 |
3 | module.exports = {
4 | data: {
5 | description: "Vous fournis le menu d'aujourd'hui",
6 | options: [
7 | {
8 | type: ApplicationCommandOptionType.String,
9 | name: "date",
10 | description: "Sélectionnez la date du menu souhaité",
11 | required: false,
12 | autocomplete: true
13 | }
14 | ],
15 | },
16 | execute: async (client, interaction) => {
17 | const dateUser = interaction.options.getString("date");
18 | let date = new Date();
19 | if (dateUser) {
20 | let parsed = dateUser.split("/");
21 | date = new Date(parseInt(parsed[2]), parseInt(parsed[1]) - 1, parseInt(parsed[0]));
22 | }
23 |
24 | await client.session.menu(date).then(async (menus) => {
25 | const menu = menus[0];
26 |
27 | const embed = new EmbedBuilder()
28 | .setTitle("Menu du jour")
29 | .setColor("#70C7A4");
30 | if (menu) embed
31 | .setDescription(`Menu du ${menu.date}`)
32 | .setTimestamp(new Date(menu.date))
33 | .addFields(menu.meals[0].map((meal) => {
34 | meal = meal[0];
35 | return {
36 | name: meal.name,
37 | value: meal.labels.map((label) => {
38 | return `• ${label}`;
39 | }).join("\n") || "\u200b",
40 | inline: false
41 | };
42 | }));
43 | else embed.setDescription("Aucun menu n'a été trouvé pour aujourd'hui");
44 |
45 | const warnEmbed = new EmbedBuilder()
46 | .setTitle("Attention")
47 | .setDescription("Cette commande est en cours de développement. Comme le développeur ne possède pas les menus sur son pronote, il ne peut pas tester correctement cette commande. Si vous rencontrez des problèmes ou que vous voulez aider, merci de contacter le développeur sur github.")
48 | .setColor("#FFA500");
49 |
50 | const current = new Date(date.getTime());
51 | const week = [];
52 | for (let i = 1; i <= 7; i++) {
53 | let first = current.getDate() - current.getDay() + i;
54 | let day = new Date(current.setDate(first));
55 | if (day.getDay() !== 0) week.push(day);
56 | }
57 | let weekString = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
58 |
59 | const selectMenu = new SelectMenuBuilder()
60 | .setCustomId("menus_date")
61 | .setPlaceholder("Sélectionnez une date pour voir les cours")
62 | .addOptions(week.map((day) => {
63 | return {
64 | label: day.toLocaleDateString(),
65 | value: day.toLocaleDateString(),
66 | description: weekString[day.getDay()] + " " + day.toLocaleDateString().split("/")[0],
67 | default: day.toLocaleDateString() === date.toLocaleDateString()
68 | };
69 | }))
70 | .setMaxValues(1)
71 | .setMinValues(1);
72 |
73 |
74 | return await interaction.editReply({
75 | embeds: [embed, warnEmbed],
76 | components: [new ActionRowBuilder().addComponents(selectMenu), client.bugActionRow]
77 | });
78 | });
79 |
80 | },
81 | };
--------------------------------------------------------------------------------
/commands/notes.js:
--------------------------------------------------------------------------------
1 | const { AttachmentBuilder, EmbedBuilder, ApplicationCommandOptionType, Colors, SelectMenuBuilder, ActionRowBuilder} = require("discord.js");
2 | const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
3 | const width = 800;
4 | const height = 300;
5 | // White color and bold font
6 | const ticksOptions = { ticks: { font: {weight: "bold"}, color: "#fff"} };
7 | const options = {
8 | // Hide legend
9 | plugins: {legend: { /*display: false,*/ labels: {
10 | font: {weight: "bold"}, color: "#fff"
11 | }}},
12 | scales: { yAxes: ticksOptions, xAxes: ticksOptions }
13 | };
14 |
15 | const generateCanvas = async (joinedXDays, lastXDays) => {
16 | const canvasRenderService = new ChartJSNodeCanvas({ width, height });
17 | const image = await canvasRenderService.renderToBuffer({
18 | type: "line",
19 | data: {
20 | labels: lastXDays,
21 | datasets: [
22 | {
23 | label: "Moyenne",
24 | data: joinedXDays,
25 | // The color of the line (the same as the fill color with full opacity)
26 | borderColor: "#70C7A4",
27 | // Fill the line with color
28 | fill: true,
29 | // Blue color and low opacity
30 | backgroundColor: "rgba(112,199,164,0.1)"
31 | }
32 | ]
33 | },
34 | options
35 | });
36 | return new AttachmentBuilder(image, {
37 | name: "graph.png",
38 | description: "Graphique de l'évolution de la moyenne"
39 | });
40 | };
41 |
42 |
43 | module.exports = {
44 | data: {
45 | description: "Voir vos notes",
46 | options: [
47 | {
48 | type: ApplicationCommandOptionType.String,
49 | name: "matière",
50 | description: "Sélectionnez la matière voulue",
51 | required: false,
52 | autocomplete: true,
53 | }
54 | ],
55 | },
56 | execute: async (client, interaction) => {
57 | const subject = interaction.options.getString("matière", false);
58 | let data = [];
59 |
60 | if (subject) {
61 | data = client.cache.marks.subjects.find(s => s.name === subject);
62 | } else {
63 | // Get the 25 last marks
64 | client.cache.marks.subjects.forEach(s => {
65 | if (s.marks.length) {
66 | s.marks.forEach(m => {
67 | m.subject = s.name;
68 | });
69 | data = data.concat(s.marks);
70 | }
71 | });
72 | data = data.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 25);
73 | // supprimer les doublons
74 | data = data.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i);
75 | }
76 | if (!data) return interaction.editReply({
77 | embeds: [new EmbedBuilder()
78 | .setTitle("Erreur")
79 | .setDescription("Aucune donnée n'a été trouvée. Réessayez plus tard, une fois que vous aurez des notes")
80 | .setColor(Colors.Red)
81 | ]
82 | });
83 |
84 | const embed = new EmbedBuilder()
85 | .setColor("#70C7A4");
86 | const attachments = [];
87 | const components = [];
88 | const selectNote = new SelectMenuBuilder()
89 | .setPlaceholder("Voir plus de précisions sur une note")
90 | .setMinValues(0)
91 | .setMaxValues(1);
92 | components.push(new ActionRowBuilder().addComponents(selectNote));
93 |
94 | if (subject) {
95 | embed.setAuthor({
96 | name: subject,
97 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
98 | url: process.env.PRONOTE_URL,
99 | })
100 | .setTitle("Moyenne: " + data.averages.student)
101 | .setColor(data.color)
102 | .addFields(data.marks.reverse().map(mark => {
103 | mark.date = new Date(mark.date);
104 | return {
105 | name: "Le " + mark.date.toLocaleDateString(),
106 | value: "Note: **" + mark.value + "/" + mark.scale + "**\nMoyenne du groupe: " + mark.average + "/" + mark.scale,
107 | inline: false
108 | };
109 | }))
110 | .setImage("attachment://graph.png")
111 | .setFooter({text: "Bot par Merlode#8128"});
112 |
113 | const graph = await generateCanvas(data.averagesHistory.map(o => o.student), data.averagesHistory.map(o => {
114 | o.date = new Date(o.date);
115 | return o.date.toLocaleDateString();
116 | }));
117 | attachments.push(graph);
118 | selectNote.addOptions(data.marks.map(mark => {
119 | return {
120 | label: (mark.value + "/" + mark.scale) + (mark.title ? (" - " + mark.title) : ""),
121 | value: mark.id,
122 | description: "Le " + mark.date.toLocaleDateString(),
123 | emoji: "📝"
124 | };
125 | }))
126 | .setCustomId("select_note-" + data.name);
127 | } else {
128 | embed.setAuthor({
129 | name: "Notes générales",
130 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
131 | url: process.env.PRONOTE_URL,
132 | })
133 | .setTitle("Moyenne générale: " + client.cache.marks.averages.student)
134 | .addFields(data.map(mark => {
135 | mark.date = new Date(mark.date);
136 | return {
137 | name: mark.subject,
138 | value: "Note: **" + mark.value + "/" + mark.scale +
139 | "**\nLe: " + mark.date.toLocaleDateString() +
140 | "\nMoyenne du groupe: " + mark.average + "/" + mark.scale,
141 | inline: false
142 | };
143 | }))
144 | .setImage("attachment://graph.png")
145 | .setFooter({text: "Bot par Merlode#8128"});
146 | const graph = await generateCanvas(client.cache.marks.averages.history.map(o => o.student), client.cache.marks.averages.history.map(o => {
147 | o.date = new Date(o.date);
148 | return o.date.toLocaleDateString();
149 | }));
150 | attachments.push(graph);
151 | selectNote.addOptions(data.map(mark => {
152 | mark.date = new Date(mark.date);
153 | return {
154 | label: mark.subject + " - " + (mark.value + "/" + mark.scale),
155 | value: mark.id,
156 | description: "Le " + mark.date.toLocaleDateString(),
157 | emoji: "📝"
158 | };
159 | }))
160 | .setCustomId("select_note-recent");
161 | }
162 |
163 |
164 | return interaction.editReply({embeds: [embed], files: attachments, components}).catch(console.error);
165 | },
166 | };
167 |
--------------------------------------------------------------------------------
/commands/ping.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder } = require("discord.js");
2 |
3 |
4 | module.exports = {
5 | data: {
6 | description: "Ping le bot",
7 | options: [],
8 | },
9 | execute: async (client, interaction) => {
10 | const msg = await interaction.fetchReply();
11 |
12 |
13 | const embed = new EmbedBuilder()
14 | .setColor("#70C7A4")
15 | .setTitle("🏓Pong !")
16 | .addFields([
17 | {
18 | name: "Lantance du bot",
19 | value: `**${msg.createdTimestamp - interaction.createdTimestamp}**ms`,
20 | },
21 | {
22 | name: "Latance de l'API",
23 | value: `**${Math.round(client.ws.ping)}**ms`,
24 | },
25 | {
26 | name: "Mémoire",
27 | value: `\`${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}\` MB`,
28 | inline: true,
29 | },
30 | {
31 | name: "Temps de fonctionnement",
32 | value: `${Math.floor(client.uptime / 1000 / 60).toString()} minutes`,
33 | inline: true,
34 | },
35 | {
36 | name: "Version",
37 | value: `\`discord.js : ${require("../package.json").dependencies["discord.js"]}\``+
38 | `\n\`node.js : ${process.version}\`` +
39 | `\n\`pronote-api-maintained : ${require("../package.json").dependencies["pronote-api-maintained"]}\``,
40 | inline: true,
41 | },
42 | {
43 | name: "Salons",
44 | value: `${client.channels.cache.size.toString()}`,
45 | inline: true,
46 | },
47 | {
48 | name: "Utilisateurs",
49 | value: `${client.guilds.cache.map(g => g.memberCount).reduce((a, b) => a + b)}`,
50 | inline: true,
51 | },
52 | ])
53 | .setFooter({text: "Bot par Merlode#8128"});
54 | return interaction.editReply({embeds: [embed], content: " "}).catch(console.error);
55 | }
56 | };
--------------------------------------------------------------------------------
/commands/points-bac.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder, SelectMenuBuilder, ActionRowBuilder, ApplicationCommandOptionType } = require("discord.js");
2 |
3 | const fs = require("fs");
4 |
5 | delete require.cache[require.resolve("../utils/subjects")];
6 | const subjects = require("../utils/subjects");
7 |
8 | const nameGenerator = (subject) => {
9 | if (subjects[subject]) {
10 | return subjects[subject].name;
11 | } else if (subject === "specialite") {
12 | return "Spécialité abandonnée";
13 | } else if (subject.startsWith("opt")) {
14 | return "Option " + subject.replace("opt", "");
15 | } else if (subject.startsWith("specialite")) {
16 | return "Spécialité " + subject.replace("opt", "");
17 | } else if (subject === "fr_ecrit") {
18 | return "Écrit de français *(Épreuve)*";
19 | } else if (subject === "fr_oral") {
20 | return "Oral de français *(Épreuve)*";
21 | } else if (subject === "grand_oral") {
22 | return "Grand oral *(Épreuve)*";
23 | }
24 | return subject.charAt(0).toUpperCase() + subject.slice(1);
25 | };
26 |
27 | module.exports = {
28 | data: {
29 | description: "Voir le nombre de points que vous avec actuellement pour le BAC",
30 | options: [
31 | {
32 | type: ApplicationCommandOptionType.Integer,
33 | name: "fr-oral",
34 | description: "Note de l'épreuve de français orale de première",
35 | required: false,
36 | minValue: 0,
37 | maxValue: 20,
38 | },
39 | {
40 | type: ApplicationCommandOptionType.Integer,
41 | name: "fr-ecrit",
42 | description: "Note de l'épreuve de français écrite de première",
43 | required: false,
44 | minValue: 0,
45 | maxValue: 20,
46 | },
47 | {
48 | type: ApplicationCommandOptionType.Integer,
49 | name: "philosophie",
50 | description: "Note de l'épreuve de philosophie en terminale",
51 | required: false,
52 | minValue: 0,
53 | maxValue: 20,
54 | },
55 | {
56 | type: ApplicationCommandOptionType.Integer,
57 | name: "grand-oral",
58 | description: "Note de l'épreuve du grand oral en terminale",
59 | required: false,
60 | minValue: 0,
61 | maxValue: 20,
62 | },
63 | {
64 | type: ApplicationCommandOptionType.String,
65 | name: "specialite1",
66 | description: "Note de l'épreuve de spécialité 1 en terminale. Si vous avez plusieurs notes, séparez-les par \",\"",
67 | required: false,
68 | },
69 | {
70 | type: ApplicationCommandOptionType.String,
71 | name: "specialite2",
72 | description: "Note de l'épreuve de spécialité 1 en terminale. Si vous avez plusieurs notes, séparez-les par \",\"",
73 | required: false,
74 | },
75 | {
76 | type: ApplicationCommandOptionType.Boolean,
77 | name: "sauver",
78 | description: "Sauvegarde les notes pour ne pas avoir à les rentrer à chaque fois",
79 | required: false,
80 | },
81 | {
82 | type: ApplicationCommandOptionType.Boolean,
83 | name: "skip",
84 | description: "Passer les erreurs qui demandent de choisir la matière",
85 | required: false,
86 | }
87 | ],
88 | },
89 | execute: async (client, interaction) => {
90 | const session = client.session;
91 | const classe = session.user.studentClass.name;
92 |
93 | if (!classe.startsWith("1") && !classe.toUpperCase().startsWith("T")) {
94 | return await interaction.editReply("❌ | Vous n'êtes pas dans une classe de bac");
95 | }
96 |
97 | const caches = fs.readdirSync("./").filter(f => f.endsWith(".json") && f.startsWith("cache_"));
98 | const notes = {
99 | "1": {
100 | subjects: [],
101 | subjectAverage: {
102 | "histoire_geographie": 0,
103 | "enseignement_scientifique": 0,
104 | "lva": 0,
105 | "lvb": 0,
106 | "emc": 0,
107 | "specialite": 0,
108 | "opt1": 0,
109 | "opt2": 0,
110 | }
111 | },
112 | "T": {
113 | subjects: [],
114 | subjectAverage: {
115 | "histoire_geographie": 0,
116 | "enseignement_scientifique": 0,
117 | "lva": 0,
118 | "lvb": 0,
119 | "eps": 0,
120 | "emc": 0,
121 | "opt1": 0,
122 | "opt2": 0,
123 | "philosophie": 0,
124 | "specialite1": 0,
125 | "specialite2": 0,
126 | "grand_oral": 0,
127 | }
128 | },
129 | };
130 | caches.forEach(cache => {
131 | const data = require("../" + cache);
132 | notes[data.classe?.startsWith("1") ? "1" : "T"].subjects = data.marks.subjects.map(subject => {
133 | return {
134 | name: subject.name,
135 | average: subject.averages.student,
136 | };
137 | });
138 | if (data.classe?.startsWith("1") && data.marks.bac_fr) {
139 | notes["1"].subjectAverage["fr_oral"] = data.marks.bac_fr.oral;
140 | notes["1"].subjectAverage["fr_ecrit"] = data.marks.bac_fr.ecrit;
141 | }
142 | if (data.classe?.startsWith("T") && data.marks.bac) {
143 | notes["T"].subjectAverage["philosophie"] = data.marks.bac.philosophie;
144 | notes["T"].subjectAverage["grand_oral"] = data.marks.bac.grand_oral;
145 | notes["T"].subjectAverage["specialite1"] = data.marks.bac.specialite1;
146 | notes["T"].subjectAverage["specialite2"] = data.marks.bac.specialite2;
147 | }
148 | });
149 |
150 | if (interaction.options.getInteger("fr-oral")) {
151 | notes["1"].subjectAverage["fr_oral"] = interaction.options.getInteger("fr-oral") ?? 0;
152 | }
153 | if (interaction.options.getInteger("fr-ecrit")) {
154 | notes["1"].subjectAverage["fr_ecrit"] = interaction.options.getInteger("fr-ecrit") ?? 0;
155 | }
156 | if (interaction.options.getInteger("philosophie")) {
157 | notes["T"].subjectAverage["philosophie"] = interaction.options.getInteger("philosophie") ?? 0;
158 | }
159 | if (interaction.options.getInteger("grand-oral")) {
160 | notes["T"].subjectAverage["grand_oral"] = interaction.options.getInteger("grand-oral") ?? 0;
161 | }
162 | if (interaction.options.getString("specialite1")) {
163 | notes["T"].subjectAverage["specialite1"] = interaction.options.getString("specialite1") ? interaction.options.getString("specialite1").split(",").map(note => parseInt(note)).reduce((a, b) => a + b, 0) / (interaction.options.getString("specialite1") ?? "").split(",").length : 0;
164 | }
165 | if (interaction.options.getString("specialite2")) {
166 | notes["T"].subjectAverage["specialite2"] = interaction.options.getString("specialite1") ? interaction.options.getString("specialite2").split(",").map(note => parseInt(note)).reduce((a, b) => a + b, 0) /(interaction.options.getString("specialite2") ?? "").split(",").length : 0;
167 | }
168 | if (interaction.options.getBoolean("sauver")) {
169 | const data1 = require("../" + caches.find(c => c.split("_")[1].startsWith("1")));
170 | data1.marks.bac_fr = {
171 | oral: notes["1"].subjectAverage["fr_oral"],
172 | ecrit: notes["1"].subjectAverage["fr_ecrit"],
173 | };
174 | fs.writeFileSync(data1.classe ? "cache_"+data1.classe.toUpperCase()+".json" : "cache.json", JSON.stringify(data1, null, 4), "utf-8");
175 | const dataT = require("../" + caches.find(c => c.split("_")[1].startsWith("T")));
176 | dataT.marks.bac = {
177 | philosophie: notes["T"].subjectAverage["philosophie"],
178 | grand_oral: notes["T"].subjectAverage["grand_oral"],
179 | specialite1: notes["T"].subjectAverage["specialite1"],
180 | specialite2: notes["T"].subjectAverage["specialite2"],
181 | };
182 | fs.writeFileSync(dataT.classe ? "cache_"+dataT.classe.toUpperCase()+".json" : "cache.json", JSON.stringify(dataT, null, 4), "utf-8");
183 | }
184 |
185 | let total = 0;
186 | let errors = [];
187 | let founds = {
188 | "1":[],
189 | "T":[],
190 | };
191 | // 1ère
192 | Object.keys(notes["1"].subjectAverage).forEach(subject => {
193 | if (![ "opt1", "opt2", "specialite", "fr_ecrit", "fr_oral" ].includes(subject)) {
194 | let actSubjects = notes["1"].subjects.filter(s => s.name.match(subjects[subject]?.regex));
195 | if (actSubjects.length === 1) {
196 | founds["1"].push(actSubjects[0].name);
197 | notes["1"].subjectAverage[subject] = actSubjects[0].average * subjects[subject]?.coef["1"];
198 | total += notes["1"].subjectAverage[subject];
199 | } else if (notes["1"].subjects.length > 0) {
200 | errors.push("1|" + subject);
201 | }
202 | } else if (subject.startsWith("fr_")) {
203 | notes["1"].subjectAverage[subject] = notes["1"].subjectAverage[subject] * 5;
204 | total += notes["1"].subjectAverage[subject];
205 | } else if (subject.startsWith("opt")) {
206 | errors.push("1|" + subject);
207 | }
208 | });
209 | // Terminale
210 | Object.keys(notes["T"].subjectAverage).forEach(subject => {
211 | if (![ "opt1", "opt2" ].includes(subject)) {
212 | let actSubjects = notes["T"].subjects.filter(s => s.name.match(subjects[subject]?.regex));
213 | if (subject === "philosophie") {
214 | notes["T"].subjectAverage[subject] = notes["T"].subjectAverage[subject] * 4;
215 | total += notes["T"].subjectAverage[subject];
216 | } else if (subject === "grand_oral") {
217 | notes["T"].subjectAverage[subject] = notes["T"].subjectAverage[subject] * 14;
218 | total += notes["T"].subjectAverage[subject];
219 | } else if (subject.startsWith("specialite")) {
220 | notes["T"].subjectAverage[subject] = notes["T"].subjectAverage[subject] * 16;
221 | total += notes["T"].subjectAverage[subject];
222 | } else if (actSubjects.length === 1) {
223 | if (subjects[subject].controleContinu) {
224 | founds["T"].push(actSubjects[0].name);
225 | notes["T"].subjectAverage[subject] = actSubjects[0].average * subjects[subject]?.coef["T"];
226 | total += notes["T"].subjectAverage[subject];
227 | }
228 | } else if (notes["T"].subjects.length > 0) {
229 | errors.push("T|" + subject);
230 | }
231 | } else if (subject.startsWith("opt")) {
232 | errors.push("T|" + subject);
233 | }
234 | });
235 |
236 | let specialites1 = Object.keys(subjects).filter(s => s.startsWith("spe") && notes["1"].subjects.find(su => su.name.match(subjects[s].regex)));
237 | let specialitesT = Object.keys(subjects).filter(s => s.startsWith("spe") && notes["T"].subjects.find(su => su.name.match(subjects[s].regex)));
238 |
239 | if (specialitesT.length > 0 && specialites1.length > 0) {
240 | let specialite1 = specialites1.filter(s => !specialitesT.includes(s)).shift();
241 | notes["1"].subjectAverage["specialite"] = notes["1"].subjects.find(s => subjects[specialite1].regex.test(s.name)).average * subjects[specialite1].coef["1"];
242 | total += notes["1"].subjectAverage["specialite"];
243 | founds["1"].push(specialite1);
244 | founds["1"] = founds["1"].concat(specialitesT.map(s => s.name));
245 | } else {
246 | errors.push("1|specialite");
247 | }
248 | if (notes["1"].subjectAverage["specialite"] === 0 && !errors.includes("1|specialite")) {
249 | errors.push("1|specialite");
250 | }
251 |
252 | if (errors.length && !interaction.options.getBoolean("skip")) {
253 | let subjects = notes[errors[0].split("|")[0]].subjects.filter(s => !founds[errors[0].split("|")[0]].includes(s.name)).map(s => {
254 | return {
255 | label: s.name,
256 | description: "Moyenne : " + s.average,
257 | value: s.name,
258 | };
259 | });
260 |
261 | subjects.splice(24);
262 |
263 | subjects.push({
264 | label: "Sans notes | Aucune",
265 | description: "Vous n'avez pas encore de notes pour cette matière, ou vous n'avez pas de matière correspondante.",
266 | value: "0",
267 | });
268 |
269 |
270 | const select = new SelectMenuBuilder()
271 | .setCustomId("bac_" + errors[0].split("|")[1])
272 | .setPlaceholder("Sélectionnez une matière")
273 | .addOptions(subjects)
274 | .setMinValues(1)
275 | .setMaxValues(subjects.length >= 3 ? 3 : 1);
276 |
277 |
278 | const row = new ActionRowBuilder()
279 | .addComponents(select);
280 | await interaction.editReply({
281 | content: "❌ | Il y a eu une erreur lors du calcul de la moyenne. Une matière n'a pas été trouvée, veuillez sélectionner la matière correspondant à **" + nameGenerator(errors[0].split("|")[1]) + "** (*"+(errors[0].split("|")[0] === "1" ? "Première" : "Terminale")+"*)",
282 | components: [row],
283 | });
284 | }
285 | const message = await interaction.fetchReply();
286 | const filter = i => {
287 | return i.customId.startsWith("bac_") && i.user.id === interaction.user.id;
288 | };
289 |
290 | const componentCollector = message.createMessageComponentCollector({ filter, idle: 20000, dispose: true });
291 | if (!errors.length && !interaction.options.getBoolean("skip")) componentCollector.stop();
292 | componentCollector.on("collect", async i => {
293 | let subName = "Aucune";
294 | let classe = errors[0].split("|")[0];
295 | const subId = errors[0].split("|")[1];
296 | const chossedSub = notes[classe].subjects.filter(s => i.values.includes(s.name));
297 | const average = chossedSub.reduce((a, b) => a + b.average, 0) / chossedSub.length;
298 | if (i.values.includes("0") || i.values.includes("null")) {
299 | notes[classe].subjectAverage[subId] = 0;
300 | } else if (subId.startsWith("opt")) {
301 | founds[classe] = founds[classe].concat(i.values);
302 | notes[classe].subjectAverage[subId] = average * 2;
303 | total += notes[classe].subjectAverage[subId];
304 | subName = chossedSub.map(s => s.name).join(", ");
305 | } else if (subId === "specialite") {
306 | founds[classe] = founds[classe].concat(i.values);
307 | notes[classe].subjectAverage[subId] = average * 8;
308 | total += notes[classe].subjectAverage[subId];
309 | subName = chossedSub.map(s => s.name).join(", ");
310 | } else {
311 | founds[classe] = founds[classe].concat(i.values);
312 | const subject = notes[classe].subjects.find(s => s.name === i.values[0]);
313 | let subjectName = subId;
314 | if (!subjects[subjectName]) subjectName = Object.keys(subjects).find(s => subjects[s].regex.test(subject.name));
315 | notes[classe].subjectAverage[subId] = average * subjects[subjectName].coef[classe];
316 | total += notes[classe].subjectAverage[subId];
317 | subName = subject.name;
318 | }
319 | errors.shift();
320 | if (errors.length) {
321 | classe = errors[0].split("|")[0];
322 | const subjects = notes[classe].subjects.filter(s => !founds[classe].includes(s.name)).map(s => {
323 | return {
324 | label: s.name,
325 | description: "Moyenne : " + s.average,
326 | value: s.name,
327 | };
328 | });
329 |
330 | subjects.splice(24);
331 |
332 | subjects.push({
333 | label: "Sans notes | Aucune",
334 | description: "Vous n'avez pas encore de notes pour cette matière ou vous n'avez pas de matière correspondante.",
335 | value: "0",
336 | });
337 |
338 | const select = new SelectMenuBuilder()
339 | .setCustomId("bac_" + subId)
340 | .setPlaceholder("Sélectionnez une matière")
341 | .addOptions(subjects)
342 | .setMinValues(1)
343 | .setMaxValues(subjects.length >= 3 ? 3 : 1);
344 |
345 |
346 | const row = new ActionRowBuilder()
347 | .addComponents(select);
348 | await message.edit({
349 | content: "❌ | Il y a eu une erreur lors du calcul de la moyenne. Une matière n'a pas été trouvée, veuillez sélectionner la matière correspondant à **" + nameGenerator(errors[0].split("|")[1]) + "** (*"+(classe === "1" ? "Première" : "Terminale")+"*)",
350 | components: [row],
351 | });
352 | } else {
353 | componentCollector.stop();
354 | }
355 | await i.reply({
356 | content: "✅ | La matière **"+subName+"** a bien été ajouté à la moyenne !",
357 | ephemeral: true,
358 | });
359 | });
360 | componentCollector.on("end", async () => {
361 | if (errors.length && !interaction.options.getBoolean("skip")) {
362 | await interaction.editReply({
363 | content: "❌ | Vous n'avez pas répondu à temps",
364 | components: [],
365 | });
366 | } else {
367 | const embed = new EmbedBuilder()
368 | .setColor("#70C7A4")
369 | .setTitle("Notes du bac de " + session.user.name)
370 | .setFooter({text: "Bot par Merlode#8128"})
371 | .addFields([
372 | {
373 | name: "Matières de 1ère",
374 | value: Object.keys(notes["1"].subjectAverage).map(subject => {
375 | return nameGenerator(subject) + " : " + Math.round(notes["1"].subjectAverage[subject] * 100) / 100;
376 | }).join("\n"),
377 | },
378 | {
379 | name: "Matières de Terminale",
380 | value: Object.keys(notes["T"].subjectAverage).map(subject => {
381 | return nameGenerator(subject) + " : " + Math.round(notes["T"].subjectAverage[subject] * 100) / 100;
382 | }).join("\n"),
383 | }
384 | ]);
385 |
386 |
387 | let description = "Moyenne obtenue pour le bac : " + Math.round(total * 100) / 100 + "/2 000";
388 | if (total >= 1600) {
389 | description += "\n\n**Félicitations !** Vous avez obtenu votre bac avec mention **Très bien** !";
390 | embed.setThumbnail("https://i.imgur.com/hqYf8Fn.png");
391 | }
392 | else if (total >= 1400) {
393 | description += "\n\n**Félicitations !** Vous avez obtenu votre bac avec mention **Bien** !";
394 | embed.setThumbnail("https://i.imgur.com/ZO9q3YE.png");
395 | }
396 | else if (total >= 1200) {
397 | description += "\n\n**Félicitations !** Vous avez obtenu votre bac avec mention **Assez bien** !";
398 | embed.setThumbnail("https://i.imgur.com/neuhAFJ.png");
399 | }
400 | else if (total >= 1000) {
401 | description += "\n\n**Félicitations !** Vous avez obtenu votre bac !";
402 | embed.setThumbnail("https://i.imgur.com/SyK94Z3.png");
403 | }
404 | embed.setDescription(description);
405 |
406 | return await interaction.editReply({
407 | content: "✅ | La moyenne a bien été calculée !",
408 | embeds: [embed],
409 | components: [],
410 | });
411 | }
412 | });
413 | },
414 | };
--------------------------------------------------------------------------------
/commands/recheck.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = {
4 | data: {
5 | description: "Vérifier de nouveau les nouvelles notifications",
6 | options: [],
7 | },
8 | execute: async (client, interaction) => {
9 | delete require.cache[require.resolve("../utils/pronoteSynchronization")];
10 | await require("../utils/pronoteSynchronization")(client);
11 |
12 | return interaction.editReply("✅ | Une nouvelle vérification a bien été effectuée");
13 | },
14 | };
--------------------------------------------------------------------------------
/commands/reloadcommands.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 |
4 | module.exports = {
5 | forDebug: true,
6 | data: {
7 | description: "Modifie les arguments des slash commands",
8 | options: [],
9 | },
10 | execute: async (client, interaction) => {
11 |
12 | // Owner verification
13 | client.application = await client.application.fetch();
14 | let owner = client.application.owner;
15 | if (!owner.tag) {
16 | owner = owner.owner;
17 | }
18 | if (owner.id !== interaction.user.id) {
19 | return interaction.editReply("❌ | Vous n'avez pas la permission de faire cette commande !");
20 | }
21 | const commands = await client.application.commands.fetch();
22 | await fs.readdirSync("./commands/").filter(file => file.endsWith(".js")).forEach(file => {
23 | delete require.cache[require.resolve(`../commands/${file}`)];
24 | const commandData = require(`../commands/${file}`);
25 | const command = commands.find(c => c.name === file.replace(".js", ""));
26 | commandData.data.name = file.split(".")[0];
27 | if (!command) {
28 | if (process.env.DEBUG_MODE === "true") {
29 | client.application.commands.create(commandData.data).catch(console.error);
30 | } else if (!commandData.forDebug) client.application.commands.create(commandData.data).catch(console.error);
31 | } else if (command.description !== commandData.data.description || command.options !== commandData.data.options) {
32 | command.edit(commandData.data);
33 | }
34 | });
35 | return interaction.editReply("✅ | Les commandes ont bien été rechargées");
36 | }
37 | };
--------------------------------------------------------------------------------
/events/autocomplete.js:
--------------------------------------------------------------------------------
1 | const parseTime0 = (time) => {
2 | if (typeof time !== "string") {
3 | time = time.toString();
4 | }
5 | if (time.length === 1) {
6 | return "0" + time;
7 | }
8 | return time;
9 | };
10 |
11 | const weekString = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
12 |
13 | module.exports = async (client, interaction) => {
14 | if (interaction.options.data.find(o => o.focused)?.name === "date" && ["fichier"].includes(interaction.commandName)) {
15 | const subject = interaction.options.data.find(o => o.name === "matière").value;
16 | let data = client.cache.files.map(f => {
17 | return {
18 | name: f.subject,
19 | value: f.subject
20 | };
21 | });
22 | data = data.filter((v, i, a) => a.findIndex(t => (t.name === v.name)) === i);
23 | if (subject) {
24 | data = data.filter(o => o.name.toLowerCase().includes(subject.toLowerCase()));
25 | }
26 | data.splice(25);
27 | interaction.respond(data);
28 | } else if (interaction.options.data.find(o => o.focused)?.name === "matière" && !["contenu", "fichier"].includes(interaction.commandName)) {
29 | const subject = interaction.options.data.find(o => o.name === "matière").value;
30 | let data = client.cache.marks.subjects.map(s => {
31 | return {
32 | name: s.name,
33 | value: s.name
34 | };
35 | });
36 | if (subject) {
37 | data = data.filter(o => o.name.toLowerCase().includes(subject.toLowerCase()));
38 | }
39 | interaction.respond(data);
40 | } else if (interaction.options.data.find(o => o.focused)?.name === "matière") {
41 |
42 | const subject = interaction.options.data.find(o => o.name === "matière").value;
43 | let data = client.cache.contents.map(s => {
44 | return {
45 | name: s.subject,
46 | value: s.subject
47 | };
48 | });
49 | // remove duplicates
50 | data = data.filter((v, i, a) => a.findIndex(t => (t.name === v.name)) === i);
51 | if (subject) {
52 | data = data.filter(o => o.name.toLowerCase().includes(subject.toLowerCase()));
53 | }
54 | data.splice(25);
55 | interaction.respond(data);
56 | } else if (interaction.options.data.find(o => o.focused)?.name === "date" && interaction.commandName !== "contenu") {
57 | const value = interaction.options.data.find(o => o.name === "date").value;
58 |
59 | let results = [];
60 | if (value.includes("/")) {
61 | const parsed = value.split("/");
62 | const lastValue = parsed[parsed.length - 1];
63 | if (parsed.length === 3) {
64 | if (lastValue.length === 4) {
65 | const date = new Date(parsed[2], parsed[1] - 1, parsed[0]);
66 | if (date) {
67 | interaction.respond([{
68 | name: value,
69 | value: value
70 | }]);
71 | }
72 | } else {
73 | const actualYear = new Date().getFullYear();
74 | for (let i = 0; i < 12; i++) {
75 | if ((actualYear + i).toString().includes(lastValue.toLowerCase())) {
76 | results.push({
77 | name: parseTime0(parsed[0]) + "/" + parseTime0(parsed[1]) + "/" + parseTime0(actualYear + i) + " (" + weekString[new Date(actualYear + i, parsed[1] - 1, parsed[0]).getDay()] + ")",
78 | value: parseTime0(parsed[0]) + "/" + parseTime0(parsed[1]) + "/" + parseTime0(actualYear + i)
79 | });
80 | }
81 | }
82 | }
83 | } else if (parsed.length === 2) {
84 | if (lastValue.length === 2) {
85 | const actualYear = new Date().getFullYear();
86 | for (let i = 0; i < 12; i++) {
87 | results.push({
88 | name: parseTime0(parsed[0]) + "/" + parseTime0(parsed[1]) + "/" + parseTime0(actualYear + i) + " (" + weekString[new Date(actualYear + i, parsed[1] - 1, parsed[0]).getDay()] + ")",
89 | value: parseTime0(parsed[0]) + "/" + parseTime0(parsed[1]) + "/" + parseTime0(actualYear + i)
90 | });
91 | }
92 | } else {
93 | // complete for month
94 | const actualYear = new Date().getFullYear();
95 | for (let i = 0; i < 12; i++) {
96 | if ((i + 1).toString().includes(lastValue.toLowerCase())) {
97 | results.push({
98 | name: parseTime0(parsed[0]) + "/" + parseTime0(i + 1) + "/" + parseTime0(actualYear) + " (" + weekString[new Date(actualYear, i, parsed[0]).getDay()] + ")",
99 | value: parseTime0(parsed[0]) + "/" + parseTime0(i + 1) + "/" + parseTime0(actualYear)
100 | });
101 | }
102 | }
103 | }
104 | } else if (parsed.length === 1) {
105 | if (lastValue.length === 2) {
106 | const actualYear = new Date().getFullYear();
107 | for (let i = 0; i < 12; i++) {
108 | results.push({
109 | name: parseTime0(parsed[0]) + "/" + parseTime0(i + 1) + "/" + parseTime0(actualYear) + " (" + weekString[new Date(actualYear, i, parsed[0]).getDay()] + ")",
110 | value: parseTime0(parsed[0]) + "/" + parseTime0(i + 1) + "/" + parseTime0(actualYear)
111 | });
112 | }
113 | } else {
114 | const actualYear = new Date().getFullYear();
115 | for (let i = 0; i < 31; i++) {
116 | if ((i + 1).toString().includes(lastValue.toLowerCase())) {
117 | results.push({
118 | name: parseTime0(i + 1) + "/" + parseTime0(new Date().getMonth() + 1) + "/" + parseTime0(actualYear) + " (" + weekString[new Date(actualYear, new Date().getMonth(), i + 1).getDay()] + ")",
119 | value: parseTime0(i + 1) + "/" + parseTime0(new Date().getMonth() + 1) + "/" + parseTime0(actualYear)
120 | });
121 | }
122 | }
123 | }
124 | } else {
125 | const actualYear = new Date().getFullYear();
126 | for (let i = 0; i < 31; i++) {
127 | results.push({
128 | name: parseTime0(i + 1) + "/" + parseTime0(new Date().getMonth() + 1) + "/" + parseTime0(actualYear) + " (" + weekString[new Date(actualYear, new Date().getMonth(), i + 1).getDay()] + ")",
129 | value: parseTime0(i + 1) + "/" + parseTime0(new Date().getMonth() + 1) + "/" + parseTime0(actualYear)
130 | });
131 | }
132 | }
133 | } else {
134 | const actualYear = new Date().getFullYear();
135 | for (let i = 0; i < 31; i++) {
136 | results.push({
137 | name: parseTime0(i + 1) + "/" + parseTime0(new Date().getMonth() + 1) + "/" + parseTime0(actualYear) + " (" + weekString[new Date(actualYear, new Date().getMonth(), i + 1).getDay()] + ")",
138 | value: parseTime0(i + 1) + "/" + parseTime0(new Date().getMonth() + 1) + "/" + parseTime0(actualYear)
139 | });
140 | }
141 | }
142 | results.splice(25);
143 | interaction.respond(results);
144 | } else if (interaction.options.data.find(o => o.focused)?.name === "date" && interaction.commandName === "contenu") {
145 | const value = interaction.options.data.find(o => o.focused).value;
146 | const subject = interaction.options.data.find(o => o.name === "matière").value;
147 |
148 | const contents = client.cache.contents.filter(c => c.subject === subject);
149 | // const parsed = value.split(/\s/);
150 | // let parsedDate = parsed.split("/");
151 | // let parsedTime = parsed.replace("h");
152 | let results = [];
153 | for (const content of contents) {
154 | if (value) {
155 | if ((content.from.toLocaleDateString() + " " + parseTime0(content.from.getHours())).includes(value)) {
156 | results.push({
157 | name: weekString[content.from.getDay()] + " " + content.from.toLocaleDateString() + " " + parseTime0(content.from.getHours()) + "h",
158 | value: content.from.toLocaleDateString() + " " + parseTime0(content.from.getHours())
159 | });
160 | }
161 | } else {
162 | results.push({
163 | name: weekString[content.from.getDay()] + " " + content.from.toLocaleDateString() + " " + parseTime0(content.from.getHours()) + "h",
164 | value: content.from.toLocaleDateString() + " " + parseTime0(content.from.getHours())
165 | });
166 | }
167 | }
168 | results = results.reverse();
169 | results.splice(25);
170 | interaction.respond(results);
171 | } else if (interaction.options.data.find(o => o.name === "cas")?.options.find(o => o.focused)?.name === "cas" && interaction.commandName === "config") {
172 | const {casList} = require("pronote-api-maintained");
173 | const value = interaction.options.data.find(o => o.name === "cas")?.options.find(o => o.focused).value;
174 | const results = [];
175 | for (const cas of casList) {
176 | if (value && cas.toLowerCase().includes(value.toLowerCase())) {
177 | results.push({
178 | name: cas,
179 | value: cas
180 | });
181 | } else if (!value) {
182 | results.push({
183 | name: cas,
184 | value: cas
185 | });
186 | }
187 | }
188 | results.splice(25);
189 | interaction.respond(results);
190 | } else if (interaction.options.data.find(o => o.focused)?.name === "fichier" && interaction.commandName === "fichier") {
191 | const value = interaction.options.data.find(o => o.focused).value;
192 | const subject = interaction.options.data.find(o => o.name === "matière").value;
193 | const files = client.cache.files.filter(c => c.subject === subject);
194 | let results = [];
195 | for (const file of files) {
196 | if (value) {
197 | if (file.name.toLowerCase().includes(value.toLowerCase())) {
198 | results.push({
199 | name: (file.name ?? "Lien") + " (" + file.time.toLocaleDateString() + " " + parseTime0(file.time.getHours()) + "h)",
200 | value: file.id
201 | });
202 | }
203 | } else {
204 | results.push({
205 | name: (file.name ?? "Lien") + " (" + file.time.toLocaleDateString() + " " + parseTime0(file.time.getHours()) + "h)",
206 | value: file.id
207 | });
208 | }
209 | }
210 | results.splice(25);
211 | interaction.respond(results);
212 | }
213 | };
214 |
--------------------------------------------------------------------------------
/events/command.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const pronote = require("pronote-api-maintained");
3 | const {EmbedBuilder, Colors} = require("discord.js");
4 |
5 | module.exports = async (client, interaction) => {
6 | try {
7 | if (!fs.existsSync(`./commands/${interaction.commandName}.js`)) {
8 | return await interaction.reply({content: "⚠ | La commande n'a pas été trouvée", ephemeral: true});
9 | }
10 | await interaction.deferReply({
11 | fetchReply: true,
12 | ephemeral: false
13 | });
14 |
15 | if (!client.session) {
16 | const cas = (process.env.PRONOTE_CAS && process.env.PRONOTE_CAS.length > 0 ? process.env.PRONOTE_CAS : "none");
17 | client.session = await pronote.login(process.env.PRONOTE_URL, process.env.PRONOTE_USERNAME, process.env.PRONOTE_PASSWORD, cas).catch(console.error);
18 | }
19 |
20 | delete require.cache[require.resolve(`../commands/${interaction.commandName}`)];
21 | await require(`../commands/${interaction.commandName}`).execute(client, interaction);
22 | } catch (error) {
23 | console.error(error);
24 | let errorString = error;
25 | if (error.toString() === "[object Object]") {
26 | errorString = JSON.stringify(error);
27 | }
28 | if (interaction.replied) await interaction.followUp(
29 | {
30 | content: "⚠ | Il y a eu une erreur lors de l'exécution de la commande!",
31 | embeds: [new EmbedBuilder().setColor(Colors.Red).setDescription(errorString.toString())],
32 | components: [client.bugActionRow],
33 | }
34 | );
35 | else if (interaction.deferred) await interaction.editReply(
36 | {
37 | content: "⚠ | Il y a eu une erreur lors de l'exécution de la commande!",
38 | embeds: [new EmbedBuilder().setColor(Colors.Red).setDescription(errorString.toString())],
39 | components: [client.bugActionRow],
40 | }
41 | );
42 | else await interaction.reply({
43 | content: "⚠ | Il y a eu une erreur lors de l'exécution de la commande!",
44 | embeds: [new EmbedBuilder().setColor(Colors.Red).setDescription(errorString.toString())],
45 | components: [client.bugActionRow],
46 | }).catch(console.error);
47 | }
48 |
49 | setTimeout(async () => {
50 | if (client.session) {
51 | await client.session.logout();
52 | client.session = null;
53 | }
54 | }, 5 * 60 * 1000);
55 | };
--------------------------------------------------------------------------------
/events/ready.js:
--------------------------------------------------------------------------------
1 | const { ActivityType } = require("discord.js");
2 | const fs = require("fs");
3 |
4 | let timeLeft = 10;
5 |
6 | module.exports = async (client) => {
7 | client.user.setActivity("Loading", {
8 | type: ActivityType.Playing
9 | });
10 | await fs.readdirSync("./commands/").filter(file => file.endsWith(".js")).forEach(file => {
11 | const command = require(`../commands/${file}`);
12 | command.data.name = file.split(".")[0];
13 | if (process.env.DEBUG_MODE === "true") {
14 | client.application.commands.create(command.data).catch(console.error);
15 | } else if (!command.forDebug) client.application.commands.create(command.data).catch(console.error);
16 | });
17 |
18 | console.log(`Connecté comme \x1b[94m${client.user.tag}\x1b[0m! Démarré à \x1b[95m${client.functions.parseTime()}\x1b[0m. J'ai \x1b[33m${client.guilds.cache.size}\x1b[0m serveurs et \x1b[33m${client.users.cache.size}\x1b[0m utilisateurs`);
19 | await require("../utils/pronoteSynchronization")(client);
20 |
21 | setInterval(async () => {
22 | delete require.cache[require.resolve("../utils/pronoteSynchronization")];
23 | await require("../utils/pronoteSynchronization")(client).catch((e) => {
24 | if (e.message === "Session has expired due to inactivity or error") {
25 | client.session?.logout();
26 | client.session = null;
27 | }
28 | console.log(`${client.functions.parseTime()} | \x1b[31m${e.message}\x1b[0m`);
29 | });
30 | timeLeft = 10;
31 | }, 10 * 60 * 1000);
32 |
33 | setInterval(() => {
34 | timeLeft = timeLeft - 1;
35 | client.user.setActivity(`Pronote | Maj dans ${timeLeft}m`, {
36 | type: ActivityType.Watching
37 | });
38 | }, 60 * 1000);
39 | };
--------------------------------------------------------------------------------
/events/selectMenu.js:
--------------------------------------------------------------------------------
1 | const {AttachmentBuilder, EmbedBuilder, SelectMenuBuilder, ActionRowBuilder} = require("discord.js");
2 | const {ChartJSNodeCanvas} = require("chartjs-node-canvas");
3 | const width = 800;
4 | const height = 300;
5 | // White color and bold font
6 | const ticksOptions = {ticks: {font: {weight: "bold"}, color: "#fff"}};
7 | const options = {
8 | // Hide legend
9 | plugins: {
10 | legend: { /*display: false,*/ labels: {
11 | font: {weight: "bold"}, color: "#fff"
12 | }
13 | }
14 | },
15 | scales: {yAxes: ticksOptions, xAxes: ticksOptions}
16 | };
17 |
18 | const generateCanvas = async (joinedXDays, lastXDays) => {
19 | const canvasRenderService = new ChartJSNodeCanvas({width, height});
20 | const image = await canvasRenderService.renderToBuffer({
21 | type: "line",
22 | data: {
23 | labels: lastXDays,
24 | datasets: [
25 | {
26 | label: "Moyenne",
27 | data: joinedXDays,
28 | // The color of the line (the same as the fill color with full opacity)
29 | borderColor: "#70C7A4",
30 | // Fill the line with color
31 | fill: true,
32 | // Blue color and low opacity
33 | backgroundColor: "rgba(112,199,164,0.1)"
34 | }
35 | ]
36 | },
37 | options
38 | });
39 | return new AttachmentBuilder(image, {
40 | name: "graph.png",
41 | description: "Graphique de l'évolution de la moyenne"
42 | });
43 | };
44 |
45 | function isLessonInInterval(lesson, from, to) {
46 | return lesson.from >= from && lesson.from <= to;
47 | }
48 |
49 | const pronote = require("pronote-api-maintained");
50 | const {NodeHtmlMarkdown} = require("node-html-markdown");
51 | const moment = require("moment/moment");
52 | module.exports = async (client, interaction) => {
53 | if (["cours_date", "content_select", "menu_files", "select_note"].includes(interaction.customId)) {
54 | await interaction.deferUpdate();
55 |
56 | if (!client.session) {
57 | const cas = (process.env.PRONOTE_CAS && process.env.PRONOTE_CAS.length > 0 ? process.env.PRONOTE_CAS : "none");
58 | client.session = await pronote.login(process.env.PRONOTE_URL, process.env.PRONOTE_USERNAME, process.env.PRONOTE_PASSWORD, cas).catch(console.error);
59 | }
60 | }
61 |
62 | if (interaction.customId === "cours_date") {
63 | const value = interaction.values[0].split("/");
64 |
65 | const date = new Date(parseInt(value[2]), parseInt(value[1]) - 1, parseInt(value[0]));
66 |
67 | await client.session.timetable(date).then((cours) => {
68 | let totalDuration = 0;
69 |
70 | let embedCours = cours.map((cour) => {
71 | // Ne pas afficher les cours si jamais ils sont annulés et qu'ils sont remplacés par un autre cours dont les horaires sont inclus par un autre cours
72 | if (cour.isCancelled && cours.find((c) => isLessonInInterval(c, cour.from, cour.to) && !c.isCancelled)) {
73 | return;
74 | }
75 | totalDuration += cour.to.getTime() - cour.from.getTime();
76 |
77 | const subHomeworks = client.cache.homeworks.filter(h => h.subject === cour.subject && cour.from.getDate()+"/"+cour.from.getMonth() === h.for.getDate()+"/"+h.for.getMonth());
78 | const coursIsAway = cour.isAway || cour.isCancelled || cour.status?.match(/(.+)?prof(.+)?absent(.+)?/giu) || cour.status == "Cours annulé";
79 | const embed = new EmbedBuilder()
80 | .setColor(cour.color ?? "#70C7A4")
81 | .setAuthor({
82 | name: cour.subject ?? (cour.status ?? "Non défini"),
83 | })
84 | .setDescription("Professeur: **" + (cour.teacher ?? "*Non précisé*") + "**" +
85 | "\nSalle: `" + (cour.room ?? " ? ") + "`" +
86 | "\nDe **" + cour.from.toLocaleTimeString().split(":")[0] +
87 | "h" + cour.from.toLocaleTimeString().split(":")[1] + "**" +
88 | " à **" + cour.to.toLocaleTimeString().split(":")[0] +
89 | "h" + cour.to.toLocaleTimeString().split(":")[1] + "**" +
90 | " *(" + (cour.to.getTime() - cour.from.getTime()) / 1000 / 60 / 60 + "h)*" +
91 | (subHomeworks.length && !coursIsAway ? `\n⚠**__\`${subHomeworks.length}\` Devoirs__**` : "") +
92 | (coursIsAway ? "\n🚫__**Cour annulé**__" : ""));
93 |
94 | if (cour.status && (!coursIsAway || cour.statut !== "Cours annulé")) {
95 | embed.addFields([
96 | {
97 | name: "Status",
98 | value: "__**" + cour.status + "**__"
99 | }
100 | ]);
101 | }
102 | return embed;
103 | }).filter(emb => !!emb);
104 |
105 |
106 | if (embedCours.length >= 9) {
107 | const embed = new EmbedBuilder()
108 | .setColor("#70C7A4")
109 | .addFields(
110 | embedCours.map((emb) => {
111 | return {
112 | name: emb.author.name,
113 | value: emb.description,
114 | inline: false
115 | };
116 | })
117 | );
118 | embedCours = [embed];
119 | }
120 |
121 | totalDuration = Math.abs(totalDuration / 1000 / 60 / 60);
122 | const embed = new EmbedBuilder()
123 | .setColor("#70C7A4")
124 | .setTitle("Vous avez " + embedCours.length + " cours " + "le `"+ date.toLocaleDateString() +"` :")
125 | .setDescription("Durée totale : **" + totalDuration + "h**");
126 |
127 |
128 | const current = new Date(date.getTime());
129 | const week = [];
130 | for (let i = 1; i <= 7; i++) {
131 | let first = current.getDate() - current.getDay() + i;
132 | let day = new Date(current.setDate(first));
133 | if (day.getDay() !== 0) week.push(day);
134 | }
135 | let weekString = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
136 |
137 | const selectMenu = new SelectMenuBuilder()
138 | .setCustomId("cours_date")
139 | .setPlaceholder("Sélectionnez une date pour voir les cours")
140 | .addOptions(week.map((day) => {
141 | return {
142 | label: day.toLocaleDateString(),
143 | value: day.toLocaleDateString(),
144 | description: weekString[day.getDay()] + " " + day.toLocaleDateString().split("/")[0],
145 | default: day.toLocaleDateString() === date.toLocaleDateString()
146 | };
147 | }))
148 | .setMaxValues(1)
149 | .setMinValues(1);
150 |
151 | interaction.message.edit({
152 | embeds: [embed].concat(embedCours),
153 | components: [new ActionRowBuilder().addComponents(selectMenu)]
154 | });
155 | });
156 | } else if (interaction.customId === "menus_date") {
157 | const value = interaction.values[0].split("/");
158 |
159 | const date = new Date(parseInt(value[2]), parseInt(value[1]) - 1, parseInt(value[0]));
160 |
161 | await client.session.menu(date).then(async (menus) => {
162 | const menu = menus[0];
163 |
164 | const embed = new EmbedBuilder()
165 | .setTitle("Menu du jour")
166 | .setColor("#70C7A4");
167 | if (menu) embed
168 | .setDescription(`Menu du ${menu.date}`)
169 | .setTimestamp(new Date(menu.date))
170 | .addFields(menu.meals[0].map((meal) => {
171 | meal = meal[0];
172 | return {
173 | name: meal.name,
174 | value: meal.labels.map((label) => {
175 | return `• ${label}`;
176 | }).join("\n") || "\u200b",
177 | inline: false
178 | };
179 | }));
180 | else embed.setDescription("Aucun menu n'a été trouvé pour aujourd'hui");
181 |
182 | const warnEmbed = new EmbedBuilder()
183 | .setTitle("Attention")
184 | .setDescription("Cette commande est en cours de développement. Comme le développeur ne possède pas les menus sur son pronote, il ne peut pas tester correctement cette commande. Si vous rencontrez des problèmes ou que vous voulez aider, merci de contacter le développeur sur github.")
185 | .setColor("#FFA500");
186 |
187 | const current = new Date(date.getTime());
188 | const week = [];
189 | for (let i = 1; i <= 7; i++) {
190 | let first = current.getDate() - current.getDay() + i;
191 | let day = new Date(current.setDate(first));
192 | if (day.getDay() !== 0) week.push(day);
193 | }
194 | let weekString = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
195 |
196 | const selectMenu = new SelectMenuBuilder()
197 | .setCustomId("menus_date")
198 | .setPlaceholder("Sélectionnez une date pour voir les cours")
199 | .addOptions(week.map((day) => {
200 | return {
201 | label: day.toLocaleDateString(),
202 | value: day.toLocaleDateString(),
203 | description: weekString[day.getDay()] + " " + day.toLocaleDateString().split("/")[0],
204 | default: day.toLocaleDateString() === date.toLocaleDateString()
205 | };
206 | }))
207 | .setMaxValues(1)
208 | .setMinValues(1);
209 |
210 |
211 | interaction.message.edit({
212 | embeds: [embed, warnEmbed],
213 | components: [new ActionRowBuilder().addComponents(selectMenu), client.bugActionRow]
214 | });
215 | });
216 | } else if (interaction.customId === "content_select") {
217 | const value = interaction.values[0].split(/-/);
218 | const subject = value[0];
219 | const dateValue = value[1].split("/");
220 | const timeValue = value[2].replace("h", "");
221 |
222 | const date = new Date(parseInt(dateValue[2]), parseInt(dateValue[1]) - 1, parseInt(dateValue[0]), parseInt(timeValue));
223 | await client.session.contents(new Date(new Date().getFullYear(), 8, 1), new Date()).then(async (data) => {
224 | data = data.filter((content) => content.subject === subject);
225 | const components = [];
226 | if (data.length > 1) {
227 | const menu = new SelectMenuBuilder()
228 | .setCustomId("content_select")
229 | .setPlaceholder("Sélectionnez une date")
230 | .addOptions(interaction.message.components[0].components[0].options.map((option) => {
231 | return {
232 | label: option.label,
233 | value: option.value,
234 | description: option.description,
235 | default: option.value === interaction.values[0]
236 | };
237 | }))
238 | .setMaxValues(1)
239 | .setMinValues(1);
240 | components.push(new ActionRowBuilder().addComponents(menu));
241 | }
242 | data = data.filter(o => o.from.getTime() === date.getTime());
243 | const content = data[0];
244 | const embed = new EmbedBuilder()
245 | .setAuthor({
246 | name: "Contenu du cours",
247 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
248 | url: process.env.PRONOTE_URL,
249 | })
250 | .setColor(content.color)
251 | .addFields([
252 | {
253 | name: content.subject,
254 | value: content.teachers.join(", ") +
255 | "\n le **" + content.from.toLocaleDateString() + "**" +
256 | " de **" + content.from.toLocaleTimeString().split(":")[0] +
257 | "h" + content.from.toLocaleTimeString().split(":")[1] + "**" +
258 | " à **" + content.to.toLocaleTimeString().split(":")[0] +
259 | "h" + content.to.toLocaleTimeString().split(":")[1] + "**",
260 | }
261 | ])
262 | .setFooter({text: "Bot par Merlode#8128"});
263 |
264 |
265 | if (content.title) {
266 | embed.setTitle(content.title);
267 | }
268 |
269 | if (content.htmlDescription) {
270 | embed.setDescription(NodeHtmlMarkdown.translate(content.htmlDescription));
271 | } else if (content.description) {
272 | embed.setDescription(content.description);
273 | }
274 |
275 | let attachments = [];
276 | let files = [];
277 | if (content.files.length > 0) {
278 | await client.functions.asyncForEach(content.files, async (file) => {
279 | await client.functions.getFileProperties(file).then(async (properties) => {
280 | if (properties.type === "file") {
281 | attachments.push(properties.attachment);
282 | }
283 | files.push(properties);
284 | });
285 | });
286 | }
287 |
288 | await interaction.message.edit({
289 | embeds: [embed],
290 | components: components,
291 | files: attachments,
292 | fetchReply: true
293 | });
294 |
295 | if (files.length > 0) {
296 | const e = await interaction.fetchReply();
297 | let string = "";
298 | if (files.length > 0) {
299 | await client.functions.asyncForEach(files, async (file) => {
300 | if (file.type === "file") {
301 | const name = client.functions.setFileName(file.name);
302 | const attachment = e.attachments.find(a => a.name === name);
303 | if (attachment) {
304 | string += `[${file.name}](${attachment.url})\n`;
305 | }
306 | } else {
307 | string += `[${file.name ?? file.url}](${file.url} "${file.url}")\n`;
308 | }
309 | });
310 | }
311 | const strings = client.functions.splitMessage(string, {
312 | maxLength: 1024,
313 | });
314 |
315 | if (string.length > 0) {
316 | const lastString = strings.pop();
317 | await client.functions.asyncForEach(strings, async (string) => {
318 | embed.data.fields.unshift({
319 | name: "",
320 | value: string,
321 | inline: false
322 | });
323 | });
324 | embed.data.fields.unshift({
325 | name: "Fichiers joints",
326 | value: lastString,
327 | inline: false
328 | });
329 | await interaction.message.edit({embeds: [embed]});
330 | }
331 | }
332 | });
333 | } else if (interaction.customId === "menu_files") {
334 | const value = interaction.values[0].split(/\|/);
335 | client.session.files().then(async (files) => {
336 | const data = files.filter((file) => file.subject === value[1]);
337 | const components = [];
338 | if (files.length > 1) {
339 | const selectMenu = new SelectMenuBuilder()
340 | .setCustomId("menu_files")
341 | .setPlaceholder("Sélectionnez un fichier")
342 | .setMinValues(1)
343 | .setMaxValues(1)
344 | .addOptions(data.map(f => {
345 | return ({
346 | label: f.name ?? "Lien",
347 | value: f.id + "|" + f.subject,
348 | description: f.time.toLocaleString(),
349 | default: f.id === value[0]
350 | });
351 | }));
352 | components.push(new ActionRowBuilder().addComponents(selectMenu));
353 | }
354 |
355 | let dataToSend = files.find(f => f.id === value[0]);
356 | if (!dataToSend) dataToSend = files[0];
357 |
358 | const properties = await client.functions.getFileProperties(dataToSend);
359 | if (properties.type === "link") {
360 | interaction.message.edit({
361 | content: `🔗 | [${properties.name ?? properties.url}](${properties.url} "${properties.url}")`,
362 | components,
363 | embeds: [],
364 | files: []
365 | });
366 | } else {
367 | const embed = new EmbedBuilder()
368 | .setColor("#70C7A4")
369 | .setAuthor({
370 | name: properties.subject,
371 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
372 | url: process.env.PRONOTE_URL,
373 | })
374 | .setTitle(properties.name)
375 | .setDescription(`📅 | **${properties.time.toLocaleString()}**`)
376 | .setFooter({text: "Bot par Merlode#8128"});
377 | // detect if file is an image
378 | if (properties.name.match(/\.(jpeg|jpg|gif|png)$/) != null) {
379 | embed.setImage("attachment://" + properties.name);
380 | }
381 | interaction.editReply({
382 | content: null,
383 | embeds: [embed],
384 | files: [properties.attachment],
385 | components,
386 | fetchReply: true
387 | }).then(async e => {
388 | if (!e) e = await interaction.fetchReply();
389 | embed.setURL(e.attachments.first().url);
390 | interaction.message.edit({embeds: [embed]});
391 | });
392 | }
393 | });
394 | } else if (interaction.customId.startsWith("select_note")) {
395 | const type = interaction.customId.split("-")[1];
396 | let data = [];
397 | if (type === "recent") {
398 | client.cache.marks.subjects.forEach(s => {
399 | if (s.marks.length) data = data.concat(s.marks.map(mark => {
400 | mark.subject = s.name;
401 | return mark;
402 | }));
403 | });
404 | data = data.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 25);
405 | data = data.filter((v, i, a) => a.findIndex(t => t.id === v.id) === i);
406 | } else {
407 | data = client.cache.marks.subjects.find(s => s.name === type);
408 | }
409 |
410 | const embed = new EmbedBuilder();
411 | const components = [];
412 | const selectNote = new SelectMenuBuilder()
413 | .setCustomId(interaction.customId)
414 | .setPlaceholder("Voir plus de précisions sur une note")
415 | .setMinValues(0)
416 | .setMaxValues(1);
417 | components.push(new ActionRowBuilder().addComponents(selectNote));
418 | const attachments = [];
419 |
420 | if (interaction.values.length > 0) {
421 | const value = interaction.values[0];
422 | let mark = data?.marks?.find(m => m.id === value);
423 | if (!mark) mark = data.find(m => m.id === value);
424 | if (mark) {
425 | let subject = data;
426 | if (type === "recent") {
427 | subject = client.cache.marks.subjects.find(s => s.name === mark.subject);
428 | }
429 |
430 | embed.setAuthor({
431 | name: "Pronote",
432 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
433 | url: process.env.PRONOTE_URL
434 | });
435 | let better = "";
436 | if (mark.value === mark.max) {
437 | better = "**__Tu as la meilleure note de la classe !__**\n";
438 | embed.setThumbnail("https://i.imgur.com/RGs62tl.gif");
439 | } else if (mark.value >= mark.average) {
440 | better = "**__Tu as une note au dessus de la moyenne de la classe !__**\n";
441 | embed.setThumbnail("https://i.imgur.com/3P5DfAZ.gif");
442 | } else if (mark.value === mark.min) {
443 | better = "**__Tu est le premier des derniers !__**\n";
444 | embed.setThumbnail("https://i.imgur.com/5H5ZASz.gif");
445 | embed.author.url = "https://youtu.be/dQw4w9WgXcQ";
446 | }
447 | let studentNote = `**Note de l'élève :** ${mark.value}/${mark.scale}`;
448 | if (mark.scale !== 20) studentNote += ` *(${+(mark.value / mark.scale * 20).toFixed(2)}/20)*`;
449 | const infos = better + studentNote + `\n**Moyenne de la classe :** ${mark.average}/${mark.scale}\n**Coefficient**: ${mark.coefficient}\n\n**Note la plus basse :** ${mark.min}/${mark.scale}\n**Note la plus haute :** ${mark.max}/${mark.scale}`;
450 | const description = mark.title ? `${mark.title}\n\n${infos}` : infos;
451 | embed.setTitle(subject.name.toUpperCase())
452 | .setDescription(description)
453 | .addFields([{
454 | name: "__Matière__",
455 | value: `**Moyenne de l'élève :** ${subject.averages.student}/20\n**Moyenne de la classe :** ${subject.averages.studentClass}/20`
456 | }
457 | ])
458 | .setFooter({text: `Date de l'évaluation : ${moment(mark.date).format("dddd Do MMMM")}`})
459 | .setURL(process.env.PRONOTE_URL)
460 | .setColor(subject.color ?? "#70C7A4");
461 | selectNote.addOptions(data.marks ?
462 | data.marks.map(m => {
463 | return {
464 | label: (m.value + "/" + m.scale) + (m.title ? (" - " + m.title) : ""),
465 | value: m.id,
466 | description: `${m.subject} - ${moment(m.date).format("dddd Do MMMM")}`,
467 | emoji: "📝",
468 | default: m.id === value
469 | };
470 | })
471 | :
472 | data.map(m => {
473 | return {
474 | label: m.subject + " - " + (m.value + "/" + m.scale),
475 | value: m.id,
476 | description: `${m.subject} - ${moment(m.date).format("dddd Do MMMM")}`,
477 | emoji: "📝",
478 | default: m.id === value
479 | };
480 | }));
481 | } else {
482 | interaction.message.edit({content: "Une erreur est survenue, veuillez réessayer.", embeds: []});
483 | }
484 | } else {
485 | embed.setColor("#70C7A4");
486 |
487 | if (type === "recent") {
488 | embed.setAuthor({
489 | name: "Notes générales",
490 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
491 | url: process.env.PRONOTE_URL,
492 | })
493 | .setTitle("Moyenne générale: " + client.cache.marks.averages.student)
494 | .addFields(data.map(mark => {
495 | mark.date = new Date(mark.date);
496 | return {
497 | name: mark.subject,
498 | value: "Note: **" + mark.value + "/" + mark.scale +
499 | "**\nLe: " + mark.date.toLocaleDateString() +
500 | "\nMoyenne du groupe: " + mark.average + "/" + mark.scale,
501 | inline: false
502 | };
503 | }))
504 | .setImage("attachment://graph.png")
505 | .setFooter({text: "Bot par Merlode#8128"});
506 | const graph = await generateCanvas(client.cache.marks.averages.history.map(o => o.student), client.cache.marks.averages.history.map(o => {
507 | o.date = new Date(o.date);
508 | return o.date.toLocaleDateString();
509 | }));
510 | attachments.push(graph);
511 | selectNote.addOptions(data.map(mark => {
512 | mark.date = new Date(mark.date);
513 | return {
514 | label: mark.subject + " - " + (mark.value + "/" + mark.scale),
515 | value: mark.id,
516 | description: "Le " + mark.date.toLocaleDateString(),
517 | emoji: "📝",
518 | };
519 | }))
520 | .setCustomId("select_note-recent");
521 | } else {
522 | embed.setAuthor({
523 | name: data.name,
524 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
525 | url: process.env.PRONOTE_URL,
526 | })
527 | .setTitle("Moyenne: " + data.averages.student)
528 | .setColor(data.color)
529 | .addFields(data.marks.reverse().map(mark => {
530 | mark.date = new Date(mark.date);
531 | return {
532 | name: "Le " + mark.date.toLocaleDateString(),
533 | value: "Note: **" + mark.value + "/" + mark.scale + "**\nMoyenne du groupe: " + mark.average + "/" + mark.scale,
534 | inline: false
535 | };
536 | }))
537 | .setImage("attachment://graph.png")
538 | .setFooter({text: "Bot par Merlode#8128"});
539 |
540 | const graph = await generateCanvas(data.averagesHistory.map(o => o.student), data.averagesHistory.map(o => {
541 | o.date = new Date(o.date);
542 | return o.date.toLocaleDateString();
543 | }));
544 | attachments.push(graph);
545 | selectNote.addOptions(data.marks.map(mark => {
546 | return {
547 | label: (mark.value + "/" + mark.scale) + (mark.title ? (" - " + mark.title) : ""),
548 | value: mark.id,
549 | description: "Le " + mark.date.toLocaleDateString(),
550 | emoji: "📝"
551 | };
552 | }))
553 | .setCustomId("select_note-" + data.name);
554 | }
555 | }
556 | interaction.message.edit({embeds: [embed], components, files: attachments});
557 | }
558 | };
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require("./utils/verif-env")();
2 |
3 | require("dotenv").config();
4 | const fs = require("fs");
5 | const pronote = require("pronote-api-maintained");
6 | const { Client, ButtonBuilder, ButtonStyle, ActionRowBuilder, GatewayIntentBits } = require("discord.js");
7 | const msgbox = require("native-msg-box");
8 | const { checkUpdate, updateFiles } = require("./utils/update");
9 |
10 | if (process.env.AUTO_UPDATE === "true") {
11 | checkUpdate().then(result => {
12 | console.log(result);
13 | if (result) {
14 | msgbox.prompt({
15 | icon: msgbox.Icon.STOP,
16 | msg: "Une nouvelle version est disponible, voulez-vous la télécharger ?",
17 | title: "Mise à jour",
18 | type: 4
19 | }, async (err, result) => {
20 | if (err) {return console.error(err);}
21 | if (result === msgbox.Result.YES) {
22 | updateFiles().then(async () => {
23 | // restart the app
24 | await require("child_process").execSync("npm install");
25 | require("child_process").execSync("node index.js");
26 | process.exit(0);
27 | }).catch(err => {
28 | console.error(err);
29 | });
30 | }
31 | });
32 | }
33 | });
34 | }
35 |
36 |
37 | const client = new Client({ intents: GatewayIntentBits.Guilds });
38 |
39 | require("./utils/db")(client);
40 | require("./utils/notifications")(client);
41 | require("./utils/functions")(client);
42 |
43 | const bugButton = new ButtonBuilder()
44 | .setStyle(ButtonStyle.Link)
45 | .setLabel("Signaler un bug")
46 | .setURL("https://github.com/Merlode11/pronote-bot-discord/issues/new?assignees=Merlode11&labels=bug%2C+help+wanted&template=signaler-un-bug.md&title=%5BBUG%5D")
47 | .setEmoji("🐛");
48 |
49 | client.bugActionRow = new ActionRowBuilder().addComponents(bugButton);
50 |
51 | client.session = null;
52 |
53 | const cas = (process.env.PRONOTE_CAS && process.env.PRONOTE_CAS.length > 0 ? process.env.PRONOTE_CAS : "none");
54 | pronote.login(process.env.PRONOTE_URL, process.env.PRONOTE_USERNAME, process.env.PRONOTE_PASSWORD, cas).then(session => {
55 | client.session = session;
56 | client.cache = {};
57 | // Si le fichier cache n'existe pas, on le crée
58 | if (!fs.existsSync("cache_" + client.session.user.studentClass.name + ".json")) {
59 | client.db.resetCache(client.session.user.studentClass.name);
60 | } else {
61 | // S'il existe, on essaie de le parser et si ça échoue, on le reset pour éviter les erreurs
62 | try {
63 | client.cache = JSON.parse(fs.readFileSync("cache_" + client.session.user.studentClass.name + ".json", "utf-8"));
64 | } catch {
65 | client.db.resetCache(client.session.user.studentClass.name);
66 | }
67 | }
68 |
69 |
70 | client.on("ready", require("./events/ready"));
71 |
72 | client.on("interactionCreate", async interaction => {
73 | if (interaction.isAutocomplete()) {
74 | delete require.cache[require.resolve("./events/autocomplete")];
75 | await require("./events/autocomplete")(client, interaction);
76 | } else if (interaction.isCommand()) {
77 | delete require.cache[require.resolve("./events/command")];
78 | await require("./events/command")(client, interaction);
79 | } else if (interaction.isSelectMenu()) {
80 | delete require.cache[require.resolve("./events/selectMenu")];
81 | await require("./events/selectMenu")(client, interaction);
82 | }
83 | });
84 |
85 | // Connexion à Discord
86 | client.login(process.env.TOKEN).then(() => {}).catch(console.error);
87 | }).catch(console.error);
88 |
89 | process.on("unhandledRejection", error => {
90 | console.error(error);
91 | });
92 | process.on("uncaughtException", error => {
93 | console.error(error);
94 | });
95 | process.on("uncaughtExceptionMonitor", error => {
96 | console.error(error);
97 | });
98 | process.on("warning", error => {
99 | console.error(error);
100 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pronote-bot-discord",
3 | "version": "5.1.1",
4 | "description": "Un bot Discord pour envoyer des notifications Pronote dans un serveur Discord 📚",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/Merlode11/pronote-bot-discord.git"
9 | },
10 | "author": "Androz2091 ",
11 | "license": "MIT",
12 | "dependencies": {
13 | "axios": "^0.21.4",
14 | "chart.js": "^3.6.0",
15 | "chartjs-node-canvas": "^4.1.6",
16 | "discord.js": "^14.7.1",
17 | "dotenv": "^8.2.0",
18 | "jszip": "^3.10.1",
19 | "moment": "^2.29.1",
20 | "native-msg-box": "^0.1.6",
21 | "node-fetch": "^2.6.1",
22 | "node-html-markdown": "^1.2.0",
23 | "pronote-api-maintained": "^2.4.1"
24 | },
25 | "devDependencies": {
26 | "eslint": "^7.10.0"
27 | },
28 | "bugs": {
29 | "url": "https://github.com/Merlode11/pronote-bot-discord/issues"
30 | },
31 | "homepage": "https://github.com/Merlode11/pronote-bot-discord#readme",
32 | "scripts": {
33 | "run": "node index.js"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/qodana.yaml:
--------------------------------------------------------------------------------
1 | version: "1.0"
2 | linter: jetbrains/qodana-js:2024.3
3 | profile:
4 | name: qodana.recommended
5 | include:
6 | - name: CheckDependencyLicenses
--------------------------------------------------------------------------------
/screen-exemple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Merlode11/pronote-bot-discord/2eb00b01f54fedd0c50512b394c907168343ed24/screen-exemple.png
--------------------------------------------------------------------------------
/start-pronote-bot.bat:
--------------------------------------------------------------------------------
1 | if not exist node_modules\ npm i
2 | node index.js
3 | cmd.exe
--------------------------------------------------------------------------------
/utils/db.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | module.exports = client => {
4 | client.db = {};
5 |
6 | /**
7 | * Écrit l'objet dans le cache et met à jour la variable
8 | * @param {object} newCache Le nouvel objet
9 | */
10 | client.db.writeCache = (newCache) => {
11 | client.cache = newCache;
12 | fs.writeFileSync(newCache.classe ? "cache_"+newCache.classe.toUpperCase()+".json" : "cache.json", JSON.stringify(newCache, null, 4), "utf-8");
13 | };
14 |
15 | /**
16 | * Réinitialise le cache
17 | * @param {string|null} classe La classe du cache à réinitialiser
18 | */
19 | client.db.resetCache = (classe = null) => client.db.writeCache({
20 | classe: classe,
21 | homeworks: [],
22 | marks: {
23 | subjects: []
24 | },
25 | lessonsAway: [],
26 | infos: [],
27 | contents: [],
28 | files: [],
29 | });
30 | };
--------------------------------------------------------------------------------
/utils/functions.js:
--------------------------------------------------------------------------------
1 | // noinspection NonAsciiCharacters
2 |
3 | // eslint-disable-next-line no-unused-vars
4 | const { File } = require("pronote-api-maintained");
5 | const fetch = require("node-fetch");
6 | const { AttachmentBuilder } = require("discord.js");
7 |
8 |
9 | module.exports = (client) => {
10 | client.functions = {
11 | /**
12 | * Like Array.prototype.forEach but asynchronous
13 | * @param array {Array} The array to loop through
14 | * @param callback {Function} The function to execute for each element
15 | * @return {Promise}
16 | */
17 | asyncForEach: async (array, callback) => {
18 | for (let index = 0; index < array.length; index++) {
19 | await callback(array[index], index, array);
20 | }
21 | },
22 | /**
23 | * Remove all accents from a string
24 | * @param str {String} The string to remove accents from
25 | * @return {String}
26 | */
27 | transliterate: (str) => {
28 | const ru = {
29 | é: "e",
30 | è: "e",
31 | ê: "e",
32 | ë: "e",
33 | É: "E",
34 | È: "E",
35 | Ê: "E",
36 | Ë: "E",
37 | à: "a",
38 | â: "a",
39 | ä: "a",
40 | À: "A",
41 | Â: "A",
42 | Ä: "A",
43 | ô: "o",
44 | ö: "o",
45 | Ö: "O",
46 | Ô: "O",
47 | û: "u",
48 | ù: "u",
49 | Ù: "U",
50 | Û: "U",
51 | ï: "i",
52 | Ï: "I",
53 | ÿ: "y",
54 | Ÿ: "Y",
55 | };
56 | return str
57 | .split("")
58 | .map((char) => ru[char] || char)
59 | .join("");
60 | },
61 | /**
62 | * Verify if a string is valid
63 | * @param data {String} The string to verify
64 | * @param error {ErrorConstructor} The error message to send if the string is invalid
65 | * @param errorMessage {String} The error message to send if the string is invalid
66 | * @param allowEmpty {Boolean} If the string can be empty
67 | * @return {string} The string
68 | */
69 | verifyString: (
70 | data,
71 | error = Error,
72 | errorMessage = `Expected a string, got ${data} instead.`,
73 | allowEmpty = true,
74 | ) => {
75 | if (typeof data !== "string") throw new error(errorMessage);
76 | if (!allowEmpty && data.length === 0) throw new error(errorMessage);
77 | return data;
78 | },
79 | /**
80 | * Split a string into chunks if it's too long
81 | * @param text {String} The string to split
82 | * @param maxLength {Number} The maximum length of each chunk
83 | * @param char {String} The character to split the string with
84 | * @param prepend {String} The string to prepend to each chunk
85 | * @param append {String} The string to append to each chunk
86 | * @return {string[]|*[]} The chunks
87 | */
88 | splitMessage: (text, { maxLength = 2000, char = "\n", prepend = "", append = "" } = {}) => {
89 | text = client.functions.verifyString(text);
90 | if (text.length <= maxLength) return [text];
91 | let splitText = [text];
92 | if (Array.isArray(char)) {
93 | while (char.length > 0 && splitText.some(elem => elem.length > maxLength)) {
94 | const currentChar = char.shift();
95 | if (currentChar instanceof RegExp) {
96 | splitText = splitText.flatMap(chunk => chunk.match(currentChar));
97 | } else {
98 | splitText = splitText.flatMap(chunk => chunk.split(currentChar));
99 | }
100 | }
101 | } else {
102 | splitText = text.split(char);
103 | }
104 | if (splitText.some(elem => elem.length > maxLength)) throw new RangeError("SPLIT_MAX_LEN");
105 | const messages = [];
106 | let msg = "";
107 | for (const chunk of splitText) {
108 | if (msg && (msg + char + chunk + append).length > maxLength) {
109 | messages.push(msg + append);
110 | msg = prepend;
111 | }
112 | msg += (msg && msg !== prepend ? char : "") + chunk;
113 | }
114 | return messages.concat(msg).filter(m => m);
115 | },
116 | /**
117 | * Set the time in a string
118 | * @param time {Date} The time to set
119 | * @return {String} the time in a string
120 | */
121 | parseTime: (time= new Date) => {
122 | const hour = `${time.getHours()}`.length === 1 ? `0${time.getHours()}` : time.getHours();
123 | const min = `${time.getMinutes()}`.length === 1 ? `0${time.getMinutes()}` : time.getMinutes();
124 | const sec = `${time.getSeconds()}`.length === 1 ? `0${time.getSeconds()}` : time.getSeconds();
125 | const hours = hour + ":" + min + ":" + sec;
126 |
127 | const day = `${time.getDate()}`.length === 1 ? `0${time.getDate()}` : time.getDate();
128 | const month = `${time.getMonth() + 1}`.length === 1 ? `0${time.getMonth() + 1}` : (time.getMonth() + 1);
129 | const date = day + "/" + month + "/" + time.getFullYear();
130 |
131 | return `${date} ${hours}`;
132 | },
133 |
134 | /**
135 | * Get a pronote file properties
136 | * @param file {File} The pronote file
137 | * @return {Object} The file properties
138 | * @property {String} name The file name
139 | * @property {String} url The file url
140 | * @property {String} type The file type
141 | * @property {String} date The file date
142 | * @property {ArrayBuffer} buffer The file buffer
143 | * @property {AttachmentBuilder} attachment The file attachment
144 | */
145 | getFileProperties: async (file) => {
146 | const newFile = {
147 | name: file.name,
148 | url: file.url,
149 | subject: file.subject,
150 | time: file.time,
151 | };
152 |
153 | if (file.type === 1) {
154 | await new Promise((resolve, reject) => {
155 | fetch(file.url, {
156 | method: "GET"
157 | }).then(response => {
158 | return response.buffer();
159 | }).then(async buffer => {
160 | newFile.buffer = buffer;
161 | newFile.type = "file";
162 | newFile.attachment = new AttachmentBuilder(buffer, {
163 | name: file.name,
164 | });
165 | resolve();
166 | }).catch(reject);
167 | }).catch(console.error);
168 | } else {
169 | await new Promise((resolve, reject) => {
170 | fetch(file.url, {
171 | method: "GET",
172 | followRedirects: true
173 | }).then(response => {
174 | newFile.url = response.url;
175 | newFile.type = "link";
176 | resolve();
177 | }).catch(reject);
178 | }).catch(console.error);
179 | }
180 | return newFile;
181 | },
182 | /**
183 | * Set a file name as a Discord compatible name
184 | * @param name {String} The file name
185 | * @return {String} The Discord compatible name
186 | */
187 | setFileName: (name) => {
188 | return client.functions.transliterate(name).replace(/[^a-zA-Z0-9.\s\-_]/g, "").replace(/\s/g, "_");
189 | }
190 | };
191 | };
--------------------------------------------------------------------------------
/utils/notifications.js:
--------------------------------------------------------------------------------
1 | const { EmbedBuilder } = require("discord.js");
2 | const { NodeHtmlMarkdown } = require("node-html-markdown");
3 |
4 | const moment = require("moment");
5 | moment.locale("fr");
6 |
7 | let averageMsg = null;
8 |
9 | module.exports = client => {
10 | client.notif = {};
11 |
12 | /**
13 | * Envoi une notification de note sur Discord
14 | * @param {Array} marksNotifications La matière de la note
15 | * @param {Array} cachedMarks La note à envoyer
16 | */
17 | client.notif.mark = async (marksNotifications, cachedMarks) => {
18 | const channel = client.channels.cache.get(process.env.MARKS_CHANNEL_ID);
19 | if (!channel) return new ReferenceError("MARKS_CHANNEL_ID is not defined");
20 | if (!averageMsg) {
21 | averageMsg = channel.lastMessage;
22 | if (!averageMsg) {
23 | averageMsg = await channel.messages.fetch({limit: 1});
24 | averageMsg = averageMsg.first();
25 | }
26 | }
27 |
28 | await client.functions.asyncForEach(marksNotifications, async markObj => {
29 | const mark = markObj.mark;
30 | const subject = markObj.subject;
31 |
32 | const embed = new EmbedBuilder()
33 | .setAuthor({
34 | name: "Pronote",
35 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
36 | url: process.env.PRONOTE_URL
37 | });
38 | let better = "";
39 | if (mark.value === mark.max) {
40 | better = "**__Tu as la meilleure note de la classe !__**\n";
41 | embed.setThumbnail("https://i.imgur.com/RGs62tl.gif");
42 | } else if (mark.value >= mark.average) {
43 | better = "**__Tu as une note au dessus de la moyenne de la classe !__**\n";
44 | embed.setThumbnail("https://i.imgur.com/3P5DfAZ.gif");
45 | } else if (mark.value === mark.min) {
46 | better = "**__Tu est le premier des derniers !__**\n";
47 | embed.setThumbnail("https://i.imgur.com/5H5ZASz.gif");
48 | embed.data.author.url = "https://youtu.be/dQw4w9WgXcQ";
49 | }
50 | let studentNote = `**Note de l'élève :** ${mark.value}/${mark.scale}`;
51 | if (mark.scale !== 20) studentNote += ` *(${+(mark.value / mark.scale * 20).toFixed(2)}/20)*`;
52 | const infos = better + studentNote + `\n**Moyenne de la classe :** ${mark.average}/${mark.scale}\n**Coefficient**: ${mark.coefficient}\n\n**Note la plus basse :** ${mark.min}/${mark.scale}\n**Note la plus haute :** ${mark.max}/${mark.scale}`;
53 | const description = mark.title ? `${mark.title}\n\n${infos}` : infos;
54 | embed.setTitle(subject.name.toUpperCase())
55 | .setDescription(description)
56 | .addFields([{
57 | name: "__Matière__",
58 | value: `**Moyenne de l'élève :** ${subject.averages.student}/20\n**Moyenne de la classe :** ${subject.averages.studentClass}/20`
59 | }
60 | ])
61 | .setFooter({text: `Date de l'évaluation : ${moment(mark.date).format("dddd Do MMMM")}`})
62 | .setURL(process.env.PRONOTE_URL)
63 | .setColor(subject.color ?? "#70C7A4");
64 |
65 | await channel.send({embeds: [embed]}).catch(console.error);
66 | });
67 | if (
68 | averageMsg &&
69 | averageMsg.author.id === client.user.id &&
70 | averageMsg.embeds[0].title.toUpperCase() === "moyenne générale".toUpperCase()
71 | ) await averageMsg.delete();
72 |
73 | const studentEdit = Math.round(((client.cache.marks?.averages?.student ?? 0) - (cachedMarks?.averages?.student ?? 0)) * 100) / 100;
74 | const classEdit = Math.round(((client.cache.marks?.averages?.studentClass ?? 0) - (cachedMarks?.averages?.studentClass ?? 0)) * 100) / 100;
75 |
76 | const generalEmbed = new EmbedBuilder()
77 | .setTitle("moyenne générale".toUpperCase())
78 | .setDescription(`**Moyenne générale de l'élève :** ${client.cache.marks?.averages?.student ?? 0}/20\n**Moyenne générale de la classe :** ${client.cache.marks?.averages?.studentClass ?? 0}/20`)
79 | .addFields([{
80 | name: "__Moyennes précédentes__",
81 | value: `**Élève :** ${cachedMarks?.averages?.student ?? 0}/20\n**Classe :** ${cachedMarks?.averages?.studentClass ?? 0}/20`
82 | }, {
83 | name: "Modification",
84 | value: `**Élève :** ${studentEdit > 0 ? "+" + studentEdit : studentEdit}\n**Classe :** ${classEdit > 0 ? "+" + classEdit : classEdit}`
85 | }
86 | ])
87 | .setColor("#70C7A4");
88 | averageMsg = await channel.send({embeds: [generalEmbed]}).catch(console.error);
89 | };
90 |
91 | /**
92 | * Envoi une notification de devoir sur Discord
93 | * @param {any} homework Le devoir à envoyer
94 | */
95 | client.notif.homework = async (homework) => {
96 | const channel = client.channels.cache.get(process.env.HOMEWORKS_CHANNEL_ID);
97 | if (!channel) return new ReferenceError("HOMEWORKS_CHANNEL_ID is not defined");
98 |
99 | const content = NodeHtmlMarkdown.translate(homework.htmlDescription);
100 |
101 | const embed = new EmbedBuilder()
102 | .setAuthor({
103 | name: "Pronote",
104 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
105 | url: process.env.PRONOTE_URL,
106 | })
107 | .setTitle(homework.subject.toUpperCase())
108 | .setDescription(content)
109 | .setFooter({text: `Devoir pour le ${moment(homework.for).format("dddd Do MMMM")}`})
110 | .setURL(process.env.PRONOTE_URL)
111 | .setColor(homework.color ?? "#70C7A4");
112 |
113 | let attachments = [];
114 | let files = [];
115 | if (homework.files.length >= 1) {
116 | await client.functions.asyncForEach(homework.files, async file => {
117 | const properties = await client.functions.getFileProperties(file);
118 | if (properties.type === "file") attachments.push(properties.attachment);
119 | files.push(properties);
120 | });
121 | }
122 |
123 | const finalAttachments = [];
124 | if (files.length > 10) {
125 | let i = 10;
126 | while (i < files.length) {
127 | channel.send({files: files.slice(i, i + 10)}).then(m => {
128 | finalAttachments.concat(m.attachments);
129 | }).catch(console.error);
130 | i += 10;
131 | }
132 | }
133 |
134 | channel.send({embeds: [embed], files: attachments}).then((e) => {
135 | if (homework.done) e.react("✅").then(() => {
136 | });
137 | if (homework.files.length) {
138 | finalAttachments.concat(e.attachments);
139 | let string = "";
140 | files.forEach(file => {
141 | if (file.type === "file") {
142 | const name = client.functions.setFileName(file.name);
143 | const attachment = finalAttachments.find(a => a.name === name);
144 | if (attachment) {
145 | string += `[${file.name}](${attachment.url})\n`;
146 | }
147 | else {
148 | string += `${file.name}\n`;
149 | console.log("Attachment not found.\nTo found name: " + name, "Original name: " + file.name, "\nAttachments: " + e.attachments.map(a => a.name));
150 | }
151 | } else {
152 | string += `[${file.name ?? file.url}](${file.url})\n`;
153 | }
154 | });
155 |
156 | const strings = client.functions.splitMessage(string, {
157 | maxLength: 1024,
158 | });
159 |
160 | embed.addFields(strings.map((s, i) => {
161 | return {
162 | name: i === 0 ? "Fichiers joints" : "\u200b",
163 | value: s,
164 | };
165 | }));
166 | e.edit({embeds: [embed]});
167 | }
168 | }).catch(console.error);
169 | };
170 |
171 | /**
172 | * Envoi une notification de cours annulé sur Discord
173 | * @param {any} awayNotif Les informations sur le cours annulé
174 | */
175 | client.notif.away = (awayNotif) => {
176 | const channel = client.channels.cache.get(process.env.AWAY_CHANNEL_ID);
177 | if (!channel) return new ReferenceError("AWAY_CHANNEL_ID is not defined");
178 |
179 | const embed = new EmbedBuilder()
180 | .setAuthor({
181 | name: "Pronote",
182 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
183 | url: process.env.PRONOTE_URL
184 | })
185 | .setTitle(awayNotif.subject.toUpperCase())
186 | .setURL(process.env.PRONOTE_URL)
187 | .setDescription(`${awayNotif.teacher} sera absent le ${moment(awayNotif.from).format("dddd Do MMMM")}`)
188 | .setFooter({text: `Cours annulé de ${awayNotif.subject}`})
189 | .setColor("#70C7A4");
190 |
191 | channel.send({embeds: [embed]}).then(() => {
192 | }).catch(console.error);
193 | };
194 |
195 | /**
196 | * Envoi une notification d'information sur Discord
197 | * @param {any} infoNotif L'information à envoyer
198 | */
199 | client.notif.info = async (infoNotif) => {
200 | const channel = client.channels.cache.get(process.env.INFOS_CHANNEL_ID);
201 | if (!channel) return new ReferenceError("INFOS_CHANNEL_ID is not defined");
202 |
203 | let content = NodeHtmlMarkdown.translate(infoNotif.htmlContent);
204 |
205 | const splitted = client.functions.splitMessage(content, {
206 | maxLength: 4096,
207 | });
208 |
209 | const embed = new EmbedBuilder()
210 | .setTitle(infoNotif.title ?? "Sans titre titre")
211 | .setDescription(splitted.shift())
212 | .setURL(process.env.PRONOTE_URL)
213 | .setColor("#70C7A4");
214 |
215 | if (infoNotif.author) embed.setAuthor({
216 | name: infoNotif.author,
217 | iconURL: "https://www.index-education.com/contenu/img/commun/logo-pronote-menu.png",
218 | url: process.env.PRONOTE_URL
219 | });
220 | const embeds = [embed];
221 | const attachments = [];
222 | let files = [];
223 |
224 | await client.functions.asyncForEach(splitted, async (s, index) => {
225 | const embed = new EmbedBuilder()
226 | .setDescription(s)
227 | .setColor("#70C7A4");
228 | if (index === splitted.length - 1) {
229 | embed.setFooter({text: `Information du ${moment(infoNotif.date).format("dddd Do MMMM")}`});
230 | }
231 | embeds.push(embed);
232 | });
233 |
234 | if (infoNotif.files.length >= 1) {
235 | await client.functions.asyncForEach(infoNotif.files, async (file) => {
236 | const properties = await client.functions.getFileProperties(file);
237 | if (properties.type === "file") {
238 | attachments.push(properties.attachment);
239 | }
240 | files.push(properties);
241 | });
242 | }
243 |
244 | const finalAttachments = [];
245 | if (attachments.length > 10) {
246 | let i = 10;
247 | while (i < attachments.length) {
248 | channel.send({files: attachments.slice(i, i + 10)}).then(m => {
249 | finalAttachments.concat(m.attachments);
250 | }).catch(console.error);
251 | i += 10;
252 | }
253 | }
254 |
255 | channel.send({embeds, files: attachments}).then(m => {
256 | if (files.length) {
257 | finalAttachments.concat(m.attachments);
258 | let string = "";
259 | files.forEach(file => {
260 | if (file.type === "file") {
261 | const name = client.functions.setFileName(file.name);
262 | const attachment = m.attachments.find(a => a.name === name);
263 | if (attachment) {
264 | string += `[${file.name}](${attachment.url})\n`;
265 | }
266 | else {
267 | string += `${file.name}\n`;
268 | console.log("Attachment not found.\nID: "+ infoNotif.id +" To found name: " + name, "Original name: " + file.name, "\nAttachments: " + m.attachments.map(a => a.name));
269 | }
270 | } else {
271 | string += `[${file.name}](${file.url})\n`;
272 | }
273 | });
274 | let strings = client.functions.splitMessage(string, {
275 | maxLength: 1024,
276 | });
277 |
278 | embeds[embeds.length - 1].addFields(strings.map((s, index) => {
279 | return {
280 | name: index === 0 ? "Pièces jointes" : "\u200b",
281 | value: s,
282 | inline: false
283 | };
284 | }));
285 | m.edit({embeds});
286 | }
287 | }).catch(console.error);
288 | };
289 | };
--------------------------------------------------------------------------------
/utils/pronoteSynchronization.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const pronote = require("pronote-api-maintained");
3 |
4 |
5 | /**
6 | * Synchronise le client.cache avec Pronote et se charge d'appeler les fonctions qui envoient les notifications
7 | * @param {Client} client Le client Discord
8 | * @returns {void}
9 | */
10 |
11 | module.exports = async (client) => {
12 | let hasAlready = client.session;
13 | if (!client.session) {
14 | const cas = (process.env.PRONOTE_CAS && process.env.PRONOTE_CAS.length > 0 ? process.env.PRONOTE_CAS : "none");
15 | client.session = await pronote.login(process.env.PRONOTE_URL, process.env.PRONOTE_USERNAME, process.env.PRONOTE_PASSWORD, cas, "student").catch(console.error);
16 | }
17 | const session = client.session;
18 |
19 | // Connexion à Pronote
20 | if (!session) return;
21 |
22 | client.cache.classe = session.user.studentClass.name;
23 |
24 | // Vérification des devoirs
25 | if (process.env.HOMEWORKS_CHANNEL_ID) {
26 | let homeworks = await session.homeworks(Date.now(), session.params.lastDay);
27 | const newHomeworks = homeworks.filter((work) => !(client.cache.homeworks.some((cacheWork) => cacheWork.id === work.id)));
28 | if (newHomeworks.length > 0) {
29 | await client.functions.asyncForEach(newHomeworks, (work) => {
30 | client.notif.homework(work);
31 | });
32 | }
33 |
34 | // Mise à jour du client.cache pour les devoirs
35 | client.db.writeCache({
36 | ...client.cache,
37 | homeworks
38 | });
39 | }
40 |
41 | // Vérification des notes
42 | if (process.env.MARKS_CHANNEL_ID) {
43 | const marks = await session.marks("trimester");
44 | const marksNotifications = [];
45 | marks.subjects.forEach((subject) => {
46 | const cachedSubject = client.cache.marks.subjects.find(sub => sub.name === subject.name && sub.color === subject.color);
47 | if (cachedSubject) {
48 | const newMarks = subject.marks.filter((mark) => !(cachedSubject.marks.some((cacheMark) => cacheMark.id === mark.id)));
49 | newMarks.forEach((mark) => marksNotifications.push({subject, mark}));
50 | } else {
51 | subject.marks.forEach((mark) => marksNotifications.push({subject, mark}));
52 | }
53 | });
54 | if (marksNotifications.length > 0) {
55 | const marksCache = JSON.parse(JSON.stringify(client.cache.marks));
56 | const date = new Date();
57 | const madeMarks = [];
58 | marksNotifications.forEach(n => {
59 | const subject = client.cache.marks.subjects.find(s => n.subject.name === s.name && n.subject.color === s.color);
60 | if (!subject) {
61 | client.cache.marks.subjects.push(Object.assign(n.subject, {
62 | averagesHistory: [{
63 | date,
64 | student: n.subject.averages.student,
65 | studentClass: n.subject.averages.studentClass,
66 | }]
67 | }));
68 | madeMarks.push(n.subject.name);
69 | } else subject.marks.push(n.mark);
70 | if (!madeMarks.includes(n.subject.name)) {
71 | subject.averages = n.subject.averages;
72 | subject.averagesHistory.push({
73 | date,
74 | student: n.subject.averages.student,
75 | studentClass: n.subject.averages.studentClass,
76 | });
77 | client.cache.marks.subjects[client.cache.marks.subjects.findIndex(s => n.subject.name === s.name)] = subject;
78 | madeMarks.push(n.subject.name);
79 | }
80 | });
81 | if (!client.cache.marks.averages) {
82 | client.cache.marks.averages = {
83 | student: 0,
84 | studentClass: 0,
85 | history: []
86 | };
87 | }
88 | client.cache.marks.averages.student = marks.averages.student;
89 | client.cache.marks.averages.studentClass = marks.averages.studentClass;
90 | client.cache.marks.averages.history.push({
91 | date,
92 | student: marks.averages.student,
93 | studentClass: marks.averages.studentClass
94 | });
95 | client.notif.mark(marksNotifications, marksCache);
96 | }
97 | // Mise à jour du client.cache pour les notes
98 | client.db.writeCache(client.cache);
99 | }
100 |
101 | // Vérification des absences
102 | if (process.env.AWAY_CHANNEL_ID) {
103 | const nextWeekDay = new Date();
104 | nextWeekDay.setDate(nextWeekDay.getDate() + 30);
105 | const timetable = await session.timetable(new Date(), nextWeekDay);
106 | const awayNotifications = [];
107 | timetable.filter((lesson) => (lesson.isAway || lesson.isCancelled) && !lesson.hasDuplicate).forEach((lesson) => {
108 | if (!client.cache.lessonsAway.some((lessonID) => lessonID === lesson.id)) {
109 | awayNotifications.push({
110 | teacher: lesson.teacher,
111 | from: lesson.from,
112 | subject: lesson.subject,
113 | id: lesson.id
114 | });
115 | }
116 | });
117 | if (awayNotifications.length) {
118 | awayNotifications.forEach((awayNotif) => client.notif.away(awayNotif));
119 | }
120 |
121 | client.db.writeCache({
122 | ...client.cache,
123 | lessonsAway: [
124 | ...client.cache.lessonsAway,
125 | ...awayNotifications.map((n) => n.id)
126 | ]
127 | });
128 | }
129 |
130 | // Vérification des informations
131 | if (process.env.INFOS_CHANNEL_ID) {
132 | const infos = await session.infos();
133 | const infosNotifications = [];
134 | infos.forEach(info => {
135 | if (!client.cache.infos.some((infId) => infId === info.id)) {
136 | infosNotifications.push(info);
137 | }
138 | });
139 | if (infosNotifications.length > 0) {
140 | infosNotifications.forEach(inf => client.notif.info(inf));
141 | }
142 | // Mise à jour du client.cache pour les notes
143 | client.db.writeCache({
144 | ...client.cache,
145 | infos: [
146 | ...client.cache.infos,
147 | ...infosNotifications.map((n) => n.id)
148 | ]
149 | });
150 |
151 | }
152 |
153 | await client.session.contents(new Date(new Date().getFullYear(), 8, 1), new Date()).then(async (contents) => {
154 | client.db.writeCache({
155 | ...client.cache,
156 | contents: contents.map((content) => {
157 | return {
158 | id: content.id,
159 | subject: content.subject,
160 | from: content.from,
161 | to: content.to,
162 | };
163 | })
164 | });
165 | });
166 |
167 | await client.session.files().then(async (files) => {
168 | client.db.writeCache({
169 | ...client.cache,
170 | files: files.map(file => {
171 | file.url = undefined;
172 | return file;
173 | })
174 | });
175 | });
176 |
177 |
178 | if (!hasAlready) {
179 | await client.session.logout();
180 | client.session = null;
181 | }
182 |
183 | return console.log("\x1b[92m" + client.functions.parseTime() + " | Une vérification vient juste d'être effectuée !\x1b[0m");
184 | };
--------------------------------------------------------------------------------
/utils/subjects.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "histoire_geographie": {
3 | name: "Histoire-Géographie",
4 | regex: /Histoire[-\s]G[ée]ograph(ie|\.)|H[-\s]G/i,
5 | coef: {
6 | "1": 3,
7 | "T": 3,
8 | },
9 | controleContinu: true,
10 | },
11 | "enseignement_scientifique": {
12 | name: "Enseignement Scientifique",
13 | regex: /Enseign(ement|\.)[-\s]Scientifique|es/i,
14 | coef: {
15 | "1": 3,
16 | "T": 3,
17 | },
18 | controleContinu: true,
19 | },
20 | "lva": {
21 | name: "Langue Vivante A",
22 | regex: /LV[A1]/i,
23 | coef: {
24 | "1": 3,
25 | "T": 3,
26 | },
27 | controleContinu: true,
28 | },
29 | "lvb": {
30 | name: "Langue Vivante B",
31 | regex: /LV[B2]/i,
32 | coef: {
33 | "1": 3,
34 | "T": 3,
35 | },
36 | controleContinu: true,
37 | },
38 | "eps": {
39 | name: "Éducation Physique et Sportive",
40 | regex: /EPS|Sport|[ée]ducation physique et sportive/i,
41 | coef: {
42 | "1": 0,
43 | "T": 6,
44 | },
45 | controleContinu: false,
46 | },
47 | "emc": {
48 | name: "Éducation Morale et Civique",
49 | regex: /EMC|Ens(eignement|\.)[-\s]moral[-\s]&?[-\s]?civique/i,
50 | coef: {
51 | "1": 1,
52 | "T": 1,
53 | },
54 | controleContinu: false,
55 | },
56 | "philosophie": {
57 | name: "Philosophie",
58 | regex: /Philo(sophie|\.)?/i,
59 | coef: {
60 | "1": 8,
61 | "T": 8,
62 | },
63 | controleContinu: false,
64 | },
65 | "spe_amc": {
66 | name: "Spécialité d'Anglais du Monde Contemporain",
67 | regex: /(spe\s)?AMC(\sspe)/i,
68 | coef: {
69 | "1": 8,
70 | "T": 16,
71 | },
72 | controleContinu: false,
73 | },
74 | "spe_art": {
75 | name: "Spécialité d'Arts",
76 | regex: /(spe\s)?art(\sspe)/i,
77 | coef: {
78 | "1": 8,
79 | "T": 16,
80 | },
81 | controleContinu: false,
82 | },
83 | "spe_bio": {
84 | name: "Spécialité de Biologie-écologie",
85 | regex: /(spe\s)?bio(\sspe)?/i,
86 | coef: {
87 | "1": 8,
88 | "T": 16,
89 | },
90 | controleContinu: false,
91 | },
92 | "spe_hggsp": {
93 | name: "Spécialité d'Histoire-Géographie-Géopolitique et Sciences Politiques",
94 | regex: /(spe\s)?HGGSP(\sspe)|hist\.geo\.geopol\.s.p/i,
95 | coef: {
96 | "1": 8,
97 | "T": 16,
98 | },
99 | controleContinu: false,
100 | },
101 | "spe_hlp": {
102 | name: "Spécialité d'Histoire-Littérature et Philosophie",
103 | regex: /(spe\s)?hlp(\sspe)|human\.litter\.philo\./i,
104 | coef: {
105 | "1": 8,
106 | "T": 16,
107 | },
108 | controleContinu: false,
109 | },
110 | "spe_llcer": {
111 | name: "Spécialité de Langues et Littératures Classiques et Étrangères et Régionales",
112 | regex: /(spe\s)?llcer(\sspe)/i,
113 | coef: {
114 | "1": 8,
115 | "T": 16,
116 | },
117 | controleContinu: false,
118 | },
119 | "spe_llca": {
120 | name: "Spécialité de Langues et Littératures Classiques et Anciennes",
121 | regex: /(spe\s)?llca(\sspe)/i,
122 | coef: {
123 | "1": 8,
124 | "T": 16,
125 | },
126 | },
127 | "spe_maths": {
128 | name: "Spécialité de Mathématiques",
129 | regex: /(spe\s)?math(s|th[eé]matiques?)(\sspe)/i,
130 | coef: {
131 | "1": 8,
132 | "T": 16,
133 | },
134 | controleContinu: false,
135 | },
136 | "spe_nsi": {
137 | name: "Spécialité de Sciences Numériques et Informatiques",
138 | regex: /(spe\s?)?nsi(\s?spe)|num([ée]rique|\.)\s?sc(ience|\.)\s?inf((o(rm(atique|\.)|\.)|\.)|\.)/i,
139 | coef: {
140 | "1": 8,
141 | "T": 16,
142 | },
143 | controleContinu: false,
144 | },
145 | "spe_phch": {
146 | name: "Spécialité de Physique-Chimie",
147 | regex: /phch|physique[-\s]chimie/i,
148 | coef: {
149 | "1": 8,
150 | "T": 16,
151 | },
152 | controleContinu: false,
153 | },
154 | "spe_svt": {
155 | name: "Spécialité de Sciences de la Vie et de la Terre",
156 | regex: /(spe\s)?svt(\sspe)|sciences?[-\s]vie[-\s]&?[-\s]?terre/i,
157 | coef: {
158 | "1": 8,
159 | "T": 16,
160 | },
161 | controleContinu: false,
162 | },
163 | "spe_si": {
164 | name: "Spécialité de Sciences de l'Ingénieur",
165 | regex: /(spe\s)?Science[-\s](de\sl')?ing[eé]nieur(\sspe)/i,
166 | coef: {
167 | "1": 8,
168 | "T": 16,
169 | },
170 | controleContinu: false,
171 | },
172 | "spe_ses": {
173 | name: "Spécialité de Sciences Économiques et Sociales",
174 | regex: /(spe\s)?ses(\sspe)/i,
175 | coef: {
176 | "1": 8,
177 | "T": 16,
178 | },
179 | controleContinu: false,
180 | }
181 | };
--------------------------------------------------------------------------------
/utils/update.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const jszip = require("jszip");
3 | const fs = require("fs");
4 |
5 | function cmpVersions (a, b) {
6 | let i, diff;
7 | let regExStrip0 = /(\.0+)+$/;
8 | let segmentsA = a.replace(regExStrip0, "").split(".");
9 | let segmentsB = b.replace(regExStrip0, "").split(".");
10 | let l = Math.min(segmentsA.length, segmentsB.length);
11 |
12 | for (i = 0; i < l; i++) {
13 | diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
14 | if (diff) {
15 | return diff;
16 | }
17 | }
18 | return segmentsA.length - segmentsB.length;
19 | }
20 |
21 | module.exports = {
22 | checkUpdate: async () => {
23 | console.log("Checking for updates...");
24 | return new Promise((resolve, reject) => {
25 | fetch("https://api.github.com/repos/Merlode11/pronote-bot-discord/releases/latest").then(res => res.json()).then(data => {
26 | console.log("Current version: " + require("../package.json").version);
27 | resolve(cmpVersions(require("../package.json").version, data.tag_name) !== 0);
28 | }).catch(err => {
29 | console.error(err);
30 | reject(err);
31 | });
32 | });
33 | },
34 | /**
35 | * Update the files in the project directory downloaded from GitHub
36 | * @return {Promise}
37 | */
38 | updateFiles: async () => {
39 | return new Promise((resolve, reject) => {
40 | fetch("https://api.github.com/repos/Merlode11/pronote-bot-discord/releases/latest").then(res => res.json()).then(data => {
41 | if (data.tag_name !== require("../package.json").version) {
42 | fetch(data.zipball_url).then(res => res.buffer()).then(async data => {
43 | try {
44 | const jszipInstance = new jszip();
45 | const result = await jszipInstance.loadAsync(data);
46 |
47 | for (let key in Object.keys(result.files)) {
48 | const item = result.files[Object.keys(result.files)[key]];
49 | item.name = item.name.split("/").slice(1).join("/");
50 | try {
51 | if (item.name) {
52 | if (item.dir && !fs.existsSync(item.name)) {
53 | fs.mkdirSync(item.name);
54 | } else {
55 | fs.writeFileSync("./"+item.name, await item.async("nodebuffer"));
56 | }
57 | }
58 | } catch (e) {
59 | reject(e);
60 | }
61 | }
62 | resolve();
63 | } catch (e) {
64 | reject(e);
65 | }
66 | }).catch(err => {
67 | reject(err);
68 | });
69 | }
70 | }).catch(err => {
71 | reject(err);
72 | });
73 | });
74 | }
75 | };
76 |
--------------------------------------------------------------------------------
/utils/verif-env.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | module.exports = async () => {
4 | if (!fs.existsSync(".env")) {
5 | console.log("No .env file found, creating one...");
6 | if (fs.existsSync(".env.example")) {
7 | fs.copyFileSync(".env.example", ".env");
8 | } else {
9 | throw new Error("No .env or .env.example file found, please create one.");
10 | }
11 | }
12 | require("dotenv").config();
13 | const env = process.env;
14 | for (let key in env) {
15 | env[key] = env[key].replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/"/g, "");
16 | if (key.endsWith("_CHANNEL_ID")) {
17 | if (!env[key] || !env[key].match(/^\d{17,20}$/)) {
18 | console.warn("\x1b[33mNo channel ID for " + key + ", please set one in .env\x1b[0m");
19 | }
20 | } else if (key === "PRONOTE_URL") {
21 | if (!env[key]) {
22 | throw new Error("No Pronote URL set, please set one in .env");
23 | } else if (!env[key].match(/https:\/\/[0-9]{7}\w\.index-education\.net\/pronote\//g)) {
24 | throw new Error("Pronote URL must be in the form https://1234567A.index-education.net/pronote/");
25 | }
26 | } else if (key === "PRONOTE_USERNAME") {
27 | if (!env[key] || env[key] === "TON_IDENTIFIANT") {
28 | throw new Error("No Pronote username set, please set one in .env");
29 | }
30 | } else if (key === "PRONOTE_PASSWORD") {
31 | if (!env[key] || env[key] === "TON_MOT_DE_PASSE") {
32 | throw new Error("No Pronote password set, please set one in .env");
33 | }
34 | } else if (key === "TOKEN") {
35 | if (!env[key] || env[key] === "TON_TOKEN") {
36 | throw new Error("No Discord bot token set, please set one in .env");
37 | }
38 | } else if (key === "PRONOTE_CAS") {
39 | const { casList } = require("pronote-api-maintained");
40 | if (!env[key]) {
41 | throw new Error("No CAS set, please set one in .env");
42 | }
43 | if (!casList.includes(env[key])) {
44 | throw new Error("Pronote CAS not found, please set a correct one in .env (none or leave empty for default)");
45 | }
46 | }
47 | }
48 | // Verify the node_modules versions
49 | const { dependencies, devDependencies } = require("../package.json");
50 | const realDeps = { };
51 | fs.readdirSync("./node_modules").forEach((module) => {
52 | if (fs.existsSync(`./node_modules/${module}/package.json`)) {
53 | realDeps[module] = require(`../node_modules/${module}/package.json`).version;
54 | }
55 | });
56 | const deps = Object.assign({}, dependencies, devDependencies);
57 | for (let dep in realDeps) {
58 | if (deps[dep] && realDeps[dep] !== deps[dep]?.replace(/[\^~]/g, "")) {
59 | console.warn("\x1b[33m" + dep + " is not up to date, please run npm update\x1b[0m");
60 | }
61 | }
62 | };
63 |
--------------------------------------------------------------------------------