├── .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 | ![screen-exemple](./screen-exemple.png) 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 | [![Miniature de la création du bot](http://img.youtube.com/vi/Y8RcqgmYVU8/0.jpg)](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.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 | --------------------------------------------------------------------------------