diff --git a/.github/workflows/deploy_gipsy-beta.yaml b/.github/workflows/deploy_gipsy-beta.yaml
new file mode 100644
index 00000000..53621802
--- /dev/null
+++ b/.github/workflows/deploy_gipsy-beta.yaml
@@ -0,0 +1,38 @@
+name: Deploy Gipsy Beta
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - beta
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0 # fetch all history
+ submodules: recursive
+ - name: Deploy to server
+ uses: swznd/sftp-deploy@master
+ with:
+ host: ${{ secrets.BETA_SFTP_IP }}
+ port: ${{ secrets.BETA_SFTP_PORT }}
+ user: ${{ secrets.BETA_SFTP_USER }}
+ password: ${{ secrets.BETA_SFTP_PASSWORD }}
+ ignore: .github/**,.gitignore,**/.gitignore,**/.git/**
+ - name: Restart running bot
+ env:
+ GIPSY_RESTART_BETA_ENDPOINT: ${{ secrets.GIPSY_RESTART_BETA_ENDPOINT }}
+ GIPSY_RESTART_BETA_BEARER: ${{ secrets.GIPSY_RESTART_BETA_BEARER }}
+ run: |
+ curl $GIPSY_RESTART_BETA_ENDPOINT \
+ -H 'Accept: application/json' \
+ -H 'Content-Type: application/json' \
+ -H "Authorization: Bearer $GIPSY_RESTART_BETA_BEARER" \
+ -X POST \
+ -d '{
+ "signal": "restart"
+ }'
diff --git a/.github/workflows/deploy_gipsy-prod.yaml b/.github/workflows/deploy_gipsy-prod.yaml
new file mode 100644
index 00000000..0ef9075a
--- /dev/null
+++ b/.github/workflows/deploy_gipsy-prod.yaml
@@ -0,0 +1,38 @@
+name: Deploy Gipsy Prod
+
+on:
+ workflow_dispatch:
+ #push:
+ # branches:
+ # - master
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0 # fetch all history
+ submodules: recursive
+ - name: Deploy to server
+ uses: swznd/sftp-deploy@master
+ with:
+ host: ${{ secrets.PROD_SFTP_IP }}
+ port: ${{ secrets.PROD_SFTP_PORT }}
+ user: ${{ secrets.PROD_SFTP_USER }}
+ password: ${{ secrets.PROD_SFTP_PASSWORD }}
+ ignore: .github/**,.gitignore,**/.gitignore,**/.git/**
+ - name: Restart running bot
+ env:
+ GIPSY_RESTART_PROD_ENDPOINT: ${{ secrets.GIPSY_RESTART_PROD_ENDPOINT }}
+ GIPSY_RESTART_PROD_BEARER: ${{ secrets.GIPSY_RESTART_PROD_BEARER }}
+ run: |
+ curl $GIPSY_RESTART_PROD_ENDPOINT \
+ -H 'Accept: application/json' \
+ -H 'Content-Type: application/json' \
+ -H 'Authorization: Bearer $GIPSY_RESTART_PROD_BEARER' \
+ -X POST \
+ -d '{
+ "signal": "restart"
+ }'
\ No newline at end of file
diff --git a/.github/workflows/python-linter-pr.yaml b/.github/workflows/python-linter-pr.yaml
index 270874a7..69bc43f4 100644
--- a/.github/workflows/python-linter-pr.yaml
+++ b/.github/workflows/python-linter-pr.yaml
@@ -29,6 +29,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Set up Python
uses: actions/setup-python@v4
@@ -36,6 +38,9 @@ jobs:
python-version: '3.10'
- name: Install Python dependencies
+ run: pip install -r requirements.txt
+
+ - name: Install Linters
run: pip install black pylint
- name: Run linters
diff --git a/.github/workflows/python-linter-push.yaml b/.github/workflows/python-linter-push.yaml
index 777d1a63..8f14405c 100644
--- a/.github/workflows/python-linter-push.yaml
+++ b/.github/workflows/python-linter-push.yaml
@@ -26,6 +26,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
+ with:
+ submodules: recursive
- name: Set up Python
uses: actions/setup-python@v4
@@ -33,6 +35,9 @@ jobs:
python-version: '3.10'
- name: Install Python dependencies
+ run: pip install -r requirements.txt
+
+ - name: Install Linters
run: pip install black pylint
- name: Run linters
@@ -42,6 +47,6 @@ jobs:
pylint: true
pylint_args: "--disable=C,R,I"
auto_fix: true
- git_name: "GuniLint"
- git_email: "linter@gunivers.net"
+ git_name: "GuniBot"
+ git_email: "gunibot@noreply.github.com"
commit_message: "🌟 style: fix code style issues with ${linter}"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1b5e6cdc..fdf82969 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
.vscode
__pycache__
configs/*.json
-logs/
+/config.yaml
+/logs/
.DS_Store
data/*.db*
config.json
@@ -12,7 +13,8 @@ config.json
.idea/
venv/
env/
-docs/_*
-docs/Makefile
-docs/make.bat
-require.json
\ No newline at end of file
+docs/_templates
+docs/_build
+docs/build
+require.json
+docs/plugins
diff --git a/LICENSE.md b/LICENSE.md
index 0820eb51..cc70a345 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,3 +1,549 @@
-# License
+For the English version of the licence, see [LICENSE_en.md](./LICENSE_en.md)
-Nothing here...
\ No newline at end of file
+ CONTRAT DE LICENCE DE LOGICIEL LIBRE CeCILL
+
+Version 2.1 du 2013-06-21
+
+
+ Avertissement
+
+Ce contrat est une licence de logiciel libre issue d'une concertation
+entre ses auteurs afin que le respect de deux grands principes préside à
+sa rédaction:
+
+ * d'une part, le respect des principes de diffusion des logiciels
+ libres: accès au code source, droits étendus conférés aux utilisateurs,
+ * d'autre part, la désignation d'un droit applicable, le droit
+ français, auquel elle est conforme, tant au regard du droit de la
+ responsabilité civile que du droit de la propriété intellectuelle et
+ de la protection qu'il offre aux auteurs et titulaires des droits
+ patrimoniaux sur un logiciel.
+
+Les auteurs de la licence CeCILL (Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
+sont:
+
+Commissariat à l'énergie atomique et aux énergies alternatives - CEA,
+établissement public de recherche à caractère scientifique, technique et
+industriel, dont le siège est situé 25 rue Leblanc, immeuble Le Ponant
+D, 75015 Paris.
+
+Centre National de la Recherche Scientifique - CNRS, établissement
+public à caractère scientifique et technologique, dont le siège est
+situé 3 rue Michel-Ange, 75794 Paris cedex 16.
+
+Institut National de Recherche en Informatique et en Automatique -
+Inria, établissement public à caractère scientifique et technologique,
+dont le siège est situé Domaine de Voluceau, Rocquencourt, BP 105, 78153
+Le Chesnay cedex.
+
+
+ Préambule
+
+Ce contrat est une licence de logiciel libre dont l'objectif est de
+conférer aux utilisateurs la liberté de modification et de
+redistribution du logiciel régi par cette licence dans le cadre d'un
+modèle de diffusion en logiciel libre.
+
+L'exercice de ces libertés est assorti de certains devoirs à la charge
+des utilisateurs afin de préserver ce statut au cours des
+redistributions ultérieures.
+
+L'accessibilité au code source et les droits de copie, de modification
+et de redistribution qui en découlent ont pour contrepartie de n'offrir
+aux utilisateurs qu'une garantie limitée et de ne faire peser sur
+l'auteur du logiciel, le titulaire des droits patrimoniaux et les
+concédants successifs qu'une responsabilité restreinte.
+
+A cet égard l'attention de l'utilisateur est attirée sur les risques
+associés au chargement, à l'utilisation, à la modification et/ou au
+développement et à la reproduction du logiciel par l'utilisateur étant
+donné sa spécificité de logiciel libre, qui peut le rendre complexe à
+manipuler et qui le réserve donc à des développeurs ou des
+professionnels avertis possédant des connaissances informatiques
+approfondies. Les utilisateurs sont donc invités à charger et tester
+l'adéquation du logiciel à leurs besoins dans des conditions permettant
+d'assurer la sécurité de leurs systèmes et/ou de leurs données et, plus
+généralement, à l'utiliser et l'exploiter dans les mêmes conditions de
+sécurité. Ce contrat peut être reproduit et diffusé librement, sous
+réserve de le conserver en l'état, sans ajout ni suppression de clauses.
+
+Ce contrat est susceptible de s'appliquer à tout logiciel dont le
+titulaire des droits patrimoniaux décide de soumettre l'exploitation aux
+dispositions qu'il contient.
+
+Une liste de questions fréquemment posées se trouve sur le site web
+officiel de la famille des licences CeCILL
+(http://www.cecill.info/index.fr.html) pour toute clarification qui
+serait nécessaire.
+
+
+ Article 1 - DEFINITIONS
+
+Dans ce contrat, les termes suivants, lorsqu'ils seront écrits avec une
+lettre capitale, auront la signification suivante:
+
+Contrat: désigne le présent contrat de licence, ses éventuelles versions
+postérieures et annexes.
+
+Logiciel: désigne le logiciel sous sa forme de Code Objet et/ou de Code
+Source et le cas échéant sa documentation, dans leur état au moment de
+l'acceptation du Contrat par le Licencié.
+
+Logiciel Initial: désigne le Logiciel sous sa forme de Code Source et
+éventuellement de Code Objet et le cas échéant sa documentation, dans
+leur état au moment de leur première diffusion sous les termes du Contrat.
+
+Logiciel Modifié: désigne le Logiciel modifié par au moins une
+Contribution.
+
+Code Source: désigne l'ensemble des instructions et des lignes de
+programme du Logiciel et auquel l'accès est nécessaire en vue de
+modifier le Logiciel.
+
+Code Objet: désigne les fichiers binaires issus de la compilation du
+Code Source.
+
+Titulaire: désigne le ou les détenteurs des droits patrimoniaux d'auteur
+sur le Logiciel Initial.
+
+Licencié: désigne le ou les utilisateurs du Logiciel ayant accepté le
+Contrat.
+
+Contributeur: désigne le Licencié auteur d'au moins une Contribution.
+
+Concédant: désigne le Titulaire ou toute personne physique ou morale
+distribuant le Logiciel sous le Contrat.
+
+Contribution: désigne l'ensemble des modifications, corrections,
+traductions, adaptations et/ou nouvelles fonctionnalités intégrées dans
+le Logiciel par tout Contributeur, ainsi que tout Module Interne.
+
+Module: désigne un ensemble de fichiers sources y compris leur
+documentation qui permet de réaliser des fonctionnalités ou services
+supplémentaires à ceux fournis par le Logiciel.
+
+Module Externe: désigne tout Module, non dérivé du Logiciel, tel que ce
+Module et le Logiciel s'exécutent dans des espaces d'adressage
+différents, l'un appelant l'autre au moment de leur exécution.
+
+Module Interne: désigne tout Module lié au Logiciel de telle sorte
+qu'ils s'exécutent dans le même espace d'adressage.
+
+GNU GPL: désigne la GNU General Public License dans sa version 2 ou
+toute version ultérieure, telle que publiée par Free Software Foundation
+Inc.
+
+GNU Affero GPL: désigne la GNU Affero General Public License dans sa
+version 3 ou toute version ultérieure, telle que publiée par Free
+Software Foundation Inc.
+
+EUPL: désigne la Licence Publique de l'Union européenne dans sa version
+1.1 ou toute version ultérieure, telle que publiée par la Commission
+Européenne.
+
+Parties: désigne collectivement le Licencié et le Concédant.
+
+Ces termes s'entendent au singulier comme au pluriel.
+
+
+ Article 2 - OBJET
+
+Le Contrat a pour objet la concession par le Concédant au Licencié d'une
+licence non exclusive, cessible et mondiale du Logiciel telle que
+définie ci-après à l'article 5 <#etendue> pour toute la durée de
+protection des droits portant sur ce Logiciel.
+
+
+ Article 3 - ACCEPTATION
+
+3.1 L'acceptation par le Licencié des termes du Contrat est réputée
+acquise du fait du premier des faits suivants:
+
+ * (i) le chargement du Logiciel par tout moyen notamment par
+ téléchargement à partir d'un serveur distant ou par chargement à
+ partir d'un support physique;
+ * (ii) le premier exercice par le Licencié de l'un quelconque des
+ droits concédés par le Contrat.
+
+3.2 Un exemplaire du Contrat, contenant notamment un avertissement
+relatif aux spécificités du Logiciel, à la restriction de garantie et à
+la limitation à un usage par des utilisateurs expérimentés a été mis à
+disposition du Licencié préalablement à son acceptation telle que
+définie à l'article 3.1 <#acceptation-acquise> ci dessus et le Licencié
+reconnaît en avoir pris connaissance.
+
+
+ Article 4 - ENTREE EN VIGUEUR ET DUREE
+
+
+ 4.1 ENTREE EN VIGUEUR
+
+Le Contrat entre en vigueur à la date de son acceptation par le Licencié
+telle que définie en 3.1 <#acceptation-acquise>.
+
+
+ 4.2 DUREE
+
+Le Contrat produira ses effets pendant toute la durée légale de
+protection des droits patrimoniaux portant sur le Logiciel.
+
+
+ Article 5 - ETENDUE DES DROITS CONCEDES
+
+Le Concédant concède au Licencié, qui accepte, les droits suivants sur
+le Logiciel pour toutes destinations et pour la durée du Contrat dans
+les conditions ci-après détaillées.
+
+Par ailleurs, si le Concédant détient ou venait à détenir un ou
+plusieurs brevets d'invention protégeant tout ou partie des
+fonctionnalités du Logiciel ou de ses composants, il s'engage à ne pas
+opposer les éventuels droits conférés par ces brevets aux Licenciés
+successifs qui utiliseraient, exploiteraient ou modifieraient le
+Logiciel. En cas de cession de ces brevets, le Concédant s'engage à
+faire reprendre les obligations du présent alinéa aux cessionnaires.
+
+
+ 5.1 DROIT D'UTILISATION
+
+Le Licencié est autorisé à utiliser le Logiciel, sans restriction quant
+aux domaines d'application, étant ci-après précisé que cela comporte:
+
+ 1.
+
+ la reproduction permanente ou provisoire du Logiciel en tout ou
+ partie par tout moyen et sous toute forme.
+
+ 2.
+
+ le chargement, l'affichage, l'exécution, ou le stockage du Logiciel
+ sur tout support.
+
+ 3.
+
+ la possibilité d'en observer, d'en étudier, ou d'en tester le
+ fonctionnement afin de déterminer les idées et principes qui sont à
+ la base de n'importe quel élément de ce Logiciel; et ceci, lorsque
+ le Licencié effectue toute opération de chargement, d'affichage,
+ d'exécution, de transmission ou de stockage du Logiciel qu'il est en
+ droit d'effectuer en vertu du Contrat.
+
+
+ 5.2 DROIT D'APPORTER DES CONTRIBUTIONS
+
+Le droit d'apporter des Contributions comporte le droit de traduire,
+d'adapter, d'arranger ou d'apporter toute autre modification au Logiciel
+et le droit de reproduire le logiciel en résultant.
+
+Le Licencié est autorisé à apporter toute Contribution au Logiciel sous
+réserve de mentionner, de façon explicite, son nom en tant qu'auteur de
+cette Contribution et la date de création de celle-ci.
+
+
+ 5.3 DROIT DE DISTRIBUTION
+
+Le droit de distribution comporte notamment le droit de diffuser, de
+transmettre et de communiquer le Logiciel au public sur tout support et
+par tout moyen ainsi que le droit de mettre sur le marché à titre
+onéreux ou gratuit, un ou des exemplaires du Logiciel par tout procédé.
+
+Le Licencié est autorisé à distribuer des copies du Logiciel, modifié ou
+non, à des tiers dans les conditions ci-après détaillées.
+
+
+ 5.3.1 DISTRIBUTION DU LOGICIEL SANS MODIFICATION
+
+Le Licencié est autorisé à distribuer des copies conformes du Logiciel,
+sous forme de Code Source ou de Code Objet, à condition que cette
+distribution respecte les dispositions du Contrat dans leur totalité et
+soit accompagnée:
+
+ 1.
+
+ d'un exemplaire du Contrat,
+
+ 2.
+
+ d'un avertissement relatif à la restriction de garantie et de
+ responsabilité du Concédant telle que prévue aux articles 8
+ <#responsabilite> et 9 <#garantie>,
+
+et que, dans le cas où seul le Code Objet du Logiciel est redistribué,
+le Licencié permette un accès effectif au Code Source complet du
+Logiciel pour une durée d'au moins 3 ans à compter de la distribution du
+logiciel, étant entendu que le coût additionnel d'acquisition du Code
+Source ne devra pas excéder le simple coût de transfert des données.
+
+
+ 5.3.2 DISTRIBUTION DU LOGICIEL MODIFIE
+
+Lorsque le Licencié apporte une Contribution au Logiciel, les conditions
+de distribution du Logiciel Modifié en résultant sont alors soumises à
+l'intégralité des dispositions du Contrat.
+
+Le Licencié est autorisé à distribuer le Logiciel Modifié, sous forme de
+code source ou de code objet, à condition que cette distribution
+respecte les dispositions du Contrat dans leur totalité et soit
+accompagnée:
+
+ 1.
+
+ d'un exemplaire du Contrat,
+
+ 2.
+
+ d'un avertissement relatif à la restriction de garantie et de
+ responsabilité du Concédant telle que prévue aux articles 8
+ <#responsabilite> et 9 <#garantie>,
+
+et, dans le cas où seul le code objet du Logiciel Modifié est redistribué,
+
+ 3.
+
+ d'une note précisant les conditions d'accès effectif au code source
+ complet du Logiciel Modifié, pendant une période d'au moins 3 ans à
+ compter de la distribution du Logiciel Modifié, étant entendu que le
+ coût additionnel d'acquisition du code source ne devra pas excéder
+ le simple coût de transfert des données.
+
+
+ 5.3.3 DISTRIBUTION DES MODULES EXTERNES
+
+Lorsque le Licencié a développé un Module Externe les conditions du
+Contrat ne s'appliquent pas à ce Module Externe, qui peut être distribué
+sous un contrat de licence différent.
+
+
+ 5.3.4 COMPATIBILITE AVEC D'AUTRES LICENCES
+
+Le Licencié peut inclure un code soumis aux dispositions d'une des
+versions de la licence GNU GPL, GNU Affero GPL et/ou EUPL dans le
+Logiciel modifié ou non et distribuer l'ensemble sous les conditions de
+la même version de la licence GNU GPL, GNU Affero GPL et/ou EUPL.
+
+Le Licencié peut inclure le Logiciel modifié ou non dans un code soumis
+aux dispositions d'une des versions de la licence GNU GPL, GNU Affero
+GPL et/ou EUPL et distribuer l'ensemble sous les conditions de la même
+version de la licence GNU GPL, GNU Affero GPL et/ou EUPL.
+
+
+ Article 6 - PROPRIETE INTELLECTUELLE
+
+
+ 6.1 SUR LE LOGICIEL INITIAL
+
+Le Titulaire est détenteur des droits patrimoniaux sur le Logiciel
+Initial. Toute utilisation du Logiciel Initial est soumise au respect
+des conditions dans lesquelles le Titulaire a choisi de diffuser son
+oeuvre et nul autre n'a la faculté de modifier les conditions de
+diffusion de ce Logiciel Initial.
+
+Le Titulaire s'engage à ce que le Logiciel Initial reste au moins régi
+par le Contrat et ce, pour la durée visée à l'article 4.2 <#duree>.
+
+
+ 6.2 SUR LES CONTRIBUTIONS
+
+Le Licencié qui a développé une Contribution est titulaire sur celle-ci
+des droits de propriété intellectuelle dans les conditions définies par
+la législation applicable.
+
+
+ 6.3 SUR LES MODULES EXTERNES
+
+Le Licencié qui a développé un Module Externe est titulaire sur celui-ci
+des droits de propriété intellectuelle dans les conditions définies par
+la législation applicable et reste libre du choix du contrat régissant
+sa diffusion.
+
+
+ 6.4 DISPOSITIONS COMMUNES
+
+Le Licencié s'engage expressément:
+
+ 1.
+
+ à ne pas supprimer ou modifier de quelque manière que ce soit les
+ mentions de propriété intellectuelle apposées sur le Logiciel;
+
+ 2.
+
+ à reproduire à l'identique lesdites mentions de propriété
+ intellectuelle sur les copies du Logiciel modifié ou non.
+
+Le Licencié s'engage à ne pas porter atteinte, directement ou
+indirectement, aux droits de propriété intellectuelle du Titulaire et/ou
+des Contributeurs sur le Logiciel et à prendre, le cas échéant, à
+l'égard de son personnel toutes les mesures nécessaires pour assurer le
+respect des dits droits de propriété intellectuelle du Titulaire et/ou
+des Contributeurs.
+
+
+ Article 7 - SERVICES ASSOCIES
+
+7.1 Le Contrat n'oblige en aucun cas le Concédant à la réalisation de
+prestations d'assistance technique ou de maintenance du Logiciel.
+
+Cependant le Concédant reste libre de proposer ce type de services. Les
+termes et conditions d'une telle assistance technique et/ou d'une telle
+maintenance seront alors déterminés dans un acte séparé. Ces actes de
+maintenance et/ou assistance technique n'engageront que la seule
+responsabilité du Concédant qui les propose.
+
+7.2 De même, tout Concédant est libre de proposer, sous sa seule
+responsabilité, à ses licenciés une garantie, qui n'engagera que lui,
+lors de la redistribution du Logiciel et/ou du Logiciel Modifié et ce,
+dans les conditions qu'il souhaite. Cette garantie et les modalités
+financières de son application feront l'objet d'un acte séparé entre le
+Concédant et le Licencié.
+
+
+ Article 8 - RESPONSABILITE
+
+8.1 Sous réserve des dispositions de l'article 8.2
+<#limite-responsabilite>, le Licencié a la faculté, sous réserve de
+prouver la faute du Concédant concerné, de solliciter la réparation du
+préjudice direct qu'il subirait du fait du Logiciel et dont il apportera
+la preuve.
+
+8.2 La responsabilité du Concédant est limitée aux engagements pris en
+application du Contrat et ne saurait être engagée en raison notamment:
+(i) des dommages dus à l'inexécution, totale ou partielle, de ses
+obligations par le Licencié, (ii) des dommages directs ou indirects
+découlant de l'utilisation ou des performances du Logiciel subis par le
+Licencié et (iii) plus généralement d'un quelconque dommage indirect. En
+particulier, les Parties conviennent expressément que tout préjudice
+financier ou commercial (par exemple perte de données, perte de
+bénéfices, perte d'exploitation, perte de clientèle ou de commandes,
+manque à gagner, trouble commercial quelconque) ou toute action dirigée
+contre le Licencié par un tiers, constitue un dommage indirect et
+n'ouvre pas droit à réparation par le Concédant.
+
+
+ Article 9 - GARANTIE
+
+9.1 Le Licencié reconnaît que l'état actuel des connaissances
+scientifiques et techniques au moment de la mise en circulation du
+Logiciel ne permet pas d'en tester et d'en vérifier toutes les
+utilisations ni de détecter l'existence d'éventuels défauts. L'attention
+du Licencié a été attirée sur ce point sur les risques associés au
+chargement, à l'utilisation, la modification et/ou au développement et à
+la reproduction du Logiciel qui sont réservés à des utilisateurs avertis.
+
+Il relève de la responsabilité du Licencié de contrôler, par tous
+moyens, l'adéquation du produit à ses besoins, son bon fonctionnement et
+de s'assurer qu'il ne causera pas de dommages aux personnes et aux biens.
+
+9.2 Le Concédant déclare de bonne foi être en droit de concéder
+l'ensemble des droits attachés au Logiciel (comprenant notamment les
+droits visés à l'article 5 <#etendue>).
+
+9.3 Le Licencié reconnaît que le Logiciel est fourni "en l'état" par le
+Concédant sans autre garantie, expresse ou tacite, que celle prévue à
+l'article 9.2 <#bonne-foi> et notamment sans aucune garantie sur sa
+valeur commerciale, son caractère sécurisé, innovant ou pertinent.
+
+En particulier, le Concédant ne garantit pas que le Logiciel est exempt
+d'erreur, qu'il fonctionnera sans interruption, qu'il sera compatible
+avec l'équipement du Licencié et sa configuration logicielle ni qu'il
+remplira les besoins du Licencié.
+
+9.4 Le Concédant ne garantit pas, de manière expresse ou tacite, que le
+Logiciel ne porte pas atteinte à un quelconque droit de propriété
+intellectuelle d'un tiers portant sur un brevet, un logiciel ou sur tout
+autre droit de propriété. Ainsi, le Concédant exclut toute garantie au
+profit du Licencié contre les actions en contrefaçon qui pourraient être
+diligentées au titre de l'utilisation, de la modification, et de la
+redistribution du Logiciel. Néanmoins, si de telles actions sont
+exercées contre le Licencié, le Concédant lui apportera son expertise
+technique et juridique pour sa défense. Cette expertise technique et
+juridique est déterminée au cas par cas entre le Concédant concerné et
+le Licencié dans le cadre d'un protocole d'accord. Le Concédant dégage
+toute responsabilité quant à l'utilisation de la dénomination du
+Logiciel par le Licencié. Aucune garantie n'est apportée quant à
+l'existence de droits antérieurs sur le nom du Logiciel et sur
+l'existence d'une marque.
+
+
+ Article 10 - RESILIATION
+
+10.1 En cas de manquement par le Licencié aux obligations mises à sa
+charge par le Contrat, le Concédant pourra résilier de plein droit le
+Contrat trente (30) jours après notification adressée au Licencié et
+restée sans effet.
+
+10.2 Le Licencié dont le Contrat est résilié n'est plus autorisé à
+utiliser, modifier ou distribuer le Logiciel. Cependant, toutes les
+licences qu'il aura concédées antérieurement à la résiliation du Contrat
+resteront valides sous réserve qu'elles aient été effectuées en
+conformité avec le Contrat.
+
+
+ Article 11 - DISPOSITIONS DIVERSES
+
+
+ 11.1 CAUSE EXTERIEURE
+
+Aucune des Parties ne sera responsable d'un retard ou d'une défaillance
+d'exécution du Contrat qui serait dû à un cas de force majeure, un cas
+fortuit ou une cause extérieure, telle que, notamment, le mauvais
+fonctionnement ou les interruptions du réseau électrique ou de
+télécommunication, la paralysie du réseau liée à une attaque
+informatique, l'intervention des autorités gouvernementales, les
+catastrophes naturelles, les dégâts des eaux, les tremblements de terre,
+le feu, les explosions, les grèves et les conflits sociaux, l'état de
+guerre...
+
+11.2 Le fait, par l'une ou l'autre des Parties, d'omettre en une ou
+plusieurs occasions de se prévaloir d'une ou plusieurs dispositions du
+Contrat, ne pourra en aucun cas impliquer renonciation par la Partie
+intéressée à s'en prévaloir ultérieurement.
+
+11.3 Le Contrat annule et remplace toute convention antérieure, écrite
+ou orale, entre les Parties sur le même objet et constitue l'accord
+entier entre les Parties sur cet objet. Aucune addition ou modification
+aux termes du Contrat n'aura d'effet à l'égard des Parties à moins
+d'être faite par écrit et signée par leurs représentants dûment habilités.
+
+11.4 Dans l'hypothèse où une ou plusieurs des dispositions du Contrat
+s'avèrerait contraire à une loi ou à un texte applicable, existants ou
+futurs, cette loi ou ce texte prévaudrait, et les Parties feraient les
+amendements nécessaires pour se conformer à cette loi ou à ce texte.
+Toutes les autres dispositions resteront en vigueur. De même, la
+nullité, pour quelque raison que ce soit, d'une des dispositions du
+Contrat ne saurait entraîner la nullité de l'ensemble du Contrat.
+
+
+ 11.5 LANGUE
+
+Le Contrat est rédigé en langue française et en langue anglaise, ces
+deux versions faisant également foi.
+
+
+ Article 12 - NOUVELLES VERSIONS DU CONTRAT
+
+12.1 Toute personne est autorisée à copier et distribuer des copies de
+ce Contrat.
+
+12.2 Afin d'en préserver la cohérence, le texte du Contrat est protégé
+et ne peut être modifié que par les auteurs de la licence, lesquels se
+réservent le droit de publier périodiquement des mises à jour ou de
+nouvelles versions du Contrat, qui posséderont chacune un numéro
+distinct. Ces versions ultérieures seront susceptibles de prendre en
+compte de nouvelles problématiques rencontrées par les logiciels libres.
+
+12.3 Tout Logiciel diffusé sous une version donnée du Contrat ne pourra
+faire l'objet d'une diffusion ultérieure que sous la même version du
+Contrat ou une version postérieure, sous réserve des dispositions de
+l'article 5.3.4 <#compatibilite>.
+
+
+ Article 13 - LOI APPLICABLE ET COMPETENCE TERRITORIALE
+
+13.1 Le Contrat est régi par la loi française. Les Parties conviennent
+de tenter de régler à l'amiable les différends ou litiges qui
+viendraient à se produire par suite ou à l'occasion du Contrat.
+
+13.2 A défaut d'accord amiable dans un délai de deux (2) mois à compter
+de leur survenance et sauf situation relevant d'une procédure d'urgence,
+les différends ou litiges seront portés par la Partie la plus diligente
+devant les Tribunaux compétents de Paris.
\ No newline at end of file
diff --git a/LICENSE_en.md b/LICENSE_en.md
new file mode 100644
index 00000000..2abecc53
--- /dev/null
+++ b/LICENSE_en.md
@@ -0,0 +1,519 @@
+Pour la version française de ce document, voir [LICENSE.md](./LICENSE.md)
+
+ CeCILL FREE SOFTWARE LICENSE AGREEMENT
+
+Version 2.1 dated 2013-06-21
+
+
+ Notice
+
+This Agreement is a Free Software license agreement that is the result
+of discussions between its authors in order to ensure compliance with
+the two main principles guiding its drafting:
+
+ * firstly, compliance with the principles governing the distribution
+ of Free Software: access to source code, broad rights granted to users,
+ * secondly, the election of a governing law, French law, with which it
+ is conformant, both as regards the law of torts and intellectual
+ property law, and the protection that it offers to both authors and
+ holders of the economic rights over software.
+
+The authors of the CeCILL (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
+license are:
+
+Commissariat à l'énergie atomique et aux énergies alternatives - CEA, a
+public scientific, technical and industrial research establishment,
+having its principal place of business at 25 rue Leblanc, immeuble Le
+Ponant D, 75015 Paris, France.
+
+Centre National de la Recherche Scientifique - CNRS, a public scientific
+and technological establishment, having its principal place of business
+at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
+
+Institut National de Recherche en Informatique et en Automatique -
+Inria, a public scientific and technological establishment, having its
+principal place of business at Domaine de Voluceau, Rocquencourt, BP
+105, 78153 Le Chesnay cedex, France.
+
+
+ Preamble
+
+The purpose of this Free Software license agreement is to grant users
+the right to modify and redistribute the software governed by this
+license within the framework of an open source distribution model.
+
+The exercising of this right is conditional upon certain obligations for
+users so as to preserve this status for all subsequent redistributions.
+
+In consideration of access to the source code and the rights to copy,
+modify and redistribute granted by the license, users are provided only
+with a limited warranty and the software's author, the holder of the
+economic rights, and the successive licensors only have limited liability.
+
+In this respect, the risks associated with loading, using, modifying
+and/or developing or reproducing the software by the user are brought to
+the user's attention, given its Free Software status, which may make it
+complicated to use, with the result that its use is reserved for
+developers and experienced professionals having in-depth computer
+knowledge. Users are therefore encouraged to load and test the
+suitability of the software as regards their requirements in conditions
+enabling the security of their systems and/or data to be ensured and,
+more generally, to use and operate it in the same conditions of
+security. This Agreement may be freely reproduced and published,
+provided it is not altered, and that no provisions are either added or
+removed herefrom.
+
+This Agreement may apply to any or all software for which the holder of
+the economic rights decides to submit the use thereof to its provisions.
+
+Frequently asked questions can be found on the official website of the
+CeCILL licenses family (http://www.cecill.info/index.en.html) for any
+necessary clarification.
+
+
+ Article 1 - DEFINITIONS
+
+For the purpose of this Agreement, when the following expressions
+commence with a capital letter, they shall have the following meaning:
+
+Agreement: means this license agreement, and its possible subsequent
+versions and annexes.
+
+Software: means the software in its Object Code and/or Source Code form
+and, where applicable, its documentation, "as is" when the Licensee
+accepts the Agreement.
+
+Initial Software: means the Software in its Source Code and possibly its
+Object Code form and, where applicable, its documentation, "as is" when
+it is first distributed under the terms and conditions of the Agreement.
+
+Modified Software: means the Software modified by at least one
+Contribution.
+
+Source Code: means all the Software's instructions and program lines to
+which access is required so as to modify the Software.
+
+Object Code: means the binary files originating from the compilation of
+the Source Code.
+
+Holder: means the holder(s) of the economic rights over the Initial
+Software.
+
+Licensee: means the Software user(s) having accepted the Agreement.
+
+Contributor: means a Licensee having made at least one Contribution.
+
+Licensor: means the Holder, or any other individual or legal entity, who
+distributes the Software under the Agreement.
+
+Contribution: means any or all modifications, corrections, translations,
+adaptations and/or new functions integrated into the Software by any or
+all Contributors, as well as any or all Internal Modules.
+
+Module: means a set of sources files including their documentation that
+enables supplementary functions or services in addition to those offered
+by the Software.
+
+External Module: means any or all Modules, not derived from the
+Software, so that this Module and the Software run in separate address
+spaces, with one calling the other when they are run.
+
+Internal Module: means any or all Module, connected to the Software so
+that they both execute in the same address space.
+
+GNU GPL: means the GNU General Public License version 2 or any
+subsequent version, as published by the Free Software Foundation Inc.
+
+GNU Affero GPL: means the GNU Affero General Public License version 3 or
+any subsequent version, as published by the Free Software Foundation Inc.
+
+EUPL: means the European Union Public License version 1.1 or any
+subsequent version, as published by the European Commission.
+
+Parties: mean both the Licensee and the Licensor.
+
+These expressions may be used both in singular and plural form.
+
+
+ Article 2 - PURPOSE
+
+The purpose of the Agreement is the grant by the Licensor to the
+Licensee of a non-exclusive, transferable and worldwide license for the
+Software as set forth in Article 5 <#scope> hereinafter for the whole
+term of the protection granted by the rights over said Software.
+
+
+ Article 3 - ACCEPTANCE
+
+3.1 The Licensee shall be deemed as having accepted the terms and
+conditions of this Agreement upon the occurrence of the first of the
+following events:
+
+ * (i) loading the Software by any or all means, notably, by
+ downloading from a remote server, or by loading from a physical medium;
+ * (ii) the first time the Licensee exercises any of the rights granted
+ hereunder.
+
+3.2 One copy of the Agreement, containing a notice relating to the
+characteristics of the Software, to the limited warranty, and to the
+fact that its use is restricted to experienced users has been provided
+to the Licensee prior to its acceptance as set forth in Article 3.1
+<#accepting> hereinabove, and the Licensee hereby acknowledges that it
+has read and understood it.
+
+
+ Article 4 - EFFECTIVE DATE AND TERM
+
+
+ 4.1 EFFECTIVE DATE
+
+The Agreement shall become effective on the date when it is accepted by
+the Licensee as set forth in Article 3.1 <#accepting>.
+
+
+ 4.2 TERM
+
+The Agreement shall remain in force for the entire legal term of
+protection of the economic rights over the Software.
+
+
+ Article 5 - SCOPE OF RIGHTS GRANTED
+
+The Licensor hereby grants to the Licensee, who accepts, the following
+rights over the Software for any or all use, and for the term of the
+Agreement, on the basis of the terms and conditions set forth hereinafter.
+
+Besides, if the Licensor owns or comes to own one or more patents
+protecting all or part of the functions of the Software or of its
+components, the Licensor undertakes not to enforce the rights granted by
+these patents against successive Licensees using, exploiting or
+modifying the Software. If these patents are transferred, the Licensor
+undertakes to have the transferees subscribe to the obligations set
+forth in this paragraph.
+
+
+ 5.1 RIGHT OF USE
+
+The Licensee is authorized to use the Software, without any limitation
+as to its fields of application, with it being hereinafter specified
+that this comprises:
+
+ 1. permanent or temporary reproduction of all or part of the Software
+ by any or all means and in any or all form.
+
+ 2. loading, displaying, running, or storing the Software on any or all
+ medium.
+
+ 3. entitlement to observe, study or test its operation so as to
+ determine the ideas and principles behind any or all constituent
+ elements of said Software. This shall apply when the Licensee
+ carries out any or all loading, displaying, running, transmission or
+ storage operation as regards the Software, that it is entitled to
+ carry out hereunder.
+
+
+ 5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS
+
+The right to make Contributions includes the right to translate, adapt,
+arrange, or make any or all modifications to the Software, and the right
+to reproduce the resulting software.
+
+The Licensee is authorized to make any or all Contributions to the
+Software provided that it includes an explicit notice that it is the
+author of said Contribution and indicates the date of the creation thereof.
+
+
+ 5.3 RIGHT OF DISTRIBUTION
+
+In particular, the right of distribution includes the right to publish,
+transmit and communicate the Software to the general public on any or
+all medium, and by any or all means, and the right to market, either in
+consideration of a fee, or free of charge, one or more copies of the
+Software by any means.
+
+The Licensee is further authorized to distribute copies of the modified
+or unmodified Software to third parties according to the terms and
+conditions set forth hereinafter.
+
+
+ 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
+
+The Licensee is authorized to distribute true copies of the Software in
+Source Code or Object Code form, provided that said distribution
+complies with all the provisions of the Agreement and is accompanied by:
+
+ 1. a copy of the Agreement,
+
+ 2. a notice relating to the limitation of both the Licensor's warranty
+ and liability as set forth in Articles 8 and 9,
+
+and that, in the event that only the Object Code of the Software is
+redistributed, the Licensee allows effective access to the full Source
+Code of the Software for a period of at least three years from the
+distribution of the Software, it being understood that the additional
+acquisition cost of the Source Code shall not exceed the cost of the
+data transfer.
+
+
+ 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
+
+When the Licensee makes a Contribution to the Software, the terms and
+conditions for the distribution of the resulting Modified Software
+become subject to all the provisions of this Agreement.
+
+The Licensee is authorized to distribute the Modified Software, in
+source code or object code form, provided that said distribution
+complies with all the provisions of the Agreement and is accompanied by:
+
+ 1. a copy of the Agreement,
+
+ 2. a notice relating to the limitation of both the Licensor's warranty
+ and liability as set forth in Articles 8 and 9,
+
+and, in the event that only the object code of the Modified Software is
+redistributed,
+
+ 3. a note stating the conditions of effective access to the full source
+ code of the Modified Software for a period of at least three years
+ from the distribution of the Modified Software, it being understood
+ that the additional acquisition cost of the source code shall not
+ exceed the cost of the data transfer.
+
+
+ 5.3.3 DISTRIBUTION OF EXTERNAL MODULES
+
+When the Licensee has developed an External Module, the terms and
+conditions of this Agreement do not apply to said External Module, that
+may be distributed under a separate license agreement.
+
+
+ 5.3.4 COMPATIBILITY WITH OTHER LICENSES
+
+The Licensee can include a code that is subject to the provisions of one
+of the versions of the GNU GPL, GNU Affero GPL and/or EUPL in the
+Modified or unmodified Software, and distribute that entire code under
+the terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL.
+
+The Licensee can include the Modified or unmodified Software in a code
+that is subject to the provisions of one of the versions of the GNU GPL,
+GNU Affero GPL and/or EUPL and distribute that entire code under the
+terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL.
+
+
+ Article 6 - INTELLECTUAL PROPERTY
+
+
+ 6.1 OVER THE INITIAL SOFTWARE
+
+The Holder owns the economic rights over the Initial Software. Any or
+all use of the Initial Software is subject to compliance with the terms
+and conditions under which the Holder has elected to distribute its work
+and no one shall be entitled to modify the terms and conditions for the
+distribution of said Initial Software.
+
+The Holder undertakes that the Initial Software will remain ruled at
+least by this Agreement, for the duration set forth in Article 4.2 <#term>.
+
+
+ 6.2 OVER THE CONTRIBUTIONS
+
+The Licensee who develops a Contribution is the owner of the
+intellectual property rights over this Contribution as defined by
+applicable law.
+
+
+ 6.3 OVER THE EXTERNAL MODULES
+
+The Licensee who develops an External Module is the owner of the
+intellectual property rights over this External Module as defined by
+applicable law and is free to choose the type of agreement that shall
+govern its distribution.
+
+
+ 6.4 JOINT PROVISIONS
+
+The Licensee expressly undertakes:
+
+ 1. not to remove, or modify, in any manner, the intellectual property
+ notices attached to the Software;
+
+ 2. to reproduce said notices, in an identical manner, in the copies of
+ the Software modified or not.
+
+The Licensee undertakes not to directly or indirectly infringe the
+intellectual property rights on the Software of the Holder and/or
+Contributors, and to take, where applicable, vis-à-vis its staff, any
+and all measures required to ensure respect of said intellectual
+property rights of the Holder and/or Contributors.
+
+
+ Article 7 - RELATED SERVICES
+
+7.1 Under no circumstances shall the Agreement oblige the Licensor to
+provide technical assistance or maintenance services for the Software.
+
+However, the Licensor is entitled to offer this type of services. The
+terms and conditions of such technical assistance, and/or such
+maintenance, shall be set forth in a separate instrument. Only the
+Licensor offering said maintenance and/or technical assistance services
+shall incur liability therefor.
+
+7.2 Similarly, any Licensor is entitled to offer to its licensees, under
+its sole responsibility, a warranty, that shall only be binding upon
+itself, for the redistribution of the Software and/or the Modified
+Software, under terms and conditions that it is free to decide. Said
+warranty, and the financial terms and conditions of its application,
+shall be subject of a separate instrument executed between the Licensor
+and the Licensee.
+
+
+ Article 8 - LIABILITY
+
+8.1 Subject to the provisions of Article 8.2, the Licensee shall be
+entitled to claim compensation for any direct loss it may have suffered
+from the Software as a result of a fault on the part of the relevant
+Licensor, subject to providing evidence thereof.
+
+8.2 The Licensor's liability is limited to the commitments made under
+this Agreement and shall not be incurred as a result of in particular:
+(i) loss due the Licensee's total or partial failure to fulfill its
+obligations, (ii) direct or consequential loss that is suffered by the
+Licensee due to the use or performance of the Software, and (iii) more
+generally, any consequential loss. In particular the Parties expressly
+agree that any or all pecuniary or business loss (i.e. loss of data,
+loss of profits, operating loss, loss of customers or orders,
+opportunity cost, any disturbance to business activities) or any or all
+legal proceedings instituted against the Licensee by a third party,
+shall constitute consequential loss and shall not provide entitlement to
+any or all compensation from the Licensor.
+
+
+ Article 9 - WARRANTY
+
+9.1 The Licensee acknowledges that the scientific and technical
+state-of-the-art when the Software was distributed did not enable all
+possible uses to be tested and verified, nor for the presence of
+possible defects to be detected. In this respect, the Licensee's
+attention has been drawn to the risks associated with loading, using,
+modifying and/or developing and reproducing the Software which are
+reserved for experienced users.
+
+The Licensee shall be responsible for verifying, by any or all means,
+the suitability of the product for its requirements, its good working
+order, and for ensuring that it shall not cause damage to either persons
+or properties.
+
+9.2 The Licensor hereby represents, in good faith, that it is entitled
+to grant all the rights over the Software (including in particular the
+rights set forth in Article 5 <#scope>).
+
+9.3 The Licensee acknowledges that the Software is supplied "as is" by
+the Licensor without any other express or tacit warranty, other than
+that provided for in Article 9.2 <#good-faith> and, in particular,
+without any warranty as to its commercial value, its secured, safe,
+innovative or relevant nature.
+
+Specifically, the Licensor does not warrant that the Software is free
+from any error, that it will operate without interruption, that it will
+be compatible with the Licensee's own equipment and software
+configuration, nor that it will meet the Licensee's requirements.
+
+9.4 The Licensor does not either expressly or tacitly warrant that the
+Software does not infringe any third party intellectual property right
+relating to a patent, software or any other property right. Therefore,
+the Licensor disclaims any and all liability towards the Licensee
+arising out of any or all proceedings for infringement that may be
+instituted in respect of the use, modification and redistribution of the
+Software. Nevertheless, should such proceedings be instituted against
+the Licensee, the Licensor shall provide it with technical and legal
+expertise for its defense. Such technical and legal expertise shall be
+decided on a case-by-case basis between the relevant Licensor and the
+Licensee pursuant to a memorandum of understanding. The Licensor
+disclaims any and all liability as regards the Licensee's use of the
+name of the Software. No warranty is given as regards the existence of
+prior rights over the name of the Software or as regards the existence
+of a trademark.
+
+
+ Article 10 - TERMINATION
+
+10.1 In the event of a breach by the Licensee of its obligations
+hereunder, the Licensor may automatically terminate this Agreement
+thirty (30) days after notice has been sent to the Licensee and has
+remained ineffective.
+
+10.2 A Licensee whose Agreement is terminated shall no longer be
+authorized to use, modify or distribute the Software. However, any
+licenses that it may have granted prior to termination of the Agreement
+shall remain valid subject to their having been granted in compliance
+with the terms and conditions hereof.
+
+
+ Article 11 - MISCELLANEOUS
+
+
+ 11.1 EXCUSABLE EVENTS
+
+Neither Party shall be liable for any or all delay, or failure to
+perform the Agreement, that may be attributable to an event of force
+majeure, an act of God or an outside cause, such as defective
+functioning or interruptions of the electricity or telecommunications
+networks, network paralysis following a virus attack, intervention by
+government authorities, natural disasters, water damage, earthquakes,
+fire, explosions, strikes and labor unrest, war, etc.
+
+11.2 Any failure by either Party, on one or more occasions, to invoke
+one or more of the provisions hereof, shall under no circumstances be
+interpreted as being a waiver by the interested Party of its right to
+invoke said provision(s) subsequently.
+
+11.3 The Agreement cancels and replaces any or all previous agreements,
+whether written or oral, between the Parties and having the same
+purpose, and constitutes the entirety of the agreement between said
+Parties concerning said purpose. No supplement or modification to the
+terms and conditions hereof shall be effective as between the Parties
+unless it is made in writing and signed by their duly authorized
+representatives.
+
+11.4 In the event that one or more of the provisions hereof were to
+conflict with a current or future applicable act or legislative text,
+said act or legislative text shall prevail, and the Parties shall make
+the necessary amendments so as to comply with said act or legislative
+text. All other provisions shall remain effective. Similarly, invalidity
+of a provision of the Agreement, for any reason whatsoever, shall not
+cause the Agreement as a whole to be invalid.
+
+
+ 11.5 LANGUAGE
+
+The Agreement is drafted in both French and English and both versions
+are deemed authentic.
+
+
+ Article 12 - NEW VERSIONS OF THE AGREEMENT
+
+12.1 Any person is authorized to duplicate and distribute copies of this
+Agreement.
+
+12.2 So as to ensure coherence, the wording of this Agreement is
+protected and may only be modified by the authors of the License, who
+reserve the right to periodically publish updates or new versions of the
+Agreement, each with a separate number. These subsequent versions may
+address new issues encountered by Free Software.
+
+12.3 Any Software distributed under a given version of the Agreement may
+only be subsequently distributed under the same version of the Agreement
+or a subsequent version, subject to the provisions of Article 5.3.4
+<#compatibility>.
+
+
+ Article 13 - GOVERNING LAW AND JURISDICTION
+
+13.1 The Agreement is governed by French law. The Parties agree to
+endeavor to seek an amicable solution to any disagreements or disputes
+that may arise during the performance of the Agreement.
+
+13.2 Failing an amicable solution within two (2) months as from their
+occurrence, and unless emergency proceedings are necessary, the
+disagreements or disputes shall be referred to the Paris Courts having
+jurisdiction, by the more diligent Party.
\ No newline at end of file
diff --git a/README.md b/README.md
index 4df1395d..08bc04ed 100644
--- a/README.md
+++ b/README.md
@@ -1,71 +1,23 @@
-# Gipsy
+
-Gipsy is a multifunction bot managed by the [Gunivers](https://gunivers.net) community.
+
-Please use at least **Python 3.9** to run this project.
+[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/Gunivers/Gipsy?color=orange&label=average%20contributions&style=for-the-badge)](#) [![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Gunivers/Gipsy?color=orange&style=for-the-badge)](#) [![GitHub Repo stars](https://img.shields.io/github/stars/Gunivers/Gipsy?color=orange&style=for-the-badge)](#)
+[![Discord](https://img.shields.io/discord/125723125685026816?color=blue&label=Discord&style=for-the-badge&logo=Discord)](https://discord.gg/E8qq6tN)
-Use `pip install -r requirements.txt` in the directory to install dependencies.
+# 👻 Gipsy
-## **Description**
+Gipsy is modular, free and open-source Discord bot whose focus on accessibility and customization. It maintained with ❤️ by volunteers inside the [Gunivers](https://gunivers.net/) community. It is written in Python and uses the Discord.py library and the project is open to experimented people as well as beginners!
-Gipsy is a Discord bot whose first objective is to meet the expectations of the Gunivers community. However, if we want to create new features, we might as well let those who might be interested in them enjoy them !
-You can invite the bot, learn what it can do and follow its evolution.
+
+
+
-## **Invite**
+*This button redirect you to a more complete presentation with invitation links, list of features, self-hosting instructions and more.*
-You can invite the bot by [![link](uploads/32dc3a164398f67799a6cfe7206c12ca/link.png) clicking here.](http://utip.io/s/1yhs7W)
-
-You can also invite the bot in beta version to enjoy the latest features added. Be careful though: the bot in beta version may contain security holes and many bugs. It may also stop working suddenly and for long periods. If you want to invite it though, [click here](https://discordapp.com/oauth2/authorize?client_id=813836349147840513&scope=bot&permissions=8)
-
-## **Features**
-
-
-## **Add a Gunibot service on linux**
-
-You can create a service for your gunibot instance, which will allow you to start and stop the bot using commands like `systemctl start gunibot`, or `service gunibot stop`. The bot will also reboot automatically after a crash.
-
-For this method, you need to have screen installed, which allows you to create detached shell:
-
-`sudo apt install screen` (debian)
-
-First, create a file in /etc/systemd/system, where `gunibot` is the name of your service:
-
-/etc/systemd/system/gunibot.service
-```ini
-[Unit]
-Description=Gunibot
-After=network.target
-
-[Service]
-WorkingDirectory=[/path/to/your/gunibot/folder]
-
-User=[the user which owns the gunibot folder]
-Group=[the user group which owns the gunibot folder]
-
-Restart=always
-
-ExecStart=/usr/bin/screen -dmS gunibot python3.9 start.py --beta
-
-ExecStop=/usr/bin/screen -p 0 -S gunibot -X eval 'stuff "^C"'
-
-[Install]
-WantedBy=multi-user.target
-```
-
-Make sure to replace `WorkingDirectory`, `User` and `Group` with the correct value. You can also set the description as you want.
-
-In the `ExecStart` command, we create a detached screen with -dmS parameters:
-
-`-dmS name Start as daemon: Screen session in detached mode.` (from screen help)
-
-In the `ExecStop` command, we write the input "^C" in the screen session, to stop the bot.
-
-You can replace gunibot in the `ExecStart` and `ExecStop` command with any value, this is going to be the name of the screen.
-
-To access the bot command line, you can simply use `screen -r gunibot` where gunibot is the name of the screen.
-
-You can use these commands to start and stop the bot :
-
-* start the bot: `sudo systemctl start gunibot` or `sudo service gunibot start` where gunibot is the name of the .service file
-* stop the bot: `sudo systemctl stop gunibot` or `sudo service gunibot stop`
-* reload the bot: `sudo systemctl restart gunibot` or `sudo service gunibot restart`
+
\ No newline at end of file
diff --git a/bot/TO_REMOVE.md b/bot/TO_REMOVE.md
new file mode 100644
index 00000000..a420af7b
--- /dev/null
+++ b/bot/TO_REMOVE.md
@@ -0,0 +1 @@
+# This folder is deprecated and have to be removed in future versions. Most of files here will be moved (and adapted) in `core/` folder.
\ No newline at end of file
diff --git a/bot/__init__.py b/bot/__init__.py
new file mode 100644
index 00000000..778e3fa6
--- /dev/null
+++ b/bot/__init__.py
@@ -0,0 +1,6 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
\ No newline at end of file
diff --git a/bot/args.py b/bot/args.py
index 5790ba5f..0d44099d 100644
--- a/bot/args.py
+++ b/bot/args.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import discord
import re
from discord.ext import commands
@@ -8,26 +15,32 @@ class tempdelta(commands.Converter):
async def convert(self, ctx: MyContext, argument: str) -> int:
d = 0
found = False
- for x in [('y', 86400*365), ('w', 604800), ('d', 86400), ('h', 3600), ('m', 60), ('min', 60)]:
- r = re.search(r'^(\d+)'+x[0]+'$', argument)
+ for x in [
+ ("y", 86400 * 365),
+ ("w", 604800),
+ ("d", 86400),
+ ("h", 3600),
+ ("m", 60),
+ ("min", 60),
+ ]:
+ r = re.search(r"^(\d+)" + x[0] + "$", argument)
if r is not None:
- d += int(r.group(1))*x[1]
+ d += int(r.group(1)) * x[1]
found = True
- r = re.search(r'^(\d+)h(\d+)m?$', argument)
+ r = re.search(r"^(\d+)h(\d+)m?$", argument)
if r is not None:
- d += int(r.group(1))*3600 + int(r.group(2))*60
+ d += int(r.group(1)) * 3600 + int(r.group(2)) * 60
found = True
if not found:
- raise commands.errors.BadArgument('Invalid duration: '+argument)
+ raise commands.errors.BadArgument("Invalid duration: " + argument)
return d
class moderatorFlag(commands.Converter):
async def convert(self, ctx: MyContext, argument: str) -> str:
- LogsFlags = ctx.bot.get_cog('ConfigCog').LogsFlags.FLAGS
+ LogsFlags = ctx.bot.get_cog("ConfigCog").LogsFlags.FLAGS
if argument not in LogsFlags.values():
- raise commands.errors.BadArgument(
- 'Invalid moderation flag: '+argument)
+ raise commands.errors.BadArgument("Invalid moderation flag: " + argument)
return argument
@@ -37,12 +50,14 @@ class Constant(commands.Converter):
async def convert(self, ctx: MyContext, arg: str):
if arg != self.w:
- raise commands.errors.BadArgument('Unknown argument')
+ raise commands.errors.BadArgument("Unknown argument")
+
return Constant
+
class arguments(commands.Converter):
async def convert(self, ctx: MyContext, argument: str) -> dict:
answer = dict()
- for result in re.finditer(r'(\w+) ?= ?\"((?:[^\"\\]|\\\"|\\)+)\"', argument):
+ for result in re.finditer(r"(\w+) ?= ?\"((?:[^\"\\]|\\\"|\\)+)\"", argument):
answer[result.group(1)] = result.group(2).replace('\\"', '"')
- return answer
\ No newline at end of file
+ return answer
diff --git a/bot/checks.py b/bot/checks.py
index 22c8f533..b1db9606 100644
--- a/bot/checks.py
+++ b/bot/checks.py
@@ -1,28 +1,52 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import discord
from utils import MyContext, CheckException
+from core import config
def is_bot_admin(ctx: MyContext):
- return ctx.author.id in ctx.bot.config['bot_admins']
+ return ctx.author.id in config.get("bot.admins")
+
async def is_admin(ctx: MyContext):
- admin = ctx.guild is None or ctx.author.guild_permissions.administrator or is_bot_admin(ctx)
+ admin = (
+ ctx.guild is None
+ or ctx.author.guild_permissions.administrator
+ or is_bot_admin(ctx)
+ )
if not admin:
- raise CheckException('is_admin')
+ raise CheckException("is_admin")
return True
+
async def is_server_manager(ctx: MyContext):
- g_manager = ctx.guild is None or ctx.author.guild_permissions.manage_guild or is_bot_admin(ctx)
+ g_manager = (
+ ctx.guild is None
+ or ctx.author.guild_permissions.manage_guild
+ or is_bot_admin(ctx)
+ )
if not g_manager:
- raise CheckException('is_server_manager')
+ raise CheckException("is_server_manager")
return True
+
async def is_roles_manager(ctx: MyContext):
- r_manager = ctx.guild is None or ctx.author.guild_permissions.manage_roles or is_bot_admin(ctx)
+ r_manager = (
+ ctx.guild is None
+ or ctx.author.guild_permissions.manage_roles
+ or is_bot_admin(ctx)
+ )
if not r_manager:
- raise CheckException('is_roles_manager')
+ raise CheckException("is_roles_manager")
return True
+
async def can_group(ctx: MyContext):
config = ctx.bot.server_configs[ctx.guild.id]
if config["group_allowed_role"] is None:
diff --git a/bot/config.py b/bot/config.py
deleted file mode 100644
index 3d2af9f1..00000000
--- a/bot/config.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import json, os
-from shutil import copyfile
-
-def get_config(path: str, isBotConfig: bool):
- if not os.path.isfile(path + ".json"):
- copyfile(path + '-example.json', path + '.json')
- if isBotConfig:
- print("TOKEN MISSING: Please, enter your bot token in the config/config.json and restart the bot.")
- return None
- with open(path + ".json") as f:
- conf = json.load(f)
- if isBotConfig:
- if conf["token"] == "Discord token for main bot":
- print("TOKEN MISSING: Please, enter your bot token in the config/config.json and restart the bot.")
- return None
- return conf
\ No newline at end of file
diff --git a/bot/docs.py b/bot/docs.py
deleted file mode 100644
index 3132f2a6..00000000
--- a/bot/docs.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import os
-from shutil import copyfile
-
-def generate_docs():
- docs = open("docs/summary.rst","w+")
- docs.write("""
-.. toctree::
- :maxdepth: 3
- :caption: Info
-
- contributing.md
- faq.md
-
-.. toctree::
- :maxdepth: 2
- :caption: Installed plugins
-
-""")
-
- if not os.path.isdir("./docs/plugins"):
- os.makedirs("./docs/plugins")
- for file in os.listdir("./docs/plugins"):
- os.remove("./docs/plugins/" + file)
- for plugin in sorted(os.listdir('./plugins/')):
- if not plugin.startswith('_'):
- if os.path.isfile('./plugins/' + plugin + "/docs/user_documentation.rst"):
- copyfile('./plugins/' + plugin + "/docs/user_documentation.rst", './docs/plugins/' + plugin + ".rst")
- docs.write(" plugins/" + plugin + ".rst\n")
- else:
- if os.path.isfile('./plugins/' + plugin + "/docs/user_documentation.md"):
- copyfile('./plugins/' + plugin + "/docs/user_documentation.md", './docs/plugins/' + plugin + ".md")
- docs.write(" plugins/" + plugin + ".md\n")
-
-
- if os.listdir('./docs/create_plugin') != []:
- docs.write("""
-.. toctree::
- :maxdepth: 2
- :caption: For developers
-
-""")
-
- for file in os.listdir('./docs/create_plugin'):
- if file[-3:] == ".md" or file[-4:] == ".rst":
- docs.write(" create_plugin/" + file + "\n")
-
-
- docs.close()
\ No newline at end of file
diff --git a/bot/utils/configManager.py b/bot/utils/configManager.py
index 4ca16e27..962738ac 100644
--- a/bot/utils/configManager.py
+++ b/bot/utils/configManager.py
@@ -1,12 +1,20 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import os
from json import dump, load
+from utils import Gunibot
from discord.ext import commands
from utils import CONFIG_OPTIONS
CONFIG_FOLDER = "configs"
-CONFIG_TEMPLATE = {k: v['default'] for k, v in CONFIG_OPTIONS.items() if 'default' in v}
+CONFIG_TEMPLATE = {k: v["default"] for k, v in CONFIG_OPTIONS.items() if "default" in v}
class serverConfig(dict):
@@ -36,14 +44,12 @@ def __delitem__(self, key):
class ConfigCog(commands.Cog):
-
def __init__(self, bot):
self.bot = bot
self.file = "configManager"
self.confManager = self.configManager()
class configManager(dict):
-
def __init__(self):
super().__init__()
self.cache = dict()
@@ -76,7 +82,9 @@ def __repr__(self):
return ""
def __len__(self):
- return len([name for name in os.listdir(CONFIG_FOLDER) if os.path.isfile(name)])
+ return len(
+ [name for name in os.listdir(CONFIG_FOLDER) if os.path.isfile(name)]
+ )
def __delitem__(self, key):
pass
@@ -106,7 +114,7 @@ def keys(self):
# return self.__dict__.pop(*args)
def __contains__(self, item):
- return self.has_key(item)
+ return item in self
class LogsFlags:
FLAGS = {
@@ -118,7 +126,7 @@ class LogsFlags:
1 << 5: "boosts",
1 << 6: "roles",
1 << 7: "members",
- 1 << 8: "emojis"
+ 1 << 8: "emojis",
}
def flagsToInt(self, flags: list) -> int:
@@ -132,5 +140,5 @@ def intToFlags(self, i: int) -> list:
return [v for k, v in self.FLAGS.items() if i & k == k]
-async def setup(bot):
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
await bot.add_cog(ConfigCog(bot))
diff --git a/bot/utils/errors.py b/bot/utils/errors.py
index f18f78a4..ffcdb834 100644
--- a/bot/utils/errors.py
+++ b/bot/utils/errors.py
@@ -1,12 +1,21 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import CheckException, Gunibot, MyContext
+from discord.ext import commands
+import discord
+from bot import checks
import re
import traceback
+from core import config
import sys
+
sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.ext import commands
-from utils import CheckException, Gunibot, MyContext
class Errors(commands.Cog):
@@ -19,17 +28,23 @@ def __init__(self, bot: Gunibot):
@commands.Cog.listener()
async def on_command_error(self, ctx: MyContext, error: Exception):
"""The event triggered when an error is raised while invoking a command."""
- # This prevents any commands with local handlers being handled here in on_command_error.
- if hasattr(ctx.command, 'on_error'):
+ # This prevents any commands with local handlers being handled here in
+ # on_command_error.
+ if hasattr(ctx.command, "on_error"):
return
- ignored = (commands.errors.CommandNotFound, commands.errors.CheckFailure,
- commands.errors.ConversionError, discord.errors.Forbidden)
- actually_not_ignored = (commands.errors.NoPrivateMessage)
+ ignored = (
+ commands.errors.CommandNotFound,
+ commands.errors.CheckFailure,
+ commands.errors.ConversionError,
+ discord.errors.Forbidden,
+ )
+ actually_not_ignored = commands.errors.NoPrivateMessage
# Allows us to check for original exceptions raised and sent to CommandInvokeError.
- # If nothing is found. We keep the exception passed to on_command_error.
- error = getattr(error, 'original', error)
+ # If nothing is found. We keep the exception passed to
+ # on_command_error.
+ error = getattr(error, "original", error)
# Anything in ignored will return and prevent anything happening.
if isinstance(error, ignored) and not isinstance(error, actually_not_ignored):
@@ -38,103 +53,179 @@ async def on_command_error(self, ctx: MyContext, error: Exception):
if checks.is_bot_admin(ctx):
await ctx.reinvoke()
return
- await ctx.send(await self.bot._(ctx.channel, "errors.cooldown", c=round(error.retry_after, 2)))
+ await ctx.send(
+ await self.bot._(
+ ctx.channel, "errors.cooldown", c=round(error.retry_after, 2)
+ )
+ )
return
elif isinstance(error, CheckException):
- return await ctx.send(await self.bot._(ctx.channel, "errors.custom_checks."+error.id))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.custom_checks." + error.id)
+ )
elif isinstance(error, (commands.BadArgument, commands.BadUnionArgument)):
raw_error = str(error)
if raw_error == "Unknown argument":
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-arg"))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-arg")
+ )
elif raw_error == "Unknown dependency action type":
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-dependency"))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-dependency")
+ )
elif raw_error == "Unknown dependency trigger type":
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-trigger"))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-trigger")
+ )
elif raw_error == "Unknown permission type":
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-permission"))
- # Could not convert "limit" into int. OR Converting to "int" failed for parameter "number".
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-permission")
+ )
+ # Could not convert "limit" into int. OR Converting to "int" failed
+ # for parameter "number".
r = re.search(
- r'Could not convert \"(?P[^\"]+)\" into (?P[^.\n]+)', raw_error)
+ r"Could not convert \"(?P[^\"]+)\" into (?P[^.\n]+)",
+ raw_error,
+ )
if r is None:
r = re.search(
- r'Converting to \"(?P[^\"]+)\" failed for parameter \"(?P[^.\n]+)\"', raw_error)
+ r"Converting to \"(?P[^\"]+)\" failed for parameter \"(?P[^.\n]+)\"",
+ raw_error,
+ )
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-arg", p=r.group('arg'), t=r.group('type')))
+ return await ctx.send(
+ await self.bot._(
+ ctx.channel,
+ "errors.unknown-arg",
+ p=r.group("arg"),
+ t=r.group("type"),
+ )
+ )
# zzz is not a recognised boolean option
r = re.search(
- r'(?P[^\"]+) is not a recognised (?P[^.\n]+) option', raw_error)
+ r"(?P[^\"]+) is not a recognised (?P[^.\n]+) option",
+ raw_error,
+ )
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-type", p=r.group('arg'), t=r.group('type')))
+ return await ctx.send(
+ await self.bot._(
+ ctx.channel,
+ "errors.invalid-type",
+ p=r.group("arg"),
+ t=r.group("type"),
+ )
+ )
# Member "Z_runner" not found
- r = re.search(r'Member \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"Member \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-member", m=r.group(1)))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-member", m=r.group(1))
+ )
# User "Z_runner" not found
- r = re.search(r'User \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"User \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-user", u=r.group(1)))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-user", u=r.group(1))
+ )
# Role "Admin" not found
- r = re.search(r'Role \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"Role \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-role", r=r.group(1)))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-role", r=r.group(1))
+ )
# Emoji ":shock:" not found
- r = re.search(r'Emoji \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"Emoji \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-emoji", e=r.group(1)))
- # Colour "blue" is invalid
- r = re.search(r'Colour \"([^\"]+)\" is invalid', raw_error)
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-emoji", e=r.group(1))
+ )
+ # Colour "blue" is invalid
+ r = re.search(r"Colour \"([^\"]+)\" is invalid", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-color", c=r.group(1)))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-color", c=r.group(1))
+ )
# Channel "twitter" not found.
- r = re.search(r'Channel \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"Channel \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-channel", c=r.group(1)))
+ return await ctx.send(
+ await self.bot._(
+ ctx.channel, "errors.unknown-channel", c=r.group(1)
+ )
+ )
# Message "1243" not found.
- r = re.search(r'Message \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"Message \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-message", m=r.group(1)))
+ return await ctx.send(
+ await self.bot._(
+ ctx.channel, "errors.unknown-message", m=r.group(1)
+ )
+ )
# Group "twitter" not found.
- r = re.search(r'Group \"([^\"]+)\" not found', raw_error)
+ r = re.search(r"Group \"([^\"]+)\" not found", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-group", g=r.group(1)))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-group", g=r.group(1))
+ )
# Too many text channels
- if raw_error == 'Too many text channels':
- return await ctx.send(await self.bot._(ctx.channel, "errors.too-many-text-channels"))
+ if raw_error == "Too many text channels":
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.too-many-text-channels")
+ )
# Invalid duration: 2d
- r = re.search(r'Invalid duration: ([^\" ]+)', raw_error)
+ r = re.search(r"Invalid duration: ([^\" ]+)", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-duration", d=r.group(1)))
+ return await ctx.send(
+ await self.bot._(
+ ctx.channel, "errors.invalid-duration", d=r.group(1)
+ )
+ )
# Invalid invite: nope
- r = re.search(r'Invalid invite: (\S+)', raw_error)
+ r = re.search(r"Invalid invite: (\S+)", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-invite"))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-invite")
+ )
# Invalid guild: test
- r = re.search(r'Invalid guild: (\S+)', raw_error)
+ r = re.search(r"Invalid guild: (\S+)", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.unknown-server"))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.unknown-server")
+ )
# Invalid url: nou
- r = re.search(r'Invalid url: (\S+)', raw_error)
+ r = re.search(r"Invalid url: (\S+)", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-url"))
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-url")
+ )
# Invalid emoji: lmao
- r = re.search(r'Invalid emoji: (\S+)', raw_error)
+ r = re.search(r"Invalid emoji: (\S+)", raw_error)
if r is not None:
- return await ctx.send(await self.bot._(ctx.channel, "errors.invalid-emoji"))
- print('errors -', error)
+ return await ctx.send(
+ await self.bot._(ctx.channel, "errors.invalid-emoji")
+ )
+ print("errors -", error)
elif isinstance(error, commands.MissingRequiredArgument):
- await ctx.send(await self.bot._(ctx.channel, "errors.missing-arg", a=error.param.name))
+ await ctx.send(
+ await self.bot._(ctx.channel, "errors.missing-arg", a=error.param.name)
+ )
return
elif isinstance(error, commands.DisabledCommand):
- await ctx.send(await self.bot._(ctx.channel, "errors.disabled-cmd", c=ctx.invoked_with))
+ await ctx.send(
+ await self.bot._(ctx.channel, "errors.disabled-cmd", c=ctx.invoked_with)
+ )
return
elif isinstance(error, commands.errors.NoPrivateMessage):
await ctx.send(await self.bot._(ctx.channel, "errors.disabled-dm"))
return
else:
await ctx.send(await self.bot._(ctx.channel, "errors.error-unknown"))
- # All other Errors not returned come here... And we can just print the default TraceBack.
- self.bot.log.warning('Ignoring exception in command {}:'.format(
- ctx.message.content), exc_info=(type(error), error, error.__traceback__))
+ # All other Errors not returned come here... And we can just print the
+ # default TraceBack.
+ self.bot.log.warning(
+ "Ignoring exception in command {}:".format(ctx.message.content),
+ exc_info=(type(error), error, error.__traceback__),
+ )
await self.on_error(error, ctx)
@commands.Cog.listener()
@@ -142,8 +233,7 @@ async def on_error(self, error, ctx=None):
try:
if isinstance(ctx, discord.Message):
ctx = await self.bot.get_context(ctx)
- tr = traceback.format_exception(
- type(error), error, error.__traceback__)
+ tr = traceback.format_exception(type(error), error, error.__traceback__)
msg = "```python\n{}\n```".format(" ".join(tr)[:1900])
if ctx is None:
await self.senf_err_msg(f"Internal Error\n{msg}")
@@ -152,18 +242,20 @@ async def on_error(self, error, ctx=None):
elif ctx.channel.id == 698547216155017236:
return await ctx.send(msg)
else:
- await self.senf_err_msg(ctx.guild.name+" | "+ctx.channel.name+"\n"+msg)
+ await self.senf_err_msg(
+ ctx.guild.name + " | " + ctx.channel.name + "\n" + msg
+ )
except Exception as e:
self.bot.log.warn(f"[on_error] {e}", exc_info=True)
async def senf_err_msg(self, msg):
"""Sends a message to the error channel"""
- salon = self.bot.get_channel(self.bot.config["errors_channel"])
- if salon is None:
+ channel = self.bot.get_channel(config.get("bot.error_channels"))
+ if channel is None:
return False
- await salon.send(msg)
+ await channel.send(msg)
return True
-async def setup(bot):
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
await bot.add_cog(Errors(bot))
diff --git a/bot/utils/gunivers.py b/bot/utils/gunivers.py
index 76540afb..f22378ef 100644
--- a/bot/utils/gunivers.py
+++ b/bot/utils/gunivers.py
@@ -1,10 +1,16 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import discord
from discord.ext import tasks, commands
from utils import Gunibot, MyContext
class Gunivers(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = "gunivers"
@@ -15,9 +21,10 @@ def cog_unload(self):
@tasks.loop(minutes=60.0 * 24.0)
async def update_loop(self):
- channel = self.bot.get_channel(757879277776535664) # Round Table
+ channel = self.bot.get_channel(757879277776535664) # Round Table
if channel is not None:
await channel.send("Bon, qu'est-ce qu'on peut poster aujourd'hui ?")
-async def setup(bot):
+
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
await bot.add_cog(Gunivers(bot))
diff --git a/bot/utils/languages.py b/bot/utils/languages.py
index 45c84a64..e74ff258 100644
--- a/bot/utils/languages.py
+++ b/bot/utils/languages.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import discord
import i18n
from discord.ext import commands
@@ -5,24 +12,22 @@
import os
i18n.translations.container.clear() # invalidate old cache
-i18n.set('filename_format', '{locale}.{format}')
-i18n.set('fallback', 'fr')
-i18n.load_path.append('./langs')
+i18n.set("filename_format", "{locale}.{format}")
+i18n.set("fallback", "fr")
+i18n.load_path.append("./langs")
# Check all plugin lang directory
-for plugin in os.listdir('./plugins/'):
- if os.path.isdir('./plugins/' + plugin + '/langs/') and plugin[0] != '_':
- i18n.load_path.append('./plugins/' + plugin + '/langs/')
-
+for plugin in os.listdir("./plugins/"):
+ if os.path.isdir("./plugins/" + plugin + "/langs/") and plugin[0] != "_":
+ i18n.load_path.append("./plugins/" + plugin + "/langs/")
class Languages(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = "languages"
- self.languages = ['fr', 'en']
- self.config_options = ['language']
+ self.languages = ["fr", "en"]
+ self.config_options = ["language"]
async def tr(self, ctx, key: str, **kwargs):
"""Translate something
@@ -30,29 +35,35 @@ async def tr(self, ctx, key: str, **kwargs):
lang = self.languages[0]
if isinstance(ctx, commands.Context):
if ctx.guild:
- lang = self.languages[await self.get_lang(ctx.guild.id)]
+ lang = await self.get_lang(ctx.guild.id, use_str=True)
elif isinstance(ctx, discord.Guild):
- lang = self.languages[await self.get_lang(ctx.id)]
+ lang = await self.get_lang(ctx.id, use_str=True)
elif isinstance(ctx, discord.abc.GuildChannel):
- lang = self.languages[await self.get_lang(ctx.guild.id)]
+ lang = await self.get_lang(ctx.guild.id, use_str=True)
elif isinstance(ctx, str) and ctx in self.languages:
lang = ctx
elif isinstance(ctx, int): # guild ID
if self.bot.get_guild(ctx): # if valid guild
- lang = self.languages[await self.get_lang(ctx)]
+ lang = await self.get_lang(ctx, use_str=True)
else:
lang = self.languages[0]
return i18n.t(key, locale=lang, **kwargs)
- async def get_lang(self, guildID: int, use_str: bool=False) -> int:
+ async def get_lang(self, guildID: int, use_str: bool = False) -> int:
if guildID is None:
as_int = 0
else:
- as_int = self.bot.server_configs[guildID]['language']
+ # migration for old format
+ if isinstance(self.bot.server_configs[guildID]["language"], int):
+ as_int = self.bot.server_configs[guildID]["language"]
+ else:
+ as_int = self.languages.index(
+ self.bot.server_configs[guildID]["language"]
+ )
if use_str:
return self.languages[as_int]
return as_int
-async def setup(bot):
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
await bot.add_cog(Languages(bot))
diff --git a/bot/utils/perms.py b/bot/utils/perms.py
deleted file mode 100644
index 26835a50..00000000
--- a/bot/utils/perms.py
+++ /dev/null
@@ -1,97 +0,0 @@
-import typing
-
-import discord
-from discord.ext import commands
-from utils import Gunibot, MyContext
-
-
-class Perms(commands.Cog):
- """Cog with a single command, allowing you to see the permissions of a member or a role in a channel."""
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.file = "perms"
- chan_perms = [key for key, value in discord.Permissions().all_channel() if value]
- self.perms_name = {'general': [key for key, value in discord.Permissions().general() if value],
- 'text': [key for key, value in discord.Permissions().text() if value],
- 'voice': [key for key, value in discord.Permissions().voice() if value]}
- self.perms_name['common_channel'] = [
- x for x in chan_perms if x in self.perms_name['general']]
-
- @commands.command(name='perms', aliases=['permissions'])
- @commands.guild_only()
- async def check_permissions(self, ctx: MyContext, channel: typing.Optional[typing.Union[discord.TextChannel, discord.VoiceChannel, discord.CategoryChannel]] = None, *, target: typing.Union[discord.Member, discord.Role] = None):
- """Permissions assigned to a member/role (the user by default)
- The channel used to view permissions is the channel in which the command is entered."""
- if target == None:
- target = ctx.author
- perms = None
- if isinstance(target, discord.Member):
- if channel == None:
- perms = target.guild_permissions
- else:
- perms = channel.permissions_for(target)
- col = target.color
- avatar = await self.bot.user_avatar_as(target, size=256)
- name = str(target)
- elif isinstance(target, discord.Role):
- perms = target.permissions
- if channel != None:
- perms.update(
- **{x[0]: x[1] for x in channel.overwrites_for(ctx.guild.default_role) if x[1] != None})
- perms.update(**{x[0]: x[1] for x in channel.overwrites_for(target) if x[1] != None})
- col = target.color
- avatar = ctx.guild.icon_url_as(format='png', size=256)
- name = str(target)
- permsl = list()
-
- if perms is None:
- return
-
- async def perms_tr(x) -> str:
- """Get the translation of a permission"""
- return await self.bot._(ctx.guild.id, "perms.list."+x)
-
- # Get the perms translations
- if perms.administrator:
- # If the user is admin, we just say it
- permsl.append(":white_check_mark: " + await perms_tr('administrator'))
- else:
- # Here we check if the value of each permission is True.
- for perm, value in perms:
- if (perm not in self.perms_name['text']+self.perms_name['common_channel'] and isinstance(channel, discord.TextChannel)) or (perm not in self.perms_name['voice']+self.perms_name['common_channel'] and isinstance(channel, discord.VoiceChannel)):
- continue
- perm = await perms_tr(perm)
- if 'perms.list.' in perm:
- # missing translation
- perm = perm.replace('_', ' ').title()
- self.bot.log.warn(f"[perms] missing permission translation: {perm}")
- if value:
- permsl.append(":white_check_mark: " + perm)
- else:
- permsl.append(":x: " + perm)
- if ctx.can_send_embed:
- # \uFEFF is a Zero-Width Space, which basically allows us to have an empty field name.
- # And to make it look nice, we wrap it in an Embed.
- desc = "Permissions générales" if channel is None else channel.mention
- embed = discord.Embed(color=col, description=desc)
- embed.set_author(name=name, icon_url=avatar)
- if len(permsl) > 10:
- sep = int(len(permsl)/2)
- if len(permsl) % 2 == 1:
- sep += 1
- embed.add_field(name='\uFEFF', value="\n".join(permsl[:sep]))
- embed.add_field(name='\uFEFF', value="\n".join(permsl[sep:]))
- else:
- embed.add_field(name='\uFEFF', value="\n".join(permsl))
- await ctx.send(embed=embed)
- # Thanks to Gio for the Command.
- else:
- try:
- await ctx.send("**Permission de '{}' :**\n\n".format(name.replace('@', '')) + "\n".join(permsl))
- except:
- pass
-
-
-async def setup(bot):
- await bot.add_cog(Perms(bot))
diff --git a/bot/utils/sconfig.py b/bot/utils/sconfig.py
index b5986748..41d2e084 100644
--- a/bot/utils/sconfig.py
+++ b/bot/utils/sconfig.py
@@ -1,59 +1,77 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import CONFIG_OPTIONS, Gunibot, MyContext
+from discord.ext import commands
+import emoji
+import discord
+from bot import checks
+from bot import args
import asyncio
import io
import json
import re
from typing import Any, List, Union
+import numpy as np
import sys
+
sys.path.append("./bot")
-import args
-import sys
sys.path.append("./bot")
-from bot import checks
-import discord
-import emoji
-from discord.ext import commands
-from utils import CONFIG_OPTIONS, Gunibot, MyContext
+SERVER_CONFIG = None
-class Sconfig(commands.Cog):
+class Sconfig(commands.Cog):
def __init__(self, bot: Gunibot):
+ global SERVER_CONFIG
+ SERVER_CONFIG = self
self.bot = bot
self.file = "sconfig"
- self.sorted_options = dict() # config options sorted by cog
- self.config_options = ['prefix']
+ self.sorted_options = dict() # config options sorted by cog
+ self.config_options = ["prefix"]
for cog in bot.cogs.values():
- if not hasattr(cog, 'config_options'):
- # if the cog doesn't have any specific config
+ if not hasattr(cog, "config_options"):
+ # if the cog doesn't have any specific config
continue
- self.sorted_options[cog.__cog_name__] = {k: v for k, v in CONFIG_OPTIONS.items() if k in cog.config_options}
- # for whatever reason, the for loop above doesn't include its own cog, so we just force it
- self.sorted_options[self.__cog_name__] = {k: v for k, v in CONFIG_OPTIONS.items() if k in self.config_options}
-
+ self.sorted_options[cog.__cog_name__] = {
+ k: v for k, v in CONFIG_OPTIONS.items() if k in cog.config_options
+ }
+ # for whatever reason, the for loop above doesn't include its own cog,
+ # so we just force it
+ self.sorted_options[self.__cog_name__] = {
+ k: v for k, v in CONFIG_OPTIONS.items() if k in self.config_options
+ }
+
def on_anycog_load(self, cog: commands.Cog):
"""Used to enable config commands when a cog is enabled
-
+
Parameters
-----------
cog: :class:`commands.Cog`
The cog which got enabled"""
- if not hasattr(cog, 'config_options'):
+ if not hasattr(cog, "config_options"):
# if the cog doesn't have any specific config
return
- self.sorted_options[cog.__cog_name__] = {k: v for k, v in CONFIG_OPTIONS.items() if k in cog.config_options}
+ self.sorted_options[cog.__cog_name__] = {
+ k: v for k, v in CONFIG_OPTIONS.items() if k in cog.config_options
+ }
for opt in self.sorted_options[cog.__cog_name__].values():
# we enable the commands if needed
- if 'command' in opt:
+ if "command" in opt:
try:
- self.bot.get_command("config "+opt['command']).enabled = True
+ self.bot.get_command("config " + opt["command"]).enabled = True
except AttributeError:
# if the command doesn't exist
pass
-
+
def on_anycog_unload(self, cog: str):
"""Used to disable config commands when a cog is disabled
-
+
Parameters
-----------
cog: :class:`str`
@@ -61,17 +79,17 @@ def on_anycog_unload(self, cog: str):
if cog in self.sorted_options:
for opt in self.sorted_options[cog].values():
# we disable the commands if needed
- if 'command' in opt:
+ if "command" in opt:
try:
- self.bot.get_command("config "+opt['command']).enabled = False
- except AttributeError:
+ self.bot.get_command("config " + opt["command"]).enabled = False
+ except (AttributeError, TypeError): # if opt["command"] is None
# if the command doesn't exist
pass
del self.sorted_options[cog]
async def edit_config(self, guildID: int, key: str, value: Any):
"""Edit or reset a config option for a guild
-
+
Parameters
-----------
guildID: :class:`int`
@@ -79,7 +97,7 @@ async def edit_config(self, guildID: int, key: str, value: Any):
key: :class:`str`
The name of the option to edit
-
+
value: :class:`Any`
The new value of the config, or None to reset"""
if value is None:
@@ -92,36 +110,44 @@ async def edit_config(self, guildID: int, key: str, value: Any):
else:
return await self.bot._(guildID, "sconfig.option-edited", opt=key)
- async def format_config(self, guild: discord.Guild, key: str, value: str, mention: bool = True) -> str:
+ async def format_config(
+ self, guild: discord.Guild, key: str, value: str, mention: bool = True
+ ) -> str:
if value is None:
return None
config = CONFIG_OPTIONS[key]
- def getname(x): return (x.mention if mention else x.name)
+ def getname(x):
+ return x.mention if mention else x.name
- sep = ' ' if mention else ' | '
+ sep = " " if mention else " | "
if key == "levelup_channel":
- if value in (None, 'none', 'any'):
+ if value in (None, "none", "any"):
return str(value).capitalize()
- if config['type'] == 'roles':
+ if config["type"] == "roles":
value = [value] if isinstance(value, int) else value
roles = [guild.get_role(x) for x in value]
roles = [getname(x) for x in roles if x is not None]
return sep.join(roles)
- if config['type'] == 'channels':
+ if config["type"] == "channels":
value = [value] if isinstance(value, int) else value
channels = [guild.get_channel(x) for x in value]
channels = [getname(x) for x in channels if x is not None]
return sep.join(channels)
- if config['type'] == 'categories':
+ if config["type"] == "categories":
value = [value] if isinstance(value, int) else value
categories = [guild.get_channel(x) for x in value]
categories = [x.name for x in categories if x is not None]
return " | ".join(categories)
- if config['type'] == 'duration':
- return await self.bot.get_cog("TimeCog").time_delta(value, lang='fr', year=True, precision=0)
- if config['type'] == 'emojis':
- def emojis_convert(s_emoji:str, bot_emojis:List[discord.Emoji]) -> Union[str, discord.Emoji]:
+ if config["type"] == "duration":
+ return await self.bot.get_cog("TimeCog").time_delta(
+ value, lang="fr", year=True, precision=0
+ )
+ if config["type"] == "emojis":
+
+ def emojis_convert(
+ s_emoji: str, bot_emojis: List[discord.Emoji]
+ ) -> Union[str, discord.Emoji]:
if s_emoji.isnumeric():
d_em = discord.utils.get(bot_emojis, id=int(s_emoji))
if d_em is None:
@@ -129,17 +155,18 @@ def emojis_convert(s_emoji:str, bot_emojis:List[discord.Emoji]) -> Union[str, di
else:
return f":{d_em.name}:"
return emoji.emojize(s_emoji, language="alias")
+
value = [value] if isinstance(value, str) else value
return " ".join([emojis_convert(x, self.bot.emojis) for x in value])
- if config['type'] == 'modlogsFlags':
+ if config["type"] == "modlogsFlags":
flags = self.bot.get_cog("ConfigCog").LogsFlags().intToFlags(value)
return " - ".join(flags) if len(flags) > 0 else None
- if config['type'] == 'language':
+ if config["type"] == "language":
cog = self.bot.get_cog("Languages")
if cog:
return cog.languages[value]
return value
- if config['type'] == 'int':
+ if config["type"] == "int":
return value
return value
@@ -149,76 +176,125 @@ def emojis_convert(s_emoji:str, bot_emojis:List[discord.Emoji]) -> Union[str, di
async def main_config(self, ctx: MyContext):
"""Edit your server configuration"""
if ctx.subcommand_passed is None:
- res = list()
# get the server config
config = ctx.bot.server_configs[ctx.guild.id]
- # get the length of the longest key
- max_length = 0
+
+ # get the length of the longest key to align the values in columns
+ max_key_length = 0
+ max_value_length = 0
for options in self.sorted_options.values():
configs_len = [len(k) for k in config.keys() if k in options]
- max_length = max(max_length, *configs_len) if len(configs_len) > 0 else max_length
- max_length += 2
- # iterate over groups
+ max_key_length = (
+ max(max_key_length, *configs_len)
+ if len(configs_len) > 0
+ else max_key_length
+ )
+ values_len = [
+ len(str(await self.format_config(ctx.guild, k, v, mention=False)))
+ for k, v in config.items()
+ if k in options
+ ]
+ max_value_length = (
+ max(max_value_length, *values_len)
+ if len(values_len) > 0
+ else max_value_length
+ )
+ max_key_length += 3
+ max_value_length += 1
+
+ # iterate over modules
+ cpt = 0
+ embeds = []
for module, options in sorted(self.sorted_options.items()):
- subconf = {k:v for k,v in config.items() if k in options}
+
+ subconf = {k: v for k, v in config.items() if k in options}
if len(subconf) == 0:
continue
- temp = " # {}\n".format(await self.bot._(ctx.guild.id, "sconfig.cog-name."+module))
+
+ module_config = ""
+
# iterate over configs for that group
for k, v in subconf.items():
value = await self.format_config(ctx.guild, k, v, False)
- temp += (f"[{k}]").ljust(max_length+1) + f" {value}\n"
- if hasattr(self.bot.get_cog(module), '_create_config'):
- for extra in await self.bot.get_cog(module)._create_config(ctx):
- temp += (f"[{extra[0]}]").ljust(max_length+1) + f" {extra[1]}\n"
- res.append(temp)
+ module_config += (
+ (f"{k}:").ljust(max_key_length)
+ + f" {value}".ljust(max_value_length)
+ + "\n"
+ )
- for category in res:
- await ctx.send("```ini\n" + "\n" + category + "```")
+ if hasattr(self.bot.get_cog(module), "_create_config"):
+ for extra in await self.bot.get_cog(module)._create_config(ctx):
+ module_config += (
+ (f"[{extra[0]}]").ljust(max_key_length)
+ + f" {extra[1]}".ljust(max_value_length)
+ + "\n"
+ )
+
+ # Put the config in embeds and stack them to be send in group
+ embeds.append(
+ discord.Embed(
+ title=module,
+ description=f"```yml\n{module_config}```",
+ colour=0x2F3136,
+ )
+ )
+
+ cpt += 1
+
+ # Send the config by group of 10 (limit of embed number per message)
+ if cpt % 10 == 0:
+ await ctx.send(embeds=embeds)
+ embeds = []
+
+ # Send the remaining embeds
+ if cpt % 10 != 0:
+ await ctx.send(embeds=embeds)
elif ctx.invoked_subcommand is None:
- await ctx.send(await self.bot._(ctx.guild.id, 'sconfig.option-notfound'))
+ await ctx.send(await self.bot._(ctx.guild.id, "sconfig.option-notfound"))
@main_config.command(name="prefix")
async def config_prefix(self, ctx: MyContext, new_prefix=None):
limit = 7
if new_prefix is not None and len(new_prefix) > limit:
- await ctx.send(await self.bot._(ctx.guild.id, "sconfig.prefix-too-long", c=limit))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "sconfig.prefix-too-long", c=limit)
+ )
return
await ctx.send(await self.edit_config(ctx.guild.id, "prefix", new_prefix))
@main_config.command(name="logs_channel")
- async def config_logs_channel(self, ctx: MyContext, *, channel: discord.TextChannel):
+ async def config_logs_channel(
+ self, ctx: MyContext, *, channel: discord.TextChannel
+ ):
await ctx.send(await self.edit_config(ctx.guild.id, "logs_channel", channel.id))
if logs_cog := self.bot.get_cog("Logs"):
- emb = discord.Embed(title=await self.bot._(ctx.guild, "sconfig.config-enabled"),
- description=await self.bot._(ctx.guild, "sconfig.modlogs-channel-enabled"),
- color=16098851)
+ emb = discord.Embed(
+ title=await self.bot._(ctx.guild, "sconfig.config-enabled"),
+ description=await self.bot._(
+ ctx.guild, "sconfig.modlogs-channel-enabled"
+ ),
+ color=16098851,
+ )
await logs_cog.send_embed(ctx.guild, emb)
-
-
-
- #--------------------------------------------------
+ # --------------------------------------------------
# Voice Channel
- #--------------------------------------------------
-
-
+ # --------------------------------------------------
- #--------------------------------------------------
+ # --------------------------------------------------
# ModLogs
- #--------------------------------------------------
+ # --------------------------------------------------
- #--------------------------------------------------
+ # --------------------------------------------------
# Thanks
- #--------------------------------------------------
+ # --------------------------------------------------
-
- #--------------------------------------------------
+ # --------------------------------------------------
# Language
- #--------------------------------------------------
+ # --------------------------------------------------
- @main_config.command(name="language", aliases=['lang'])
+ @main_config.command(name="language", aliases=["lang"])
async def language(self, ctx: MyContext, lang: str):
"""Change the bot language in your server
Use the 'list' option to get the available languages"""
@@ -227,40 +303,37 @@ async def language(self, ctx: MyContext, lang: str):
await ctx.send("Unable to load languages, please try again later")
elif lang == "list": # send a list of available languages
availabe = " - ".join(cog.languages)
- await ctx.send(await self.bot._(ctx.guild.id, "sconfig.languages-list", list=availabe))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "sconfig.languages-list", list=availabe)
+ )
elif lang not in cog.languages: # invalid language
- await ctx.send(await self.bot._(ctx.guild.id, 'sconfig.invalid-language', p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "sconfig.invalid-language", p=ctx.prefix)
+ )
else: # correct case
- selected = cog.languages.index(lang)
- await ctx.send(await self.edit_config(ctx.guild.id, "language", selected))
+ await ctx.send(
+ await self.edit_config(ctx.guild.id, "language", lang)
+ ) # lang should be a string
- #--------------------------------------------------
+ # --------------------------------------------------
# Hypesquad
- #--------------------------------------------------
-
+ # --------------------------------------------------
-
- #--------------------------------------------------
+ # --------------------------------------------------
# Giveaway
- #--------------------------------------------------
-
-
+ # --------------------------------------------------
- #--------------------------------------------------
+ # --------------------------------------------------
# XP
- #--------------------------------------------------
+ # --------------------------------------------------
-
-
- #--------------------------------------------------
+ # --------------------------------------------------
# Groups
- #--------------------------------------------------
-
-
+ # --------------------------------------------------
- #--------------------------------------------------
+ # --------------------------------------------------
# Backup
- #--------------------------------------------------
+ # --------------------------------------------------
"""
@config_backup.command(name="get", aliases=["create"])
async def backup_create(self, ctx: MyContext):
@@ -302,10 +375,10 @@ def check(reaction, user):
await ctx.send('👍')
"""
- #--------------------------------------------------
+ # --------------------------------------------------
# Archives
- #--------------------------------------------------
+ # --------------------------------------------------
-async def setup(bot):
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
await bot.add_cog(Sconfig(bot))
diff --git a/bot/utils/timeclass.py b/bot/utils/timeclass.py
index 03a0d925..1b74099b 100644
--- a/bot/utils/timeclass.py
+++ b/bot/utils/timeclass.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import datetime
import time
from utils import Gunibot
@@ -5,12 +12,48 @@
import discord
from discord.ext import tasks
-fr_months = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
- "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Décembre"]
-en_months = ["January", "February", "March", "April", "May", "June",
- "July", "August", "September", "October", "November", "December"]
-fi_months = ['tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', 'toukokuu',
- 'kesäkuu', 'heinäkuu', 'elokuu', 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu']
+fr_months = [
+ "Janvier",
+ "Février",
+ "Mars",
+ "Avril",
+ "Mai",
+ "Juin",
+ "Juillet",
+ "Aout",
+ "Septembre",
+ "Octobre",
+ "Novembre",
+ "Décembre",
+]
+en_months = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+]
+fi_months = [
+ "tammikuu",
+ "helmikuu",
+ "maaliskuu",
+ "huhtikuu",
+ "toukokuu",
+ "kesäkuu",
+ "heinäkuu",
+ "elokuu",
+ "syyskuu",
+ "lokakuu",
+ "marraskuu",
+ "joulukuu",
+]
class TimeCog(discord.ext.commands.Cog):
@@ -27,21 +70,36 @@ def add_task(self, delay: int, coro, *args, **kwargs):
async def launch(task, coro, *args, **kwargs):
if task.current_loop != 0:
await self.bot.wait_until_ready()
- self.bot.log.info("[TaskManager] Tâche {} arrivée à terme".format(coro.__func__))
+ self.bot.log.info(
+ "[TaskManager] Tâche {} arrivée à terme".format(coro.__func__)
+ )
try:
await coro(*args, **kwargs)
except Exception as e:
self.bot.get_cog("Errors").on_error(e)
+
a = tasks.loop(seconds=delay, count=2)(launch)
a.error(self.bot.get_cog("Errors").on_error)
a.start(a, coro, *args, **kwargs)
self.bot.log.info(
- "[TaskManager] Nouvelle tâche {} programmée pour dans {}s".format(coro.__func__, delay))
+ "[TaskManager] Nouvelle tâche {} programmée pour dans {}s".format(
+ coro.__func__, delay
+ )
+ )
return a
class timedelta:
-
- def __init__(self, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, total_seconds=0, precision=2):
+ def __init__(
+ self,
+ years=0,
+ months=0,
+ days=0,
+ hours=0,
+ minutes=0,
+ seconds=0,
+ total_seconds=0,
+ precision=2,
+ ):
self.years = years
self.months = months
self.days = days
@@ -54,8 +112,8 @@ def __init__(self, years=0, months=0, days=0, hours=0, minutes=0, seconds=0, tot
def set_from_seconds(self):
t = self.total_seconds
rest = 0
- years, rest = divmod(t, 86400*365)
- months, rest = divmod(rest, 86400*365/12)
+ years, rest = divmod(t, 86400 * 365)
+ months, rest = divmod(rest, 86400 * 365 / 12)
days, rest = divmod(rest, 86400)
hours, rest = divmod(rest, 3600)
minutes, rest = divmod(rest, 60)
@@ -70,10 +128,19 @@ def set_from_seconds(self):
else:
self.seconds = round(seconds, self.precision)
- async def time_delta(self, date1, date2=None, lang='en', year=False, hour=True, form='developed', precision=2):
+ async def time_delta(
+ self,
+ date1,
+ date2=None,
+ lang="en",
+ year=False,
+ hour=True,
+ form="developed",
+ precision=2,
+ ):
"""Traduit un intervale de deux temps datetime.datetime en chaine de caractère lisible"""
- if date2 != None:
- if type(date2) == datetime.datetime:
+ if date2 is not None:
+ if isinstance(date2, datetime.datetime):
delta = abs(date2 - date1)
t = await self.time_interval(delta, precision)
else:
@@ -81,79 +148,128 @@ async def time_delta(self, date1, date2=None, lang='en', year=False, hour=True,
else:
t = self.timedelta(total_seconds=date1, precision=precision)
t.set_from_seconds()
- if form == 'digital':
+ if form == "digital":
if hour:
h = "{}:{}:{}".format(t.hours, t.minutes, t.seconds)
else:
- h = ''
- if lang == 'fr':
- text = '{}/{}{} {}'.format(t.days, t.months,
- "/"+str(t.years) if year else '', h)
+ h = ""
+ if lang == "fr":
+ text = "{}/{}{} {}".format(
+ t.days, t.months, "/" + str(t.years) if year else "", h
+ )
else:
- text = '{}/{}{} {}'.format(t.months, t.days,
- "/"+str(t.years) if year else '', h)
- elif form == 'temp':
+ text = "{}/{}{} {}".format(
+ t.months, t.days, "/" + str(t.years) if year else "", h
+ )
+ elif form == "temp":
text = str()
- if t.days + t.months*365/12 + t.years*365 > 0:
- d = round(t.days+t.months*365/12)
+ if t.days + t.months * 365 / 12 + t.years * 365 > 0:
+ d = round(t.days + t.months * 365 / 12)
if not year:
- d += round(t.years*365)
+ d += round(t.years * 365)
elif year and t.years > 0:
- text += str(t.years) + \
- 'a ' if lang == 'fr' else str(t.years)+'y '
- text += str(d)+'j ' if lang == 'fr' else str(d)+'d '
+ text += str(t.years) + "a " if lang == "fr" else str(t.years) + "y "
+ text += str(d) + "j " if lang == "fr" else str(d) + "d "
if hour:
if t.hours > 0:
- text += str(t.hours)+'h '
+ text += str(t.hours) + "h "
if t.minutes > 0:
- text += str(t.minutes)+'m '
+ text += str(t.minutes) + "m "
if t.seconds > 0:
- text += str(t.seconds)+'s '
+ text += str(t.seconds) + "s "
text = text.strip()
else:
text = str()
- if lang == 'fr':
- lib = ['ans', 'an', 'mois', 'mois', 'jours', 'jour', 'heures',
- 'heure', 'minutes', 'minute', 'secondes', 'seconde']
- elif lang == 'lolcat':
- lib = ['yearz', 'year', 'mons', 'month', 'dayz', 'day',
- 'hourz', 'hour', 'minutz', 'minut', 'secondz', 'secnd']
- elif lang == 'fi':
- lib = ['Vuotta', 'vuosi', 'kuukautta', 'kuukausi', 'päivää', 'päivä',
- 'tuntia', 'h', 'minuuttia', 'minute', 'sekuntia', 'toinen']
+ if lang == "fr":
+ lib = [
+ "ans",
+ "an",
+ "mois",
+ "mois",
+ "jours",
+ "jour",
+ "heures",
+ "heure",
+ "minutes",
+ "minute",
+ "secondes",
+ "seconde",
+ ]
+ elif lang == "lolcat":
+ lib = [
+ "yearz",
+ "year",
+ "mons",
+ "month",
+ "dayz",
+ "day",
+ "hourz",
+ "hour",
+ "minutz",
+ "minut",
+ "secondz",
+ "secnd",
+ ]
+ elif lang == "fi":
+ lib = [
+ "Vuotta",
+ "vuosi",
+ "kuukautta",
+ "kuukausi",
+ "päivää",
+ "päivä",
+ "tuntia",
+ "h",
+ "minuuttia",
+ "minute",
+ "sekuntia",
+ "toinen",
+ ]
else:
- lib = ['years', 'year', 'months', 'month', 'days', 'day',
- 'hours', 'hour', 'minutes', 'minute', 'seconds', 'second']
+ lib = [
+ "years",
+ "year",
+ "months",
+ "month",
+ "days",
+ "day",
+ "hours",
+ "hour",
+ "minutes",
+ "minute",
+ "seconds",
+ "second",
+ ]
if year and t.years != 0:
if t.years > 1:
- text += str(t.years)+" "+lib[0]
+ text += str(t.years) + " " + lib[0]
else:
- text += str(t.years)+" "+lib[1]
+ text += str(t.years) + " " + lib[1]
text += " "
if t.months > 1:
- text += str(t.months)+" "+lib[2]
+ text += str(t.months) + " " + lib[2]
elif t.months == 1:
- text += str(t.months)+" "+lib[3]
+ text += str(t.months) + " " + lib[3]
text += " "
if t.days > 1:
- text += str(t.days)+" "+lib[4]
+ text += str(t.days) + " " + lib[4]
elif t.days == 1:
- text += str(t.days)+" "+lib[5]
+ text += str(t.days) + " " + lib[5]
if hour:
if t.hours > 1:
- text += " "+str(t.hours)+" "+lib[6]
+ text += " " + str(t.hours) + " " + lib[6]
elif t.hours == 1:
- text += " "+str(t.hours)+" "+lib[7]
+ text += " " + str(t.hours) + " " + lib[7]
text += " "
if t.minutes > 1:
- text += str(t.minutes)+" "+lib[8]
+ text += str(t.minutes) + " " + lib[8]
elif t.minutes == 1:
- text += str(t.minutes)+" "+lib[9]
+ text += str(t.minutes) + " " + lib[9]
text += " "
if t.seconds > 1:
- text += str(t.seconds)+" "+lib[10]
+ text += str(t.seconds) + " " + lib[10]
elif t.seconds == 1:
- text += str(t.seconds)+" "+lib[11]
+ text += str(t.seconds) + " " + lib[11]
return text.strip()
async def time_interval(self, tmd, precision=2):
@@ -163,52 +279,60 @@ async def time_interval(self, tmd, precision=2):
obj.set_from_seconds()
return obj
- async def date(self, date, lang='fr', year=False, hour=True, digital=False):
+ async def date(self, date, lang="fr", year=False, hour=True, digital=False):
"""Traduit un objet de type datetime.datetime en chaine de caractère lisible. Renvoie un str"""
- if type(date) == time.struct_time:
+ if isinstance(date, time.struct_time):
date = datetime.datetime(*date[:6])
- if type(date) == datetime.datetime:
+ if isinstance(date, datetime.datetime):
if len(str(date.day)) == 1:
- jour = "0"+str(date.day)
+ jour = "0" + str(date.day)
else:
jour = str(date.day)
h = []
- if lang == 'fr':
+ if lang == "fr":
month = fr_months
- elif lang == 'fi':
+ elif lang == "fi":
month = fi_months
else:
month = en_months
- for i in ['hour', 'minute', 'second']:
- a = eval(str("date."+i))
+ for i in ["hour", "minute", "second"]:
+ a = eval(str("date." + i))
if len(str(a)) == 1:
- h.append("0"+str(a))
+ h.append("0" + str(a))
else:
h.append(str(a))
if digital:
if date.month < 10:
- month = "0"+str(date.month)
+ month = "0" + str(date.month)
else:
month = str(date.month)
separator = "/"
- if lang == 'fr':
+ if lang == "fr":
df = "{d}/{m}{y} {h}"
- elif lang == 'fi':
+ elif lang == "fi":
df = "{d}.{m}{y} {h}"
separator = "."
else:
df = "{m}/{d}{y} {h}"
- df = df.format(d=jour, m=month, y=separator+str(date.year)
- if year else "", h=":".join(h) if hour else "")
+ df = df.format(
+ d=jour,
+ m=month,
+ y=separator + str(date.year) if year else "",
+ h=":".join(h) if hour else "",
+ )
else:
- if lang == 'fr' or lang == 'fi':
+ if lang == "fr" or lang == "fi":
df = "{d} {m} {y} {h}"
else:
df = "{m} {d}, {y} {h}"
- df = df.format(d=jour, m=month[date.month-1], y=str(
- date.year) if year else "", h=":".join(h) if hour else "")
+ df = df.format(
+ d=jour,
+ m=month[date.month - 1],
+ y=str(date.year) if year else "",
+ h=":".join(h) if hour else "",
+ )
return df.strip()
-async def setup(bot):
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
await bot.add_cog(TimeCog(bot))
diff --git a/config/config-example.json b/config/config-example.json
deleted file mode 100644
index 564a481a..00000000
--- a/config/config-example.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "token": "Discord token for main bot",
- "token_beta": "Discord token for a beta bot (use with --beta arg)",
- "main_server_id": 695253389411483739,
- "bot_admins": [279568324260528128,125722240896598016],
- "errors_channel": 698557078964273279
-}
\ No newline at end of file
diff --git a/config/global_options.json b/config/global_options.json
deleted file mode 100644
index f038401b..00000000
--- a/config/global_options.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "prefix": {
- "default": "&",
- "type": "prefix",
- "command": "prefix"
- },
- "language": {
- "default": 0,
- "type": "language",
- "command": "language"
- }
-}
\ No newline at end of file
diff --git a/configs/TO_REMOVE.md b/configs/TO_REMOVE.md
new file mode 100644
index 00000000..9795a99f
--- /dev/null
+++ b/configs/TO_REMOVE.md
@@ -0,0 +1 @@
+# This folder is deprecated and have to be removed in future versions. Most of files here will be moved (and adapted) in `data/` folder.
\ No newline at end of file
diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644
index 00000000..cc86cce9
--- /dev/null
+++ b/core/__init__.py
@@ -0,0 +1,9 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+version = (1, 4)
+version_string = '1.4'
\ No newline at end of file
diff --git a/core/config.py b/core/config.py
new file mode 100644
index 00000000..8a3d92ff
--- /dev/null
+++ b/core/config.py
@@ -0,0 +1,196 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import yaml
+import os
+import importlib
+from LRFutils import color
+from LRFutils import logs
+
+accept = ["y", "yes", "yeah", "ye"]
+decline = ["n", "no", "nope", "nah"]
+
+_global_config = {}
+
+# Check basic requirements and start this script if something is missing
+def check():
+ if not os.path.isfile("config.yaml"):
+ print(" ")
+ logs.warn("⛔ The bot is not correctly setup. Running setup script...")
+ os.system("python3 setup.py")
+ exit()
+
+
+def get(config: str):
+ path = config.split(".")
+ conf = _global_config
+ for i in path:
+ conf = conf[i]
+ return conf
+
+
+#################
+# Reload config #
+#################
+
+
+def reload_config():
+ """This function read the core/default_config.yaml file and store it in a dictionnary.
+ Then, it update the dict' using all the plugins//config.yaml files.
+ Finally, it update the dict' using the config.yaml file wich is defined by the user.
+ Each step overwrite the previus one."""
+
+ with open("core/default_config.yaml", "r") as f:
+ _global_config.update(yaml.safe_load(f))
+
+ for plugin in os.listdir(f"plugins"):
+ if os.path.isfile(file := f"plugins/" + plugin + "/config.yaml"):
+ with open(file) as f:
+ _global_config.update({plugin: yaml.safe_load(f)})
+
+ if os.path.isfile("config.yaml"):
+ with open("config.yaml", "r") as f:
+ _global_config.update(yaml.safe_load(f))
+
+ # Save config
+ with open("config.yaml", "w") as f:
+ yaml.dump(_global_config, f)
+
+
+# Automatically load config when the file is imported
+if _global_config == {}:
+ reload_config()
+
+################
+# Plugin Setup #
+################
+
+
+def setup_plugins():
+ """Run the "run" function of each plugin's "setup.py" file in order to allow user to configure the plugins.
+ Called once in the main setup script."""
+
+ for plugin in os.listdir(f"plugins"):
+ if os.path.isfile(f"plugins/" + plugin + "/setup.py"):
+
+ plugin_setup = importlib.import_module(f"plugins." + plugin + ".setup")
+
+ choice = input(
+ f"\n{color.fg.blue}🔌 Do you want to configure {plugin} plugin? [Y/n]:{color.stop} "
+ )
+
+ if choice.lower() not in decline:
+ plugin_config = plugin_setup.run()
+ if plugin_config is not None:
+ _global_config.update({plugin: plugin_config})
+
+ # Save config
+ with open("config.yaml", "w") as f:
+ yaml.dump(_global_config, f)
+
+
+###############
+# TOKEN CHECK #
+###############
+
+
+def token_set(force_set=False):
+ """Check if the token is set, if not, ask for it. Return True if the token is set, False if not."""
+
+ if _global_config["bot"]["token"] is not None and not force_set:
+ choice = input(
+ f"\n🔑 {color.fg.blue}A token is already set. Do you want to edit it? [y/N]:{color.stop} "
+ )
+ if choice.lower() not in accept:
+ return
+
+ print(
+ f"\n🔑 You need to set your Discord bot token in the config file.\n To do so, go on {color.fg.blue}https://discord.com/developers/applications{color.stop}, select your application, go in bot section and copy your token.\n To create a bot application, please refere to this page: {color.fg.blue}https://discord.com/developers/docs/intro{color.stop}.\n Also, be sure to anable all intents."
+ )
+
+ token = ""
+ while token == "":
+ token = input(f"\n🔑 {color.fg.blue}Your bot token:{color.stop} ")
+ if token == "":
+ print(f"\n{color.fg.red}🔑 You need to set a token.{color.stop}")
+ else:
+ _global_config["bot"]["token"] = token
+
+ with open("config.yaml", "w") as f:
+ yaml.dump(_global_config, f)
+ return True
+
+
+#########################
+# Advanced config setup #
+#########################
+
+
+def advanced_setup():
+
+ # Language
+
+ lang = "Baguette de fromage"
+ language = _global_config["bot"]["default_language"]
+ while lang.lower() not in ["en", "fr", ""]:
+ lang = input(
+ f"\n{color.fg.blue}🌐 Choose your language [en/fr] (current: {language}):{color.stop} "
+ )
+ if lang.lower() not in ["en", "fr", ""]:
+ print(f"{color.fg.red}🌐 Invalid language.{color.stop}")
+ if lang != "":
+ _global_config["bot"]["default_language"] = lang.lower()
+
+ # Prefix
+
+ prefix = _global_config["bot"]["default_prefix"]
+ choice = input(
+ f"\n{color.fg.blue}⚜️ Choose the bot command prefix? (current: {prefix}):{color.stop} "
+ )
+ if choice != "":
+ _global_config["bot"]["default_prefix"] = choice
+
+ # Admins
+
+ error = True
+ while error:
+ error = False
+ choice = input(
+ f"\n{color.fg.blue}👑 Bot admins (User ID separated with comma. Let empty to ignore):{color.stop} "
+ )
+ if choice != "":
+ admins = choice.replace(" ", "").split(",")
+ try:
+ for admin in admins:
+ admin = int(admin)
+ _global_config["bot"]["admins"] = admins
+ except:
+ print(
+ f"{color.fg.red}👑 Invalid entry. Only user ID (integers), comma and space are expected.{color.stop}"
+ )
+ error = True
+
+ # Error channel
+
+ error = True
+ while error:
+ error = False
+ choice = input(
+ f"\n{color.fg.blue}🤕 Error channel (Channel ID. Let empty to ignore):{color.stop} "
+ )
+ if choice != "":
+ try:
+ channel = int(choice)
+ _global_config["bot"]["error_channels"] = channel
+ except:
+ print(
+ f"{color.fg.red}🤕 Invalid entry. Only channel ID (integers) are expected.{color.stop}"
+ )
+ error = True
+
+ with open("config.yaml", "w") as f:
+ yaml.dump(_global_config, f)
diff --git a/core/default_config.yaml b/core/default_config.yaml
new file mode 100644
index 00000000..18c52f86
--- /dev/null
+++ b/core/default_config.yaml
@@ -0,0 +1,7 @@
+bot:
+ token: null
+ default_prefix: "!"
+ default_language: "en"
+ admins: [279568324260528128,125722240896598016] # (Default: Leirof & Z_runner, the project leaders of Gipsy)
+ error_channels: 823813751018487848 # (Default: Hidden channel on Gunivers)
+
diff --git a/credits.md b/credits.md
new file mode 100644
index 00000000..a4a4258c
--- /dev/null
+++ b/credits.md
@@ -0,0 +1,11 @@
+Copyright © ZRunner 2020 - 2023
+Copyright © Aeris One 2020 - 2023
+Copyright © Leirof 2020 - 2023
+Copyright © theogiraudet 2020 - 2023
+Copyright © ascpial 2021 - 2023
+Copyright © Theaustudio 2022 - 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/data/model.sql b/data/model.sql
index e69de29b..949b3523 100644
--- a/data/model.sql
+++ b/data/model.sql
@@ -0,0 +1,4 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..ed880990
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/_static/banner.png b/docs/_static/banner.png
new file mode 100644
index 00000000..c6bbc056
Binary files /dev/null and b/docs/_static/banner.png differ
diff --git a/docs/_static/css/stylesheet.css b/docs/_static/css/stylesheet.css
new file mode 100644
index 00000000..263ae837
--- /dev/null
+++ b/docs/_static/css/stylesheet.css
@@ -0,0 +1,17 @@
+html[data-theme="light"] {
+ --pst-color-primary: #fa9632;
+ --pst-color-secondary: #3296fa;
+ --sd-color-card-border-hover: var(--pst-color-primary);
+ --pst-color-primary-highlight: var(--pst-color-secondary);
+}
+
+html[data-theme="dark"] {
+ --pst-color-primary: #fa9632;
+ --pst-color-secondary: #3296fa;
+ --sd-color-card-border-hover: var(--pst-color-primary);
+ --pst-color-primary-highlight: var(--pst-color-secondary);
+}
+
+a.headerlink {
+ color: var(--pst-color-secondary);
+}
\ No newline at end of file
diff --git a/docs/_static/logo-discord.png b/docs/_static/logo-discord.png
new file mode 100644
index 00000000..30d61361
Binary files /dev/null and b/docs/_static/logo-discord.png differ
diff --git a/docs/_static/logo-gunivers.png b/docs/_static/logo-gunivers.png
new file mode 100644
index 00000000..a1622c1a
Binary files /dev/null and b/docs/_static/logo-gunivers.png differ
diff --git a/docs/_static/logo.png b/docs/_static/logo.png
new file mode 100644
index 00000000..dd03f874
Binary files /dev/null and b/docs/_static/logo.png differ
diff --git a/docs/_static/meet_gipsy.png b/docs/_static/meet_gipsy.png
new file mode 100644
index 00000000..120c5898
Binary files /dev/null and b/docs/_static/meet_gipsy.png differ
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 00000000..77f892ef
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1,9 @@
+# 📜 Changelog
+
+> Coming soon
+
+```{admonition} 🤝 Help us building this project!
+:class: note
+
+This part of the documentation is still under construction. If you want to help us, you can contribute to the project on [GitHub](https://github.com/Gunivers/Gipsy) or come on our [Discord server](https://discord.gg/E8qq6tN).
+```
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index 1dcfb6e9..78ada2a7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,60 +1,180 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# This file only contains a selection of the most common options. For a full
-# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-# -- Path setup --------------------------------------------------------------
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+# -- Project information -----------------------------------------------------
+project = "Gipsy"
+copyright = "2023, Gunivers"
+author = "Z_runner, Leirof, Aeris One, ascpial, theogiraudet, fantomitechno, Just_a_Player and Aragorn"
-# -- Project information -----------------------------------------------------
+import os
+
+# Project information ---------------------------------------------------------
project = 'Gipsy'
-copyright = '2021, Gunivers'
+copyright = '2023, Gunivers'
author = 'Gunivers'
+html_favicon = "_static/logo.png"
-# The full version, including alpha/beta/rc tags
-release = '1.3'
+# -- General configuration ----------------------------------------------------
-
-# -- General configuration ---------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
extensions = [
- 'myst_parser'
+ 'myst_parser',
+ 'sphinx_design',
+ 'sphinx_togglebutton',
+ 'sphinx_copybutton',
]
-
-
-# Add any paths that contain templates here, relative to this directory.
+myst_heading_anchors = 6
templates_path = ['_templates']
-
-# The root document.
-root_doc = 'index'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+# Options for HTML output -----------------------------------------------------
+
+html_theme = 'pydata_sphinx_theme'
+
+# html_css_files = [
+# 'credits.css',
+# ]
+
+html_theme_options = {
+ "github_url": "https://github.com/Gunivers/Gipsy",
+ "announcement": "⚠️ You are reading a doc of an undergoing development version. Information can be out of date and/or change at any time. ⚠️",
+ "logo": {
+ "image_dark": "_static/logo.png",
+ "text": "Gipsy", # Uncomment to try text with logo
+ },
+ "icon_links": [
+ {
+ "name": "Support us",
+ "url": "https://utip.io/gunivers",
+ "icon": "fa fa-heart",
+ },
+ {
+ "name": "Gunivers",
+ "url": "https://gunivers.net",
+ "icon": "_static/logo-gunivers.png",
+ "type": "local",
+ },
+ {
+ "name": "Discord server",
+ "url": "https://discord.gg/E8qq6tN",
+ "icon": "_static/logo-discord.png",
+ "type": "local",
+ },
+ ]
+}
+
+html_logo = "_static/logo.png"
+
+html_static_path = ['_static']
+
+html_css_files = [
+ 'css/stylesheet.css',
+]
-# -- Options for HTML output -------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-#
-html_theme = 'sphinx_rtd_theme'
+myst_enable_extensions = [
+ "amsmath",
+ "colon_fence",
+ "deflist",
+ "dollarmath",
+ "fieldlist",
+ "html_admonition",
+ "html_image",
+ #"linkify",
+ "replacements",
+ "smartquotes",
+ "strikethrough",
+ "substitution",
+ "tasklist",
+]
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
\ No newline at end of file
+# Plugin doc generation -------------------------------------------------------
+
+import shutil
+
+CONTRIBUTE = """
+```{admonition} 🤝 Help us to improve this documentation!
+:class: tip
+If you want to help us to improve this documentation, you can edit it on the [GitHub repo](https://github.com/Gunivers/Gipsy/) or come and discuss with us on our [Discord server](https://discord.gg/E8qq6tN)!
+```
+"""
+
+GITHUB_DISCUSSION_FOOTER = """
+---
+## 💬 Did it help you?
+Feel free to leave your questions and feedbacks below!
+
+"""
+
+def generate_plugin_doc():
+
+ # Saving index.md file content except plugin toctree
+ with open("user_guide.md", "r", encoding="utf-8") as f:
+ before = []
+ for line in f:
+ before.append(line)
+ if line.startswith("
+
+# ❓ FAQ
+
+Nothing here... for the moment.
+
+
+```{admonition} 🤝 Help us to improve this documentation!
+:class: tip
+
+If you want to help us to improve this documentation, you can edit it on the [GitHub repo](https://github.com/Gunivers/Gipsy/) or come and discuss with us on our [Discord server](https://discord.gg/E8qq6tN)!
+```
diff --git a/docs/getting_started.md b/docs/getting_started.md
new file mode 100644
index 00000000..ba26a809
--- /dev/null
+++ b/docs/getting_started.md
@@ -0,0 +1,136 @@
+---
+html_theme.sidebar_secondary.remove: true
+html_theme.sidebar_primary.remove: true
+---
+
+# 👋 Getting Started
+
+## 📥 Get it on your Discord server
+
+::::{tab-set}
+:::{tab-item} 💌 Invite Gunivers' instance
+
+
+
+You can simply invite the bot instance hosted by the Gunivers community itself by clicking on the button below!
+
+```{button-link} http://utip.io/s/1yhs7W
+:color: primary
+:align: center
+:shadow:
+
+Invite the bot on your server
+```
+
+Alternatively, you can invite the beta version by [clicking here](https://discordapp.com/oauth2/authorize?client_id=813836349147840513&scope=bot)
+
+
+
+```{admonition} If you use the beta version
+:class: important
+
+If you invite the beta version, you will be able to test the latest features added to the bot. However, the bot in beta version may contain security holes and many bugs. It may also stop working suddenly and for long periods.
+```
+
+:::
+:::{tab-item} ⚙️ Self-host the bot
+
+1. Create a Discord application and add a bot to it by following [this tutorial](https://discord.com/developers/docs/getting-started).
+
+2. Install [Python 3.9](https://www.python.org/downloads/release/python-390/) or higher.
+
+3. Install [Git CLI](https://git-scm.com/book/en/v2/Getting-Started-The-Command-Line)
+
+4. Open a terminal and go where you want to install it
+
+ ```bash
+ cd /path/to/installation
+ ```
+
+5. Clone the repository
+
+ ```bash
+ git clone https://github.com/Gunivers/Gipsy
+ ```
+
+6. (Optional) Create a virtual environment with
+
+ ```
+ python3.9 -m venv venv
+ ```
+ and activate it with
+ ```
+ source venv/bin/activate # Linux.
+ venv\Scripts\activate # Windows
+ ```
+
+7. Install dependencies
+
+ ```
+ pip install -r requirements.txt
+ ```
+
+8. Run the setup script and answer the questions.
+
+ ```bash
+ python setup.py
+ ```
+
+9. Create a `plugins` folder and add all the plugins you wan to use on your bot. You can find all the official plugins [here](https://github.com/Gunivers/Gipsy-plugins). To install a plugin, simply copy the folder of the plugin in the `plugins` folder.
+
+10. Start the bot
+ ```bash
+ python start.py
+ ```
+
+11. In the logs, find a line like this:
+
+ ```
+ 09/02/2023 at 19:59:32 | [INFO] ID : 786546781919641600
+ ```
+
+12. Copy the ID an place it in the following URL (replace the underscores with the ID):
+
+ ```
+ https://discord.com/oauth2/authorize?scope=bot&client_id=__________________
+ ```
+
+13. Open the URL in your browser and invite the bot to your server.
+
+:::
+:::{tab-item} 🌐 Other instances
+
+Here is the list of other Gipsy instances hosted by trusted peoples:
+
+```` {grid} 2
+```{grid-item-card} Axobot
+:link: https://top.gg/bot/1048011651145797673
+:link-type: url
+:margin: 0 3 0 0
+
+
+
+
+
+Axobot is not really a Gipsy instance, but it is maintained by the same original creator and both project share the same origin. It is designed to a ready-to-use high quality bot capable of handling a huge amount of servers, such as most popular bots like Mee6 or Dyno.
+
+```
+````
+
+:::
+::::
+
+## 👶 First steps
+
+In a channel where the bot can read and write messages, follow the instructions below.
+
+1. Define your language with the command `@gipsy config language `
+2. Define the prefix with the command `@gipsy config prefix `.
+ Exemple: type `@gipsy config prefix !` and then `!ping`
+
+```{note}
+In this documentation, we will use `@gipsy` as the prefix because it always work, even if another prefix is set.
+```
+3. Type `@gipsy config` to see the rest of the option you can configure. To edit a config option, enter `@gipsy config `
+4. Type `@gipsy help` to see the list of commands.
+5. Enjoy!
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..cd768938
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,82 @@
+---
+html_theme.sidebar_secondary.remove: true
+html_theme.sidebar_primary.remove: true
+sd_hide_title: true
+---
+
+# Home page
+
+
+
+
+```{image} _static/banner.png
+:class: dark-light
+```
+
+
+
+
+## 🔎 What is it?
+
+Gipsy is modular, free and open-source Discord bot whose focus on accessibility and customization. It is written in Python and uses the Discord.py library and is managed by the [Gunivers](https://gunivers.net) community.
+
+::::{grid} 2
+
+:::{grid-item}
+
+```{button-link} getting_started.html
+:color: primary
+:align: right
+:shadow:
+
+Start using Gipsy!
+```
+
+:::
+:::{grid-item}
+
+```{button-link} https://youtu.be/HaYenuxtibU
+:color: primary
+:align: left
+:outline:
+
+Watch the presentation video
+```
+
+::::
+
+## ⚙️ Built-in features
+
+```{include} plugins_grid.md
+```
+
+## 🏃 Motivation
+
+As Discord bot developers we noticed that there is a lot of different service bots, and a lot of tool to create your own bots, but nothing in between. We wanted to create a bot that would be easy to use and to configure, but also powerful and customizable.
+
+This project has then 2 objectives:
+- On the core side, the goal is to design a system that allow to simplify as much as possible the creation of plugins, and to make them as customizable as possible.
+- On the plugin side, the goal is to create a lot of plugins that can be used by anyone, and that can be easily configured to fit the needs of the server.
+
+## 🤝 Follow and/or contribute
+
+You can come on [our Discord](https://discord.gg/E8qq6tN) server to talk with us and/or take part of the project!
+
+If you want to contribute, please read at least the "Getting started" section in the ["Contributing" page](https://glib-core.readthedocs.io/en/latest/contributing.html) that contain all the development convention used in this project.
+
+
+```{toctree}
+:maxdepth: 3
+:caption: Info
+:hidden:
+
+getting_started
+user_guide
+developer_guide
+faq
+changelog
+special_thanks
+```
+
+
+
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index 62f6394b..00000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-.. Gipsy documentation master file, created by
- sphinx-quickstart on Sat Sep 25 11:57:54 2021.
- You can adapt this file completely to your liking, but it should at least
- contain the root `toctree` directive.
-
-Welcome to Gipsy's documentation!
-=================================
-
-Gipsy is a multifunction bot managed by the `Gunivers `_ community.
-
-Please use at least **Python 3.9** to run this project.
-
-Use ``pip install -r requirements.txt`` in the directory to install dependencies.
-
-**Description**
--------------------
-
-Gipsy is a Discord bot whose first objective is to meet the expectations of the Gunivers community. However, if we want to create new features, we might as well let those who might be interested in them enjoy them !
-You can invite the bot, learn what it can do and follow its evolution.
-
-**Invite**
---------------
-
-You can invite the bot by `clicking here `_
-
-
-You can also invite the bot in beta version to enjoy the latest features added. Be careful though: the bot in beta version may contain security holes and many bugs. It may also stop working suddenly and for long periods. If you want to invite it though, `click here `_
-
-.. include:: summary.rst
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
\ No newline at end of file
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 00000000..63e18088
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=%~dp0
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs/plugins/antikikoo.md b/docs/plugins/antikikoo.md
deleted file mode 100644
index 85a81c6f..00000000
--- a/docs/plugins/antikikoo.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Anti-kikoo
-
-Nothing here...
diff --git a/docs/plugins/channelArchive.md b/docs/plugins/channelArchive.md
deleted file mode 100644
index a8ff32ed..00000000
--- a/docs/plugins/channelArchive.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Channel archive
-
-Nothing here...
diff --git a/docs/plugins/contact.md b/docs/plugins/contact.md
deleted file mode 100644
index 54bcc6b4..00000000
--- a/docs/plugins/contact.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Contact
-
-With this bot, you can create a room in your server where your members can contact the staff. The bot will then create a private room, visible only to said member and your staff, and delete the original message to keep the room clean.
-A command also exists to delete contact rooms older than X days.
-
-Three configuration options exist for this module:
-
-* `contact_category` : The category in which the private room will be created
-* `contact_channel` : The channel your members can access to start the process
-* `contact_roles` : The roles that will have access to the private contact rooms
-
-It is your duty to make sure that the bot can read the contact channel, and create the contact categories with the necessary permissions. Without this, nothing will happen.
-
-The bot will send the message posted by the user back to his private room, if possible as a webhook with the user's name and avatar.
-
-The command to semi-automatically clean up contact rooms is `contact-clear`. You can specify a minimum number of days of inactivity, by default 15: the bot will then look if the last message posted is old enough before deleting the room.
diff --git a/docs/plugins/general.md b/docs/plugins/general.md
deleted file mode 100644
index 2402d226..00000000
--- a/docs/plugins/general.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Global commands
-
-Nothing here...
\ No newline at end of file
diff --git a/docs/plugins/giveaways.md b/docs/plugins/giveaways.md
deleted file mode 100644
index df540752..00000000
--- a/docs/plugins/giveaways.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# **Giveaways**
-
-A giveaway system is available to offer gifts to your members when you feel like it. For this, you can use the command `/giveaway` or its short form `/gaw`. Many options for the giveaway are available, so we invite you to enter the command `/help gaw` to see the different options, and `/help gaw start` to understand how to start a fully customized giveaway. Once the giveaway is started, the bot will send a message with a reaction to the specified room. Members using the command to enter the giveaway or adding the reaction to the message will be added to the list of participants. At the end of the giveaway or when you use the `/gaw pick ` command, a member will be randomly selected from this list.
\ No newline at end of file
diff --git a/docs/plugins/groups.md b/docs/plugins/groups.md
deleted file mode 100644
index fa64f3b3..00000000
--- a/docs/plugins/groups.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Group
-
-Nothing here...
diff --git a/docs/plugins/hypesquad.md b/docs/plugins/hypesquad.md
deleted file mode 100644
index 79f48a43..00000000
--- a/docs/plugins/hypesquad.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# **Hypesquad**
-
-You can configure the bot to automatically add a role based on the users hypesquad home. This role will be updated for all members every 12 hours, or for a specific member when he joins the server. This feature allows you to create house rooms and organize events using the houses in Discord HypeSquad.
diff --git a/docs/plugins/logs.md b/docs/plugins/logs.md
deleted file mode 100644
index e802f7dc..00000000
--- a/docs/plugins/logs.md
+++ /dev/null
@@ -1,12 +0,0 @@
-## **Logs**
-
-This bot offers a log system for your server, which grows over time. So you can set up a room where you can send messages when a member leaves the server, when a message is deleted, when an invitation is created... and many other things.
-
-For this, only two configurations are needed:
-
-* `logs_channel`: The textual channel where to send logs
-* `modlogs`: The list of active logs
-
-To enable or disable a log category, the command is `config modlogs enable/disable `. You can use the `config modlogs list` command to get a list of available categories.
-
-Make sure the bot can send embeds to the log room. Also, some logs are only accessible under certain permissions: for example, the bot must have the `Manage Lounges` permission to send invitation creation logs.
diff --git a/docs/plugins/messageManager.md b/docs/plugins/messageManager.md
deleted file mode 100644
index 582b085a..00000000
--- a/docs/plugins/messageManager.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# **Message manager**
-
-## **Moving messages**
-
-Gunibot.py offers the possibility to manage messages by moving them to other rooms. To do this, you will need to perform the command:
-
-`/move `
-
-Where `` can be replaced by the ID or link to a message, and `` can be replaced by the ID or mention of the destination channel. It is also possible to move multiple messages at once by doing:
-
-`/move `
-
-This will move all the messages copied between the two messages indicated. Make sure that these two messages are in the same channel, and that there are no more than 20 messages separating them (this limitation prevents the bot from being considered a spammer by Discord).
-
-## **Request to change room**
-
-If a discussion is rambling and no longer appropriate for the channel, you can send a message visible to everyone with the command: /hs ``. The bot will then ask all participants to continue their discussion in the indicated channel, or to find a more appropriate one if you don't indicate one.
diff --git a/docs/plugins/misc.md b/docs/plugins/misc.md
deleted file mode 100644
index b65899b1..00000000
--- a/docs/plugins/misc.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Misc
-
-## **Distribute Cookies**
-
-You can distribute or request cookies via the command: `/cookie `. A villager will then take a cookie to the specified member. If you do not specify the member, the villager will give you the cookie.
-
-## **Toss a coin**
-
-Ever wanted to throw a coin but because you're poor you never had a coin on hand? We have solved this problem by adding the `/flipacoin` command which will flip a coin for you and tell you the result. Note that there is a non-zero chance that the coin will fall on its side, we have pushed the realism to the limit.
-
-## **Hoster**
-
-Enter the `hoster` command to get information about the bot's host.
-
-## **Don't ask to ask, just ask.**
-
-> It is often said that the best weapon is the one you don't have to use. I don't agree. In my opinion, the best weapon is the one you only have to use once." - Tony Stark
-
-Are you tired of people asking if there are people to answer a question without saying what the question is? Well, so are we! After extensive research we have developed a formidable weapon in the form of a simple command that makes these people realize that it is better to ask a question directly than to make unnecessary detours that waste everyone's time. This command is `/dataja`, to be handled with the utmost care.
diff --git a/docs/plugins/quizz.md b/docs/plugins/quizz.md
deleted file mode 100644
index c8d717f1..00000000
--- a/docs/plugins/quizz.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Quizz
-
-Nothing here...
\ No newline at end of file
diff --git a/docs/plugins/roleLink.md b/docs/plugins/roleLink.md
deleted file mode 100644
index b057b256..00000000
--- a/docs/plugins/roleLink.md
+++ /dev/null
@@ -1,22 +0,0 @@
-## Role link
-
-This system allows you to create dependencies between roles and thus manage automatically the gain/withdrawal of roles according to the acquisition or loss of other roles. To create a link between roles, you will need to perform the command:
-
-`/rolelink add when `
-
-Where
-
-* `` can take the value `grant` or `revoke`.
-* `` is the ID or mention of the role that will be given/removed automatically.
-* `` corresponds to the trigger for the action, which can take the value `get-one`, `get-all`, `loose-one` or `loose-all`.
-* `` is the list of roles targeted by the trigger.
-
-To see the list of links between roles, you can enter the command :
-
-`/rolelink list`
-
-To remove one of them, you can simply do:
-
-`/rolelink remove `
-
-Where `` is the number present in front of the corresponding role links in the link list.
diff --git a/docs/plugins/rss.md b/docs/plugins/rss.md
deleted file mode 100644
index 491cc661..00000000
--- a/docs/plugins/rss.md
+++ /dev/null
@@ -1,93 +0,0 @@
-# RSS
-
-Gipsy offers you the possibility to follow different content sources, from social networks like Youtube or Twitter to simple blogs equipped with an RSS feed system.
-
-## **Add or remove a feed**
-
-```
-rss add
-```
-
-where ` ` is the link to the source of the content. If it's a social network account, put e link to the main page of the account (because it's the only one that shows account ID correctly in the URL). If it is a blog, forum or other soruce with an RSS feed, simply add the link to the RSS feed.
-
-To remove a feed:
-
-```
-rss remove
-```
-
-Where `` is the number of the feed you will find in the feed list.
-
-## **See feed list**
-
-```
-rss list
-```
-
-## **Move a feed to another room**.
-
-```
-rss move
-```
-
-Where `` is the number of the feed you will find in the feed list, and `` is the lounge you want to move it to.
-
-## **Mention a role when content appears**
-
-```
-rss roles
-```
-
-Will run a script that will guide you through making changes.
-
-```
-rss mentions [role2] [role3] ...
-```
-
-Will directly modify the feed to mention the filled-in roles
-
-## **Change the text of an rss feed**
-
-```
-rss text
-```
-
-Modifies the text of the feed. Several variables can be used in the text:
-
-* `{author}` : the author of the post
-* `{channel}` : the name of the channel
-* `{date}` : the date of publication of the post (UTC)
-* `{link}` or `{url}` : the link to the post
-* `{logo}` : an emoji representing the type of post (web, Twitter, YouTube...)
-* `{mentions}` : the list of roles mentioned
-* `{title}` : the title of the post
-
-You can also use the command
-
-```
-rss text
-```
-
-Which will run a guided script, similar to the rss roles command.
-
-## **Test if the feed is working properly**
-
-```
-rss test
-```
-
-Where link is the link to the rss feed or the social network account.
-
-## **Use embeds to display content**
-
-```
-rss embed
-```
-
-Enables/disables embeds for the specified feed.
-
-```
-rss embed 6678466620137 true title="hey u" footer = "Hi \nI'm a footer"
-```
-
-Changes the content of the embed for the specified feed. You can also use variables, like the `rss text` command.
diff --git a/docs/plugins/thanks.md b/docs/plugins/thanks.md
deleted file mode 100644
index 43955a57..00000000
--- a/docs/plugins/thanks.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# **Thanks**
-
-At Gunivers, we believe that active members of a project should be rewarded for their efforts. We have therefore set up a reward system, allowing the server staff to congratulate certain people via a simple command: the `thanks` command.
-
-After a certain number of thanks (1 at the moment, but later it will be possible to configure levels), the member receives a specific role that you will have configured. You are free to assign specific permissions.
-
-In order for the system to reward only active members, and to encourage users to stay active, a thank you is only valid for a certain time: 30 days by default. It is also possible to configure this duration according to your choice, between one minute and .... hundred years if you like.
-
-Three configuration options are available:
-
-* `thanks_allowed_roles`: A list of roles allowed to use the `thanks` command
-* `thanks_roles`: The roles given to thanked members
-* `thanks_duration` : The duration of validity of a thank you
-
-The `thankslist` command can be used to see the list of thanks given to a user, and `thanksreload` to refresh the roles.
diff --git a/docs/plugins/voice.md b/docs/plugins/voice.md
deleted file mode 100644
index 25d282b0..00000000
--- a/docs/plugins/voice.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# **Voice**
-
-## **Automatic role giving**
-
-It is possible to automatically give a role to any member being in a voice room, and to remove them when this member is not in any voice room. To do this, you just have to configure the option `voice_roles` with the list of roles to give. As simple as that.
-
-## **Creation of rooms on demand**
-
-The bot also has a voice room creation feature: when a member enters a specific voice room, the bot creates a special one, gives the permissions to manage the room to that user, and then moves the user to that new room. It is possible to customize the name of this room, which by default will take a random name via an API.
-
-Three options exist for this module:
-
-* `voice_category` : The category where to create the voice rooms
-* voice_channel` : The lobby where users must connect to trigger the procedure
-* `voice_channel_format` : The format of the name of the created channels. Use `{random}` for a random name, `{asterix}` for a random name from the adventures of Asterix, or `{user}` for the user's name
-
-It's up to you to make sure the bot has sufficient permissions to create the rooms and move members in them.
diff --git a/docs/plugins/welcome.md b/docs/plugins/welcome.md
deleted file mode 100644
index 3c9f5549..00000000
--- a/docs/plugins/welcome.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# **Welcome**
-
-## **Verification system**
-
-The bot has a member verification system, where each newcomer will have to enter a key phrase that will give him access to the rest of the server.
-
-The system is designed in such a way that the bot will only do three actions: send a message when the member joins the server, detect when the member sends the right keyphrase in the right room, and then give/remove the configured role by sending a confirmation message at the same time.
-
-The configuration currently has 4 options:
-
-* `verification_channel` : Channel in which the bot messages will be sent, and in which the member will have to send the keyphrase
-* `verification_role` : Role given or removed to verified members
-* `verification_add_role` : Boolean (true/false) indicating if the role should be given (true) or removed (false)
-* `pass_message` : The phrase to enter to be verified
-
-It is up to you to make sure that the unverified member can write in the verification room, and to configure your roles as you wish.
-
-Other options will come later, like customizing the welcome message.
-
-## **Automatic roles**
-
-It is also possible to configure the bot to give a role to any newcomer, independently of the verification system detailed above. To do so, you just have to configure the `welcome_roles` option with the list of roles to give.
diff --git a/docs/plugins/wormhole.md b/docs/plugins/wormhole.md
deleted file mode 100644
index 70d399f9..00000000
--- a/docs/plugins/wormhole.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Wormhole
-
-Nothing here...
\ No newline at end of file
diff --git a/docs/plugins/xp.md b/docs/plugins/xp.md
deleted file mode 100644
index 5e521487..00000000
--- a/docs/plugins/xp.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# XP
-
-Nothing here...
diff --git a/docs/plugins_grid.md b/docs/plugins_grid.md
new file mode 100644
index 00000000..adc2e2ee
--- /dev/null
+++ b/docs/plugins_grid.md
@@ -0,0 +1,69 @@
+::::{grid} 2
+
+:::{grid-item-card} 🎁 Giveways
+:link: plugins/giveaways/user_documentation
+:link-type: doc
+:margin: 0 3 0 0
+
+Create giveaways to reward your community!
+:::
+
+:::{grid-item-card} 🎖️ XP System
+:link: plugins/xp/user_documentation
+:link-type: doc
+:margin: 0 3 0 0
+
+Give XP to your active members and let them level up and getting roles!
+:::
+
+:::{grid-item-card} 🔊 Auto Voice Channels
+:link: plugins/voice/user_documentation
+:link-type: doc
+:margin: 0 3 0 0
+
+Create voice channels automatically when a user joins a voice channel!
+:::
+
+:::{grid-item-card} ❓ Quizz
+:link: plugins/quizz/user_documentation
+:link-type: doc
+:margin: 0 3 0 0
+
+Challenge your community with a huge variety of quizz!
+:::
+
+:::{grid-item-card} 📎 Role Link
+:link: plugins/roleLink/user_documentation
+:link-type: doc
+:margin: 0 3 0 0
+
+Link roles between them to automatize the role management and create hierarchy relations!
+:::
+
+:::{grid-item-card} 🌀 Wormhole
+:link: plugins/wormhole/user_documentation
+:link-type: doc
+:margin: 0 3 0 0
+
+Create a wormhole between two channels to make a unique inter-server channel!
+:::
+
+:::{grid-item-card} 💬 Message Management
+:link: plugins/messageManager/user_documentation
+:link-type: doc
+:margin: 0 0 0 0
+
+Move messages between channels or imitate another user!
+:::
+
+:::{grid-item-card} 🎭 Groups
+:link: plugins/groups/user_documentation
+:link-type: doc
+:margin: 0 0 0 0
+
+Let members create private groups with associated role and channel!
+:::
+
+::::
+
+And much more!
\ No newline at end of file
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 66923709..0e884d49 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,2 +1,6 @@
myst-parser
-sphinx-rtd-theme
\ No newline at end of file
+pydata-sphinx-theme
+sphinx-design
+sphinx-copybutton
+sphinx-togglebutton
+shutils
\ No newline at end of file
diff --git a/docs/special_thanks.md b/docs/special_thanks.md
new file mode 100644
index 00000000..2ccfd1fb
--- /dev/null
+++ b/docs/special_thanks.md
@@ -0,0 +1,13 @@
+---
+html_theme.sidebar_secondary.remove: true
+---
+
+# ❤️ Special thanks
+
+> Coming soon
+
+```{admonition} 🤝 Help us building this project!
+:class: note
+
+This part of the documentation is still under construction. If you want to help us, you can contribute to the project on [GitHub](https://github.com/Gunivers/Gipsy) or come on our [Discord server](https://discord.gg/E8qq6tN).
+```
\ No newline at end of file
diff --git a/docs/summary.rst b/docs/summary.rst
deleted file mode 100644
index 5e2ac185..00000000
--- a/docs/summary.rst
+++ /dev/null
@@ -1,37 +0,0 @@
-
-.. toctree::
- :maxdepth: 3
- :caption: Info
-
- contributing.md
- faq.md
-
-.. toctree::
- :maxdepth: 2
- :caption: Installed plugins
-
- plugins/antikikoo.md
- plugins/channelArchive.md
- plugins/contact.md
- plugins/general.md
- plugins/giveaways.md
- plugins/groups.md
- plugins/hypesquad.md
- plugins/logs.md
- plugins/messageManager.md
- plugins/misc.md
- plugins/quizz.md
- plugins/roleLink.md
- plugins/rss.md
- plugins/thanks.md
- plugins/voice.md
- plugins/welcome.md
- plugins/wormhole.md
- plugins/xp.md
-
-.. toctree::
- :maxdepth: 2
- :caption: For developers
-
- create_plugin/01-Plugin_structure.md
- create_plugin/02-Server_configuration_variables.md
diff --git a/docs/user_guide.md b/docs/user_guide.md
new file mode 100644
index 00000000..5e7be50d
--- /dev/null
+++ b/docs/user_guide.md
@@ -0,0 +1,41 @@
+---
+html_theme.sidebar_secondary.remove: true
+---
+
+# 🕹️ User guide
+
+Here is all the plugins documentation to let you enjoy Gipsy to the fullest!
+
+On the official Gipsy instance, you can find the following plugins:
+
+```{include} plugins_grid.md
+```
+
+
+
+
+```{toctree}
+:maxdepth: 2
+:hidden:
+
+plugins/antikikoo/user_documentation.md
+plugins/channelArchive/user_documentation.md
+plugins/contact/user_documentation.md
+plugins/general/user_documentation.md
+plugins/giveaways/user_documentation.md
+plugins/groups/user_documentation.md
+plugins/hypesquad/user_documentation.md
+plugins/logs/user_documentation.md
+plugins/messageManager/user_documentation.md
+plugins/misc/user_documentation.md
+plugins/permissions/user_documentation.md
+plugins/quizz/user_documentation.md
+plugins/roleLink/user_documentation.md
+plugins/rss/user_documentation.md
+plugins/thanks/user_documentation.md
+plugins/voice/user_documentation.md
+plugins/welcome/user_documentation.md
+plugins/wormhole/user_documentation.md
+plugins/xp/user_documentation.md
+
+```
\ No newline at end of file
diff --git a/langs/en.yml b/langs/en.yml
index 6d58c1cc..b23c2ea1 100644
--- a/langs/en.yml
+++ b/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
errors:
custom_checks:
@@ -142,41 +147,6 @@ en:
footer1: "Author: %{author} • Message ID: %{message}"
footer2: "Author: %{author}"
footer3: "Member: %{member}"
- perms:
- list:
- add_reactions: Add reactions
- administrator: Administrator
- attach_files: Attach files
- ban_members: Ban members
- change_nickname: Change nickname
- connect: Connect
- create_instant_invite: Create invite
- deafen_members: Deafen members
- embed_links: Embed links
- external_emojis: Use external emoji
- kick_members: Kick members
- manage_channels: Manage channels
- manage_emojis: Manage emojis
- manage_guild: Mannage server
- manage_messages: Manage messages
- manage_nicknames: Manage nicknames
- manage_roles: Manage roles
- manage_webhooks: Manage webhooks
- mention_everyone: Mention everyone, here and all roles
- move_members: Move members
- mute_members: Mute members
- priority_speaker: Priority speaker
- read_message_history: Read message history
- read_messages: View channel
- request_to_speak: Request to speak
- send_messages: Send messages
- send_tts_messages: Send Text-to-Speech messages
- speak: Speak
- stream: Video
- use_slash_commands: Use slash commands
- use_voice_activation: Use voice activity
- view_audit_log: View audit log
- view_guild_insights: View server insights
sconfig:
invalid-emoji: This emoji is invalid. If it is a custom emoji, make sure I'm on its server
invalid-duration: Invalid duration
diff --git a/langs/fr.yml b/langs/fr.yml
index 14ab6a14..8796eba2 100644
--- a/langs/fr.yml
+++ b/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
errors:
custom_checks:
@@ -142,41 +147,6 @@ fr:
footer1: "Auteur : %{author} • ID du message : %{message}"
footer2: "Auteur : %{author}"
footer3: "Membre : %{member}"
- perms:
- list:
- add_reactions: Ajouter des réactions
- administrator: Administrateur
- attach_files: Joindre des fichiers
- ban_members: Bannir des membres
- change_nickname: Changer de pseudo
- connect: Se connecter
- create_instant_invite: Créer une invitation
- deafen_members: Mettre en sourdine des membres
- embed_links: Intégrer des liens
- external_emojis: Utiliser des émojis externes
- kick_members: Expulser des membres
- manage_channels: Gérer les salons
- manage_emojis: Gérer les émojis
- manage_guild: Gérer le serveur
- manage_messages: Gérer les messages
- manage_nicknames: Gérer les pseudos
- manage_roles: Gérer les rôles
- manage_webhooks: Gérer les webhooks
- mention_everyone: Mentionner everyone, here et tous les rôles
- move_members: Déplacer des membres
- mute_members: Couper le micro de membres
- priority_speaker: Voix prioritaire
- read_message_history: Voir les anciens messages
- read_messages: Voir le salon
- request_to_speak: Demander la parole
- send_messages: Envoyer des messages
- send_tts_messages: Envoyer des messages TTS
- speak: Parler
- stream: Utiliser la vidéo
- use_slash_commands: Utiliser les commandes slash
- use_voice_activation: Utiliser la Détection de la voix
- view_audit_log: Voir les logs du serveur
- view_guild_insights: Voir les analyses de serveur
sconfig:
invalid-emoji: Cet émoji est invalide. S'il s'agit d'un émoji custom, assurez-vous que je sois dans son serveur
invalid-duration: Durée invalide
diff --git a/plugins/__template__/__template__.py b/plugins/__template__/__template__.py
new file mode 100644
index 00000000..e165433b
--- /dev/null
+++ b/plugins/__template__/__template__.py
@@ -0,0 +1,28 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import tasks, commands
+from utils import Gunibot, MyContext
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Template(bot))
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
+class Template(commands.Cog):
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.file = "template"
+
+ @commands.command(name="hello")
+ @commands.guild_only()
+ async def hello(self, ctx: MyContext):
+ await ctx.send(await self.bot._(ctx.guild.id, "template.hello"))
diff --git a/plugins/__template__/bot/main.py b/plugins/__template__/bot/main.py
deleted file mode 100644
index d26cd68e..00000000
--- a/plugins/__template__/bot/main.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import discord
-from discord.ext import tasks, commands
-from utils import Gunibot, MyContext
-
-
-class Template(commands.Cog):
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.file = "template"
-
- @commands.command(name="hello")
- @commands.guild_only()
- async def hello(self, ctx: MyContext):
- await ctx.send(self.bot._(ctx.guild.id, 'template.hello'))
-
-async def setup(bot):
- await bot.add_cog(Template(bot))
diff --git a/plugins/__template__/credits.md b/plugins/__template__/credits.md
new file mode 100644
index 00000000..48f99531
--- /dev/null
+++ b/plugins/__template__/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © Aeris One 2022
+Copyright © ascpial 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/__template__/langs/en.yml b/plugins/__template__/langs/en.yml
index d896638f..27733cd4 100644
--- a/plugins/__template__/langs/en.yml
+++ b/plugins/__template__/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
template:
- hello: "Hello World!"
\ No newline at end of file
+ hello: "Hello World!"
diff --git a/plugins/__template__/langs/fr.yml b/plugins/__template__/langs/fr.yml
index 16588b7b..6e6f6078 100644
--- a/plugins/__template__/langs/fr.yml
+++ b/plugins/__template__/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
template:
- hello: "Bonjour Monde !"
\ No newline at end of file
+ hello: "Bonjour Monde !"
diff --git a/plugins/admin/bot/main.py b/plugins/admin/admin.py
similarity index 56%
rename from plugins/admin/bot/main.py
rename to plugins/admin/admin.py
index 3b12d359..5677c5f1 100644
--- a/plugins/admin/bot/main.py
+++ b/plugins/admin/admin.py
@@ -1,3 +1,15 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import Gunibot
+from git import Repo, exc
+from discord.ext import commands
+import discord
+from bot import checks
import io
import os
import sys
@@ -6,44 +18,40 @@
from contextlib import redirect_stdout
sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.ext import commands
-from git import Repo, exc
-from utils import Gunibot
def cleanup_code(content):
"""Automatically removes code blocks from the code."""
# remove ```py\n```
- if content.startswith('```') and content.endswith('```'):
- return '\n'.join(content.split('\n')[1:-1])
+ if content.startswith("```") and content.endswith("```"):
+ return "\n".join(content.split("\n")[1:-1])
# remove `foo`
- return content.strip('` \n')
+ return content.strip("` \n")
class Admin(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self._last_result = None
- @commands.group(name='admin', hidden=True)
+ @commands.group(name="admin", hidden=True)
@commands.check(checks.is_bot_admin)
async def main_msg(self, ctx: commands.Context):
"""Commandes réservées aux administrateurs de GuniBot"""
if ctx.subcommand_passed is None:
text = "Liste des commandes disponibles :"
for cmd in sorted(self.main_msg.commands, key=lambda x: x.name):
- text += "\n- {} *({})*".format(cmd.name,
- '...' if cmd.help is None else cmd.help.split('\n')[0])
- if type(cmd) == commands.core.Group:
+ text += "\n- {} *({})*".format(
+ cmd.name, "..." if cmd.help is None else cmd.help.split("\n")[0]
+ )
+ if isinstance(cmd, commands.core.Group):
for cmds in cmd.commands:
text += "\n - {} *({})*".format(
- cmds.name, cmds.help.split('\n')[0])
+ cmds.name, cmds.help.split("\n")[0]
+ )
await ctx.send(text)
- @main_msg.command(name='pull')
+ @main_msg.command(name="pull")
async def gitpull(self, ctx: commands.Context, branch: str = None):
"""Tire des changements de GitLab"""
m = await ctx.send("Mise à jour depuis gitlab...")
@@ -54,44 +62,55 @@ async def gitpull(self, ctx: commands.Context, branch: str = None):
repo.git.checkout(branch)
except exc.GitCommandError as e:
self.bot.log.exception(e)
- if 'Your local changes to the following files would be overwritten by checkout' in str(e):
- await m.edit(content=m.content+"\nCertains fichiers ont été modifiés localement - abandon de la procédure")
+ if (
+ "Your local changes to the following files would be overwritten by checkout"
+ in str(e)
+ ):
+ await m.edit(
+ content=m.content
+ + "\nCertains fichiers ont été modifiés localement - abandon de la procédure"
+ )
else:
- await m.edit(content=m.content+"\nNom de branche invalide - abandon de la procédure")
+ await m.edit(
+ content=m.content
+ + "\nNom de branche invalide - abandon de la procédure"
+ )
return
else:
- await m.edit(content=m.content+f"\nBranche {branch} correctement sélectionnée")
+ await m.edit(
+ content=m.content + f"\nBranche {branch} correctement sélectionnée"
+ )
origin = repo.remotes.origin
origin.pull()
await self.restart_bot(ctx)
-
- @main_msg.command(name="branches", aliases=['branch-list'])
+
+ @main_msg.command(name="branches", aliases=["branch-list"])
async def git_branches(self, ctx: commands.Context):
"""Montre la liste des branches disponibles"""
repo = Repo(os.getcwd())
- branches = repo.git.branch('-r').split('\n')
- branches = [x.strip().replace('origin/','') for x in branches[1:]]
- await ctx.send("Liste des branches : "+" ".join(branches))
+ branches = repo.git.branch("-r").split("\n")
+ branches = [x.strip().replace("origin/", "") for x in branches[1:]]
+ await ctx.send("Liste des branches : " + " ".join(branches))
- @main_msg.command(name='shutdown')
+ @main_msg.command(name="shutdown")
async def shutdown(self, ctx: commands.Context):
"""Eteint le bot"""
m = await ctx.send("Nettoyage de l'espace de travail...")
await self.cleanup_workspace()
await m.edit(content="Bot en voie d'extinction")
- await self.bot.change_presence(status=discord.Status('offline'))
+ await self.bot.change_presence(status=discord.Status("offline"))
self.bot.log.info("Fermeture du bot")
await self.bot.close()
async def cleanup_workspace(self):
- for folderName, _, filenames in os.walk('..'):
+ for folderName, _, filenames in os.walk(".."):
for filename in filenames:
- if filename.endswith('.pyc'):
- os.unlink(folderName+'/'+filename)
- if folderName.endswith('__pycache__'):
+ if filename.endswith(".pyc"):
+ os.unlink(folderName + "/" + filename)
+ if folderName.endswith("__pycache__"):
os.rmdir(folderName)
- @main_msg.command(name='reboot')
+ @main_msg.command(name="reboot")
async def restart_bot(self, ctx: commands.Context):
"""Relance le bot"""
await ctx.send(content="Redémarrage en cours...")
@@ -99,25 +118,29 @@ async def restart_bot(self, ctx: commands.Context):
self.bot.log.info("Redémarrage du bot")
os.execl(sys.executable, sys.executable, *sys.argv)
- @main_msg.command(name='purge')
+ @main_msg.command(name="purge")
@commands.guild_only()
async def clean(self, ctx: commands.Context, limit: int):
"""Enleve messages"""
if not ctx.bot_permissions.manage_messages:
await ctx.send("Il me manque la permission de gérer les messages")
elif not ctx.bot_permissions.read_message_history:
- await ctx.send("Il me manque la permission de lire l'historique des messages")
+ await ctx.send(
+ "Il me manque la permission de lire l'historique des messages"
+ )
else:
await ctx.message.delete()
deleted = await ctx.channel.purge(limit=limit)
- await ctx.send('{} messages supprimés !'.format(len(deleted)), delete_after=3.0)
+ await ctx.send(
+ "{} messages supprimés !".format(len(deleted)), delete_after=3.0
+ )
- @main_msg.command(name='reload')
+ @main_msg.command(name="reload")
async def reload_cog(self, ctx: commands.Context, *, cog: str):
"""Recharge un module"""
cogs = cog.split(" ")
errors_cog = self.bot.get_cog("Errors")
- if len(cogs) == 1 and cogs[0] == 'all':
+ if len(cogs) == 1 and cogs[0] == "all":
cogs = sorted([x.file for x in self.bot.cogs.values()])
reloaded_cogs = list()
for cog in cogs:
@@ -129,29 +152,33 @@ async def reload_cog(self, ctx: commands.Context, *, cog: str):
await ctx.send("Cog {} was never loaded".format(cog))
except Exception as e:
await errors_cog.on_error(e, ctx)
- await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
+ await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
else:
self.bot.log.info("Module {} rechargé".format(cog))
reloaded_cogs.append(cog)
if len(reloaded_cogs) > 0:
await ctx.bot.get_cog("General").count_lines_code()
- await ctx.send("These cogs has successfully reloaded: {}".format(", ".join(reloaded_cogs)))
+ await ctx.send(
+ "These cogs has successfully reloaded: {}".format(
+ ", ".join(reloaded_cogs)
+ )
+ )
@main_msg.command(name="add_cog", hidden=True)
async def add_cog(self, ctx: commands.Context, name: str):
"""Ajouter un cog au bot"""
try:
- self.bot.load_extension("plugins."+name)
+ self.bot.load_extension("plugins." + name)
await ctx.send("Module '{}' ajouté !".format(name))
self.bot.log.info("Module {} ajouté".format(name))
except Exception as e:
await ctx.send(str(e))
- @main_msg.command(name="del_cog", aliases=['remove_cog'], hidden=True)
+ @main_msg.command(name="del_cog", aliases=["remove_cog"], hidden=True)
async def rm_cog(self, ctx: commands.Context, name: str):
"""Enlever un cog au bot"""
try:
- self.bot.unload_extension("plugins."+name)
+ self.bot.unload_extension("plugins." + name)
await ctx.send("Module '{}' désactivé !".format(name))
self.bot.log.info("Module {} désactivé".format(name))
except Exception as e:
@@ -162,38 +189,46 @@ async def cogs_list(self, ctx: commands.Context):
"""Voir la liste de tout les cogs"""
text = str()
for k, v in self.bot.cogs.items():
- text += "- {} ({}) \n".format(v.file if hasattr(v, "file") else '?', k)
+ text += "- {} ({}) \n".format(v.file if hasattr(v, "file") else "?", k)
await ctx.send(text)
@main_msg.command(name="activity")
- async def change_activity(self, ctx: commands.Context, Type: str, * act: str):
+ async def change_activity(self, ctx: commands.Context, Type: str, *act: str):
"""Change l'activité du bot (play, watch, listen, stream)"""
act = " ".join(act)
- if Type in ['game', 'play', 'playing']:
+ if Type in ["game", "play", "playing"]:
await self.bot.change_presence(activity=discord.Game(name=act))
- elif Type in ['watch', 'see', 'watching']:
- await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=act))
- elif Type in ['listen', 'listening']:
- await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=act))
- elif Type in ['stream']:
- await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.streaming, name=act))
+ elif Type in ["watch", "see", "watching"]:
+ await self.bot.change_presence(
+ activity=discord.Activity(type=discord.ActivityType.watching, name=act)
+ )
+ elif Type in ["listen", "listening"]:
+ await self.bot.change_presence(
+ activity=discord.Activity(type=discord.ActivityType.listening, name=act)
+ )
+ elif Type in ["stream"]:
+ await self.bot.change_presence(
+ activity=discord.Activity(type=discord.ActivityType.streaming, name=act)
+ )
else:
- await ctx.send("Sélectionnez *play*, *watch*, *listen* ou *stream* suivi du nom")
+ await ctx.send(
+ "Sélectionnez *play*, *watch*, *listen* ou *stream* suivi du nom"
+ )
await ctx.message.delete()
- @main_msg.command(name='eval')
+ @main_msg.command(name="eval")
@commands.check(checks.is_bot_admin)
async def _eval(self, ctx: commands.Context, *, body: str):
"""Evaluates a code
Credits: Rapptz (https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/admin.py)"""
env = {
- 'bot': self.bot,
- 'ctx': ctx,
- 'channel': ctx.channel,
- 'author': ctx.author,
- 'guild': ctx.guild,
- 'message': ctx.message,
- '_': self._last_result
+ "bot": self.bot,
+ "ctx": ctx,
+ "channel": ctx.channel,
+ "author": ctx.author,
+ "guild": ctx.guild,
+ "message": ctx.message,
+ "_": self._last_result,
}
env.update(globals())
@@ -202,30 +237,34 @@ async def _eval(self, ctx: commands.Context, *, body: str):
try:
to_compile = f'async def func():\n{textwrap.indent(body, " ")}'
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e, ctx)
+ await self.bot.get_cog("Errors").on_error(e, ctx)
return
try:
exec(to_compile, env)
except Exception as e:
- return await ctx.send(f'```py\n{e.__class__.__name__}: {e}\n```')
+ return await ctx.send(f"```py\n{e.__class__.__name__}: {e}\n```")
- func = env['func']
+ func = env["func"]
try:
with redirect_stdout(stdout):
ret = await func()
except Exception as e:
value = stdout.getvalue()
- await ctx.send(f'```py\n{value}{traceback.format_exc()}\n```')
+ await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```")
else:
value = stdout.getvalue()
if ret is None:
if value:
- await ctx.send(f'```py\n{value}\n```')
+ await ctx.send(f"```py\n{value}\n```")
else:
self._last_result = ret
- await ctx.send(f'```py\n{value}{ret}\n```')
-
+ await ctx.send(f"```py\n{value}{ret}\n```")
-async def setup(bot: Gunibot):
- await bot.add_cog(Admin(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Admin(bot), icon="🚨")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
\ No newline at end of file
diff --git a/plugins/admin/credits.md b/plugins/admin/credits.md
new file mode 100644
index 00000000..79bf54bb
--- /dev/null
+++ b/plugins/admin/credits.md
@@ -0,0 +1,8 @@
+Copyright © ZRunner 2020
+Copyright © Leirof 2021 - 2022
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/antikikoo/antikikoo.py b/plugins/antikikoo/antikikoo.py
new file mode 100644
index 00000000..1b452b2c
--- /dev/null
+++ b/plugins/antikikoo/antikikoo.py
@@ -0,0 +1,231 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import Gunibot, MyContext
+from discord.ext import commands
+from discord.channel import TextChannel
+import discord
+from bot import checks
+import sys
+
+sys.path.append("./bot")
+
+WELCOME_MESSAGE = """(FR) Bienvenue sur {server} {user} !
+Vous n'avez accès qu'au salon Lobby pour le moment. Pour débloquer l'accès au reste du Discord, lisez les instructions présentes dans le salon {channel} :wink:
+
+(EN) Welcome to {server} {user} !
+You only have access to the Lobby channel. To unlock the acess to the rest of our Discord, please follow the instructions in the {channel} channel :wink:"""
+
+CONFIRM_MESSAGE = """{user} a lu {channel}
+
+{user} read {channel}"""
+
+
+class Antikikoo(commands.Cog):
+ """Prevents kikoos from entering the server"""
+
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.config_options = [
+ "verification_channel",
+ "info_channel",
+ "pass_message",
+ "verification_add_role",
+ "verification_info_message",
+ "verification_role",
+ ]
+
+ bot.get_command("config").add_command(self.ak_channel)
+ bot.get_command("config").add_command(self.ak_msg)
+ bot.get_command("config").add_command(self.pass_message)
+ bot.get_command("config").add_command(self.info_channel)
+ bot.get_command("config").add_command(self.verification_role)
+ bot.get_command("config").add_command(self.verification_add_role)
+
+ @commands.Cog.listener()
+ async def on_member_join(self, member: discord.Member):
+ """Called when a member joins a guild
+ Sends a message in the verification channel to inform new users"""
+ self.bot.log.info(f"{member} ({member.id}) joined the server")
+ config = self.bot.server_configs[member.guild.id]
+ # if nothing has been configured
+ if (
+ config["verification_channel"] is None
+ or config["verification_info_message"] == "None"
+ ):
+ return
+ verif_channel: TextChannel = self.bot.get_channel(
+ config["verification_channel"]
+ )
+ info_channel = "<#{}>".format(config["info_channel"])
+ # if config is None, we use the default one
+ welcome_msg: str = config["verification_info_message"] or WELCOME_MESSAGE
+ await verif_channel.send(
+ welcome_msg.format(
+ user=member.mention, channel=info_channel, server=member.guild.name
+ )
+ )
+
+ @commands.Cog.listener()
+ async def on_message(self, message: discord.Message):
+ """Called for every new message
+ We use it to check when someone send the verification message"""
+ if message.guild is None: # if the message is not in a server
+ return
+ config = self.bot.server_configs[message.guild.id]
+ if message.channel.id != config["verification_channel"]:
+ return
+
+ if config["pass_message"] is None: # not set
+ return
+
+ info_channel = "<#{}>".format(config["info_channel"])
+ if message.content.lower() == config["pass_message"].lower():
+ emb = discord.Embed(
+ description=CONFIRM_MESSAGE.format(
+ user=message.author.mention, channel=info_channel
+ )
+ )
+ await message.channel.send(embed=emb)
+ try:
+ await message.delete()
+ except BaseException:
+ self.bot.log.exception("Cannot delete the verification message")
+ verif_role = message.guild.get_role(config["verification_role"])
+ if verif_role is None:
+ return
+ try:
+ if config["verification_add_role"]:
+ await message.author.add_roles(verif_role)
+ else:
+ await message.author.remove_roles(verif_role)
+ except BaseException:
+ self.bot.log.exception(
+ f"Cannot give or take away verification role from member {message.author}"
+ )
+
+ @commands.group(name="antikikoo", aliases=["ak", "antitroll"])
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def ak_main(self, ctx: MyContext):
+ """Kikoo filter configuration"""
+ if ctx.subcommand_passed is None:
+ await ctx.send_help("antikikoo")
+
+ @ak_main.command(name="verification_channel")
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def ak_channel(self, ctx: MyContext, channel: discord.TextChannel):
+ """Modifies the channel where members will have to check themselves"""
+ self.bot.server_configs[ctx.guild.id]["verification_channel"] = channel.id
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "antikikoo.channel-edited", channel=channel.mention
+ )
+ )
+
+ @ak_main.command(name="verification_info_message")
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def ak_msg(self, ctx: MyContext, *, message: str = None):
+ """Modifies the informative message sent in the verification channel
+ Put nothing to reset it, or "None" for no message"""
+ if message.lower() == "none":
+ value = "None" # no message
+ self.bot.server_configs[ctx.guild.id]["verification_info_message"] = message
+ await ctx.send(await self.bot._(ctx.guild.id, "antikikoo.msg-edited"))
+
+ @commands.command(name='pass_message')
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def pass_message(
+ self,
+ context: MyContext,
+ message: str,
+ ):
+ """Set the pass message required to enter the server."""
+ # because of the check above, we don't need to check again
+ config = self.bot.server_configs[context.guild.id]
+ config['pass_message'] = message
+ await context.send(
+ await self.bot._(
+ context.guild.id, "antikikoo.pass-edited",
+ )
+ )
+
+ @commands.command(name='info_channel')
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def info_channel(
+ self,
+ context: MyContext,
+ channel: discord.TextChannel,
+ ):
+ """Change the channel where users can read more informations about the rules."""
+ # because of the check above, we don't need to check again
+ config = self.bot.server_configs[context.guild.id]
+ config['info_channel'] = channel.id
+ await context.send(
+ await self.bot._(
+ context.guild.id,
+ "antikikoo.info-channel-edited",
+ channel=channel.mention,
+ )
+ )
+
+ @commands.command(name='verification_role')
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def verification_role(
+ self,
+ context: MyContext,
+ role: discord.Role,
+ ):
+ """Set the role given by the bot when the user gets verified.
+ Use the command "config verification_add_role" to toggle on or off.
+ """
+ config = self.bot.server_configs[context.guild.id]
+ config['verification_role'] = role.id
+ await context.send(
+ await self.bot._(
+ context.guild.id,
+ "antikikoo.role-edited",
+ role=role.mention,
+ ),
+ allowed_mentions=discord.AllowedMentions.none(),
+ )
+
+ @commands.command(name='verification_add_role')
+ @commands.guild_only()
+ @commands.has_permissions(administrator=True)
+ async def verification_add_role(
+ self,
+ context: MyContext,
+ enabled: bool = True,
+ ):
+ """Enable or disable the give role feature of the verification system.
+ """
+ config = self.bot.server_configs[context.guild.id]
+ config['verification_add_role'] = enabled
+ role = context.guild.get_role(config['verification_role'])
+ await context.send(
+ await self.bot._(
+ context.guild.id,
+ "antikikoo.add-role-enabled" if enabled else "antikikoo.add-role-disabled",
+ role=role.mention,
+ ),
+ allowed_mentions=discord.AllowedMentions.none(),
+ )
+
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Antikikoo(bot), icon="⛔")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/antikikoo/bot/main.py b/plugins/antikikoo/bot/main.py
deleted file mode 100644
index e195bf2c..00000000
--- a/plugins/antikikoo/bot/main.py
+++ /dev/null
@@ -1,96 +0,0 @@
-import sys
-sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.channel import TextChannel
-from discord.ext import commands
-from utils import Gunibot, MyContext
-
-WELCOME_MESSAGE = """(FR) Bienvenue sur {server} {user} !
-Vous n'avez accès qu'au salon Lobby pour le moment. Pour débloquer l'accès au reste du Discord, lisez les instructions présentes dans le salon {channel} :wink:
-
-(EN) Welcome to {server} {user} !
-You only have access to the Lobby channel. To unlock the acess to the rest of our Discord, please follow the instructions in the {channel} channel :wink:"""
-
-CONFIRM_MESSAGE = """{user} a lu {channel}
-
-{user} read {channel}"""
-
-
-class Antikikoo(commands.Cog):
- """Prevents kikoos from entering the server"""
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.config_options = ["verification_channel", "info_channel", "pass_message", "verification_add_role", "verification_info_message", "verification_role"]
-
- @commands.Cog.listener()
- async def on_member_join(self, member: discord.Member):
- """Called when a member joins a guild
- Sends a message in the verification channel to inform new users"""
- self.bot.log.info(f"{member} ({member.id}) joined the server")
- config = self.bot.server_configs[member.guild.id]
- if config["verification_channel"] is None or config["verification_info_message"] == "None": # if nothing has been configured
- return
- verif_channel: TextChannel = self.bot.get_channel(config["verification_channel"])
- info_channel = "<#{}>".format(config["info_channel"])
- welcome_msg: str = config["verification_info_message"] or WELCOME_MESSAGE # if config is None, we use the default one
- await verif_channel.send(welcome_msg.format(user=member.mention, channel=info_channel, server=member.guild.name))
-
- @commands.Cog.listener()
- async def on_message(self, message: discord.Message):
- """Called for every new message
- We use it to check when someone send the verification message"""
- if message.guild is None: # if the message is not in a server
- return
- config = self.bot.server_configs[message.guild.id]
- if message.channel.id != config["verification_channel"]:
- return
- info_channel = "<#{}>".format(config["info_channel"])
- if message.content.lower() == config["pass_message"].lower():
- emb = discord.Embed(description=CONFIRM_MESSAGE.format(
- user=message.author.mention, channel=info_channel))
- await message.channel.send(embed=emb)
- try:
- await message.delete()
- except:
- self.bot.log.exception("Cannot delete the verification message")
- verif_role = message.guild.get_role(config["verification_role"])
- if verif_role == None:
- return
- try:
- if config["verification_add_role"]:
- await message.author.add_roles(verif_role)
- else:
- await message.author.remove_roles(verif_role)
- except:
- self.bot.log.exception(
- f"Cannot give or take away verification role from member {message.author}")
-
- @commands.group(name="antikikoo", aliases=["ak", "antitroll"])
- @commands.guild_only()
- async def ak_main(self, ctx: MyContext):
- """Kikoo filter configuration"""
- if ctx.subcommand_passed is None:
- await ctx.send_help('antikikoo')
-
- @ak_main.command(name="channel")
- @commands.check(checks.is_admin)
- async def ak_channel(self, ctx: MyContext, channel: discord.TextChannel):
- """Modifies the channel where members will have to check themselves"""
- self.bot.server_configs[ctx.guild.id]["verification_channel"] = channel.id
- await ctx.send(await self.bot._(ctx.guild.id, "antikikoo.channel-edited", channel=channel.mention))
-
- @ak_main.command(name="info_message")
- @commands.check(checks.is_admin)
- async def ak_msg(self, ctx: MyContext, *, message: str = None):
- """Modifies the informative message sent in the verification channel
- Put nothing to reset it, or "None" for no message"""
- if message.lower() == "none":
- value = "None" # no message
- self.bot.server_configs[ctx.guild.id]["verification_info_message"] = message
- await ctx.send(await self.bot._(ctx.guild.id, "antikikoo.msg-edited"))
-
-
-async def setup(bot):
- await bot.add_cog(Antikikoo(bot))
diff --git a/plugins/antikikoo/credits.md b/plugins/antikikoo/credits.md
new file mode 100644
index 00000000..48f99531
--- /dev/null
+++ b/plugins/antikikoo/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © Aeris One 2022
+Copyright © ascpial 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/antikikoo/docs/user_documentation.md b/plugins/antikikoo/docs/user_documentation.md
index 85a81c6f..39d519ba 100644
--- a/plugins/antikikoo/docs/user_documentation.md
+++ b/plugins/antikikoo/docs/user_documentation.md
@@ -1,3 +1,11 @@
-# Anti-kikoo
+
+
+# 🛑 Anti-kikoo
+
+Nothing here... for the moment.
-Nothing here...
diff --git a/plugins/antikikoo/langs/en.yml b/plugins/antikikoo/langs/en.yml
index d432d4eb..7b872253 100644
--- a/plugins/antikikoo/langs/en.yml
+++ b/plugins/antikikoo/langs/en.yml
@@ -1,4 +1,14 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
antikikoo:
channel-edited: The verification channel is now %{channel}!
- msg-edited: The information message has been successfully modified!
\ No newline at end of file
+ msg-edited: The information message has been successfully modified!
+ pass-edited: The message pass has been successfully modified!
+ info-channel-edited: The information channel is now %{channel}!
+ role-edited: The verification role is now %{role}!
+ add-role-enabled: The role %{role} will be given to verified users!
+ add-role-disabled: No role will be given to verified users.
\ No newline at end of file
diff --git a/plugins/antikikoo/langs/fr.yml b/plugins/antikikoo/langs/fr.yml
index 8e86cc46..f88cb2e6 100644
--- a/plugins/antikikoo/langs/fr.yml
+++ b/plugins/antikikoo/langs/fr.yml
@@ -1,4 +1,14 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
antikikoo:
channel-edited: Le salon de vérification est maintenant %{channel} !
- msg-edited: Le message d'information a bien été modifié !
\ No newline at end of file
+ msg-edited: Le message d'information a bien été modifié !
+ pass-edited: Les message de pass a bien été modifié !
+ info-channel-edited: Le salon d'informations est maintenant %{channel} !
+ role-edited: Le rôle de vérification est maintenant paramétré sur %{role} !
+ add-role-enabled: Le rôle %{role} va être donné aux utilisateurs vérifiés !
+ add-role-disabled: Aucun rôle ne va être donné aux utilisateurs vérifiés.
\ No newline at end of file
diff --git a/plugins/ban/ban.py b/plugins/ban/ban.py
new file mode 100644
index 00000000..01cecb3c
--- /dev/null
+++ b/plugins/ban/ban.py
@@ -0,0 +1,311 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import importlib
+import random
+
+import discord.abc
+import discord
+from discord.ext import commands
+from utils import Gunibot, MyContext
+
+from core import config
+
+class Ban(commands.Cog):
+ friendly_ban_guilds: list[int]
+
+ friendly_ban_events: list[dict]
+ systematic_events: list
+ random_events: list
+ friendly_ban_whitelisted_roles: list[int]
+
+ roles_backup: dict[int,list[discord.Role]]
+
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.file = "ban"
+
+ self.load_friendly_ban()
+
+ @commands.Cog.listener()
+ async def on_member_join(self, member: discord.Member):
+ if member.guild.id in self.friendly_ban_guilds:
+ if member.id in self.roles_backup:
+ # Give the roles back to the users
+
+ # setup a list of the role that could not be given back
+ forbidden: list[discord.Role] = []
+
+ for role in self.roles_backup.pop(member.id):
+ if role.id != role.guild.id and role not in member.roles: # We ignore the @everyone role
+ try:
+ await member.add_roles(role)
+ except discord.Forbidden:
+ forbidden.append(role)
+
+ # send a message to the user if some roles could not be given back
+ if len(forbidden) > 0:
+ await member.send(
+ (await self.bot._(member, "ban.gunivers.missing_roles") \
+ + ", ".join([role.name for role in forbidden]))[:2000]
+ )
+
+ async def ban_perm_check(ctx: commands.Context) -> bool:
+ """Checks if the user has the permission to ban"""
+
+ self: Ban = ctx.bot.get_cog("Ban")
+
+ if ctx.guild.id not in self.friendly_ban_guilds:
+ return await commands.has_guild_permissions(ban_members=True).predicate(ctx)
+ else:
+ for role in ctx.author.roles:
+ if role.id in self.friendly_ban_whitelisted_roles:
+ return True
+
+ return await commands.has_guild_permissions(ban_members=True).predicate(ctx)
+
+ async def fake_ban_guild_check(ctx: commands.Context) -> bool:
+ """Checks if the guild is configured for the friendly ban command"""
+
+ self: Ban = ctx.bot.get_cog("Ban")
+
+ return ctx.guild.id in self.friendly_ban_guilds
+
+ # ------------------#
+ # Commande /ban #
+ # ------------------#
+
+ @commands.command(name="ban")
+ @commands.guild_only()
+ @commands.check(ban_perm_check)
+ async def ban(
+ self,
+ ctx: MyContext,
+ user: discord.User,
+ reason: str = "Aucune raison donnée",
+ ):
+ "Banhammer. Use at your own risk."
+ if user == ctx.author and not ctx.guild.id in self.friendly_ban_guilds:
+ await ctx.send("Tu ne peux pas te bannir toi-même !")
+ return
+ if not ctx.guild.me.guild_permissions.ban_members:
+ await ctx.send("Permission 'Bannir des membres' manquante :confused:")
+ return
+ member = ctx.guild.get_member(user.id)
+ if (
+ member is not None
+ and member.roles[-1].position >= ctx.guild.me.roles[-1].position
+ ):
+ await ctx.send(
+ "Mon rôle n'est pas assez haut pour bannir cet individu :confused:"
+ )
+ return
+
+ # Normal ban
+ if not ctx.guild.id in self.friendly_ban_guilds:
+ try:
+ await ctx.guild.ban(
+ user,
+ delete_message_days=0,
+ reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}",
+ )
+ except discord.Forbidden:
+ await ctx.send(
+ "Permissions manquantes :confused: (vérifiez la hiérarchie)"
+ )
+ else:
+ await ctx.send(f"{user} a bien été banni !")
+ await ctx.send("https://thumbs.gfycat.com/LikelyColdBasil-small.gif")
+ return
+
+ # Friendly ban if the guild is in the config
+ else:
+ for event in self.systematic_events:
+ # you should note that systematic events can return None to
+ # indicate that they should be ignored
+ if await event(self, ctx, user, reason):
+ return
+
+ # Pick a random event and execute it if no systematic event has been executed
+ # random events should always run successfully
+ await random.choice(self.random_events)(self, ctx, user, reason)
+
+ # ------------------#
+ # Commande /rban #
+ # ------------------#
+ # Because it may be useful to ban nonetheless
+
+ @commands.command(name="rban")
+ @commands.guild_only()
+ @commands.check(fake_ban_guild_check)
+ @commands.has_guild_permissions(ban_members=True)
+ async def rban(
+ self,
+ ctx: MyContext,
+ user: discord.User,
+ reason: str = "Aucune raison donnée",
+ ):
+ "Bans a user. If you really don't like his face."
+ if ctx.guild.id == 125723125685026816 or ctx.guild.id == 689159304049197131:
+ if user == ctx.author:
+ await ctx.send("Tu ne peux pas te bannir toi-même abruti !")
+ return
+ if not ctx.guild.me.guild_permissions.ban_members:
+ await ctx.send(
+ "Permission 'Bannir des membres' manquante, c'est con :confused:"
+ )
+ return
+ member = ctx.guild.get_member(user.id)
+ if (
+ member is not None
+ and member.roles[-1].position >= ctx.guild.me.roles[-1].position
+ ):
+ await ctx.send(
+ "Mon rôle n'est pas assez haut pour bannir cet individu :confused:"
+ )
+ return
+ try:
+ await ctx.guild.ban(
+ user,
+ delete_message_days=0,
+ reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}",
+ )
+ except discord.Forbidden:
+ await ctx.send(
+ "Permissions manquantes :confused: (vérifiez la hiérarchie)"
+ )
+ else:
+ await ctx.send(f"{user} a bien été banni !")
+ await ctx.send("https://thumbs.gfycat.com/LikelyColdBasil-small.gif")
+
+ def load_friendly_ban(self):
+ """Loads configuration and events for the friendly ban command"""
+
+ # look for the configuration, gets an empty dict if it doesn't exist
+ self.config = config.get('ban') or {}
+
+ # loads the guild ids from the configuration
+ self.friendly_ban_guilds: list[int] = self.config.get("guilds", [])
+
+ self.friendly_ban_whitelisted_roles: list[int] = self.config.get(
+ "whitelisted_roles",
+ []
+ )
+
+ # loads the events
+ self.friendly_ban_events = [
+ {
+ "name": "Autoban?",
+ "chances": None, # systematic
+ "module_name": "autoban"
+ },
+ {
+ "name": "Baldban",
+ "chances": None, # systematic
+ "module_name": "baldban"
+ },
+ {
+ "name": "UnoReverse",
+ "chances": 1,
+ "module_name": "reverse"
+ },
+ {
+ "name": "Bothban",
+ "chances": 1,
+ "module_name": "bothban"
+ },
+ {
+ "name": "Rickroll",
+ "chances": 1,
+ "module_name": "rickroll"
+ },
+ {
+ "name": "Normal ban",
+ "chances": 7,
+ "module_name": "just_a_message"
+ }
+ ]
+ self.systematic_events: list[function] = []
+ self.random_events: list[function] = []
+
+ for event in self.friendly_ban_events:
+ chances = event.get("chances", None)
+ if chances is None:
+ self.systematic_events.append(
+ importlib.import_module(
+ f"plugins.ban.events.{event['module_name']}"
+ ).execute
+ )
+ else:
+ for _ in range(chances):
+ self.random_events.append(
+ importlib.import_module(
+ f"plugins.ban.events.{event['module_name']}"
+ ).execute
+ )
+
+ # initiate the cache for the banned users roles
+ self.roles_backup: dict[int,list[discord.Role]] = {}
+
+ async def fake_ban(
+ self,
+ ctx: commands.Context,
+ user: discord.User,
+ show_error: bool = True,
+ ) -> bool:
+ """Friendly ban a user
+ If the ban doesn't succeed, returns False
+ ctx: the context used to send the error message if necessary
+ user: the user to ban
+ show_error: whether to show an error message if the ban fails
+ """
+
+ if isinstance(ctx.channel, discord.Thread):
+ channel = ctx.channel.parent
+ else:
+ channel = ctx.channel
+
+ # send the invitation to allow the user to rejoin the guild
+ try:
+ invitation = await channel.create_invite(
+ reason="Friendly ban",
+ max_uses=1,
+ unique=True,
+ )
+ except discord.Forbidden:
+ if show_error:
+ await ctx.send(await ctx.bot._(ctx, 'ban.gunivers.whoups'))
+
+ try:
+ invite_message = await user.send(
+ invitation
+ )
+ except discord.Forbidden:
+ if show_error:
+ await ctx.send(await ctx.bot._(ctx, 'ban.gunivers.whoups'))
+
+ # store the roles somewhere to give them back to the user
+ self.roles_backup[user.id] = ctx.guild.get_member(
+ user.id
+ ).roles
+
+ try:
+ await ctx.guild.kick(user, reason=f"Auto-ban!")
+ except discord.Forbidden:
+ if show_error:
+ await ctx.send(
+ await ctx.bot._(ctx, "ban.gunivers.missing_permissions")
+ )
+ await invite_message.edit(
+ content=await ctx.bot._(ctx, 'ban.gunivers.urbetter')
+ )
+ return False
+ return True
+
+# The end.
+async def setup(bot:Gunibot=None):
+ await bot.add_cog(Ban(bot), icon="🔨")
\ No newline at end of file
diff --git a/plugins/ban/bot/main.py b/plugins/ban/bot/main.py
deleted file mode 100644
index 065b96bb..00000000
--- a/plugins/ban/bot/main.py
+++ /dev/null
@@ -1,238 +0,0 @@
-import random
-
-import discord.abc
-import discord
-from discord.ext import commands
-from utils import Gunibot, MyContext
-
-specialGuilds = [125723125685026816,689159304049197131,835218602511958116]
-altearn = 835218602511958116
-gunivers = 125723125685026816
-curiosity = 689159304049197131
-altearnInvite = "https://discord.gg/uS9cXuyeFQ"
-guniversInvite = "https://discord.gg/E8qq6tN"
-curiosityInvite = "https://discord.gg/jtntCqXz53"
-banRolesDict = {}
-
-
-class Ban(commands.Cog):
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.file = "ban"
-
- @commands.Cog.listener()
- async def on_member_join(self, member):
- global banRolesDict
- # Pourquoi global ? Pour avoir un accès en écriture et pouvoir pop
-
- if member.guild.id in specialGuilds:
- if member.id in banRolesDict:
- # On pop pour ne pas garder inutilement la liste des rôles dans le dictionnaire
- for role in banRolesDict.pop(member.id):
- if role.name != "@everyone":
- await member.add_roles(role)
-
-
-
-
- # ------------------#
- # Commande /ban #
- # ------------------#
-
- @commands.command(name="ban")
- @commands.guild_only()
- @commands.has_guild_permissions(ban_members=True)
- async def ban(self, ctx: MyContext, *, user: discord.User, reason: str = "Aucune raison donnée"):
- # On accède au dictionnaire des roles
- global banRolesDict
-
- if user == ctx.author and not ctx.guild.id in specialGuilds:
- await ctx.send("Tu ne peux pas te bannir toi-même !")
- return
- if not ctx.guild.me.guild_permissions.ban_members:
- await ctx.send("Permission 'Bannir des membres' manquante :confused:")
- return
- member = ctx.guild.get_member(user.id)
- if member is not None and member.roles[-1].position >= ctx.guild.me.roles[-1].position:
- await ctx.send("Mon rôle n'est pas assez haut pour bannir cet individu :confused:")
- return
- if not ctx.guild.id in specialGuilds:
- try:
- await ctx.guild.ban(user, delete_message_days=0,
- reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- await ctx.send(f"{user} a bien été banni !")
- await ctx.send("https://thumbs.gfycat.com/LikelyColdBasil-small.gif")
- return
-
- # GUNIVERS/CURIOSITY SPECIAL CASES
- else:
- # auto-ban, special Laizo
- if user == ctx.author:
- if ctx.guild.id == gunivers:
- await ctx.author.send(f"{guniversInvite}")
- if ctx.guild.id == curiosity:
- await ctx.author.send(f"{curiosityInvite}")
- if ctx.guild.id == altearn:
- await ctx.author.send(f"{altearnInvite}")
- banRolesDict[user.id] = ctx.guild.get_member(user.id).roles
- try:
- await ctx.guild.kick(user, reason=f"Auto-ban!")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- # Find and send some random message
- choice = random.randint(0, 2)
- msg = await self.bot._(ctx.channel, f"ban.gunivers.autoban.{choice}")
- await ctx.send(msg.format(ctx.author.mention, user.mention))
- await ctx.send("https://thumbs.gfycat.com/CompleteLeafyAardwolf-size_restricted.gif")
- return
-
- # 1/10th chance of banning the command executor instead, Uno Reverse event.
- if random.randint(1, 10) == 1:
- if ctx.guild.id == gunivers:
- await ctx.author.send(f"{guniversInvite}")
- if ctx.guild.id == curiosity:
- await ctx.author.send(f"{curiosityInvite}")
- if ctx.guild.id == altearn:
- await ctx.author.send(f"{altearnInvite}")
- banRolesDict[ctx.author] = ctx.author.roles
- try:
- await ctx.guild.kick(ctx.author,
- reason=f"Banned by himself. Reason : {user} ({user.id}) used Uno Reverse card.")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- # Find and send some random message
- choice = random.randint(0, 3)
- msg = await self.bot._(ctx.channel, f"ban.gunivers.selfban.{choice}")
- await ctx.send(msg.format(ctx.author.mention, user.mention))
- await ctx.send("https://thumbs.gfycat.com/BackInsignificantAfricanaugurbuzzard-size_restricted.gif")
- return
-
- # 1/10th chance of banning both banned and executor, Bothban event.
- if random.randint(1, 10) == 1:
- if ctx.guild.id == gunivers:
- await user.send(f"{guniversInvite}")
- if ctx.guild.id == curiosity:
- await user.send(f"{curiosityInvite}")
- if ctx.guild.id == altearn:
- await user.send(f"{altearnInvite}")
- banRolesDict[user.id] = ctx.guild.get_member(user.id).roles
- banRolesDict[ctx.author.id] = ctx.author.roles
- try:
- await ctx.guild.kick(user, reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- if ctx.guild.id == gunivers:
- await ctx.author.send(f"{guniversInvite}")
- if ctx.guild.id == curiosity:
- await ctx.author.send(f"{curiosityInvite}")
- if ctx.guild.id == altearn:
- await ctx.author.send(f"{altearnInvite}")
- try:
- await ctx.guild.kick(ctx.author,
- reason=f"Banned by himself. Reason : {user} ({user.id}) banned him back.")
- except discord.Forbidden:
- # If there's an error when banning the author, we don't care and act like if a one-way ban happened.
- choice = random.randint(0, 9)
- msg = await self.bot._(ctx.channel, f"ban.gunivers.ban.{choice}")
- await ctx.send(msg.format(ctx.author.mention, user.mention))
- await ctx.send("https://thumbs.gfycat.com/PepperyEminentIndianspinyloach-size_restricted.gif")
- else:
- # If there's no error, find a random message and send it.
- choice = random.randint(0, 3)
- msg = await self.bot._(ctx.channel, f"ban.gunivers.bothban.{choice}")
- await ctx.send(msg.format(ctx.author.mention, user.mention))
- await ctx.send(
- "https://thumbs.gfycat.com/BackInsignificantAfricanaugurbuzzard-size_restricted.gif")
- return
-
- # 1/10th chance of rickrolling people, Rickroll event.
- if random.randint(1, 10) == 1:
- await self.bot._(ctx.channel, f"ban.gunivers.rickroll")
- await ctx.send(
- "Never gonna give you up,\nnever gonna let you down,\nnever gonna run around and ban you :musical_note:")
- await ctx.send("https://thumbs.gfycat.com/VengefulDesertedHalibut-size_restricted.gif")
- return
-
- # If ban is issued by Leirof, then Bald ban event.
- if ctx.author.id == 125722240896598016:
- if ctx.guild.id == gunivers:
- await user.send(f"{guniversInvite}")
- if ctx.guild.id == curiosity:
- await user.send(f"{curiosityInvite}")
- if ctx.guild.id == altearn:
- await user.send(f"{altearnInvite}")
- banRolesDict[user.id] = ctx.guild.get_member(user.id).roles
- try:
- await ctx.guild.kick(user, reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- # Find and send some random message
- choice = random.randint(0, 9)
- msg = await self.bot._(ctx.channel, f"ban.gunivers.ban.{choice}")
- await ctx.send(msg.format(ctx.author.mention, user.mention))
- await ctx.send("https://thumbs.gfycat.com/PepperyEminentIndianspinyloach-size_restricted.gif")
- await ctx.send(
- "https://media.discordapp.net/attachments/791335982666481675/979052868915064862/Chauve_qui_peut_.png")
- return
-
- # else, normal ban w/ random message
- else:
- if ctx.guild.id == gunivers:
- await user.send(f"{guniversInvite}")
- if ctx.guild.id == curiosity:
- await user.send(f"{curiosityInvite}")
- if ctx.guild.id == altearn:
- await user.send(f"{altearnInvite}")
- banRolesDict[user.id] = ctx.guild.get_member(user.id).roles
- try:
- await ctx.guild.kick(user, reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- # Find and send some random message
- choice = random.randint(0, 9)
- msg = await self.bot._(ctx.channel, f"ban.gunivers.ban.{choice}")
- await ctx.send(msg.format(ctx.author.mention, user.mention))
- await ctx.send("https://thumbs.gfycat.com/PepperyEminentIndianspinyloach-size_restricted.gif")
- return
-
- # ------------------#
- # Commande /rban #
- # ------------------#
- # Parce qu'il peut être pratique de bannir tout de même
-
- @commands.command(name="rban")
- @commands.guild_only()
- @commands.has_guild_permissions(ban_members=True)
- async def rban(self, ctx: MyContext, *, user: discord.User, reason: str = "Aucune raison donnée"):
- if ctx.guild.id == 125723125685026816 or ctx.guild.id == 689159304049197131:
- if user == ctx.author:
- await ctx.send("Tu ne peux pas te bannir toi-même abruti !")
- return
- if not ctx.guild.me.guild_permissions.ban_members:
- await ctx.send("Permission 'Bannir des membres' manquante, c'est con :confused:")
- return
- member = ctx.guild.get_member(user.id)
- if member is not None and member.roles[-1].position >= ctx.guild.me.roles[-1].position:
- await ctx.send("Mon rôle n'est pas assez haut pour bannir cet individu :confused:")
- return
- try:
- await ctx.guild.ban(user, delete_message_days=0,
- reason=f"Banned by {ctx.author} ({ctx.author.id}). Reason : {reason}")
- except discord.Forbidden:
- await ctx.send("Permissions manquantes :confused: (vérifiez la hiérarchie)")
- else:
- await ctx.send(f"{user} a bien été banni !")
- await ctx.send("https://thumbs.gfycat.com/LikelyColdBasil-small.gif")
-
-
-# The end.
-async def setup(bot):
- await bot.add_cog(Ban(bot))
diff --git a/plugins/ban/config.yaml b/plugins/ban/config.yaml
new file mode 100644
index 00000000..504f9ad3
--- /dev/null
+++ b/plugins/ban/config.yaml
@@ -0,0 +1,5 @@
+guilds:
+- 125723125685026816
+- 689159304049197131
+whitelisted_roles:
+- 968203819127279626
diff --git a/plugins/ban/config/require-example.json b/plugins/ban/config/require-example.json
new file mode 100644
index 00000000..beb6e72d
--- /dev/null
+++ b/plugins/ban/config/require-example.json
@@ -0,0 +1,42 @@
+{
+ "friendly_ban": [
+ 125723125685026816,
+ 689159304049197131,
+ 835218602511958116
+ ],
+ "events": [
+ {
+ "name": "Autoban?",
+ "chances": null,
+ "module_name": "autoban"
+ },
+ {
+ "name": "Baldban",
+ "chances": null,
+ "module_name": "baldban"
+ },
+ {
+ "name": "UnoReverse",
+ "chances": 1,
+ "module_name": "reverse"
+ },
+ {
+ "name": "Bothban",
+ "chances": 1,
+ "module_name": "bothban"
+ },
+ {
+ "name": "Rickroll",
+ "chances": 1,
+ "module_name": "rickroll"
+ },
+ {
+ "name": "Normal ban",
+ "chances": 7,
+ "module_name": "just_a_message"
+ }
+ ],
+ "whitelisted_roles": [
+ 968203819127279626
+ ]
+}
\ No newline at end of file
diff --git a/plugins/ban/credits.md b/plugins/ban/credits.md
new file mode 100644
index 00000000..714ef6ac
--- /dev/null
+++ b/plugins/ban/credits.md
@@ -0,0 +1,8 @@
+Copyright © Aeris One 2022
+Copyright © ascpial 2022 - 2023
+Copyright © Leirof 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/ban/events/autoban.py b/plugins/ban/events/autoban.py
new file mode 100644
index 00000000..cf550b0c
--- /dev/null
+++ b/plugins/ban/events/autoban.py
@@ -0,0 +1,35 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+
+import random
+
+
+async def execute(
+ ban_plugin,
+ ctx: commands.Context,
+ user: discord.User,
+ reason: str,
+) -> bool:
+ """Execute the autoban event.
+ If the event doest't succeed, the function returns False.
+ """
+
+ if ctx.author.id == user.id:
+ if await ban_plugin.fake_ban(ctx, ctx.author):
+ choice = random.randint(0, 2)
+ msg = await ctx.bot._(ctx.channel, f"ban.gunivers.autoban.{choice}")
+ await ctx.send(msg.format(ctx.author.mention, user.mention))
+ await ctx.send(
+ "https://thumbs.gfycat.com/CompleteLeafyAardwolf-size_restricted.gif"
+ )
+ return True
+
+ else:
+ return False
diff --git a/plugins/ban/events/baldban.py b/plugins/ban/events/baldban.py
new file mode 100644
index 00000000..7a083d9e
--- /dev/null
+++ b/plugins/ban/events/baldban.py
@@ -0,0 +1,26 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+
+import random
+
+
+async def execute(
+ ban_plugin,
+ ctx: commands.Context,
+ user: discord.User,
+ reason: str,
+) -> bool:
+ """If the ban is issued by Leirof, then Bald ban event"""
+ if ctx.author.id == 125722240896598016:
+ await ctx.send(
+ "https://media.discordapp.net/attachments/791335982666481675/979052868915064862/Chauve_qui_peut_.png"
+ )
+
+ return False # allow Leirof to also get reverse ban and other fun things
diff --git a/plugins/ban/events/bothban.py b/plugins/ban/events/bothban.py
new file mode 100644
index 00000000..3ace8b4f
--- /dev/null
+++ b/plugins/ban/events/bothban.py
@@ -0,0 +1,39 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+
+import random
+
+
+async def execute(
+ ban_plugin,
+ ctx: commands.Context,
+ user: discord.User,
+ reason: str,
+) -> bool:
+ """Ban both the executor and the targeted user."""
+ if await ban_plugin.fake_ban(ctx, user):
+ if await ban_plugin.fake_ban(ctx, ctx.author, False):
+ # If there's no error, find a random message and send
+ # it.
+ choice = random.randint(0, 3)
+ msg = await ctx.bot._(ctx.channel, f"ban.gunivers.bothban.{choice}")
+ await ctx.send(msg.format(ctx.author.mention, user.mention))
+ await ctx.send(
+ "https://thumbs.gfycat.com/BackInsignificantAfricanaugurbuzzard-size_restricted.gif"
+ )
+ return
+ else:
+ # we cannot ban the author, so we act as if it was a one-way ban
+ choice = random.randint(0, 9)
+ msg = await ctx.bot._(ctx.channel, f"ban.gunivers.ban.{choice}")
+ await ctx.send(msg.format(ctx.author.mention, user.mention))
+ await ctx.send(
+ "https://thumbs.gfycat.com/PepperyEminentIndianspinyloach-size_restricted.gif"
+ )
diff --git a/plugins/ban/events/just_a_message.py b/plugins/ban/events/just_a_message.py
new file mode 100644
index 00000000..d2ce9238
--- /dev/null
+++ b/plugins/ban/events/just_a_message.py
@@ -0,0 +1,28 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+
+import random
+
+
+async def execute(
+ ban_plugin,
+ ctx: commands.Context,
+ user: discord.User,
+ reason: str,
+) -> bool:
+ """Normally ban the user, with a little goodbye message"""
+ if await ban_plugin.fake_ban(ctx, user):
+ # Find and send some random message
+ choice = random.randint(0, 9)
+ msg = await ctx.bot._(ctx.channel, f"ban.gunivers.ban.{choice}")
+ await ctx.send(msg.format(ctx.author.mention, user.mention))
+ await ctx.send(
+ "https://thumbs.gfycat.com/PepperyEminentIndianspinyloach-size_restricted.gif"
+ )
diff --git a/plugins/ban/events/reverse.py b/plugins/ban/events/reverse.py
new file mode 100644
index 00000000..af0477da
--- /dev/null
+++ b/plugins/ban/events/reverse.py
@@ -0,0 +1,39 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+
+import random
+
+
+async def execute(
+ ban_plugin,
+ ctx: commands.Context,
+ user: discord.User,
+ reason: str,
+) -> bool:
+ """Reverse the ban, uno reverse card
+ The command executor is banned instead of the targeted user.
+ """
+ if await ban_plugin.fake_ban(ctx, ctx.author, False):
+ # Find and send some random message
+ choice = random.randint(0, 3)
+ msg = await ctx.bot._(ctx.channel, f"ban.gunivers.selfban.{choice}")
+ await ctx.send(msg.format(ctx.author.mention, user.mention))
+ await ctx.send(
+ "https://thumbs.gfycat.com/BackInsignificantAfricanaugurbuzzard-size_restricted.gif"
+ )
+ else:
+ # fallback to a normal ban
+ if await ban_plugin.fake_ban(ctx, user):
+ choice = random.randint(0, 9)
+ msg = await ctx.bot._(ctx.channel, f"ban.gunivers.ban.{choice}")
+ await ctx.send(msg.format(ctx.author.mention, user.mention))
+ await ctx.send(
+ "https://thumbs.gfycat.com/PepperyEminentIndianspinyloach-size_restricted.gif"
+ )
diff --git a/plugins/ban/events/rickroll.py b/plugins/ban/events/rickroll.py
new file mode 100644
index 00000000..94ceaf91
--- /dev/null
+++ b/plugins/ban/events/rickroll.py
@@ -0,0 +1,26 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+
+
+async def execute(
+ ban_plugin,
+ ctx: commands.Context,
+ user: discord.User,
+ reason: str,
+) -> bool:
+ """Just send a rickroll"""
+ await ctx.bot._(ctx.channel, f"ban.gunivers.rickroll")
+ await ctx.send(
+ "Never gonna give you up,\nnever gonna let you down,\nnever gonna run around and ban you :musical_note:"
+ )
+ await ctx.send(
+ "https://thumbs.gfycat.com/VengefulDesertedHalibut-size_restricted.gif"
+ )
+ return
diff --git a/plugins/ban/langs/en.yml b/plugins/ban/langs/en.yml
index 6713181d..78ca3e1b 100644
--- a/plugins/ban/langs/en.yml
+++ b/plugins/ban/langs/en.yml
@@ -29,4 +29,9 @@ en:
autoban:
"0": "You want to ban yourself ? Ok :)"
"1": "Why are you doing that ?"
- "2": "WHY ?"
\ No newline at end of file
+ "2": "WHY ?"
+
+ missing_roles: "I could not give you these roles back: "
+ whoups: "I could not send an invitation to the user... So he will never go back. Goodbye - forever."
+ urbetter: "It seems like you're immune to the ban argument ¯\\_(ツ)_/¯"
+ missing_permissions: "Missing permissions :confused: (please check the hierarchy)"
\ No newline at end of file
diff --git a/plugins/ban/langs/fr.yml b/plugins/ban/langs/fr.yml
index 71efe67e..ed2a65d2 100644
--- a/plugins/ban/langs/fr.yml
+++ b/plugins/ban/langs/fr.yml
@@ -28,5 +28,10 @@ fr:
autoban:
"0": "Tiens, tu veux te bannir toi-même ? Soit !"
- "1": "N'en viens pas à de telles extrémites ! Tu veux qu'on en parles ? Tiens : [SOS Amitié](https://www.sos-amitie.com/)"
- "2": "Rooh, pourquoi ?"
\ No newline at end of file
+ "1": "N'en viens pas à de telles extrémités ! Tu veux qu'on en parles ? Tiens : [SOS Amitié](https://www.sos-amitie.com/)"
+ "2": "Rooh, pourquoi ?"
+
+ missing_roles: "Je n'ai pas pu vous remettre les rôles suivants : "
+ whoups: "Je n'ai pas pu inviter l'utilisateur... Il ne reviendra jamais... Adieu, donc."
+ urbetter: "Apparemment tu es immunisé contre le ban ¯\\_(ツ)_/¯"
+ missing_permissions: "Permissions manquantes :confused: (vérifiez la hiérarchie)"
\ No newline at end of file
diff --git a/plugins/channelArchive/bot/main.py b/plugins/channelArchive/channelArchive.py
similarity index 55%
rename from plugins/channelArchive/bot/main.py
rename to plugins/channelArchive/channelArchive.py
index 5358265c..a019282b 100644
--- a/plugins/channelArchive/bot/main.py
+++ b/plugins/channelArchive/channelArchive.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import typing
import discord
from discord.ext import tasks, commands
@@ -6,28 +13,39 @@
class ChannelArchive(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
- self.config_options = ['archive_category','archive_duration']
+ self.config_options = ["archive_category", "archive_duration"]
self.update_loop.start()
bot.get_command("config").add_command(self.config_archive_category)
bot.get_command("config").add_command(self.config_archive_duration)
@commands.command(name="archive_category")
- async def config_archive_category(self, ctx: MyContext, *, category: discord.CategoryChannel):
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "archive_category", category.id))
+ async def config_archive_category(
+ self, ctx: MyContext, *, category: discord.CategoryChannel
+ ):
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "archive_category", category.id
+ )
+ )
@commands.command(name="archive_duration")
- async def config_archive_duration(self, ctx: MyContext, duration: commands.Greedy[args.tempdelta]):
+ async def config_archive_duration(
+ self, ctx: MyContext, duration: commands.Greedy[args.tempdelta]
+ ):
duration = sum(duration)
if duration == 0:
if ctx.message.content.split(" ")[-1] != "archive_duration":
- await ctx.send(await self.bot._(ctx.guild.id, "sconfig.invalid-duration"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "sconfig.invalid-duration")
+ )
return
duration = None
- x = await self.bot.sconfig.edit_config(ctx.guild.id, "archive_duration", duration)
+ x = await self.bot.sconfig.edit_config(
+ ctx.guild.id, "archive_duration", duration
+ )
await ctx.send(x)
def cog_unload(self):
@@ -39,19 +57,27 @@ async def add_to_archive(self, guild: discord.Guild, channel: discord.TextChanne
archive = self.bot.get_channel(config["archive_category"])
# Move channel
- await channel.move(beginning=True, category=archive, sync_permissions=True, reason="Channel archived")
+ await channel.move(
+ beginning=True,
+ category=archive,
+ sync_permissions=True,
+ reason="Channel archived",
+ )
# Add record to database
query = "INSERT INTO archive (guild, channel) VALUES (?, ?)"
self.bot.db_query(query, (guild.id, channel.id))
- async def update(self, guild: discord.Guild, log_channel: typing.Optional[discord.TextChannel]):
+ async def update(
+ self, guild: discord.Guild, log_channel: typing.Optional[discord.TextChannel]
+ ):
# Get archive duration
config = self.bot.server_configs[guild.id]
duration = config["archive_duration"]
archive_category = config["archive_category"]
- if self.bot.get_channel(archive_category) is None: return
+ if self.bot.get_channel(archive_category) is None:
+ return
query = f"SELECT * FROM archive WHERE guild = {guild.id}"
records = self.bot.db_query(query, ())
@@ -69,11 +95,15 @@ async def update(self, guild: discord.Guild, log_channel: typing.Optional[discor
added += 1
await self.add_to_archive(guild, channel)
- # Clear db records corresponding to channels outside the archive category
+ # Clear db records corresponding to channels outside the archive
+ # category
unarchived = 0
for record in records:
if self.bot.get_channel(record["channel"]) is not None:
- if self.bot.get_channel(record["channel"]).category.id != archive_category:
+ if (
+ self.bot.get_channel(record["channel"]).category.id
+ != archive_category
+ ):
query = f"DELETE FROM archive WHERE channel = {record['channel']} AND guild = {guild.id}"
unarchived += 1
self.bot.db_query(query, ())
@@ -104,30 +134,42 @@ async def update(self, guild: discord.Guild, log_channel: typing.Optional[discor
self.bot.db_query(query, ())
# Send confirmation
- message = await self.bot._(guild.id, 'archive_channel.channel_deleted', count=removed_channels)
- message += "\n" + await self.bot._(guild.id, 'archive_channel.record_deleted', count=removed_records)
- message += "\n" + await self.bot._(guild.id, 'archive_channel.unarchived', count=unarchived)
- message += "\n" + await self.bot._(guild.id, 'archive_channel.archived', count=added)
+ message = await self.bot._(
+ guild.id, "archive_channel.channel_deleted", count=removed_channels
+ )
+ message += "\n" + await self.bot._(
+ guild.id, "archive_channel.record_deleted", count=removed_records
+ )
+ message += "\n" + await self.bot._(
+ guild.id, "archive_channel.unarchived", count=unarchived
+ )
+ message += "\n" + await self.bot._(
+ guild.id, "archive_channel.archived", count=added
+ )
if log_channel is not None:
- await log_channel.send(embed=discord.Embed(
- description=message,
- title=await self.bot._(guild.id, 'archive_channel.title-update'),
- colour=discord.Colour.green()))
-
-
+ await log_channel.send(
+ embed=discord.Embed(
+ description=message,
+ title=await self.bot._(guild.id, "archive_channel.title-update"),
+ colour=discord.Colour.green(),
+ )
+ )
- #-----------------------#
+ # -----------------------#
# Commande list_archive #
- #-----------------------#
+ # -----------------------#
@commands.command(name="list_archive")
@commands.guild_only()
+ @commands.has_permissions(manage_guild=True)
async def list_archive(self, ctx: MyContext):
config = self.bot.server_configs[ctx.guild.id]
if self.bot.get_channel(config["archive_category"]) is None:
- await ctx.send(await self.bot._(ctx.guild.id, 'archive_channel.no-category'))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "archive_channel.no-category")
+ )
return
# Get records
@@ -139,29 +181,39 @@ async def list_archive(self, ctx: MyContext):
message = ""
if len(records) > 0:
for record in records:
- i+=1
+ i += 1
if i != 1:
message += "\n"
if self.bot.get_channel(record["channel"]) is not None:
- message += self.bot.get_channel(record["channel"]).mention + " - " + record["timestamp"]
+ message += (
+ self.bot.get_channel(record["channel"]).mention
+ + " - "
+ + record["timestamp"]
+ )
else:
message += "#deleted-channel - " + record["timestamp"]
- await ctx.send(embed=discord.Embed(
- description=message,
- title=await self.bot._(ctx.guild.id, 'archive_channel.title-list'),
- colour=discord.Colour.green()))
+ await ctx.send(
+ embed=discord.Embed(
+ description=message,
+ title=await self.bot._(ctx.guild.id, "archive_channel.title-list"),
+ colour=discord.Colour.green(),
+ )
+ )
else:
- await ctx.send(embed=discord.Embed(
- description=await self.bot._(ctx.guild.id, 'archive_channel.no-channel'),
- title=await self.bot._(ctx.guild.id, 'archive_channel.title-list'),
- colour=discord.Colour.green()))
-
-
+ await ctx.send(
+ embed=discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id, "archive_channel.no-channel"
+ ),
+ title=await self.bot._(ctx.guild.id, "archive_channel.title-list"),
+ colour=discord.Colour.green(),
+ )
+ )
- #-------------------------#
+ # -------------------------#
# Commande update_archive #
- #-------------------------#
+ # -------------------------#
@commands.command(name="update_archive")
@commands.guild_only()
@@ -169,7 +221,9 @@ async def list_archive(self, ctx: MyContext):
async def update_archive(self, ctx: MyContext):
config = self.bot.server_configs[ctx.guild.id]
if self.bot.get_channel(config["archive_category"]) is None:
- await ctx.send(await self.bot._(ctx.guild.id, 'archive_channel.no-category'))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "archive_channel.no-category")
+ )
return
await self.update(ctx.guild, ctx.channel)
@@ -180,15 +234,14 @@ async def update_loop(self):
log_channel = self.bot.get_channel(config["logs_channel"])
await self.update(guild, log_channel)
-
-
- #------------------#
+ # ------------------#
# Commande archive #
- #------------------#
+ # ------------------#
@commands.command(name="archive")
@commands.guild_only()
- async def archive(self, ctx: MyContext, channel: discord.TextChannel=None):
+ @commands.has_permissions(manage_channels=True, manage_permissions=True)
+ async def archive(self, ctx: MyContext, channel: discord.TextChannel = None):
"""Archive a channel"""
# Get target channel
@@ -196,30 +249,44 @@ async def archive(self, ctx: MyContext, channel: discord.TextChannel=None):
channel = ctx.channel
# Check permissions
- if channel.permissions_for(ctx.author).manage_channels is True and channel.permissions_for(ctx.author).manage_permissions is True:
+ if (
+ channel.permissions_for(ctx.author).manage_channels is True
+ and channel.permissions_for(ctx.author).manage_permissions is True
+ ):
config = self.bot.server_configs[ctx.guild.id]
if self.bot.get_channel(config["archive_category"]) is None:
- await ctx.send(await self.bot._(ctx.guild.id, 'archive_channel.no-category'))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "archive_channel.no-category")
+ )
return
await self.add_to_archive(ctx.guild, channel)
# Success message
embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'archive_channel.success', channel=channel.mention),
- colour=discord.Colour(51711)
+ description=await self.bot._(
+ ctx.guild.id, "archive_channel.success", channel=channel.mention
+ ),
+ colour=discord.Colour(51711),
)
else:
# Missing permission message
embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'archive_channel.missing_permission'),
- colour=0x992d22
+ description=await self.bot._(
+ ctx.guild.id, "archive_channel.missing_permission"
+ ),
+ colour=0x992D22,
)
-
+
await ctx.send(embed=embed)
-async def setup(bot):
- await bot.add_cog(ChannelArchive(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(ChannelArchive(bot), icon="🗃️")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/channelArchive/credits.md b/plugins/channelArchive/credits.md
new file mode 100644
index 00000000..7f4f9445
--- /dev/null
+++ b/plugins/channelArchive/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2020 - 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021 - 2023
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/channelArchive/data/model.sql b/plugins/channelArchive/data/model.sql
index ad79318c..8a63af36 100644
--- a/plugins/channelArchive/data/model.sql
+++ b/plugins/channelArchive/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `archive` (
`guild` BIGINT NOT NULL,
`channel` BIGINT NOT NULL,
diff --git a/plugins/channelArchive/docs/user_documentation.md b/plugins/channelArchive/docs/user_documentation.md
index a8ff32ed..14891cea 100644
--- a/plugins/channelArchive/docs/user_documentation.md
+++ b/plugins/channelArchive/docs/user_documentation.md
@@ -1,3 +1,12 @@
-# Channel archive
+
+
+# 🗃️ Channel archive
+
+Nothing here... for the moment.
+
-Nothing here...
diff --git a/plugins/channelArchive/langs/en.yml b/plugins/channelArchive/langs/en.yml
index 175beafa..aa183c2c 100644
--- a/plugins/channelArchive/langs/en.yml
+++ b/plugins/channelArchive/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
archive_channel:
success: "%{channel} channel has been successfully archived"
diff --git a/plugins/channelArchive/langs/fr.yml b/plugins/channelArchive/langs/fr.yml
index 31da7696..c9de529a 100644
--- a/plugins/channelArchive/langs/fr.yml
+++ b/plugins/channelArchive/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
archive_channel:
success: "Le salon %{channel} à bien été archivé"
diff --git a/plugins/contact/bot/main.py b/plugins/contact/contact.py
similarity index 56%
rename from plugins/contact/bot/main.py
rename to plugins/contact/contact.py
index 97da8ddb..6ed529c6 100644
--- a/plugins/contact/bot/main.py
+++ b/plugins/contact/contact.py
@@ -1,55 +1,92 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import sqlite3
+from utils import Gunibot, MyContext
+from discord.utils import snowflake_time
+from discord.ext import commands
+import discord
+from bot import checks
import typing
from datetime import datetime, timedelta
import aiohttp
import sys
-sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.ext import commands
-from discord.utils import snowflake_time
-from utils import Gunibot, MyContext
-import sqlite3
+# sys.path.append("./bot")
-class Contact(commands.Cog):
+class Contact(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = "contact"
- self.config_options = ['contact_channel',
- 'contact_category', 'contact_roles', 'contact_title']
-
+ self.config_options = [
+ "contact_channel",
+ "contact_category",
+ "contact_roles",
+ "contact_title",
+ ]
+
bot.get_command("config").add_command(self.config_contact_channel)
bot.get_command("config").add_command(self.config_contact_category)
bot.get_command("config").add_command(self.config_contact_roles)
bot.get_command("config").add_command(self.config_contact_title)
@commands.command(name="contact_channel")
- async def config_contact_channel(self, ctx: MyContext, *, channel: discord.TextChannel):
- await ctx.send(await self.bot.sconfig.edit_config(self, ctx.guild.id, "contact_channel", channel.id))
+ async def config_contact_channel(
+ self, ctx: MyContext, *, channel: discord.TextChannel
+ ):
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ self, ctx.guild.id, "contact_channel", channel.id
+ )
+ )
@commands.command(name="contact_category")
- async def config_contact_category(self, ctx: MyContext, *, category: discord.CategoryChannel):
- await ctx.send(await self.bot.sconfig.edit_config(self, ctx.guild.id, "contact_category", category.id))
+ async def config_contact_category(
+ self, ctx: MyContext, *, category: discord.CategoryChannel
+ ):
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ self, ctx.guild.id, "contact_category", category.id
+ )
+ )
@commands.command(name="contact_roles")
- async def config_contact_roles(self, ctx: MyContext, roles: commands.Greedy[discord.Role]):
+ async def config_contact_roles(
+ self, ctx: MyContext, roles: commands.Greedy[discord.Role]
+ ):
if len(roles) == 0:
roles = None
else:
roles = [role.id for role in roles]
- await ctx.send(await self.bot.sconfig.edit_config(self, ctx.guild.id, "contact_roles", roles))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ self, ctx.guild.id, "contact_roles", roles
+ )
+ )
@commands.command(name="contact_title")
async def config_contact_title(self, ctx: MyContext, *, title):
if title == "author" or title == "object":
- await ctx.send(await self.bot.sconfig.edit_config(self, ctx.guild.id, "contact_title", title))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ self, ctx.guild.id, "contact_title", title
+ )
+ )
else:
- await ctx.send(await self.bot._(self, ctx.guild.id, "contact.invalid-title"))
+ await ctx.send(
+ await self.bot._(self, ctx.guild.id, "contact.invalid-title")
+ )
async def urlToByte(self, url: str) -> typing.Optional[bytes]:
- async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
+ async with aiohttp.ClientSession(
+ timeout=aiohttp.ClientTimeout(total=10)
+ ) as session:
async with session.get(url) as response:
if response.status >= 200 and response.status < 300:
res = await response.read()
@@ -58,13 +95,15 @@ async def urlToByte(self, url: str) -> typing.Optional[bytes]:
return res
def db_get_channels(self, guildID: int):
- query = 'SELECT * FROM contact_channels WHERE guild=?'
+ query = "SELECT * FROM contact_channels WHERE guild=?"
res = self.bot.db_query(query, (guildID,))
return res
def db_add_channel(self, channel: discord.TextChannel, authorID):
try:
- query = "INSERT INTO contact_channels (guild,channel, author) VALUES (?, ?, ?)"
+ query = (
+ "INSERT INTO contact_channels (guild,channel, author) VALUES (?, ?, ?)"
+ )
self.bot.db_query(query, (channel.guild.id, channel.id, authorID))
except sqlite3.OperationalError as e:
print(e)
@@ -85,29 +124,50 @@ async def on_message(self, message: discord.Message):
if message.channel.id != config["contact_channel"]:
return
category: discord.CategoryChannel = self.bot.get_channel(
- config["contact_category"])
- if category is None: return
+ config["contact_category"]
+ )
+ if category is None:
+ return
try:
perms = dict()
if config["contact_roles"]:
- over = discord.PermissionOverwrite(
- **dict(discord.Permissions.all()))
- perms = {message.guild.get_role(
- x): over for x in config["contact_roles"]}
+ over = discord.PermissionOverwrite(**dict(discord.Permissions.all()))
+ perms = {
+ message.guild.get_role(x): over for x in config["contact_roles"]
+ }
if message.guild.default_role not in perms.keys():
perms[message.guild.default_role] = discord.PermissionOverwrite(
- read_messages=False)
+ read_messages=False
+ )
perms.pop(None, None)
- perms[message.author] = discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_messages=True,
- embed_links=True, attach_files=True, read_message_history=True, use_external_emojis=True, add_reactions=True)
+ perms[message.author] = discord.PermissionOverwrite(
+ read_messages=True,
+ send_messages=True,
+ manage_messages=True,
+ embed_links=True,
+ attach_files=True,
+ read_message_history=True,
+ use_external_emojis=True,
+ add_reactions=True,
+ )
if config["contact_title"] == "author":
- channel = await category.create_text_channel(str(message.author), topic= message.content + " | " + str(message.author.id), overwrites=perms)
+ channel = await category.create_text_channel(
+ str(message.author),
+ topic=message.content + " | " + str(message.author.id),
+ overwrites=perms,
+ )
else:
- channel = await category.create_text_channel(message.content[:100], topic=str(message.author) + " - " + str(message.author.id), overwrites=perms)
+ channel = await category.create_text_channel(
+ message.content[:100],
+ topic=str(message.author) + " - " + str(message.author.id),
+ overwrites=perms,
+ )
self.db_add_channel(channel, message.author.id)
except discord.errors.Forbidden as e:
- await self.bot.get_cog("Errors").on_error(e, await self.bot.get_context(message))
+ await self.bot.get_cog("Errors").on_error(
+ e, await self.bot.get_context(message)
+ )
return
try:
await message.delete()
@@ -132,12 +192,12 @@ async def ct_clear(self, ctx: commands.Context, days: int = 15):
return
i = 0 # compteur de suppressions
errors = list() # liste des éventuelles erreurs
- max_date = datetime.now()-timedelta(days=days)
+ max_date = datetime.now() - timedelta(days=days)
channels = self.db_get_channels(ctx.guild.id)
for data in channels:
- chan = ctx.guild.get_channel(data['channel'])
+ chan = ctx.guild.get_channel(data["channel"])
if chan is None:
- self.db_delete_channel(ctx.guild.id, data['channel'])
+ self.db_delete_channel(ctx.guild.id, data["channel"])
else:
# si la date du dernier message est trop ancienne
if snowflake_time(chan.last_message_id) < max_date:
@@ -147,13 +207,20 @@ async def ct_clear(self, ctx: commands.Context, days: int = 15):
except discord.DiscordException as e:
errors.append(str(e))
else:
- self.db_delete_channel(ctx.guild.id, data['channel'])
+ self.db_delete_channel(ctx.guild.id, data["channel"])
answer = await self.bot._(ctx.guild.id, "contact.deleted", count=i)
if len(errors) > 0:
- answer += "\n" + await self.bot._(ctx.guild.id, "contact.not-deleted", count=len(errors))
+ answer += "\n" + await self.bot._(
+ ctx.guild.id, "contact.not-deleted", count=len(errors)
+ )
answer += "\n • {}" + "\n • ".join(errors)
await ctx.send(answer)
-async def setup(bot):
- await bot.add_cog(Contact(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Contact(bot), icon="💬")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/contact/credits.md b/plugins/contact/credits.md
new file mode 100644
index 00000000..dbafbba5
--- /dev/null
+++ b/plugins/contact/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/contact/data/model.sql b/plugins/contact/data/model.sql
index 9c429024..d2d45b7a 100644
--- a/plugins/contact/data/model.sql
+++ b/plugins/contact/data/model.sql
@@ -1,3 +1,7 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
CREATE TABLE IF NOT EXISTS `contact_channels` (
`guild` BIGINT NOT NULL,
diff --git a/plugins/contact/docs/user_documentation.md b/plugins/contact/docs/user_documentation.md
index 54bcc6b4..d2bd2208 100644
--- a/plugins/contact/docs/user_documentation.md
+++ b/plugins/contact/docs/user_documentation.md
@@ -1,4 +1,11 @@
-# Contact
+
+
+# 📞 Contact
With this bot, you can create a room in your server where your members can contact the staff. The bot will then create a private room, visible only to said member and your staff, and delete the original message to keep the room clean.
A command also exists to delete contact rooms older than X days.
@@ -14,3 +21,4 @@ It is your duty to make sure that the bot can read the contact channel, and crea
The bot will send the message posted by the user back to his private room, if possible as a webhook with the user's name and avatar.
The command to semi-automatically clean up contact rooms is `contact-clear`. You can specify a minimum number of days of inactivity, by default 15: the bot will then look if the last message posted is old enough before deleting the room.
+
diff --git a/plugins/contact/langs/en.yml b/plugins/contact/langs/en.yml
index e6da2ae4..fbebf94e 100644
--- a/plugins/contact/langs/en.yml
+++ b/plugins/contact/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
contact:
duration-short: You cannot select a duration of less than a day
diff --git a/plugins/contact/langs/fr.yml b/plugins/contact/langs/fr.yml
index c31d9039..ac527da3 100644
--- a/plugins/contact/langs/fr.yml
+++ b/plugins/contact/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
contact:
duration-short: Vous ne pouvez pas choisir une durée de moins d'un jour
diff --git a/plugins/contact/bot/test.py b/plugins/contact/test.py
similarity index 56%
rename from plugins/contact/bot/test.py
rename to plugins/contact/test.py
index 47f6ccd7..092ed019 100644
--- a/plugins/contact/bot/test.py
+++ b/plugins/contact/test.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import typing
import discord
@@ -11,22 +18,35 @@ def __init__(self):
# Set the options that will be presented inside the dropdown
options = [
- discord.SelectOption(label='Red', description='Your favourite colour is red', emoji='🟥'),
- discord.SelectOption(label='Green', description='Your favourite colour is green', emoji='🟩'),
- discord.SelectOption(label='Blue', description='Your favourite colour is blue', emoji='🟦')
+ discord.SelectOption(
+ label="Red", description="Your favourite colour is red", emoji="🟥"
+ ),
+ discord.SelectOption(
+ label="Green", description="Your favourite colour is green", emoji="🟩"
+ ),
+ discord.SelectOption(
+ label="Blue", description="Your favourite colour is blue", emoji="🟦"
+ ),
]
# The placeholder is what will be shown when no option is chosen
# The min and max values indicate we can only pick one of the three options
# The options parameter defines the dropdown options. We defined this above
- super().__init__(placeholder='Choose your favourite colour...', min_values=1, max_values=1, options=options)
+ super().__init__(
+ placeholder="Choose your favourite colour...",
+ min_values=1,
+ max_values=1,
+ options=options,
+ )
async def callback(self, interaction: discord.Interaction):
# Use the interaction object to send a response message containing
# the user's favourite colour or choice. The self object refers to the
- # Select object, and the values attribute gets a list of the user's
+ # Select object, and the values attribute gets a list of the user's
# selected options. We only want the first one.
- await interaction.response.send_message(f'Your favourite colour is {self.values[0]}')
+ await interaction.response.send_message(
+ f"Your favourite colour is {self.values[0]}"
+ )
class DropdownView(discord.ui.View):
@@ -39,13 +59,13 @@ def __init__(self):
class Bot(commands.Bot):
def __init__(self):
- super().__init__(command_prefix=commands.when_mentioned_or('$'))
+ super().__init__(command_prefix=commands.when_mentioned_or("$"))
async def on_ready(self):
- print(f'Logged in as {self.user} (ID: {self.user.id})')
- print('------')
-
-
+ print(f"Logged in as {self.user} (ID: {self.user.id})")
+ print("------")
+
+
bot = Bot()
@@ -57,7 +77,7 @@ async def colour(ctx):
view = DropdownView()
# Sending a message containing our view
- await ctx.send('Pick your favourite colour:', view=view)
+ await ctx.send("Pick your favourite colour:", view=view)
-bot.run('token')
\ No newline at end of file
+bot.run("token")
diff --git a/plugins/general/credits.md b/plugins/general/credits.md
new file mode 100644
index 00000000..0c1054ff
--- /dev/null
+++ b/plugins/general/credits.md
@@ -0,0 +1,8 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/general/docs/user_documentation.md b/plugins/general/docs/user_documentation.md
index 2402d226..52d2bd54 100644
--- a/plugins/general/docs/user_documentation.md
+++ b/plugins/general/docs/user_documentation.md
@@ -1,3 +1,11 @@
-# Global commands
+
+
+# 🌐 Global commands
+
+Nothing here... for the moment.
-Nothing here...
\ No newline at end of file
diff --git a/plugins/general/bot/main.py b/plugins/general/general.py
similarity index 50%
rename from plugins/general/bot/main.py
rename to plugins/general/general.py
index b654f774..c4e4ef77 100644
--- a/plugins/general/bot/main.py
+++ b/plugins/general/general.py
@@ -1,19 +1,27 @@
-import asyncio
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from typing import Union
import os
import sys
-import time
-from platform import system as system_name # Returns the system/OS name
-from subprocess import call as system_call # Execute a shell command
import discord
import psutil
from discord.ext import commands
from git import Repo
+
from utils import Gunibot, MyContext
+CHANNEL_TYPES = Union[
+ discord.Thread,
+ discord.abc.GuildChannel,
+]
class General(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.codelines = 0
@@ -27,25 +35,31 @@ async def count_lines_code(self):
count = 0
try:
for root, dirs, files in os.walk("."):
- if '/lib/python' in root:
+ if "/lib/python" in root:
continue
for file in files:
if file.endswith(".py"):
- with open(os.path.join(root, file), 'r', encoding='utf8') as f:
+ with open(os.path.join(root, file), "r", encoding="utf8") as f:
for line in f.read().split("\n"):
- if len(line.strip()) > 2 and line[0] != '#':
+ if len(line.strip()) > 2 and line[0] != "#":
count += 1
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e, None)
+ await self.bot.get_cog("Errors").on_error(e, None)
self.codelines = count
-
- @commands.command(name='hs')
- async def hs(self, ctx: MyContext, channel: discord.TextChannel = None):
+ @commands.command(name="hs")
+ async def hs(self, ctx: MyContext, channel: CHANNEL_TYPES = None):
if channel:
- msg = await self.bot._(ctx.channel, "general.hs-1", current=ctx.channel.mention, dest=channel.mention)
+ msg = await self.bot._(
+ ctx.channel,
+ "general.hs-1",
+ current=ctx.channel.mention,
+ dest=channel.mention,
+ )
else:
- msg = await self.bot._(ctx.channel, "general.hs-2", current=ctx.channel.mention)
+ msg = await self.bot._(
+ ctx.channel, "general.hs-2", current=ctx.channel.mention
+ )
if ctx.can_send_embed:
emb = discord.Embed(description=msg, color=discord.Color.red())
await ctx.send(embed=emb)
@@ -58,25 +72,29 @@ async def rep(self, ctx: MyContext):
m = await ctx.send("Ping...")
t = (m.created_at - ctx.message.created_at).total_seconds()
try:
- p = round(self.bot.latency*1000)
+ p = round(self.bot.latency * 1000)
except OverflowError:
p = "∞"
- await m.edit(content=":ping_pong: Pong !\nBot ping: {}ms\nDiscord ping: {}ms".format(round(t*1000), p))
+ await m.edit(
+ content=":ping_pong: Pong !\nBot ping: {}ms\nDiscord ping: {}ms".format(
+ round(t * 1000), p
+ )
+ )
@commands.command(name="stats")
@commands.cooldown(2, 60, commands.BucketType.guild)
async def stats(self, ctx: MyContext):
"""Display some statistics about the bot"""
v = sys.version_info
- version = str(v.major)+"."+str(v.minor)+"."+str(v.micro)
+ version = str(v.major) + "." + str(v.minor) + "." + str(v.micro)
pid = os.getpid()
try:
py = psutil.Process(pid)
- ram_usage = round(py.memory_info()[0]/2.**30, 3) #, py.cpu_percent()]
+ ram_usage = round(py.memory_info()[0] / 2.0**30, 3) # , py.cpu_percent()]
except OSError:
ram_usage = latency = "?"
py = None
- latency = round(self.bot.latency*1000, 3)
+ latency = round(self.bot.latency * 1000, 3)
CPU_INTERVAL = 3.0
try:
async with ctx.channel.typing():
@@ -85,34 +103,57 @@ async def stats(self, ctx: MyContext):
users = len(ctx.bot.users)
bots = len([None for u in ctx.bot.users if u.bot])
d = await self.bot._(ctx.channel, "general.stats.servs", c=len_servers)
- d += "\n" + await self.bot._(ctx.channel, "general.stats.members", c=users, bots=bots)
- d += "\n" + await self.bot._(ctx.channel, "general.stats.codelines", c=self.codelines)
- d += "\n" + await self.bot._(ctx.channel, "general.stats.pyver", v=version)
- d += "\n" + await self.bot._(ctx.channel, "general.stats.diver", v=discord.__version__)
+ d += "\n" + await self.bot._(
+ ctx.channel, "general.stats.members", c=users, bots=bots
+ )
+ d += "\n" + await self.bot._(
+ ctx.channel, "general.stats.codelines", c=self.codelines
+ )
+ d += "\n" + await self.bot._(
+ ctx.channel, "general.stats.pyver", v=version
+ )
+ d += "\n" + await self.bot._(
+ ctx.channel, "general.stats.diver", v=discord.__version__
+ )
d += "\n" + await self.bot._(ctx.channel, "general.stats.git", b=branch)
- d += "\n" + await self.bot._(ctx.channel, "general.stats.ram", c=ram_usage)
+ d += "\n" + await self.bot._(
+ ctx.channel, "general.stats.ram", c=ram_usage
+ )
cpu_txt = await self.bot._(ctx.channel, "general.stats.cpu-loading")
d += "\n" + cpu_txt
- d += "\n" + await self.bot._(ctx.channel, "general.stats.ping", c=latency)
+ d += "\n" + await self.bot._(
+ ctx.channel, "general.stats.ping", c=latency
+ )
if ctx.can_send_embed:
- title = '**' + await self.bot._(ctx.channel, "general.stats.title") + '**'
- embed = discord.Embed(title=title, color=8311585, timestamp=ctx.message.created_at, description=d)
+ title = (
+ "**" + await self.bot._(ctx.channel, "general.stats.title") + "**"
+ )
+ embed = discord.Embed(
+ title=title,
+ color=8311585,
+ timestamp=ctx.message.created_at,
+ description=d,
+ )
embed.set_thumbnail(url=self.bot.user.display_avatar)
msg: discord.Message = await ctx.send(embed=embed)
- if py is None: # PSUtil can't be used
+ if py is None: # PSUtil can't be used
cpu_usage = "?"
else:
cpu_usage = py.cpu_percent(CPU_INTERVAL)
- cpu_ended = await self.bot._(ctx.channel, "general.stats.cpu-ended", c=cpu_usage)
+ cpu_ended = await self.bot._(
+ ctx.channel, "general.stats.cpu-ended", c=cpu_usage
+ )
embed.description = embed.description.replace(cpu_txt, cpu_ended)
await msg.edit(embed=embed)
else:
msg = await ctx.send(d)
- if py is None: # PSUtil can't be used
+ if py is None: # PSUtil can't be used
cpu_usage = "?"
else:
cpu_usage = py.cpu_percent(CPU_INTERVAL)
- cpu_ended = await self.bot._(ctx.channel, "general.stats.cpu-ended", c=cpu_usage)
+ cpu_ended = await self.bot._(
+ ctx.channel, "general.stats.cpu-ended", c=cpu_usage
+ )
d = d.replace(cpu_txt, cpu_ended)
await msg.edit(content=d)
except Exception as e:
@@ -120,15 +161,16 @@ async def stats(self, ctx: MyContext):
@commands.command(name="halp", enabled=False)
async def halp(self, ctx):
- embed = discord.Embed(
- name="Help",
- colour=discord.Colour.green()
- )
- embed.set_author(name=f'Gunibot commands')
+ embed = discord.Embed(name="Help", colour=discord.Colour.green())
+ embed.set_author(name=f"Gunibot commands")
embed.add_field(name="admin", value="Affiche les commandes admin disponibles")
embed.add_field(name="admin", value="Affiche les commandes admin disponibles")
await ctx.send(embed=embed)
-
-async def setup(bot):
- await bot.add_cog(General(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(General(bot), icon="🌍")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/giveaways/credits.md b/plugins/giveaways/credits.md
new file mode 100644
index 00000000..2a3786d0
--- /dev/null
+++ b/plugins/giveaways/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021-2022
+Copyright © ascpial 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/giveaways/data/model.sql b/plugins/giveaways/data/model.sql
index 38584568..3b5905fa 100644
--- a/plugins/giveaways/data/model.sql
+++ b/plugins/giveaways/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `giveaways` (
`guild` BIGINT NOT NULL,
`channel` BIGINT NOT NULL,
diff --git a/plugins/giveaways/docs/user_documentation.md b/plugins/giveaways/docs/user_documentation.md
index df540752..43d63993 100644
--- a/plugins/giveaways/docs/user_documentation.md
+++ b/plugins/giveaways/docs/user_documentation.md
@@ -1,3 +1,10 @@
-# **Giveaways**
+
-A giveaway system is available to offer gifts to your members when you feel like it. For this, you can use the command `/giveaway` or its short form `/gaw`. Many options for the giveaway are available, so we invite you to enter the command `/help gaw` to see the different options, and `/help gaw start` to understand how to start a fully customized giveaway. Once the giveaway is started, the bot will send a message with a reaction to the specified room. Members using the command to enter the giveaway or adding the reaction to the message will be added to the list of participants. At the end of the giveaway or when you use the `/gaw pick ` command, a member will be randomly selected from this list.
\ No newline at end of file
+# 🎁 Giveaways
+
+A giveaway system is available to offer gifts to your members when you feel like it. For this, you can use the command `/giveaway` or its short form `/gaw`. Many options for the giveaway are available, so we invite you to enter the command `/help gaw` to see the different options, and `/help gaw start` to understand how to start a fully customized giveaway. Once the giveaway is started, the bot will send a message with a reaction to the specified room. Members using the command to enter the giveaway or adding the reaction to the message will be added to the list of participants. At the end of the giveaway or when you use the `/gaw pick ` command, a member will be randomly selected from this list.
diff --git a/plugins/giveaways/bot/main.py b/plugins/giveaways/giveaways.py
similarity index 59%
rename from plugins/giveaways/bot/main.py
rename to plugins/giveaways/giveaways.py
index 14b0daaf..68c46cb6 100644
--- a/plugins/giveaways/bot/main.py
+++ b/plugins/giveaways/giveaways.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import datetime
import random
import time
@@ -5,9 +12,11 @@
from typing import List, Optional, Union
import sys
+
sys.path.append("./bot")
import args
import sys
+
sys.path.append("./bot")
from bot import checks
import discord
@@ -18,21 +27,24 @@
class Giveaways(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
- self.config_options = ['giveaways_emojis']
+ self.config_options = ["giveaways_emojis"]
self.internal_task.start()
-
+
bot.get_command("config").add_command(self.giveaways_emojis)
@commands.command(name="giveaways_emojis")
- async def giveaways_emojis(self, ctx: MyContext, emojis: commands.Greedy[Union[discord.Emoji, str]]):
+ async def giveaways_emojis(
+ self, ctx: MyContext, emojis: commands.Greedy[Union[discord.Emoji, str]]
+ ):
"""Set a list of usable emojis for giveaways
Only these emojis will be usable to participate in a giveaway
If no emoji is specified, every emoji will be usable"""
# check if every emoji is valid
- emojis = [x for x in emojis if isinstance(x, discord.Emoji) or emoji.is_emoji(x)]
+ emojis = [
+ x for x in emojis if isinstance(x, discord.Emoji) or emoji.is_emoji(x)
+ ]
# if one or more emojis were invalid (couldn't be converted)
if len(ctx.args[2]) != len(emojis):
await ctx.send(await self.bot._(ctx.guild.id, "sconfig.invalid-emoji"))
@@ -43,9 +55,18 @@ async def giveaways_emojis(self, ctx: MyContext, emojis: commands.Greedy[Union[d
# convert discord emojis to IDs if needed
emojis = [str(x.id) if isinstance(x, discord.Emoji) else x for x in emojis]
# save result
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "giveaways_emojis", emojis))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "giveaways_emojis", emojis)
+ )
- def db_add_giveaway(self, channel: discord.TextChannel, name: str, message: int, max_entries: int, ends_at: datetime.datetime = None) -> int:
+ def db_add_giveaway(
+ self,
+ channel: discord.TextChannel,
+ name: str,
+ message: int,
+ max_entries: int,
+ ends_at: datetime.datetime = None,
+ ) -> int:
"""
Add a giveaway into the database
channel: the channel where the giveaway started
@@ -54,7 +75,15 @@ def db_add_giveaway(self, channel: discord.TextChannel, name: str, message: int,
ends_at: the end date of the giveaway (null for a manual end)
Returns: the row ID of the giveaway
"""
- data = (channel.guild.id, channel.id, name[:64], max_entries, ends_at, message, dumps(list()))
+ data = (
+ channel.guild.id,
+ channel.id,
+ name[:64],
+ max_entries,
+ ends_at,
+ message,
+ dumps(list()),
+ )
query = "INSERT INTO giveaways (guild, channel, name, max_entries, ends_at, message, users) VALUES (?, ?, ?, ?, ?, ?, ?)"
rowid: int = self.bot.db_query(query, data)
return rowid
@@ -69,10 +98,10 @@ def db_get_giveaways(self, guildID: int) -> List[dict]:
liste = self.bot.db_query(query, (guildID,))
res = list(map(dict, liste))
for r in res:
- r['users'] = loads(r['users'])
- r['ends_at'] = datetime.datetime.strptime(r['ends_at'], '%Y-%m-%d %H:%M:%S')
+ r["users"] = loads(r["users"])
+ r["ends_at"] = datetime.datetime.strptime(r["ends_at"], "%Y-%m-%d %H:%M:%S")
return res
-
+
def db_get_expired_giveaways(self) -> List[dict]:
"""
Get every running giveaway
@@ -82,8 +111,8 @@ def db_get_expired_giveaways(self) -> List[dict]:
liste = self.bot.db_query(query, (datetime.datetime.now(),))
res = list(map(dict, liste))
for r in res:
- r['users'] = loads(r['users'])
- r['ends_at'] = datetime.datetime.strptime(r['ends_at'], '%Y-%m-%d %H:%M:%S')
+ r["users"] = loads(r["users"])
+ r["ends_at"] = datetime.datetime.strptime(r["ends_at"], "%Y-%m-%d %H:%M:%S")
return res
def db_get_users(self, rowID: int) -> List[int]:
@@ -96,9 +125,9 @@ def db_get_users(self, rowID: int) -> List[int]:
res = self.bot.db_query(query, (rowID,))
if len(res) == 0:
return None
- return loads(res[0]['users'])
+ return loads(res[0]["users"])
- def db_edit_participant(self, rowID: int, userID: int, add: bool=True) -> bool:
+ def db_edit_participant(self, rowID: int, userID: int, add: bool = True) -> bool:
"""
Add a participant to a giveaway
rowID: the ID of the giveaway to edit
@@ -114,7 +143,7 @@ def db_edit_participant(self, rowID: int, userID: int, add: bool=True) -> bool:
if userID in current_participants:
# user was already participating
return
- current_participants = dumps(current_participants+[userID])
+ current_participants = dumps(current_participants + [userID])
else:
try:
current_participants.remove(userID)
@@ -145,18 +174,22 @@ def db_delete_giveaway(self, rowID: int) -> bool:
query = "DELETE FROM giveaways WHERE rowid=?"
rowcount = self.bot.db_query(query, (rowID,), returnrowcount=True)
return rowcount == 1
-
- async def get_allowed_emojis(self, guildID:int) -> List[Union[discord.Emoji, str]]:
+
+ async def get_allowed_emojis(self, guildID: int) -> List[Union[discord.Emoji, str]]:
"""Get a list of allowed emojis for a specific guild"""
- value = self.bot.server_configs[guildID]['giveaways_emojis']
+ value = self.bot.server_configs[guildID]["giveaways_emojis"]
if value is None:
return None
- def emojis_convert(s_emoji:str, bot_emojis:List[discord.Emoji]) -> Union[str, discord.Emoji]:
+
+ def emojis_convert(
+ s_emoji: str, bot_emojis: List[discord.Emoji]
+ ) -> Union[str, discord.Emoji]:
if s_emoji.isnumeric():
d_em = discord.utils.get(bot_emojis, id=int(s_emoji))
if d_em is not None:
return d_em
return emoji.emojize(s_emoji, language="alias")
+
value = [value] if isinstance(value, str) else value
result = list(filter(None, [emojis_convert(x, self.bot.emojis) for x in value]))
if len(result) >= 0:
@@ -167,7 +200,7 @@ def emojis_convert(s_emoji:str, bot_emojis:List[discord.Emoji]) -> Union[str, di
async def giveaway(self, ctx: MyContext):
"""Start or stop giveaways."""
if ctx.subcommand_passed is None:
- await ctx.send_help('giveaways')
+ await ctx.send_help("giveaways")
@giveaway.command()
@commands.check(checks.is_admin)
@@ -186,71 +219,120 @@ async def start(self, ctx: MyContext, *, settings: str):
[p]giveaway start name: Minecraft account; duration: 5h 3min; entries: 5"""
i_settings = settings.split("; ")
existing_giveaways = self.db_get_giveaways(ctx.guild.id)
- existing_giveaways = [x['name'] for x in existing_giveaways]
-
+ existing_giveaways = [x["name"] for x in existing_giveaways]
# Setting all of the settings.
settings = {"name": "", "duration": -1, "channel": ctx.channel, "entries": 1}
for setting in i_settings:
if setting.startswith("name: "):
if setting[6:] in existing_giveaways:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.invalid-name"))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.creation.invalid-name"
+ )
+ )
return
else:
- settings['name'] = setting[6:].strip()
+ settings["name"] = setting[6:].strip()
elif setting.startswith("entries: "):
entries = setting.replace("entries: ", "").strip()
if (not entries.isnumeric()) or (entries == "0"):
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.invalid-winners"))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.creation.invalid-winners"
+ )
+ )
return
- settings['entries'] = int(entries)
+ settings["entries"] = int(entries)
elif setting.startswith("duration: "):
total = 0
for elem in setting[10:].split():
total += await args.tempdelta().convert(ctx, elem)
if total > 0:
- settings['duration'] = total
+ settings["duration"] = total
elif setting.startswith("channel: "):
try:
- channel = await commands.TextChannelConverter().convert(ctx, setting.replace('channel: ', ''))
+ channel = await commands.TextChannelConverter().convert(
+ ctx, setting.replace("channel: ", "")
+ )
except:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.invalid-channel"))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.creation.invalid-channel"
+ )
+ )
return
perms = channel.permissions_for(ctx.guild.me)
if not (perms.send_messages or perms.embed_links):
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.invalid-perms"))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.creation.invalid-perms"
+ )
+ )
return
- settings['channel'] = channel
+ settings["channel"] = channel
# Checking if mandatory settings are there.
- if settings['name'] == "":
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.empty-name"))
+ if settings["name"] == "":
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.creation.empty-name")
+ )
return
- if settings['duration'] == -1:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.empty-duration"))
+ if settings["duration"] == -1:
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.creation.empty-duration")
+ )
return
- settings['ends_at'] = datetime.datetime.fromtimestamp(round(time.time()) + settings['duration'])
+ settings["ends_at"] = datetime.datetime.fromtimestamp(
+ round(time.time()) + settings["duration"]
+ )
# If the channel is too big, bugs will for sure happen, so we abort
- if len(settings['channel'].members) > 10000:
- await ctx.send(await self.bot._(ctx.guild.id, 'giveaways.too-many-members'))
+ if len(settings["channel"].members) > 10000:
+ await ctx.send(await self.bot._(ctx.guild.id, "giveaways.too-many-members"))
return
# Send embed now
try:
title = await self.bot._(ctx.guild.id, "giveaways.embed.title")
ends_at = await self.bot._(ctx.guild.id, "giveaways.embed.ends-at")
- emb = discord.Embed(title=title, description=settings["name"], timestamp=datetime.datetime.utcnow(
- )+datetime.timedelta(seconds=settings['duration']), color=discord.Colour.random()).set_footer(text=ends_at)
- msg: discord.Message = await settings['channel'].send(embed=emb)
- settings['message'] = msg.id
+ emb = discord.Embed(
+ title=title,
+ description=settings["name"],
+ timestamp=datetime.datetime.utcnow()
+ + datetime.timedelta(seconds=settings["duration"]),
+ color=discord.Colour.random(),
+ ).set_footer(text=ends_at)
+ msg: discord.Message = await settings["channel"].send(embed=emb)
+ settings["message"] = msg.id
except discord.HTTPException as e:
- await self.bot.get_cog("Errors").on_error(e, ctx) # send error logs
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.httpexception", channe=settings['channel'].mention))
+ await self.bot.get_cog("Errors").on_error(e, ctx) # send error logs
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "giveaways.creation.httpexception",
+ channe=settings["channel"].mention,
+ )
+ )
return
# Save settings in database
- rowid = self.db_add_giveaway(settings['channel'], settings['name'], settings['message'], settings['entries'], settings['ends_at'])
+ rowid = self.db_add_giveaway(
+ settings["channel"],
+ settings["name"],
+ settings["message"],
+ settings["entries"],
+ settings["ends_at"],
+ )
if rowid:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.creation.success", name=settings['name'], id=rowid))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "giveaways.creation.success",
+ name=settings["name"],
+ id=rowid,
+ )
+ )
else:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.something-went-wrong"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.something-went-wrong")
+ )
allowed_emojis = await self.get_allowed_emojis(ctx.guild.id)
if allowed_emojis is None:
return
@@ -274,18 +356,23 @@ async def stop(self, ctx: MyContext, *, giveaway: str):
if len(giveaways) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
- giveaway = [x for x in giveaways if x['name']
- == giveaway or str(x['rowid']) == giveaway]
+ giveaway = [
+ x for x in giveaways if x["name"] == giveaway or str(x["rowid"]) == giveaway
+ ]
if len(giveaway) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix
+ )
+ )
return
giveaway = giveaway[0]
- if not giveaway['running']:
+ if not giveaway["running"]:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.already-stopped"))
return
- self.db_stop_giveaway(giveaway['rowid'])
+ self.db_stop_giveaway(giveaway["rowid"])
await self.send_results(giveaway, await self.pick_winners(ctx.guild, giveaway))
-
+
@giveaway.command()
@commands.check(checks.is_admin)
async def delete(self, ctx: MyContext, *, giveaway: str):
@@ -296,15 +383,23 @@ async def delete(self, ctx: MyContext, *, giveaway: str):
if len(giveaways) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
- giveaway = [x for x in giveaways if x['name'] == giveaway or str(x['rowid']) == giveaway]
+ giveaway = [
+ x for x in giveaways if x["name"] == giveaway or str(x["rowid"]) == giveaway
+ ]
if len(giveaway) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix
+ )
+ )
return
giveaway = giveaway[0]
- if self.db_delete_giveaway(giveaway['rowid']):
+ if self.db_delete_giveaway(giveaway["rowid"]):
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.success-deleted"))
else:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.something-went-wrong"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.something-went-wrong")
+ )
@giveaway.command()
@commands.check(checks.is_admin)
@@ -316,37 +411,56 @@ async def pick(self, ctx: MyContext, *, giveaway: str):
if len(giveaways) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
- giveaway = [x for x in giveaways if x['name'] == giveaway or str(x['rowid']) == giveaway]
+ giveaway = [
+ x for x in giveaways if x["name"] == giveaway or str(x["rowid"]) == giveaway
+ ]
if len(giveaway) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix
+ )
+ )
return
giveaway = giveaway[0]
- if giveaway['running']:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.not-stopped", p=ctx.prefix, id=giveaway['rowid']))
+ if giveaway["running"]:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "giveaways.not-stopped",
+ p=ctx.prefix,
+ id=giveaway["rowid"],
+ )
+ )
return
allowed_reactions = await self.get_allowed_emojis(ctx.guild.id)
- users = set(giveaway['users']) | await self.get_users(giveaway['channel'], giveaway['message'], allowed_reactions)
+ users = set(giveaway["users"]) | await self.get_users(
+ giveaway["channel"], giveaway["message"], allowed_reactions
+ )
if len(users) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.picking.no-participant"))
- self.db_delete_giveaway(giveaway['rowid'])
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.picking.no-participant")
+ )
+ self.db_delete_giveaway(giveaway["rowid"])
else:
- amount = min(giveaway['max_entries'], len(users))
+ amount = min(giveaway["max_entries"], len(users))
status = await ctx.send("Choix des gagnants...")
winners = []
trials = 0
users = list(users)
while len(winners) < amount and trials < 20:
- w = discord.utils.get(
- ctx.guild.members, id=random.choice(users))
+ w = discord.utils.get(ctx.guild.members, id=random.choice(users))
if w != None:
winners.append(w.mention)
else:
trials += 1
- self.db_delete_giveaway(giveaway['rowid'])
- txt = await self.bot._(ctx.guild.id, "giveaways.picking.winners",
+ self.db_delete_giveaway(giveaway["rowid"])
+ txt = await self.bot._(
+ ctx.guild.id,
+ "giveaways.picking.winners",
count=amount,
users=" ".join(winners),
- price=giveaway)
+ price=giveaway,
+ )
await status.edit(content=txt)
@giveaway.command()
@@ -364,21 +478,35 @@ async def enter(self, ctx: MyContext, *, giveaway: str):
if len(giveaways) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
- giveaways = [x for x in giveaways if x['name'] == giveaway or str(x['rowid']) == giveaway]
+ giveaways = [
+ x for x in giveaways if x["name"] == giveaway or str(x["rowid"]) == giveaway
+ ]
if len(giveaways) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix
+ )
+ )
return
ga = giveaways[0]
- if author.id in ga['users']:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.already-participant"))
- elif not ga['running']:
+ if author.id in ga["users"]:
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.already-participant")
+ )
+ elif not ga["running"]:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.been-stopped"))
else:
- if self.db_edit_participant(ga['rowid'], author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.subscribed", name=ga['name']))
+ if self.db_edit_participant(ga["rowid"], author.id):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.subscribed", name=ga["name"]
+ )
+ )
else:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.something-went-wrong"))
-
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.something-went-wrong")
+ )
+
@giveaway.command()
@commands.cooldown(2, 40, commands.BucketType.user)
async def leave(self, ctx: MyContext, *, giveaway: str):
@@ -394,20 +522,32 @@ async def leave(self, ctx: MyContext, *, giveaway: str):
if len(giveaways) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
- giveaways = [x for x in giveaways if x['name'] == giveaway or str(x['rowid']) == giveaway]
+ giveaways = [
+ x for x in giveaways if x["name"] == giveaway or str(x["rowid"]) == giveaway
+ ]
if len(giveaways) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix
+ )
+ )
return
ga = giveaways[0]
- if author.id not in ga['users']:
+ if author.id not in ga["users"]:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.already-left"))
- elif not ga['running']:
+ elif not ga["running"]:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.been-stopped"))
else:
- if self.db_edit_participant(ga['rowid'], author.id, add=False):
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.success-left", name=ga['name']))
+ if self.db_edit_participant(ga["rowid"], author.id, add=False):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.success-left", name=ga["name"]
+ )
+ )
else:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.something-went-wrong"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "giveaways.something-went-wrong")
+ )
@giveaway.command()
async def list(self, ctx: MyContext):
@@ -418,8 +558,10 @@ async def list(self, ctx: MyContext):
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
else:
- running = [f"{x['rowid']}. {x['name']}" for x in giveaways if x['running']]
- stopped = [f"{x['rowid']}. {x['name']}" for x in giveaways if not x['running']]
+ running = [f"{x['rowid']}. {x['name']}" for x in giveaways if x["running"]]
+ stopped = [
+ f"{x['rowid']}. {x['name']}" for x in giveaways if not x["running"]
+ ]
text = ""
if len(running) > 0:
text += await self.bot._(ctx.guild.id, "giveaways.list-active")
@@ -441,22 +583,44 @@ async def info(self, ctx: MyContext, *, giveaway: str):
if len(giveaways) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "giveaways.no-giveaway"))
return
- giveaway = [x for x in giveaways if x['name'] == giveaway or str(x['rowid']) == giveaway]
+ giveaway = [
+ x for x in giveaways if x["name"] == giveaway or str(x["rowid"]) == giveaway
+ ]
if len(giveaway) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "giveaways.unknown-giveaway", p=ctx.prefix
+ )
+ )
return
giveaway = giveaway[0]
allowed_reactions = await self.get_allowed_emojis(ctx.guild.id)
- entries = len(set(giveaway['users']) | await self.get_users(giveaway['channel'], giveaway['message'], allowed_reactions))
- d1, d2 = datetime.datetime.now(), giveaway['ends_at']
+ entries = len(
+ set(giveaway["users"])
+ | await self.get_users(
+ giveaway["channel"], giveaway["message"], allowed_reactions
+ )
+ )
+ d1, d2 = datetime.datetime.now(), giveaway["ends_at"]
if d1 < d2:
- time_left = await self.bot.get_cog("TimeCog").time_delta(d2, d1, 'fr', precision=0)
+ time_left = await self.bot.get_cog("TimeCog").time_delta(
+ d2, d1, "fr", precision=0
+ )
elif d1 == d2:
time_left = await self.bot._(ctx.guild.id, "giveaways.info.soon")
else:
time_left = await self.bot._(ctx.guild.id, "giveaways.info.ended")
- name = giveaway['name']
- await ctx.send(await self.bot._(ctx.guild.id, "giveaways.info.summary", name=name, time=time_left, nbr=entries, channel=giveaway['channel']))
+ name = giveaway["name"]
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "giveaways.info.summary",
+ name=name,
+ time=time_left,
+ nbr=entries,
+ channel=giveaway["channel"],
+ )
+ )
def cog_unload(self):
self.internal_task.cancel()
@@ -464,17 +628,22 @@ def cog_unload(self):
@tasks.loop(seconds=2.0)
async def internal_task(self):
for giveaway in self.db_get_expired_giveaways():
- if giveaway['running']:
+ if giveaway["running"]:
try:
- serv = self.bot.get_guild(giveaway['guild'])
+ serv = self.bot.get_guild(giveaway["guild"])
winners = await self.pick_winners(serv, giveaway)
await self.send_results(giveaway, winners)
- self.db_stop_giveaway(giveaway['rowid'])
+ self.db_stop_giveaway(giveaway["rowid"])
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
- self.db_stop_giveaway(giveaway['rowid'])
+ await self.bot.get_cog("Errors").on_error(e)
+ self.db_stop_giveaway(giveaway["rowid"])
- async def get_users(self, channel: int, message: int, allowed_reactions: Optional[set[Union[discord.Emoji, str]]]):
+ async def get_users(
+ self,
+ channel: int,
+ message: int,
+ allowed_reactions: Optional[set[Union[discord.Emoji, str]]],
+ ):
"""Get users who reacted to a message"""
channel: discord.TextChannel = self.bot.get_channel(channel)
if channel is None:
@@ -490,29 +659,42 @@ async def get_users(self, channel: int, message: int, allowed_reactions: Optiona
users.add(user.id)
return users
- async def edit_embed(self, channel: discord.TextChannel, message: int, winners: List[discord.Member]) -> int:
+ async def edit_embed(
+ self, channel: discord.TextChannel, message: int, winners: List[discord.Member]
+ ) -> int:
"""Edit the embed to display results
Returns the embed color if the embed was found, None else"""
message: discord.Message = await channel.fetch_message(message)
- if message is None or message.author != self.bot.user or len(message.embeds) == 0:
+ if (
+ message is None
+ or message.author != self.bot.user
+ or len(message.embeds) == 0
+ ):
return None
emb: discord.Embed = message.embeds[0]
emb.set_footer(text=await self.bot._(channel, "giveaways.embed.ended-at"))
- emb.description = await self.bot._(channel, "giveaways.embed.desc",
+ emb.description = await self.bot._(
+ channel,
+ "giveaways.embed.desc",
price=emb.description,
- winners=" ".join([x.mention for x in winners]))
+ winners=" ".join([x.mention for x in winners]),
+ )
await message.edit(embed=emb)
return emb.color
- async def pick_winners(self, guild: discord.Guild, giveaway: dict) -> List[discord.Member]:
+ async def pick_winners(
+ self, guild: discord.Guild, giveaway: dict
+ ) -> List[discord.Member]:
"""Select the winner of a giveaway, from both participants using the command and using the message reactions
Returns a list of members"""
allowed_reactions = await self.get_allowed_emojis(guild.id)
- users = set(giveaway['users']) | await self.get_users(giveaway['channel'], giveaway['message'], allowed_reactions)
+ users = set(giveaway["users"]) | await self.get_users(
+ giveaway["channel"], giveaway["message"], allowed_reactions
+ )
if len(users) == 0:
return list()
else:
- amount = min(giveaway['max_entries'], len(users))
+ amount = min(giveaway["max_entries"], len(users))
winners = list()
trials = 0
users = list(users)
@@ -527,19 +709,33 @@ async def pick_winners(self, guild: discord.Guild, giveaway: dict) -> List[disco
async def send_results(self, giveaway: dict, winners: List[discord.Member]):
"""Send the giveaway results in a new embed"""
self.bot.log.info(f"Giveaway '{giveaway['name']}' has stopped")
- channel: discord.TextChannel = self.bot.get_channel(giveaway['channel'])
+ channel: discord.TextChannel = self.bot.get_channel(giveaway["channel"])
if channel is None:
return None
- emb_color = await self.edit_embed(channel, giveaway['message'], winners)
+ emb_color = await self.edit_embed(channel, giveaway["message"], winners)
if emb_color is None:
# old embed wasn't found, we select a new color
emb_color = discord.Colour.random()
- win = await self.bot._(channel, "giveaways.embed.winners", count=len(winners), winner=" ".join([x.mention for x in winners]))
- desc = "{}: {} \n\n{}".format(await self.bot._(channel, "giveaways.embed.price"), giveaway['name'], win)
- emb = discord.Embed(title="Giveaway is over!", description=desc, color=emb_color)
+ win = await self.bot._(
+ channel,
+ "giveaways.embed.winners",
+ count=len(winners),
+ winner=" ".join([x.mention for x in winners]),
+ )
+ desc = "{}: {} \n\n{}".format(
+ await self.bot._(channel, "giveaways.embed.price"), giveaway["name"], win
+ )
+ emb = discord.Embed(
+ title="Giveaway is over!", description=desc, color=emb_color
+ )
await channel.send(embed=emb)
- self.db_delete_giveaway(giveaway['rowid'])
+ self.db_delete_giveaway(giveaway["rowid"])
-async def setup(bot):
- await bot.add_cog(Giveaways(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Giveaways(bot), icon="🎁")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/giveaways/langs/en.yml b/plugins/giveaways/langs/en.yml
index da9f1aa8..b8bd7c9b 100644
--- a/plugins/giveaways/langs/en.yml
+++ b/plugins/giveaways/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
giveaways:
creation:
diff --git a/plugins/giveaways/langs/fr.yml b/plugins/giveaways/langs/fr.yml
index 66f1e618..3bd23cef 100644
--- a/plugins/giveaways/langs/fr.yml
+++ b/plugins/giveaways/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
giveaways:
creation:
diff --git a/plugins/groups/credits.md b/plugins/groups/credits.md
new file mode 100644
index 00000000..cbe74a72
--- /dev/null
+++ b/plugins/groups/credits.md
@@ -0,0 +1,10 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021
+Copyright © Theaustudio 2022
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/groups/data/model.sql b/plugins/groups/data/model.sql
index 886413ea..56307fef 100644
--- a/plugins/groups/data/model.sql
+++ b/plugins/groups/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `groups` (
`guild` BIGINT NOT NULL,
`roleID` BIGINT NOT NULL,
diff --git a/plugins/groups/docs/user_documentation.md b/plugins/groups/docs/user_documentation.md
index fa64f3b3..fa95f735 100644
--- a/plugins/groups/docs/user_documentation.md
+++ b/plugins/groups/docs/user_documentation.md
@@ -1,3 +1,11 @@
-# Group
+
+
+# 🎭 Groups
+
+Nothing here... for the moment.
-Nothing here...
diff --git a/plugins/groups/bot/main.py b/plugins/groups/groups.py
similarity index 64%
rename from plugins/groups/bot/main.py
rename to plugins/groups/groups.py
index 42a8d449..45427c72 100644
--- a/plugins/groups/bot/main.py
+++ b/plugins/groups/groups.py
@@ -1,16 +1,32 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import Gunibot, MyContext
+from discord.ext import commands
+import discord
+from bot import checks
import asyncio
from typing import List
import sys
+
sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.ext import commands
-from utils import Gunibot, MyContext
class Group:
- def __init__(self, bot : Gunibot, guildID: int, roleID: int, ownerID: int, channelID: int, privacy: bool):
+ def __init__(
+ self,
+ bot: Gunibot,
+ guildID: int,
+ roleID: int,
+ ownerID: int,
+ channelID: int,
+ privacy: bool,
+ ):
self.roleID = roleID
self.ownerID = ownerID
self.channelID = channelID
@@ -19,36 +35,53 @@ def __init__(self, bot : Gunibot, guildID: int, roleID: int, ownerID: int, chann
self.id = None
self._role = None
self._channel = None
-
+
try:
bot.get_command("config").add_command(self.config_group_allowed_role)
bot.get_command("config").add_command(self.config_group_channel_category)
bot.get_command("config").add_command(self.config_group_over_role)
bot.get_command("config").add_command(self.config_max_group)
bot.get_command("config").add_command(self.config_backup)
- except commands.errors.CommandRegistrationError: pass
+ except commands.errors.CommandRegistrationError:
+ pass
@commands.command(name="group_allowed_role")
- async def config_group_allowed_role(self, ctx: MyContext, *, role: discord.Role=None):
+ async def config_group_allowed_role(
+ self, ctx: MyContext, *, role: discord.Role = None
+ ):
"""Role allowed to create groups"""
role = role.id if isinstance(role, discord.Role) else None
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "group_allowed_role", role))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "group_allowed_role", role)
+ )
@commands.command(name="group_channel_category")
- async def config_group_channel_category(self, ctx: MyContext, *, category: discord.CategoryChannel):
+ async def config_group_channel_category(
+ self, ctx: MyContext, *, category: discord.CategoryChannel
+ ):
"""Category were group channel will be created"""
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "group_channel_category", category.id))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "group_channel_category", category.id
+ )
+ )
@commands.command(name="group_over_role")
- async def config_group_over_role(self, ctx: MyContext, *, role: discord.Role = None):
+ async def config_group_over_role(
+ self, ctx: MyContext, *, role: discord.Role = None
+ ):
"""Role under the groups roles will be created"""
role = role.id if isinstance(role, discord.Role) else None
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "group_over_role", role))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "group_over_role", role)
+ )
@commands.command(name="max_group")
async def config_max_group(self, ctx: MyContext, *, number: int = None):
"""Max groups by user"""
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "max_group", number))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "max_group", number)
+ )
@commands.group(name="config-backup", aliases=["config-bkp"])
@commands.guild_only()
@@ -56,8 +89,8 @@ async def config_max_group(self, ctx: MyContext, *, number: int = None):
async def config_backup(self, ctx: MyContext):
"""Create or load your server configuration"""
if ctx.subcommand_passed is None:
- await ctx.send_help('config-backup')
-
+ await ctx.send_help("config-backup")
+
def role(self, bot: Gunibot) -> discord.Role:
"""Get the Discord Role attached to that group"""
if self._role is None:
@@ -81,12 +114,14 @@ def member_is_in(self, member: discord.Member) -> bool:
def to_str(self) -> str:
"""Transform the group to a human-readable string"""
- channel = f"<#{self.channelID}>" if self.channelID else 'None'
+ channel = f"<#{self.channelID}>" if self.channelID else "None"
private = "True" if self.privacy == 1 else "False"
return f"Group: <@&{self.roleID}> (*id : {self.roleID}*)\n┗━▷ Owner: <@{self.ownerID}> - Channel: {channel} - Private: {private}"
+
class GroupConverter(commands.Converter):
"""Convert a user argument to the corresponding group, by looking for the Role name/id/mention"""
+
async def convert(self, ctx: MyContext, arg: str) -> Group:
try:
# try to convert it to a role
@@ -100,43 +135,51 @@ async def convert(self, ctx: MyContext, arg: str) -> Group:
return res
raise commands.BadArgument(f'Group "{arg}" not found.')
-class Groups(commands.Cog):
+class Groups(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
- self.config_options = ['group_allowed_role', 'group_channel_category', 'group_over_role','max_group']
+ self.config_options = [
+ "group_allowed_role",
+ "group_channel_category",
+ "group_over_role",
+ "max_group",
+ ]
def db_get_config(self, guildID: int) -> List[Group]:
"""Get every group of a specific guild"""
- query = 'SELECT rowid, * FROM groups WHERE guild=?'
+ query = "SELECT rowid, * FROM groups WHERE guild=?"
liste = self.bot.db_query(query, (guildID,), astuple=True)
# comes as: (rowid, guild, roleID, ownerID, channelID, privacy)
res: List[Group] = list()
for row in liste:
- res.append(Group(self.bot,*row[1:]))
+ res.append(Group(self.bot, *row[1:]))
res[-1].id = row[0]
return res if len(res) > 0 else None
-
+
def db_get_group(self, guildID: int, roleID: int) -> Group:
"""Get a specific group from its role ID"""
- query = 'SELECT rowid, * FROM groups WHERE guild=? AND roleID=?;'
+ query = "SELECT rowid, * FROM groups WHERE guild=? AND roleID=?;"
res = self.bot.db_query(query, (guildID, roleID), fetchone=True, astuple=True)
# comes as: (rowid, guild, roleID, ownerID, channelID, privacy)
- if res is None: return None
+ if not res:
+ return None
group = Group(self.bot, *res[1:])
group.id = res[0]
return group
def db_get_n_group(self, guildID: int, ownerID) -> int:
"""Get the number of groups owned by someone in a specific guild"""
- query = 'SELECT COUNT(*) as count FROM groups WHERE guild=? AND ownerID=?'
+ query = "SELECT COUNT(*) as count FROM groups WHERE guild=? AND ownerID=?"
res = self.bot.db_query(query, (guildID, ownerID), fetchone=True)
- return res['count']
+ return res["count"]
def db_add_groups(self, guild, roleID, ownerID, privacy) -> int:
"""Add a group into a guild
Return the inserted row ID"""
- query = "INSERT INTO groups (guild, roleID, ownerID, privacy) VALUES (?, ?, ?, ?)"
+ query = (
+ "INSERT INTO groups (guild, roleID, ownerID, privacy) VALUES (?, ?, ?, ?)"
+ )
self.bot.db_query(query, (guild, roleID, ownerID, privacy))
def db_delete_group(self, guildID: int, toDelete) -> bool:
@@ -146,86 +189,104 @@ def db_delete_group(self, guildID: int, toDelete) -> bool:
rowcount = self.bot.db_query(query, (guildID, toDelete), returnrowcount=True)
return rowcount == 1
- def db_update_group_owner(self, guildID :int, roleID, ownerID) -> bool:
+ def db_update_group_owner(self, guildID: int, roleID, ownerID) -> bool:
"""Update a group from a guild, based on its row ID
Return True if a row was updated, False else"""
query = "UPDATE groups SET ownerID=? WHERE roleID=? AND guild=? "
- rowcount = self.bot.db_query(query, (ownerID,roleID,guildID), returnrowcount=True)
+ rowcount = self.bot.db_query(
+ query, (ownerID, roleID, guildID), returnrowcount=True
+ )
return rowcount == 1
- def db_update_group_privacy(self, guildID :int, roleID, privacy) -> bool:
+ def db_update_group_privacy(self, guildID: int, roleID, privacy) -> bool:
"""Update a group from a guild, based on its row ID
Return True if a row was updated, False else"""
query = "UPDATE groups SET privacy=? WHERE roleID=? AND guild=? "
- rowcount = self.bot.db_query(query, (privacy,roleID,guildID), returnrowcount=True)
+ rowcount = self.bot.db_query(
+ query, (privacy, roleID, guildID), returnrowcount=True
+ )
return rowcount == 1
- def db_update_group_channel(self, guildID :int, roleID, channelID) -> bool:
+ def db_update_group_channel(self, guildID: int, roleID, channelID) -> bool:
"""Update a group from a guild, based on its row ID
Return True if a row was updated, False else"""
query = "UPDATE groups SET channelID=? WHERE roleID=? AND guild=? "
- rowcount = self.bot.db_query(query, (channelID,roleID,guildID), returnrowcount=True)
+ rowcount = self.bot.db_query(
+ query, (channelID, roleID, guildID), returnrowcount=True
+ )
return rowcount == 1
@commands.group(name="group", aliases=["groups"])
@commands.guild_only()
async def group_main(self, ctx: MyContext):
"""Manage your groups
-
- by fantomitechno 🦊#5973"""
+ """
if ctx.subcommand_passed is None:
- await ctx.send_help('group')
+ await ctx.send_help("group")
return
- @group_main.command(name='add')
+ @group_main.command(name="add")
@commands.check(checks.can_group)
@commands.cooldown(1, 15, commands.BucketType.guild)
async def group_add(self, ctx: MyContext, name: str):
"""Create a new group
The name is only one word, no space allowed
-
+
Example: group add cool-guys"""
# remove spaces if needed
- name = name.replace(' ', '-')
+ name = name.replace(" ", "-")
# check if the role exists
role = discord.utils.get(ctx.guild.roles, name=name)
if role:
# if the role exists, check if a group is already created with it
check = self.db_get_group(ctx.guild.id, role.id)
if check:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.exist", name=name))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.exist", name=name)
+ )
return
config = ctx.bot.server_configs[ctx.guild.id]
- # if the user has already too many groups and is not a server admin, we abort it
- if self.db_get_n_group(ctx.guild.id, ctx.author.id) >= config["max_group"] and not ctx.author.guild_permissions.administrator:
+ # if the user has already too many groups and is not a server admin, we
+ # abort it
+ if (
+ self.db_get_n_group(ctx.guild.id, ctx.author.id) >= config["max_group"]
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.tomanygroup"))
return
# actually create the group role
- role = await ctx.guild.create_role(name=name, hoist=False, reason="A new group was created")
+ role = await ctx.guild.create_role(
+ name=name, hoist=False, reason="A new group was created"
+ )
# make sure to place it correctly if needed
if config["group_over_role"]:
under_role = ctx.guild.get_role(config["group_over_role"])
if under_role:
- await role.edit(position=under_role.position-1)
+ await role.edit(position=under_role.position - 1)
self.db_add_groups(ctx.guild.id, role.id, ctx.author.id, 1)
# add the user into the group (cuz well...)
await ctx.author.add_roles(role)
await ctx.send(await self.bot._(ctx.guild.id, "groups.created", name=name))
- @group_main.command(name='remove')
+ @group_main.command(name="remove")
@commands.check(checks.can_group)
async def group_remove(self, ctx: MyContext, group: GroupConverter):
"""Delete a group
Use its name, role ID or mention"""
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
return ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-owner"))
deleted = self.db_delete_group(ctx.guild.id, group.roleID)
- if deleted: # if everything went fine
+ if deleted: # if everything went fine
role = group.role(self.bot)
- await ctx.send(await self.bot._(ctx.guild.id, "groups.delete", name=role.name))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.delete", name=role.name)
+ )
await role.delete()
- # try to get the channel
+ # try to get the channel
if not (group.channelID and group.channel(self.bot)):
return
else:
@@ -234,13 +295,19 @@ async def group_remove(self, ctx: MyContext, group: GroupConverter):
if update:
# delete the channel now
await group.channel(self.bot).delete()
- await ctx.send(await self.bot._(ctx.guild.id, "groups.channel_delete", group=role.name))
- else: # oops
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-delete-channel"))
- else: # wtf?!
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "groups.channel_delete", group=role.name
+ )
+ )
+ else: # oops
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.no-delete-channel")
+ )
+ else: # wtf?!
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-delete"))
- @group_main.command(name='register')
+ @group_main.command(name="register")
@commands.check(checks.is_admin)
async def group_register(self, ctx: MyContext, role: discord.Role):
"""Register a group from an existing role
@@ -249,20 +316,26 @@ async def group_register(self, ctx: MyContext, role: discord.Role):
roleID = role.id
self.db_add_groups(ctx.guild.id, roleID, ctx.author.id, 1)
await ctx.author.add_roles(role)
- await ctx.send(await self.bot._(ctx.guild.id, "groups.registred", name=roleName))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.registred", name=roleName)
+ )
- @group_main.command(name='unregister')
+ @group_main.command(name="unregister")
@commands.check(checks.is_admin)
async def group_unregister(self, ctx: MyContext, group: GroupConverter):
"""Unregister a group without deleting the role
Use his ID, name or mention"""
roleID = group.roleID
deleted = self.db_delete_group(ctx.guild.id, roleID)
- if deleted: # deletion confirmed
+ if deleted: # deletion confirmed
roleName = group.role(self.bot).name
- await ctx.send(await self.bot._(ctx.guild.id, "groups.unregistred", name=roleName))
- else: # https://youtu.be/t3otBjVZzT0
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-unregistred"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.unregistred", name=roleName)
+ )
+ else: # https://youtu.be/t3otBjVZzT0
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.no-unregistred")
+ )
@group_main.group(name="modify", aliases=["edit"])
async def group_modify_main(self, ctx: MyContext):
@@ -270,41 +343,76 @@ async def group_modify_main(self, ctx: MyContext):
if ctx.subcommand_passed is None:
await ctx.send_help("group modify")
- @group_modify_main.command(name='leader')
+ @group_modify_main.command(name="leader")
@commands.cooldown(1, 30, commands.BucketType.user)
- async def group_modify_owner(self, ctx: MyContext, group: GroupConverter, user: discord.Member):
+ async def group_modify_owner(
+ self, ctx: MyContext, group: GroupConverter, user: discord.Member
+ ):
"""Edit the owner of a group"""
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-update"))
return
config = ctx.bot.server_configs[ctx.guild.id]
# if target has too many groups and is not a server admin, we abort it
- if self.db_get_n_group(ctx.guild.id, user.id) >= config["max_group"] and not user.guild_permissions.administrator:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.update_owner", user=user.name))
+ if (
+ self.db_get_n_group(ctx.guild.id, user.id) >= config["max_group"]
+ and not user.guild_permissions.administrator
+ ):
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.update_owner", user=user.name)
+ )
return
# ask the target to confirm the action
def check(reaction, user2):
- return user2.id == user.id and str(reaction.emoji) == '✅' and reaction.message.id == msg.id
+ return (
+ user2.id == user.id
+ and str(reaction.emoji) == "✅"
+ and reaction.message.id == msg.id
+ )
+
roleID = group.roleID
- msg = await ctx.send(await self.bot._(ctx.guild.id, "groups.give", user=user.mention, owner=ctx.author.name, group=group.role(self.bot).name))
+ msg = await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.give",
+ user=user.mention,
+ owner=ctx.author.name,
+ group=group.role(self.bot).name,
+ )
+ )
await msg.add_reaction("✅")
- try: # the target has now 60s to click on the reaction
- await self.bot.wait_for('reaction_add', timeout=60.0, check=check)
+ try: # the target has now 60s to click on the reaction
+ await self.bot.wait_for("reaction_add", timeout=60.0, check=check)
except asyncio.TimeoutError:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.timeout", user=user.name))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.timeout", user=user.name)
+ )
else:
# update the database
update = self.db_update_group_owner(ctx.guild.id, roleID, user.id)
if update:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.update_owner", owner=user.name, group=group.role(self.bot).name))
-
- @group_modify_main.command(name='name')
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.update_owner",
+ owner=user.name,
+ group=group.role(self.bot).name,
+ )
+ )
+
+ @group_modify_main.command(name="name")
@commands.cooldown(1, 10, commands.BucketType.user)
async def group_modify_name(self, ctx: MyContext, group: GroupConverter, name):
"""Edit the name of a group"""
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-update"))
return
roleName = group.role(self.bot).name
@@ -313,29 +421,45 @@ async def group_modify_name(self, ctx: MyContext, group: GroupConverter, name):
# if we should also update the channel name
if roleName.lower() == group.channel(self.bot).name:
await group.channel(self.bot).edit(name=name)
- await ctx.send(await self.bot._(ctx.guild.id, "groups.update_name", name=name, group=roleName))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "groups.update_name", name=name, group=roleName
+ )
+ )
@group_modify_main.command(name="privacy")
@commands.cooldown(1, 20, commands.BucketType.user)
- async def group_modify_privacy(self, ctx: MyContext, group: GroupConverter, privacy: str):
+ async def group_modify_privacy(
+ self, ctx: MyContext, group: GroupConverter, privacy: str
+ ):
"""Edit the privacy of a group
Privacy parameter needs to be either 'private' or 'public'
-
+
Example: group modify privacy CoolGuys private"""
# if user is nor group owner nor server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-update"))
return
# if parameter isn't what we expected
- if privacy.lower() not in ('public', 'private'):
+ if privacy.lower() not in ("public", "private"):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.badarg"))
return
# it's private if the user asked to (yes)
- private = (privacy.lower() == 'private')
+ private = privacy.lower() == "private"
update = self.db_update_group_privacy(ctx.guild.id, group.roleID, private)
if update:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.update_privacy", privacy=privacy, group=group.role(self.bot).name))
- else: # bruh
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.update_privacy",
+ privacy=privacy,
+ group=group.role(self.bot).name,
+ )
+ )
+ else: # bruh
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-update"))
@group_main.command(name="list")
@@ -344,7 +468,9 @@ async def group_list(self, ctx: MyContext):
"""List server's groups"""
groups = self.db_get_config(ctx.guild.id)
if not groups: # we can't list an empty list
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-group", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.no-group", p=ctx.prefix)
+ )
return
txt = "**" + await self.bot._(ctx.guild.id, "groups.list") + "**\n"
for group in groups:
@@ -360,7 +486,7 @@ async def group_list(self, ctx: MyContext):
@commands.cooldown(1, 10, commands.BucketType.user)
async def group_join(self, ctx: MyContext, group: GroupConverter):
"""Join a group"""
- if group.privacy is None: # group doesn't exist
+ if group.privacy is None: # group doesn't exist
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-exist"))
return
# if user is already in it (duh)
@@ -371,7 +497,11 @@ async def group_join(self, ctx: MyContext, group: GroupConverter):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.private"))
else:
await ctx.author.add_roles(group.role(self.bot), reason="Joined a group")
- await ctx.send(await self.bot._(ctx.guild.id, "groups.join", name=group.role(self.bot).name))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "groups.join", name=group.role(self.bot).name
+ )
+ )
@group_main.command(name="leave")
@commands.cooldown(1, 10, commands.BucketType.user)
@@ -386,9 +516,13 @@ async def group_leave(self, ctx: MyContext, group: GroupConverter):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-in"))
return
await ctx.author.remove_roles(group.role(self.bot), reason="Left a group")
- await ctx.send(await self.bot._(ctx.guild.id, "groups.leave", name=group.role(self.bot).name))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "groups.leave", name=group.role(self.bot).name
+ )
+ )
- @group_main.group(name="admin", aliases=['manage'])
+ @group_main.group(name="admin", aliases=["manage"])
@commands.cooldown(1, 10, commands.BucketType.guild)
async def group_admin_main(self, ctx: MyContext):
"""Manage the users in your group"""
@@ -400,7 +534,10 @@ async def group_admin_main(self, ctx: MyContext):
async def group_admin_list(self, ctx: MyContext, group: GroupConverter):
"""Give the userlist of your group"""
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-owner"))
return
txt = "**" + await self.bot._(ctx.guild.id, "groups.userlist") + "**\n"
@@ -416,26 +553,45 @@ async def group_admin_list(self, ctx: MyContext, group: GroupConverter):
@group_admin_main.command(name="add")
@commands.cooldown(1, 8, commands.BucketType.user)
- async def group_admin_add(self, ctx: MyContext, group: GroupConverter, user: discord.Member):
+ async def group_admin_add(
+ self, ctx: MyContext, group: GroupConverter, user: discord.Member
+ ):
"""Add a user to a group (by force)
Use that if the group is set to private"""
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-owner"))
return
# if target is already in it
if group.member_is_in(user):
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.already-in-user"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.already-in-user")
+ )
return
await user.add_roles(group.role(self.bot))
- await ctx.send(await self.bot._(ctx.guild.id, "groups.joinbyforce", name=group.role(self.bot).name, user=user.name))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.joinbyforce",
+ name=group.role(self.bot).name,
+ user=user.name,
+ )
+ )
@group_admin_main.command(name="remove")
@commands.cooldown(1, 8, commands.BucketType.user)
- async def group_admin_remove(self, ctx: MyContext, group: GroupConverter, user: discord.Member):
+ async def group_admin_remove(
+ self, ctx: MyContext, group: GroupConverter, user: discord.Member
+ ):
"""Remove a user to a group (by force)"""
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-owner"))
return
# if target is not in the group
@@ -443,7 +599,14 @@ async def group_admin_remove(self, ctx: MyContext, group: GroupConverter, user:
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-in-user"))
return
await user.remove_roles(group.role(self.bot))
- await ctx.send(await self.bot._(ctx.guild.id, "groups.leavebyforce", name=group.role(self.bot).name, user=user.name))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.leavebyforce",
+ name=group.role(self.bot).name,
+ user=user.name,
+ )
+ )
@group_main.group(name="channel")
async def group_channel_main(self, ctx: MyContext):
@@ -456,10 +619,13 @@ async def group_channel_main(self, ctx: MyContext):
async def group_channel_remove(self, ctx: MyContext, group: GroupConverter):
"""Remove a group channel"""
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-owner"))
return
- # try to get the channel
+ # try to get the channel
if not (group.channelID and group.channel(self.bot)):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-channel"))
return
@@ -469,9 +635,17 @@ async def group_channel_remove(self, ctx: MyContext, group: GroupConverter):
if update:
# delete the channel now
await group.channel(self.bot).delete()
- await ctx.send(await self.bot._(ctx.guild.id, "groups.channel_delete", group=group.role(self.bot).name))
- else: # oops
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-delete-channel"))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.channel_delete",
+ group=group.role(self.bot).name,
+ )
+ )
+ else: # oops
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.no-delete-channel")
+ )
@group_channel_main.command(name="add")
@commands.cooldown(1, 30, commands.BucketType.user)
@@ -481,7 +655,10 @@ async def group_channel_add(self, ctx: MyContext, group: GroupConverter, name=No
if not name:
name = group.role(self.bot).name
# if user is not the group owner and neither a server admin, we abort
- if group.ownerID != ctx.author.id and not ctx.author.guild_permissions.administrator:
+ if (
+ group.ownerID != ctx.author.id
+ and not ctx.author.guild_permissions.administrator
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "groups.error.not-owner"))
return
# if channel already exists
@@ -491,27 +668,39 @@ async def group_channel_add(self, ctx: MyContext, group: GroupConverter, name=No
# if no category has been created
config = ctx.bot.server_configs[ctx.guild.id]
if config["group_channel_category"] is None:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-category", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.no-category", p=ctx.prefix)
+ )
return
# if category can't be found (probably got deleted)
categ = ctx.guild.get_channel(config["group_channel_category"])
if categ is None:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.error.no-category", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "groups.error.no-category", p=ctx.prefix)
+ )
return
# prepare channel overwrites
overwrite = {
ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False),
- group.role(self.bot): discord.PermissionOverwrite(read_messages=True)
+ group.role(self.bot): discord.PermissionOverwrite(read_messages=True),
}
# create channel, save it, say success, end of the story.
- channel = await ctx.guild.create_text_channel(name=name, overwrites=overwrite, category=categ)
+ channel = await ctx.guild.create_text_channel(
+ name=name, overwrites=overwrite, category=categ
+ )
self.db_update_group_channel(ctx.guild.id, group.roleID, channel.id)
- await ctx.send(await self.bot._(ctx.guild.id, "groups.channel-create", name=group.role(self.bot).name))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "groups.channel-create", name=group.role(self.bot).name
+ )
+ )
@group_channel_main.command(name="register")
@commands.check(checks.is_admin)
@commands.cooldown(1, 30, commands.BucketType.guild)
- async def group_channel_register(self, ctx: MyContext, group: GroupConverter, channel: discord.TextChannel):
+ async def group_channel_register(
+ self, ctx: MyContext, group: GroupConverter, channel: discord.TextChannel
+ ):
"""Register a channel as a group channel
You'll have to edit the permissions yourself :/"""
# if a channel already exists for that group
@@ -520,7 +709,11 @@ async def group_channel_register(self, ctx: MyContext, group: GroupConverter, ch
return
# update database, say yeepee
self.db_update_group_channel(ctx.guild.id, group.roleID, channel.id)
- await ctx.send(await self.bot._(ctx.guild.id, "groups.channel-registred", name=group.role(self.bot).name))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "groups.channel-registred", name=group.role(self.bot).name
+ )
+ )
@group_channel_main.command(name="unregister")
@commands.check(checks.is_admin)
@@ -535,8 +728,18 @@ async def group_channel_unregister(self, ctx: MyContext, group: GroupConverter):
else:
update = self.db_update_group_channel(ctx.guild.id, group.roleID, None)
if update:
- await ctx.send(await self.bot._(ctx.guild.id, "groups.channel_unregister", group=group.role(self.bot).name))
-
-
-async def setup(bot):
- await bot.add_cog(Groups(bot))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "groups.channel_unregister",
+ group=group.role(self.bot).name,
+ )
+ )
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Groups(bot), icon="🎭")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/groups/langs/en.yml b/plugins/groups/langs/en.yml
index dcbe8b30..4f9db9e3 100644
--- a/plugins/groups/langs/en.yml
+++ b/plugins/groups/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
groups:
list: "List of server groups:"
diff --git a/plugins/groups/langs/fr.yml b/plugins/groups/langs/fr.yml
index 2608f73d..ae7dc2b9 100644
--- a/plugins/groups/langs/fr.yml
+++ b/plugins/groups/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
groups:
list: "Liste des groupes du serveur :"
@@ -40,4 +45,4 @@ fr:
no-channel: "Ce group ne possède pas de salon"
no-delete-channel: "Impossible de trouver un groupe avec cet identifiant ou ce nom"
channel-exist: "Ce groupe possède déja un salon"
- no-category: "Vous devez d'abord configuré une catégorie pour les salons de groupes : `%{p}config group_channel_category`"
\ No newline at end of file
+ no-category: "Vous devez d'abord configurer une catégorie pour les salons de groupes : `%{p}config group_channel_category`"
\ No newline at end of file
diff --git a/plugins/help/config/options.json b/plugins/help/config/options.json
index 607ee353..9fd48c6c 100644
--- a/plugins/help/config/options.json
+++ b/plugins/help/config/options.json
@@ -1,6 +1,6 @@
{
"help_embed_color": {
- "default": 0,
+ "default": 3092790,
"type": "int",
"command": "help_embed_color"
},
diff --git a/plugins/help/credits.md b/plugins/help/credits.md
new file mode 100644
index 00000000..30ed52c4
--- /dev/null
+++ b/plugins/help/credits.md
@@ -0,0 +1,9 @@
+Copyright © ascpial 2021 - 2023
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/help/bot/main.py b/plugins/help/help.py
similarity index 60%
rename from plugins/help/bot/main.py
rename to plugins/help/help.py
index 4960030c..062e6fc7 100644
--- a/plugins/help/bot/main.py
+++ b/plugins/help/help.py
@@ -1,13 +1,54 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from __future__ import annotations
+
import inspect
import itertools
import typing
from typing import Any, List, Optional, Union
+import math
import discord
from discord.ext import commands
from utils import Gunibot, MyContext
+def permission_check(callback: callable):
+ """Decorator used to check if the user has the permission to use the
+ command and returns a "not found" message else.
+
+ This is used to prevent the user from seing commands he can't run.
+ """
+
+ async def decorator(
+ self: Help,
+ command: Union[commands.Command, commands.Group, commands.Cog]
+ ):
+ # to check if the user can run the command in the context, we use the
+ # filter function of the helper class to remove the command if the user
+ # don't have the permission
+ if issubclass(type(command), commands.Cog):
+ # we check the permission for each command
+ if len(await self.filter_commands(command.get_commands())) == 0:
+ return await self.send_error_message(
+ await self.command_not_found(command.qualified_name)
+ )
+ else:
+ if len(await self.filter_commands([command])) == 0: # the user can't use the command
+ return await self.send_error_message(
+ await self.command_not_found(command.qualified_name)
+ )
+
+ # the user can use the command
+ return await callback(self, command)
+
+ return decorator
+
class Help(commands.HelpCommand):
ANNOTATION_TRANSLATION = {
discord.User: "annotation.user",
@@ -47,43 +88,49 @@ async def get_help_embed(self, *args, **kwargs) -> discord.Embed:
The help embed
"""
- #load the config options
- color = self.context.bot.config.get("help_embed_color", 0)
- author = self.context.bot.config.get("help_author").format(user=self.context.bot.user)
- icon_url = self.context.bot.config.get("help_author_icon_url").format(user=self.context.bot.user)
-
- embed = discord.Embed(
- *args, **kwargs,
- color=color
+ # load the config options
+ color = self.context.bot.server_configs[self.context.guild.id].get("help_embed_color", 0)
+ author = self.context.bot.server_configs[self.context.guild.id].get("help_author").format(
+ user=self.context.bot.user
)
-
- embed.set_author(
- name=author,
- icon_url=icon_url
+ icon_url = self.context.bot.server_configs[self.context.guild.id].get("help_author_icon_url").format(
+ user=self.context.bot.user
)
+
+ embed = discord.Embed(*args, **kwargs, color=color)
+
embed.set_footer(
- text=await self.context.bot._(self.context, "help.help-tip", prefix=self.context.clean_prefix)
+ text=await self.context.bot._(
+ self.context, "help.help-tip", prefix=self.context.clean_prefix
+ )
)
-
+
return embed
- async def get_bot_command_formating(self, commands_: List[commands.Command]) -> str:
+ async def get_bot_command_formating(self, commands_: List[commands.Command], size:int=None) -> str:
"""Returns a string representing `commands_`
-
+
Attributes
----------
commands_: List[:class:`discord.ext.commands.Command`]
The commands fro which to return the string representation
-
+
Returns
-------
:class:`str`
The string representation of the commands
"""
- output = ''
+ output = "```asciidoc\n"
+
+ # Get max size of command name
+ name_size = 0
+ for commands in commands_:
+ name_size = max(name_size, len(commands.name))
+
for command in commands_:
- output += await self.get_command_list_string(command)
+ output += await self.get_command_list_string(command, name_size=name_size, total_size=size)
output += "\n"
+ output += "```"
return output
async def get_type_string(self, type: Any) -> Optional[str]:
@@ -94,7 +141,7 @@ async def get_type_string(self, type: Any) -> Optional[str]:
----------
type: Any
The type for which to return the string representation
-
+
Returns
-------
Optional[:class:`str`]
@@ -102,36 +149,40 @@ async def get_type_string(self, type: Any) -> Optional[str]:
If not translation is found, it return None
"""
if type in self.ANNOTATION_TRANSLATION:
- return await self.context.bot._(self.context, self.ANNOTATION_TRANSLATION[type])
-
- async def get_annotation_type_string(self, parameter: inspect.Parameter) -> Optional[str]:
+ return await self.context.bot._(
+ self.context, self.ANNOTATION_TRANSLATION[type]
+ )
+
+ async def get_annotation_type_string(
+ self, parameter: inspect.Parameter
+ ) -> Optional[str]:
"""Returns the string representation of type annotation in parameter
-
+
Attributes
----------
annotation: :class:`inspect.Parameter`
The annotation for which to return the type string representation
-
+
Returns
-------
Optional[:class:`str`]
The string representation
If no translation can be found, it returns None
"""
- type_str = await self.get_type_string(parameter.annotation)
+ type_str = await self.get_type_string(parameter.annotation)
if type_str is None:
return await self.get_type_string(parameter.default)
else:
return type_str
-
+
async def get_parameter_string(self, parameter: inspect.Parameter) -> str:
"""Returns the string representation of a command parameter
-
+
Attributes
----------
parameter: :class:`inspect.Parameter`
The parameter for which to return the string representation
-
+
Returns
-------
:class:`str`
@@ -139,16 +190,16 @@ async def get_parameter_string(self, parameter: inspect.Parameter) -> str:
"""
annotation = parameter.annotation
types = []
- if type(annotation) == commands.Greedy:
+ if isinstance(annotation, commands.Greedy):
type_ = await self.get_type_string(annotation.converter)
if type_ is not None:
types.append(
await self.context.bot._(self.context, "help.greedy", type=type_)
)
- elif type(annotation) == typing._UnionGenericAlias:
+ elif isinstance(annotation, typing._UnionGenericAlias):
for arg in annotation.__args__:
type_ = await self.get_type_string(arg)
- if type_ != None:
+ if type_ is not None:
types.append(type_)
else:
type_ = await self.get_annotation_type_string(parameter)
@@ -156,17 +207,18 @@ async def get_parameter_string(self, parameter: inspect.Parameter) -> str:
types.append(type_)
return ", ".join(types) if len(types) > 0 else None
-
- async def get_parameters_string(self, command: commands.Command, sep="\n") -> Optional[str]:
+ async def get_parameters_string(
+ self, command: commands.Command, sep="\n"
+ ) -> Optional[str]:
"""Returns the string representing all command parameters
-
+
Attributes
----------
command: :class:`discord.ext.commands.Command`
The command for which to get the parameters string representation
sep: :class:`str`
The separator to put between parameters
-
+
Returns
-------
Optional[:class:`str`]
@@ -182,70 +234,94 @@ async def get_parameters_string(self, command: commands.Command, sep="\n") -> Op
type = await bot._(self.context, "help.type", type=type)
else:
type = ""
-
- if parameter.default and parameter.default != inspect._empty: #parse default
+
+ if (
+ parameter.default and parameter.default != inspect._empty
+ ): # parse default
if parameter.default in self.DEFAULT_TRANSLATION:
- default = await bot._(self.context, self.DEFAULT_TRANSLATION[parameter.default])
+ default = await bot._(
+ self.context, self.DEFAULT_TRANSLATION[parameter.default]
+ )
default = await bot._(self.context, "help.default", default=default)
else:
- default = await bot._(self.context, "help.default", default=repr(parameter.default))
+ default = await bot._(
+ self.context, "help.default", default=repr(parameter.default)
+ )
else:
default = ""
-
- result += f"**{name}**{type}{default}"
- result += sep #add end separator
+
+ result += f"**`{name}`**{type}{default}"
+ result += sep # add end separator
return result if len(result) > 0 else None
-
- async def get_command_list_string(self, command: commands.Command) -> str:
+
+ async def get_command_list_string(
+ self, command: commands.Command,
+ name_size:int=0,
+ total_size:int=0
+ ) -> str:
"""Returns a string representing `command` in a list of commands
-
+
Attributes
----------
command: :class:`discord.ext.commands.Command`
The command for which to return the representation
-
+
Returns
-------
:class:`str`
The string representation of `command`
"""
- name = f"• **{command.name}**"
+ name = f"{command.name.ljust(name_size)} :: "
+ total_size += 4 # number of additionnal characters
if command.short_doc:
- short_doc = await self.context.bot._(self.context, "help.short_doc", short_doc=command.short_doc[:40]+("…" if len(command.short_doc)>40 else ""))
+ short_doc = await self.context.bot._(
+ self.context,
+ "help.short_doc",
+ short_doc=command.short_doc#[:40]
+ #+ ("…" if len(command.short_doc) > 40 else ""),
+ )
+ short_doc = short_doc[3:-1]
else:
short_doc = ""
- return name+short_doc
-
- async def get_subcommand_string(self, group: Union[commands.Group, commands.Cog], sep="\n") -> Optional[str]:
+ return (name + short_doc).ljust(total_size)
+
+ async def get_subcommand_string(
+ self, group: Union[commands.Group, commands.Cog], sep="\n"
+ ) -> Optional[str]:
"""Returns the string representing all group subcommands
-
+
Attributes
----------
group: :class:`discord.ext.commands.Group`
The group for which to get the subcommand string representation
sep: :class:`str`
The separator to put between subcommands
-
+
Returns
-------
Optional[:class:`str`]
The string representation of group subcommands
If the group has no subcommand, the function returns None
"""
- bot: Gunibot = self.context.bot
- result = ""
-
- if type(group) == commands.Group:
- commands_ = sorted(group.commands, key=lambda command: command.name)
+ result = "```asciidoc\n"
+
+ if isinstance(group, commands.Group):
+ commands_ = sorted(
+ await self.filter_commands(group.commands),
+ key=lambda command: command.name
+ )
elif issubclass(type(group), commands.Cog):
- commands_ = sorted(group.get_commands(), key=lambda command: command.name)
+ commands_ = sorted(
+ await self.filter_commands(group.get_commands()),
+ key=lambda command: command.name
+ )
for command in commands_:
result += await self.get_command_list_string(command)
result += sep
-
+
if len(result) > 1024:
result = "**"
for command in commands_:
@@ -254,9 +330,11 @@ async def get_subcommand_string(self, group: Union[commands.Group, commands.Cog]
result += "**"
if len(result) > 1024:
result = result[:1023] + "…"
+ else:
+ result += "\n```"
return result if len(result) > 0 else None
-
+
async def add_aliases(self, command: Union[commands.Command, commands.Group]):
"""Add the alias field in the embed if necessary
@@ -265,15 +343,15 @@ async def add_aliases(self, command: Union[commands.Command, commands.Group]):
command: Union[:class:`commands.Command`, :class:`commands.Group`]
The command for which to check the aliases
"""
-
+
if len(command.aliases) > 0:
- aliases = '`'+"`, `".join(command.aliases+[command.name])+'`'
+ aliases = "`" + "`, `".join(command.aliases + [command.name]) + "`"
self.embed.add_field(
name=await self.context.bot._(self.context, "help.alias"),
value=aliases,
- inline=False
+ inline=False,
)
-
+
async def add_parameters(self, command: Union[commands.Command, commands.Group]):
"""Add the parameters field in the embed if necessary
@@ -287,9 +365,9 @@ async def add_parameters(self, command: Union[commands.Command, commands.Group])
self.embed.add_field(
name=await self.context.bot._(self.context, "help.parameters"),
value=parameters,
- inline=False
+ inline=False,
)
-
+
async def add_subcommands(self, group: Union[commands.Group, commands.Cog]):
"""Add the subcommands field in the embed if necessary
@@ -303,7 +381,7 @@ async def add_subcommands(self, group: Union[commands.Group, commands.Cog]):
self.embed.add_field(
name=await self.context.bot._(self.context, "help.subcommands"),
value=subcommands,
- inline=False
+ inline=False,
)
async def send_bot_help(self, mapping) -> None:
@@ -311,33 +389,58 @@ async def send_bot_help(self, mapping) -> None:
ctx = self.context
bot: Gunibot = ctx.bot
- self.embed = await self.get_help_embed(
- title=await bot._(ctx, "help.bot-help-title")
- )
-
- if bot.description:
- self.embed.description = f"```\n{bot.description}```"
-
no_category = await bot._(ctx, "help.no-category")
+
def get_category(command, *, no_category=no_category):
cog = command.cog
return cog.qualified_name if cog is not None else no_category
+
filtered = await self.filter_commands(bot.commands, sort=True, key=get_category)
to_iterate = itertools.groupby(filtered, key=get_category)
+ max_lenght = 0
+ for category, commands_ in to_iterate:
+ for command in commands_:
+ max_lenght = max(max_lenght, len(command.name) + len(command.short_doc))
+
+ embeds = []
+ to_iterate = itertools.groupby(filtered, key=get_category)
for category, commands_ in to_iterate:
commands_ = sorted(commands_, key=lambda c: c.name)
- self.embed.add_field(
- name=f"{category}",
- value=await self.get_bot_command_formating(commands_),
- inline=False
+
+ icon = ""
+ if bot.get_cog_icon(category):
+ icon = bot.get_cog_icon(category)
+
+ embeds.append(
+ await self.get_help_embed(
+ title= f"{icon} {category}",
+ description=await self.get_bot_command_formating(commands_, size=max_lenght)
+ )
)
-
- await ctx.send(embed=self.embed)
-
+ if len(commands_) == 1:
+ embeds[-1].set_footer(
+ text=await bot._(
+ ctx, "help.help-cog-tip",
+ prefix=self.context.clean_prefix,
+ cog=commands_[0].name
+ )
+ )
+ else:
+ embeds[-1].set_footer(
+ text=await bot._(
+ ctx, "help.help-tip",
+ prefix=self.context.clean_prefix
+ )
+ )
+
+ for i in range(int(math.ceil(len(embeds)/10))):
+ await ctx.send(embeds=embeds[i*10: min((i+1)*10, len(embeds))])
+
+ @permission_check
async def send_command_help(self, command: commands.Command) -> None:
"""Send the help message for command in the context channel
-
+
Attributes
----------
command: :class:`discord.ext.commands.Command`
@@ -346,27 +449,35 @@ async def send_command_help(self, command: commands.Command) -> None:
ctx = self.context
bot: Gunibot = ctx.bot
self.embed = await self.get_help_embed(
- title = await bot._(ctx, "help.command-help-title", name=command.name)
+ title=await bot._(ctx, "help.command-help-title", command=command.name)
+ )
+
+ description = (
+ "```autohotkey\n" # include signature and description in the same code field
+ )
+ description += await bot._(
+ ctx,
+ "help.help-signature-format",
+ signature=self.get_command_signature(command),
)
- description = "```\n" #include signature and description in the same code field
- description += await bot._(ctx, "help.help-signature-format", signature=self.get_command_signature(command))
+ description += "\n```"
if command.help != "":
- description += f"\n{command.help}"
- description += "```"
-
+ description += f"{command.help}"
+
self.embed.description = description
await self.add_parameters(command)
-
+
await self.add_aliases(command)
await ctx.send(embed=self.embed)
-
+
+ @permission_check
async def send_group_help(self, group: commands.Group) -> None:
"""Send the help message for a group in the context channel
-
+
Attributes
----------
group: :class:`discord.ext.commands.Group`
@@ -376,29 +487,36 @@ async def send_group_help(self, group: commands.Group) -> None:
bot: Gunibot = ctx.bot
self.embed = await self.get_help_embed(
- title = await bot._(ctx, "help.group-help-title", name=group.name)
+ title=await bot._(ctx, "help.group-help-title", name=group.name)
)
- description = "```\n" #include signature and description in the same code field
- description += await bot._(ctx, "help.help-signature-format", signature=self.get_command_signature(group))
+ description = (
+ "```\n" # include signature and description in the same code field
+ )
+ description += await bot._(
+ ctx,
+ "help.help-signature-format",
+ signature=self.get_command_signature(group),
+ )
+ description += "\n```"
if group.help != "":
- description += f"\n{group.help}"
- description += "```"
+ description += f"{group.help}"
self.embed.description = description
await self.add_parameters(group)
-
+
await self.add_subcommands(group)
-
+
await self.add_aliases(group)
await ctx.send(embed=self.embed)
-
+
+ @permission_check
async def send_cog_help(self, cog: commands.Cog) -> None:
"""Send the help message for the cog in the context channel
-
+
Attributes
----------
cog: :class:`discord.ext.commands.Cog`
@@ -408,7 +526,7 @@ async def send_cog_help(self, cog: commands.Cog) -> None:
bot: Gunibot = ctx.bot
self.embed = await self.get_help_embed(
- title = await bot._(ctx, "help.cog-help-title", name=cog.qualified_name)
+ title=await bot._(ctx, "help.cog-help-title", name=cog.qualified_name)
)
description = ""
@@ -417,24 +535,24 @@ async def send_cog_help(self, cog: commands.Cog) -> None:
description += f"\n```\n{cog.description}```"
self.embed.description = description
-
+
await self.add_subcommands(cog)
await ctx.send(embed=self.embed)
-
+
async def command_not_found(self, command: str) -> str:
"""Return the string for command not found error
-
+
Attributes
----------
command: :class:`str`
The command name
"""
return await self.context.bot._(self.context, "help.not-found", command=command)
-
+
async def send_error_message(self, error: str) -> None:
"""Raise the help error message in context channel
-
+
Attributes
----------
error: :class:`str`
@@ -443,30 +561,35 @@ async def send_error_message(self, error: str) -> None:
ctx = self.context
bot: Gunibot = ctx.bot
- self.embed = await self.get_help_embed(
- title=error
- )
+ self.embed = await self.get_help_embed(title=error)
await ctx.send(embed=self.embed)
class HelpCog(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = "help"
- self.config_options = ["help_embed_color", "help_author", "help_author_icon_url"]
+ self.config_options = [
+ "help_embed_color",
+ "help_author",
+ "help_author_icon_url",
+ ]
bot.get_command("config").add_command(self.help_embed_color)
bot.get_command("config").add_command(self.help_author)
bot.get_command("config").add_command(self.help_author_icon_url)
-
+
@commands.command(name="help_embed_color")
@commands.guild_only()
async def help_embed_color(self, ctx: MyContext, color: discord.Color):
"""Edit the help embed color"""
# save result
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "help_embed_color", color.value))
-
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "help_embed_color", color.value
+ )
+ )
+
@commands.command(name="help_author")
@commands.guild_only()
async def help_author(self, ctx: MyContext, *, text: str):
@@ -474,18 +597,28 @@ async def help_author(self, ctx: MyContext, *, text: str):
if len(text) > 250:
await ctx.send("Your text can't be longer than 250 characters")
return
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "help_author", text))
-
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "help_author", text)
+ )
+
@commands.command(name="help_author_icon_url")
@commands.guild_only()
async def help_author_icon_url(self, ctx: MyContext, url: str):
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "help_author_icon_url", url))
-
-
-async def setup(bot: Gunibot):
- bot.help_command = Help()
- await bot.add_cog(HelpCog(bot))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "help_author_icon_url", url
+ )
+ )
-def teardown(bot: Gunibot):
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ bot.help_command = Help()
+ await bot.add_cog(HelpCog(bot), icon="🤝")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
+async def teardown(bot: Gunibot):
bot.help_command = commands.DefaultHelpCommand()
- bot.remove_cog("HelpCog")
+ await bot.remove_cog("HelpCog")
diff --git a/plugins/help/langs/en.yml b/plugins/help/langs/en.yml
index bd6a88c0..7fa12fcb 100644
--- a/plugins/help/langs/en.yml
+++ b/plugins/help/langs/en.yml
@@ -1,19 +1,25 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
help:
bot-help-title: Help for Gipsy bot
- help-tip: Use %{prefix}help [command] for more on a command !
- command-help-title: Help for the command {0}
- help-signature-format: "`%{signature}`"
- group-help-title: Help for the group `%{name}`
- cog-help-title: Help for the cog `%{name}`
+ help-tip: Use "%{prefix}help [command]" for more on a command!
+ help-cog-tip: Use "%{prefix}help %{cog}" for more on this plugin!
+ command-help-title: Help for the command `%{command}`
+ help-signature-format: "%{signature}"
+ group-help-title: Help for the command `%{name}`
+ cog-help-title: Help for the plugin `%{name}`
no-category: No category
type: ": %{type}"
default: " = `%{default}`"
short_doc: ": *%{short_doc}*"
- parameters: Commande parameters
+ parameters: Command parameters
alias: Aliases
subcommands: Subcommands
- not-found: The command "%{command}" does not exist !
+ not-found: The command "%{command}" does not exist!
greedy: "Suite : %{type}"
annotation:
diff --git a/plugins/help/langs/fr.yml b/plugins/help/langs/fr.yml
index 2acdbae1..f4fb4c4a 100644
--- a/plugins/help/langs/fr.yml
+++ b/plugins/help/langs/fr.yml
@@ -1,8 +1,14 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
help:
bot-help-title: Aide pour le bot Gipsy
- help-tip: Utilisez %{prefix}help [command] pour en savoir plus sur une commande !
- command-help-title: Aide pour la commande `%{name}`
+ help-tip: Utilisez "%{prefix}help [command]" pour en savoir plus sur une commande !
+ help-cog-tip: Utilisez "%{prefix}help %{cog}" pour en savoir plus sur ce plugin !
+ command-help-title: Aide pour la commande `%{command}`
help-signature-format: "%{signature}\n"
group-help-title: Aide pour le groupe `%{name}`
cog-help-title: Aide pour le cog `%{name}`
diff --git a/plugins/hypesquad/credits.md b/plugins/hypesquad/credits.md
new file mode 100644
index 00000000..dbafbba5
--- /dev/null
+++ b/plugins/hypesquad/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/hypesquad/docs/user_documentation.md b/plugins/hypesquad/docs/user_documentation.md
index 79f48a43..18333975 100644
--- a/plugins/hypesquad/docs/user_documentation.md
+++ b/plugins/hypesquad/docs/user_documentation.md
@@ -1,3 +1,11 @@
-# **Hypesquad**
+
+
+# 🔰 Hypesquad
You can configure the bot to automatically add a role based on the users hypesquad home. This role will be updated for all members every 12 hours, or for a specific member when he joins the server. This feature allows you to create house rooms and organize events using the houses in Discord HypeSquad.
+
diff --git a/plugins/hypesquad/bot/main.py b/plugins/hypesquad/hypesquad.py
similarity index 52%
rename from plugins/hypesquad/bot/main.py
rename to plugins/hypesquad/hypesquad.py
index 3feffa3e..81605e5f 100644
--- a/plugins/hypesquad/bot/main.py
+++ b/plugins/hypesquad/hypesquad.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import time
from typing import Dict
@@ -6,37 +13,56 @@
from discord.ext import commands, tasks
from utils import Gunibot, MyContext
-class Hypesquad(commands.Cog):
+class Hypesquad(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
- self.config_options = ['hs_bravery_role', 'hs_brilliance_role', 'hs_balance_role', 'hs_none_role']
+ self.config_options = [
+ "hs_bravery_role",
+ "hs_brilliance_role",
+ "hs_balance_role",
+ "hs_none_role",
+ ]
self.roles_loop.start()
bot.get_command("config").add_command(self.hs_main)
- @commands.group(name="hypesquad", aliases=['hs'], enabled=False)
+ @commands.group(name="hypesquad", aliases=["hs"], enabled=False)
async def hs_main(self, ctx: MyContext):
"""Manage options about Discord ypesquads"""
if ctx.subcommand_passed is None:
await ctx.send_help("config hypesquad")
-
+
@hs_main.command(name="role")
- async def hs_role(self, ctx: MyContext, house: str, *, role: discord.Role=None):
+ async def hs_role(self, ctx: MyContext, house: str, *, role: discord.Role = None):
"""Set a role to give to a hypesquad house members
Valid houses are: bravery, brilliance, balance and none"""
role = role.id if isinstance(role, discord.Role) else None
house = house.lower()
- if house == 'none':
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "hs_none_role", role))
- elif house == 'bravery':
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "hs_bravery_role", role))
- elif house == 'brilliance':
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "hs_brilliance_role", role))
- elif house == 'balance':
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "hs_balance_role", role))
+ if house == "none":
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "hs_none_role", role)
+ )
+ elif house == "bravery":
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "hs_bravery_role", role
+ )
+ )
+ elif house == "brilliance":
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "hs_brilliance_role", role
+ )
+ )
+ elif house == "balance":
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "hs_balance_role", role
+ )
+ )
else:
- await ctx.send(await self.bot._(ctx.guild.id, 'sconfig.hypesquad.unknown'))
+ await ctx.send(await self.bot._(ctx.guild.id, "sconfig.hypesquad.unknown"))
@tasks.loop(hours=12)
async def roles_loop(self):
@@ -52,9 +78,13 @@ async def roles_loop(self):
count += await self.edit_roles(member, roles)
except discord.Forbidden:
# missing a perm
- self.bot.log.warn(f'[hypesquad] Unable to give roles in guild {g.id} ({g.name})')
- delta = round(time.time()-t1, 2)
- self.bot.log.info(f"[hypesquad] Finished roles check in {delta}s with {count} editions")
+ self.bot.log.warn(
+ f"[hypesquad] Unable to give roles in guild {g.id} ({g.name})"
+ )
+ delta = round(time.time() - t1, 2)
+ self.bot.log.info(
+ f"[hypesquad] Finished roles check in {delta}s with {count} editions"
+ )
@roles_loop.before_loop
async def before_roles_loop(self):
@@ -75,7 +105,9 @@ async def on_member_join(self, member: discord.Member):
if any(roles.values()):
await self.edit_roles(member, roles)
- async def edit_roles(self, member: discord.Member, roles: Dict[str, discord.Role]) -> bool:
+ async def edit_roles(
+ self, member: discord.Member, roles: Dict[str, discord.Role]
+ ) -> bool:
"""Add or remove roles to a member based on their hypesquad
Returns True if a role has been given/removed"""
if member.bot: # we don't want bots here
@@ -83,36 +115,48 @@ async def edit_roles(self, member: discord.Member, roles: Dict[str, discord.Role
roles_list = list(member.roles)
unwanted = list()
if member.public_flags.hypesquad_bravery:
- if roles['hs_bravery_role']:
- if roles['hs_bravery_role'] not in member.roles:
+ if roles["hs_bravery_role"]:
+ if roles["hs_bravery_role"] not in member.roles:
# add bravery
- roles_list.append(roles['hs_bravery_role'])
+ roles_list.append(roles["hs_bravery_role"])
# remove brilliance balance none
- unwanted = (roles['hs_brilliance_role'],
- roles['hs_balance_role'], roles['hs_none_role'])
+ unwanted = (
+ roles["hs_brilliance_role"],
+ roles["hs_balance_role"],
+ roles["hs_none_role"],
+ )
elif member.public_flags.hypesquad_brilliance:
- if roles['hs_brilliance_role']:
- if roles['hs_brilliance_role'] not in member.roles:
+ if roles["hs_brilliance_role"]:
+ if roles["hs_brilliance_role"] not in member.roles:
# add brilliance
- roles_list.append(roles['hs_brilliance_role'])
+ roles_list.append(roles["hs_brilliance_role"])
# remove bravery balance none
- unwanted = (roles['hs_bravery_role'],
- roles['hs_balance_role'], roles['hs_none_role'])
+ unwanted = (
+ roles["hs_bravery_role"],
+ roles["hs_balance_role"],
+ roles["hs_none_role"],
+ )
elif member.public_flags.hypesquad_balance:
- if roles['hs_balance_role']:
+ if roles["hs_balance_role"]:
# add balance
- if roles['hs_balance_role'] not in member.roles:
- roles_list.append(roles['hs_balance_role'])
+ if roles["hs_balance_role"] not in member.roles:
+ roles_list.append(roles["hs_balance_role"])
# remove brilliance bravery none
- unwanted = (roles['hs_brilliance_role'],
- roles['hs_bravery_role'], roles['hs_none_role'])
- elif roles['hs_none_role']:
- if roles['hs_none_role'] not in member.roles:
+ unwanted = (
+ roles["hs_brilliance_role"],
+ roles["hs_bravery_role"],
+ roles["hs_none_role"],
+ )
+ elif roles["hs_none_role"]:
+ if roles["hs_none_role"] not in member.roles:
# add none
- roles_list.append(roles['hs_none_role'])
+ roles_list.append(roles["hs_none_role"])
# remove brilliance balance bravery
- unwanted = (roles['hs_brilliance_role'],
- roles['hs_balance_role'], roles['hs_bravery_role'])
+ unwanted = (
+ roles["hs_brilliance_role"],
+ roles["hs_balance_role"],
+ roles["hs_bravery_role"],
+ )
# we remove unwanted roles
roles_list = [r for r in roles_list if r not in unwanted]
# we remove duplicates
@@ -128,7 +172,12 @@ async def get_roles(self, guild: discord.Guild) -> Dict[str, discord.Role]:
"""Get the hypesquads roles according to the guild config"""
config = self.bot.server_configs[guild.id]
result = dict()
- for k in ('hs_bravery_role', 'hs_brilliance_role', 'hs_balance_role', 'hs_none_role'):
+ for k in (
+ "hs_bravery_role",
+ "hs_brilliance_role",
+ "hs_balance_role",
+ "hs_none_role",
+ ):
if config[k] is None:
result[k] = None
else:
@@ -147,18 +196,26 @@ async def hs_reload(self, ctx: MyContext, *, user: discord.Member):
"""Reload Hypesquad roles for a member"""
roles = await self.get_roles(ctx.guild)
if not any(roles.values()):
- await ctx.send(await self.bot._(ctx.guild.id, 'hypesquad.no-role'))
+ await ctx.send(await self.bot._(ctx.guild.id, "hypesquad.no-role"))
return
try:
edited = await self.edit_roles(user, roles)
except discord.Forbidden:
- await ctx.send(await self.bot._(ctx.guild.id, 'hypesquad.forbidden'))
+ await ctx.send(await self.bot._(ctx.guild.id, "hypesquad.forbidden"))
return
if edited:
- await ctx.send(await self.bot._(ctx.guild.id, 'hypesquad.edited', user=user))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "hypesquad.edited", user=user)
+ )
else:
- await ctx.send(await self.bot._(ctx.guild.id, 'hypesquad.not-edited', user=user))
-
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "hypesquad.not-edited", user=user)
+ )
-async def setup(bot):
- await bot.add_cog(Hypesquad(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Hypesquad(bot), icon="⚜️")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/hypesquad/langs/en.yml b/plugins/hypesquad/langs/en.yml
index 5402a8e7..db19a890 100644
--- a/plugins/hypesquad/langs/en.yml
+++ b/plugins/hypesquad/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
hypesquad:
no-role: You don't have any house roles configured!
diff --git a/plugins/hypesquad/langs/fr.yml b/plugins/hypesquad/langs/fr.yml
index 6a1ae93d..78d4e1f2 100644
--- a/plugins/hypesquad/langs/fr.yml
+++ b/plugins/hypesquad/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
hypesquad:
no-role: Vous n'avez aucun rôle de maison configuré !
diff --git a/plugins/inviteTracker/credits.md b/plugins/inviteTracker/credits.md
new file mode 100644
index 00000000..41ab8ffd
--- /dev/null
+++ b/plugins/inviteTracker/credits.md
@@ -0,0 +1,9 @@
+Copyright © ascpial 2021 - 2023
+Copyright © Leirof 2021 - 2022
+Copyright © ZRunner 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/inviteTracker/data/model.sql b/plugins/inviteTracker/data/model.sql
index 34e620f6..6155926b 100644
--- a/plugins/inviteTracker/data/model.sql
+++ b/plugins/inviteTracker/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `invites` (
`guild` BIGINT NOT NULL,
`channel` BIGINT NOT NULL,
diff --git a/plugins/inviteTracker/bot/main.py b/plugins/inviteTracker/inviteTracker.py
similarity index 61%
rename from plugins/inviteTracker/bot/main.py
rename to plugins/inviteTracker/inviteTracker.py
index decad4c0..7115bb83 100644
--- a/plugins/inviteTracker/bot/main.py
+++ b/plugins/inviteTracker/inviteTracker.py
@@ -1,12 +1,20 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
from typing import Any, Dict, List, Optional, Tuple, Union
import discord
from discord.ext import tasks, commands
from utils import Gunibot, MyContext
from bot import checks
+
class DatabaseInvite:
- """Represent a database invitation object
- """
+ """Represent a database invitation object"""
+
guild: discord.Guild
channel: discord.TextChannel
user: int
@@ -17,7 +25,7 @@ class DatabaseInvite:
def __init__(self, data: Tuple[Any], parent: Gunibot) -> None:
"""Constrcut the classe with the database data
-
+
Attributes
----------
parent: Gunibot
@@ -33,10 +41,10 @@ def __init__(self, data: Tuple[Any], parent: Gunibot) -> None:
self.code = data[4]
self.uses = data[5]
self.description = data[6]
-
+
async def check_use(self) -> bool:
"""Return if the invitation has been used and edit if necessary
-
+
Returns
-------
bool
@@ -51,18 +59,15 @@ async def check_use(self) -> bool:
else:
self.delete()
return False
-
+
def delete(self) -> None:
"""Delete the invitation in the database"""
query = "DELETE FROM invites WHERE id = ?"
- self.parent.db_query(
- query,
- (self.id,)
- )
-
+ self.parent.db_query(query, (self.id,))
+
def update(self, invite: discord.Invite) -> None:
"""Update the invite to match the given in database
-
+
Attributes
----------
invite: discord.Invite
@@ -72,19 +77,25 @@ def update(self, invite: discord.Invite) -> None:
raise ValueError("The invitation is not the current one")
self.uses = invite.uses
query = "UPDATE invites SET uses=? WHERE id=?;"
- self.parent.db_query(query, (self.uses, self.id,))
-
+ self.parent.db_query(
+ query,
+ (
+ self.uses,
+ self.id,
+ ),
+ )
+
@classmethod
def add(cls, invite: discord.Invite, parent: Gunibot):
"""Create a new invitation in the database from a discord invitation
-
+
Attributes
----------
invite: discord.Invite
The discord invitation
parent: Gunibot
The parent for which to add in the database
-
+
Returns
-------
DatabaseInvite
@@ -102,73 +113,66 @@ def add(cls, invite: discord.Invite, parent: Gunibot):
invite.uses,
"",
),
- astuple=True
+ astuple=True,
)
data = parent.db_query(
"SELECT * FROM invites WHERE id=?",
(invite.id,),
astuple=True,
- fetchone=True
+ fetchone=True,
)
return cls(data, parent)
-
+
async def fetch_inviter(self) -> discord.User:
"""Return the user that owns the invitation
-
+
Returns
-------
discord.User
The user that owns the invite
"""
return await self.parent.fetch_user(self.user)
-
+
def set_description(self, description: str) -> None:
"""Change the description for the invite in the database
-
+
Attributes
----------
description: str
The new description
"""
query = "UPDATE invites SET description=? WHERE id=?;"
- self.parent.db_query(
- query,
- (description, self.id)
- )
-
+ self.parent.db_query(query, (description, self.id))
+
def __eq__(self, object: Union[int, str, "Invite", discord.Invite]) -> bool:
- if type(object) == int:
+ if isinstance(object, int):
return self.id == object
- elif type(object) == str:
+ elif isinstance(object, str):
return self.code == object
- elif type(object) == Invite:
+ elif isinstance(object, Invite):
return self.id == object.id
- elif type(object) == discord.Invite:
+ elif isinstance(object, discord.Invite):
return self.id == object.id
-class Invite(commands.Cog):
+class Invite(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
- self.config_options = [
- "invite_log"
- ]
+ self.config_options = ["invite_log"]
bot.get_command("config").add_command(self.invite_log)
-
+
@commands.command(name="invite_log")
@commands.check(checks.is_admin)
- async def invite_log(self, ctx: MyContext, channel: discord.TextChannel = None) -> None:
+ async def invite_log(
+ self, ctx: MyContext, channel: discord.TextChannel = None
+ ) -> None:
"""Change le salon où sont envoyés les messages avec les invitations utilisées"""
if channel is not None:
channel = channel.id
await ctx.send(
- await self.bot.sconfig.edit_config(
- ctx.guild.id,
- "invite_log",
- channel
- )
+ await self.bot.sconfig.edit_config(ctx.guild.id, "invite_log", channel)
)
-
+
@commands.command(name="set_description")
@commands.check(checks.is_admin)
async def set_description(self, ctx: MyContext, code: str, *description) -> None:
@@ -178,20 +182,22 @@ async def set_description(self, ctx: MyContext, code: str, *description) -> None
await self.check_invites(ctx.guild)
invite = self.get_invite_by_code(code)
if invite is not None:
- invite.set_description(' '.join(description))
+ invite.set_description(" ".join(description))
await ctx.send(
- await self.bot._(ctx.guild.id, "invite-tracker.set-description-done", code=invite.code)
+ await self.bot._(
+ ctx.guild.id,
+ "invite-tracker.set-description-done",
+ code=invite.code,
+ )
)
else:
- await ctx.send(
- await self.bot._(ctx.guild.id, "invite-tracker.not-found")
- )
+ await ctx.send(await self.bot._(ctx.guild.id, "invite-tracker.not-found"))
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member) -> None:
"""Called when a momber join a guild.
This event check the join invitation
-
+
Attributes
----------
member: discord.Member
@@ -199,37 +205,56 @@ async def on_member_join(self, member: discord.Member) -> None:
"""
if not member.guild.me.guild_permissions.manage_guild:
return
- invite = await self.check_invites(member.guild)
- if invite is not None:
- channel = self.bot.server_configs[member.guild.id]['invite_log']
- if channel is not None:
- channel = self.bot.get_channel(channel)
- if invite.description == '':
- await channel.send(
- await self.bot._(
- member.guild.id,
- "invite-tracker.join-code",
- member=member.mention,
- guild=member.guild,
- code=invite.code,
- inviter=(await invite.fetch_inviter()).mention,
- uses=invite.uses
- )
- )
- else:
- await channel.send(
- await self.bot._(
- member.guild.id,
- "invite-tracker.join-description",
- member=member.mention,
- guild=member.guild,
- code=invite.code,
- inviter=(await invite.fetch_inviter()).mention,
- description=invite.description,
- uses=invite.uses
- )
+ invites = await self.check_invites(member.guild)
+ if len(invites) == 1:
+ invite = invites[0]
+
+ invite_string = await self.get_invitation_string(
+ invite,
+ member,
+ )
+
+ message = await self.bot._(
+ member.guild.id,
+ "invite-tracker.joined",
+ member=member.mention,
+ guild=member.guild,
+ invitation=invite_string,
+ )
+ elif len(invites) == 0:
+ message = await self.bot._(
+ member.guild.id,
+ "invite-tracker.joined-no-invite",
+ member=member.mention,
+ guild=member.guild,
+ )
+ else: # multiple invitations
+ invites_string = ', '.join(
+ [
+ await self.get_invitation_string(
+ invite,
+ member,
)
-
+ for invite in invites
+ ]
+ )
+
+ message = await self.bot._(
+ member.guild.id,
+ "invite-tracker.joined-many-invites",
+ member=member.mention,
+ guild=member.guild,
+ invitations=invites_string,
+ )
+
+ channel = self.bot.server_configs[member.guild.id]["invite_log"]
+ if channel is not None:
+ channel = self.bot.get_channel(channel)
+ await channel.send(
+ content=message,
+ allowed_mentions=discord.AllowedMentions.none(),
+ )
+
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Called when the bot is ready.
@@ -239,31 +264,32 @@ async def on_ready(self) -> None:
if guild.me.guild_permissions.manage_guild:
await self.check_invites(guild)
self.bot.log.info("Invitations successfully synced")
-
- async def check_invites(self, guild: discord.Guild) -> Optional[DatabaseInvite]:
+
+ async def check_invites(self, guild: discord.Guild) -> List[DatabaseInvite]:
"""Check for all guild invite and changes
-
+
Attributes
----------
guild: discord.Guild
The guild for which to check the invites
-
+
Returns
-------
- Optional[discord.Invite]
- The last discord invite with changes detected
+ List[discord.Invite]
+ All the invitations that changed since last refresh
"""
invites = await guild.invites()
- output_invite = None
+ output_invites = []
for invite in invites:
database_invite = self.get_invite_by_id(invite.id)
if database_invite is None:
database_invite = DatabaseInvite.add(invite, self.bot)
if invite.uses > 0:
- output_invite = database_invite
+ output_invites.append(database_invite)
else:
- if await database_invite.check_use():
- output_invite = database_invite
+ if invite.uses > database_invite.uses:
+ database_invite.update(invite)
+ output_invites.append(database_invite)
for invitation in self.get_invite_by_server(guild):
is_in = False
for database_invite in invites:
@@ -271,80 +297,106 @@ async def check_invites(self, guild: discord.Guild) -> Optional[DatabaseInvite]:
is_in = True
if not is_in:
invitation.delete()
- return output_invite
+ return output_invites
def get_invite_by_code(self, code: str) -> Optional[DatabaseInvite]:
"""Return a dict representing the discord invitation stored in database
-
+
Attributes
----------
code: str
The code to look for
-
+
Returns
-------
Optional[DatabaseInvite]
The representation of the database object
"""
query = "SELECT * FROM invites WHERE code = ?"
- data = self.bot.db_query(
- query,
- (code,),
- fetchone=True,
- astuple=True
- )
+ data = self.bot.db_query(query, (code,), fetchone=True, astuple=True)
if data is not tuple():
return DatabaseInvite(data, self.bot)
else:
return None
-
+
def get_invite_by_id(self, id: int) -> Optional[DatabaseInvite]:
"""Return a dict representing the discord invitation stored in database
-
+
Attributes
----------
id: int
The id for which to look
-
+
Returns
-------
Optional[DatabaseInvite]
The representation of the database object
"""
query = "SELECT * FROM invites WHERE id = ?"
- data = self.bot.db_query(
- query,
- (id,),
- fetchone=True,
- astuple=True
- )
+ data = self.bot.db_query(query, (id,), fetchone=True, astuple=True)
if data is not tuple():
return DatabaseInvite(data, self.bot)
else:
return None
-
- def get_invite_by_server(self, guild: Union[int, discord.Guild]) -> List[DatabaseInvite]:
- """Retrieve all invites stored in database in a guild
-
+
+ def get_invite_by_server(
+ self, guild: Union[int, discord.Guild]
+ ) -> List[DatabaseInvite]:
+ """Retrieve all invites stored in database for a guild
+
Attributes
----------
guild: Union[int, discord.Guild]
The guild for which to look
-
+
Returns
-------
List[DatabaseInvite]
The list of invitations found
"""
- if type(guild) == discord.Guild:
+ if isinstance(guild, discord.Guild):
guild = guild.id
query = f"SELECT * FROM invites WHERE guild = ?;"
- datas = self.bot.db_query(
- query,
- (guild,),
- astuple=True
- )
+ datas = self.bot.db_query(query, (guild,), astuple=True)
return [DatabaseInvite(data, self.bot) for data in datas]
+
+ async def get_invitation_string(
+ self,
+ invite: DatabaseInvite,
+ member: discord.Member,
+ ) -> str:
+ """Returns a string representation for the given invitation
+
+ Attributes
+ ----------
+ invite: DatabaseInvite
+ The invitation to represent
+ member: discord.Member
+ The member used to represent the invitation, to get the language
+ """
+ if invite.description == "":
+ return await self.bot._(
+ member.guild.id,
+ "invite-tracker.code-only",
+ code=invite.code,
+ inviter=(await invite.fetch_inviter()).mention,
+ uses=invite.uses,
+ )
+ else:
+ return await self.bot._(
+ member.guild.id,
+ "invite-tracker.description",
+ description=invite.description,
+ code=invite.code,
+ inviter=(await invite.fetch_inviter()).mention,
+ uses=invite.uses,
+ )
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Invite(bot), icon="👋")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
-async def setup(bot: Gunibot):
- await bot.add_cog(Invite(bot))
diff --git a/plugins/inviteTracker/langs/en.yml b/plugins/inviteTracker/langs/en.yml
index 4f9eb71a..b07c649f 100644
--- a/plugins/inviteTracker/langs/en.yml
+++ b/plugins/inviteTracker/langs/en.yml
@@ -1,6 +1,15 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
invite-tracker:
- join-code: "%{member} joins %{guild} with the invite `%{code}` created by %{inviter}, which has been used `%{uses}` times now !"
- join-description: "%{member} joins %{guild} with the invite \"%{description}\" (`%{code}`) created by %{inviter}, which has been used `%{uses}` times now !"
+ joined: "%{member} joined %{guild} using the invitation %{invitation}!"
+ joined-no-invite: "%{member} joined %{guild}, but I did not found the invitation he used..."
+ joined-many-invites: "%{member} joined %{guild}, he could have used one of the following invitations: %{invitations}."
not-found: Invitation not found.
- set-description-done: "`%{code}`'s description changed."
\ No newline at end of file
+ set-description-done: "`%{code}`'s description changed."
+
+ description: "\"%{description}\" (`%{code}`) created by %{inviter} used %{uses} times now"
+ code-only: "`%{code}` created by %{inviter} used %{uses} times now"
\ No newline at end of file
diff --git a/plugins/inviteTracker/langs/fr.yml b/plugins/inviteTracker/langs/fr.yml
index 17ae7b44..1bd17d89 100644
--- a/plugins/inviteTracker/langs/fr.yml
+++ b/plugins/inviteTracker/langs/fr.yml
@@ -1,6 +1,15 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
invite-tracker:
- join-code: "%{member} a rejoint %{guild} avec l'invitation `%{code}` créée par %{inviter}, qui a maintenant `%{uses}` utilisations !"
- join-description: "%{member} a rejoint %{guild} avec l'invitation \"%{description}\" (`%{code}`) créée par %{inviter}, qui a maintenant `%{uses}` utilisations !"
+ joined: "%{member} a rejoint %{guild} avec l'invitation %{invitation} !"
+ joined-no-invite: "%{member} a rejoint %{guild}, mais je n'ai pas trouvé l'invitation qu'il a utilisé..."
+ joined-many-invites: "%{member} a rejoint %{guild}, il a pu utiliser une des invitations suivantes : %{invitations}."
not-found: J'ai cherché partout, mais l'invitation reste introuvable !
- set-description-done: J'ai bien changé la description de `%{code}` !
\ No newline at end of file
+ set-description-done: "J'ai bien changé la description de `%{code}` !"
+
+ description: "\"%{description}\" (`%{code}`) créée par %{inviter} qui a été utilisée %{uses} fois"
+ code-only: "`%{code}` créée par %{inviter} qui a été utilisée %{uses} fois"
\ No newline at end of file
diff --git a/plugins/logs/credits.md b/plugins/logs/credits.md
new file mode 100644
index 00000000..8e5ed2b3
--- /dev/null
+++ b/plugins/logs/credits.md
@@ -0,0 +1,10 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © Theaustudio 2021
+Copyright © Aeris One 2022
+Copyright © ascpial 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/logs/docs/user_documentation.md b/plugins/logs/docs/user_documentation.md
index e802f7dc..798dfae4 100644
--- a/plugins/logs/docs/user_documentation.md
+++ b/plugins/logs/docs/user_documentation.md
@@ -1,4 +1,11 @@
-## **Logs**
+
+
+# 📜 Log
This bot offers a log system for your server, which grows over time. So you can set up a room where you can send messages when a member leaves the server, when a message is deleted, when an invitation is created... and many other things.
diff --git a/plugins/logs/bot/main.py b/plugins/logs/logs.py
similarity index 91%
rename from plugins/logs/bot/main.py
rename to plugins/logs/logs.py
index 0b0701df..55bd2d9d 100644
--- a/plugins/logs/bot/main.py
+++ b/plugins/logs/logs.py
@@ -1,10 +1,17 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import datetime
from typing import List
import discord
from discord.ext import commands
from utils import Gunibot, MyContext
-from bot.utils.sconfig import Sconfig
+from bot.utils.sconfig import SERVER_CONFIG
import args
@@ -13,10 +20,10 @@ class Logs(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
self.config_options = ['logs_channel', 'modlogs_flags']
-
+
bot.get_command("config").add_command(self.config_modlogs_flags)
bot.get_command("config").add_command(self.config_modlogs)
-
+
@commands.command(name="modlogs_flags")
async def config_modlogs_flags(self, ctx: MyContext):
await ctx.send(await self.bot._(ctx.guild.id, "sconfig.modlogs-help", p=ctx.prefix))
@@ -37,9 +44,9 @@ async def modlogs_enable(self, ctx: MyContext, options: commands.Greedy[args.mod
LogsFlags = self.bot.get_cog('ConfigCog').LogsFlags()
flags = self.bot.server_configs[ctx.guild.id]['modlogs_flags']
flags = LogsFlags.intToFlags(flags) + options
- flags = list(set(flags)) # remove duplicates
- await Sconfig.edit_config(ctx.guild.id, 'modlogs_flags',
- LogsFlags.flagsToInt(flags))
+ flags = list(set(flags)) # remove duplicates
+ await SERVER_CONFIG.edit_config(ctx.guild.id, 'modlogs_flags',
+ LogsFlags.flagsToInt(flags))
await ctx.send(await self.bot._(ctx.guild.id, "sconfig.modlogs-enabled", type=', '.join(options)))
@config_modlogs.command(name="disable")
@@ -52,7 +59,7 @@ async def modlogs_disable(self, ctx: MyContext, options: commands.Greedy[args.mo
flags = self.bot.server_configs[ctx.guild.id]['modlogs_flags']
flags = LogsFlags.intToFlags(flags)
flags = [x for x in flags if x not in options]
- await Sconfig.edit_config(ctx.guild.id, 'modlogs_flags', LogsFlags.flagsToInt(flags))
+ await SERVER_CONFIG.edit_config(ctx.guild.id, 'modlogs_flags', LogsFlags.flagsToInt(flags))
await ctx.send(await self.bot._(ctx.guild.id, "sconfig.modlogs-disabled", type=', '.join(options)))
@config_modlogs.command(name="list")
@@ -61,7 +68,6 @@ async def modlogs_list(self, ctx: MyContext):
f = self.bot.get_cog('ConfigCog').LogsFlags.FLAGS.values()
await ctx.send(await self.bot._(ctx.guild.id, "sconfig.modlogs-list", list=" - ".join(f)))
-
async def has_logs(self, guild) -> bool:
"""Check if a Guild has a valid logs channel"""
if guild is None:
@@ -103,7 +109,7 @@ async def on_message_delete(self, message: discord.Message):
colour=discord.Colour(13632027)
)
embed.set_author(name=str(message.author),
- icon_url=message.author.avatar_url)
+ icon_url=message.author.display_avatar.url)
_footer = await self.bot._(message.guild.id, "logs.footer1", author=message.author.id, message=message.id)
embed.set_footer(text=_footer)
if len(message.content) > 1024:
@@ -128,7 +134,7 @@ async def on_message_edit(self, before: discord.Message, after: discord.Message)
colour=discord.Colour(16294684)
)
embed.set_author(name=str(before.author),
- icon_url=before.author.avatar_url)
+ icon_url=before.author.display_avatar.url)
_footer = await self.bot._(before.guild.id, "logs.footer1", author=before.author.id, message=before.id)
embed.set_footer(text=_footer)
if len(before.content) > 1024:
@@ -172,8 +178,10 @@ async def on_invite_create(self, invite: discord.Invite):
colour=discord.Colour.green()
)
if invite.inviter: # sometimes Discord doesn't send that info
- embed.set_author(name=f'{invite.inviter.name}#{invite.inviter.discriminator}',
- icon_url=invite.inviter.avatar_url_as(static_format='png'))
+ embed.set_author(
+ name=f'{invite.inviter.name}#{invite.inviter.discriminator}',
+ icon_url=invite.inviter.display_avatar.url_as(
+ static_format='png'))
_footer = await self.bot._(invite.guild.id, "logs.footer2", author=invite.inviter.id)
embed.set_footer(text=_footer)
_duration = await self.bot._(invite.guild.id, "logs.invite_created.duration")
@@ -181,11 +189,14 @@ async def on_invite_create(self, invite: discord.Invite):
embed.add_field(name=_duration, value="♾")
else:
embed.add_field(
- name=_duration, value=f"{datetime.timedelta(seconds=invite.max_age)}")
+ name=_duration,
+ value=f"{datetime.timedelta(seconds=invite.max_age)}")
embed.add_field(name="URL", value=invite.url)
_max_uses = await self.bot._(invite.guild.id, "logs.invite_created.max_uses")
- embed.add_field(name=_max_uses,
- value="♾" if invite.max_uses == 0 else str(invite.max_uses))
+ embed.add_field(
+ name=_max_uses,
+ value="♾" if invite.max_uses == 0 else str(
+ invite.max_uses))
await self.send_embed(invite.guild, embed)
@commands.Cog.listener()
@@ -201,8 +212,10 @@ async def on_invite_delete(self, invite: discord.Invite):
colour=discord.Colour.green()
)
if invite.inviter:
- embed.set_author(name=f'{invite.inviter.name}#{invite.inviter.discriminator}',
- icon_url=invite.inviter.avatar_url_as(static_format='png'))
+ embed.set_author(
+ name=f'{invite.inviter.name}#{invite.inviter.discriminator}',
+ icon_url=invite.inviter.display_avatar.url_as(
+ static_format='png'))
_footer = await self.bot._(invite.guild.id, "logs.footer2", author=invite.inviter.id)
embed.set_footer(text=_footer)
embed.add_field(name="URL", value=invite.url)
@@ -242,7 +255,7 @@ async def on_member_join_remove(self, member: discord.Member, join: bool):
delta = await self.bot.get_cog("TimeCog").time_delta(member.joined_at, datetime.datetime.utcnow(), lang="fr", year=True, precision=0)
_date = await self.bot._(member.guild.id, "logs.member_left.date")
embed.add_field(name=_date, value=delta)
- embed.set_author(name=str(member), icon_url=member.avatar_url)
+ embed.set_author(name=str(member), icon_url=member.display_avatar.url)
_footer = await self.bot._(member.guild.id, "logs.footer3", member=member.id)
embed.set_footer(text=_footer)
await self.send_embed(member.guild, embed)
@@ -259,7 +272,7 @@ async def on_member_ban(self, guild: discord.Guild, user: discord.User):
description=await self.bot._(guild.id, "logs.member_ban.desc", user=user.mention),
colour=discord.Colour.red()
)
- embed.set_author(name=str(user), icon_url=user.avatar_url)
+ embed.set_author(name=str(user), icon_url=user.display_avatar.url)
_footer = await self.bot._(guild.id, "logs.footer3", member=user.id)
embed.set_footer(text=_footer)
await self.send_embed(guild, embed)
@@ -277,7 +290,7 @@ async def on_member_unban(self, guild: discord.Guild, user: discord.User):
colour=discord.Colour.green()
)
embed.set_author(
- name=str(user), icon_url=user.avatar_url_as(static_format='png'))
+ name=str(user), icon_url=user.display_avatar.url_as(static_format='png'))
_footer = await self.bot._(guild.id, "logs.footer3", member=user.id)
embed.set_footer(text=_footer)
await self.send_embed(guild, embed)
@@ -306,10 +319,10 @@ async def on_voice_state_update(self, member: discord.Member, before: discord.Vo
_title = await self.bot._(member.guild.id, "logs.voice_move.title")
embed = discord.Embed(
title=_title,
- description=await self.bot._(member.guild.id, "logs.voice_move."+_desc, user=member.mention, **kw)
+ description=await self.bot._(member.guild.id, "logs.voice_move." + _desc, user=member.mention, **kw)
)
embed.colour = discord.Color.light_gray()
- embed.set_author(name=str(member), icon_url=member.avatar_url)
+ embed.set_author(name=str(member), icon_url=member.display_avatar.url)
_footer = await self.bot._(member.guild.id, "logs.footer3", member=member.id)
embed.set_footer(text=_footer)
await self.send_embed(member.guild, embed)
@@ -354,7 +367,7 @@ async def on_guild_role_create(self, role: discord.Role):
_yes = await self.bot._(role.guild.id, "logs._yes")
_pos = await self.bot._(role.guild.id, "logs.role_created.pos")
_ment = await self.bot._(role.guild.id, "logs.role_created.mentionnable")
- data = [_pos + ' {role.position}',
+ data = [_pos + f' {role.position}',
_ment + ' ' + (_yes if role.mentionable else _no)]
if role.color != discord.Color.default():
data.append(await self.bot._(role.guild.id, "logs.role_created.color") + f' {role.color}')
@@ -382,7 +395,7 @@ async def on_guild_role_delete(self, role: discord.Role):
_yes = await self.bot._(role.guild.id, "logs._yes")
_pos = await self.bot._(role.guild.id, "logs.role_created.pos")
_ment = await self.bot._(role.guild.id, "logs.role_created.mentionnable")
- data = [_pos + ' {role.position}',
+ data = [_pos + f' {role.position}',
_ment + ' ' + (_yes if role.mentionable else _no)]
if role.color != discord.Color.default():
data.append(await self.bot._(role.guild.id, "logs.role_created.color") + f' {role.color}')
@@ -439,7 +452,7 @@ async def on_guild_role_update(self, before: discord.Role, after: discord.Role):
embed.add_field(name=_changes, value="\n".join(data))
embed.color = discord.Color.orange()
await self.send_embed(before.guild, embed)
-
+
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
"""https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_update"""
@@ -465,7 +478,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member):
if len(embed.fields) == 0:
return
embed.set_author(name=str(before),
- icon_url=before.avatar_url_as(static_format='png'))
+ icon_url=before.display_avatar.url_as(static_format='png'))
_footer = await self.bot._(after.guild.id, "logs.footer3", member=before.id)
embed.set_footer(text=_footer)
await self.send_embed(before.guild, embed)
@@ -493,7 +506,8 @@ async def on_guild_emojis_update(self, guild: discord.Guild, before: List[discor
b_roles = ' '.join([x.mention for x in b.roles])
restrict.append(f'{a} {b_roles} -> {a_roles}')
if not (new or lost or renamed):
- # can happen when Discord fetch emojis from Twitch without any change
+ # can happen when Discord fetch emojis from Twitch without any
+ # change
return
if new:
n = await self.bot._(guild.id, "logs.emoji_update.added", count=len(new))
@@ -509,7 +523,10 @@ async def on_guild_emojis_update(self, guild: discord.Guild, before: List[discor
embed.add_field(name=n, value="\n".join(restrict), inline=False)
await self.send_embed(guild, embed)
-
-
-async def setup(bot):
- await bot.add_cog(Logs(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Logs(bot), icon="📜")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
diff --git a/plugins/messageManager/bot/main.py b/plugins/messageManager/bot/main.py
deleted file mode 100644
index 8a1436ac..00000000
--- a/plugins/messageManager/bot/main.py
+++ /dev/null
@@ -1,214 +0,0 @@
-import discord
-from discord.ext import commands
-from utils import Gunibot
-from typing import Union
-
-
-# Moves a message from its original channel to a parameterized channel using a given webhook
-async def moveMessage(msg: discord.Message, webhook: discord.Webhook):
- files = [await x.to_file() for x in msg.attachments]
- # grab mentions from the source message
- mentions = discord.AllowedMentions(
- everyone=msg.mention_everyone,
- users=msg.mentions,
- roles=msg.role_mentions)
- new_msg: discord.WebhookMessage = await webhook.send(content=msg.content,
- files=files,
- embeds=msg.embeds,
- avatar_url=msg.author.display_avatar,
- username=msg.author.name,
- allowed_mentions=discord.AllowedMentions.none(),
- wait=True)
- # edit the message to include mentions without notifications
- if mentions.roles or mentions.users or mentions.everyone:
- await new_msg.edit(allowed_mentions=mentions)
- await msg.delete()
-
-class MessageManager(commands.Cog):
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.file = "messageManager"
-
- #-------------------#
- # Command /imitate #
- #-------------------#
-
- @commands.command(name="imitate")
- @commands.guild_only()
- async def imitate(self, ctx: commands.Context, user: discord.User = None, *, text=None):
- """Say something with someone else's appearance"""
-
- if user and text and ctx.channel.permissions_for(ctx.author).manage_messages and ctx.channel.permissions_for(ctx.author).manage_nicknames:
- # Create a webhook in the image of the targeted member
- webhook = await ctx.channel.create_webhook(name=user.name)
- await webhook.send(content=text, avatar_url=user.display_avatar)
-
- # Deletes the original message as well as the webhook
- await webhook.delete()
- await ctx.message.delete()
-
- #----------------#
- # Command /move #
- #----------------#
-
- @commands.command(names="move", aliases=['mv'])
- @commands.guild_only()
- async def move(self, ctx: commands.Context, msg: discord.Message, channel: Union[discord.TextChannel,str], *, confirm=True):
- """Move a message in another channel"""
-
- if type(channel) == str:
- try:
- channel = self.bot.get_channel(int(channel))
- except:
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.no-channel"))
- return
- if type(channel) != discord.TextChannel:
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.no-channel"))
- return
-
- author = channel.guild.get_member(ctx.author.id)
-
- # Check bot permissions
- perm1: discord.Permissions = ctx.channel.permissions_for(ctx.guild.me)
- perm2: discord.Permissions = channel.permissions_for(channel.guild.me)
-
- if not (perm1.read_messages
- and perm1.read_message_history
- and perm1.manage_messages
- and perm2.manage_messages):
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.moveall.missing-perm"))
- self.bot.log.info(f"Alakon - /move: Missing permissions on guild \"{ctx.guild.name}\"")
- return
-
- # Check permission
- if not ctx.channel.permissions_for(ctx.author).manage_messages \
- or not ctx.channel.permissions_for(ctx.author).read_messages \
- or not ctx.channel.permissions_for(ctx.author).read_message_history \
- or not channel.permissions_for(author).manage_messages:
- embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'message_manager.permission'),
- colour=discord.Colour.red())
- await ctx.send(embed=embed)
- return
-
-
-
- # Creates a webhook to resend the message to another channel
- webhook = await channel.create_webhook(name="Gunipy Hook")
- await moveMessage(msg, webhook)
- await webhook.delete()
-
- if confirm:
- # Creates an embed to notify that the message has been moved
- embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'message_manager.move.confirm', user=msg.author.mention, channel=channel.mention),
- colour=discord.Colour(51711)
- )
- embed.set_footer(text=await self.bot._(ctx.guild.id, 'message_manager.move.footer', user=ctx.author.name))
- await ctx.send(embed=embed)
-
- # Deletes the command
- await ctx.message.delete()
-
- #-------------------#
- # Command /moveall #
- #-------------------#
-
- @commands.command(names="moveall", aliases=['mva'])
- @commands.guild_only()
- async def moveall(self, ctx: commands.Context, msg1: discord.Message, msg2: discord.Message, channel: Union[discord.TextChannel,str], *, confirm=True):
- """Move several messages in another channel
- msg1 and msg2 need to be from the same channel"""
-
- if type(channel) == str:
- try:
- channel = self.bot.get_channel(int(channel))
- except:
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.no-channel"))
- return
- if type(channel) != discord.TextChannel:
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.no-channel"))
- return
-
- author = channel.guild.get_member(ctx.author.id)
-
-
- # Check bot permissions
- perm1: discord.Permissions = ctx.channel.permissions_for(ctx.guild.me)
- perm2: discord.Permissions = channel.permissions_for(channel.guild.me)
-
- if not (perm1.read_messages
- and perm1.read_message_history
- and perm1.manage_messages
- and perm2.manage_messages):
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.moveall.missing-perm"))
- self.bot.log.info(f"Alakon - /moveall: Missing permissions on guild \"{ctx.guild.name}\"")
- return
-
- # Check member permissions
- if not ctx.channel.permissions_for(ctx.author).manage_messages \
- or not ctx.channel.permissions_for(ctx.author).read_messages \
- or not ctx.channel.permissions_for(ctx.author).read_message_history \
- or not channel.permissions_for(author).manage_messages:
- embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'message_manager.permission'),
- colour=discord.Colour.red())
- await ctx.send(embed=embed)
- return
-
- # Send confirmation that the bot started to move messages
- embed = discord.Embed(description=await self.bot._(ctx.guild.id, 'message_manager.moveall.running', channel=channel.mention), colour=discord.Colour.blue())
- embed.set_footer(text=await self.bot._(ctx.guild.id, 'message_manager.moveall.footer', user=ctx.author.name))
- confirmation = await ctx.send(embed=embed)
-
- # Send a little introduction in the destination channel
- embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'message_manager.moveall.introduce', channel=ctx.channel.mention, link=confirmation.jump_url),
- colour=discord.Colour.blue()
- )
- embed.set_footer(text=await self.bot._(ctx.guild.id, 'message_manager.moveall.footer', user=ctx.author.name))
- introduction = await channel.send(embed=embed)
-
- # Checks that the messages are in the same channel
- if msg1.channel != msg2.channel:
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.moveall.channel-conflict"))
- return
-
- # Ensures that msg1 is indeed the first message of the two
- if msg1.created_at > msg2.created_at:
- msg2, msg1 = msg1, msg2
-
- # Webhook creation (common to all messages)
- webhook = await channel.create_webhook(name="Gunipy Hook")
-
- counter = 0
-
- # Retrieves the message list from msg1 to msg2
- await moveMessage(msg1, webhook)
- async for msg in msg1.channel.history(limit=200, after=msg1.created_at, before=msg2, oldest_first=True):
- await moveMessage(msg, webhook)
- counter += 1
- await moveMessage(msg2, webhook)
-
- if counter == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "message_manager.moveall.no-msg"))
- await webhook.delete()
- return
-
- if confirm:
- # Creates an embed to notify that the messages have been moved
- embed = discord.Embed(
- description=await self.bot._(ctx.guild.id, 'message_manager.moveall.confirm', channel=channel.mention, link=introduction.jump_url),
- colour=discord.Colour.green()
- )
- embed.set_footer(text=await self.bot._(ctx.guild.id, 'message_manager.moveall.footer', user=ctx.author.name))
- await confirmation.edit(embed=embed)
- await ctx.message.delete()
-
- await webhook.delete()
-
-
-# The End.
-async def setup(bot):
- await bot.add_cog(MessageManager(bot))
diff --git a/plugins/messageManager/credits.md b/plugins/messageManager/credits.md
new file mode 100644
index 00000000..5f7dcb7b
--- /dev/null
+++ b/plugins/messageManager/credits.md
@@ -0,0 +1,10 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © Aeris One 2022
+Copyright © ascpial 2023
+Copyright © Majoras16 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/messageManager/docs/user_documentation.md b/plugins/messageManager/docs/user_documentation.md
index 582b085a..7812bcee 100644
--- a/plugins/messageManager/docs/user_documentation.md
+++ b/plugins/messageManager/docs/user_documentation.md
@@ -1,6 +1,13 @@
-# **Message manager**
+
-## **Moving messages**
+# 💬 Message manager
+
+## Moving messages
Gunibot.py offers the possibility to manage messages by moving them to other rooms. To do this, you will need to perform the command:
@@ -12,6 +19,7 @@ Where `` can be replaced by the ID or link to a message, and `
This will move all the messages copied between the two messages indicated. Make sure that these two messages are in the same channel, and that there are no more than 20 messages separating them (this limitation prevents the bot from being considered a spammer by Discord).
-## **Request to change room**
+## Request to change room
If a discussion is rambling and no longer appropriate for the channel, you can send a message visible to everyone with the command: /hs ``. The bot will then ask all participants to continue their discussion in the indicated channel, or to find a more appropriate one if you don't indicate one.
+
diff --git a/plugins/messageManager/langs/en.yml b/plugins/messageManager/langs/en.yml
index c9632a47..2eb6a452 100644
--- a/plugins/messageManager/langs/en.yml
+++ b/plugins/messageManager/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
message_manager:
move:
@@ -10,6 +15,6 @@ en:
introduce: "The following conversation come from %{channel}. [Click here](%{link}) to go to the original conversation."
footer: "Messages moved by %{user}"
missing-perm: "Unable to do this :confused: Please check that I have permission to Read channel, Read message history, Manage messages in this channel, and Read channel, Manage webhooks in the targeted chat"
- no-msg: No message found
+ same-message: "The start and end message cannot be the same. If you want to move only one message, use the `move` command"
permission: "You cannot move messages. Make sure you have the permissions to see the messages you want to move, and manage the messages in both channels."
no-channel: "Unable to find the channel. If it is a channel on another server, please indicate its ID"
\ No newline at end of file
diff --git a/plugins/messageManager/langs/fr.yml b/plugins/messageManager/langs/fr.yml
index ca0ee90b..912f9de1 100644
--- a/plugins/messageManager/langs/fr.yml
+++ b/plugins/messageManager/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
message_manager:
move:
@@ -10,6 +15,6 @@ fr:
introduce: "La conversation suivante vient du salon %{channel}. [Cliquez ici](%{link}) pour retourner à la conversation initiale."
footer: "Messages déplacés par %{user}"
missing-perm: "Impossible de faire cela :confused: Vérifiez que je possède la permission de Lire le salon, Lire l'historique des messages, Gérer les messages dans ce salon, et Lire le salon, Gérer les webhooks dans le salon ciblé"
- no-msg: Aucun message trouvé
+ same-message: "Le message de départ et de fin ne peuvent pas être les mêmes. Si vous souhaitez déplacer un seul message, utilisez la commande `move`"
permission: "Vous ne pouvez pas déplacer les messages. Assurez vous d'avoir les permissions pour voir les messages que vous souhtaitez déplacer, et gérer les messages dans les deux salons."
no-channel: "Impossible de trouver le salon. S'il s'agit d'un salon sur un autre serveur, merci d'indiquer son identifiant"
\ No newline at end of file
diff --git a/plugins/messageManager/messageManager.py b/plugins/messageManager/messageManager.py
new file mode 100644
index 00000000..b3d42d4f
--- /dev/null
+++ b/plugins/messageManager/messageManager.py
@@ -0,0 +1,337 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import discord
+from discord.ext import commands
+from utils import Gunibot
+from typing import Union
+
+
+# Moves a message from its original channel to a parameterized channel
+# using a given webhook
+async def moveMessage(msg: discord.Message, webhook: discord.Webhook, thread: discord.Thread = None):
+ files = [await x.to_file() for x in msg.attachments]
+ # grab mentions from the source message
+ mentions = discord.AllowedMentions(
+ everyone=msg.mention_everyone, users=msg.mentions, roles=msg.role_mentions
+ )
+
+ kargs = {
+ "content": msg.content,
+ "files": files,
+ "embeds": msg.embeds,
+ "avatar_url": msg.author.display_avatar,
+ "username": msg.author.name,
+ "allowed_mentions": discord.AllowedMentions.none(),
+ "wait": True,
+ }
+ if thread:
+ kargs["thread"] = thread
+
+ new_msg: discord.WebhookMessage = await webhook.send(**kargs)
+
+ # edit the message to include mentions without notifications
+ if mentions.roles or mentions.users or mentions.everyone:
+ await new_msg.edit(allowed_mentions=mentions)
+ await msg.delete()
+
+
+class MessageManager(commands.Cog):
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.file = "messageManager"
+
+ # -------------------#
+ # Command /imitate #
+ # -------------------#
+
+ @commands.command(name="imitate")
+ @commands.guild_only()
+ @commands.has_permissions(manage_messages=True, manage_nicknames=True)
+ async def imitate(
+ self, ctx: commands.Context, member: discord.Member = None, *, text=None
+ ):
+ """Say something with someone else's appearance"""
+
+ if (member is not None and text is not None): # c'est python, autant être verbeux
+ # Create a webhook in the image of the targeted member
+ webhook = await ctx.channel.create_webhook(name=member.display_name)
+ await webhook.send(content=text, avatar_url=member.display_avatar)
+
+ # Deletes the original message as well as the webhook
+ await webhook.delete()
+ await ctx.message.delete()
+
+ # ----------------#
+ # Command /move #
+ # ----------------#
+
+ @commands.command(names="move", aliases=["mv"])
+ @commands.guild_only()
+ @commands.has_permissions(
+ manage_messages=True,
+ read_messages=True,
+ read_message_history=True,
+ )
+ async def move(
+ self,
+ ctx: commands.Context,
+ msg: discord.Message,
+ channel: Union[discord.abc.Messageable, str],
+ *,
+ confirm=True,
+ ):
+ """Move a message in another channel"""
+
+ if isinstance(channel, str):
+ try:
+ channel = self.bot.get_channel(int(channel.replace("<#", "").replace(">", "")))
+ except BaseException:
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "message_manager.no-channel")
+ )
+ return
+
+ if not isinstance(channel, discord.abc.Messageable):
+ await ctx.send(await self.bot._(ctx.guild.id, "message_manager.no-channel"))
+ return
+
+ author = channel.guild.get_member(ctx.author.id)
+
+ # Check bot permissions
+ perm1: discord.Permissions = ctx.channel.permissions_for(ctx.guild.me)
+ perm2: discord.Permissions = channel.permissions_for(channel.guild.me)
+
+ if not (
+ perm1.read_messages
+ and perm1.read_message_history
+ and perm1.manage_messages
+ and perm2.manage_messages
+ ):
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "message_manager.moveall.missing-perm")
+ )
+ self.bot.log.info(
+ f'Alakon - /move: Missing permissions on guild "{ctx.guild.name}"'
+ )
+ return
+
+ # Check permission
+ if (not channel.permissions_for(author).manage_messages):
+ embed = discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id, "message_manager.permission"
+ ),
+ colour=discord.Colour.red(),
+ )
+ await ctx.send(embed=embed)
+ return
+
+ dest = channel
+ thread = None
+ if isinstance(channel, discord.Thread):
+ thread = channel
+ channel = thread.parent
+
+ # Creates a webhook to resend the message to another channel
+ webhook = await channel.create_webhook(name="Gunipy Hook")
+ await moveMessage(msg, webhook, thread=thread)
+ await webhook.delete()
+
+ if confirm:
+ # Creates an embed to notify that the message has been moved
+ embed = discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id,
+ "message_manager.move.confirm",
+ user=msg.author.mention,
+ channel=dest.mention,
+ ),
+ colour=discord.Colour(51711),
+ )
+ embed.set_footer(
+ text=await self.bot._(
+ ctx.guild.id, "message_manager.move.footer", user=ctx.author.name
+ )
+ )
+ await ctx.send(embed=embed)
+
+ # Deletes the command
+ await ctx.message.delete()
+
+ # -------------------#
+ # Command /moveall #
+ # -------------------#
+
+ @commands.command(names="moveall", aliases=["mva"])
+ @commands.guild_only()
+ @commands.has_permissions(
+ manage_messages=True,
+ read_messages=True,
+ read_message_history=True,
+ )
+ async def moveall(
+ self,
+ ctx: commands.Context,
+ msg1: discord.Message,
+ msg2: discord.Message,
+ channel: Union[discord.abc.Messageable, str],
+ *,
+ confirm=True,
+ ):
+ """Move several messages in another channel
+ msg1 and msg2 need to be from the same channel"""
+
+ if isinstance(channel, str):
+ try:
+ channel = self.bot.get_channel(int(channel.replace("<#", "").replace(">", "")))
+ except BaseException:
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "message_manager.no-channel")
+ )
+ return
+
+ if not isinstance(channel, discord.abc.Messageable):
+ await ctx.send(await self.bot._(ctx.guild.id, "message_manager.no-channel"))
+ return
+
+ author = channel.guild.get_member(ctx.author.id)
+
+
+ # Check bot permissions
+ perm1: discord.Permissions = ctx.channel.permissions_for(ctx.guild.me)
+ perm2: discord.Permissions = channel.permissions_for(channel.guild.me)
+
+ if not (
+ perm1.read_messages
+ and perm1.read_message_history
+ and perm1.manage_messages
+ and perm2.manage_messages
+ ):
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "message_manager.moveall.missing-perm")
+ )
+ self.bot.log.info(
+ f'messageManager - /moveall: Missing permissions on guild "{ctx.guild.name}"'
+ )
+ return
+
+ # Check member permissions
+ if (not channel.permissions_for(author).manage_messages):
+ embed = discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id, "message_manager.permission"
+ ),
+ colour=discord.Colour.red(),
+ )
+ await ctx.send(embed=embed)
+ return
+
+ # Checks that the messages are not the same
+ if msg1 == msg2:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "message_manager.moveall.same-message"
+ )
+ )
+ return
+
+ # Checks that the messages are in the same channel
+ if msg1.channel != msg2.channel:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "message_manager.moveall.channel-conflict"
+ )
+ )
+ return
+
+ # Ensures that msg1 is indeed the first message of the two
+ if msg1.created_at > msg2.created_at:
+ msg2, msg1 = msg1, msg2
+
+ # Send confirmation that the bot started to move messages
+ embed = discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id, "message_manager.moveall.running", channel=channel.mention
+ ),
+ colour=discord.Colour.blue(),
+ )
+ embed.set_footer(
+ text=await self.bot._(
+ ctx.guild.id, "message_manager.moveall.footer", user=ctx.author.name
+ )
+ )
+ confirmation = await ctx.send(embed=embed)
+
+ # Send a little introduction in the destination channel
+ embed = discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id,
+ "message_manager.moveall.introduce",
+ channel=ctx.channel.mention,
+ link=confirmation.jump_url,
+ ),
+ colour=discord.Colour.blue(),
+ )
+ embed.set_footer(
+ text=await self.bot._(
+ ctx.guild.id, "message_manager.moveall.footer", user=ctx.author.name
+ )
+ )
+ introduction = await channel.send(embed=embed)
+
+ dest = channel
+ thread = None
+ if isinstance(channel, discord.Thread):
+ thread = channel
+ channel = thread.parent
+
+ # Webhook creation (common to all messages)
+ webhook = await channel.create_webhook(name="Gunipy Hook")
+
+ counter = 0
+
+ # Retrieves the message list from msg1 to msg2
+ await moveMessage(msg1, webhook, thread=thread)
+ async for msg in msg1.channel.history(
+ limit=200, after=msg1.created_at, before=msg2, oldest_first=True
+ ):
+ await moveMessage(msg, webhook, thread=thread)
+ counter += 1
+ await moveMessage(msg2, webhook, thread=thread)
+
+ if confirm:
+ # Creates an embed to notify that the messages have been moved
+ embed = discord.Embed(
+ description=await self.bot._(
+ ctx.guild.id,
+ "message_manager.moveall.confirm",
+ channel=dest.mention,
+ link=introduction.jump_url,
+ ),
+ colour=discord.Colour.green(),
+ )
+ embed.set_footer(
+ text=await self.bot._(
+ ctx.guild.id, "message_manager.moveall.footer", user=ctx.author.name
+ )
+ )
+ await confirmation.edit(embed=embed)
+ await ctx.message.delete()
+
+ await webhook.delete()
+
+
+# The End.
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(MessageManager(bot), icon="📋")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
diff --git a/plugins/misc/bot/main.py b/plugins/misc/bot/main.py
deleted file mode 100644
index 7f7e7edf..00000000
--- a/plugins/misc/bot/main.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import random
-from datetime import datetime
-
-import discord
-from discord.ext import commands
-from utils import Gunibot, MyContext
-
-from typing import Union
-
-
-class Misc(commands.Cog):
-
- CONTAINS_TIMESTAMP = Union[
- int,
- discord.User,
- discord.TextChannel,
- discord.VoiceChannel,
- discord.StageChannel,
- discord.GroupChannel,
- discord.Message,
- discord.Emoji,
- discord.Guild
- ]
-
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.file = "misc"
-
- #------------------#
- # Commande /cookie #
- #------------------#
-
- @commands.command(name="cookie")
- @commands.guild_only()
- async def cookie(self, ctx: MyContext, *, user: discord.User = None):
- """The most useful command: give a cookie to yourself or someone else."""
- if user:
- message = await self.bot._(ctx.guild.id, 'misc.cookie.give', to=user.mention, giver=ctx.author.mention)
- else:
- message = await self.bot._(ctx.guild.id, 'misc.cookie.self', to=ctx.author.mention)
-
- # Créer un webhook qui prend l'apparence d'un Villageois
- webhook: discord.Webhook = await ctx.channel.create_webhook(name=f"Villager #{random.randint(1, 9)}")
- await webhook.send(content=message, avatar_url="https://d31sxl6qgne2yj.cloudfront.net/wordpress/wp-content/uploads/20190121140737/Minecraft-Villager-Head.jpg")
- await webhook.delete()
- try:
- await ctx.message.delete()
- except discord.errors.NotFound:
- pass
-
- #------------------#
- # Commande /hoster #
- #------------------#
-
- @commands.command(name="hoster")
- @commands.guild_only()
- async def hoster(self, ctx: MyContext):
- """Give all informations about the hoster"""
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.add_field(name="mTx Serv", value=await self.bot._(ctx.guild.id, 'misc.hoster.info'))
- embed.set_thumbnail(url="http://gunivers.net/wp-content/uploads/2021/07/Logo-mTxServ.png")
-
- # Créer un webhook qui prend l'apparence d'Inovaperf
- webhook: discord.Webhook = await ctx.channel.create_webhook(name="mTx Serv")
- await webhook.send(embed=embed, avatar_url="http://gunivers.net/wp-content/uploads/2021/07/Logo-mTxServ.png")
- await webhook.delete()
- await ctx.message.delete()
-
- #---------------------#
- # Commande /flipacoin #
- #---------------------#
-
- @commands.command(name="flipacoin", aliases=['fc'])
- async def flip(self, ctx: MyContext):
- """Flip a coin."""
- a = random.randint(-100, 100)
- if a > 0:
- await ctx.send(await self.bot._(ctx.guild.id, 'misc.flipacoin.tails'))
- elif a < 0:
- await ctx.send(await self.bot._(ctx.guild.id, 'misc.flipacoin.heads'))
- else:
- await ctx.send(await self.bot._(ctx.guild.id, 'misc.flipacoin.side'))
-
- #------------------#
- # Commande /dataja #
- #------------------#
-
- @commands.command(name="dataja")
- async def dataja(self, ctx: MyContext):
- """Don't ask to ask, just ask."""
- await ctx.send(await self.bot._(ctx.guild.id, 'misc.dataja'))
-
-
-
- #------------------#
- # Commande /kill #
- #------------------#
-
- @commands.command(name="kill")
- async def kill(self, ctx: MyContext, *, target: str=None):
- """Wanna kill someone?"""
- if target is None: # victim is user
- victime = ctx.author.display_name
- ex = ctx.author.display_name.replace(" ","\_")
- else: # victim is target
- victime = target
- ex = target.replace(" ","\_")
- author = ctx.author.mention
- tries = 0
- # now let's find a random answer
- msg = 'misc.kills'
- while msg.startswith('misc.kills') or ('{0}' in msg and target is None and tries<50):
- choice = random.randint(0, 23)
- msg = await self.bot._(ctx.channel, f"misc.kills.{choice}")
- tries += 1
- # and send it
- await ctx.send(msg.format(author, victime, ex), allowed_mentions=discord.AllowedMentions.none())
-
- @commands.group(name="timestamp")
- async def timestamp(self, ctx: MyContext):
- """This command helps you to use the discord timestamp feature.
- Use the timestamp command to see more !
- """
- if not ctx.subcommand_passed:
- await ctx.author.send(await self.bot._(ctx, "misc.timestamp.help"))
-
- @timestamp.command(name="get")
- async def get(self, ctx: MyContext, snowflake: CONTAINS_TIMESTAMP = None):
- """If you want to know how old is a thing
-
- Supported args :
- • Discord ID
- • User mention
- • Channel mention
- • Message link
- • Custom emoji
- """
- if isinstance(snowflake, int):
- source = f"`{snowflake}`"
- elif isinstance(snowflake, (
- discord.User,
- discord.abc.GuildChannel
- )):
- source = snowflake.mention
- snowflake = snowflake.id
- elif isinstance(snowflake, discord.Message):
- source = snowflake.jump_url
- snowflake = snowflake.id
- elif isinstance(snowflake, discord.Emoji):
- source = snowflake
- snowflake = snowflake.id
- elif isinstance(snowflake, discord.Guild):
- source = snowflake.name
- snowflake = snowflake.id
- elif snowflake is None: # we get the user id
- source = ctx.author.mention
- snowflake = ctx.author.id
- else:
- await ctx.send(await self.bot._(ctx.guild.id, "misc.timestamp.not-found", source=snowflake))
- return
- timestamp = ((snowflake >> 22) + 1420070400000) // 1000
- await ctx.send(
- await self.bot._(ctx.guild.id, "misc.timestamp.read-result", source=source, timestamp=timestamp),
- allowed_mentions=discord.AllowedMentions(everyone=False, users=False, roles=False)
- )
-
- @timestamp.command(name="create")
- async def create(
- self,
- ctx: MyContext,
- year: int,
- month: int=1,
- day: int=1,
- hour: int=0,
- minute: int=0,
- second: int=0,
- ):
- """Show the timestamp for the specified date"""
- date = datetime(year, month, day, hour, minute, second)
- timestamp = int(date.timestamp())
- await ctx.send(
- await self.bot._(ctx, "misc.timestamp.create-result", timestamp = timestamp)
- )
-
-
-
-# The end.
-async def setup(bot):
- await bot.add_cog(Misc(bot))
diff --git a/plugins/misc/credits.md b/plugins/misc/credits.md
new file mode 100644
index 00000000..60e0426a
--- /dev/null
+++ b/plugins/misc/credits.md
@@ -0,0 +1,10 @@
+Copyright © Leirof 2021 - 2022
+Copyright © ZRunner 2021 - 2022
+Copyright © ascpial 2021
+Copyright © theogiraudet 2022
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/misc/docs/user_documentation.md b/plugins/misc/docs/user_documentation.md
index b65899b1..3ef93edb 100644
--- a/plugins/misc/docs/user_documentation.md
+++ b/plugins/misc/docs/user_documentation.md
@@ -1,18 +1,25 @@
-# Misc
+
-## **Distribute Cookies**
+# 🕳️ Misc
+
+## Distribute Cookies
You can distribute or request cookies via the command: `/cookie `. A villager will then take a cookie to the specified member. If you do not specify the member, the villager will give you the cookie.
-## **Toss a coin**
+## Toss a coin
Ever wanted to throw a coin but because you're poor you never had a coin on hand? We have solved this problem by adding the `/flipacoin` command which will flip a coin for you and tell you the result. Note that there is a non-zero chance that the coin will fall on its side, we have pushed the realism to the limit.
-## **Hoster**
+## Hoster
Enter the `hoster` command to get information about the bot's host.
-## **Don't ask to ask, just ask.**
+## Don't ask to ask, just ask.
> It is often said that the best weapon is the one you don't have to use. I don't agree. In my opinion, the best weapon is the one you only have to use once." - Tony Stark
diff --git a/plugins/misc/langs/en.yml b/plugins/misc/langs/en.yml
index 4b42f0c6..8259f66f 100644
--- a/plugins/misc/langs/en.yml
+++ b/plugins/misc/langs/en.yml
@@ -1,11 +1,23 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
misc:
+ dice:
+ title: "The dice are thrown!"
+ description: "The result is: %{value}"
+ error:
+ not_int: "The value must be an integer"
+ not_positive: "The value must be positive"
cookie:
give: "Here is for you %{to}: :cookie:\nFrom %{giver}"
self: "Here is for you %{to}: :cookie:"
hoster:
info: "Game server hosting service\n:speech_balloon: [Discord server](https://mtxserv.com/discord?utm_source=altearn_website_discord&utm_medium=website&utm_campaign=altearn_to_discord)\n:globe_with_meridians: [Website](https://mtxserv.com/fr/?utm_source=altearn_website&utm_medium=website&utm_campaign=altearn)"
flipacoin:
+ title: Pling !
heads: "Heads!"
tails: "Tails!"
side: "Side!"
@@ -34,33 +46,36 @@ en:
"21": "{1}, lava baths are hot, but lava burns..."
"22": "{1} tried a rocket jump"
"23": "You shouldn't listen to the pretty melody of the Lullaby, {1} :musical_note:"
- dataja: "https://zrunner.me/d-a-t-a/en.html"
+ "footer": "Death messages created by Aragorn1202"
+ dataja:
+ title: "Don't ask to ask, just ask"
+ description: "Learn the best practices to ask question:\nhttps://zrunner.me/d-a-t-a/en.html"
timestamp:
read-result: "The date in %{source} is () (timestamp: %{timestamp})"
not-found: "Date not found in %{source} !"
create-result: "The timestamp for the is: `%{timestamp}` (Discord timestamp : ``)"
help: |
- ***The timestamp in discord***
-
- *Example : Discord was created , the .*
-
- __**Use the timestamp feature**__ :
- You needs to get a UNIX timestamp (you can use the `timestamp get` and `timestamp create` commands), then you can use it with the following syntax:
- ```
-
- ```
- `[TIMESTAMP]` should be replaced with the timestamp you get.
-
- __**Utiliser les styles**__ :
- You can also style the timestamp using this syntax :
- ```
-
- ```
- `[STYLE]` should be replaced by one of the following values:
- • `t` ()
- • `T` ()
- • `d` ()
- • `D` ()
- • `f` (, by default)
- • `F` ()
- • `R` ()
\ No newline at end of file
+ ***The timestamp in discord***
+
+ *Example : Discord was created , the .*
+
+ __**Use the timestamp feature**__ :
+ You needs to get a UNIX timestamp (you can use the `timestamp get` and `timestamp create` commands), then you can use it with the following syntax:
+ ```
+
+ ```
+ `[TIMESTAMP]` should be replaced with the timestamp you get.
+
+ __**Utiliser les styles**__ :
+ You can also style the timestamp using this syntax :
+ ```
+
+ ```
+ `[STYLE]` should be replaced by one of the following values:
+ • `t` ()
+ • `T` ()
+ • `d` ()
+ • `D` ()
+ • `f` (, by default)
+ • `F` ()
+ • `R` ()
\ No newline at end of file
diff --git a/plugins/misc/langs/fr.yml b/plugins/misc/langs/fr.yml
index 40951d7f..6c0d4437 100644
--- a/plugins/misc/langs/fr.yml
+++ b/plugins/misc/langs/fr.yml
@@ -1,14 +1,26 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
misc:
+ dice:
+ title: "Les dés sont jetés !"
+ description: "Le résultat est : %{value}"
+ error:
+ not_int: "La valeur doit être un entier"
+ not_positive: "La valeur doit être positive"
cookie:
give: "Voilà pour vous %{to}: :cookie:\nDe la part de %{giver}"
self: "Voilà pour vous %{to}: :cookie:"
hoster:
- info: "Service d'hébergement de serveur de jeu\n:speech_balloon: [Serveur Discord](https://mtxserv.com/discord?utm_source=altearn_website_discord&utm_medium=website&utm_campaign=altearn_to_discord)\n:globe_with_meridians: [Site web](https://mtxserv.com/fr/?utm_source=altearn_website&utm_medium=website&utm_campaign=altearn)"
+ info: "mTx Serv proposent un service d'hébergement de serveur, en particulier destiné aux jeux. Leurs serveurs sont équipé des dernières technologies et sont extrêmement fiable. Ajoutez à celà leur SAV très réactif et sympatique et vous avez le parfait hébergeur pour vos projets !\n:speech_balloon: [Serveur Discord](https://mtxserv.com/discord?utm_source=altearn_website_discord&utm_medium=website&utm_campaign=altearn_to_discord)\n:globe_with_meridians: [Site web](https://mtxserv.com/fr/?utm_source=altearn_website&utm_medium=website&utm_campaign=altearn)"
flipacoin:
- heads: "Face !"
- tails: "Pile !"
- side: "Coté !"
+ title: Pling !
+ heads: "C'est face !"
+ tails: "C'est pile !"
+ side: "C'est ... tranche ! 😱\n(oui oui, ça peut arriver !)"
kills:
"0": "Oh toi, tu vas mourir !"
"1": "***BOUM !*** {1} est tombé dans un piège posé par {0} !"
@@ -34,7 +46,10 @@ fr:
"21": "{1} a tenté un rocket jump"
"22": "Il ne fallait pas écouter la jolie mélodie de la Lullaby, {1} :musical_note:"
"23": "{2}.exe *a cessé de fonctionner*"
- dataja: "https://zrunner.me/d-a-t-a/fr.html"
+ "footer": "Messages de morts créés par Aragorn1202"
+ dataja:
+ title: "Don't ask to ask, just ask"
+ description: "Demander si vous pouvez demander est une perte de temps.\nDemander si il y a une personne compétent demande un engagement aveugle de cette dernière en cas de réponse, personne ne voudra donc répondre.\n\nApprenez les bonnes pratiques pour demander quelque chose : \nhttps://zrunner.me/d-a-t-a/fr.html"
timestamp:
read-result: "La date contenue dans %{source} est le (soit ) (timestamp : %{timestamp})"
not-found: "Je n'ai pas trouvé de date dans %{source} !"
diff --git a/plugins/misc/misc.py b/plugins/misc/misc.py
new file mode 100644
index 00000000..bf42739c
--- /dev/null
+++ b/plugins/misc/misc.py
@@ -0,0 +1,350 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import random
+from datetime import datetime
+
+import discord
+from discord.ext import commands
+from utils import Gunibot, MyContext
+
+from typing import Union
+
+class Misc(commands.Cog):
+
+ CONTAINS_TIMESTAMP = Union[
+ int,
+ discord.User,
+ discord.TextChannel,
+ discord.VoiceChannel,
+ discord.StageChannel,
+ discord.GroupChannel,
+ discord.Message,
+ discord.Emoji,
+ discord.Guild,
+ ]
+
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.file = "misc"
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # 🍪 Cookie
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.command(name="cookie", aliases=["🍪"])
+ @commands.guild_only()
+ async def cookie(self, ctx: MyContext, *, user: discord.User = None):
+ """The most useful command of the world: give a cookie to yourself or someone else.
+ Usage:
+ - Get a cookie: cookie
+ - Give a cookie to someone: cookie
+ - user: the member you want to give the cookie to. Can be a mention, an id or simply it's name."""
+
+ # If the cookie is given
+ if user:
+ message = await self.bot._(
+ ctx.guild.id,
+ "misc.cookie.give",
+ to=user.mention,
+ giver=ctx.author.mention,
+ )
+
+ # If the cookie is for the the command sender
+ else:
+ message = await self.bot._(
+ ctx.guild.id, "misc.cookie.self", to=ctx.author.mention
+ )
+
+ # # Creating a webhook that makes reference to villagers of Element Animation
+ webhook: discord.Webhook = await ctx.channel.create_webhook(
+ name=f"Villager #{random.randint(1, 9)}"
+ )
+
+ # Sending the message
+ await webhook.send(
+ content=message,
+ avatar_url="https://d31sxl6qgne2yj.cloudfront.net/wordpress/wp-content/uploads/20190121140737/Minecraft-Villager-Head.jpg",
+ )
+
+ # Cleaning webhook & command
+ await webhook.delete()
+ try:
+ await ctx.message.delete()
+ except discord.errors.NotFound:
+ pass
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # 🖥️ Hoster
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.command(name="hoster", aliases=["host","mtxserv","mtx","🖥️","💻"])
+ @commands.guild_only()
+ async def hoster(self, ctx: MyContext):
+ """Give all informations about the hoster.
+ Usage:
+ - Get information about the hoster: hoster"""
+
+ # Building the result message
+ embed = discord.Embed(colour=discord.Colour.blue())
+ embed.add_field(
+ name="mTx Serv",
+ value=await self.bot._(ctx.guild.id, "misc.hoster.info")
+ )
+ embed.set_thumbnail(url="http://gunivers.net/wp-content/uploads/2021/07/Logo-mTxServ.png")
+
+ # Building the webhook that will take the appearance off the hoster
+ webhook: discord.Webhook = await ctx.channel.create_webhook(name="mTx Serv")
+ await webhook.send(
+ embed=embed,
+ avatar_url="http://gunivers.net/wp-content/uploads/2021/07/Logo-mTxServ.png",
+ )
+
+ # Cleaning the webhook
+ await webhook.delete()
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # 🪙 Flip a coin
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.command(name="flipacoin", aliases=["fc","coin","🪙"])
+ async def flip(self, ctx: MyContext):
+ """Flip a coin.
+ Usage:
+ - Flip a coin: coin
+ """
+
+ a = random.randint(-100, 100)
+
+ # The sign of the number define the result. 0 correspond to the side of the coin
+ if a > 0:
+ description = await self.bot._(ctx.guild.id, "misc.flipacoin.tails")
+ elif a < 0:
+ description = await self.bot._(ctx.guild.id, "misc.flipacoin.heads")
+ else:
+ description = await self.bot._(ctx.guild.id, "misc.flipacoin.side")
+
+ # Building the result messge
+ embed = discord.Embed(
+ title=await self.bot._(ctx.guild.id, "misc.flipacoin.title"),
+ description=description,
+ colour=0x2F3136,
+ )
+ embed.set_thumbnail(url="https://cdn-icons-png.flaticon.com/512/867/867351.png")
+
+ await ctx.send(embed = embed)
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # 🎲 Roll a dice
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.command(name="rolladice", aliases=["rad","dice","🎲"])
+ async def rolladice(self, ctx: MyContext, *, dice:int = 6):
+ """Roll a dice.
+ Usage:
+ - Throw a classic dice: dice
+ - Throw a custom dice: dice
+ - number: allow to set the random number generation from 1 and `number`. Default: 6
+ """
+
+ # Test if the parameter is an integer
+ try:
+ dice = int(dice)
+ except:
+ embed = discord.Embed(
+ description=await self.bot._(ctx.guild.id, "misc.dice.error.not_int"),
+ colour=0xe74c3c,
+ )
+ embed.set_author(name="Error", icon_url="https://cdn-icons-png.flaticon.com/512/738/738884.png")
+ ctx.send(embed=embed)
+
+ # Test if the parameter is upper than 0
+ if dice <= 0:
+ embed = discord.Embed(
+ description=await self.bot._(ctx.guild.id, "misc.dice.error.not_positive"),
+ colour=0xe74c3c,
+ )
+ embed.set_author(name="Error", icon_url="https://cdn-icons-png.flaticon.com/512/738/738884.png")
+ ctx.send(embed=embed)
+
+ # Generate the random value and print it
+ value = random.randint(1, dice)
+
+ # Building the result message
+ embed = discord.Embed(
+ title=await self.bot._(ctx.guild.id, "misc.dice.title"),
+ description=await self.bot._(ctx.guild.id, "misc.dice.description", value=value),
+ colour=0x2F3136,
+ )
+ embed.set_thumbnail(url="https://cdn-icons-png.flaticon.com/512/1055/1055855.png")
+
+ await ctx.send(embed = embed)
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # ❓ dataja
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.command(name="dataja", aliases=["data", "ask", "❓"])
+ async def dataja(self, ctx: MyContext):
+ """Don't ask to ask, just ask.
+ Usage:
+ - Explain how to ask something: dataja"""
+
+ embed = discord.Embed(
+ title=await self.bot._(ctx.guild.id, "misc.dataja.title"),
+ description=await self.bot._(ctx.guild.id, "misc.dataja.description"),
+ colour=0x2F3136,
+ )
+ embed.set_thumbnail(url="https://cdn-icons-png.flaticon.com/512/1180/1180260.png")
+
+ await ctx.send(embed = embed)
+
+ # Deleting command
+ try:
+ await ctx.message.delete()
+ except discord.errors.NotFound:
+ pass
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # 🗡️ kill
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.command(name="kill", aliases=["🗡️"])
+ async def kill(self, ctx: MyContext, *, target: str = None):
+ """Wanna kill someone? Take a breath
+ Usage:
+ - kill someone: kill
+ - user: the guy you want to kill. It can be a mention, an ID or simply a name."""
+
+ # Suicide
+ if target is None:
+ victime = ctx.author.display_name
+ ex = ctx.author.display_name.replace(" ", "\\_")
+
+ # Murder
+ else:
+ victime = target
+ ex = target.replace(" ", "\\_")
+
+ author = ctx.author.mention
+ tries = 0
+
+ # Generating a random answer
+ msg = "misc.kills"
+ while msg.startswith("misc.kills") or (
+ "{0}" in msg
+ and target is None
+ and tries < 50
+ ):
+ choice = random.randint(0, 23)
+ msg = await self.bot._(ctx.channel, f"misc.kills.{choice}")
+ tries += 1
+
+ footer = self.bot._(ctx.channel, f"misc.kills.footer")
+
+ # Building the result message
+ embed = discord.Embed(
+ description=msg.format(author, victime, ex),
+ colour=0x2F3136,
+ footer=footer
+ )
+ embed.set_thumbnail(url="https://cdn-icons-png.flaticon.com/512/3074/3074476.png")
+
+ # Send it
+ await ctx.send(
+ embed=embed,
+ allowed_mentions=discord.AllowedMentions.none(),
+ )
+
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+ # ⌚ Timestamp
+ #➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
+
+ @commands.group(name="timestamp")
+ async def timestamp(self, ctx: MyContext):
+ """This command helps you to use the discord timestamp feature.
+ Use the timestamp command to see more !
+ """
+ if not ctx.subcommand_passed:
+ await ctx.author.send(await self.bot._(ctx, "misc.timestamp.help"))
+
+ @timestamp.command(name="get")
+ async def get(self, ctx: MyContext, snowflake: CONTAINS_TIMESTAMP = None):
+ """If you want to know how old is a thing
+
+ Supported args :
+ • Discord ID
+ • User mention
+ • Channel mention
+ • Message link
+ • Custom emoji
+ """
+ if isinstance(snowflake, int):
+ source = f"`{snowflake}`"
+ elif isinstance(snowflake, (discord.User, discord.abc.GuildChannel)):
+ source = snowflake.mention
+ snowflake = snowflake.id
+ elif isinstance(snowflake, discord.Message):
+ source = snowflake.jump_url
+ snowflake = snowflake.id
+ elif isinstance(snowflake, discord.Emoji):
+ source = snowflake
+ snowflake = snowflake.id
+ elif isinstance(snowflake, discord.Guild):
+ source = snowflake.name
+ snowflake = snowflake.id
+ elif snowflake is None: # we get the user id
+ source = ctx.author.mention
+ snowflake = ctx.author.id
+ else:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "misc.timestamp.not-found", source=snowflake
+ )
+ )
+ return
+ timestamp = ((snowflake >> 22) + 1420070400000) // 1000
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "misc.timestamp.read-result",
+ source=source,
+ timestamp=timestamp,
+ ),
+ allowed_mentions=discord.AllowedMentions(
+ everyone=False, users=False, roles=False
+ ),
+ )
+
+ @timestamp.command(name="create")
+ async def create(
+ self,
+ ctx: MyContext,
+ year: int,
+ month: int = 1,
+ day: int = 1,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ ):
+ """Show the timestamp for the specified date"""
+ date = datetime(year, month, day, hour, minute, second)
+ timestamp = int(date.timestamp())
+ await ctx.send(
+ await self.bot._(ctx, "misc.timestamp.create-result", timestamp=timestamp)
+ )
+
+
+# The end.
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Misc(bot), icon="🍪")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
diff --git a/plugins/permissions/credits.md b/plugins/permissions/credits.md
new file mode 100644
index 00000000..97b3daca
--- /dev/null
+++ b/plugins/permissions/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2020 - 2021
+Copyright © Gamer4life 2020
+Copyright © Leirof 2020 - 2023
+Copyright © ascpial 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/permissions/docs/user_documentation.md b/plugins/permissions/docs/user_documentation.md
new file mode 100644
index 00000000..941cebc0
--- /dev/null
+++ b/plugins/permissions/docs/user_documentation.md
@@ -0,0 +1,23 @@
+
+
+# 🔐 Permissions
+
+This plugin allows you to see the permissions of a role or a member in a given context (the guild globally or a channel specifically).
+
+`!perms [] `
+
+Where
+
+* `[]` indicates the channel for which the permissions will be displayed.
+ If ignored, the permissions for the server as a whole will be displayed.
+* `` indicates the user for which to display permissions.
+ If ignored, displays the permissions of the user using the command.
+
+To display its own global permissions, the command `!perms` can simply be used.
+
+To see the permissions of the user `@ascpial` in the `#gipsy` channel, you could use the command `!perms #gipsy @ascpial`.
\ No newline at end of file
diff --git a/plugins/permissions/langs/en.yml b/plugins/permissions/langs/en.yml
new file mode 100644
index 00000000..478de973
--- /dev/null
+++ b/plugins/permissions/langs/en.yml
@@ -0,0 +1,44 @@
+en:
+ perms:
+ list:
+ add_reactions: Add reactions
+ administrator: Administrator
+ attach_files: Attach files
+ ban_members: Ban members
+ change_nickname: Change nickname
+ connect: Connect to a voice channel
+ create_instant_invite: Create invite
+ create_private_threads: Create private threads
+ create_public_threads: Create public threads
+ deafen_members: Deafen members
+ embed_links: Embed links
+ external_emojis: Use external emoji
+ external_stickers: Use external stickers
+ kick_members: Kick members
+ manage_channels: Manage channels
+ manage_emojis: Manage emojis
+ manage_events: Manage events
+ manage_guild: Mannage server
+ manage_messages: Manage messages
+ manage_nicknames: Manage nicknames
+ manage_roles: Manage roles
+ manage_threads: Manage threads
+ manage_webhooks: Manage webhooks
+ mention_everyone: Mention everyone, here and all roles
+ moderate_members: Timeout members
+ move_members: Move members
+ mute_members: Mute members
+ priority_speaker: Priority speaker
+ read_message_history: Read message history
+ read_messages: View channel
+ request_to_speak: Request to speak
+ send_messages: Send messages
+ send_messages_in_threads: Send messages in threads
+ send_tts_messages: Send Text-to-Speech messages
+ speak: Speak in voice channel
+ stream: Video in voice channel
+ use_application_commands: Use slash commands
+ use_embedded_activities: Use Activites in voice channel
+ use_voice_activation: Use voice activity
+ view_audit_log: View audit log
+ view_guild_insights: View server insights
\ No newline at end of file
diff --git a/plugins/permissions/langs/fr.yml b/plugins/permissions/langs/fr.yml
new file mode 100644
index 00000000..f02dd8ab
--- /dev/null
+++ b/plugins/permissions/langs/fr.yml
@@ -0,0 +1,44 @@
+fr:
+ perms:
+ list:
+ add_reactions: Ajouter des réactions
+ administrator: Administrateur
+ attach_files: Joindre des fichiers
+ ban_members: Bannir des membres
+ change_nickname: Changer de pseudo
+ connect: Se connecter à un salon vocal
+ create_instant_invite: Créer une invitation
+ create_private_threads: Créer des fils privés
+ create_public_threads: Créer des fils publics
+ deafen_members: Mettre en sourdine des membres
+ embed_links: Intégrer des liens
+ external_emojis: Utiliser des émojis externes
+ external_stickers: Utiliser des autocollants externes
+ kick_members: Expulser des membres
+ manage_channels: Gérer les salons
+ manage_emojis: Gérer les émojis
+ manage_events: Gérer les évènements
+ manage_guild: Gérer le serveur
+ manage_messages: Gérer les messages
+ manage_nicknames: Gérer les pseudos
+ manage_roles: Gérer les rôles
+ manage_threads: Gérer les fils
+ manage_webhooks: Gérer les webhooks
+ mention_everyone: Mentionner everyone, here et tous les rôles
+ moderate_members: Exclure des membres
+ move_members: Déplacer des membres
+ mute_members: Couper le micro de membres
+ priority_speaker: Voix prioritaire
+ read_message_history: Voir les anciens messages
+ read_messages: Voir le salon
+ request_to_speak: Demander la parole
+ send_messages: Envoyer des messages
+ send_messages_in_threads: Envoyer des messages dans des fils
+ send_tts_messages: Envoyer des messages TTS
+ speak: Parler dans un salon vocal
+ stream: Utiliser la vidéo dans un salon vocal
+ use_application_commands: Utiliser les commandes slash
+ use_embedded_activities: Utiliser les activités dans les salons vocaux
+ use_voice_activation: Utiliser la Détection de la voix
+ view_audit_log: Voir les logs du serveur
+ view_guild_insights: Voir les analyses de serveur
\ No newline at end of file
diff --git a/plugins/permissions/permissions.py b/plugins/permissions/permissions.py
new file mode 100644
index 00000000..b72ea03e
--- /dev/null
+++ b/plugins/permissions/permissions.py
@@ -0,0 +1,144 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import typing
+
+import discord
+from discord.ext import commands
+from utils import Gunibot, MyContext
+
+
+class Perms(commands.Cog):
+ """Cog with a single command, allowing you to see the permissions of a member or a role in a channel."""
+
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.file = "perms"
+ chan_perms = [
+ key for key, value in discord.Permissions().all_channel() if value
+ ]
+ self.perms_name = {
+ "general": [key for key, value in discord.Permissions().general() if value],
+ "text": [key for key, value in discord.Permissions().text() if value],
+ "voice": [key for key, value in discord.Permissions().voice() if value],
+ }
+ self.perms_name["common_channel"] = [
+ x for x in chan_perms if x in self.perms_name["general"]
+ ]
+
+ @commands.command(name="perms", aliases=["permissions"])
+ @commands.guild_only()
+ async def check_permissions(
+ self,
+ ctx: MyContext,
+ channel: typing.Optional[
+ typing.Union[
+ discord.TextChannel, discord.VoiceChannel, discord.CategoryChannel
+ ]
+ ] = None,
+ *,
+ target: typing.Union[discord.Member, discord.Role] = None,
+ ):
+ """Permissions assigned to a member/role (the user by default)
+ The channel used to view permissions is the channel in which the command is entered."""
+ if target is None:
+ target = ctx.author
+ perms = None
+ if isinstance(target, discord.Member):
+ if channel is None:
+ perms = target.guild_permissions
+ else:
+ perms = channel.permissions_for(target)
+ col = target.color
+ avatar = await self.bot.user_avatar_as(target, size=256)
+ name = str(target)
+ elif isinstance(target, discord.Role):
+ perms = target.permissions
+ if channel is not None:
+ perms.update(
+ **{
+ x[0]: x[1]
+ for x in channel.overwrites_for(ctx.guild.default_role)
+ if x[1] is not None
+ }
+ )
+ perms.update(
+ **{
+ x[0]: x[1]
+ for x in channel.overwrites_for(target)
+ if x[1] is not None
+ }
+ )
+ col = target.color
+ if target.guild.icon is not None: # the guild could have no icon
+ avatar = ctx.guild.icon.with_size(256).with_format('png')
+ else:
+ avatar = None
+ name = str(target)
+ permsl = list()
+
+ if perms is None:
+ return
+
+ async def perms_tr(x) -> str:
+ """Get the translation of a permission"""
+ return await self.bot._(ctx.guild.id, "perms.list." + x)
+
+ # Get the perms translations
+ if perms.administrator:
+ # If the user is admin, we just say it
+ permsl.append(":white_check_mark: " + await perms_tr("administrator"))
+ else:
+ # Here we check if the value of each permission is True.
+ for perm, value in perms:
+ if (
+ perm
+ not in self.perms_name["text"] + self.perms_name["common_channel"]
+ and isinstance(channel, discord.TextChannel)
+ ) or (
+ perm
+ not in self.perms_name["voice"] + self.perms_name["common_channel"]
+ and isinstance(channel, discord.VoiceChannel)
+ ):
+ continue
+ perm = await perms_tr(perm)
+ if "perms.list." in perm:
+ # missing translation
+ perm = perm.replace("_", " ").title()
+ self.bot.log.warn(f"[perms] missing permission translation: {perm}")
+ if value:
+ permsl.append(":white_check_mark: " + perm)
+ else:
+ permsl.append(":x: " + perm)
+ if ctx.can_send_embed:
+ # \uFEFF is a Zero-Width Space, which basically allows us to have an empty field name.
+ # And to make it look nice, we wrap it in an Embed.
+ desc = "Permissions générales" if channel is None else channel.mention
+ embed = discord.Embed(color=col, description=desc)
+ embed.set_author(name=name, icon_url=avatar)
+ if len(permsl) > 10:
+ sep = int(len(permsl) / 2)
+ if len(permsl) % 2 == 1:
+ sep += 1
+ embed.add_field(name="\uFEFF", value="\n".join(permsl[:sep]))
+ embed.add_field(name="\uFEFF", value="\n".join(permsl[sep:]))
+ else:
+ embed.add_field(name="\uFEFF", value="\n".join(permsl))
+ await ctx.send(embed=embed)
+ # Thanks to Gio for the Command.
+ else:
+ try:
+ await ctx.send(
+ "**Permission de '{}' :**\n\n".format(name.replace("@", ""))
+ + "\n".join(permsl)
+ )
+ except BaseException:
+ pass
+
+
+async def setup(bot: Gunibot = None, plugin_config: dict = None):
+ await bot.add_cog(Perms(bot))
diff --git a/plugins/quizz/bot/QuiPyQuizz.py b/plugins/quizz/QuiPyQuizz.py
similarity index 52%
rename from plugins/quizz/bot/QuiPyQuizz.py
rename to plugins/quizz/QuiPyQuizz.py
index 8cb7b331..9840481c 100644
--- a/plugins/quizz/bot/QuiPyQuizz.py
+++ b/plugins/quizz/QuiPyQuizz.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import requests
import json
import os
@@ -5,43 +12,56 @@
class QuiPyQuizz:
def __init__(self):
- with open('plugins/quizz/data/quizz.json', 'r', encoding='utf-8') as f:
+ with open("plugins/quizz/data/quizz.json", "r", encoding="utf-8") as f:
self.data = json.load(f)
@staticmethod
def request_questions(quizz):
- params = {'quiz': str(quizz)} # Payload
- r = requests.get(url="https://quipoquiz.com/module/sed/quiz/fr/start_quiz.snc", params=params)
+ params = {"quiz": str(quizz)} # Payload
+ r = requests.get(
+ url="https://quipoquiz.com/module/sed/quiz/fr/start_quiz.snc", params=params
+ )
paulaod = json.loads(r.text)
- return paulaod['questions']
+ return paulaod["questions"]
@staticmethod
def request_answer(uid_variation, question_id, answer: str):
- params = {'quiz': uid_variation, # Payload
- 'answer': answer.lower(),
- 'question': question_id}
- r = requests.get(url="https://quipoquiz.com/module/sed/quiz/fr/answer_question.snc", params=params)
+ params = {
+ "quiz": uid_variation, # Payload
+ "answer": answer.lower(),
+ "question": question_id,
+ }
+ r = requests.get(
+ url="https://quipoquiz.com/module/sed/quiz/fr/answer_question.snc",
+ params=params,
+ )
pauload = json.loads(r.text)
return pauload["answer"]
@staticmethod
def request_stats(quizz):
- params = {'quiz': str(quizz)} # Payload
- r = requests.get(url="https://quipoquiz.com/module/sed/quiz/fr/end_quiz.snc", params=params)
+ params = {"quiz": str(quizz)} # Payload
+ r = requests.get(
+ url="https://quipoquiz.com/module/sed/quiz/fr/end_quiz.snc", params=params
+ )
pauload = json.loads(r.text)
return pauload["result"]["statistics"]
def get_name(self, quizz_id):
- if quizz_id in self.data: return self.data[quizz_id]['name']
- else: return None
+ if quizz_id in self.data:
+ return self.data[quizz_id]["name"]
+ else:
+ return None
def get_url(self, quizz_id):
- if quizz_id in self.data: return f"https://quipoquiz.com/quiz/{self.data[quizz_id]['url']}"
- else: return None
+ if quizz_id in self.data:
+ return f"https://quipoquiz.com/quiz/{self.data[quizz_id]['url']}"
+ else:
+ return None
def get_question(self, quizz_id, question_id):
- if quizz_id in self.data and question_id in self.data[quizz_id]['questions']:
- return self.data[quizz_id]['questions'][question_id]
+ if quizz_id in self.data and question_id in self.data[quizz_id]["questions"]:
+ return self.data[quizz_id]["questions"][question_id]
"""
Exemple:
{
@@ -50,11 +70,12 @@ def get_question(self, quizz_id, question_id):
"image": "/sn_uploads/quizzes/13_wiki_Synchytrium_on_Erodium_cicutarium.jpg"
}
"""
- else: return None
+ else:
+ return None
def get_questions(self, quizz_id):
if quizz_id in self.data:
- return self.data[quizz_id]['questions']
+ return self.data[quizz_id]["questions"]
"""
Exemple:
{
@@ -66,11 +87,12 @@ def get_questions(self, quizz_id):
...
}
"""
- else: return None
+ else:
+ return None
def get_answer(self, quizz_id, question_id):
- if quizz_id in self.data and question_id in self.data[quizz_id]['answers']:
- return self.data[quizz_id]['answers'][question_id]
+ if quizz_id in self.data and question_id in self.data[quizz_id]["answers"]:
+ return self.data[quizz_id]["answers"][question_id]
"""
Exemple:
{
@@ -78,4 +100,5 @@ def get_answer(self, quizz_id, question_id):
"explanation": "La réponse est VRAI. Ce sont les seuls champignons à avoir encore des spores uniflagellées.
\n"
}
"""
- else: return None
+ else:
+ return None
diff --git a/plugins/quizz/bot/TODO.md b/plugins/quizz/TODO.md
similarity index 100%
rename from plugins/quizz/bot/TODO.md
rename to plugins/quizz/TODO.md
diff --git a/plugins/quizz/credits.md b/plugins/quizz/credits.md
new file mode 100644
index 00000000..ccac809f
--- /dev/null
+++ b/plugins/quizz/credits.md
@@ -0,0 +1,9 @@
+Copyright © Just_a_player 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ZRunner 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/quizz/docs/user_documentation.md b/plugins/quizz/docs/user_documentation.md
index c8d717f1..bfe15b3f 100644
--- a/plugins/quizz/docs/user_documentation.md
+++ b/plugins/quizz/docs/user_documentation.md
@@ -1,3 +1,11 @@
-# Quizz
+
+
+# ❓ Quizz
+
+Nothing here... for the moment.
-Nothing here...
\ No newline at end of file
diff --git a/plugins/quizz/bot/main.py b/plugins/quizz/quizz.py
similarity index 51%
rename from plugins/quizz/bot/main.py
rename to plugins/quizz/quizz.py
index 22b75ba1..c7d8960c 100644
--- a/plugins/quizz/bot/main.py
+++ b/plugins/quizz/quizz.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import random
import time
from typing import Optional
@@ -10,112 +17,138 @@
def clean_question(question: str):
- junk_to_remove = ['', '
']
+ junk_to_remove = ["", "
"]
for junk in junk_to_remove:
- question = question.replace(junk, '')
+ question = question.replace(junk, "")
return question
def clean_answer(answer: str):
- answer = answer.replace('', '\n')
- answer = answer.replace('
', '')
+ answer = answer.replace("", "\n")
+ answer = answer.replace("
", "")
return answer
+
class REACTIONS:
- ANSWER_TRUE = '✅'
- ANSWER_FALSE = '❎'
- ANSWER_SEPARATOR = '⬛'
- ANSWER_LEAVE = '⏹️'
+ ANSWER_TRUE = "✅"
+ ANSWER_FALSE = "❎"
+ ANSWER_SEPARATOR = "⬛"
+ ANSWER_LEAVE = "⏹️"
answers_reactions = (ANSWER_TRUE, ANSWER_FALSE, ANSWER_SEPARATOR, ANSWER_LEAVE)
- START_QUIZ = '🆗'
- STOP_QUIZ = '❌'
- JOIN_QUIZ = '✅'
- FORWARD_QUESTION = '⏭'
- PREVIOUS_QUESTION = '⬅️'
- NEXT_QUESTION = '➡️'
- all_reactions = (ANSWER_TRUE, ANSWER_FALSE, ANSWER_SEPARATOR, ANSWER_LEAVE, START_QUIZ, STOP_QUIZ, JOIN_QUIZ, FORWARD_QUESTION, PREVIOUS_QUESTION, NEXT_QUESTION)
-
-
+ START_QUIZ = "🆗"
+ STOP_QUIZ = "❌"
+ JOIN_QUIZ = "✅"
+ FORWARD_QUESTION = "⏭"
+ PREVIOUS_QUESTION = "⬅️"
+ NEXT_QUESTION = "➡️"
+ all_reactions = (
+ ANSWER_TRUE,
+ ANSWER_FALSE,
+ ANSWER_SEPARATOR,
+ ANSWER_LEAVE,
+ START_QUIZ,
+ STOP_QUIZ,
+ JOIN_QUIZ,
+ FORWARD_QUESTION,
+ PREVIOUS_QUESTION,
+ NEXT_QUESTION,
+ )
def sort_dict(leaders: dict) -> list[tuple]:
"""Sort a dict by each value, returning a list of (key, value) couples
Note that it will sort in lexicographical order
For mathematical way, change it to float"""
- return (sorted(leaders.items(), key=
- lambda kv: (kv[1], kv[0])))
+ return sorted(leaders.items(), key=lambda kv: (kv[1], kv[0]))
class Quizz(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = "quizz"
- self.parties = {"0": None} # Voir l'exemple de dict dans _quizz_start(ctx, quizz_id)
- self.quick_quizz_channels = [] # Utile pour réduire le nombre de requêtes envoyées, cf: on_aw_reaction_add/remove
+ # Voir l'exemple de dict dans _quizz_start(ctx, quizz_id)
+ self.parties = {"0": None}
+ # Utile pour réduire le nombre de requêtes envoyées, cf:
+ # on_aw_reaction_add/remove
+ self.quick_quizz_channels = []
self.quick_quizz_messages = [] # Same
self.check_if_active.start() # Démarrage du check des quizz inactifs
self.QPQ = QuiPyQuizz()
def update_timestamp(self, party_id):
- self.parties[party_id]['timestamp'] = time.time()
-
- def ez_set_author(self, embed: discord.Embed, party_id): # Vu que c'est gros et qu'il faut le foutre partout j'en ai fait une fonction
- quizz_id = self.parties[party_id]['quizz']['id'] # Récupère l'id du quizz
- embed.set_author(name=self.QPQ.get_name(quizz_id),
- url=self.QPQ.get_url(quizz_id),
- icon_url='https://scontent.fcdg1-1.fna.fbcdn.net/v/t1.6435-1/p148x148/48416529_2354370714793494_5893141918379933696_n.png?_nc_cat=110&ccb=1-3&_nc_sid=1eb0c7&_nc_ohc=AI2a2_Vn0c4AX9pPIK8&_nc_ht=scontent.fcdg1-1.fna&tp=30&oh=f8c88dae60c23d52fe81b8264031bf9f&oe=60D6AFB7')
+ self.parties[party_id]["timestamp"] = time.time()
+
+ # Vu que c'est gros et qu'il faut le foutre partout j'en ai fait une
+ # fonction
+ def ez_set_author(self, embed: discord.Embed, party_id):
+ # Récupère l'id du quizz
+ quizz_id = self.parties[party_id]["quizz"]["id"]
+ embed.set_author(
+ name=self.QPQ.get_name(quizz_id),
+ url=self.QPQ.get_url(quizz_id),
+ icon_url="https://scontent.fcdg1-1.fna.fbcdn.net/v/t1.6435-1/p148x148/48416529_2354370714793494_5893141918379933696_n.png?_nc_cat=110&ccb=1-3&_nc_sid=1eb0c7&_nc_ohc=AI2a2_Vn0c4AX9pPIK8&_nc_ht=scontent.fcdg1-1.fna&tp=30&oh=f8c88dae60c23d52fe81b8264031bf9f&oe=60D6AFB7",
+ )
return embed
def ez_players_list(self, party_id, waiting=False):
final_list = []
for player in self.parties[party_id]["players"]:
final_list.append(
- f"- <@!{player}>: {self.parties[party_id]['players'][player]['score']}/10{' ' if waiting else ''}")
+ f"- <@!{player}>: {self.parties[party_id]['players'][player]['score']}/10{' ' if waiting else ''}"
+ )
return final_list
- def ez_question_embed(self, party_id, leaderboard=False, waiting=False): # Used to generate question embed lmao
- curent_question_id = self.parties[party_id]['ids'][
- self.parties[party_id]['quizz']['current']] # Choppe l'id de la question en cours
- raw_question = self.QPQ.get_question(self.parties[party_id]['quizz']['id'],
- curent_question_id) # Récupère le paquet de la question
+ # Used to generate question embed lmao
+ def ez_question_embed(self, party_id, leaderboard=False, waiting=False):
+ curent_question_id = self.parties[party_id]["ids"][
+ self.parties[party_id]["quizz"]["current"]
+ ] # Choppe l'id de la question en cours
+ raw_question = self.QPQ.get_question(
+ self.parties[party_id]["quizz"]["id"], curent_question_id
+ ) # Récupère le paquet de la question
embed = discord.Embed(
title=f"Question {str(self.parties[party_id]['quizz']['current'] + 1)}/10",
color=discord.Colour.random(),
- description=clean_question(raw_question['question'])
+ description=clean_question(raw_question["question"]),
)
try:
- embed.set_thumbnail(
- url=f"https://quipoquiz.com{raw_question['image']}") # Rajoute une thumbnail si il y en a une
+ # Rajoute une thumbnail si il y en a une
+ embed.set_thumbnail(url=f"https://quipoquiz.com{raw_question['image']}")
except KeyError:
pass
embed.set_footer(text=party_id)
embed = self.ez_set_author(embed, party_id)
if leaderboard:
players = self.parties[party_id]["players"]
- embed.add_field(name=f"{len(players)} joueur{'s' if len(players) > 1 else ''}",
- value="\n".join(self.ez_players_list(party_id, waiting)))
+ embed.add_field(
+ name=f"{len(players)} joueur{'s' if len(players) > 1 else ''}",
+ value="\n".join(self.ez_players_list(party_id, waiting)),
+ )
self.update_timestamp(party_id)
return embed
def ez_answer_embed(self, party_id):
- curent_question_id = self.parties[party_id]['ids'][
- self.parties[party_id]['quizz']['current']] # Choppe l'id de la question en cours
- raw_answer = self.QPQ.get_answer(self.parties[party_id]['quizz']['id'],
- curent_question_id) # Choppe le paquet de la réponse
- raw_question = self.QPQ.get_question(self.parties[party_id]['quizz']['id'],
- curent_question_id) # Choppe le paquet de la question
+ curent_question_id = self.parties[party_id]["ids"][
+ self.parties[party_id]["quizz"]["current"]
+ ] # Choppe l'id de la question en cours
+ raw_answer = self.QPQ.get_answer(
+ self.parties[party_id]["quizz"]["id"], curent_question_id
+ ) # Choppe le paquet de la réponse
+ raw_question = self.QPQ.get_question(
+ self.parties[party_id]["quizz"]["id"], curent_question_id
+ ) # Choppe le paquet de la question
embed = discord.Embed(
title=f"Question {str(self.parties[party_id]['quizz']['current'] + 1)}/10",
color=discord.Colour.random(),
- description=f"{clean_answer(raw_question['question'])}\n{clean_answer(raw_answer['explanation'])}"
+ description=f"{clean_answer(raw_question['question'])}\n{clean_answer(raw_answer['explanation'])}",
)
try:
- embed.set_thumbnail(
- url=f"https://quipoquiz.com{raw_question['image']}") # Rajoute une thumbnail si il y en a une
+ # Rajoute une thumbnail si il y en a une
+ embed.set_thumbnail(url=f"https://quipoquiz.com{raw_question['image']}")
except KeyError:
pass
embed.set_footer(text=party_id)
@@ -123,20 +156,24 @@ def ez_answer_embed(self, party_id):
players = []
for player in self.parties[party_id]["players"]:
- if self.parties[party_id]["players"][player]["answer"] == raw_answer['real_answer']:
- self.parties[party_id]["players"][player]['score'] += 1
- players.append(f"- <@!{player}>: {self.parties[party_id]['players'][player]['score']}/10")
+ if (
+ self.parties[party_id]["players"][player]["answer"]
+ == raw_answer["real_answer"]
+ ):
+ self.parties[party_id]["players"][player]["score"] += 1
+ players.append(
+ f"- <@!{player}>: {self.parties[party_id]['players'][player]['score']}/10"
+ )
- embed.add_field(name=f"{len(players)} joueur{'s' if len(players) > 1 else ''}",
- value='\n'.join(players))
+ embed.add_field(
+ name=f"{len(players)} joueur{'s' if len(players) > 1 else ''}",
+ value="\n".join(players),
+ )
self.update_timestamp(party_id)
return embed
def ez_summary_embed(self, party_id):
- embed = discord.Embed(
- title="Quizz terminé !",
- color=discord.Colour.gold()
- )
+ embed = discord.Embed(title="Quizz terminé !", color=discord.Colour.gold())
embed = self.ez_set_author(embed, party_id)
embed.set_footer(text=party_id)
@@ -158,93 +195,123 @@ def ez_summary_embed(self, party_id):
def has_everyone_answered(self, party_id):
"""Test si tout le monde a répondu"""
verif = True
- for player_id in self.parties[party_id]['players']:
- if self.parties[party_id]['players'][player_id]['answer'] is None: verif = False
+ for player_id in self.parties[party_id]["players"]:
+ if self.parties[party_id]["players"][player_id]["answer"] is None:
+ verif = False
self.update_timestamp(party_id)
return verif
async def send_question(self, player_id, party_id):
- player: discord.User = await self.bot.fetch_user(player_id) # Fetch player
+ # Fetch player
+ player: discord.User = await self.bot.fetch_user(player_id)
embed = self.ez_question_embed(party_id) # Generate question embed
msg = await player.send(embed=embed) # Send it to the player
emotes = REACTIONS.answers_reactions # liste des emotes a rajouter
- for emote in emotes: await msg.add_reaction(emote) # Rajoute les emote sur le message
- self.parties[party_id]['players'][player_id]['msg_id'] = msg.id
- self.quick_quizz_messages.append(msg.id) # Rajoute le message dans la withelist
- self.quick_quizz_channels.append(msg.channel.id) # Rajoute le message dans la withelist
+ for emote in emotes:
+ await msg.add_reaction(emote) # Rajoute les emote sur le message
+ self.parties[party_id]["players"][player_id]["msg_id"] = msg.id
+ # Rajoute le message dans la withelist
+ self.quick_quizz_messages.append(msg.id)
+ # Rajoute le message dans la withelist
+ self.quick_quizz_channels.append(msg.channel.id)
async def send_party_question(self, party_id):
"""Envoie les question a tout les joueurs"""
- for player_id in self.parties[party_id]['players']:
+ for player_id in self.parties[party_id]["players"]:
await self.send_question(player_id, party_id)
self.update_timestamp(party_id)
async def send_answer(self, party_id):
- for player_id in self.parties[party_id]['players']:
- player: discord.User = await self.bot.fetch_user(player_id) # Fetch player
- msg = await player.fetch_message(self.parties[party_id]['players'][player_id]['msg_id'])
+ for player_id in self.parties[party_id]["players"]:
+ # Fetch player
+ player: discord.User = await self.bot.fetch_user(player_id)
+ msg = await player.fetch_message(
+ self.parties[party_id]["players"][player_id]["msg_id"]
+ )
embed = self.ez_question_embed(party_id) # Generate question embed
- raw_answer = self.QPQ.get_answer(self.parties[party_id]['quizz']['id'],
- self.parties[party_id]['ids'][self.parties[party_id]['quizz']['current']])
- embed.add_field(name="VRAI :" if raw_answer['real_answer'] else "FAUX :",
- value=clean_answer(raw_answer['explanation']))
+ raw_answer = self.QPQ.get_answer(
+ self.parties[party_id]["quizz"]["id"],
+ self.parties[party_id]["ids"][
+ self.parties[party_id]["quizz"]["current"]
+ ],
+ )
+ embed.add_field(
+ name="VRAI :" if raw_answer["real_answer"] else "FAUX :",
+ value=clean_answer(raw_answer["explanation"]),
+ )
await msg.edit(embed=embed) # Send it to the player
- # liste des emotes à rajouter (bordel j'altearn entre le français et l'anglais)
+ # liste des emotes à rajouter (bordel j'altearn entre le français
+ # et l'anglais)
emotes = REACTIONS.answers_reactions
- for emote in emotes: await msg.add_reaction(emote) # Rajoute les emote sur le message
+ for emote in emotes:
+ # Rajoute les emote sur le message
+ await msg.add_reaction(emote)
self.update_timestamp(party_id)
async def update_main_embed(self, embed: discord.Embed, party_id, player_id):
old_field = embed.fields[0]
- raw_players = old_field.value.split('\n')
+ raw_players = old_field.value.split("\n")
new_field_value = []
for raw_ligne in raw_players:
if str(player_id) in raw_ligne:
- raw_ligne = raw_ligne.split(' ')
+ raw_ligne = raw_ligne.split(" ")
del raw_ligne[len(raw_ligne) - 1]
- raw_ligne = ' '.join(raw_ligne)
- raw_ligne += '✅'
+ raw_ligne = " ".join(raw_ligne)
+ raw_ligne += "✅"
new_field_value.append(raw_ligne)
embed.clear_fields()
embed.add_field(name=old_field.name, value="\n".join(new_field_value))
- channel: discord.TextChannel = await self.bot.fetch_channel(self.parties[party_id]['channel_id'])
- msg: discord.Message = await channel.fetch_message(self.parties[party_id]['msg_id'])
+ channel: discord.TextChannel = await self.bot.fetch_channel(
+ self.parties[party_id]["channel_id"]
+ )
+ msg: discord.Message = await channel.fetch_message(
+ self.parties[party_id]["msg_id"]
+ )
self.update_timestamp(party_id)
return await msg.edit(embed=embed)
async def player_leave_update(self, message: discord.Message, party_id, user):
embed = message.embeds[0] # Récupère l'embed
- raw_players = embed.fields[0].value.split('\n') # Split tout les joueurs
+ raw_players = embed.fields[0].value.split("\n") # Split tout les joueurs
players = []
for player in raw_players:
- if str(user.id) not in player: players.append(
- player) # Si c'est le joueur qui se barre alors on le remet pas dans la liste
+ if str(user.id) not in player:
+ # Si c'est le joueur qui se barre alors on le remet pas dans la
+ # liste
+ players.append(player)
temp = len(players)
players = "\n".join(players) # Refait la liste
embed.clear_fields() # Cleanup
- embed.add_field(name="{} joueur{}".format(temp, "s" if temp > 1 else ''),
- value=players) # Refait la liste des joueurs
- self.parties[party_id]['players'].pop(user.id) # remove le joueur de la party
+ embed.add_field(
+ name="{} joueur{}".format(temp, "s" if temp > 1 else ""), value=players
+ ) # Refait la liste des joueurs
+ self.parties[party_id]["players"].pop(user.id) # remove le joueur de la party
embed.set_footer(text=party_id)
embed = self.ez_set_author(embed, party_id)
self.update_timestamp(party_id)
return await message.edit(embed=embed) # Nouvel embed
async def update_player_choice(self, party_id, player_id):
- channel: discord.TextChannel = await self.bot.fetch_channel(self.parties[party_id]['channel_id'])
- msg: discord.Message = await channel.fetch_message(self.parties[party_id]['msg_id'])
+ channel: discord.TextChannel = await self.bot.fetch_channel(
+ self.parties[party_id]["channel_id"]
+ )
+ msg: discord.Message = await channel.fetch_message(
+ self.parties[party_id]["msg_id"]
+ )
embed: discord.Embed = msg.embeds[0]
field_name = embed.fields[0].name
- field_value = embed.fields[0].value.split('\n')
+ field_value = embed.fields[0].value.split("\n")
new_value = ""
for line in field_value:
if str(player_id) in line:
- new_value += "\n{}".format(line.replace('', '✅'))
+ new_value += "\n{}".format(
+ line.replace("", "✅")
+ )
else:
new_value += "\n{}".format(line)
embed.clear_fields()
@@ -259,10 +326,13 @@ async def check_if_active(self):
for party_id in self.parties:
if party_id != "0":
if self.parties[party_id]["timestamp"] < timestamp + 60 * 100000:
- channel: Optional[discord.TextChannel] = await self.bot.get_channel(self.parties[party_id]["channel_id"])
+ channel: Optional[discord.TextChannel] = await self.bot.get_channel(
+ self.parties[party_id]["channel_id"]
+ )
if channel is not None:
await channel.send(
- f"<@{self.parties[party_id]['author_id']}> ton quizz sur {self.QPQ.get_name(self.parties[party_id]['quizz']['id'])} s'est arrêté car inactif !")
+ f"<@{self.parties[party_id]['author_id']}> ton quizz sur {self.QPQ.get_name(self.parties[party_id]['quizz']['id'])} s'est arrêté car inactif !"
+ )
partys_to_pop.append(party_id)
else:
self.parties[party_id]["timestamp"] = timestamp
@@ -283,29 +353,34 @@ async def on_raw_reaction_add(self, pauload: discord.RawReactionActionEvent):
channel: discord.DMChannel = await self.bot.fetch_channel(pauload.channel_id)
message: discord.Message = await channel.fetch_message(pauload.message_id)
- if len(message.embeds) == 0: return # Vu que tout passe par embeds, si y'en a pas on passe
+ if len(message.embeds) == 0:
+ return # Vu que tout passe par embeds, si y'en a pas on passe
if pauload.emoji.name == REACTIONS.PREVIOUS_QUESTION:
- if '/' in message.embeds[0].footer.text:
- raw_footer = message.embeds[0].footer.text.split('/')
+ if "/" in message.embeds[0].footer.text:
+ raw_footer = message.embeds[0].footer.text.split("/")
embed = message.embeds[0]
embed.clear_fields()
- if raw_footer[0] == '1':
+ if raw_footer[0] == "1":
param = 1
else:
param = int(raw_footer[0]) - 1
ids = [quizz_id for quizz_id in self.QPQ.data]
for n in range(15):
- embed.add_field(name=self.QPQ.data[ids[n + ((param-1) * 15)]]["name"],
- value=f"ID du quizz: `{ids[n + ((param-1) * 15)]}`")
+ embed.add_field(
+ name=self.QPQ.data[ids[n + ((param - 1) * 15)]]["name"],
+ value=f"ID du quizz: `{ids[n + ((param-1) * 15)]}`",
+ )
embed.set_footer(text=f"{param}/{len(self.QPQ.data) // 15}")
- await message.remove_reaction(REACTIONS.PREVIOUS_QUESTION, pauload.member)
+ await message.remove_reaction(
+ REACTIONS.PREVIOUS_QUESTION, pauload.member
+ )
return await message.edit(embed=embed)
elif pauload.emoji.name == REACTIONS.NEXT_QUESTION:
- if '/' in message.embeds[0].footer.text:
- raw_footer = message.embeds[0].footer.text.split('/')
+ if "/" in message.embeds[0].footer.text:
+ raw_footer = message.embeds[0].footer.text.split("/")
embed = message.embeds[0]
embed.clear_fields()
if raw_footer[0] == str(len(self.QPQ.data) // 15):
@@ -315,58 +390,86 @@ async def on_raw_reaction_add(self, pauload: discord.RawReactionActionEvent):
ids = [quizz_id for quizz_id in self.QPQ.data]
for n in range(15):
- embed.add_field(name=self.QPQ.data[ids[n + ((param-1) * 15)]]["name"],
- value=f"ID du quizz: `{ids[n + ((param-1) * 15)]}`")
+ embed.add_field(
+ name=self.QPQ.data[ids[n + ((param - 1) * 15)]]["name"],
+ value=f"ID du quizz: `{ids[n + ((param-1) * 15)]}`",
+ )
embed.set_footer(text=f"{param}/{len(self.QPQ.data) // 15}")
await message.remove_reaction(REACTIONS.NEXT_QUESTION, pauload.member)
return await message.edit(embed=embed)
try:
- party_id = int(message.embeds[0].footer.text) # On vérifie que y'est bien l'id de la party dans le footer
+ # On vérifie que y'est bien l'id de la party dans le footer
+ party_id = int(message.embeds[0].footer.text)
except ValueError:
return # Sinon on passe
party_id = str(party_id)
self.update_timestamp(party_id)
- if party_id not in self.parties: return
+ if party_id not in self.parties:
+ return
if pauload.guild_id is not None: # Si la réaction est sur un serveur
- if pauload.user_id == self.parties[party_id]['author_id']: # Si celui qui a réagis est le créateur du quizz
- if pauload.emoji.name == REACTIONS.START_QUIZ: # 🆗 => Commencer le quizz
-
- embed = self.ez_question_embed(party_id) # Génération de l'embed de question
- prev_players_markdown = message.embeds[0].fields[0].value.split('\n') # On récupère les joueurs
+ # Si celui qui a réagis est le créateur du quizz
+ if pauload.user_id == self.parties[party_id]["author_id"]:
+ if (
+ pauload.emoji.name == REACTIONS.START_QUIZ
+ ): # 🆗 => Commencer le quizz
+
+ # Génération de l'embed de question
+ embed = self.ez_question_embed(party_id)
+ prev_players_markdown = (
+ message.embeds[0].fields[0].value.split("\n")
+ ) # On récupère les joueurs
for n, player in enumerate(prev_players_markdown):
- prev_players_markdown[n] = player + " " # On rajoute le petit emote de sablier
- embed.add_field(name="{} joueur{}".format(len(prev_players_markdown), # Nombre de joueurs
- "s" if len(prev_players_markdown) > 1 else ''),
- value='\n'.join(prev_players_markdown)) # Remise de la liste des joueurs
+ # On rajoute le petit emote de sablier
+ prev_players_markdown[n] = (
+ player + " "
+ )
+ embed.add_field(
+ name="{} joueur{}".format(
+ len(prev_players_markdown), # Nombre de joueurs
+ "s" if len(prev_players_markdown) > 1 else "",
+ ),
+ value="\n".join(prev_players_markdown),
+ ) # Remise de la liste des joueurs
await message.edit(embed=embed) # Edit de l'ancien embed
await message.clear_reaction(REACTIONS.START_QUIZ)
await message.add_reaction(REACTIONS.FORWARD_QUESTION)
- self.parties[party_id]['started'] = True
- return await self.send_party_question(party_id) # On envoit les questions en mp
+ self.parties[party_id]["started"] = True
+ # On envoit les questions en mp
+ return await self.send_party_question(party_id)
- elif pauload.emoji.name == REACTIONS.STOP_QUIZ: # ❌ => annulation du quizz
+ elif (
+ pauload.emoji.name == REACTIONS.STOP_QUIZ
+ ): # ❌ => annulation du quizz
embed = discord.Embed(title="Quizz annulé")
self.parties.pop(party_id) # Supression dans le dict
await message.clear_reactions() # Retire toute les réactions
return await message.edit(embed=embed) # Feedback user
- elif pauload.emoji.name == REACTIONS.FORWARD_QUESTION: # on skip cette question
+ elif (
+ pauload.emoji.name == REACTIONS.FORWARD_QUESTION
+ ): # on skip cette question
if not self.has_everyone_answered(party_id):
return await channel.send(
- f"<@{self.parties[party_id]['author_id']}> tout le monde n'a pas encore répondu !")
+ f"<@{self.parties[party_id]['author_id']}> tout le monde n'a pas encore répondu !"
+ )
- if self.parties[party_id]['quizz']['current'] < 10:
- embed = self.ez_question_embed(party_id, leaderboard=True, waiting=True)
+ if self.parties[party_id]["quizz"]["current"] < 10:
+ embed = self.ez_question_embed(
+ party_id, leaderboard=True, waiting=True
+ )
await message.edit(embed=embed)
- await self.send_party_question(party_id) # On envoit les questions en mp
+ # On envoit les questions en mp
+ await self.send_party_question(party_id)
for player in self.parties[party_id]["players"]:
self.parties[party_id]["players"][player]["answer"] = None
- await message.remove_reaction(REACTIONS.FORWARD_QUESTION, pauload.member)
+ await message.remove_reaction(
+ REACTIONS.FORWARD_QUESTION, pauload.member
+ )
else:
await message.clear_reactions()
await message.edit(embed=self.ez_summary_embed(party_id))
@@ -374,43 +477,70 @@ async def on_raw_reaction_add(self, pauload: discord.RawReactionActionEvent):
else:
if pauload.emoji.name == REACTIONS.JOIN_QUIZ: # Un joueur join
embed = message.embeds[0]
- players = embed.fields[0].value + f'\n- <@!{pauload.user_id}> 0/10' \
- f'{" " if self.parties[party_id]["started"] else ""}' # Rajoute le joueur dans l'embed
+ # Rajoute le joueur dans l'embed
+ players = (
+ embed.fields[0].value + f"\n- <@!{pauload.user_id}> 0/10"
+ f'{" " if self.parties[party_id]["started"] else ""}'
+ )
embed.clear_fields() # Cleanup
- temp = players.split('\n')
- embed.add_field(name="{} joueur{}".format(len(temp), "s" if len(temp) > 1 else ''),
- value=players)
+ temp = players.split("\n")
+ embed.add_field(
+ name="{} joueur{}".format(
+ len(temp), "s" if len(temp) > 1 else ""
+ ),
+ value=players,
+ )
embed.set_footer(text=party_id)
embed = self.ez_set_author(embed, party_id)
- self.parties[party_id]["players"][int(pauload.user_id)] = {'score': 0, 'answer': None, 'msg_id': 0}
- if self.parties[party_id]["started"]: await self.send_question(pauload.user_id, party_id)
+ self.parties[party_id]["players"][int(pauload.user_id)] = {
+ "score": 0,
+ "answer": None,
+ "msg_id": 0,
+ }
+ if self.parties[party_id]["started"]:
+ await self.send_question(pauload.user_id, party_id)
return await message.edit(embed=embed) # Nouvel embed
else: # Si c'est en mp
if pauload.emoji.name == REACTIONS.ANSWER_FALSE:
- self.parties[party_id]['players'][pauload.user_id]['answer'] = False # Choisi la réponse négative
+ # Choisi la réponse négative
+ self.parties[party_id]["players"][pauload.user_id]["answer"] = False
await self.update_player_choice(party_id, pauload.user_id)
elif pauload.emoji.name == REACTIONS.ANSWER_TRUE:
- self.parties[party_id]['players'][pauload.user_id]['answer'] = True # Choisi la réponse positive
+ # Choisi la réponse positive
+ self.parties[party_id]["players"][pauload.user_id]["answer"] = True
await self.update_player_choice(party_id, pauload.user_id)
if pauload.emoji.name in [REACTIONS.ANSWER_TRUE, REACTIONS.ANSWER_FALSE]:
if self.has_everyone_answered(party_id):
- main_channel: discord.TextChannel = await self.bot.fetch_channel(self.parties[party_id]['channel_id'])
- main_message: discord.Message = await main_channel.fetch_message(self.parties[party_id]['msg_id'])
+ main_channel: discord.TextChannel = await self.bot.fetch_channel(
+ self.parties[party_id]["channel_id"]
+ )
+ main_message: discord.Message = await main_channel.fetch_message(
+ self.parties[party_id]["msg_id"]
+ )
await main_message.edit(embed=self.ez_answer_embed(party_id))
await self.send_answer(party_id)
- self.parties[party_id]['quizz']['current'] += 1
+ self.parties[party_id]["quizz"]["current"] += 1
return
- if pauload.emoji.name == REACTIONS.ANSWER_LEAVE: # Le joueur veut quitter le quizz
- main_channel: discord.TextChannel = await self.bot.fetch_channel(self.parties[party_id]['channel_id'])
- main_message: discord.Message = await main_channel.fetch_message(self.parties[party_id]['msg_id'])
- user = await self.bot.fetch_user(pauload.user_id) # Faut optimiser ct'e merde
- await self.player_leave_update(main_message, party_id, user) # Retire le joueur
- return await channel.send("Vous avez quitté le quizz") # Feedback user
+ if (
+ pauload.emoji.name == REACTIONS.ANSWER_LEAVE
+ ): # Le joueur veut quitter le quizz
+ main_channel: discord.TextChannel = await self.bot.fetch_channel(
+ self.parties[party_id]["channel_id"]
+ )
+ main_message: discord.Message = await main_channel.fetch_message(
+ self.parties[party_id]["msg_id"]
+ )
+ # Faut optimiser ct'e merde
+ user = await self.bot.fetch_user(pauload.user_id)
+ # Retire le joueur
+ await self.player_leave_update(main_message, party_id, user)
+ # Feedback user
+ return await channel.send("Vous avez quitté le quizz")
@commands.Cog.listener()
async def on_raw_reaction_remove(self, pauload: discord.RawReactionActionEvent):
@@ -425,33 +555,44 @@ async def on_raw_reaction_remove(self, pauload: discord.RawReactionActionEvent):
channel: discord.DMChannel = await self.bot.fetch_channel(pauload.channel_id)
message: discord.Message = await channel.fetch_message(pauload.message_id)
- if len(message.embeds) == 0: return # Vu que tout passe par embeds, si y'en a pas on passe
+ if len(message.embeds) == 0:
+ return # Vu que tout passe par embeds, si y'en a pas on passe
try:
- party_id = int(message.embeds[0].footer.text) # On vérifie que y'est bien l'id de la party dans le footer
+ # On vérifie que y'est bien l'id de la party dans le footer
+ party_id = int(message.embeds[0].footer.text)
except ValueError:
return # Sinon on passe
party_id = str(party_id)
self.update_timestamp(party_id)
- if party_id not in self.parties: return
-
- if pauload.guild_id is not None and pauload.emoji.name == REACTIONS.JOIN_QUIZ and not self.parties[party_id][
- 'started']: # Si un joueur se barre
- user: discord.User = await self.bot.fetch_user(pauload.user_id) # Récupère l'user
- return await self.player_leave_update(message, party_id, user) # Generate new player list
+ if party_id not in self.parties:
+ return
+
+ if (
+ pauload.guild_id is not None
+ and pauload.emoji.name == REACTIONS.JOIN_QUIZ
+ and not self.parties[party_id]["started"]
+ ): # Si un joueur se barre
+ # Récupère l'user
+ user: discord.User = await self.bot.fetch_user(pauload.user_id)
+ # Generate new player list
+ return await self.player_leave_update(message, party_id, user)
@commands.group(name="quizz")
async def quizz_core(self, ctx: MyContext):
await ctx.message.delete()
if ctx.invoked_subcommand is None:
- embed = discord.Embed(
- title="Quizz help",
- color=discord.Colour.orange()
- )
+ embed = discord.Embed(title="Quizz help", color=discord.Colour.orange())
embed.add_field(name=f"`quizz`", value="Shows this message", inline=False)
- embed.add_field(name=f"`quizz start `", value="Démarre un quizz", inline=False)
- embed.add_field(name="`quizz themes`", value="Donne la liste des thèmes WIP", inline=False)
+ embed.add_field(
+ name=f"`quizz start `", value="Démarre un quizz", inline=False
+ )
+ embed.add_field(
+ name="`quizz themes`",
+ value="Donne la liste des thèmes WIP",
+ inline=False,
+ )
return await ctx.send(embed=embed)
@quizz_core.command(name="start")
@@ -462,30 +603,32 @@ async def _quizz_start(self, ctx: MyContext, quizz_id: str):
question_ids = []
raw_question = self.QPQ.get_questions(quizz_id)
- if raw_question is None: return await ctx.send("L'ID du quizz est invalide.")
+ if raw_question is None:
+ return await ctx.send("L'ID du quizz est invalide.")
for question_id in raw_question:
question_ids.append(question_id)
- self.parties[party_id] = {'author_id': ctx.author.id,
- 'guild_id': ctx.guild.id,
- 'timestamp': time.time(),
- 'msg_id': 0,
- 'channel_id': ctx.channel.id,
- 'players': {int(ctx.author.id): {'score': 0, 'answer': None, 'msg_id': 0}},
- 'quizz': {'id': quizz_id, 'current': 0},
- 'ids': question_ids,
- 'started': False
- }
+ self.parties[party_id] = {
+ "author_id": ctx.author.id,
+ "guild_id": ctx.guild.id,
+ "timestamp": time.time(),
+ "msg_id": 0,
+ "channel_id": ctx.channel.id,
+ "players": {int(ctx.author.id): {"score": 0, "answer": None, "msg_id": 0}},
+ "quizz": {"id": quizz_id, "current": 0},
+ "ids": question_ids,
+ "started": False,
+ }
embed = discord.Embed(
- title=f'Partie de {ctx.author.display_name}',
- description=f"Sur le thème de :\n\t- **{self.QPQ.get_name(quizz_id)}**"
+ title=f"Partie de {ctx.author.display_name}",
+ description=f"Sur le thème de :\n\t- **{self.QPQ.get_name(quizz_id)}**",
)
embed.add_field(name="1 joueur", value=f"- {ctx.author.mention}: 0/10")
embed.set_footer(text=party_id)
embed = self.ez_set_author(embed, party_id)
msg: discord.Message = await ctx.send(embed=embed)
- self.parties[party_id]['msg_id'] = msg.id
+ self.parties[party_id]["msg_id"] = msg.id
emojis = [REACTIONS.JOIN_QUIZ, REACTIONS.STOP_QUIZ, REACTIONS.START_QUIZ]
for emoji in emojis:
await msg.add_reaction(emoji)
@@ -494,14 +637,12 @@ async def _quizz_start(self, ctx: MyContext, quizz_id: str):
@quizz_core.command(name="themes")
async def _quizz_themes(self, ctx: MyContext):
- embed = discord.Embed(
- title="THEMES",
- color=discord.Colour.random()
- )
+ embed = discord.Embed(title="THEMES", color=discord.Colour.random())
ids = [quizz_id for quizz_id in self.QPQ.data]
for n in range(15):
- embed.add_field(name=self.QPQ.data[ids[n]]["name"],
- value=f"ID du quizz: `{ids[n]}`")
+ embed.add_field(
+ name=self.QPQ.data[ids[n]]["name"], value=f"ID du quizz: `{ids[n]}`"
+ )
embed.set_footer(text=f"1/{len(self.QPQ.data)//15}")
msg: discord.Message = await ctx.send(embed=embed)
emojis = ["⬅️", "➡️"]
@@ -511,6 +652,11 @@ async def _quizz_themes(self, ctx: MyContext):
self.quick_quizz_channels.append(ctx.channel.id)
return
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Quizz(bot), icon="❓")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
-async def setup(bot):
- await bot.add_cog(Quizz(bot))
diff --git a/plugins/roleLink/credits.md b/plugins/roleLink/credits.md
new file mode 100644
index 00000000..9d682887
--- /dev/null
+++ b/plugins/roleLink/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2022 - 2022
+Copyright © Aeris One 2022
+Copyright © ascpial 2023
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/roleLink/data/model.sql b/plugins/roleLink/data/model.sql
index c3c20deb..437d1567 100644
--- a/plugins/roleLink/data/model.sql
+++ b/plugins/roleLink/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `group_roles` (
`guild` BIGINT NOT NULL,
`action` SMALLINT NOT NULL DEFAULT 0,
diff --git a/plugins/roleLink/docs/user_documentation.md b/plugins/roleLink/docs/user_documentation.md
index b057b256..c0cdb503 100644
--- a/plugins/roleLink/docs/user_documentation.md
+++ b/plugins/roleLink/docs/user_documentation.md
@@ -1,4 +1,11 @@
-## Role link
+
+
+# 📎 Role link
This system allows you to create dependencies between roles and thus manage automatically the gain/withdrawal of roles according to the acquisition or loss of other roles. To create a link between roles, you will need to perform the command:
diff --git a/plugins/roleLink/langs/en.yml b/plugins/roleLink/langs/en.yml
deleted file mode 100644
index d6ac6b01..00000000
--- a/plugins/roleLink/langs/en.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-en:
- grouproles:
- dep-added: A new dependency has been added, with the ID %{id}
- dep-deleted: Your role-link has been removed
- dep-notfound: Cannot find a role-link with this identifier. You can get the identifier of a link with the `%{p}rolelink list` command
- infinite: "Oops, it seems that this dependency can lead to an infinite loop with at least the following dependency: \"%{dep}\"\nIf you're sure you want to continue, enter 'yes' in the next %{t} seconds..."
- list: "List of your roles-links:"
- no-dep: "You do not have any dependencies configured at the moment.\nUse the `%{p}rolelink create` command to add more dependencies"
\ No newline at end of file
diff --git a/plugins/roleLink/langs/fr.yml b/plugins/roleLink/langs/fr.yml
deleted file mode 100644
index 4bbca769..00000000
--- a/plugins/roleLink/langs/fr.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-fr:
- grouproles:
- dep-added: Une nouvelle dépendance a été ajoutée, avec l'ID %{id}
- dep-deleted: Votre rôle-liaison a bien été supprimée
- dep-notfound: Impossible de trouver une rôle-liaison avec cet identifiant.\nVous pouvez obtenir l'identifiant d'une liaison avec la commande `%{p}rolelink list`
- infinite: "Oups, il semble que cette dépendance puisse entraîner une boucle infinie avec au moins la dépendance suivante : \"%{dep}\"\nSi vous êtes sûr de vouloir continuer, entrez 'oui' dans les %{t} prochaines secondes"
- list: "Liste de vos rôles-liaisons :"
- no-dep: "Vous n'avez aucune dépendance de configurée pour le moment.\nUtilisez la commande `%{p}rolelink create` pour en ajouter"
\ No newline at end of file
diff --git a/plugins/roleLink/bot/main.py b/plugins/roleLink/roleLink.py
similarity index 64%
rename from plugins/roleLink/bot/main.py
rename to plugins/roleLink/roleLink.py
index 0a1ea2a5..ca1bb647 100644
--- a/plugins/roleLink/bot/main.py
+++ b/plugins/roleLink/roleLink.py
@@ -1,22 +1,29 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import Gunibot
+from discord.ext import commands
+import discord
+from bot import checks
+import args
import asyncio
from marshal import dumps, loads
from typing import List, Union
import sys
+
sys.path.append("./bot")
-import args
-import sys
sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.ext import commands
-from utils import Gunibot
# /rolelink when
class ActionType(commands.Converter):
- types = ['grant', 'revoke']
+ types = ["grant", "revoke"]
def __init__(self, action: Union[str, int] = None):
if isinstance(action, str):
@@ -34,7 +41,7 @@ async def convert(self, ctx: commands.Context, argument: str):
class TriggerType(commands.Converter):
- types = ['get-one', 'get-all', 'loose-one', 'loose-all']
+ types = ["get-one", "get-all", "loose-one", "loose-all"]
def __init__(self, trigger: Union[str, int] = None):
if isinstance(trigger, str):
@@ -52,7 +59,14 @@ async def convert(self, ctx: commands.Context, argument: str):
class Dependency:
- def __init__(self, action: ActionType, target_role: int, trigger: TriggerType, trigger_roles: List[int], guild: int):
+ def __init__(
+ self,
+ action: ActionType,
+ target_role: int,
+ trigger: TriggerType,
+ trigger_roles: List[int],
+ guild: int,
+ ):
self.action = action
self.target_role = target_role
self.trigger = trigger
@@ -62,41 +76,51 @@ def __init__(self, action: ActionType, target_role: int, trigger: TriggerType, t
self.id = None
def to_str(self, useID: bool = True) -> str:
- triggers = ' '.join([f'<@&{r}>' for r in self.trigger_roles])
- target = f'<@&{self.target_role}>'
- ID = f"{self.id}. " if useID else ''
+ triggers = " ".join([f"<@&{r}>" for r in self.trigger_roles])
+ target = f"<@&{self.target_role}>"
+ ID = f"{self.id}. " if useID else ""
return f"{ID}{self.action.name} {target} when {self.trigger.name.replace('-', ' ')} of {triggers}"
class ConflictingCyclicDependencyError(Exception):
"""Used when a loop is found when analyzing a role dependencies system"""
+
pass
class GroupRoles(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = ""
def db_get_config(self, guildID: int) -> List[Dependency]:
"""Get every dependencies of a specific guild"""
- query = 'SELECT rowid, * FROM group_roles WHERE guild=?'
+ query = "SELECT rowid, * FROM group_roles WHERE guild=?"
# comes as: (rowid, guild, action, target, trigger, trigger-roles)
res = list()
liste = self.bot.db_query(query, (guildID,))
for row in liste:
- temp = (ActionType(row['action']), row['target'], TriggerType(
- row['trigger']), loads(row['trigger-roles']), row['guild'])
+ temp = (
+ ActionType(row["action"]),
+ row["target"],
+ TriggerType(row["trigger"]),
+ loads(row["trigger-roles"]),
+ row["guild"],
+ )
res.append(Dependency(*temp))
- res[-1].id = row['rowid']
+ res[-1].id = row["rowid"]
return res if len(res) > 0 else None
def db_add_action(self, action: Dependency) -> int:
"""Add an action into a guild
Return the inserted row ID"""
- data = (action.guild, action.action.type, action.target_role,
- action.trigger.type, action.b_trigger_roles)
+ data = (
+ action.guild,
+ action.action.type,
+ action.target_role,
+ action.trigger.type,
+ action.b_trigger_roles,
+ )
query = "INSERT INTO group_roles (guild, action, target, trigger, `trigger-roles`) VALUES (?, ?, ?, ?, ?)"
lastrowid = self.bot.db_query(query, data)
return lastrowid
@@ -108,7 +132,9 @@ def db_delete_action(self, guildID: int, actionID: int) -> bool:
rowcount = self.bot.db_query(query, (guildID, actionID))
return rowcount == 1
- async def filter_allowed_roles(self, guild: discord.Guild, roles: List[discord.Role]) -> List[discord.Role]:
+ async def filter_allowed_roles(
+ self, guild: discord.Guild, roles: List[discord.Role]
+ ) -> List[discord.Role]:
"""Return every role that the bot is allowed to give/remove
IE: role exists, role is under bot's highest role
If bot doesn't have the "manage roles" perm, list will be empty"""
@@ -116,8 +142,7 @@ async def filter_allowed_roles(self, guild: discord.Guild, roles: List[discord.R
return list()
pos: int = guild.me.top_role.position
roles = [guild.get_role(x) for x in roles]
- roles = list(filter(lambda x: (x is not None)
- and (x.position < pos), roles))
+ roles = list(filter(lambda x: (x is not None) and (x.position < pos), roles))
return roles
@commands.Cog.listener()
@@ -133,7 +158,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member):
if lost:
await self.check_lost_roles(after, lost)
- async def give_remove_roles(self, member: discord.Member, roles: List[discord.Role], action: ActionType):
+ async def give_remove_roles(
+ self, member: discord.Member, roles: List[discord.Role], action: ActionType
+ ):
if not roles: # list is empty or None
return
names = [x.name for x in roles]
@@ -147,46 +174,68 @@ async def give_remove_roles(self, member: discord.Member, roles: List[discord.Ro
async def check_got_roles(self, member: discord.Member, roles: List[discord.Role]):
"""Trigger dependencies based on granted roles"""
actions = self.db_get_config(member.guild.id)
- if actions == None:
+ if actions is None:
return
for action in actions:
if action.trigger.type == 0: # if trigger is 'get-one'
for r in roles:
- if r.id in action.trigger_roles: # if one given role triggers that action
- alwd_roles = await self.filter_allowed_roles(member.guild, [action.target_role])
+ if (
+ r.id in action.trigger_roles
+ ): # if one given role triggers that action
+ alwd_roles = await self.filter_allowed_roles(
+ member.guild, [action.target_role]
+ )
await self.give_remove_roles(member, alwd_roles, action.action)
break
elif action.trigger.type == 1: # if trigger is 'get-all'
for r in roles:
- if r.id in action.trigger_roles: # if one given role triggers that action
+ if (
+ r.id in action.trigger_roles
+ ): # if one given role triggers that action
member_roles = [x.id for x in member.roles]
if all([(x in member_roles) for x in action.trigger_roles]):
- alwd_roles = await self.filter_allowed_roles(member.guild, [action.target_role])
- await self.give_remove_roles(member, alwd_roles, action.action)
+ alwd_roles = await self.filter_allowed_roles(
+ member.guild, [action.target_role]
+ )
+ await self.give_remove_roles(
+ member, alwd_roles, action.action
+ )
break
async def check_lost_roles(self, member: discord.Member, roles: List[discord.Role]):
"""Trigger dependencies based on revoked roles"""
actions = self.db_get_config(member.guild.id)
- if actions == None:
+ if actions is None:
return
for action in actions:
if action.trigger.type == 2: # if trigger is 'loose-one'
for r in roles:
- if r.id in action.trigger_roles: # if one lost role triggers that action
- alwd_roles = await self.filter_allowed_roles(member.guild, [action.target_role])
+ if (
+ r.id in action.trigger_roles
+ ): # if one lost role triggers that action
+ alwd_roles = await self.filter_allowed_roles(
+ member.guild, [action.target_role]
+ )
await self.give_remove_roles(member, alwd_roles, action.action)
break
elif action.trigger.type == 3: # if trigger is 'loose-all'
for r in roles:
- if r.id in action.trigger_roles: # if one lost role triggers that action
+ if (
+ r.id in action.trigger_roles
+ ): # if one lost role triggers that action
member_roles = [x.id for x in member.roles]
if all([(x not in member_roles) for x in action.trigger_roles]):
- alwd_roles = await self.filter_allowed_roles(member.guild, [action.target_role])
- await self.give_remove_roles(member, alwd_roles, action.action)
+ alwd_roles = await self.filter_allowed_roles(
+ member.guild, [action.target_role]
+ )
+ await self.give_remove_roles(
+ member, alwd_roles, action.action
+ )
break
- async def get_triggers(self, action: Dependency, actions: List[Dependency]) -> List[Dependency]:
+ async def get_triggers(
+ self, action: Dependency, actions: List[Dependency]
+ ) -> List[Dependency]:
"""Get every dependency which will directly trigger a selected action"""
triggers = list()
unwanted_action = 0 if action.trigger.type <= 1 else 1
@@ -194,7 +243,10 @@ async def get_triggers(self, action: Dependency, actions: List[Dependency]) -> L
if a.id == action.id:
continue
# if a will trigger action
- if a.action.type == unwanted_action and a.target_role in action.trigger_roles:
+ if (
+ a.action.type == unwanted_action
+ and a.target_role in action.trigger_roles
+ ):
triggers.append(a)
if action.trigger.type in (1, 3): # get-all or loose-all
roles = list(action.trigger_roles)
@@ -205,7 +257,9 @@ async def get_triggers(self, action: Dependency, actions: List[Dependency]) -> L
return triggers
return triggers
- async def compute_actions(self, action: Dependency, actions_done: list, all_actions: list):
+ async def compute_actions(
+ self, action: Dependency, actions_done: list, all_actions: list
+ ):
"""Check if a list of dependencies may contain a loop"""
for target_action in await self.get_triggers(action, all_actions):
already_noted = target_action in actions_done
@@ -216,46 +270,73 @@ async def compute_actions(self, action: Dependency, actions_done: list, all_acti
@commands.group(name="rolelink")
@commands.guild_only()
+ @commands.has_permissions(manage_guild=True)
async def rolelink_main(self, ctx: commands.Context):
"""Manage your roles-links"""
if ctx.subcommand_passed is None:
- await ctx.send_help('rolelink')
+ await ctx.send_help("rolelink")
@rolelink_main.command(name="add")
- @commands.check(checks.is_server_manager)
- async def rolelink_create(self, ctx: commands.Context, action: ActionType, target_role: discord.Role, when: args.constant('when'), trigger: TriggerType, trigger_roles: commands.Greedy[discord.Role]):
+ @commands.has_permissions(manage_guild=True)
+ async def rolelink_create(
+ self,
+ ctx: commands.Context,
+ action: ActionType,
+ target_role: discord.Role,
+ when: args.constant("when"),
+ trigger: TriggerType,
+ trigger_roles: commands.Greedy[discord.Role],
+ ):
"""Create a new roles-link
Actions can be either grant or revoke
Trigger can be either get-one, get-all, loose-one or loose-all"""
if not trigger_roles:
await ctx.send("Il vous faut au moins 1 rôle déclencheur !")
return
- action = Dependency(action, target_role.id, trigger, [
- x.id for x in trigger_roles], ctx.guild.id)
+ action = Dependency(
+ action, target_role.id, trigger, [x.id for x in trigger_roles], ctx.guild.id
+ )
try:
all_actions = self.db_get_config(ctx.guild.id)
if all_actions is not None:
- await self.compute_actions(action, list(), all_actions+[action])
+ await self.compute_actions(action, list(), all_actions + [action])
except ConflictingCyclicDependencyError as e:
timeout = 20
- await ctx.send(await self.bot._(ctx.guild.id, "grouproles.infinite", dep=e.args[0].to_str(False), t=timeout))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "grouproles.infinite",
+ dep=e.args[0].to_str(False),
+ t=timeout,
+ )
+ )
def check(m: discord.Message):
- return m.author == ctx.author and m.channel == ctx.channel and m.content.lower() in ("oui", "yes")
+ return (
+ m.author == ctx.author
+ and m.channel == ctx.channel
+ and m.content.lower() in ("oui", "yes")
+ )
+
try:
- await self.bot.wait_for('message', check=check, timeout=timeout)
+ await self.bot.wait_for("message", check=check, timeout=timeout)
except asyncio.TimeoutError:
return
actionID = self.db_add_action(action)
- await ctx.send(await self.bot._(ctx.guild.id, "grouproles.dep-added",id=actionID))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "grouproles.dep-added", id=actionID)
+ )
@rolelink_main.command(name="list")
@commands.cooldown(1, 10, commands.BucketType.guild)
+ @commands.has_permissions(manage_guild=True)
async def rolelink_list(self, ctx: commands.Context):
"""List your roles-links"""
actions = self.db_get_config(ctx.guild.id)
if not actions:
- await ctx.send(await self.bot._(ctx.guild.id, "grouproles.no-dep", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "grouproles.no-dep", p=ctx.prefix)
+ )
return
txt = "**" + await self.bot._(ctx.guild.id, "grouproles.list") + "**\n"
for action in actions:
@@ -263,15 +344,23 @@ async def rolelink_list(self, ctx: commands.Context):
await ctx.send(txt)
@rolelink_main.command(name="remove")
- @commands.check(checks.is_server_manager)
+ @commands.has_permissions(manage_guild=True)
async def rolelink_delete(self, ctx: commands.Context, id: int):
"""Delete one of your roles-links"""
deleted = self.db_delete_action(ctx.guild.id, id)
if deleted:
await ctx.send(await self.bot._(ctx.guild.id, "grouproles.dep-deleted"))
else:
- await ctx.send(await self.bot._(ctx.guild.id, "grouproles.dep-notfound", p=ctx.prefix))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "grouproles.dep-notfound", p=ctx.prefix)
+ )
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(GroupRoles(bot), icon="🔰")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
-async def setup(bot):
- await bot.add_cog(GroupRoles(bot))
diff --git a/plugins/rss/bot/main.py b/plugins/rss/bot/main.py
deleted file mode 100644
index 69826848..00000000
--- a/plugins/rss/bot/main.py
+++ /dev/null
@@ -1,1431 +0,0 @@
-import asyncio
-import datetime
-import html
-import re
-import time
-import typing
-from marshal import dumps, loads
-
-import sys
-sys.path.append("./bot")
-import args
-import async_timeout
-import sys
-sys.path.append("./bot")
-from bot import checks
-import discord
-import feedparser
-import twitter
-from aiohttp import client_exceptions
-from aiohttp.client import ClientSession
-from discord.ext import commands, tasks
-from feedparser.util import FeedParserDict
-from utils import Gunibot, MyContext
-
-
-class Rss(commands.Cog):
- """Cog which deals with everything related to rss flows. Whether it is to add automatic tracking to a stream, or just to see the latest video released by Discord, it is this cog that will be used."""
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.time_loop = 15 # min minutes between two rss loops
- self.time_between_flows_check = 0.15 # seconds between two rss checks within a loop
- self.max_feeds_per_guild = 100
-
- self.embed_color = discord.Color(6017876)
- self.loop_processing = False
- self.twitterAPI = twitter.Api(**bot.config['twitter'], tweet_mode="extended")
- self.twitter_over_capacity = False
- self.min_time_between_posts = {
- 'web': 120,
- 'tw': 15,
- 'yt': 120
- }
- self.cache = dict()
- self.table = 'rss_flows'
- try:
- self.date = bot.get_cog("TimeCog").date
- except:
- pass
- # launch rss loop
- self.loop_child.change_interval(minutes=self.time_loop)
- self.loop_child.start()
-
- @commands.Cog.listener()
- async def on_ready(self):
- self.date = self.bot.get_cog("TimeCog").date
-
- def cog_unload(self):
- self.loop_child.cancel()
-
- class rssMessage:
- def __init__(self,bot:Gunibot,Type,url,title,date=datetime.datetime.now(),author=None,Format=None,channel=None,image=None):
- self.bot = bot
- self.Type = Type
- self.url = url
- self.title = title
- self.embed = False # WARNING COOKIES WARNINNG
- self.image = image
- if type(date) == datetime.datetime:
- self.date = date
- elif type(date) == time.struct_time:
- self.date = datetime.datetime(*date[:6])
- elif type(date) == str:
- self.date = date
- else:
- date = None
- self.author = author
- self.format = Format
- self.logo = ':newspaper:'
- self.channel = channel
- self.mentions = []
- if self.author is None:
- self.author = channel
-
- def fill_embed_data(self, flow: dict):
- if not flow['use_embed']:
- return
- self.embed_data = {'color':discord.Colour(0).default(),
- 'footer':'',
- 'title':None}
- if not flow['embed_structure']:
- return
- structure: dict = flow['embed_structure']
- if title := structure.get('title', None):
- self.embed_data['title'] = title[:256]
- if footer := structure.get('footer', None):
- self.embed_data['footer'] = footer[:2048]
- if color := structure.get('color', None):
- self.embed_data['color'] = color
- return
-
- async def fill_mention(self, guild: discord.Guild, roles: typing.List[str], translate):
- if roles == []:
- r = await translate(guild.id,"keywords.none")
- else:
- r = list()
- for item in roles:
- if item=='':
- continue
- role = discord.utils.get(guild.roles,id=int(item))
- if role is not None:
- r.append(role.mention)
- else:
- r.append(item)
- self.mentions = r
- return self
-
- async def create_msg(self, language, Format=None):
- if Format is None:
- Format = self.format
- if not isinstance(self.date,str):
- d = await self.bot.get_cog("TimeCog").date(self.date,lang=language,year=False,hour=True,digital=True)
- else:
- d = self.date
- Format = Format.replace('\\n','\n')
- _channel = discord.utils.escape_markdown(self.channel)
- _author = discord.utils.escape_markdown(self.author)
- text = Format.format_map(self.bot.SafeDict(channel=_channel,title=self.title,date=d,url=self.url,link=self.url,mentions=", ".join(self.mentions),logo=self.logo,author=_author))
- if not self.embed:
- return text
- else:
- emb = discord.Embed(description=text,
- timestamp=self.date,
- color=self.embed_data.get('color', 0))
- if footer := self.embed_data.get('footer', None):
- emb.set_footer(text=footer)
- if self.embed_data.get('title', None) is None:
- if self.Type != 'tw':
- emb.title = self.title
- else:
- emb.title = self.author
- else:
- emb.title = self.embed_data['title']
- emb.add_field(name='URL',value=self.url, inline=False)
- if self.image is not None:
- emb.set_thumbnail(url=self.image)
- return emb
-
- async def get_lang(self, guild: typing.Optional[discord.Guild]) -> str:
- guildID = guild.id if guild else None
- return await self.bot.get_cog("Languages").get_lang(guildID, True)
-
- @commands.group(name="rss")
- @commands.cooldown(2,15,commands.BucketType.channel)
- async def rss_main(self, ctx: MyContext):
- """See the last post of a rss feed"""
- if ctx.subcommand_passed is None:
- await self.bot.get_cog('Help').help_command(ctx,['rss'])
-
- @rss_main.command(name="youtube",aliases=['yt'])
- async def request_yt(self, ctx: MyContext, ID):
- """The last video of a YouTube channel
-
- ..Examples:
- - rss youtube UCZ5XnGb-3t7jCkXdawN2tkA
- - rss youtube https://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA"""
- if "youtube.com" in ID or "youtu.be" in ID:
- ID = await self.parse_yt_url(ID)
- if ID is None:
- return await ctx.send(await self.bot._(ctx.channel, "rss.web-invalid"))
- text = await self.rss_yt(ctx.channel,ID)
- if type(text) == str:
- await ctx.send(text)
- else:
- form = await self.bot._(ctx.channel,"rss.yt-form-last")
- obj = await text[0].create_msg(await self.get_lang(ctx.guild),form)
- if isinstance(obj,discord.Embed):
- await ctx.send(embed=obj)
- else:
- await ctx.send(obj)
-
- @rss_main.command(name="twitch",aliases=['tv'])
- async def request_twitch(self, ctx: MyContext, channel):
- """The last video of a Twitch channel
-
- ..Examples:
- - rss twitch aureliensama
- - rss tv https://www.twitch.tv/aureliensama"""
- if "twitch.tv" in channel:
- channel = await self.parse_twitch_url(channel)
- text = await self.rss_twitch(ctx.channel,channel)
- if type(text) == str:
- await ctx.send(text)
- else:
- form = await self.bot._(ctx.channel,"rss.twitch-form-last")
- obj = await text[0].create_msg(await self.get_lang(ctx.guild),form)
- if isinstance(obj,discord.Embed):
- await ctx.send(embed=obj)
- else:
- await ctx.send(obj)
-
- @rss_main.command(name='twitter',aliases=['tw'])
- async def request_tw(self, ctx: MyContext, name):
- """The last tweet of a Twitter account
-
- ..Examples:
- - rss twitter https://twitter.com/z_runnerr
- - rss tw z_runnerr
- """
- if "twitter.com" in name:
- name = await self.parse_tw_url(name)
- try:
- text = await self.rss_tw(ctx.channel,name)
- except Exception as e:
- return await self.bot.get_cog('Errors').on_error(e,ctx)
- if type(text) == str:
- await ctx.send(text)
- else:
- form = await self.bot._(ctx.channel,"rss.tw-form-last")
- for single in text[:5]:
- obj = await single.create_msg(await self.get_lang(ctx.guild),form)
- if isinstance(obj,discord.Embed):
- await ctx.send(embed=obj)
- else:
- await ctx.send(obj)
-
- @rss_main.command(name="web")
- async def request_web(self, ctx: MyContext, link):
- """The last post on any other rss feed
-
- Example: rss web https://fr-minecraft.net/rss.php"""
- text = await self.rss_web(ctx.channel,link)
- if type(text) == str:
- await ctx.send(text)
- else:
- form = await self.bot._(ctx.channel,"rss.web-form-last")
- obj = await text[0].create_msg(await self.get_lang(ctx.guild),form)
- if isinstance(obj,discord.Embed):
- await ctx.send(embed=obj)
- else:
- await ctx.send(obj)
-
- @rss_main.command(name="deviantart",aliases=['deviant'])
- async def request_deviant(self, ctx: MyContext, user):
- """The last pictures of a DeviantArt user
-
- Example: rss deviant https://www.deviantart.com/adri526"""
- if "deviantart.com" in user:
- user = await self.parse_deviant_url(user)
- text = await self.rss_deviant(ctx.guild,user)
- if type(text) == str:
- await ctx.send(text)
- else:
- form = await self.bot._(ctx.channel,"rss.deviant-form-last")
- obj = await text[0].create_msg(await self.get_lang(ctx.guild),form)
- if isinstance(obj,discord.Embed):
- await ctx.send(embed=obj)
- else:
- await ctx.send(obj)
-
-
- async def is_overflow(self, guild: discord.Guild) -> bool:
- """Check if a guild still has at least a slot
- True if max number reached, followed by the flow limit"""
- return len(await self.db_get_guild_flows(guild.id)) >= self.max_feeds_per_guild
-
- @rss_main.command(name="add")
- @commands.guild_only()
- @commands.check(commands.has_guild_permissions(manage_webhooks=True))
- async def system_add(self, ctx: MyContext, link):
- """Subscribe to a rss feed, displayed on this channel regularly
-
- ..Examples:
- - rss add https://www.deviantart.com/adri526
- - rss add https://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA"""
- is_over = await self.is_overflow(ctx.guild)
- if is_over:
- await ctx.send(str(await self.bot._(ctx.guild.id, "rss.flow-limit")).format(self.max_feeds_per_guild))
- return
- identifiant = await self.parse_yt_url(link)
- Type = None
- if identifiant is not None:
- Type = 'yt'
- display_type = 'youtube'
- if identifiant is None:
- identifiant = await self.parse_tw_url(link)
- if identifiant is not None:
- Type = 'tw'
- display_type = 'twitter'
- if identifiant is None:
- identifiant = await self.parse_twitch_url(link)
- if identifiant is not None:
- Type = 'twitch'
- display_type = 'twitch'
- if identifiant is None:
- identifiant = await self.parse_deviant_url(link)
- if identifiant is not None:
- Type = 'deviant'
- display_type = 'deviantart'
- if identifiant is not None and not link.startswith("https://"):
- link = "https://"+link
- if identifiant is None and link.startswith("http"):
- identifiant = link
- Type = "web"
- display_type = 'website'
- elif not link.startswith("http"):
- await ctx.send(await self.bot._(ctx.guild,"rss.invalid-link"))
- return
- if Type is None or not await self.check_rss_url(link):
- return await ctx.send(await self.bot._(ctx.guild.id,"rss.invalid-flow"))
- try:
- ID = await self.db_add_flow(ctx.guild.id,ctx.channel.id,Type,identifiant)
- await ctx.send(str(await self.bot._(ctx.guild,"rss.success-add")).format(display_type,link,ctx.channel.mention))
- self.bot.log.info("RSS feed added into server {} ({} - {})".format(ctx.guild.id,link,ID))
- await self.send_log("Feed added into server {} ({})".format(ctx.guild.id,ID),ctx.guild)
- except Exception as e:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- await self.bot.get_cog("Errors").on_error(e,ctx)
-
- @rss_main.command(name="remove",aliases=['delete'])
- @commands.guild_only()
- @commands.check(commands.has_guild_permissions(manage_webhooks=True))
- async def systeme_rm(self, ctx: MyContext, ID:int=None):
- """Delete an rss feed from the list
-
- Example: rss remove"""
- flow = await self.askID(ID,
- ctx,
- await self.bot._(ctx.guild.id, "rss.choose-delete"),
- allow_mc=True,
- display_mentions=False
- )
- if flow is None:
- return
- try:
- await self.db_remove_flow(flow[0]['ID'])
- except Exception as e:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- await self.bot.get_cog("Errors").on_error(e,ctx)
- return
- await ctx.send(await self.bot._(ctx.guild,"rss.delete-success"))
- self.bot.log.info("RSS feed deleted into server {} ({})".format(ctx.guild.id,flow[0]['ID']))
- await self.send_log("Feed deleted into server {} ({})".format(ctx.guild.id,flow[0]['ID']),ctx.guild)
-
- @rss_main.command(name="list")
- @commands.guild_only()
- @commands.check(commands.has_permissions(manage_webhooks=True))
- async def list_flows(self, ctx: MyContext):
- """Get a list of every rss/Minecraft feed"""
- liste = await self.db_get_guild_flows(ctx.guild.id)
- if len(liste) == 0:
- # no rss feed
- await ctx.send(await self.bot._(ctx.guild.id, "rss.no-feed2"))
- return
- title = await self.bot._(ctx.guild.id, "rss.list-title", server=ctx.guild.name)
- translation = await self.bot._(ctx.guild.id,"rss.list-result")
- l = list()
- for x in liste:
- c = self.bot.get_channel(x['channel'])
- if c is not None:
- c = c.mention
- else:
- c = x['channel']
- if len(x['roles']) == 0:
- r = await self.bot._(ctx.guild.id,"keywords.none")
- else:
- r = list()
- for item in x['roles']:
- role = discord.utils.get(ctx.guild.roles,id=int(item))
- if role is not None:
- r.append(role.mention)
- else:
- r.append(item)
- r = ", ".join(r)
- Type = await self.bot._(ctx.guild.id,'rss.'+x['type'])
- if len(l) > 20:
- embed = discord.Embed(title=title, color=self.embed_color, timestamp=ctx.message.created_at)
- embed.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar)
- for text in l:
- embed.add_field(name="\uFEFF", value=text, inline=False)
- await ctx.send(embed=embed)
- l.clear()
- l.append(translation.format(Type,c,x['link'],r,x['ID'],x['date']))
- if len(l) > 0:
- embed = discord.Embed(title=title, color=self.embed_color, timestamp=ctx.message.created_at)
- embed.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar)
- for x in l:
- embed.add_field(name="\uFEFF", value=x, inline=False)
- await ctx.send(embed=embed)
-
- async def askID(self, ID, ctx: MyContext, title:str, allow_mc: bool=False, display_mentions: bool=True):
- """Request the ID of an rss stream"""
- flow = list()
- if ID is not None:
- flow = await self.db_get_flow(ID)
- if flow == []:
- ID = None
- elif str(flow[0]['guild']) != str(ctx.guild.id):
- ID = None
- elif (not allow_mc) and flow[0]['type']=='mc':
- ID = None
- userID = ctx.author.id
- if ID is None:
- gl = await self.db_get_guild_flows(ctx.guild.id)
- if len(gl) == 0:
- await ctx.send(await self.bot._(ctx.guild.id,"rss.no-feed"))
- return
- if display_mentions:
- text = [await self.bot._(ctx.guild.id, 'rss.list')]
- else:
- text = [await self.bot._(ctx.guild.id, 'rss.list2')]
- list_of_IDs = list()
- iterator = 1
- translations = dict()
- for x in gl:
- if (not allow_mc) and x['type'] == 'mc':
- continue
- if x['type'] == 'tw' and x['link'].isnumeric():
- try:
- x['link'] = self.twitterAPI.GetUser(user_id=int(x['link'])).screen_name
- except twitter.TwitterError as e:
- pass
- list_of_IDs.append(x['ID'])
- c = self.bot.get_channel(x['channel'])
- if c is not None:
- c = c.mention
- else:
- c = x['channel']
- Type = translations.get(x['type'], await self.bot._(ctx.guild.id,'rss.'+x['type']))
- if display_mentions:
- if len(x['roles']) == 0:
- r = await self.bot._(ctx.guild.id,"keywords.none")
- else:
- r = list()
- for item in x['roles']:
- role = discord.utils.get(ctx.guild.roles,id=int(item))
- if role is not None:
- r.append(role.mention)
- else:
- r.append(item)
- r = ", ".join(r)
- text.append("{}) {} - {} - {} - {}".format(iterator, Type, x['link'], c, r))
- else:
- text.append("{}) {} - {} - {}".format(iterator, Type, x['link'], c))
- iterator += 1
- if len("\n".join(text)) < 2048:
- desc = "\n".join(text)
- fields = None
- else:
- desc = text[0].split("\n")[0]
- fields = []
- field = {'name': text[0].split("\n")[-2], 'value': ''}
- for line in text[1:]:
- if len(field['value'] + line) > 1020:
- fields.append(field)
- field = {'name': text[0].split("\n")[-2], 'value': ''}
- field['value'] += line+"\n"
- fields.append(field)
- embed = discord.Embed(title=title, color=self.embed_color, description=desc, timestamp=ctx.message.created_at)
- embed.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar)
- if fields is not None:
- for f in fields:
- embed.add_field(**f)
- emb_msg: discord.Message = await ctx.send(embed=embed)
- def check(msg):
- if not msg.content.isnumeric():
- return False
- return msg.author.id==userID and int(msg.content) in range(1,iterator)
- try:
- msg = await self.bot.wait_for('message', check = check, timeout = max(20, 1.5*len(text)))
- except asyncio.TimeoutError:
- await ctx.send(await self.bot._(ctx.guild.id, "rss.too-long"))
- await emb_msg.delete()
- return
- flow = await self.db_get_flow(list_of_IDs[int(msg.content)-1])
- if len(flow) == 0:
- await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
- return
- return flow
-
- def parse_output(self, arg):
- r = re.findall(r'((? 0:
- flatten = lambda l: [item for sublist in l for item in sublist]
- params = [[x for x in group if x != '"'] for group in r]
- return flatten(params)
- else:
- return arg.split(" ")
-
- @rss_main.command(name="roles", aliases=['mentions', 'mention'])
- @commands.guild_only()
- @commands.check(commands.has_permissions(manage_webhooks=True))
- async def roles_flows(self, ctx: MyContext, ID:int=None, mentions:commands.Greedy[discord.Role]=None):
- """Configures a role to be notified when a news is posted
- If you want to use the @everyone role, please put the server ID instead of the role name.
-
- Examples:
- - rss mentions
- - rss mentions 6678466620137
- - rss mentions 6678466620137 "Announcements" "Twitch subs"
- """
- try:
- # ask for flow ID
- flow = await self.askID(ID,
- ctx,
- await self.bot._(ctx.guild.id,"rss.choose-mentions-1"))
- except Exception as e:
- flow = []
- await self.bot.get_cog("Errors").on_error(e,ctx)
- if flow is None:
- return
- if len(flow) == 0:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- return
- flow = flow[0]
- no_role = ['aucun','none','_','del']
- if mentions is None: # if no roles was specified: we ask for them
- if flow['roles'] == '':
- text = await self.bot._(ctx.guild.id,"rss.no-roles")
- else:
- r = list()
- for item in flow['roles']:
- role = discord.utils.get(ctx.guild.roles,id=int(item))
- if role is not None:
- r.append(role.mention)
- else:
- r.append(item)
- r = ", ".join(r)
- text = str(await self.bot._(ctx.guild.id,"rss.roles-list")).format(r)
- # ask for roles
- embed = discord.Embed(title=await self.bot._(ctx.guild.id,"rss.choose-roles"), color=discord.Colour(0x77ea5c), description=text, timestamp=ctx.message.created_at)
- emb_msg = await ctx.send(embed=embed)
- err = await self.bot._(ctx.guild.id,'find.role-0')
- userID = ctx.author.id
- def check2(msg):
- return msg.author.id == userID
- cond = False
- while cond==False:
- try:
- msg = await self.bot.wait_for('message', check=check2, timeout=30.0)
- if msg.content.lower() in no_role: # if no role should be mentionned
- IDs = [None]
- else:
- l = self.parse_output(msg.content)
- IDs = list()
- Names = list()
- for x in l:
- x = x.strip()
- try:
- r = await commands.RoleConverter().convert(ctx,x)
- IDs.append(str(r.id))
- Names.append(r.name)
- except:
- await ctx.send(err)
- IDs = []
- break
- if len(IDs) > 0:
- cond = True
- except asyncio.TimeoutError:
- await ctx.send(await self.bot._(ctx.guild.id,"rss.too-long"))
- await emb_msg.delete()
- return
- else: # if roles were specified
- if mentions in no_role: # if no role should be mentionned
- IDs = None
- else:
- IDs = list()
- Names = list()
- for r in mentions:
- IDs.append(r.id)
- Names.append(r.name)
- if len(IDs) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, 'find.role-0'))
- return
- try:
- if IDs is None:
- await self.db_update_flow(flow['ID'],values=[('roles', None)])
- await ctx.send(await self.bot._(ctx.guild.id,"rss.roles-1"))
- else:
- await self.db_update_flow(flow['ID'],values=[('roles', dumps(IDs))])
- txt = ", ".join(Names)
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.roles-0")).format(txt))
- except Exception as e:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- await self.bot.get_cog("Errors").on_error(e,ctx)
- return
-
-
- @rss_main.command(name="reload")
- @commands.guild_only()
- @commands.check(commands.has_permissions(manage_webhooks=True))
- @commands.cooldown(1,600,commands.BucketType.guild)
- async def reload_guild_flows(self, ctx: MyContext):
- """Reload every rss feeds from your server"""
- try:
- t = time.time()
- msg: discord.Message = await ctx.send(str(await self.bot._(ctx.guild.id,"rss.guild-loading")).format('...'))
- liste = await self.db_get_guild_flows(ctx.guild.id)
- await self.main_loop(ctx.guild.id)
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.guild-complete")).format(len(liste),round(time.time()-t,1)))
- await msg.delete()
- except Exception as e:
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.guild-error")).format(e))
-
- @rss_main.command(name="move")
- @commands.guild_only()
- @commands.check(commands.has_permissions(manage_webhooks=True))
- async def move_guild_flow(self, ctx:MyContext, ID:typing.Optional[int]=None, channel:discord.TextChannel=None):
- """Move a rss feed in another channel
-
- Example:
- - rss move
- - rss move 3078731683662
- - rss move #cool-channels
- - rss move 3078731683662 #cool-channels
- """
- try:
- if channel is None:
- channel = ctx.channel
- try:
- flow = await self.askID(ID,
- ctx,
- await self.bot._(ctx.guild.id, "rss.choose-mentions-1"))
- e = None
- except Exception as e:
- flow = []
- if flow is None:
- return
- if len(flow) == 0:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- if e is not None:
- await self.bot.get_cog("Errors").on_error(e,ctx)
- return
- flow = flow[0]
- await self.db_update_flow(flow['ID'],[('channel',channel.id)])
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.move-success")).format(flow['ID'],channel.mention))
- except Exception as e:
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.guild-error")).format(e))
-
- @rss_main.command(name="text")
- @commands.guild_only()
- @commands.check(commands.has_permissions(manage_webhooks=True))
- async def change_text_flow(self, ctx: MyContext, ID: typing.Optional[int]=None, *, text=None):
- """Change the text of an rss feed
-
- Available variables:
- - `{author}`: the author of the post
- - `{channel}`: the channel name (usually the same as author)
- - `{date}`: the post date (UTC)
- - `{link}` or `{url}`: a link to the post
- - `{logo}`: an emoji representing the type of post (web, Twitter, YouTube...)
- - `{mentions}`: the list of mentioned roles
- - `{title}`: the title of the post
-
- Examples:
- - rss text 3078731683662
- - rss text 3078731683662 {logo} | New post of {author} right here: {url}! [{date}]
- - rss text
- """
- try:
- try:
- flow = await self.askID(ID,
- ctx,
- await self.bot._(ctx.guild.id, "rss.choose-mentions-1"))
- except Exception as e:
- flow = []
- if flow is None:
- return
- if len(flow) == 0:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- await self.bot.get_cog("Errors").on_error(e,ctx)
- return
- flow = flow[0]
- if text is None:
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.change-txt")).format_map(self.bot.SafeDict(text=flow['structure'])))
- def check(msg):
- return msg.author==ctx.author and msg.channel==ctx.channel
- try:
- msg = await self.bot.wait_for('message', check=check,timeout=90)
- except asyncio.TimeoutError:
- return await ctx.send(await self.bot._(ctx.guild.id,"rss.too-long"))
- text = msg.content
- await self.db_update_flow(flow['ID'],[('structure',text)])
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.text-success")).format(flow['ID'],text))
- except Exception as e:
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.guild-error")).format(e))
- await ctx.bot.get_cog('Errors').on_error(e,ctx)
-
- @rss_main.command(name="use_embed",aliases=['embed'])
- @commands.guild_only()
- @commands.check(commands.has_permissions(manage_webhooks=True))
- async def change_use_embed(self,ctx:MyContext,ID:typing.Optional[int]=None,value:bool=None,*,arguments:args.arguments=None):
- """Use an embed (or not) for a flow
- You can also provide arguments to change the color/text of the embed. Followed arguments are usable:
- - color: color of the embed (hex or decimal value)
- - title: title override, which will disable the default one (max 256 characters)
- - footer: small text displayed at the bottom of the embed
-
- Examples:
- - rss embed 6678466620137 true title="hey u" footer = "Hi \\n i'm a footer"
- - rss embed 6678466620137 false
- - rss embed 6678466620137 1
- """
- try:
- e = None
- try:
- flow = await self.askID(ID,
- ctx,
- await self.bot._(ctx.guild.id, "rss.choose-mentions-1"))
- except Exception as e:
- flow = []
- await self.bot.get_cog("Errors").on_error(e,ctx)
- if flow is None:
- return
- if len(flow) == 0:
- await ctx.send(await self.bot._(ctx.guild,"rss.fail-add"))
- if e is not None:
- await self.bot.get_cog("Errors").on_error(e,ctx)
- return
- if arguments is None or len(arguments.keys()) == 0:
- arguments = None
- flow = flow[0]
- embed_data = flow['embed_structure'] or dict()
- txt = list()
- if value is None and arguments is None:
- await ctx.send(await self.bot._(ctx.guild.id,"rss.use_embed_true" if flow['use_embed'] else 'use_embed_false'))
- def check(msg):
- try:
- _ = commands.core._convert_to_bool(msg.content)
- except:
- return False
- return msg.author==ctx.author and msg.channel==ctx.channel
- try:
- msg = await self.bot.wait_for('message', check=check,timeout=20)
- except asyncio.TimeoutError:
- return await ctx.send(await self.bot._(ctx.guild.id,"rss.too-long"))
- value = commands.core._convert_to_bool(msg.content)
- if value is not None and value != flow['use_embed']:
- embed_data['use_embed'] = value
- txt.append(await self.bot._(ctx.guild.id,"rss.use_embed-success",v=value,f=flow['ID']))
- elif value == flow['use_embed'] and arguments is None:
- await ctx.send(await self.bot._(ctx.guild.id,"rss.use_embed-same"))
- return
- if arguments is not None:
- if 'color' in arguments.keys():
- c = await commands.ColourConverter().convert(ctx, arguments['color'])
- if c is not None:
- embed_data['color'] = c.value
- if 'title' in arguments.keys():
- embed_data['title'] = arguments['title']
- if 'footer' in arguments.keys():
- embed_data['footer'] = arguments['footer']
- txt.append(await self.bot._(ctx.guild.id,"rss.embed-json-changed"))
- if len(embed_data) > 0:
- await self.db_update_flow(flow['ID'],[('embed_structure', dumps(embed_data))])
- await ctx.send("\n".join(txt))
- except Exception as e:
- await ctx.send(str(await self.bot._(ctx.guild.id,"rss.guild-error")).format(e))
- await ctx.bot.get_cog('Errors').on_error(e,ctx)
-
- @rss_main.command(name="test")
- @commands.check(checks.is_bot_admin)
- async def test_rss(self, ctx: MyContext, url, *, args=None):
- """Test if an rss feed is usable"""
- url = url.replace('<','').replace('>','')
- try:
- feeds = await self.feed_parse(url, 8)
- txt = "feeds.keys()\n```py\n{}\n```".format(feeds.keys())
- if 'bozo_exception' in feeds.keys():
- txt += "\nException ({}): {}".format(feeds['bozo'],str(feeds['bozo_exception']))
- return await ctx.send(txt)
- if len(str(feeds.feed))<1400-len(txt):
- txt += "feeds.feed\n```py\n{}\n```".format(feeds.feed)
- else:
- txt += "feeds.feed.keys()\n```py\n{}\n```".format(feeds.feed.keys())
- if len(feeds.entries) > 0:
- if len(str(feeds.entries[0]))<1950-len(txt):
- txt += "feeds.entries[0]\n```py\n{}\n```".format(feeds.entries[0])
- else:
- txt += "feeds.entries[0].keys()\n```py\n{}\n```".format(feeds.entries[0].keys())
- if args is not None and 'feeds' in args and 'ctx' not in args:
- txt += "\n{}\n```py\n{}\n```".format(args,eval(args))
- try:
- await ctx.send(txt)
- except Exception as e:
- print("[rss_test] Error:",e)
- await ctx.send("`Error`: "+str(e))
- print(txt)
- if args is None:
- ok = '✅'
- notok = '❌'
- nothing = '\t'
- txt = ['**__Analyse :__**','']
- yt = await self.parse_yt_url(feeds.feed['link'])
- if yt is None:
- tw = await self.parse_tw_url(feeds.feed['link'])
- if tw is not None:
- txt.append("<:twitter:437220693726330881> "+tw)
- elif 'link' in feeds.feed.keys():
- txt.append(":newspaper: <"+feeds.feed['link']+'>')
- else:
- txt.append(":newspaper: No 'link' var")
- else:
- txt.append("<:youtube:447459436982960143> "+yt)
- txt.append("Entrées : {}".format(len(feeds.entries)))
- if len(feeds.entries) > 0:
- entry = feeds.entries[0]
- if 'title' in entry.keys():
- txt.append(nothing+ok+" title: ")
- if len(entry['title'].split('\n')) > 1:
- txt[-1] += entry['title'].split('\n')[0]+"..."
- else:
- txt[-1] += entry['title']
- else:
- txt.append(nothing+notok+' title')
- if 'published_parsed' in entry.keys():
- txt.append(nothing+ok+" published_parsed")
- elif 'published' in entry.keys():
- txt.append(nothing+ok+" published")
- elif 'updated_parsed' in entry.keys():
- txt.append(nothing+ok+" updated_parsed")
- else:
- txt.append(nothing+notok+' date')
- if 'author' in entry.keys():
- txt.append(nothing+ok+" author: "+entry['author'])
- else:
- txt.append(nothing+notok+' author')
- await ctx.send("\n".join(txt))
- except Exception as e:
- await ctx.bot.get_cog('Errors').on_command_error(ctx,e)
-
- async def check_rss_url(self, url):
- r = await self.parse_yt_url(url)
- if r is not None:
- return True
- r = await self.parse_tw_url(url)
- if r is not None:
- return True
- r = await self.parse_twitch_url(url)
- if r is not None:
- return True
- r = await self.parse_deviant_url(url)
- if r is not None:
- return True
- try:
- f = await self.feed_parse(url, 8)
- _ = f.entries[0]
- return True
- except:
- return False
-
-
- async def parse_yt_url(self, url):
- r = r'(?:http.*://)?(?:www.)?(?:youtube.com|youtu.be)(?:(?:/channel/|/user/)(.+)|/[\w-]+$)'
- match = re.search(r,url)
- if match is None:
- return None
- else:
- return match.group(1)
-
- async def parse_tw_url(self, url):
- r = r'(?:http.*://)?(?:www.)?(?:twitter.com/)([^?\s]+)'
- match = re.search(r,url)
- if match is None:
- return None
- else:
- name = match.group(1)
- try:
- user = self.twitterAPI.GetUser(screen_name=name)
- except twitter.TwitterError:
- return None
- return user.id
-
- async def parse_twitch_url(self, url):
- r = r'(?:http.*://)?(?:www.)?(?:twitch.tv/)([^?\s]+)'
- match = re.search(r,url)
- if match is None:
- return None
- else:
- return match.group(1)
-
- async def parse_deviant_url(self, url):
- r = r'(?:http.*://)?(?:www.)?(?:deviantart.com/)([^?\s]+)'
- match = re.search(r,url)
- if match is None:
- return None
- else:
- return match.group(1)
-
- async def feed_parse(self, url: str, timeout: int, session: ClientSession = None) -> feedparser.FeedParserDict:
- """Asynchronous parsing using cool methods"""
- # if session is provided, we have to not close it
- _session = session or ClientSession()
- try:
- async with async_timeout.timeout(timeout) as cm:
- async with _session.get(url) as response:
- html = await response.text()
- headers = response.raw_headers
- except (client_exceptions.ClientConnectorCertificateError, UnicodeDecodeError, client_exceptions.TooManyRedirects, client_exceptions.ClientConnectorError, client_exceptions.ClientPayloadError):
- if session is None:
- await _session.close()
- return FeedParserDict(entries=[])
- except asyncio.exceptions.TimeoutError:
- if session is None:
- await _session.close()
- return None
- if session is None:
- await _session.close()
- if cm.expired:
- # request was cancelled by timeout
- self.bot.info("[RSS] feed_parse got a timeout")
- return None
- headers = {k.decode("utf-8").lower(): v.decode("utf-8") for k, v in headers}
- return feedparser.parse(html, response_headers=headers)
-
-
- async def rss_yt(self, channel: discord.TextChannel, identifiant: str, date=None, session: ClientSession=None):
- if identifiant=='help':
- return await self.bot._(channel,"rss.yt-help")
- url = 'https://www.youtube.com/feeds/videos.xml?channel_id='+identifiant
- feeds = await self.feed_parse(url, 7, session)
- if feeds is None:
- return await self.bot._(channel,"rss.research-timeout")
- if not feeds.entries:
- url = 'https://www.youtube.com/feeds/videos.xml?user='+identifiant
- feeds = await self.feed_parse(url, 7, session)
- if feeds is None:
- return await self.bot._(channel,"rss.nothing")
- if not feeds.entries:
- return await self.bot._(channel,"rss.nothing")
- if not date:
- feed = feeds.entries[0]
- img_url = None
- if 'media_thumbnail' in feed.keys() and len(feed['media_thumbnail']) > 0:
- img_url = feed['media_thumbnail'][0]['url']
- obj = self.rssMessage(bot=self.bot,Type='yt',url=feed['link'],title=feed['title'],date=feed['published_parsed'],author=feed['author'],channel=feed['author'],image=img_url)
- return [obj]
- else:
- liste = list()
- for feed in feeds.entries:
- if len(liste)>10:
- break
- if 'published_parsed' not in feed or (datetime.datetime(*feed['published_parsed'][:6]) - date).total_seconds() <= self.min_time_between_posts['yt']:
- break
- img_url = None
- if 'media_thumbnail' in feed.keys() and len(feed['media_thumbnail']) > 0:
- img_url = feed['media_thumbnail'][0]['url']
- obj = self.rssMessage(bot=self.bot,Type='yt',url=feed['link'],title=feed['title'],date=feed['published_parsed'],author=feed['author'],channel=feed['author'],image=img_url)
- liste.append(obj)
- liste.reverse()
- return liste
-
- async def rss_tw(self, channel: discord.TextChannel, name: str, date: datetime.datetime=None):
- if name == 'help':
- return await self.bot._(channel,"rss.tw-help")
- try:
- if name.isnumeric():
- posts = self.twitterAPI.GetUserTimeline(user_id=int(name), exclude_replies=True)
- username = self.twitterAPI.GetUser(user_id=int(name)).screen_name
- else:
- posts = self.twitterAPI.GetUserTimeline(screen_name=name, exclude_replies=True)
- username = name
- except twitter.error.TwitterError as e:
- if e.message == "Not authorized.":
- return await self.bot._(channel,"rss.nothing")
- if 'Unknown error' in e.message:
- return await self.bot._(channel,"rss.nothing")
- if "The twitter.Api instance must be authenticated." in e.message:
- return await self.bot._(channel,"rss.wrong-token")
- if e.message[0]['code'] == 34:
- return await self.bot._(channel,"rss.nothing")
- raise e
- if not date:
- if len(posts) == 0:
- return []
- lastpost = posts[0]
- text = html.unescape(getattr(lastpost, 'full_text', lastpost.text))
- url = "https://twitter.com/{}/status/{}".format(username.lower(), lastpost.id)
- img = None
- if lastpost.media: # if exists and is not empty
- img = lastpost.media[0].media_url_https
- obj = self.rssMessage(
- bot=self.bot,
- Type='tw',
- url=url,
- title=text,
- date=datetime.datetime.fromtimestamp(lastpost.created_at_in_seconds),
- author=lastpost.user.screen_name,
- channel=lastpost.user.name,
- image=img)
- return [obj]
- else:
- liste = list()
- for post in posts:
- if len(liste)>10:
- break
- if (datetime.datetime.fromtimestamp(post.created_at_in_seconds) - date).total_seconds() < self.min_time_between_posts['tw']:
- break
- text = html.unescape(getattr(post, 'full_text', post.text))
- if r := re.search(r"https://t.co/([^\s]+)", text):
- text = text.replace(r.group(0), '')
- url = "https://twitter.com/{}/status/{}".format(name.lower(), post.id)
- img = None
- if post.media: # if exists and is not empty
- img = post.media[0].media_url_https
- obj = self.rssMessage(
- bot=self.bot,
- Type='tw',
- url=url,
- title=text,
- date=datetime.datetime.fromtimestamp(post.created_at_in_seconds),
- author=post.user.screen_name,
- channel=post.user.name,
- image=img)
- liste.append(obj)
- liste.reverse()
- return liste
-
- async def rss_twitch(self, channel: discord.TextChannel, nom: str, date: datetime.datetime=None, session: ClientSession=None):
- url = 'https://twitchrss.appspot.com/vod/'+nom
- feeds = await self.feed_parse(url, 5, session)
- if feeds is None:
- return await self.bot._(channel,"rss.research-timeout")
- if feeds.entries==[]:
- return await self.bot._(channel,"rss.nothing")
- if not date:
- feed = feeds.entries[0]
- r = re.search(r' ',feed['summary'])
- img_url = None
- if r is not None:
- img_url = r.group(1)
- obj = self.rssMessage(bot=self.bot,Type='twitch',url=feed['link'],title=feed['title'],date=feed['published_parsed'],author=feeds.feed['title'].replace("'s Twitch video RSS",""),image=img_url,channel=nom)
- return [obj]
- else:
- liste = list()
- for feed in feeds.entries:
- if len(liste)>10:
- break
- if datetime.datetime(*feed['published_parsed'][:6]) <= date:
- break
- r = re.search(r' ',feed['summary'])
- img_url = None
- if r is not None:
- img_url = r.group(1)
- obj = self.rssMessage(bot=self.bot,Type='twitch',url=feed['link'],title=feed['title'],date=feed['published_parsed'],author=feeds.feed['title'].replace("'s Twitch video RSS",""),image=img_url,channel=nom)
- liste.append(obj)
- liste.reverse()
- return liste
-
- async def rss_web(self, channel: discord.TextChannel, url: str, date: datetime.datetime=None, session: ClientSession=None):
- if url == 'help':
- return await self.bot._(channel,"rss.web-help")
- feeds = await self.feed_parse(url, 9, session)
- if feeds is None:
- return await self.bot._(channel,"rss.research-timeout")
- if 'bozo_exception' in feeds.keys() or len(feeds.entries) == 0:
- return await self.bot._(channel,"rss.web-invalid")
- published = None
- for i in ['published_parsed','published','updated_parsed']:
- if i in feeds.entries[0].keys() and feeds.entries[0][i] is not None:
- published = i
- break
- if published is not None and len(feeds.entries) > 1:
- while (len(feeds.entries) > 1) and (feeds.entries[1][published] is not None) and (feeds.entries[0][published] < feeds.entries[1][published]):
- del feeds.entries[0]
- if not date or published not in ['published_parsed','updated_parsed']:
- feed = feeds.entries[0]
- if published is None:
- datz = 'Unknown'
- else:
- datz = feed[published]
- if 'link' in feed.keys():
- l = feed['link']
- elif 'link' in feeds.keys():
- l = feeds['link']
- else:
- l = url
- if 'author' in feed.keys():
- author = feed['author']
- elif 'author' in feeds.keys():
- author = feeds['author']
- elif 'title' in feeds['feed'].keys():
- author = feeds['feed']['title']
- else:
- author = '?'
- if 'title' in feed.keys():
- title = feed['title']
- elif 'title' in feeds.keys():
- title = feeds['title']
- else:
- title = '?'
- img = None
- r = re.search(r'(http(s?):)([/|.|\w|\s|-])*\.(?:jpe?g|gif|png|webp)', str(feed))
- if r is not None:
- img = r.group(0)
- obj = self.rssMessage(
- bot=self.bot,
- Type='web',
- url=l,
- title=title,
- date=datz,
- author=author,
- channel=feeds.feed['title'] if 'title' in feeds.feed.keys() else '?',
- image=img)
- return [obj]
- else:
- liste = list()
- for feed in feeds.entries:
- if len(liste)>10:
- break
- try:
- datz = feed[published]
- if feed[published] is None or (datetime.datetime(*feed[published][:6]) - date).total_seconds() < self.min_time_between_posts['web']:
- break
- if 'link' in feed.keys():
- l = feed['link']
- elif 'link' in feeds.keys():
- l = feeds['link']
- else:
- l = url
- if 'author' in feed.keys():
- author = feed['author']
- elif 'author' in feeds.keys():
- author = feeds['author']
- elif 'title' in feeds['feed'].keys():
- author = feeds['feed']['title']
- else:
- author = '?'
- if 'title' in feed.keys():
- title = feed['title']
- elif 'title' in feeds.keys():
- title = feeds['title']
- else:
- title = '?'
- img = None
- r = re.search(r'(http(s?):)([/|.|\w|\s|-])*\.(?:jpe?g|gif|png|webp)', str(feed))
- if r is not None:
- img = r.group(0)
- obj = self.rssMessage(
- bot=self.bot,
- Type='web',
- url=l,
- title=title,
- date=datz,
- author=author,
- channel=feeds.feed['title'] if 'title' in feeds.feed.keys() else '?',
- image=img)
- liste.append(obj)
- except:
- pass
- liste.reverse()
- return liste
-
-
- async def rss_deviant(self, guild: discord.Guild, nom: str, date: datetime.datetime=None, session: ClientSession=None):
- url = 'https://backend.deviantart.com/rss.xml?q=gallery%3A'+nom
- feeds = await self.feed_parse(url, 5, session)
- if feeds is None:
- return await self.bot._(guild,"rss.research-timeout")
- if feeds.entries==[]:
- return await self.bot._(guild,"rss.nothing")
- if not date:
- feed = feeds.entries[0]
- img_url = feed['media_content'][0]['url']
- title = re.search(r"DeviantArt: ([^ ]+)'s gallery",feeds.feed['title']).group(1)
- obj = self.rssMessage(bot=self.bot,Type='deviant',url=feed['link'],title=feed['title'],date=feed['published_parsed'],author=title,image=img_url)
- return [obj]
- else:
- liste = list()
- for feed in feeds.entries:
- if datetime.datetime(*feed['published_parsed'][:6]) <= date:
- break
- img_url = feed['media_content'][0]['url']
- title = re.search(r"DeviantArt: ([^ ]+)'s gallery",feeds.feed['title']).group(1)
- obj = self.rssMessage(bot=self.bot,Type='deviant',url=feed['link'],title=feed['title'],date=feed['published_parsed'],author=title,image=img_url)
- liste.append(obj)
- liste.reverse()
- return liste
-
- async def transform_feed(self, data: dict) -> dict:
- """Transform a feed from the database to be useful for the code
- ie blobs get their correct objects, dates become datetime objects"""
- if data['roles']:
- try:
- data['roles'] = loads(data['roles'])
- except TypeError:
- data['roles'] = None
- else:
- data['roles'] = list()
- if data['embed_structure']:
- try:
- data['embed_structure'] = loads(data['embed_structure'])
- except TypeError:
- data['embed_structure'] = None
- else:
- data['embed_structure'] = None
- if data['date']:
- data['date'] = datetime.datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
- data['added_at'] = datetime.datetime.strptime(data['added_at'], "%Y-%m-%d %H:%M:%S")
- return data
-
- async def db_get_flow(self, ID: int):
- query = f"SELECT rowid as ID, * FROM {self.table} WHERE `rowid`=?"
- liste = self.bot.db_query(query, (ID,))
- for e in range(len(liste)):
- liste[e] = await self.transform_feed(liste[e])
- return liste
-
- async def db_get_guild_flows(self, guildID: int):
- """Get every flow of a guild"""
- query = f"SELECT rowid as ID, * FROM {self.table} WHERE `guild`=?"
- liste = self.bot.db_query(query, (guildID,))
- for e in range(len(liste)):
- liste[e] = await self.transform_feed(liste[e])
- return liste
-
- async def db_add_flow(self, guildID:int, channelID:int, _type:str, link:str):
- """Add a flow in the database"""
- if _type == 'mc':
- form = ''
- else:
- form = await self.bot._(guildID, "rss."+_type+"-default-flow")
- query = "INSERT INTO `{}` (`guild`,`channel`,`type`,`link`,`structure`) VALUES (:g, :c, :t, :l, :f)".format(self.table)
- ID = self.bot.db_query(query, { 'g': guildID, 'c': channelID, 't': _type, 'l': link, 'f': form })
- return ID
-
- async def db_remove_flow(self, ID: int):
- """Remove a flow from the database"""
- if not isinstance(ID, int):
- raise ValueError
- query = f"DELETE FROM {self.table} WHERE rowid=?"
- self.bot.db_query(query, (ID,))
- return True
-
- async def db_get_all_flows(self):
- """Get every flow of the database"""
- query = "SELECT rowid as ID, * FROM `{}` WHERE `guild` in ({})".format(self.table,','.join(["'{}'".format(x.id) for x in self.bot.guilds]))
- liste = self.bot.db_query(query, ())
- for e in range(len(liste)):
- liste[e] = await self.transform_feed(liste[e])
- return liste
-
- async def db_get_count(self, get_disabled:bool=False):
- """Get the number of rss feeds"""
- query = "SELECT COUNT(*) FROM `{}`".format(self.table)
- if not get_disabled:
- query += " WHERE `guild` in (" + ','.join(["'{}'".format(x.id) for x in self.bot.guilds]) + ")"
- result = self.bot.db_query(query, (), fetchone=True)
- return result[0]
-
- async def db_update_flow(self, ID: int, values=[(None,None)]):
- """Update a flow in the database"""
- temp = ", ".join([f"{v[0]}=?" for v in values])
- values = [v[1] for v in values]
- query = f"UPDATE `{self.table}` SET {temp} WHERE rowid={ID}"
- self.bot.db_query(query, values)
-
- async def send_rss_msg(self, obj, channel: discord.TextChannel, roles: typing.List[str], send_stats):
- if channel is not None:
- t = await obj.create_msg(await self.get_lang(channel.guild))
- mentions = list()
- for item in roles:
- if item=='':
- continue
- role = discord.utils.get(channel.guild.roles,id=int(item))
- if role is not None:
- mentions.append(role)
- try:
- if isinstance(t, discord.Embed):
- await channel.send(" ".join(obj.mentions), embed=t, allowed_mentions=discord.AllowedMentions(everyone=False, roles=True))
- else:
- await channel.send(t, allowed_mentions=discord.AllowedMentions(everyone=False, roles=True))
- if send_stats:
- if statscog := self.bot.get_cog("BotStats"):
- statscog.rss_stats['messages'] += 1
- except Exception as e:
- self.bot.log.info("[send_rss_msg] Cannot send message on channel {}: {}".format(channel.id,e))
-
- async def check_flow(self, flow: dict, session: ClientSession = None, send_stats: bool=False):
- try:
- guild = self.bot.get_guild(flow['guild'])
- if flow['link'] in self.cache.keys():
- objs = self.cache[flow['link']]
- else:
- funct = getattr(self, f"rss_{flow['type']}")
- if flow["type"] == "tw":
- objs = await funct(guild,flow['link'], flow['date'])
- else:
- objs = await funct(guild,flow['link'], flow['date'], session=session)
- if isinstance(objs,twitter.error.TwitterError):
- self.twitter_over_capacity = True
- return False
- flow['link'] = objs
- if isinstance(objs,twitter.TwitterError):
- await self.bot.get_user(279568324260528128).send(f"[send_rss_msg] twitter error dans `await check_flow(): {objs}`")
- raise objs
- if isinstance(objs,(str,type(None),int)) or len(objs) == 0:
- return True
- elif type(objs) == list:
- for o in objs:
- guild = self.bot.get_guild(flow['guild'])
- if guild is None:
- self.bot.log.info("[send_rss_msg] Can not send message on server {} (unknown)".format(flow['guild']))
- return False
- chan = guild.get_channel(flow['channel'])
- if guild is None:
- self.bot.log.info("[send_rss_msg] Can not send message on channel {} (unknown)".format(flow['channel']))
- return False
- o.format = flow['structure']
- o.embed = bool(flow['use_embed'])
- if o.embed:
- o.fill_embed_data(flow)
- await o.fill_mention(guild,flow['roles'], self.bot._)
- await self.send_rss_msg(o,chan,flow['roles'], send_stats)
- await self.db_update_flow(flow['ID'], [('date',o.date)],)
- return True
- else:
- return True
- except Exception as e:
- await self.bot.get_cog('Errors').senf_err_msg("Erreur rss sur le flux {} (type {} - salon {})".format(flow['link'],flow['type'],flow['channel']))
- await self.bot.get_cog('Errors').on_error(e,None)
- return False
-
-
- async def main_loop(self, guildID: int=None):
- if not self.bot.config['rss_loop_enabled']:
- return
- t = time.time()
- if self.loop_processing:
- return
- if guildID is None:
- self.bot.log.info("Check RSS lancé")
- self.loop_processing = True
- liste = await self.db_get_all_flows()
- else:
- self.bot.log.info(f"Check RSS lancé pour le serveur {guildID}")
- liste = await self.db_get_guild_flows(guildID)
- check = 0
- errors = []
- if guildID is None:
- if statscog := self.bot.get_cog("BotStats"):
- statscog.rss_stats['messages'] = 0
- session = ClientSession()
- for flow in liste:
- try:
- if flow['type'] == 'tw' and self.twitter_over_capacity:
- continue
- if flow['type'] == 'mc':
- if MCcog := self.bot.get_cog('Minecraft'):
- await MCcog.check_flow(flow, send_stats=(guildID is None))
- check +=1
- else:
- if await self.check_flow(flow, session, send_stats=(guildID is None)):
- check += 1
- else:
- errors.append(flow['ID'])
- except Exception as e:
- await self.bot.get_cog('Errors').on_error(e,None)
- await asyncio.sleep(self.time_between_flows_check)
- await session.close()
- if MCcog := self.bot.get_cog('Minecraft'):
- MCcog.flows = dict()
- d = ["**RSS loop done** in {}s ({}/{} flows)".format(round(time.time()-t,3),check,len(liste))]
- if guildID is None:
- if statscog := self.bot.get_cog("BotStats"):
- statscog.rss_stats['checked'] = check
- statscog.rss_stats['errors'] = len(errors)
- if len(errors) > 0:
- d.append('{} errors: {}'.format(len(errors),' '.join([str(x) for x in errors])))
- emb = discord.Embed(description='\n'.join(d), color=1655066, timestamp=datetime.datetime.utcnow())
- emb.set_author(name=str(self.bot.user), icon_url=self.bot.user.display_avatar)
- # await self.bot.get_cog("Embeds").send([emb],url="loop")
- self.bot.log.debug(d[0])
- if len(errors) > 0:
- self.bot.log.warn("[Rss loop] "+d[1])
- if guildID is None:
- self.loop_processing = False
- self.twitter_over_capacity = False
- self.cache = dict()
-
- @tasks.loop(minutes=20)
- async def loop_child(self):
- self.bot.log.info(" Boucle rss commencée !")
- t1 = time.time()
- await self.bot.get_cog("Rss").main_loop()
- self.bot.log.info(" Boucle rss terminée en {}s!".format(round(time.time()-t1,2)))
-
- @loop_child.before_loop
- async def before_loop(self):
- """Wait until the bot is ready"""
- await self.bot.wait_until_ready()
-
-
- @commands.command(name="rss_loop",hidden=True)
- @commands.check(checks.is_bot_admin)
- async def rss_loop_admin(self, ctx: MyContext, new_state: str = "start"):
- """Manage the rss loop
- new_state can be start, stop or once"""
- if new_state == "start":
- try:
- await self.loop_child.start()
- except RuntimeError:
- await ctx.send("La boucle est déjà en cours !")
- else:
- await ctx.send("Boucle rss relancée !")
- elif new_state == "stop":
- await self.loop_child.cancel()
- self.bot.log.info(" Boucle rss arrêtée de force par un admin")
- await ctx.send("Boucle rss arrêtée de force !")
- elif new_state == "once":
- if self.loop_processing:
- await ctx.send("Une boucle rss est déjà en cours !")
- else:
- await ctx.send("Et hop ! Une itération de la boucle en cours !")
- self.bot.log.info(" Boucle rss forcée")
- await self.main_loop()
- else:
- await ctx.send("Option `new_start` invalide - choisissez start, stop ou once")
-
- async def send_log(self, text: str, guild: discord.Guild):
- """Send a log to the logging channel"""
- return
- # try:
- # emb = self.bot.get_cog("Embeds").Embed(desc="[RSS] "+text,color=5366650,footer_text=guild.name).update_timestamp().set_author(self.bot.user)
- # await self.bot.get_cog("Embeds").send([emb])
- # except Exception as e:
- # await self.bot.get_cog("Errors").on_error(e,None)
-
-
-async def setup(bot):
- await bot.add_cog(Rss(bot))
diff --git a/plugins/rss/config.yaml b/plugins/rss/config.yaml
new file mode 100644
index 00000000..ff25b019
--- /dev/null
+++ b/plugins/rss/config.yaml
@@ -0,0 +1,11 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
+twitter:
+ consumer_key: null
+ consumer_secret: null
+ access_token_key: null
+ access_token_secret: null
+rss_loop_enabled: true
\ No newline at end of file
diff --git a/plugins/rss/config/require-example.json b/plugins/rss/config/require-example.json
deleted file mode 100644
index 110507e8..00000000
--- a/plugins/rss/config/require-example.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "twitter": {
- "consumer_key": "",
- "consumer_secret": "",
- "access_token_key": "",
- "access_token_secret": ""
- },
- "rss_loop_enabled": true
-}
\ No newline at end of file
diff --git a/plugins/rss/credits.md b/plugins/rss/credits.md
new file mode 100644
index 00000000..d271b963
--- /dev/null
+++ b/plugins/rss/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2022 - 2023
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/rss/data/model.sql b/plugins/rss/data/model.sql
index daed4bdf..b8a5a184 100644
--- a/plugins/rss/data/model.sql
+++ b/plugins/rss/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `rss_flows` (
`guild` BIGINT NOT NULL,
`channel` BIGINT NOT NULL,
diff --git a/plugins/rss/docs/user_documentation.md b/plugins/rss/docs/user_documentation.md
index 491cc661..571b01e9 100644
--- a/plugins/rss/docs/user_documentation.md
+++ b/plugins/rss/docs/user_documentation.md
@@ -1,8 +1,15 @@
-# RSS
+
+
+# 📰 RSS
Gipsy offers you the possibility to follow different content sources, from social networks like Youtube or Twitter to simple blogs equipped with an RSS feed system.
-## **Add or remove a feed**
+## Add or remove a feed
```
rss add
@@ -18,13 +25,13 @@ rss remove
Where `` is the number of the feed you will find in the feed list.
-## **See feed list**
+## See feed list
```
rss list
```
-## **Move a feed to another room**.
+## Move a feed to another room.
```
rss move
@@ -32,7 +39,7 @@ rss move
Where `` is the number of the feed you will find in the feed list, and `` is the lounge you want to move it to.
-## **Mention a role when content appears**
+## Mention a role when content appears
```
rss roles
@@ -46,7 +53,7 @@ rss mentions [role2] [role3] ...
Will directly modify the feed to mention the filled-in roles
-## **Change the text of an rss feed**
+## Change the text of an rss feed
```
rss text
@@ -70,7 +77,7 @@ rss text
Which will run a guided script, similar to the rss roles command.
-## **Test if the feed is working properly**
+## Test if the feed is working properly
```
rss test
@@ -78,7 +85,7 @@ rss test
Where link is the link to the rss feed or the social network account.
-## **Use embeds to display content**
+## Use embeds to display content
```
rss embed
@@ -91,3 +98,4 @@ rss embed 6678466620137 true title="hey u" footer = "Hi \nI'm a footer"
```
Changes the content of the embed for the specified feed. You can also use variables, like the `rss text` command.
+
diff --git a/plugins/rss/langs/en.yml b/plugins/rss/langs/en.yml
index 1b8fddb0..6e7dbfe9 100644
--- a/plugins/rss/langs/en.yml
+++ b/plugins/rss/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
rss:
change-txt: "The current message contain \n```\n{text}\n```\nPlease enter the text to be used in a new post. You can use several variables, there is the list:\n- `{author}`: post author\n- `{channel}`: channel name (usually the same as the author)\n- `{date}`: post date (UTC)\n- `{link}` or `{url}`: a link to the post\n- `{logo}`: an emoji representing the type of post (web, Twitter, YouTube...)\n- `{mentions}`: list of mentionned roles\n- `{title}`: post title"
diff --git a/plugins/rss/langs/fr.yml b/plugins/rss/langs/fr.yml
index 645740e7..9eac1100 100644
--- a/plugins/rss/langs/fr.yml
+++ b/plugins/rss/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
rss:
change-txt: "Le message actuel contient \n```\n{text}\n```\nVeuillez entrer le texte à utiliser lors d'un nouveau post. Vous pouvez utiliser plusieurs variables, dont voici la liste :\n- `{author}` : l'auteur du post\n- `{channel}` : le nom de la chaîne (généralement le même que l'auteur)\n- `{date}` : la date du post (UTC)\n- `{link}` ou `{url}` : un lien vers le post\n- `{logo}` : un emoji représentant le type de post (web, Twitter, YouTube...)\n- `{mentions}` : la liste des rôles mentionnés\n- `{title}` : le titre du post"
diff --git a/plugins/rss/rss.py b/plugins/rss/rss.py
new file mode 100644
index 00000000..d5c4d2cc
--- /dev/null
+++ b/plugins/rss/rss.py
@@ -0,0 +1,1782 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from utils import Gunibot, MyContext
+from feedparser.util import FeedParserDict
+from discord.ext import commands, tasks
+from aiohttp.client import ClientSession
+from aiohttp import client_exceptions
+import twitter
+import feedparser
+import discord
+from bot import checks
+import async_timeout
+import args
+import asyncio
+import datetime
+import html
+import re
+import time
+import typing
+from marshal import dumps, loads
+import core
+
+async def setup(bot:Gunibot): await bot.add_cog(Rss(bot), icon="📰")
+
+class Rss(commands.Cog):
+ """Cog which deals with everything related to rss flows. Whether it is to add automatic tracking to a stream, or just to see the latest video released by Discord, it is this cog that will be used."""
+
+ def __init__(self, bot: Gunibot):
+ self.config = core.config.get("rss")
+ self.bot = bot
+ self.time_loop = 15 # min minutes between two rss loops
+ # seconds between two rss checks within a loop
+ self.time_between_flows_check = 0.15
+ self.max_feeds_per_guild = 100
+
+ self.embed_color = discord.Color(6017876)
+ self.loop_processing = False
+ self.twitterAPI = twitter.Api(**self.config["twitter"], tweet_mode="extended")
+ self.twitter_over_capacity = False
+ self.min_time_between_posts = {"web": 120, "tw": 15, "yt": 120}
+ self.cache = dict()
+ self.table = "rss_flows"
+ try:
+ self.date = bot.get_cog("TimeCog").date
+ except BaseException:
+ pass
+ # launch rss loop
+ self.loop_child.change_interval(minutes=self.time_loop)
+ self.loop_child.start()
+
+ @commands.Cog.listener()
+ async def on_ready(self):
+ self.date = self.bot.get_cog("TimeCog").date
+
+ def cog_unload(self):
+ self.loop_child.cancel()
+
+ class rssMessage:
+ def __init__(
+ self,
+ bot: Gunibot,
+ Type,
+ url,
+ title,
+ date=datetime.datetime.now(),
+ author=None,
+ Format=None,
+ channel=None,
+ image=None,
+ ):
+ self.bot = bot
+ self.Type = Type
+ self.url = url
+ self.title = title
+ self.embed = False # WARNING COOKIES WARNINNG
+ self.image = image
+ if isinstance(date, datetime.datetime):
+ self.date = date
+ elif isinstance(date, time.struct_time):
+ self.date = datetime.datetime(*date[:6])
+ elif isinstance(date, str):
+ self.date = date
+ else:
+ date = None
+ self.author = author
+ self.format = Format
+ self.logo = ":newspaper:"
+ self.channel = channel
+ self.mentions = []
+ if self.author is None:
+ self.author = channel
+
+ def fill_embed_data(self, flow: dict):
+ if not flow["use_embed"]:
+ return
+ self.embed_data = {
+ "color": discord.Colour(0).default(),
+ "footer": "",
+ "title": None,
+ }
+ if not flow["embed_structure"]:
+ return
+ structure: dict = flow["embed_structure"]
+ if title := structure.get("title", None):
+ self.embed_data["title"] = title[:256]
+ if footer := structure.get("footer", None):
+ self.embed_data["footer"] = footer[:2048]
+ if color := structure.get("color", None):
+ self.embed_data["color"] = color
+ return
+
+ async def fill_mention(
+ self, guild: discord.Guild, roles: typing.List[str], translate
+ ):
+ if roles == []:
+ r = await translate(guild.id, "keywords.none")
+ else:
+ r = list()
+ for item in roles:
+ if item == "":
+ continue
+ role = discord.utils.get(guild.roles, id=int(item))
+ if role is not None:
+ r.append(role.mention)
+ else:
+ r.append(item)
+ self.mentions = r
+ return self
+
+ async def create_msg(self, language, Format=None):
+ if Format is None:
+ Format = self.format
+ if not isinstance(self.date, str):
+ d = await self.bot.get_cog("TimeCog").date(
+ self.date, lang=language, year=False, hour=True, digital=True
+ )
+ else:
+ d = self.date
+ Format = Format.replace("\\n", "\n")
+ _channel = discord.utils.escape_markdown(self.channel)
+ _author = discord.utils.escape_markdown(self.author)
+ text = Format.format_map(
+ self.bot.SafeDict(
+ channel=_channel,
+ title=self.title,
+ date=d,
+ url=self.url,
+ link=self.url,
+ mentions=", ".join(self.mentions),
+ logo=self.logo,
+ author=_author,
+ )
+ )
+ if not self.embed:
+ return text
+ else:
+ emb = discord.Embed(
+ description=text,
+ timestamp=self.date,
+ color=self.embed_data.get("color", 0),
+ )
+ if footer := self.embed_data.get("footer", None):
+ emb.set_footer(text=footer)
+ if self.embed_data.get("title", None) is None:
+ if self.Type != "tw":
+ emb.title = self.title
+ else:
+ emb.title = self.author
+ else:
+ emb.title = self.embed_data["title"]
+ emb.add_field(name="URL", value=self.url, inline=False)
+ if self.image is not None:
+ emb.set_thumbnail(url=self.image)
+ return emb
+
+ async def get_lang(self, guild: typing.Optional[discord.Guild]) -> str:
+ guildID = guild.id if guild else None
+ return await self.bot.get_cog("Languages").get_lang(guildID, True)
+
+ @commands.group(name="rss")
+ @commands.cooldown(2, 15, commands.BucketType.channel)
+ async def rss_main(self, ctx: MyContext):
+ """See the last post of a rss feed"""
+ if ctx.subcommand_passed is None:
+ await ctx.send_help('rss')
+
+ @rss_main.command(name="youtube", aliases=["yt"])
+ async def request_yt(self, ctx: MyContext, ID):
+ """The last video of a YouTube channel
+
+ ..Examples:
+ - rss youtube UCZ5XnGb-3t7jCkXdawN2tkA
+ - rss youtube https://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA"""
+ if "youtube.com" in ID or "youtu.be" in ID:
+ ID = await self.parse_yt_url(ID)
+ if ID is None:
+ return await ctx.send(await self.bot._(ctx.channel, "rss.web-invalid"))
+ text = await self.rss_yt(ctx.channel, ID)
+ if isinstance(text, str):
+ await ctx.send(text)
+ else:
+ form = await self.bot._(ctx.channel, "rss.yt-form-last")
+ obj = await text[0].create_msg(await self.get_lang(ctx.guild), form)
+ if isinstance(obj, discord.Embed):
+ await ctx.send(embed=obj)
+ else:
+ await ctx.send(obj)
+
+ @rss_main.command(name="twitch", aliases=["tv"])
+ async def request_twitch(self, ctx: MyContext, channel):
+ """The last video of a Twitch channel
+
+ ..Examples:
+ - rss twitch aureliensama
+ - rss tv https://www.twitch.tv/aureliensama"""
+ if "twitch.tv" in channel:
+ channel = await self.parse_twitch_url(channel)
+ text = await self.rss_twitch(ctx.channel, channel)
+ if isinstance(text, str):
+ await ctx.send(text)
+ else:
+ form = await self.bot._(ctx.channel, "rss.twitch-form-last")
+ obj = await text[0].create_msg(await self.get_lang(ctx.guild), form)
+ if isinstance(obj, discord.Embed):
+ await ctx.send(embed=obj)
+ else:
+ await ctx.send(obj)
+
+ @rss_main.command(name="twitter", aliases=["tw"])
+ async def request_tw(self, ctx: MyContext, name):
+ """The last tweet of a Twitter account
+
+ ..Examples:
+ - rss twitter https://twitter.com/z_runnerr
+ - rss tw z_runnerr
+ """
+ if "twitter.com" in name:
+ name = await self.parse_tw_url(name)
+ try:
+ text = await self.rss_tw(ctx.channel, name)
+ except Exception as e:
+ return await self.bot.get_cog("Errors").on_error(e, ctx)
+ if isinstance(text, str):
+ await ctx.send(text)
+ else:
+ form = await self.bot._(ctx.channel, "rss.tw-form-last")
+ for single in text[:5]:
+ obj = await single.create_msg(await self.get_lang(ctx.guild), form)
+ if isinstance(obj, discord.Embed):
+ await ctx.send(embed=obj)
+ else:
+ await ctx.send(obj)
+
+ @rss_main.command(name="web")
+ async def request_web(self, ctx: MyContext, link):
+ """The last post on any other rss feed
+
+ Example: rss web https://fr-minecraft.net/rss.php"""
+ text = await self.rss_web(ctx.channel, link)
+ if isinstance(text, str):
+ await ctx.send(text)
+ else:
+ form = await self.bot._(ctx.channel, "rss.web-form-last")
+ obj = await text[0].create_msg(await self.get_lang(ctx.guild), form)
+ if isinstance(obj, discord.Embed):
+ await ctx.send(embed=obj)
+ else:
+ await ctx.send(obj)
+
+ @rss_main.command(name="deviantart", aliases=["deviant"])
+ async def request_deviant(self, ctx: MyContext, user):
+ """The last pictures of a DeviantArt user
+
+ Example: rss deviant https://www.deviantart.com/adri526"""
+ if "deviantart.com" in user:
+ user = await self.parse_deviant_url(user)
+ text = await self.rss_deviant(ctx.guild, user)
+ if isinstance(text, str):
+ await ctx.send(text)
+ else:
+ form = await self.bot._(ctx.channel, "rss.deviant-form-last")
+ obj = await text[0].create_msg(await self.get_lang(ctx.guild), form)
+ if isinstance(obj, discord.Embed):
+ await ctx.send(embed=obj)
+ else:
+ await ctx.send(obj)
+
+ async def is_overflow(self, guild: discord.Guild) -> bool:
+ """Check if a guild still has at least a slot
+ True if max number reached, followed by the flow limit"""
+ return len(await self.db_get_guild_flows(guild.id)) >= self.max_feeds_per_guild
+
+ @rss_main.command(name="add")
+ @commands.guild_only()
+ @commands.check(commands.has_guild_permissions(manage_webhooks=True))
+ async def system_add(self, ctx: MyContext, link):
+ """Subscribe to a rss feed, displayed on this channel regularly
+
+ ..Examples:
+ - rss add https://www.deviantart.com/adri526
+ - rss add https://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA"""
+ is_over = await self.is_overflow(ctx.guild)
+ if is_over:
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.flow-limit")).format(
+ self.max_feeds_per_guild
+ )
+ )
+ return
+ identifiant = await self.parse_yt_url(link)
+ Type = None
+ if identifiant is not None:
+ Type = "yt"
+ display_type = "youtube"
+ if identifiant is None:
+ identifiant = await self.parse_tw_url(link)
+ if identifiant is not None:
+ Type = "tw"
+ display_type = "twitter"
+ if identifiant is None:
+ identifiant = await self.parse_twitch_url(link)
+ if identifiant is not None:
+ Type = "twitch"
+ display_type = "twitch"
+ if identifiant is None:
+ identifiant = await self.parse_deviant_url(link)
+ if identifiant is not None:
+ Type = "deviant"
+ display_type = "deviantart"
+ if identifiant is not None and not link.startswith("https://"):
+ link = "https://" + link
+ if identifiant is None and link.startswith("http"):
+ identifiant = link
+ Type = "web"
+ display_type = "website"
+ elif not link.startswith("http"):
+ await ctx.send(await self.bot._(ctx.guild, "rss.invalid-link"))
+ return
+ if Type is None or not await self.check_rss_url(link):
+ return await ctx.send(await self.bot._(ctx.guild.id, "rss.invalid-flow"))
+ try:
+ ID = await self.db_add_flow(ctx.guild.id, ctx.channel.id, Type, identifiant)
+ await ctx.send(
+ str(await self.bot._(ctx.guild, "rss.success-add")).format(
+ display_type, link, ctx.channel.mention
+ )
+ )
+ self.bot.log.info(
+ "RSS feed added into server {} ({} - {})".format(ctx.guild.id, link, ID)
+ )
+ await self.send_log(
+ "Feed added into server {} ({})".format(ctx.guild.id, ID), ctx.guild
+ )
+ except Exception as e:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+
+ @rss_main.command(name="remove", aliases=["delete"])
+ @commands.guild_only()
+ @commands.check(commands.has_guild_permissions(manage_webhooks=True))
+ async def systeme_rm(self, ctx: MyContext, ID: int = None):
+ """Delete an rss feed from the list
+
+ Example: rss remove"""
+ flow = await self.askID(
+ ID,
+ ctx,
+ await self.bot._(ctx.guild.id, "rss.choose-delete"),
+ allow_mc=True,
+ display_mentions=False,
+ )
+ if flow is None:
+ return
+ try:
+ await self.db_remove_flow(flow[0]["ID"])
+ except Exception as e:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ return
+ await ctx.send(await self.bot._(ctx.guild, "rss.delete-success"))
+ self.bot.log.info(
+ "RSS feed deleted into server {} ({})".format(ctx.guild.id, flow[0]["ID"])
+ )
+ await self.send_log(
+ "Feed deleted into server {} ({})".format(ctx.guild.id, flow[0]["ID"]),
+ ctx.guild,
+ )
+
+ @rss_main.command(name="list")
+ @commands.guild_only()
+ @commands.check(commands.has_permissions(manage_webhooks=True))
+ async def list_flows(self, ctx: MyContext):
+ """Get a list of every rss/Minecraft feed"""
+ liste = await self.db_get_guild_flows(ctx.guild.id)
+ if len(liste) == 0:
+ # no rss feed
+ await ctx.send(await self.bot._(ctx.guild.id, "rss.no-feed2"))
+ return
+ title = await self.bot._(ctx.guild.id, "rss.list-title", server=ctx.guild.name)
+ translation = await self.bot._(ctx.guild.id, "rss.list-result")
+ l = list()
+ for x in liste:
+ c = self.bot.get_channel(x["channel"])
+ if c is not None:
+ c = c.mention
+ else:
+ c = x["channel"]
+ if len(x["roles"]) == 0:
+ r = await self.bot._(ctx.guild.id, "keywords.none")
+ else:
+ r = list()
+ for item in x["roles"]:
+ role = discord.utils.get(ctx.guild.roles, id=int(item))
+ if role is not None:
+ r.append(role.mention)
+ else:
+ r.append(item)
+ r = ", ".join(r)
+ Type = await self.bot._(ctx.guild.id, "rss." + x["type"])
+ if len(l) > 20:
+ embed = discord.Embed(
+ title=title,
+ color=self.embed_color,
+ timestamp=ctx.message.created_at,
+ )
+ embed.set_footer(
+ text=str(ctx.author), icon_url=ctx.author.display_avatar
+ )
+ for text in l:
+ embed.add_field(name="\uFEFF", value=text, inline=False)
+ await ctx.send(embed=embed)
+ l.clear()
+ l.append(translation.format(Type, c, x["link"], r, x["ID"], x["date"]))
+ if len(l) > 0:
+ embed = discord.Embed(
+ title=title, color=self.embed_color, timestamp=ctx.message.created_at
+ )
+ embed.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar)
+ for x in l:
+ embed.add_field(name="\uFEFF", value=x, inline=False)
+ await ctx.send(embed=embed)
+
+ async def askID(
+ self,
+ ID,
+ ctx: MyContext,
+ title: str,
+ allow_mc: bool = False,
+ display_mentions: bool = True,
+ ):
+ """Request the ID of an rss stream"""
+ flow = list()
+ if ID is not None:
+ flow = await self.db_get_flow(ID)
+ if flow == []:
+ ID = None
+ elif str(flow[0]["guild"]) != str(ctx.guild.id):
+ ID = None
+ elif (not allow_mc) and flow[0]["type"] == "mc":
+ ID = None
+ userID = ctx.author.id
+ if ID is None:
+ gl = await self.db_get_guild_flows(ctx.guild.id)
+ if len(gl) == 0:
+ await ctx.send(await self.bot._(ctx.guild.id, "rss.no-feed"))
+ return
+ if display_mentions:
+ text = [await self.bot._(ctx.guild.id, "rss.list")]
+ else:
+ text = [await self.bot._(ctx.guild.id, "rss.list2")]
+ list_of_IDs = list()
+ iterator = 1
+ translations = dict()
+ for x in gl:
+ if (not allow_mc) and x["type"] == "mc":
+ continue
+ if x["type"] == "tw" and x["link"].isnumeric():
+ try:
+ x["link"] = self.twitterAPI.GetUser(
+ user_id=int(x["link"])
+ ).screen_name
+ except twitter.TwitterError as e:
+ pass
+ list_of_IDs.append(x["ID"])
+ c = self.bot.get_channel(x["channel"])
+ if c is not None:
+ c = c.mention
+ else:
+ c = x["channel"]
+ Type = translations.get(
+ x["type"], await self.bot._(ctx.guild.id, "rss." + x["type"])
+ )
+ if display_mentions:
+ if len(x["roles"]) == 0:
+ r = await self.bot._(ctx.guild.id, "keywords.none")
+ else:
+ r = list()
+ for item in x["roles"]:
+ role = discord.utils.get(ctx.guild.roles, id=int(item))
+ if role is not None:
+ r.append(role.mention)
+ else:
+ r.append(item)
+ r = ", ".join(r)
+ text.append(
+ "{}) {} - {} - {} - {}".format(iterator, Type, x["link"], c, r)
+ )
+ else:
+ text.append("{}) {} - {} - {}".format(iterator, Type, x["link"], c))
+ iterator += 1
+ if len("\n".join(text)) < 2048:
+ desc = "\n".join(text)
+ fields = None
+ else:
+ desc = text[0].split("\n")[0]
+ fields = []
+ field = {"name": text[0].split("\n")[-2], "value": ""}
+ for line in text[1:]:
+ if len(field["value"] + line) > 1020:
+ fields.append(field)
+ field = {"name": text[0].split("\n")[-2], "value": ""}
+ field["value"] += line + "\n"
+ fields.append(field)
+ embed = discord.Embed(
+ title=title,
+ color=self.embed_color,
+ description=desc,
+ timestamp=ctx.message.created_at,
+ )
+ embed.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar)
+ if fields is not None:
+ for f in fields:
+ embed.add_field(**f)
+ emb_msg: discord.Message = await ctx.send(embed=embed)
+
+ def check(msg):
+ if not msg.content.isnumeric():
+ return False
+ return msg.author.id == userID and int(msg.content) in range(
+ 1, iterator
+ )
+
+ try:
+ msg = await self.bot.wait_for(
+ "message", check=check, timeout=max(20, 1.5 * len(text))
+ )
+ except asyncio.TimeoutError:
+ await ctx.send(await self.bot._(ctx.guild.id, "rss.too-long"))
+ await emb_msg.delete()
+ return
+ flow = await self.db_get_flow(list_of_IDs[int(msg.content) - 1])
+ if len(flow) == 0:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ return
+ return flow
+
+ def parse_output(self, arg):
+ r = re.findall(r"((? 0:
+
+ def flatten(l):
+ return [item for sublist in l for item in sublist]
+
+ params = [[x for x in group if x != '"'] for group in r]
+ return flatten(params)
+ else:
+ return arg.split(" ")
+
+ @rss_main.command(name="roles", aliases=["mentions", "mention"])
+ @commands.guild_only()
+ @commands.check(commands.has_permissions(manage_webhooks=True))
+ async def roles_flows(
+ self,
+ ctx: MyContext,
+ ID: int = None,
+ mentions: commands.Greedy[discord.Role] = None,
+ ):
+ """Configures a role to be notified when a news is posted
+ If you want to use the @everyone role, please put the server ID instead of the role name.
+
+ Examples:
+ - rss mentions
+ - rss mentions 6678466620137
+ - rss mentions 6678466620137 "Announcements" "Twitch subs"
+ """
+ try:
+ # ask for flow ID
+ flow = await self.askID(
+ ID, ctx, await self.bot._(ctx.guild.id, "rss.choose-mentions-1")
+ )
+ except Exception as e:
+ flow = []
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ if flow is None:
+ return
+ if len(flow) == 0:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ return
+ flow = flow[0]
+ no_role = ["aucun", "none", "_", "del"]
+ if mentions is None: # if no roles was specified: we ask for them
+ if flow["roles"] == "":
+ text = await self.bot._(ctx.guild.id, "rss.no-roles")
+ else:
+ r = list()
+ for item in flow["roles"]:
+ role = discord.utils.get(ctx.guild.roles, id=int(item))
+ if role is not None:
+ r.append(role.mention)
+ else:
+ r.append(item)
+ r = ", ".join(r)
+ text = str(await self.bot._(ctx.guild.id, "rss.roles-list")).format(r)
+ # ask for roles
+ embed = discord.Embed(
+ title=await self.bot._(ctx.guild.id, "rss.choose-roles"),
+ color=discord.Colour(0x77EA5C),
+ description=text,
+ timestamp=ctx.message.created_at,
+ )
+ emb_msg = await ctx.send(embed=embed)
+ err = await self.bot._(ctx.guild.id, "find.role-0")
+ userID = ctx.author.id
+
+ def check2(msg):
+ return msg.author.id == userID
+
+ cond = False
+ while cond == False:
+ try:
+ msg = await self.bot.wait_for("message", check=check2, timeout=30.0)
+ if (
+ msg.content.lower() in no_role
+ ): # if no role should be mentionned
+ IDs = [None]
+ else:
+ l = self.parse_output(msg.content)
+ IDs = list()
+ Names = list()
+ for x in l:
+ x = x.strip()
+ try:
+ r = await commands.RoleConverter().convert(ctx, x)
+ IDs.append(str(r.id))
+ Names.append(r.name)
+ except BaseException:
+ await ctx.send(err)
+ IDs = []
+ break
+ if len(IDs) > 0:
+ cond = True
+ except asyncio.TimeoutError:
+ await ctx.send(await self.bot._(ctx.guild.id, "rss.too-long"))
+ await emb_msg.delete()
+ return
+ else: # if roles were specified
+ if mentions in no_role: # if no role should be mentionned
+ IDs = None
+ else:
+ IDs = list()
+ Names = list()
+ for r in mentions:
+ IDs.append(r.id)
+ Names.append(r.name)
+ if len(IDs) == 0:
+ await ctx.send(await self.bot._(ctx.guild.id, "find.role-0"))
+ return
+ try:
+ if IDs is None:
+ await self.db_update_flow(flow["ID"], values=[("roles", None)])
+ await ctx.send(await self.bot._(ctx.guild.id, "rss.roles-1"))
+ else:
+ await self.db_update_flow(flow["ID"], values=[("roles", dumps(IDs))])
+ txt = ", ".join(Names)
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.roles-0")).format(txt)
+ )
+ except Exception as e:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ return
+
+ @rss_main.command(name="reload")
+ @commands.guild_only()
+ @commands.check(commands.has_permissions(manage_webhooks=True))
+ @commands.cooldown(1, 600, commands.BucketType.guild)
+ async def reload_guild_flows(self, ctx: MyContext):
+ """Reload every rss feeds from your server"""
+ try:
+ t = time.time()
+ msg: discord.Message = await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.guild-loading")).format("...")
+ )
+ liste = await self.db_get_guild_flows(ctx.guild.id)
+ await self.main_loop(ctx.guild.id)
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.guild-complete")).format(
+ len(liste), round(time.time() - t, 1)
+ )
+ )
+ await msg.delete()
+ except Exception as e:
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.guild-error")).format(e)
+ )
+
+ @rss_main.command(name="move")
+ @commands.guild_only()
+ @commands.check(commands.has_permissions(manage_webhooks=True))
+ async def move_guild_flow(
+ self,
+ ctx: MyContext,
+ ID: typing.Optional[int] = None,
+ channel: discord.TextChannel = None,
+ ):
+ """Move a rss feed in another channel
+
+ Example:
+ - rss move
+ - rss move 3078731683662
+ - rss move #cool-channels
+ - rss move 3078731683662 #cool-channels
+ """
+ try:
+ if channel is None:
+ channel = ctx.channel
+ try:
+ flow = await self.askID(
+ ID, ctx, await self.bot._(ctx.guild.id, "rss.choose-mentions-1")
+ )
+ e = None
+ except Exception as e:
+ flow = []
+ if flow is None:
+ return
+ if len(flow) == 0:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ if e is not None:
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ return
+ flow = flow[0]
+ await self.db_update_flow(flow["ID"], [("channel", channel.id)])
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.move-success")).format(
+ flow["ID"], channel.mention
+ )
+ )
+ except Exception as e:
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.guild-error")).format(e)
+ )
+
+ @rss_main.command(name="text")
+ @commands.guild_only()
+ @commands.check(commands.has_permissions(manage_webhooks=True))
+ async def change_text_flow(
+ self, ctx: MyContext, ID: typing.Optional[int] = None, *, text=None
+ ):
+ """Change the text of an rss feed
+
+ Available variables:
+ - `{author}`: the author of the post
+ - `{channel}`: the channel name (usually the same as author)
+ - `{date}`: the post date (UTC)
+ - `{link}` or `{url}`: a link to the post
+ - `{logo}`: an emoji representing the type of post (web, Twitter, YouTube...)
+ - `{mentions}`: the list of mentioned roles
+ - `{title}`: the title of the post
+
+ Examples:
+ - rss text 3078731683662
+ - rss text 3078731683662 {logo} | New post of {author} right here: {url}! [{date}]
+ - rss text
+ """
+ try:
+ try:
+ flow = await self.askID(
+ ID, ctx, await self.bot._(ctx.guild.id, "rss.choose-mentions-1")
+ )
+ except Exception as e:
+ flow = []
+ if flow is None:
+ return
+ if len(flow) == 0:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ return
+ flow = flow[0]
+ if text is None:
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.change-txt")).format_map(
+ self.bot.SafeDict(text=flow["structure"])
+ )
+ )
+
+ def check(msg):
+ return msg.author == ctx.author and msg.channel == ctx.channel
+
+ try:
+ msg = await self.bot.wait_for("message", check=check, timeout=90)
+ except asyncio.TimeoutError:
+ return await ctx.send(
+ await self.bot._(ctx.guild.id, "rss.too-long")
+ )
+ text = msg.content
+ await self.db_update_flow(flow["ID"], [("structure", text)])
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.text-success")).format(
+ flow["ID"], text
+ )
+ )
+ except Exception as e:
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.guild-error")).format(e)
+ )
+ await ctx.bot.get_cog("Errors").on_error(e, ctx)
+
+ @rss_main.command(name="use_embed", aliases=["embed"])
+ @commands.guild_only()
+ @commands.check(commands.has_permissions(manage_webhooks=True))
+ async def change_use_embed(
+ self,
+ ctx: MyContext,
+ ID: typing.Optional[int] = None,
+ value: bool = None,
+ *,
+ arguments: args.arguments = None,
+ ):
+ """Use an embed (or not) for a flow
+ You can also provide arguments to change the color/text of the embed. Followed arguments are usable:
+ - color: color of the embed (hex or decimal value)
+ - title: title override, which will disable the default one (max 256 characters)
+ - footer: small text displayed at the bottom of the embed
+
+ Examples:
+ - rss embed 6678466620137 true title="hey u" footer = "Hi \\n i'm a footer"
+ - rss embed 6678466620137 false
+ - rss embed 6678466620137 1
+ """
+ try:
+ e = None
+ try:
+ flow = await self.askID(
+ ID, ctx, await self.bot._(ctx.guild.id, "rss.choose-mentions-1")
+ )
+ except Exception as e:
+ flow = []
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ if flow is None:
+ return
+ if len(flow) == 0:
+ await ctx.send(await self.bot._(ctx.guild, "rss.fail-add"))
+ if e is not None:
+ await self.bot.get_cog("Errors").on_error(e, ctx)
+ return
+ if arguments is None or len(arguments.keys()) == 0:
+ arguments = None
+ flow = flow[0]
+ embed_data = flow["embed_structure"] or dict()
+ txt = list()
+ if value is None and arguments is None:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "rss.use_embed_true"
+ if flow["use_embed"]
+ else "use_embed_false",
+ )
+ )
+
+ def check(msg):
+ try:
+ _ = commands.core._convert_to_bool(msg.content)
+ except BaseException:
+ return False
+ return msg.author == ctx.author and msg.channel == ctx.channel
+
+ try:
+ msg = await self.bot.wait_for("message", check=check, timeout=20)
+ except asyncio.TimeoutError:
+ return await ctx.send(
+ await self.bot._(ctx.guild.id, "rss.too-long")
+ )
+ value = commands.core._convert_to_bool(msg.content)
+ if value is not None and value != flow["use_embed"]:
+ embed_data["use_embed"] = value
+ txt.append(
+ await self.bot._(
+ ctx.guild.id, "rss.use_embed-success", v=value, f=flow["ID"]
+ )
+ )
+ elif value == flow["use_embed"] and arguments is None:
+ await ctx.send(await self.bot._(ctx.guild.id, "rss.use_embed-same"))
+ return
+ if arguments is not None:
+ if "color" in arguments.keys():
+ c = await commands.ColourConverter().convert(
+ ctx, arguments["color"]
+ )
+ if c is not None:
+ embed_data["color"] = c.value
+ if "title" in arguments.keys():
+ embed_data["title"] = arguments["title"]
+ if "footer" in arguments.keys():
+ embed_data["footer"] = arguments["footer"]
+ txt.append(await self.bot._(ctx.guild.id, "rss.embed-json-changed"))
+ if len(embed_data) > 0:
+ await self.db_update_flow(
+ flow["ID"], [("embed_structure", dumps(embed_data))]
+ )
+ await ctx.send("\n".join(txt))
+ except Exception as e:
+ await ctx.send(
+ str(await self.bot._(ctx.guild.id, "rss.guild-error")).format(e)
+ )
+ await ctx.bot.get_cog("Errors").on_error(e, ctx)
+
+ @rss_main.command(name="test")
+ @commands.check(checks.is_bot_admin)
+ async def test_rss(self, ctx: MyContext, url, *, args=None):
+ """Test if an rss feed is usable"""
+ url = url.replace("<", "").replace(">", "")
+ try:
+ feeds = await self.feed_parse(url, 8)
+ txt = "feeds.keys()\n```py\n{}\n```".format(feeds.keys())
+ if "bozo_exception" in feeds.keys():
+ txt += "\nException ({}): {}".format(
+ feeds["bozo"], str(feeds["bozo_exception"])
+ )
+ return await ctx.send(txt)
+ if len(str(feeds.feed)) < 1400 - len(txt):
+ txt += "feeds.feed\n```py\n{}\n```".format(feeds.feed)
+ else:
+ txt += "feeds.feed.keys()\n```py\n{}\n```".format(feeds.feed.keys())
+ if len(feeds.entries) > 0:
+ if len(str(feeds.entries[0])) < 1950 - len(txt):
+ txt += "feeds.entries[0]\n```py\n{}\n```".format(feeds.entries[0])
+ else:
+ txt += "feeds.entries[0].keys()\n```py\n{}\n```".format(
+ feeds.entries[0].keys()
+ )
+ if args is not None and "feeds" in args and "ctx" not in args:
+ txt += "\n{}\n```py\n{}\n```".format(args, eval(args))
+ try:
+ await ctx.send(txt)
+ except Exception as e:
+ print("[rss_test] Error:", e)
+ await ctx.send("`Error`: " + str(e))
+ print(txt)
+ if args is None:
+ ok = "✅"
+ notok = "❌"
+ nothing = "\t"
+ txt = ["**__Analyse :__**", ""]
+ yt = await self.parse_yt_url(feeds.feed["link"])
+ if yt is None:
+ tw = await self.parse_tw_url(feeds.feed["link"])
+ if tw is not None:
+ txt.append("<:twitter:437220693726330881> " + tw)
+ elif "link" in feeds.feed.keys():
+ txt.append(":newspaper: <" + feeds.feed["link"] + ">")
+ else:
+ txt.append(":newspaper: No 'link' var")
+ else:
+ txt.append("<:youtube:447459436982960143> " + yt)
+ txt.append("Entrées : {}".format(len(feeds.entries)))
+ if len(feeds.entries) > 0:
+ entry = feeds.entries[0]
+ if "title" in entry.keys():
+ txt.append(nothing + ok + " title: ")
+ if len(entry["title"].split("\n")) > 1:
+ txt[-1] += entry["title"].split("\n")[0] + "..."
+ else:
+ txt[-1] += entry["title"]
+ else:
+ txt.append(nothing + notok + " title")
+ if "published_parsed" in entry.keys():
+ txt.append(nothing + ok + " published_parsed")
+ elif "published" in entry.keys():
+ txt.append(nothing + ok + " published")
+ elif "updated_parsed" in entry.keys():
+ txt.append(nothing + ok + " updated_parsed")
+ else:
+ txt.append(nothing + notok + " date")
+ if "author" in entry.keys():
+ txt.append(nothing + ok + " author: " + entry["author"])
+ else:
+ txt.append(nothing + notok + " author")
+ await ctx.send("\n".join(txt))
+ except Exception as e:
+ await ctx.bot.get_cog("Errors").on_command_error(ctx, e)
+
+ async def check_rss_url(self, url):
+ r = await self.parse_yt_url(url)
+ if r is not None:
+ return True
+ r = await self.parse_tw_url(url)
+ if r is not None:
+ return True
+ r = await self.parse_twitch_url(url)
+ if r is not None:
+ return True
+ r = await self.parse_deviant_url(url)
+ if r is not None:
+ return True
+ try:
+ f = await self.feed_parse(url, 8)
+ _ = f.entries[0]
+ return True
+ except BaseException:
+ return False
+
+ async def parse_yt_url(self, url):
+ r = r"(?:http.*://)?(?:www.)?(?:youtube.com|youtu.be)(?:(?:/channel/|/user/)(.+)|/[\w-]+$)"
+ match = re.search(r, url)
+ if match is None:
+ return None
+ else:
+ return match.group(1)
+
+ async def parse_tw_url(self, url):
+ r = r"(?:http.*://)?(?:www.)?(?:twitter.com/)([^?\s]+)"
+ match = re.search(r, url)
+ if match is None:
+ return None
+ else:
+ name = match.group(1)
+ try:
+ user = self.twitterAPI.GetUser(screen_name=name)
+ except twitter.TwitterError:
+ return None
+ return user.id
+
+ async def parse_twitch_url(self, url):
+ r = r"(?:http.*://)?(?:www.)?(?:twitch.tv/)([^?\s]+)"
+ match = re.search(r, url)
+ if match is None:
+ return None
+ else:
+ return match.group(1)
+
+ async def parse_deviant_url(self, url):
+ r = r"(?:http.*://)?(?:www.)?(?:deviantart.com/)([^?\s]+)"
+ match = re.search(r, url)
+ if match is None:
+ return None
+ else:
+ return match.group(1)
+
+ async def feed_parse(
+ self, url: str, timeout: int, session: ClientSession = None
+ ) -> feedparser.FeedParserDict:
+ """Asynchronous parsing using cool methods"""
+ # if session is provided, we have to not close it
+ _session = session or ClientSession()
+ try:
+ async with async_timeout.timeout(timeout) as cm:
+ async with _session.get(url) as response:
+ html = await response.text()
+ headers = response.raw_headers
+ except (
+ client_exceptions.ClientConnectorCertificateError,
+ UnicodeDecodeError,
+ client_exceptions.TooManyRedirects,
+ client_exceptions.ClientConnectorError,
+ client_exceptions.ClientPayloadError,
+ ):
+ if session is None:
+ await _session.close()
+ return FeedParserDict(entries=[])
+ except asyncio.exceptions.TimeoutError:
+ if session is None:
+ await _session.close()
+ return None
+ if session is None:
+ await _session.close()
+ if cm.expired:
+ # request was cancelled by timeout
+ self.bot.info("[RSS] feed_parse got a timeout")
+ return None
+ headers = {k.decode("utf-8").lower(): v.decode("utf-8") for k, v in headers}
+ return feedparser.parse(html, response_headers=headers)
+
+ async def rss_yt(
+ self,
+ channel: discord.TextChannel,
+ identifiant: str,
+ date=None,
+ session: ClientSession = None,
+ ):
+ if identifiant == "help":
+ return await self.bot._(channel, "rss.yt-help")
+ url = "https://www.youtube.com/feeds/videos.xml?channel_id=" + identifiant
+ feeds = await self.feed_parse(url, 7, session)
+ if feeds is None:
+ return await self.bot._(channel, "rss.research-timeout")
+ if not feeds.entries:
+ url = "https://www.youtube.com/feeds/videos.xml?user=" + identifiant
+ feeds = await self.feed_parse(url, 7, session)
+ if feeds is None:
+ return await self.bot._(channel, "rss.nothing")
+ if not feeds.entries:
+ return await self.bot._(channel, "rss.nothing")
+ if not date:
+ feed = feeds.entries[0]
+ img_url = None
+ if "media_thumbnail" in feed.keys() and len(feed["media_thumbnail"]) > 0:
+ img_url = feed["media_thumbnail"][0]["url"]
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="yt",
+ url=feed["link"],
+ title=feed["title"],
+ date=feed["published_parsed"],
+ author=feed["author"],
+ channel=feed["author"],
+ image=img_url,
+ )
+ return [obj]
+ else:
+ liste = list()
+ for feed in feeds.entries:
+ if len(liste) > 10:
+ break
+ if (
+ "published_parsed" not in feed
+ or (
+ datetime.datetime(*feed["published_parsed"][:6]) - date
+ ).total_seconds()
+ <= self.min_time_between_posts["yt"]
+ ):
+ break
+ img_url = None
+ if (
+ "media_thumbnail" in feed.keys()
+ and len(feed["media_thumbnail"]) > 0
+ ):
+ img_url = feed["media_thumbnail"][0]["url"]
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="yt",
+ url=feed["link"],
+ title=feed["title"],
+ date=feed["published_parsed"],
+ author=feed["author"],
+ channel=feed["author"],
+ image=img_url,
+ )
+ liste.append(obj)
+ liste.reverse()
+ return liste
+
+ async def rss_tw(
+ self, channel: discord.TextChannel, name: str, date: datetime.datetime = None
+ ):
+ if name == "help":
+ return await self.bot._(channel, "rss.tw-help")
+ try:
+ if name.isnumeric():
+ posts = self.twitterAPI.GetUserTimeline(
+ user_id=int(name), exclude_replies=True
+ )
+ username = self.twitterAPI.GetUser(user_id=int(name)).screen_name
+ else:
+ posts = self.twitterAPI.GetUserTimeline(
+ screen_name=name, exclude_replies=True
+ )
+ username = name
+ except twitter.error.TwitterError as e:
+ if e.message == "Not authorized.":
+ return await self.bot._(channel, "rss.nothing")
+ if "Unknown error" in e.message:
+ return await self.bot._(channel, "rss.nothing")
+ if "The twitter.Api instance must be authenticated." in e.message:
+ return await self.bot._(channel, "rss.wrong-token")
+ if e.message[0]["code"] == 34:
+ return await self.bot._(channel, "rss.nothing")
+ raise e
+ if not date:
+ if len(posts) == 0:
+ return []
+ lastpost = posts[0]
+ text = html.unescape(getattr(lastpost, "full_text", lastpost.text))
+ url = "https://twitter.com/{}/status/{}".format(
+ username.lower(), lastpost.id
+ )
+ img = None
+ if lastpost.media: # if exists and is not empty
+ img = lastpost.media[0].media_url_https
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="tw",
+ url=url,
+ title=text,
+ date=datetime.datetime.fromtimestamp(lastpost.created_at_in_seconds),
+ author=lastpost.user.screen_name,
+ channel=lastpost.user.name,
+ image=img,
+ )
+ return [obj]
+ else:
+ liste = list()
+ for post in posts:
+ if len(liste) > 10:
+ break
+ if (
+ datetime.datetime.fromtimestamp(post.created_at_in_seconds) - date
+ ).total_seconds() < self.min_time_between_posts["tw"]:
+ break
+ text = html.unescape(getattr(post, "full_text", post.text))
+ if r := re.search(r"https://t.co/([^\s]+)", text):
+ text = text.replace(r.group(0), "")
+ url = "https://twitter.com/{}/status/{}".format(name.lower(), post.id)
+ img = None
+ if post.media: # if exists and is not empty
+ img = post.media[0].media_url_https
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="tw",
+ url=url,
+ title=text,
+ date=datetime.datetime.fromtimestamp(post.created_at_in_seconds),
+ author=post.user.screen_name,
+ channel=post.user.name,
+ image=img,
+ )
+ liste.append(obj)
+ liste.reverse()
+ return liste
+
+ async def rss_twitch(
+ self,
+ channel: discord.TextChannel,
+ nom: str,
+ date: datetime.datetime = None,
+ session: ClientSession = None,
+ ):
+ url = "https://twitchrss.appspot.com/vod/" + nom
+ feeds = await self.feed_parse(url, 5, session)
+ if feeds is None:
+ return await self.bot._(channel, "rss.research-timeout")
+ if feeds.entries == []:
+ return await self.bot._(channel, "rss.nothing")
+ if not date:
+ feed = feeds.entries[0]
+ r = re.search(r' ', feed["summary"])
+ img_url = None
+ if r is not None:
+ img_url = r.group(1)
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="twitch",
+ url=feed["link"],
+ title=feed["title"],
+ date=feed["published_parsed"],
+ author=feeds.feed["title"].replace("'s Twitch video RSS", ""),
+ image=img_url,
+ channel=nom,
+ )
+ return [obj]
+ else:
+ liste = list()
+ for feed in feeds.entries:
+ if len(liste) > 10:
+ break
+ if datetime.datetime(*feed["published_parsed"][:6]) <= date:
+ break
+ r = re.search(r' ', feed["summary"])
+ img_url = None
+ if r is not None:
+ img_url = r.group(1)
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="twitch",
+ url=feed["link"],
+ title=feed["title"],
+ date=feed["published_parsed"],
+ author=feeds.feed["title"].replace("'s Twitch video RSS", ""),
+ image=img_url,
+ channel=nom,
+ )
+ liste.append(obj)
+ liste.reverse()
+ return liste
+
+ async def rss_web(
+ self,
+ channel: discord.TextChannel,
+ url: str,
+ date: datetime.datetime = None,
+ session: ClientSession = None,
+ ):
+ if url == "help":
+ return await self.bot._(channel, "rss.web-help")
+ feeds = await self.feed_parse(url, 9, session)
+ if feeds is None:
+ return await self.bot._(channel, "rss.research-timeout")
+ if "bozo_exception" in feeds.keys() or len(feeds.entries) == 0:
+ return await self.bot._(channel, "rss.web-invalid")
+ published = None
+ for i in ["published_parsed", "published", "updated_parsed"]:
+ if i in feeds.entries[0].keys() and feeds.entries[0][i] is not None:
+ published = i
+ break
+ if published is not None and len(feeds.entries) > 1:
+ while (
+ (len(feeds.entries) > 1)
+ and (feeds.entries[1][published] is not None)
+ and (feeds.entries[0][published] < feeds.entries[1][published])
+ ):
+ del feeds.entries[0]
+ if not date or published not in ["published_parsed", "updated_parsed"]:
+ feed = feeds.entries[0]
+ if published is None:
+ datz = "Unknown"
+ else:
+ datz = feed[published]
+ if "link" in feed.keys():
+ l = feed["link"]
+ elif "link" in feeds.keys():
+ l = feeds["link"]
+ else:
+ l = url
+ if "author" in feed.keys():
+ author = feed["author"]
+ elif "author" in feeds.keys():
+ author = feeds["author"]
+ elif "title" in feeds["feed"].keys():
+ author = feeds["feed"]["title"]
+ else:
+ author = "?"
+ if "title" in feed.keys():
+ title = feed["title"]
+ elif "title" in feeds.keys():
+ title = feeds["title"]
+ else:
+ title = "?"
+ img = None
+ r = re.search(
+ r"(http(s?):)([/|.|\w|\s|-])*\.(?:jpe?g|gif|png|webp)", str(feed)
+ )
+ if r is not None:
+ img = r.group(0)
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="web",
+ url=l,
+ title=title,
+ date=datz,
+ author=author,
+ channel=feeds.feed["title"] if "title" in feeds.feed.keys() else "?",
+ image=img,
+ )
+ return [obj]
+ else:
+ liste = list()
+ for feed in feeds.entries:
+ if len(liste) > 10:
+ break
+ try:
+ datz = feed[published]
+ if (
+ feed[published] is None
+ or (
+ datetime.datetime(*feed[published][:6]) - date
+ ).total_seconds()
+ < self.min_time_between_posts["web"]
+ ):
+ break
+ if "link" in feed.keys():
+ l = feed["link"]
+ elif "link" in feeds.keys():
+ l = feeds["link"]
+ else:
+ l = url
+ if "author" in feed.keys():
+ author = feed["author"]
+ elif "author" in feeds.keys():
+ author = feeds["author"]
+ elif "title" in feeds["feed"].keys():
+ author = feeds["feed"]["title"]
+ else:
+ author = "?"
+ if "title" in feed.keys():
+ title = feed["title"]
+ elif "title" in feeds.keys():
+ title = feeds["title"]
+ else:
+ title = "?"
+ img = None
+ r = re.search(
+ r"(http(s?):)([/|.|\w|\s|-])*\.(?:jpe?g|gif|png|webp)",
+ str(feed),
+ )
+ if r is not None:
+ img = r.group(0)
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="web",
+ url=l,
+ title=title,
+ date=datz,
+ author=author,
+ channel=feeds.feed["title"]
+ if "title" in feeds.feed.keys()
+ else "?",
+ image=img,
+ )
+ liste.append(obj)
+ except BaseException:
+ pass
+ liste.reverse()
+ return liste
+
+ async def rss_deviant(
+ self,
+ guild: discord.Guild,
+ nom: str,
+ date: datetime.datetime = None,
+ session: ClientSession = None,
+ ):
+ url = "https://backend.deviantart.com/rss.xml?q=gallery%3A" + nom
+ feeds = await self.feed_parse(url, 5, session)
+ if feeds is None:
+ return await self.bot._(guild, "rss.research-timeout")
+ if feeds.entries == []:
+ return await self.bot._(guild, "rss.nothing")
+ if not date:
+ feed = feeds.entries[0]
+ img_url = feed["media_content"][0]["url"]
+ title = re.search(
+ r"DeviantArt: ([^ ]+)'s gallery", feeds.feed["title"]
+ ).group(1)
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="deviant",
+ url=feed["link"],
+ title=feed["title"],
+ date=feed["published_parsed"],
+ author=title,
+ image=img_url,
+ )
+ return [obj]
+ else:
+ liste = list()
+ for feed in feeds.entries:
+ if datetime.datetime(*feed["published_parsed"][:6]) <= date:
+ break
+ img_url = feed["media_content"][0]["url"]
+ title = re.search(
+ r"DeviantArt: ([^ ]+)'s gallery", feeds.feed["title"]
+ ).group(1)
+ obj = self.rssMessage(
+ bot=self.bot,
+ Type="deviant",
+ url=feed["link"],
+ title=feed["title"],
+ date=feed["published_parsed"],
+ author=title,
+ image=img_url,
+ )
+ liste.append(obj)
+ liste.reverse()
+ return liste
+
+ async def transform_feed(self, data: dict) -> dict:
+ """Transform a feed from the database to be useful for the code
+ ie blobs get their correct objects, dates become datetime objects"""
+ if data["roles"]:
+ try:
+ data["roles"] = loads(data["roles"])
+ except TypeError:
+ data["roles"] = None
+ else:
+ data["roles"] = list()
+ if data["embed_structure"]:
+ try:
+ data["embed_structure"] = loads(data["embed_structure"])
+ except TypeError:
+ data["embed_structure"] = None
+ else:
+ data["embed_structure"] = None
+ if data["date"]:
+ data["date"] = datetime.datetime.strptime(data["date"], "%Y-%m-%d %H:%M:%S")
+ data["added_at"] = datetime.datetime.strptime(
+ data["added_at"], "%Y-%m-%d %H:%M:%S"
+ )
+ return data
+
+ async def db_get_flow(self, ID: int):
+ query = f"SELECT rowid as ID, * FROM {self.table} WHERE `rowid`=?"
+ liste = self.bot.db_query(query, (ID,))
+ for e in range(len(liste)):
+ liste[e] = await self.transform_feed(liste[e])
+ return liste
+
+ async def db_get_guild_flows(self, guildID: int):
+ """Get every flow of a guild"""
+ query = f"SELECT rowid as ID, * FROM {self.table} WHERE `guild`=?"
+ liste = self.bot.db_query(query, (guildID,))
+ for e in range(len(liste)):
+ liste[e] = await self.transform_feed(liste[e])
+ return liste
+
+ async def db_add_flow(self, guildID: int, channelID: int, _type: str, link: str):
+ """Add a flow in the database"""
+ if _type == "mc":
+ form = ""
+ else:
+ form = await self.bot._(guildID, "rss." + _type + "-default-flow")
+ query = "INSERT INTO `{}` (`guild`,`channel`,`type`,`link`,`structure`) VALUES (:g, :c, :t, :l, :f)".format(
+ self.table
+ )
+ ID = self.bot.db_query(
+ query, {"g": guildID, "c": channelID, "t": _type, "l": link, "f": form}
+ )
+ return ID
+
+ async def db_remove_flow(self, ID: int):
+ """Remove a flow from the database"""
+ if not isinstance(ID, int):
+ raise ValueError
+ query = f"DELETE FROM {self.table} WHERE rowid=?"
+ self.bot.db_query(query, (ID,))
+ return True
+
+ async def db_get_all_flows(self):
+ """Get every flow of the database"""
+ query = "SELECT rowid as ID, * FROM `{}` WHERE `guild` in ({})".format(
+ self.table, ",".join(["'{}'".format(x.id) for x in self.bot.guilds])
+ )
+ liste = self.bot.db_query(query, ())
+ for e in range(len(liste)):
+ liste[e] = await self.transform_feed(liste[e])
+ return liste
+
+ async def db_get_count(self, get_disabled: bool = False):
+ """Get the number of rss feeds"""
+ query = "SELECT COUNT(*) FROM `{}`".format(self.table)
+ if not get_disabled:
+ query += (
+ " WHERE `guild` in ("
+ + ",".join(["'{}'".format(x.id) for x in self.bot.guilds])
+ + ")"
+ )
+ result = self.bot.db_query(query, (), fetchone=True)
+ return result[0]
+
+ async def db_update_flow(self, ID: int, values=[(None, None)]):
+ """Update a flow in the database"""
+ temp = ", ".join([f"{v[0]}=?" for v in values])
+ values = [v[1] for v in values]
+ query = f"UPDATE `{self.table}` SET {temp} WHERE rowid={ID}"
+ self.bot.db_query(query, values)
+
+ async def send_rss_msg(
+ self, obj, channel: discord.TextChannel, roles: typing.List[str], send_stats
+ ):
+ if channel is not None:
+ t = await obj.create_msg(await self.get_lang(channel.guild))
+ mentions = list()
+ for item in roles:
+ if item == "":
+ continue
+ role = discord.utils.get(channel.guild.roles, id=int(item))
+ if role is not None:
+ mentions.append(role)
+ try:
+ if isinstance(t, discord.Embed):
+ await channel.send(
+ " ".join(obj.mentions),
+ embed=t,
+ allowed_mentions=discord.AllowedMentions(
+ everyone=False, roles=True
+ ),
+ )
+ else:
+ await channel.send(
+ t,
+ allowed_mentions=discord.AllowedMentions(
+ everyone=False, roles=True
+ ),
+ )
+ if send_stats:
+ if statscog := self.bot.get_cog("BotStats"):
+ statscog.rss_stats["messages"] += 1
+ except Exception as e:
+ self.bot.log.info(
+ "[send_rss_msg] Cannot send message on channel {}: {}".format(
+ channel.id, e
+ )
+ )
+
+ async def check_flow(
+ self, flow: dict, session: ClientSession = None, send_stats: bool = False
+ ):
+ try:
+ guild = self.bot.get_guild(flow["guild"])
+ if flow["link"] in self.cache.keys():
+ objs = self.cache[flow["link"]]
+ else:
+ funct = getattr(self, f"rss_{flow['type']}")
+ if flow["type"] == "tw":
+ objs = await funct(guild, flow["link"], flow["date"])
+ else:
+ objs = await funct(
+ guild, flow["link"], flow["date"], session=session
+ )
+ if isinstance(objs, twitter.error.TwitterError):
+ self.twitter_over_capacity = True
+ return False
+ flow["link"] = objs
+ if isinstance(objs, twitter.TwitterError):
+ await self.bot.get_user(279568324260528128).send(
+ f"[send_rss_msg] twitter error dans `await check_flow(): {objs}`"
+ )
+ raise objs
+ if isinstance(objs, (str, type(None), int)) or len(objs) == 0:
+ return True
+ elif isinstance(objs, list):
+ for o in objs:
+ guild = self.bot.get_guild(flow["guild"])
+ if guild is None:
+ self.bot.log.info(
+ "[send_rss_msg] Can not send message on server {} (unknown)".format(
+ flow["guild"]
+ )
+ )
+ return False
+ chan = guild.get_channel(flow["channel"])
+ if guild is None:
+ self.bot.log.info(
+ "[send_rss_msg] Can not send message on channel {} (unknown)".format(
+ flow["channel"]
+ )
+ )
+ return False
+ o.format = flow["structure"]
+ o.embed = bool(flow["use_embed"])
+ if o.embed:
+ o.fill_embed_data(flow)
+ await o.fill_mention(guild, flow["roles"], self.bot._)
+ await self.send_rss_msg(o, chan, flow["roles"], send_stats)
+ await self.db_update_flow(
+ flow["ID"],
+ [("date", o.date)],
+ )
+ return True
+ else:
+ return True
+ except Exception as e:
+ await self.bot.get_cog("Errors").senf_err_msg(
+ "Erreur rss sur le flux {} (type {} - salon {})".format(
+ flow["link"], flow["type"], flow["channel"]
+ )
+ )
+ await self.bot.get_cog("Errors").on_error(e, None)
+ return False
+
+ async def main_loop(self, guildID: int = None):
+ if not self.config["rss_loop_enabled"]:
+ return
+ t = time.time()
+ if self.loop_processing:
+ return
+ if guildID is None:
+ self.bot.log.info("Check RSS lancé")
+ self.loop_processing = True
+ liste = await self.db_get_all_flows()
+ else:
+ self.bot.log.info(f"Check RSS lancé pour le serveur {guildID}")
+ liste = await self.db_get_guild_flows(guildID)
+ check = 0
+ errors = []
+ if guildID is None:
+ if statscog := self.bot.get_cog("BotStats"):
+ statscog.rss_stats["messages"] = 0
+ session = ClientSession()
+ for flow in liste:
+ try:
+ if flow["type"] == "tw" and self.twitter_over_capacity:
+ continue
+ if flow["type"] == "mc":
+ if MCcog := self.bot.get_cog("Minecraft"):
+ await MCcog.check_flow(flow, send_stats=(guildID is None))
+ check += 1
+ else:
+ if await self.check_flow(
+ flow, session, send_stats=(guildID is None)
+ ):
+ check += 1
+ else:
+ errors.append(flow["ID"])
+ except Exception as e:
+ await self.bot.get_cog("Errors").on_error(e, None)
+ await asyncio.sleep(self.time_between_flows_check)
+ await session.close()
+ if MCcog := self.bot.get_cog("Minecraft"):
+ MCcog.flows = dict()
+ d = [
+ "**RSS loop done** in {}s ({}/{} flows)".format(
+ round(time.time() - t, 3), check, len(liste)
+ )
+ ]
+ if guildID is None:
+ if statscog := self.bot.get_cog("BotStats"):
+ statscog.rss_stats["checked"] = check
+ statscog.rss_stats["errors"] = len(errors)
+ if len(errors) > 0:
+ d.append(
+ "{} errors: {}".format(len(errors), " ".join([str(x) for x in errors]))
+ )
+ emb = discord.Embed(
+ description="\n".join(d),
+ color=1655066,
+ timestamp=datetime.datetime.utcnow(),
+ )
+ emb.set_author(name=str(self.bot.user), icon_url=self.bot.user.display_avatar)
+ # await self.bot.get_cog("Embeds").send([emb],url="loop")
+ self.bot.log.debug(d[0])
+ if len(errors) > 0:
+ self.bot.log.warn("[Rss loop] " + d[1])
+ if guildID is None:
+ self.loop_processing = False
+ self.twitter_over_capacity = False
+ self.cache = dict()
+
+ @tasks.loop(minutes=20)
+ async def loop_child(self):
+ self.bot.log.info(" Boucle rss commencée !")
+ t1 = time.time()
+ await self.bot.get_cog("Rss").main_loop()
+ self.bot.log.info(
+ " Boucle rss terminée en {}s!".format(round(time.time() - t1, 2))
+ )
+
+ @loop_child.before_loop
+ async def before_loop(self):
+ """Wait until the bot is ready"""
+ await self.bot.wait_until_ready()
+
+ @commands.command(name="rss_loop", hidden=True)
+ @commands.check(checks.is_bot_admin)
+ async def rss_loop_admin(self, ctx: MyContext, new_state: str = "start"):
+ """Manage the rss loop
+ new_state can be start, stop or once"""
+ if new_state == "start":
+ try:
+ await self.loop_child.start()
+ except RuntimeError:
+ await ctx.send("La boucle est déjà en cours !")
+ else:
+ await ctx.send("Boucle rss relancée !")
+ elif new_state == "stop":
+ await self.loop_child.cancel()
+ self.bot.log.info(" Boucle rss arrêtée de force par un admin")
+ await ctx.send("Boucle rss arrêtée de force !")
+ elif new_state == "once":
+ if self.loop_processing:
+ await ctx.send("Une boucle rss est déjà en cours !")
+ else:
+ await ctx.send("Et hop ! Une itération de la boucle en cours !")
+ self.bot.log.info(" Boucle rss forcée")
+ await self.main_loop()
+ else:
+ await ctx.send(
+ "Option `new_start` invalide - choisissez start, stop ou once"
+ )
+
+ async def send_log(self, text: str, guild: discord.Guild):
+ """Send a log to the logging channel"""
+ return
+ # try:
+ # emb = self.bot.get_cog("Embeds").Embed(desc="[RSS] "+text,color=5366650,footer_text=guild.name).update_timestamp().set_author(self.bot.user)
+ # await self.bot.get_cog("Embeds").send([emb])
+ # except Exception as e:
+ # await self.bot.get_cog("Errors").on_error(e,None)
diff --git a/plugins/rss/setup.py b/plugins/rss/setup.py
new file mode 100644
index 00000000..93e242ff
--- /dev/null
+++ b/plugins/rss/setup.py
@@ -0,0 +1,77 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+from LRFutils import color
+import core
+
+def run(save=False):
+
+ blue = color.fg.blue
+ NC = color.stop
+
+ accept = ["yes", "y", "yeah","yep"]
+ decline = ["no", "n", "nah", "nope"]
+
+ config = core.config.get("rss")
+
+ choice = input(f"\n{blue}🔄️ Do you want to enable the RSS loop? [Y/n]:{NC} ")
+ if choice not in decline:
+ config["rss_loop_enabled"] = True
+
+ # Consumer key
+
+ def set_consumer_key():
+ if c := input(f"\n🔑 {blue}Twitter consumer key (let empty to ignore):{NC} ") != "":
+ config["twitter"]["consumer_key"] = c
+
+ if config["twitter"]["consumer_key"] is not None:
+ choice = input(f"\n{blue}A consumer kkey is already set. Do you want to edit it? [y/N]:{NC} ")
+ if choice in accept:
+ set_consumer_key()
+ else:
+ set_consumer_key()
+
+ # Consumer secret
+
+ def set_consumer_secret():
+ if c := input(f"\n🔑 {blue}Twitter consumer secret (let empty to ignore):{NC} ") != "":
+ config["twitter"]["consumer_secret"] = c
+
+ if config["twitter"]["consumer_secret"] is not None:
+ choice = input(f"\n{blue}A consumer secret is already set. Do you want to edit it? [y/N]:{NC} ")
+ if choice in accept:
+ set_consumer_secret()
+ else:
+ set_consumer_secret()
+
+ # Access token key
+
+ def set_access_token_key():
+ if c := input(f"\n🔑 {blue}Twitter access token key (let empty to ignore):{NC} ") != "":
+ config["twitter"]["access_token_key"] = c
+
+ if config["twitter"]["access_token_key"] is not None:
+ choice = input(f"\n{blue}An access token key is already set. Do you want to edit it? [y/N]:{NC} ")
+ if choice in accept:
+ set_access_token_key()
+ else:
+ set_access_token_key()
+
+ # Access token secret
+
+ def set_access_token_secret():
+ if c := input(f"\n🔑 {blue}Twitter access token secret (let empty to ignore):{NC} ") != "":
+ config["twitter"]["access_token_secret"] = c
+
+ if config["twitter"]["access_token_secret"] is not None:
+ choice = input(f"\n{blue}An access token secret is already set. Do you want to edit it? [y/N]:{NC} ")
+ if choice in accept:
+ set_access_token_secret()
+ else:
+ set_access_token_secret()
+
+ return config
\ No newline at end of file
diff --git a/plugins/thanks/credits.md b/plugins/thanks/credits.md
new file mode 100644
index 00000000..dbafbba5
--- /dev/null
+++ b/plugins/thanks/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/thanks/data/model.sql b/plugins/thanks/data/model.sql
index bda96333..4d1ba2ce 100644
--- a/plugins/thanks/data/model.sql
+++ b/plugins/thanks/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `thanks` (
`guild` BIGINT NOT NULL,
`user` BIGINT NOT NULL,
diff --git a/plugins/thanks/docs/user_documentation.md b/plugins/thanks/docs/user_documentation.md
index 43955a57..5aaf2dc8 100644
--- a/plugins/thanks/docs/user_documentation.md
+++ b/plugins/thanks/docs/user_documentation.md
@@ -1,4 +1,11 @@
-# **Thanks**
+
+
+# 💖 Thanks
At Gunivers, we believe that active members of a project should be rewarded for their efforts. We have therefore set up a reward system, allowing the server staff to congratulate certain people via a simple command: the `thanks` command.
diff --git a/plugins/thanks/langs/en.yml b/plugins/thanks/langs/en.yml
index b4e808b8..149ac7a8 100644
--- a/plugins/thanks/langs/en.yml
+++ b/plugins/thanks/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
thanks:
add:
diff --git a/plugins/thanks/langs/fr.yml b/plugins/thanks/langs/fr.yml
index ecc947c8..0db5600c 100644
--- a/plugins/thanks/langs/fr.yml
+++ b/plugins/thanks/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
thanks:
add:
diff --git a/plugins/thanks/bot/main.py b/plugins/thanks/thanks.py
similarity index 67%
rename from plugins/thanks/bot/main.py
rename to plugins/thanks/thanks.py
index a32bce13..67b0080e 100644
--- a/plugins/thanks/bot/main.py
+++ b/plugins/thanks/thanks.py
@@ -1,56 +1,73 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import args
+from utils import Gunibot, MyContext
+from discord.ext import commands
+import discord
+from bot import checks
import asyncio
import datetime
from typing import Dict, List, Optional, Tuple
import sys
+
sys.path.append("./bot")
-from bot import checks
-import discord
-from discord.ext import commands
-from utils import Gunibot, MyContext
-import args
class Thanks(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.tasks = list()
if bot.is_ready():
self.schedule_tasks()
- self.config_options = ['_thanks_cmd',
- 'thanks_duration', 'thanks_allowed_roles']
-
+ self.config_options = ["_thanks_cmd", "thanks_duration", "thanks_allowed_roles"]
+
bot.get_command("config").add_command(self.config_thanks_allowed_roles)
bot.get_command("config").add_command(self.config_thanks_duration)
bot.get_command("config").add_command(self.thanks_main)
-
@commands.command(name="thanks_allowed_roles")
- async def config_thanks_allowed_roles(self, ctx: MyContext, roles: commands.Greedy[discord.Role]):
+ async def config_thanks_allowed_roles(
+ self, ctx: MyContext, roles: commands.Greedy[discord.Role]
+ ):
if len(roles) == 0:
roles = None
else:
roles = [role.id for role in roles]
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "thanks_allowed_roles", roles))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "thanks_allowed_roles", roles
+ )
+ )
@commands.command(name="thanks_duration")
- async def config_thanks_duration(self, ctx: MyContext, duration: commands.Greedy[args.tempdelta]):
+ async def config_thanks_duration(
+ self, ctx: MyContext, duration: commands.Greedy[args.tempdelta]
+ ):
duration = sum(duration)
if duration == 0:
if ctx.message.content.split(" ")[-1] != "thanks_duration":
- await ctx.send(await self.bot._(ctx.guild.id, "sconfig.invalid-duration"))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "sconfig.invalid-duration")
+ )
return
duration = None
- x = await self.bot.sconfig.edit_config(ctx.guild.id, "thanks_duration", duration)
+ x = await self.bot.sconfig.edit_config(
+ ctx.guild.id, "thanks_duration", duration
+ )
await ctx.send(x)
-
- @commands.group(name="thanks", aliases=['thx'], enabled=False)
+
+ @commands.group(name="thanks", aliases=["thx"], enabled=False)
async def thanks_main(self, ctx: MyContext):
"""Edit your thanks-levels settings"""
if ctx.subcommand_passed is None:
await ctx.send_help("config thanks")
-
+
@thanks_main.command(name="list")
async def thanks_list(self, ctx: MyContext):
"""List your current thanks levels"""
@@ -72,21 +89,25 @@ async def thanks_reset(self, ctx: MyContext, amount: int = None):
else:
await self.bot.get_cog("Thanks").thankslevel_remove(ctx, amount)
'''
+
@commands.Cog.listener()
async def on_ready(self):
self.schedule_tasks()
def schedule_tasks(self):
- res = self.bot.db_query('SELECT guild, user, timestamp FROM thanks', (), astuple=True)
+ res = self.bot.db_query(
+ "SELECT guild, user, timestamp FROM thanks", (), astuple=True
+ )
now = datetime.datetime.now()
for task in res:
task = list(task)
task[2] = datetime.datetime.strptime(task[2], "%Y-%m-%d %H:%M:%S")
delta = (task[2] - now).total_seconds()
- delta += self.bot.server_configs[task[0]]['thanks_duration']
+ delta += self.bot.server_configs[task[0]]["thanks_duration"]
if delta > 0:
T = self.bot.get_cog("TimeCog").add_task(
- delta, self.reload_roles, *task)
+ delta, self.reload_roles, *task
+ )
self.tasks.append(T)
def cog_unload(self):
@@ -95,7 +116,9 @@ def cog_unload(self):
if self.bot.get_cog("Sconfig"):
self.bot.get_command("config thanks").enabled = False
- async def _create_config(self, ctx: MyContext, mentions: bool = False) -> List[Tuple[str, str]]:
+ async def _create_config(
+ self, ctx: MyContext, mentions: bool = False
+ ) -> List[Tuple[str, str]]:
"""Create a list of (key,value) for the /config command"""
roles: dict = self.db_get_roles(ctx.guild.id)
result = list()
@@ -109,24 +132,26 @@ async def _create_config(self, ctx: MyContext, mentions: bool = False) -> List[T
return result
def db_get_user(self, guildID: int, userID: int) -> Optional[dict]:
- query = 'SELECT * FROM thanks WHERE guild=? AND user=?'
+ query = "SELECT * FROM thanks WHERE guild=? AND user=?"
res = self.bot.db_query(query, (guildID, userID))
return res if len(res) > 0 else None
- def db_get_last(self, guildID: int, userID: int, authorID: int = None) -> Optional[dict]:
+ def db_get_last(
+ self, guildID: int, userID: int, authorID: int = None
+ ) -> Optional[dict]:
if authorID is None:
res = self.db_get_user(guildID, userID)
else:
- query = 'SELECT * FROM thanks WHERE guild=? AND user=? AND author=?'
+ query = "SELECT * FROM thanks WHERE guild=? AND user=? AND author=?"
res = self.bot.db_query(query, (guildID, userID, authorID))
return res[-1] if len(res) > 0 else None
def db_get_amount(self, guildID: int, userID: int, duration: int = None) -> int:
- query = 'SELECT COUNT(*) as count FROM thanks WHERE guild=? AND user=?'
+ query = "SELECT COUNT(*) as count FROM thanks WHERE guild=? AND user=?"
if duration:
query += f" AND timestamp >= datetime('now','-{duration} seconds')"
res = self.bot.db_query(query, (guildID, userID), fetchone=True)
- return res['count']
+ return res["count"]
def db_add_thanks(self, guildID: int, userID: int, authorID: int):
query = "INSERT INTO thanks (guild,user,author) VALUES (?, ?, ?)"
@@ -151,7 +176,7 @@ def db_get_roles(self, guildID: int, level: int = None):
liste = self.bot.db_query(query, (guildID,))
res = dict()
for lvl in liste:
- res[lvl['level']] = res.get(lvl['level'], list()) + [lvl['role']]
+ res[lvl["level"]] = res.get(lvl["level"], list()) + [lvl["role"]]
return res
def db_remove_level(self, guildID: int, level: int):
@@ -162,8 +187,10 @@ def db_reset_level(self, guildID: int):
query = "DELETE FROM thanks_levels WHERE guild=?"
self.bot.db_query(query, (guildID,))
- async def has_allowed_roles(self, guild: discord.Guild, member: discord.Member) -> bool:
- config = self.bot.server_configs[guild.id]['thanks_allowed_roles']
+ async def has_allowed_roles(
+ self, guild: discord.Guild, member: discord.Member
+ ) -> bool:
+ config = self.bot.server_configs[guild.id]["thanks_allowed_roles"]
if config is None:
return False
roles = [guild.get_role(x) for x in config]
@@ -172,12 +199,18 @@ async def has_allowed_roles(self, guild: discord.Guild, member: discord.Member)
return True
return False
- async def give_remove_roles(self, member: discord.Member, roles_conf: Dict[int, List[discord.Role]] = None, duration: int = None) -> bool:
+ async def give_remove_roles(
+ self,
+ member: discord.Member,
+ roles_conf: Dict[int, List[discord.Role]] = None,
+ duration: int = None,
+ ) -> bool:
"""Give or remove thanks roles if needed
Return True if roles were given/removed, else False"""
if not member.guild.me.guild_permissions.manage_roles:
self.bot.log.info(
- f"Module - Thanks: Missing \"manage_roles\" permission on guild \"{member.guild.name}\"")
+ f'Module - Thanks: Missing "manage_roles" permission on guild "{member.guild.name}"'
+ )
return False
g: discord.Guild = member.guild
pos: int = g.me.top_role.position
@@ -188,11 +221,12 @@ async def give_remove_roles(self, member: discord.Member, roles_conf: Dict[int,
continue
r = [g.get_role(x) for x in v]
roles_conf[k] = list(
- filter(lambda x: (x is not None) and (x.position < pos), r))
+ filter(lambda x: (x is not None) and (x.position < pos), r)
+ )
if len(roles_conf[k]) == 0:
del roles_conf[k]
if duration is None:
- duration = self.bot.server_configs[member.guild.id]['thanks_duration']
+ duration = self.bot.server_configs[member.guild.id]["thanks_duration"]
amount = self.db_get_amount(member.guild.id, member.id, duration)
gave_anything = False
for lvl, roles in roles_conf.items():
@@ -200,19 +234,27 @@ async def give_remove_roles(self, member: discord.Member, roles_conf: Dict[int,
roles = list(filter(lambda x: x not in member.roles, roles))
if len(roles) > 0:
await member.add_roles(*roles, reason="Thanks system")
- self.bot.log.debug("[Thanks] Rôles {0} ajoutés à {1} ({1.id})".format(roles, member))
+ self.bot.log.debug(
+ "[Thanks] Rôles {0} ajoutés à {1} ({1.id})".format(
+ roles, member
+ )
+ )
gave_anything = True
else: # should remove roles
roles = list(filter(lambda x: x in member.roles, roles))
if len(roles) > 0:
await member.remove_roles(*roles, reason="Thanks system")
- self.bot.log.debug("[Thanks] Rôles {0} enlevés à {1} ({1.id})".format(roles, member))
+ self.bot.log.debug(
+ "[Thanks] Rôles {0} enlevés à {1} ({1.id})".format(
+ roles, member
+ )
+ )
gave_anything = True
return gave_anything
async def reload_roles(self, guildID: int, memberID: int, date: datetime.datetime):
"""Remove roles if needed"""
- delta = self.bot.server_configs[guildID]['thanks_duration']
+ delta = self.bot.server_configs[guildID]["thanks_duration"]
if (datetime.datetime.now() - date).total_seconds() < delta:
return
guild: discord.Guild = self.bot.get_guild(guildID)
@@ -223,7 +265,7 @@ async def reload_roles(self, guildID: int, memberID: int, date: datetime.datetim
return
await self.give_remove_roles(member, duration=delta)
- @commands.command(name="thanks", aliases=['thx'])
+ @commands.command(name="thanks", aliases=["thx"])
@commands.guild_only()
async def thanks(self, ctx: MyContext, *, user: discord.User):
"""Thanks a user for their work.
@@ -240,23 +282,31 @@ async def thanks(self, ctx: MyContext, *, user: discord.User):
last = self.db_get_last(ctx.guild.id, user.id, ctx.author.id)
if last:
last_date = datetime.datetime.strptime(
- last['timestamp'], "%Y-%m-%d %H:%M:%S")
+ last["timestamp"], "%Y-%m-%d %H:%M:%S"
+ )
delta = datetime.datetime.utcnow() - last_date
if delta.days < 1:
await ctx.send(await self.bot._(ctx.guild.id, "thanks.add.too-soon"))
return
self.db_add_thanks(ctx.guild.id, user.id, ctx.author.id)
- duration = self.bot.server_configs[ctx.guild.id]['thanks_duration']
+ duration = self.bot.server_configs[ctx.guild.id]["thanks_duration"]
amount = self.db_get_amount(ctx.guild.id, user.id, duration)
- await ctx.send(await self.bot._(ctx.guild.id, "thanks.add.done", user=user, amount=amount))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "thanks.add.done", user=user, amount=amount)
+ )
T = self.bot.get_cog("TimeCog").add_task(
- duration, self.reload_roles, ctx.guild.id, user.id, datetime.datetime.utcnow())
+ duration,
+ self.reload_roles,
+ ctx.guild.id,
+ user.id,
+ datetime.datetime.utcnow(),
+ )
self.tasks.append(T)
member = ctx.guild.get_member(user.id)
if member is not None:
await self.give_remove_roles(member)
- @commands.command(name="thankslist", aliases=['thanks-list', 'thxlist'])
+ @commands.command(name="thankslist", aliases=["thanks-list", "thxlist"])
@commands.guild_only()
async def thanks_list(self, ctx: MyContext, *, user: discord.User = None):
"""Get the list of thanks given to a user (or you by default)"""
@@ -272,43 +322,64 @@ async def thanks_list(self, ctx: MyContext, *, user: discord.User = None):
await ctx.send(txt)
return
for e, l in enumerate(liste):
- liste[e] = [self.bot.get_guild(l['guild']), self.bot.get_user(l['user']), self.bot.get_user(l['author']), datetime.datetime.strptime(l['timestamp'], "%Y-%m-%d %H:%M:%S")]
- duration = self.bot.server_configs[ctx.guild.id]['thanks_duration']
- current = [x for x in liste if (datetime.datetime.utcnow() -
- x[3]).total_seconds() < duration]
+ liste[e] = [
+ self.bot.get_guild(l["guild"]),
+ self.bot.get_user(l["user"]),
+ self.bot.get_user(l["author"]),
+ datetime.datetime.strptime(l["timestamp"], "%Y-%m-%d %H:%M:%S"),
+ ]
+ duration = self.bot.server_configs[ctx.guild.id]["thanks_duration"]
+ current = [
+ x
+ for x in liste
+ if (datetime.datetime.utcnow() - x[3]).total_seconds() < duration
+ ]
if ctx.can_send_embed:
_title = await self.bot._(ctx.guild.id, "thanks.list.title", user=user)
emb = discord.Embed(title=_title)
- _active = await self.bot._(ctx.guild.id, "thanks.list.active", count=len(current))
+ _active = await self.bot._(
+ ctx.guild.id, "thanks.list.active", count=len(current)
+ )
if len(current) > 0:
- t = ["• {} ({})".format(x[2].mention, x[3].strftime("%d/%m/%y %HH%M"))
- for x in current]
+ t = [
+ "• {} ({})".format(x[2].mention, x[3].strftime("%d/%m/%y %HH%M"))
+ for x in current
+ ]
emb.add_field(name=_active, value="\n".join(t))
else:
emb.add_field(name=_active, value="0")
old = len(liste) - len(current)
if old > 0:
- _inactive = await self.bot._(ctx.guild.id, "thanks.list.inactive", count=old)
+ _inactive = await self.bot._(
+ ctx.guild.id, "thanks.list.inactive", count=old
+ )
emb.add_field(name="\u200b", value=_inactive, inline=False)
await ctx.send(embed=emb)
else:
txt = "```md\n"
if len(current) > 0:
- t = ["- {} ({})".format(str(x[2]), x[3].strftime("%d/%m/%y %HH%M"))
- for x in current]
- _active = await self.bot._(ctx.guild.id, "thanks.list.active", count=len(current))
- txt += "# " + _active + "\n{}\n".format(
- len(current), "\n".join(t))
+ t = [
+ "- {} ({})".format(str(x[2]), x[3].strftime("%d/%m/%y %HH%M"))
+ for x in current
+ ]
+ _active = await self.bot._(
+ ctx.guild.id, "thanks.list.active", count=len(current)
+ )
+ txt += "# " + _active + "\n{}\n".format(len(current), "\n".join(t))
old = len(liste) - len(current)
if old > 0:
- _inactive = await self.bot._(ctx.guild.id, "thanks.list.inactive", count=len(current))
+ _inactive = await self.bot._(
+ ctx.guild.id, "thanks.list.inactive", count=len(current)
+ )
txt += "\n" + _inactive + "\n"
- await ctx.send(txt+"```")
+ await ctx.send(txt + "```")
- @commands.command(name="thanksreload", aliases=['thanks-reload'])
+ @commands.command(name="thanksreload", aliases=["thanks-reload"])
@commands.guild_only()
@commands.check(checks.is_admin)
- async def thanks_reload(self, ctx: commands.Context, *, user: discord.Member = None):
+ async def thanks_reload(
+ self, ctx: commands.Context, *, user: discord.Member = None
+ ):
"""Reload the thanks roles for a user, or everyone"""
users = [user] if user is not None else ctx.guild.members
users = list(filter(lambda x: not x.bot, users))
@@ -327,15 +398,19 @@ async def thanks_reload(self, ctx: commands.Context, *, user: discord.Member = N
await ctx.send(await self.bot._(ctx.guild.id, "thanks.reload.no-perm"))
return
del roles
- delta = self.bot.server_configs[ctx.guild.id]['thanks_duration']
+ delta = self.bot.server_configs[ctx.guild.id]["thanks_duration"]
i = 0
for m in users:
if await self.give_remove_roles(m, rolesID, delta):
i += 1
if i == 0:
- txt = await self.bot._(ctx.guild.id, "thanks.reload.nothing-done", count=len(users))
+ txt = await self.bot._(
+ ctx.guild.id, "thanks.reload.nothing-done", count=len(users)
+ )
elif i == 1:
- txt = await self.bot._(ctx.guild.id, "thanks.reload.one-done", count=len(users))
+ txt = await self.bot._(
+ ctx.guild.id, "thanks.reload.one-done", count=len(users)
+ )
else:
txt = await self.bot._(ctx.guild.id, "thanks.reload.many-done", i=i)
await ctx.send(txt)
@@ -345,32 +420,41 @@ async def thankslevels_list(self, ctx: MyContext):
async def g(k: int) -> str:
return await self.bot._(ctx.guild.id, "thanks.thanks", count=k)
+
text = "\n".join(
- [await g(k)+" ".join([f"<@&{r}>" for r in v]) for k, v in roles.items()])
+ [await g(k) + " ".join([f"<@&{r}>" for r in v]) for k, v in roles.items()]
+ )
if text == "":
text = await self.bot._(ctx.guild.id, "thanks.no-role")
_title = await self.bot._(ctx.guild.id, "thanks.roles-list")
if ctx.can_send_embed:
- embed = discord.Embed(
- title=_title, description=text)
+ embed = discord.Embed(title=_title, description=text)
await ctx.send(embed=embed)
else:
await ctx.send("__" + _title + ":__\n" + text)
- async def thankslevel_add(self, ctx: commands.Context, level: int, role: discord.Role):
+ async def thankslevel_add(
+ self, ctx: commands.Context, level: int, role: discord.Role
+ ):
self.db_set_role(ctx.guild.id, role.id, level)
roles = self.db_get_roles(ctx.guild.id, level)
if len(roles) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "thanks.went-wrong"))
return
roles = roles[level]
- await ctx.send(await self.bot._(ctx.guild.id, "thanks.role-added", count=len(roles), lvl=level))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "thanks.role-added", count=len(roles), lvl=level
+ )
+ )
async def thankslevel_remove(self, ctx: commands.Context, level: int):
self.db_remove_level(ctx.guild.id, level)
roles = self.db_get_roles(ctx.guild.id, level)
if len(roles) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "thanks.roles-deleted", lvl=level))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "thanks.roles-deleted", lvl=level)
+ )
else:
await ctx.send(await self.bot._(ctx.guild.id, "thanks.went-wrong"))
@@ -379,13 +463,20 @@ async def thankslevel_reset(self, ctx: commands.Context):
if len(roles) == 0:
await ctx.send(await self.bot._(ctx.guild.id, "thanks.reload.no-role"))
return
- msg: discord.Message = await ctx.send(await self.bot._(ctx.guild.id, "thanks.confirm", count=len(roles)))
+ msg: discord.Message = await ctx.send(
+ await self.bot._(ctx.guild.id, "thanks.confirm", count=len(roles))
+ )
await msg.add_reaction("✅")
def check(reaction, user):
- return user == ctx.author and str(reaction.emoji) == "✅" and reaction.message.id == msg.id
+ return (
+ user == ctx.author
+ and str(reaction.emoji) == "✅"
+ and reaction.message.id == msg.id
+ )
+
try:
- await self.bot.wait_for('reaction_add', timeout=30.0, check=check)
+ await self.bot.wait_for("reaction_add", timeout=30.0, check=check)
except asyncio.TimeoutError:
await ctx.send(await self.bot._(ctx.guild.id, "thanks.too-long"))
return
@@ -397,5 +488,11 @@ def check(reaction, user):
await ctx.send(await self.bot._(ctx.guild.id, "thanks.went-wrong"))
-async def setup(bot):
- await bot.add_cog(Thanks(bot))
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Thanks(bot), icon="❤️")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
diff --git a/plugins/voice/config.yaml b/plugins/voice/config.yaml
new file mode 100644
index 00000000..22bb79ac
--- /dev/null
+++ b/plugins/voice/config.yaml
@@ -0,0 +1 @@
+randommer_api_key: ''
\ No newline at end of file
diff --git a/plugins/voice/credits.md b/plugins/voice/credits.md
new file mode 100644
index 00000000..dbafbba5
--- /dev/null
+++ b/plugins/voice/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/voice/data/model.sql b/plugins/voice/data/model.sql
index 460e142f..b77a2ad7 100644
--- a/plugins/voice/data/model.sql
+++ b/plugins/voice/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `voices_chats` (
`guild` BIGINT NOT NULL,
`channel` BIGINT NOT NULL,
diff --git a/plugins/voice/docs/user_documentation.md b/plugins/voice/docs/user_documentation.md
index 25d282b0..e25d7f0f 100644
--- a/plugins/voice/docs/user_documentation.md
+++ b/plugins/voice/docs/user_documentation.md
@@ -1,10 +1,17 @@
-# **Voice**
+
-## **Automatic role giving**
+# 🔊 Voice
+
+## Automatic role giving
It is possible to automatically give a role to any member being in a voice room, and to remove them when this member is not in any voice room. To do this, you just have to configure the option `voice_roles` with the list of roles to give. As simple as that.
-## **Creation of rooms on demand**
+## Creation of rooms on demand
The bot also has a voice room creation feature: when a member enters a specific voice room, the bot creates a special one, gives the permissions to manage the room to that user, and then moves the user to that new room. It is possible to customize the name of this room, which by default will take a random name via an API.
diff --git a/plugins/voice/langs/en.yml b/plugins/voice/langs/en.yml
index 9c66b9c8..d2a817da 100644
--- a/plugins/voice/langs/en.yml
+++ b/plugins/voice/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
voices:
no-channel: There is no voice chat room generated by me here
diff --git a/plugins/voice/langs/fr.yml b/plugins/voice/langs/fr.yml
index d7586f42..189509b5 100644
--- a/plugins/voice/langs/fr.yml
+++ b/plugins/voice/langs/fr.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
voices:
no-channel: Il n'y a aucun salon vocal généré par moi ici
diff --git a/plugins/voice/bot/asterix_names.txt b/plugins/voice/rsrc/asterix_names.txt
similarity index 100%
rename from plugins/voice/bot/asterix_names.txt
rename to plugins/voice/rsrc/asterix_names.txt
diff --git a/plugins/voice/bot/main.py b/plugins/voice/voice.py
similarity index 55%
rename from plugins/voice/bot/main.py
rename to plugins/voice/voice.py
index 287e81e7..50b088cb 100644
--- a/plugins/voice/bot/main.py
+++ b/plugins/voice/voice.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import random
import aiohttp
@@ -5,16 +12,24 @@
from discord.ext import commands
from utils import Gunibot, MyContext
-class VoiceChannels(commands.Cog):
+from core import config
+
+class VoiceChannels(commands.Cog):
def __init__(self, bot: Gunibot):
self.bot = bot
self.file = "voices"
- self.names = {'random': [], 'asterix': []}
+ self.names = {"random": [], "asterix": []}
self.channels = dict()
- self.config_options = ['voice_channel', 'voice_channel_format', 'voice_roles', 'voices_category']
+ self.config_options = [
+ "voice_channel",
+ "voice_channel_format",
+ "voice_roles",
+ "voices_category",
+ ]
self.db_get_channels()
-
+ self.config = config.get("voice") or {}
+
bot.get_command("config").add_command(self.config_voice_channel_format)
bot.get_command("config").add_command(self.config_voice_roles)
bot.get_command("config").add_command(self.config_voices_category)
@@ -24,38 +39,66 @@ def __init__(self, bot: Gunibot):
async def config_voice_channel_format(self, ctx: MyContext, *, text: str):
"""Format of voice channels names
Use {random} for any random name, {asterix} for any asterix name"""
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "voice_channel_format", text[:40]))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "voice_channel_format", text[:40]
+ )
+ )
@commands.command(name="voice_roles")
- async def config_voice_roles(self, ctx: MyContext, roles: commands.Greedy[discord.Role]):
+ async def config_voice_roles(
+ self, ctx: MyContext, roles: commands.Greedy[discord.Role]
+ ):
if len(roles) == 0:
roles = None
else:
roles = [role.id for role in roles]
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "voice_roles", roles))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "voice_roles", roles)
+ )
@commands.command(name="voices_category")
- async def config_voices_category(self, ctx: MyContext, *, category: discord.CategoryChannel):
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "voices_category", category.id))
+ async def config_voices_category(
+ self, ctx: MyContext, *, category: discord.CategoryChannel
+ ):
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "voices_category", category.id
+ )
+ )
@commands.command(name="voice_channel")
- async def config_voice_channel(self, ctx: MyContext, *, channel: discord.VoiceChannel):
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "voice_channel", channel.id))
+ async def config_voice_channel(
+ self, ctx: MyContext, *, channel: discord.VoiceChannel
+ ):
+ await ctx.send(
+ await self.bot.sconfig.edit_config(
+ ctx.guild.id, "voice_channel", channel.id
+ )
+ )
def db_get_channels(self):
- liste = self.bot.db_query('SELECT guild, channel FROM voices_chats', ())
+ liste = self.bot.db_query("SELECT guild, channel FROM voices_chats", ())
for row in liste:
- self.channels[row['guild']] = self.channels.get(row['guild'], list()) + [row['channel']]
+ self.channels[row["guild"]] = self.channels.get(row["guild"], list()) + [
+ row["channel"]
+ ]
def db_add_channel(self, channel: discord.VoiceChannel):
query = "INSERT INTO voices_chats (guild,channel) VALUES (?, ?)"
- rowcount = self.bot.db_query(query, (channel.guild.id, channel.id), returnrowcount=True)
+ rowcount = self.bot.db_query(
+ query, (channel.guild.id, channel.id), returnrowcount=True
+ )
if rowcount == 1:
- self.channels[channel.guild.id] = self.channels.get(channel.guild.id, list()) + [channel.id]
+ self.channels[channel.guild.id] = self.channels.get(
+ channel.guild.id, list()
+ ) + [channel.id]
def db_delete_channel(self, channel: discord.VoiceChannel):
query = "DELETE FROM voices_chats WHERE guild=? AND channel=?"
- rowcount = self.bot.db_query(query, (channel.guild.id, channel.id), returnrowcount=True)
+ rowcount = self.bot.db_query(
+ query, (channel.guild.id, channel.id), returnrowcount=True
+ )
if rowcount == 1:
try:
self.channels[channel.guild.id].remove(channel.id)
@@ -65,10 +108,12 @@ def db_delete_channel(self, channel: discord.VoiceChannel):
async def give_roles(self, member: discord.Member, remove=False):
if not member.guild.me.guild_permissions.manage_roles:
- self.bot.log.info(f"Module - Voice: Missing \"manage_roles\" permission on guild \"{member.guild.name}\"")
+ self.bot.log.info(
+ f'Module - Voice: Missing "manage_roles" permission on guild "{member.guild.name}"'
+ )
return
g = member.guild
- rolesID = self.bot.server_configs[g.id]['voice_roles']
+ rolesID = self.bot.server_configs[g.id]["voice_roles"]
if not rolesID:
return
roles = [g.get_role(x) for x in rolesID]
@@ -87,7 +132,12 @@ async def on_guild_channel_delete(self, channel: discord.abc.GuildChannel):
# other cases are not interesting
@commands.Cog.listener()
- async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState):
+ async def on_voice_state_update(
+ self,
+ member: discord.Member,
+ before: discord.VoiceState,
+ after: discord.VoiceState,
+ ):
"""Check if a member joined/left a voice channel"""
if before.channel == after.channel:
return
@@ -95,13 +145,22 @@ async def on_voice_state_update(self, member: discord.Member, before: discord.Vo
if config["voice_channel"] is None: # si rien n'a été configuré
return
if after.channel is not None and after.channel.id == config["voice_channel"]:
- if before.channel is not None and len(before.channel.members) == 0: # move from another channel which is now empty
- if (member.guild.id in self.channels.keys()) and (before.channel.id in self.channels[member.guild.id]):
- # if they come from an automated channel, we move them back if the channel is now empty
+ if (
+ before.channel is not None and len(before.channel.members) == 0
+ ): # move from another channel which is now empty
+ if (member.guild.id in self.channels.keys()) and (
+ before.channel.id in self.channels[member.guild.id]
+ ):
+ # if they come from an automated channel, we move them back
+ # if the channel is now empty
await member.move_to(before.channel)
return
await self.create_channel(member, config)
- if (before.channel is not None) and (member.guild.id in self.channels.keys()) and (before.channel.id in self.channels[member.guild.id]):
+ if (
+ (before.channel is not None)
+ and (member.guild.id in self.channels.keys())
+ and (before.channel.id in self.channels[member.guild.id])
+ ):
await self.delete_channel(before.channel)
if after.channel is None:
await self.give_roles(member, remove=True)
@@ -114,13 +173,16 @@ async def create_channel(self, member: discord.Member, config: dict):
if config["voices_category"] is None: # si rien n'a été configuré
return
voice_category: discord.CategoryChannel = self.bot.get_channel(
- config["voices_category"])
+ config["voices_category"]
+ )
if not isinstance(voice_category, discord.CategoryChannel):
return
perms = voice_category.permissions_for(member.guild.me)
# S'il manque des perms au bot: abort
if not (perms.manage_channels and perms.move_members):
- self.bot.log.info(f"Module - Voice: Missing \"manage_channels, move_members\" permission on guild \"{member.guild.name}\"")
+ self.bot.log.info(
+ f'Module - Voice: Missing "manage_channels, move_members" permission on guild "{member.guild.name}"'
+ )
return
p = len(voice_category.channels)
# try to calculate the correct permissions
@@ -130,52 +192,67 @@ async def create_channel(self, member: discord.Member, config: dict):
# remove manage roles cuz DISCOOOOOOOOOOORD
over[member].manage_roles = None
# build channel name from config and random
- chan_name = config['voice_channel_format']
- args = {'user': str(member)}
+ chan_name = config["voice_channel_format"]
+ args = {"user": str(member)}
if "{random}" in chan_name:
- args['random'] = await self.get_names()
+ args["random"] = await self.get_names()
if "{asterix}" in chan_name:
- args['asterix'] = await self.get_names('asterix')
+ args["asterix"] = await self.get_names("asterix")
chan_name = chan_name.format_map(self.bot.SafeDict(args))
# actually create the channel
- new_channel = await voice_category.create_voice_channel(name=chan_name, position=p, overwrites=over)
+ new_channel = await voice_category.create_voice_channel(
+ name=chan_name, position=p, overwrites=over
+ )
# move user
await member.move_to(new_channel)
# add to database
self.db_add_channel(new_channel)
- channel = self.bot.get_channel(379308111774875650)
- await channel.send("Hello World!")
- liste = self.bot.db_query('SELECT guild, channel FROM voices_chats', ())
- for row in liste:
- await channel.send(row)
-
async def delete_channel(self, channel: discord.VoiceChannel):
"""Delete an unusued channel if no one is in"""
- if len(channel.members) == 0 and channel.permissions_for(channel.guild.me).manage_channels:
+ if (
+ len(channel.members) == 0
+ and channel.permissions_for(channel.guild.me).manage_channels
+ ):
await channel.delete(reason="Unusued")
self.db_delete_channel(channel)
- async def get_names(self, source='random'):
+ async def get_names(self, source="random"):
+ # If we have some names in cache, we use one of them
if len(self.names[source]) != 0:
return self.names[source].pop()
- async with aiohttp.ClientSession() as session:
- h = {'X-Api-Key': self.bot.config['random_api_token']}
- if source == 'asterix':
- with open("plugins/voice/bot/asterix_names.txt", 'r', encoding='utf-8') as file:
- self.names["asterix"] = file.readlines()
- random.shuffle(self.names[source])
- else:
- async with session.get('https://randommer.io/api/Name?nameType=surname&quantity=20', headers=h) as resp:
+
+ # If we don't have any names in cache, we get some new ones
+ randommer_api_key = self.config.get("randommer_api_key")
+ if source != "asterix" and randommer_api_key != '':
+ headers = {"X-Api-Key": randommer_api_key}
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ "https://randommer.io/api/Name?nameType=surname&quantity=20",
+ headers=headers,
+ ) as resp:
self.names[source] = await resp.json()
- return self.names[source].pop()
+ return self.names[source].pop()
+
+ # If asked, or as fallback if API key isn't defined, we use Asterix names
+ else:
+ with open(
+ "plugins/voice/rsrc/asterix_names.txt", "r", encoding="utf-8"
+ ) as file:
+ self.names["asterix"] = file.readlines()
+ random.shuffle(self.names["asterix"])
+ return self.names["asterix"].pop()
+
@commands.command(name="voice-clean")
@commands.guild_only()
@commands.has_guild_permissions(manage_channels=True)
async def voice_clean(self, ctx: commands.Context):
"""Delete every unusued voice channels previously generated by the bot"""
- if not ctx.guild.id in self.channels.keys() or len(self.channels[ctx.guild.id]) == 0:
+ if (
+ ctx.guild.id not in self.channels.keys()
+ or len(self.channels[ctx.guild.id]) == 0
+ ):
await ctx.send(await self.bot._(ctx.guild.id, "voices.no-channel"))
return
i = 0
@@ -191,5 +268,6 @@ async def voice_clean(self, ctx: commands.Context):
await ctx.send(await self.bot._(ctx.guild.id, "voices.result", count=i))
-async def setup(bot):
- await bot.add_cog(VoiceChannels(bot))
+async def setup(bot:Gunibot=None):
+ if bot is not None:
+ await bot.add_cog(VoiceChannels(bot), icon="🎙️")
diff --git a/plugins/welcome/credits.md b/plugins/welcome/credits.md
new file mode 100644
index 00000000..dbafbba5
--- /dev/null
+++ b/plugins/welcome/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/welcome/docs/user_documentation.md b/plugins/welcome/docs/user_documentation.md
index 3c9f5549..5758e8c0 100644
--- a/plugins/welcome/docs/user_documentation.md
+++ b/plugins/welcome/docs/user_documentation.md
@@ -1,6 +1,13 @@
-# **Welcome**
+
-## **Verification system**
+# 👋 Welcome
+
+## Verification system
The bot has a member verification system, where each newcomer will have to enter a key phrase that will give him access to the rest of the server.
@@ -17,6 +24,6 @@ It is up to you to make sure that the unverified member can write in the verific
Other options will come later, like customizing the welcome message.
-## **Automatic roles**
+## Automatic roles
It is also possible to configure the bot to give a role to any newcomer, independently of the verification system detailed above. To do so, you just have to configure the `welcome_roles` option with the list of roles to give.
diff --git a/plugins/welcome/bot/main.py b/plugins/welcome/welcome.py
similarity index 63%
rename from plugins/welcome/bot/main.py
rename to plugins/welcome/welcome.py
index 77656dbc..3231dafe 100644
--- a/plugins/welcome/bot/main.py
+++ b/plugins/welcome/welcome.py
@@ -1,10 +1,16 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import discord
from discord.ext import commands
from utils import Gunibot, MyContext
class Welcome(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.config_options = ["welcome_roles"]
@@ -12,13 +18,17 @@ def __init__(self, bot: Gunibot):
bot.get_command("config").add_command(self.config_welcome_roles)
@commands.command(name="welcome_roles")
- async def config_welcome_roles(self, ctx: MyContext, roles: commands.Greedy[discord.Role]):
+ async def config_welcome_roles(
+ self, ctx: MyContext, roles: commands.Greedy[discord.Role]
+ ):
if len(roles) == 0:
roles = None
else:
roles = [role.id for role in roles]
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "welcome_roles", roles))
-
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "welcome_roles", roles)
+ )
+
async def give_welcome_roles(self, member: discord.Member):
g = member.guild
config = self.bot.server_configs[g.id]
@@ -35,18 +45,27 @@ async def on_member_join(self, member: discord.Member):
"""Called when a member joins a guild"""
g = member.guild
if not g.me.guild_permissions.manage_roles: # if not allowed to manage roles
- self.bot.log.info(f"Module - Welcome: Missing \"manage_roles\" permission on guild \"{g.name}\"")
+ self.bot.log.info(
+ f'Module - Welcome: Missing "manage_roles" permission on guild "{g.name}"'
+ )
return
if "MEMBER_VERIFICATION_GATE_ENABLED" not in g.features:
# we give new members roles if the verification gate is disabled
await self.give_welcome_roles(member)
-
+
@commands.Cog.listener()
- async def on_member_update(self, before:discord.Member, after:discord.Member):
+ async def on_member_update(self, before: discord.Member, after: discord.Member):
"""Main function called when a member got verified in a community server"""
if before.pending and not after.pending:
if "MEMBER_VERIFICATION_GATE_ENABLED" in after.guild.features:
await self.give_welcome_roles(after)
-async def setup(bot):
- await bot.add_cog(Welcome(bot))
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Welcome(bot), icon="👋")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
diff --git a/plugins/wormhole/bot/main.py b/plugins/wormhole/bot/main.py
deleted file mode 100644
index 75e50815..00000000
--- a/plugins/wormhole/bot/main.py
+++ /dev/null
@@ -1,469 +0,0 @@
-from typing import List, Union
-import discord
-from discord.ext import commands
-from itertools import chain
-
-import sys
-sys.path.append("./bot")
-from bot import checks
-from utils import Gunibot, MyContext
-from aiohttp import ClientSession
-
-import difflib
-
-# Check if a message is similar to another one with 80% similarity
-def similar(msg1, msg2):
- return difflib.SequenceMatcher(a=msg1.lower(), b=msg2.lower()).ratio() > 0.8
-
-
-# Get the corresponding answered message in other channels
-async def get_corresponding_answer(channel: discord.TextChannel, message: discord.Message) -> discord.Message:
- date = message.created_at
- async for msg in channel.history(limit=20, after=date, oldest_first=True):
- if similar(message.content, msg.content):
- return msg
- async for msg in channel.history(limit=20, before=date, oldest_first=False):
- if similar(message.content, msg.content):
- return msg
- return None
-
-async def sendMessage(msg: discord.Message, webhook: discord.Webhook, username: str, pp_guild: bool, embed_reply: discord.Embed = None):
- files = [await x.to_file() for x in msg.attachments]
- # grab mentions from the source message
- mentions = discord.AllowedMentions(
- everyone=msg.mention_everyone,
- users=msg.mentions,
- roles=msg.role_mentions)
- username = username.replace("{user}", msg.author.name, 10).replace("{guild}", msg.guild.name, 10).replace(
- "{channel}", msg.channel.name, 10)
- avatar_url = msg.author.display_avatar
- if pp_guild:
- avatar_url = msg.guild.icon_url
-
- embeds = [embed for embed in msg.embeds if embed.type == "rich"]
- if embed_reply and embeds:
- if len(embeds) >= 10:
- embeds.pop()
- embeds.append(embed_reply)
- new_msg: discord.WebhookMessage = await webhook.send(content=msg.content,
- files=files,
- embeds=embeds,
- avatar_url=avatar_url,
- username=username,
- allowed_mentions=discord.AllowedMentions.none(),
- wait=True)
- # edit the message to include mentions without notifications
- if mentions.roles or mentions.users or mentions.everyone:
- await new_msg.edit(allowed_mentions=mentions)
-
-
-class PermissionType(commands.Converter):
- types = ['w', 'r', 'wr']
-
- def __init__(self, action: Union[str, int] = None):
- if isinstance(action, str):
- self.type = self.types.index(action)
- elif isinstance(action, int):
- self.type = action
- else:
- return
- self.name = self.types[self.type]
-
- async def convert(self, ctx: commands.Context, argument: str):
- if argument in self.types:
- return PermissionType(argument)
- raise commands.errors.BadArgument("Unknown permission type")
-
-
-class Wormhole:
- def __init__(self, name: str, privacy: bool, owners: List[int], bot: Gunibot, channels: int):
- self.bot = bot
- self.name = name
- self.privacy = privacy
- self.owners = owners
- self.channels = channels
-
- def to_str(self) -> str:
- """Transform the Wormhole to a human-readable string"""
- private = (self.privacy == 1)
- owners: List[str] = []
- for o in self.owners:
- user = self.bot.get_user(o)
- owners.append(user.name if user else "Unknown user")
- return f"Wormhole: {self.name}\n┗━▷ Private: {private} - Admins: {', '.join(owners)} - **{self.channels}** Discord channels are linked"
-
-
-class WormholeChannel:
- def __init__(self, name: str, channelID: int, guildID: int, perms: str):
- self.wh = name
- self.channelID = channelID
- self.guildID = guildID
- self.perms = perms
-
- def to_str(self) -> str:
- """Transform the Channel to a human-readable string"""
- perms = "Write and Read" if self.perms == "wr" else "Read" if self.perms == "r" else "Write"
- return f"Channel: <#{self.channelID}>\n┗━▷ Linked to **{self.wh}** - Permissions: *{perms}*"
-
-
-class Wormholes(commands.Cog):
-
- def __init__(self, bot: Gunibot):
- self.bot = bot
- self.file = "wormhole"
-
- def db_get_wormholes(self) -> List[Wormhole]:
- """Get every wormhole"""
- query = 'SELECT rowid, * FROM wormhole_list'
- wormholes = self.bot.db_query(query, (), astuple=True)
- # comes as: (rowid, name, privacy)
- res: List[Wormhole] = list()
- for row in wormholes:
- query = "SELECT rowid, * FROM wormhole_admin WHERE name = ?"
- owners = self.bot.db_query(query, (row[1],), astuple=True)
- # come as: (rowid, name, admin)
- owner_list: List[int] = []
- for o in owners:
- owner_list.append(o[2])
- query = "SELECT * FROM wormhole_channel WHERE name = ?"
- channels = len(self.bot.db_query(query, (row[1],), astuple=True))
- res.append(Wormhole(*row[1:3], owner_list, self.bot, channels))
- res[-1].id = row[0]
- return res if len(res) > 0 else None
-
- def db_get_channels(self, guildID: int):
- """Get every channel linked to a wormhole in this channel"""
- query = "SELECT rowid, * FROM wormhole_channel WHERE guildID = ?"
- channels = self.bot.db_query(query, (guildID,), astuple=True)
- # come as: (rowid, name, channelID, guildID, type, webhookID, webhookTOKEN)
- res: List[WormholeChannel] = []
- for row in channels:
- res.append(WormholeChannel(*row[1:5]))
- res[-1].id = row[0]
- return res if len(res) > 0 else None
-
- def check_is_admin(self, wormhole: str, user: int):
- """Check if the provided user is an admin of the provided wormhole"""
- query = "SELECT 1 FROM wormhole_admin WHERE name = ? AND admin = ?"
- query_res = self.bot.db_query(query, (wormhole, user))
- return len(query_res) > 0
-
- def check_wh_exists(self, wormhole: str):
- """Check if a wormhole already exist with the provided name"""
- query = "SELECT 1 FROM wormhole_list WHERE name = ?"
- query_res = self.bot.db_query(query, (wormhole,), astuple=True)
- # comes as: (name, privacy, webhook_name, webhook_pp_guild)
- return len(query_res) > 0
-
- @commands.Cog.listener(name = "on_message_delete")
- async def on_message_delete(self, message):
- """Executed every time a message is deleted"""
- query = "SELECT name, type FROM wormhole_channel WHERE channelID = ?"
- wh_channel = self.bot.db_query(query, (message.channel.id,), astuple=True, fetchone=True)
- # come as: (name, type)
- if len(wh_channel) == 0: return # Check if there is a wormhole linked to the current channel
- if "w" not in wh_channel[1]: return # Check if the current channel as Write permission
- wh_name = wh_channel[0]
- query = "SELECT * FROM wormhole_channel WHERE name = ? AND type LIKE '%r%' AND NOT channelID = ?"
- wh_targets = self.bot.db_query(query, (wh_name, message.channel.id), astuple=True)
- # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
- query = "SELECT webhook_name, webhook_pp FROM wormhole_list WHERE name = ?"
- wormhole = self.bot.db_query(query, (wh_name,), astuple=True, fetchone=True)
- # come as: (webhook_name, webhook_pp)
- async with ClientSession() as session:
- for row in wh_targets:
- # We're starting to send the message in all the channels linked to that wormhole
- channel: discord.TextChannel = self.bot.get_channel(row[1])
- if channel:
- webhook = discord.Webhook.partial(row[4], row[5], session=session)
- oldmessage = await get_corresponding_answer(channel, message)
- await webhook.delete_message(oldmessage.id)
-
- @commands.Cog.listener(name = "on_message_edit")
- async def on_message_edit(self, message, newmessage):
- """Executed every time a message is edited"""
- if message.author.bot or "wormhole unlink" in message.content or "wh unlink" in message.content:
- return
- query = "SELECT name, type FROM wormhole_channel WHERE channelID = ?"
- wh_channel = self.bot.db_query(query, (message.channel.id,), astuple=True, fetchone=True)
- # come as: (name, type)
- if len(wh_channel) == 0: return # Check if there is a wormhole linked to the current channel
- if "w" not in wh_channel[1]: return # Check if the current channel as Write permission
- wh_name = wh_channel[0]
- query = "SELECT * FROM wormhole_channel WHERE name = ? AND type LIKE '%r%' AND NOT channelID = ?"
- wh_targets = self.bot.db_query(query, (wh_name, message.channel.id), astuple=True)
- # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
- query = "SELECT webhook_name, webhook_pp FROM wormhole_list WHERE name = ?"
- wormhole = self.bot.db_query(query, (wh_name,), astuple=True, fetchone=True)
- # come as: (webhook_name, webhook_pp)
- async with ClientSession() as session:
- for row in wh_targets:
- # We're starting to send the message in all the channels linked to that wormhole
- channel: discord.TextChannel = self.bot.get_channel(row[1])
- if channel:
- webhook = discord.Webhook.partial(row[4], row[5], session=session)
- embed_reply = None
- oldmessage = await get_corresponding_answer(channel, message)
- await webhook.edit_message(oldmessage.id, content=newmessage.content, embeds=newmessage.embeds, files=newmessage.attachments, allowed_mentions=None)
-
-
- @commands.Cog.listener()
- async def on_message(self, message: discord.Message):
- """Executed every time a message is sent"""
- if message.author.bot or "wormhole unlink" in message.content or "wh unlink" in message.content:
- return
- query = "SELECT name, type FROM wormhole_channel WHERE channelID = ?"
- wh_channel = self.bot.db_query(query, (message.channel.id,), astuple=True, fetchone=True)
- # come as: (name, type)
- if len(wh_channel) == 0: # Check if there is a wormhole linked to the current channel
- return
- if "w" not in wh_channel[1]: # Check if the current channel as Write permissions
- return
- wh_name = wh_channel[0]
- query = "SELECT * FROM wormhole_channel WHERE name = ? AND type LIKE '%r%' AND NOT channelID = ?"
- wh_targets = self.bot.db_query(query, (wh_name, message.channel.id), astuple=True)
- # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
- query = "SELECT webhook_name, webhook_pp FROM wormhole_list WHERE name = ?"
- wormhole = self.bot.db_query(query, (wh_name,), astuple=True, fetchone=True)
- # come as: (webhook_name, webhook_pp)
- async with ClientSession() as session:
- for row in wh_targets:
- # We're starting to send the message in all the channels linked to that wormhole
- channel: discord.TextChannel = self.bot.get_channel(row[1])
- if channel:
- webhook = discord.Webhook.partial(row[4], row[5], session=session)
- embed_reply = None
- if message.reference is not None:
- reply = await message.channel.fetch_message(message.reference.message_id)
- reply = await get_corresponding_answer(channel, reply)
- if reply is None:
- embed = discord.Embed(
- description= await self.bot._(message.guild.id, "wormhole.reply_notfound"), #"https://gunivers.net"), #
- colour=0x2f3136 #2F3136
- )
- else:
- content = reply.content
- content = content.replace("\n"," ")
- if len(content) > 80: content = content[:80] + "..."
- embed = discord.Embed(
- description= await self.bot._(message.guild.id, "wormhole.reply_to", link = reply.jump_url), #"https://gunivers.net"), #
- colour=0x2f3136 #2F3136
- ).set_footer(text= content, icon_url=reply.author.display_avatar)
- username = wormhole[0].replace("{user}", message.author.name, 10).replace("{guild}", message.guild.name, 10).replace("{channel}", message.channel.name, 10)
- await sendMessage(message, webhook, wormhole[0], wormhole[1], embed_reply)
-
-
- @commands.group(name="wormhole", aliases=["wh"])
- @commands.guild_only()
- @commands.cooldown(2, 15, commands.BucketType.channel)
- async def wormhole(self, ctx: MyContext):
- """Connect 2 points through space-time (or 2 text channels if you prefer)"""
- if ctx.subcommand_passed is None:
- await ctx.send_help("wormhole")
-
-
- @wormhole.command(name="add")
- async def add(self, ctx: MyContext, name: str, privacy: bool = True, webhook_name: str = "{user}",
- webhook_pp_guild: bool = False):
- """Create a wormhole
- webhook_name is for how names will be displayed:
- for example: "{user} - {guild}"
- will display "fantomitechno - Gunivers"
- ⚠️ The " are required if you want spaces in your webhook name
- Available variables are {user}, {guild} and {channel}
- webhook_pp_guild is for which avatar will be the profile picture of the webhook
- if True it will be the Guild from where it comes
- and if False it will be the User who sent the message
- """
- if self.check_wh_exists(name):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.already-exists", name=name))
- return
- query = "INSERT INTO wormhole_list (name, privacy, webhook_name, webhook_pp) VALUES (?, ?, ?, ?)"
- self.bot.db_query(query, (name, privacy, webhook_name, webhook_pp_guild))
- query = "INSERT INTO wormhole_admin (name, admin) VALUES (?,?)"
- self.bot.db_query(query, (name, ctx.author.id))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.wormhole-created"))
-
- @wormhole.command(name="link")
- @commands.check(checks.is_server_manager)
- async def link(self, ctx: MyContext, wormhole: str, perms: PermissionType = PermissionType("wr")):
- """Link the current channel to a wormhole
- Permissions are Write and/or Read, defined by their first letter
- Examples:
- - a channel with the permissions 'wr' can Send and Receive messages from the wormhole
- - a channel with 'r' can only receive
- """
- query = "SELECT * FROM wormhole_channel WHERE channelID = ?"
- row = self.bot.db_query(query, (ctx.channel.id,), fetchone=True)
- if len(row) != 0:
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.already-linked", c=ctx.channel))
- return
- if not self.check_wh_exists(wormhole):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- else:
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "INSERT INTO wormhole_channel (name, channelID, guildID, type, webhookID, webhookTOKEN) VALUES (?, ?, ?, ?, ?, ?)"
- webhook: discord.Webhook = await ctx.channel.create_webhook(name=wormhole)
- self.bot.db_query(query, (wormhole, ctx.channel.id, ctx.guild.id, perms.name, webhook.id, webhook.token))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.channel-linked"))
-
- @wormhole.command(name="unlink")
- @commands.check(checks.is_server_manager)
- async def unlink(self, ctx: MyContext):
- """Unlink the current channel to a wormhole"""
- query = "SELECT * FROM wormhole_channel WHERE channelID = ?"
- wh_channel = self.bot.db_query(query, (ctx.channel.id,), astuple=True, fetchone=True)
- # comes as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
- if len(wh_channel) == 0:
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-linked"))
- return
- query = "DELETE FROM wormhole_channel WHERE channelID = ? AND name = ?"
- async with ClientSession() as session:
- webhook = discord.Webhook.partial(wh_channel[4], wh_channel[5], session=session)
- await webhook.delete()
- self.bot.db_query(query, (wh_channel[0], ctx.channel.id))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.channel-unlinked"))
-
- @wormhole.command(name="remove", aliases=["delete"])
- async def remove(self, ctx: MyContext, wormhole: str):
- """Delete a wormhole"""
- if not self.check_wh_exists(wormhole):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- return
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "DELETE FROM wormhole_channel WHERE name = ?"
- self.bot.db_query(query, (wormhole,))
- query = "DELETE FROM wormhole_admin WHERE name = ?"
- self.bot.db_query(query, (wormhole,))
- query = "DELETE FROM wormhole_list WHERE name = ?"
- self.bot.db_query(query, (wormhole,))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.wormhole-deleted"))
-
- @wormhole.group(name="modify", aliases=["edit"])
- async def modify(self, ctx: MyContext):
- """Edit a wormhole"""
- if ctx.subcommand_passed is None:
- await ctx.send_help("wormhole modify")
-
- @modify.command(name="privacy")
- async def modify_privacy(self, ctx: MyContext, wormhole: str, privacy: str):
- """Edit the privacy of a wormhole
- Options for privacy are "public" and "private" """
- if privacy.lower() not in ["public", "private"]:
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-privacy"))
- return
- if not self.check_wh_exists(wormhole):
- return await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "UPDATE wormhole_list SET privacy = ? WHERE name = ?"
- private = privacy.lower() == "private"
- self.bot.db_query(query, (private, wormhole))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.modified"))
-
- @modify.command(name="webhook_name")
- async def modify_webhook_name(self, ctx: MyContext, wormhole: str, *, webhook_name: str):
- """webhook_name is for how names will be displayed:
- for example: "{user} - {guild}"
- will display "fantomitechno - Gunivers"
- Available variables are {user}, {guild} and {channel}"""
- if not self.check_wh_exists(wormhole):
- return await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "UPDATE wormhole_list SET webhook_name = ? WHERE name = ?"
- self.bot.db_query(query, (webhook_name, wormhole))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.modified"))
-
- @modify.command(name="webhook_pp")
- async def modify_webhook_pp(self, ctx: MyContext, wormhole: str, webhook_pp: bool):
- """webhook_pp_guild is for which avatar will be the profile picture of the webhook
- if True it will be the Guild from where it comes
- and if False it will be the User who sent the message"""
- if not self.check_wh_exists(wormhole):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- return
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "UPDATE wormhole_list SET webhook_pp = ? WHERE name = ?"
- self.bot.db_query(query, (webhook_pp, wormhole))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.modified"))
-
- @wormhole.group(name="admin")
- async def admin(self, ctx: MyContext):
- """Add or remove Wormhole Admins"""
- if ctx.subcommand_passed is None:
- await ctx.send_help("wormhole admin")
-
- @admin.command(name="add")
- async def admin_add(self, ctx: MyContext, wormhole: str, user: discord.User):
- """Add a user as a wormhole admin"""
- if not self.check_wh_exists(wormhole):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- return
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "SELECT 1 FROM wormhole_admin WHERE name = ? AND admin = ?"
- isAlready = len(self.bot.db_query(query, (wormhole, user.id))) > 0
- if not isAlready:
- query = "INSERT INTO wormhole_admin (name, admin) VALUES (?, ?)"
- self.bot.db_query(query, (wormhole, user.id))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.admin-added"))
- else:
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.already-admin", user=user.name))
-
- @admin.command(name="remove", aliases=['revoke'])
- async def admin_remove(self, ctx: MyContext, wormhole: str, user: discord.User):
- """Revoke an admin of a wormhole"""
- if not self.check_wh_exists(wormhole):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-exists", name=wormhole))
- return
- if not self.check_is_admin(wormhole, ctx.author.id):
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
- return
- query = "SELECT 1 FROM wormhole_admin WHERE name = ? AND admin = ?"
- isAlready = len(self.bot.db_query(query, (wormhole, user.id))) > 0
- if isAlready:
- query = "DELETE FROM wormhole_admin WHERE admin = ? AND name = ?"
- self.bot.db_query(query, (user.id, wormhole))
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.admin-removed"))
- else:
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin", user=user.name))
-
- @wormhole.group(name="list")
- async def list(self, ctx: MyContext):
- """Get a list of available wormholes or channels"""
- if ctx.subcommand_passed is None:
- await ctx.send_help("wormhole list")
-
- @list.command(name="wormhole", aliases=["wh"])
- async def list_wh(self, ctx: MyContext):
- """List all wormholes"""
- wormholes = self.db_get_wormholes()
- if not wormholes: # we can't send an empty list
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.no-wormhole", p=ctx.prefix))
- return
- txt = "\n".join([w.to_str() for w in wormholes])
- await ctx.send(txt)
-
- @list.command(name="channel")
- async def list_channel(self, ctx: MyContext):
- """List all channels linked to a Wormhole in the current server"""
- channels = self.db_get_channels(ctx.guild.id)
- if not channels: # we can't send an empty list
- await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.no-channels", p=ctx.prefix))
- return
- txt = "\n".join([c.to_str() for c in channels])
- await ctx.send(txt)
-
-
-async def setup(bot):
- await bot.add_cog(Wormholes(bot))
diff --git a/plugins/wormhole/credits.md b/plugins/wormhole/credits.md
new file mode 100644
index 00000000..f33e1966
--- /dev/null
+++ b/plugins/wormhole/credits.md
@@ -0,0 +1,11 @@
+Copyright © fantomitechno 2021
+Copyright © Leirof 2021 - 2022
+Copyright © ZRunner 2021
+Copyright © Theaustudio 2021
+Copyright © ascpial 2021 - 2023
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/wormhole/data/model.sql b/plugins/wormhole/data/model.sql
index 7c7e09d3..71189025 100644
--- a/plugins/wormhole/data/model.sql
+++ b/plugins/wormhole/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `wormhole_list` (
`name` TEXT PRIMARY KEY NOT NULL,
`privacy` BOOLEAN NOT NULL DEFAULT 0,
diff --git a/plugins/wormhole/docs/user_documentation.md b/plugins/wormhole/docs/user_documentation.md
index 70d399f9..ed5c5920 100644
--- a/plugins/wormhole/docs/user_documentation.md
+++ b/plugins/wormhole/docs/user_documentation.md
@@ -1,3 +1,11 @@
-# Wormhole
+
+
+# 🌀 Wormhole
+
+Nothing here... for the moment.
-Nothing here...
\ No newline at end of file
diff --git a/plugins/wormhole/langs/en.yml b/plugins/wormhole/langs/en.yml
index cf79ef11..ef629153 100644
--- a/plugins/wormhole/langs/en.yml
+++ b/plugins/wormhole/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
wormhole:
error:
diff --git a/plugins/wormhole/langs/fr.yml b/plugins/wormhole/langs/fr.yml
index cba63487..946df832 100644
--- a/plugins/wormhole/langs/fr.yml
+++ b/plugins/wormhole/langs/fr.yml
@@ -1,9 +1,14 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
wormhole:
error:
already-exists: "Le trou de ver %{name} existe déjà"
already-linked: "Le salon %{c} est déjà lié à ce trou de ver"
- not-exists: "Le trou de ver %{name} n'exisste pas"
+ not-exists: "Le trou de ver %{name} n'existe pas"
not-linked: "Le salon %{c} n'est lié à aucun trou de ver"
not-admin: "Vous devez être administrateur de ce trou de ver"
not-privacy: L'option de visibilité doit être définie sur "public" ou "private"
diff --git a/plugins/wormhole/wormhole.py b/plugins/wormhole/wormhole.py
new file mode 100644
index 00000000..0afc6275
--- /dev/null
+++ b/plugins/wormhole/wormhole.py
@@ -0,0 +1,809 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import difflib
+from aiohttp import ClientSession
+from utils import Gunibot, MyContext
+from bot import checks
+from typing import List, Union
+import discord
+from discord.ext import commands
+
+import sys
+
+sys.path.append("./bot")
+
+from LRFutils import logs
+
+# Check if a message is similar to another one with 80% similarity
+
+
+def similar(msg1, msg2):
+ return difflib.SequenceMatcher(a=msg1.lower(), b=msg2.lower()).ratio() > 0.8
+
+
+# Get the corresponding answered message in other channels
+async def get_corresponding_answer(
+ channel: discord.abc.Messageable, message: discord.Message
+) -> discord.Message:
+ date = message.created_at
+ async for msg in channel.history(limit=20, after=date, oldest_first=True):
+ if similar(message.content, msg.content):
+ return msg
+ async for msg in channel.history(limit=20, before=date, oldest_first=False):
+ if similar(message.content, msg.content):
+ return msg
+ return None
+
+
+async def sendMessage(
+ msg: discord.Message,
+ webhook: discord.Webhook,
+ username: str,
+ pp_guild: bool,
+ embed_reply: discord.Embed = None,
+ thread: discord.Thread = None
+):
+ files = [await x.to_file() for x in msg.attachments]
+ # grab mentions from the source message
+ mentions = discord.AllowedMentions(
+ everyone=msg.mention_everyone, users=msg.mentions, roles=msg.role_mentions
+ )
+ username = (
+ username.replace("{user}", msg.author.name, 10)
+ .replace("{guild}", msg.guild.name, 10)
+ .replace("{channel}", msg.channel.name, 10)
+ )
+ avatar_url = msg.author.display_avatar
+ if pp_guild:
+ avatar_url = msg.guild.icon_url
+
+ embeds = [embed for embed in msg.embeds if embed.type == "rich"]
+ if embed_reply and embeds:
+ while len(embeds) >= 10:
+ embeds.pop()
+ embeds.append(embed_reply)
+ elif embed_reply:
+ embeds = [embed_reply]
+
+ if thread is None:
+ new_msg: discord.WebhookMessage = await webhook.send(
+ content=msg.content,
+ files=files,
+ embeds=embeds,
+ avatar_url=avatar_url,
+ username=username,
+ allowed_mentions=discord.AllowedMentions.none(),
+ wait=True,
+ )
+ else:
+ new_msg: discord.WebhookMessage = await webhook.send(
+ content=msg.content,
+ files=files,
+ embeds=embeds,
+ thread = thread,
+ avatar_url=avatar_url,
+ username=username,
+ allowed_mentions=discord.AllowedMentions.none(),
+ wait=True,
+ )
+ # edit the message to include mentions without notifications
+ if mentions.roles or mentions.users or mentions.everyone:
+ await new_msg.edit(allowed_mentions=mentions)
+
+
+class PermissionType(commands.Converter):
+ types = ["w", "r", "wr"]
+
+ def __init__(self, action: Union[str, int] = None):
+ if isinstance(action, str):
+ self.type = self.types.index(action)
+ elif isinstance(action, int):
+ self.type = action
+ else:
+ return
+ self.name = self.types[self.type]
+
+ async def convert(self, ctx: commands.Context, argument: str):
+ if argument in self.types:
+ return PermissionType(argument)
+ raise commands.errors.BadArgument("Unknown permission type")
+
+
+class Wormhole:
+ def __init__(
+ self, name: str, privacy: bool, owners: List[int], bot: Gunibot, channels: int
+ ):
+ self.bot = bot
+ self.name = name
+ self.privacy = privacy
+ self.owners = owners
+ self.channels = channels
+
+ def to_str(self) -> str:
+ """Transform the Wormhole to a human-readable string"""
+ private = self.privacy == 1
+ owners: List[str] = []
+ for o in self.owners:
+ user = self.bot.get_user(o)
+ owners.append(user.name if user else "Unknown user")
+ return f"Wormhole: {self.name}\n┗━▷ Private: {private} - Admins: {', '.join(owners)} - **{self.channels}** Discord channels are linked"
+
+
+class WormholeChannel:
+ def __init__(self, name: str, channelID: int, guildID: int, perms: str):
+ self.wh = name
+ self.channelID = channelID
+ self.guildID = guildID
+ self.perms = perms
+
+ def to_str(self) -> str:
+ """Transform the Channel to a human-readable string"""
+ perms = (
+ "Write and Read"
+ if self.perms == "wr"
+ else "Read"
+ if self.perms == "r"
+ else "Write"
+ )
+ return f"Channel: <#{self.channelID}>\n┗━▷ Linked to **{self.wh}** - Permissions: *{perms}*"
+
+
+class Wormholes(commands.Cog):
+ def __init__(self, bot: Gunibot):
+ self.bot = bot
+ self.file = "wormhole"
+
+ def db_get_wormholes(self) -> List[Wormhole]:
+ """Get every wormhole"""
+ query = "SELECT rowid, * FROM wormhole_list"
+ wormholes = self.bot.db_query(query, (), astuple=True)
+ # comes as: (rowid, name, privacy)
+ res: List[Wormhole] = list()
+ for row in wormholes:
+ query = "SELECT rowid, * FROM wormhole_admin WHERE name = ?"
+ owners = self.bot.db_query(query, (row[1],), astuple=True)
+ # come as: (rowid, name, admin)
+ owner_list: List[int] = []
+ for o in owners:
+ owner_list.append(o[2])
+ query = "SELECT * FROM wormhole_channel WHERE name = ?"
+ channels = len(self.bot.db_query(query, (row[1],), astuple=True))
+ res.append(Wormhole(*row[1:3], owner_list, self.bot, channels))
+ res[-1].id = row[0]
+ return res if len(res) > 0 else None
+
+ def db_get_channels(self, guildID: int):
+ """Get every channel linked to a wormhole in this channel"""
+ query = "SELECT rowid, * FROM wormhole_channel WHERE guildID = ?"
+ channels = self.bot.db_query(query, (guildID,), astuple=True)
+ # come as: (rowid, name, channelID, guildID, type, webhookID,
+ # webhookTOKEN)
+ res: List[WormholeChannel] = []
+ for row in channels:
+ res.append(WormholeChannel(*row[1:5]))
+ res[-1].id = row[0]
+ return res if len(res) > 0 else None
+
+ def check_is_admin(self, wormhole: str, user: int):
+ """Check if the provided user is an admin of the provided wormhole"""
+ query = "SELECT 1 FROM wormhole_admin WHERE name = ? AND admin = ?"
+ query_res = self.bot.db_query(query, (wormhole, user))
+ return len(query_res) > 0
+
+ def check_wh_exists(self, wormhole: str):
+ """Check if a wormhole already exist with the provided name"""
+ query = "SELECT 1 FROM wormhole_list WHERE name = ?"
+ query_res = self.bot.db_query(query, (wormhole,), astuple=True)
+ # comes as: (name, privacy, webhook_name, webhook_pp_guild)
+ return len(query_res) > 0
+
+ async def update_webhook(
+ self,
+ channel: Union[discord.TextChannel, discord.Thread],
+ wormhole_name: str,
+ ) -> discord.Webhook:
+ """Fetchs a webhook for the specified channel, updates the linked
+ channels and returns the webhook.
+ """
+ if isinstance(channel, discord.Thread):
+ new_webhook: discord.Webhook = await channel.parent.create_webhook(
+ name=wormhole_name,
+ )
+ else:
+ new_webhook: discord.Webhook = await channel.create_webhook(
+ name=wormhole_name,
+ )
+ query = "UPDATE wormhole_channel SET webhookID=?, webhookTOKEN=? WHERE name=? AND channelID=?;"
+
+ self.bot.db_query(
+ query,
+ (
+ new_webhook.id,
+ new_webhook.token,
+ wormhole_name,
+ channel.id,
+ )
+ ) # update the webhook in the database
+
+ return new_webhook
+
+ @commands.Cog.listener(name="on_message_delete")
+ async def on_message_delete(self, message):
+ """Executed every time a message is deleted"""
+ query = "SELECT name, type FROM wormhole_channel WHERE channelID = ?"
+ wh_channel = self.bot.db_query(
+ query, (message.channel.id,), astuple=True, fetchone=True
+ )
+ # come as: (name, type)
+ if len(wh_channel) == 0:
+ return # Check if there is a wormhole linked to the current channel
+ if "w" not in wh_channel[1]:
+ return # Check if the current channel as Write permission
+ wh_name = wh_channel[0]
+ query = "SELECT * FROM wormhole_channel WHERE name = ? AND type LIKE '%r%' AND NOT channelID = ?"
+ wh_targets = self.bot.db_query(
+ query, (wh_name, message.channel.id), astuple=True
+ )
+ # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
+ query = "SELECT webhook_name, webhook_pp FROM wormhole_list WHERE name = ?"
+ wormhole = self.bot.db_query(query, (wh_name,), astuple=True, fetchone=True)
+ # come as: (webhook_name, webhook_pp)
+ async with ClientSession() as session:
+ for row in wh_targets:
+ # We're starting to send the message in all the channels linked
+ # to that wormhole
+ channel: discord.abc.Messageable = self.bot.get_channel(row[1])
+ if channel:
+ webhook = discord.Webhook.partial(row[4], row[5], session=session)
+ oldmessage = await get_corresponding_answer(channel, message)
+ if oldmessage:
+ # The webhook try to delete the message (will work only if the message belong to the webhook)
+ try:
+ await webhook.delete_message(oldmessage.id)
+ except (discord.errors.NotFound, discord.errors.Forbidden):
+ pass
+ try :
+ await oldmessage.delete()
+ except (discord.errors.NotFound, discord.errors.Forbidden):
+ pass
+
+ @commands.Cog.listener(name="on_message_edit")
+ async def on_message_edit(self, message:discord.Message, newmessage:discord.Message):
+ """Executed every time a message is edited"""
+ if (
+ "wormhole unlink" in message.content
+ or "wh unlink" in message.content
+ ):
+ return
+ query = "SELECT name, type FROM wormhole_channel WHERE channelID = ?"
+ wh_channel = self.bot.db_query(
+ query, (message.channel.id,), astuple=True, fetchone=True
+ )
+ # come as: (name, type)
+ if len(wh_channel) == 0:
+ return # Check if there is a wormhole linked to the current channel
+ if "w" not in wh_channel[1]:
+ return # Check if the current channel as Write permission
+
+ wh_name = wh_channel[0]
+
+ # If the sender is a webhook used by the wormhole, then we don't want to send the message
+ query = "SELECT * FROM wormhole_channel WHERE name = ? AND channelID = ?"
+ wh_local = self.bot.db_query(query, (wh_name, message.channel.id), astuple=True)[0]
+ # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
+ if message.author.id == wh_local[4]: # sender id is the webhook used here
+ return
+
+ query = "SELECT * FROM wormhole_channel WHERE name = ? AND type LIKE '%r%' AND NOT channelID = ?"
+ wh_targets = self.bot.db_query(
+ query, (wh_name, message.channel.id), astuple=True
+ )
+ # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
+ query = "SELECT webhook_name, webhook_pp FROM wormhole_list WHERE name = ?"
+ wormhole = self.bot.db_query(query, (wh_name,), astuple=True, fetchone=True)
+ # come as: (webhook_name, webhook_pp)
+ async with ClientSession() as session:
+ for row in wh_targets:
+ # We're starting to send the message in all the channels linked
+ # to that wormhole
+ channel: discord.abc.Messageable = self.bot.get_channel(row[1])
+ embeds = newmessage.embeds.copy()
+
+ if channel:
+ webhook = discord.Webhook.partial(row[4], row[5], session=session)
+
+ if message.reference is not None:
+ reply = await message.channel.fetch_message(
+ message.reference.message_id
+ )
+ reply = await get_corresponding_answer(channel, reply)
+ if reply is None:
+ embeds.append(
+ discord.Embed(
+ # "https://gunivers.net"), #
+ description=await self.bot._(
+ message.guild.id, "wormhole.reply_notfound"
+ ),
+ colour=0x2F3136, # 2F3136
+ )
+ )
+ else:
+ content = reply.content.replace("\n", " ")
+ if len(content) > 80:
+ content = content[:80] + "..."
+ embeds.append(
+ discord.Embed(
+ # "https://gunivers.net"), #
+ description=await self.bot._(
+ message.guild.id,
+ "wormhole.reply_to",
+ link=reply.jump_url,
+ ),
+ colour=0x2F3136, # 2F3136
+ ).set_footer(
+ text=content, icon_url=reply.author.display_avatar
+ )
+ )
+
+ oldmessage = await get_corresponding_answer(channel, message)
+
+ try:
+ if isinstance(channel, discord.Thread):
+ await webhook.edit_message(
+ oldmessage.id,
+ content=newmessage.content,
+ embeds=embeds,
+ allowed_mentions=None,
+ thread = channel,
+ )
+ else:
+ await webhook.edit_message(
+ oldmessage.id,
+ content=newmessage.content,
+ embeds=embeds,
+ allowed_mentions=None,
+ )
+ except discord.NotFound: # the webhook has been deleted
+ logs.info(
+ f"The webhook for channel {row[1]} for wormhole {wh_name} has been deleted and a message has not been edited."
+ )
+ except discord.Forbidden: # the webhook has changed, should not be possible due to checks before
+ logs.info(
+ f"The webhook for channel {row[1]} for wormhole {wh_name} has changed and a message has not been edited."
+ )
+
+
+ @commands.Cog.listener()
+ async def on_message(self, message: discord.Message):
+ """Executed every time a message is sent"""
+ if ("wormhole unlink" in message.content
+ or "wh unlink" in message.content
+ ):
+ return
+
+ query = "SELECT name, type FROM wormhole_channel WHERE channelID = ?"
+ wh_channel = self.bot.db_query(
+ query, (message.channel.id,), astuple=True, fetchone=True
+ )
+ # come as: (name, type)
+ if (
+ len(wh_channel) == 0
+ ): # Check if there is a wormhole linked to the current channel
+ return
+ # Check if the current channel as Write permissions
+ if "w" not in wh_channel[1]:
+ return
+
+ # Getting wormhole name
+ wh_name = wh_channel[0]
+
+ # If the sender is a webhook used by the wormhole, then we don't want to send the message
+ query = "SELECT * FROM wormhole_channel WHERE name = ? AND channelID = ?"
+ wh_local = self.bot.db_query(query, (wh_name, message.channel.id), astuple=True)[0]
+ # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
+ if message.author.id == wh_local[4]: # sender id is the webhook used here
+ return
+
+ # Getting all the other channels linked to the wormhole
+ query = "SELECT * FROM wormhole_channel WHERE name = ? AND type LIKE '%r%' AND NOT channelID = ?"
+ wh_targets = self.bot.db_query(
+ query, (wh_name, message.channel.id), astuple=True
+ )
+ # come as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
+ query = "SELECT webhook_name, webhook_pp FROM wormhole_list WHERE name = ?"
+ wormhole = self.bot.db_query(query, (wh_name,), astuple=True, fetchone=True)
+ # come as: (webhook_name, webhook_pp)
+
+ async with ClientSession() as session:
+
+ # We're starting to send the message in all the channels linked to that wormhole
+ for connected_channel in wh_targets:
+ channel: Union[discord.TextChannel, discord.Thread] = self.bot.get_channel(connected_channel[1])
+
+ if channel:
+ # Get the webhook associated to the wormhole
+ webhook = discord.Webhook.partial(connected_channel[4], connected_channel[5], session=session)
+
+ embed_reply = None
+ if message.reference is not None:
+ reply = await message.channel.fetch_message(
+ message.reference.message_id
+ )
+ reply = await get_corresponding_answer(channel, reply)
+ if reply is None:
+ embed_reply = discord.Embed(
+ # "https://gunivers.net"), #
+ description=await self.bot._(
+ message.guild.id, "wormhole.reply_notfound"
+ ),
+ colour=0x2F3136, # 2F3136
+ )
+ else:
+ content = reply.content.replace("\n", " ")
+ if len(content) > 80:
+ content = content[:80] + "..."
+ embed_reply = discord.Embed(
+ # "https://gunivers.net"), #
+ description=await self.bot._(
+ message.guild.id,
+ "wormhole.reply_to",
+ link=reply.jump_url,
+ ),
+ colour=0x2F3136, # 2F3136
+ ).set_footer(
+ text=content, icon_url=reply.author.display_avatar
+ )
+
+ try:
+ await sendMessage(
+ message,
+ webhook,
+ wormhole[0],
+ wormhole[1],
+ embed_reply,
+ thread = channel if isinstance(channel, discord.Thread) else None,
+ )
+ except discord.NotFound: # the webhook has been deleted
+ new_webhook = await self.update_webhook(
+ channel,
+ wh_name,
+ )
+
+ await sendMessage(
+ message,
+ new_webhook,
+ wormhole[0],
+ wormhole[1],
+ embed_reply,
+ thread=channel if isinstance(channel, discord.Thread) else None,
+ ) # send the message again
+
+ @commands.group(name="wormhole", aliases=["wh"])
+ @commands.guild_only()
+ @commands.cooldown(2, 15, commands.BucketType.channel)
+ async def wormhole(self, ctx: MyContext):
+ """Connect 2 points through space-time (or 2 text channels if you prefer)"""
+ if ctx.subcommand_passed is None:
+ await ctx.send_help("wormhole")
+
+ @wormhole.command(name="add")
+ async def add(
+ self,
+ ctx: MyContext,
+ name: str,
+ privacy: bool = True,
+ webhook_name: str = "{user}",
+ webhook_pp_guild: bool = False,
+ ):
+ """Create a wormhole
+ webhook_name is for how names will be displayed:
+ for example: "{user} - {guild}"
+ will display "fantomitechno - Gunivers"
+ ⚠️ The " are required if you want spaces in your webhook name
+ Available variables are {user}, {guild} and {channel}
+ webhook_pp_guild is for which avatar will be the profile picture of the webhook
+ if True it will be the Guild from where it comes
+ and if False it will be the User who sent the message
+ """
+ if self.check_wh_exists(name):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.already-exists", name=name
+ )
+ )
+ return
+ query = "INSERT INTO wormhole_list (name, privacy, webhook_name, webhook_pp) VALUES (?, ?, ?, ?)"
+ self.bot.db_query(query, (name, privacy, webhook_name, webhook_pp_guild))
+ query = "INSERT INTO wormhole_admin (name, admin) VALUES (?,?)"
+ self.bot.db_query(query, (name, ctx.author.id))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.success.wormhole-created")
+ )
+
+ @wormhole.command(name="link")
+ @commands.check(checks.is_server_manager)
+ async def link(
+ self,
+ ctx: MyContext,
+ wormhole: str,
+ perms: PermissionType = PermissionType("wr"),
+ ):
+ """Link the current channel to a wormhole
+ Permissions are Write and/or Read, defined by their first letter
+ Examples:
+ - a channel with the permissions 'wr' can Send and Receive messages from the wormhole
+ - a channel with 'r' can only receive
+ """
+ query = "SELECT * FROM wormhole_channel WHERE channelID = ?"
+ row = self.bot.db_query(query, (ctx.channel.id,), fetchone=True)
+ if len(row) != 0:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.already-linked", c=ctx.channel
+ )
+ )
+ return
+ if not self.check_wh_exists(wormhole):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ else:
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.error.not-admin")
+ )
+ return
+ query = "INSERT INTO wormhole_channel (name, channelID, guildID, type, webhookID, webhookTOKEN) VALUES (?, ?, ?, ?, ?, ?)"
+
+ if isinstance(ctx.channel, discord.Thread):
+ webhook: discord.Webhook = await ctx.channel.parent.create_webhook(name=wormhole)
+ else:
+ webhook: discord.Webhook = await ctx.channel.create_webhook(name=wormhole)
+
+ self.bot.db_query(
+ query,
+ (
+ wormhole,
+ ctx.channel.id,
+ ctx.guild.id,
+ perms.name,
+ webhook.id,
+ webhook.token,
+ ),
+ )
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.success.channel-linked")
+ )
+
+ @wormhole.command(name="unlink")
+ @commands.check(checks.is_server_manager)
+ async def unlink(self, ctx: MyContext):
+ """Unlink the current channel to a wormhole"""
+ query = "SELECT * FROM wormhole_channel WHERE channelID = ?"
+ wh_channel = self.bot.db_query(
+ query, (ctx.channel.id,), astuple=True, fetchone=True
+ )
+ # comes as: (name, channelID, guildID, type, webhookID, webhookTOKEN)
+ if len(wh_channel) == 0:
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-linked"))
+ return
+ query = "DELETE FROM wormhole_channel WHERE channelID = ? AND name = ?"
+ self.bot.db_query(query, (ctx.channel.id, wh_channel[0]))
+ async with ClientSession() as session:
+ webhook = discord.Webhook.partial(
+ wh_channel[4], wh_channel[5], session=session
+ )
+ await webhook.delete()
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.success.channel-unlinked")
+ )
+
+ @wormhole.command(name="remove", aliases=["delete"])
+ async def remove(self, ctx: MyContext, wormhole: str):
+ """Delete a wormhole"""
+ if not self.check_wh_exists(wormhole):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ return
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
+ return
+ query = "DELETE FROM wormhole_channel WHERE name = ?"
+ self.bot.db_query(query, (wormhole,))
+ query = "DELETE FROM wormhole_admin WHERE name = ?"
+ self.bot.db_query(query, (wormhole,))
+ query = "DELETE FROM wormhole_list WHERE name = ?"
+ self.bot.db_query(query, (wormhole,))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.success.wormhole-deleted")
+ )
+
+ @wormhole.group(name="modify", aliases=["edit"])
+ async def modify(self, ctx: MyContext):
+ """Edit a wormhole"""
+ if ctx.subcommand_passed is None:
+ await ctx.send_help("wormhole modify")
+
+ @modify.command(name="privacy")
+ async def modify_privacy(self, ctx: MyContext, wormhole: str, privacy: str):
+ """Edit the privacy of a wormhole
+ Options for privacy are "public" and "private" """
+ if privacy.lower() not in ["public", "private"]:
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-privacy"))
+ return
+ if not self.check_wh_exists(wormhole):
+ return await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
+ return
+ query = "UPDATE wormhole_list SET privacy = ? WHERE name = ?"
+ private = privacy.lower() == "private"
+ self.bot.db_query(query, (private, wormhole))
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.modified"))
+
+ @modify.command(name="webhook_name")
+ async def modify_webhook_name(
+ self, ctx: MyContext, wormhole: str, *, webhook_name: str
+ ):
+ """
+ Edit the name of the wormhole's webhook. Available variables:
+ - {guild}: name of the guild
+ - {channel}: name of the channel
+ - {user}: name of the user
+
+ For example: "!wh modify MyWH webhook_name {user} from {guild}"
+
+ If fantomitechno send a message in a Gunivers channel linked to the wormhole "MyWH", the other connected channels will see the message from a webhook called "fantomitechno from Gunivers".
+ """
+
+ if not self.check_wh_exists(wormhole):
+ return await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
+ return
+ query = "UPDATE wormhole_list SET webhook_name = ? WHERE name = ?"
+ self.bot.db_query(query, (webhook_name, wormhole))
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.modified"))
+
+ @modify.command(name="webhook_pp")
+ async def modify_webhook_pp(self, ctx: MyContext, wormhole: str, webhook_pp: bool):
+ """webhook_pp_guild is for which avatar will be the profile picture of the webhook
+ if True it will be the Guild from where it comes
+ and if False it will be the User who sent the message"""
+ if not self.check_wh_exists(wormhole):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ return
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
+ return
+ query = "UPDATE wormhole_list SET webhook_pp = ? WHERE name = ?"
+ self.bot.db_query(query, (webhook_pp, wormhole))
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.success.modified"))
+
+ @wormhole.group(name="admin")
+ async def admin(self, ctx: MyContext):
+ """Add or remove Wormhole Admins"""
+ if ctx.subcommand_passed is None:
+ await ctx.send_help("wormhole admin")
+
+ @admin.command(name="add")
+ async def admin_add(self, ctx: MyContext, wormhole: str, user: discord.User):
+ """Add a user as a wormhole admin"""
+ if not self.check_wh_exists(wormhole):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ return
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
+ return
+ query = "SELECT 1 FROM wormhole_admin WHERE name = ? AND admin = ?"
+ isAlready = len(self.bot.db_query(query, (wormhole, user.id))) > 0
+ if not isAlready:
+ query = "INSERT INTO wormhole_admin (name, admin) VALUES (?, ?)"
+ self.bot.db_query(query, (wormhole, user.id))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.success.admin-added")
+ )
+ else:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.already-admin", user=user.name
+ )
+ )
+
+ @admin.command(name="remove", aliases=["revoke"])
+ async def admin_remove(self, ctx: MyContext, wormhole: str, user: discord.User):
+ """Revoke an admin of a wormhole"""
+ if not self.check_wh_exists(wormhole):
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-exists", name=wormhole
+ )
+ )
+ return
+ if not self.check_is_admin(wormhole, ctx.author.id):
+ await ctx.send(await self.bot._(ctx.guild.id, "wormhole.error.not-admin"))
+ return
+ query = "SELECT 1 FROM wormhole_admin WHERE name = ? AND admin = ?"
+ isAlready = len(self.bot.db_query(query, (wormhole, user.id))) > 0
+ if isAlready:
+ query = "DELETE FROM wormhole_admin WHERE admin = ? AND name = ?"
+ self.bot.db_query(query, (user.id, wormhole))
+ await ctx.send(
+ await self.bot._(ctx.guild.id, "wormhole.success.admin-removed")
+ )
+ else:
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.not-admin", user=user.name
+ )
+ )
+
+ @wormhole.group(name="list")
+ async def list(self, ctx: MyContext):
+ """Get a list of available wormholes or channels"""
+ if ctx.subcommand_passed is None:
+ await ctx.send_help("wormhole list")
+
+ @list.command(name="wormhole", aliases=["wh"])
+ async def list_wh(self, ctx: MyContext):
+ """List all wormholes"""
+ wormholes = self.db_get_wormholes()
+ if not wormholes: # we can't send an empty list
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.no-wormhole", p=ctx.prefix
+ )
+ )
+ return
+ txt = "\n".join([w.to_str() for w in wormholes])
+ await ctx.send(txt)
+
+ @list.command(name="channel")
+ async def list_channel(self, ctx: MyContext):
+ """List all channels linked to a Wormhole in the current server"""
+ channels = self.db_get_channels(ctx.guild.id)
+ if not channels: # we can't send an empty list
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "wormhole.error.no-channels", p=ctx.prefix
+ )
+ )
+ return
+ txt = "\n".join([c.to_str() for c in channels])
+ await ctx.send(txt)
+
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(Wormholes(bot), icon="🌀")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
+
diff --git a/plugins/xp/config/options.json b/plugins/xp/config/options.json
index bc76437d..12f60c30 100644
--- a/plugins/xp/config/options.json
+++ b/plugins/xp/config/options.json
@@ -1,6 +1,6 @@
{
"enable_xp": {
- "default": true,
+ "default": false,
"type": "boolean",
"command": "enable_xp"
},
@@ -9,6 +9,11 @@
"type": "channels",
"command": "noxp_channels"
},
+ "xp_reduction": {
+ "default": false,
+ "type": "boolean",
+ "command": "xp_reduction"
+ },
"levelup_channel": {
"default": "any",
"type": "channels",
diff --git a/plugins/xp/credits.md b/plugins/xp/credits.md
new file mode 100644
index 00000000..c2cc6c24
--- /dev/null
+++ b/plugins/xp/credits.md
@@ -0,0 +1,9 @@
+Copyright © ZRunner 2021 - 2022
+Copyright © Leirof 2021 - 2022
+Copyright © ascpial 2021 - 2023
+Copyright © Aeris One 2022
+
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
diff --git a/plugins/xp/data/model.sql b/plugins/xp/data/model.sql
index 962d7aea..b5bd3eb5 100644
--- a/plugins/xp/data/model.sql
+++ b/plugins/xp/data/model.sql
@@ -1,3 +1,8 @@
+-- Ce programme est régi par la licence CeCILL soumise au droit français et
+-- respectant les principes de diffusion des logiciels libres. Vous pouvez
+-- utiliser, modifier et/ou redistribuer ce programme sous les conditions
+-- de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
CREATE TABLE IF NOT EXISTS `xp` (
`guild` BIGINT,
`userid` BIGINT NOT NULL,
diff --git a/plugins/xp/docs/user_documentation.md b/plugins/xp/docs/user_documentation.md
index 5e521487..4602f34b 100644
--- a/plugins/xp/docs/user_documentation.md
+++ b/plugins/xp/docs/user_documentation.md
@@ -1,3 +1,10 @@
-# XP
+
+# 🎖️ XP
+
+Nothing here... for the moment.
-Nothing here...
diff --git a/plugins/xp/langs/en.yml b/plugins/xp/langs/en.yml
index 78d8b94f..0fddc4af 100644
--- a/plugins/xp/langs/en.yml
+++ b/plugins/xp/langs/en.yml
@@ -1,3 +1,8 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
en:
xp:
card:
diff --git a/plugins/xp/langs/fr.yml b/plugins/xp/langs/fr.yml
index 3fccfde9..0cfc777a 100644
--- a/plugins/xp/langs/fr.yml
+++ b/plugins/xp/langs/fr.yml
@@ -1,5 +1,13 @@
+# Ce programme est régi par la licence CeCILL soumise au droit français et
+# respectant les principes de diffusion des logiciels libres. Vous pouvez
+# utiliser, modifier et/ou redistribuer ce programme sous les conditions
+# de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+
fr:
xp:
+ config:
+ enable_xp_reduction:
+ type_error: La valeur doit être un booléen ("True" ou "False")
card:
level: Niveau
rank: Rang
diff --git a/plugins/xp/bot/main.py b/plugins/xp/xp.py
similarity index 56%
rename from plugins/xp/bot/main.py
rename to plugins/xp/xp.py
index 0226ae52..76476c94 100644
--- a/plugins/xp/bot/main.py
+++ b/plugins/xp/xp.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import asyncio
import re
import string
@@ -8,12 +15,12 @@
import discord
import emoji
-from discord.ext import commands
+from discord.ext import commands, tasks
from utils import Gunibot, MyContext
+import distutils
class XP(commands.Cog):
-
def __init__(self, bot: Gunibot):
self.bot = bot
self.cooldown = 30
@@ -24,64 +31,98 @@ def __init__(self, bot: Gunibot):
self.cache: Dict[Dict[int, int]] = dict() # xp cache
self.levels = [0] # xp required per level
self.xp_channels_cache = dict() # no-xp channels
- self.embed_color = discord.Colour(0xffcf50)
- self.config_options = ['enable_xp', 'noxp_channels',
- 'levelup_channel', 'levelup_message','levelup_reaction','reaction_emoji']
+ self.embed_color = discord.Colour(0xFFCF50)
+ self.config_options = [
+ "enable_xp",
+ "noxp_channels",
+ "xp_reduction",
+ "levelup_channel",
+ "levelup_message",
+ "levelup_reaction",
+ "reaction_emoji",
+ ]
bot.get_command("config").add_command(self.config_enable_xp)
bot.get_command("config").add_command(self.config_noxp_channels)
+ bot.get_command("config").add_command(self.config_xp_reduction)
bot.get_command("config").add_command(self.config_levelup_channel)
bot.get_command("config").add_command(self.config_levelup_message)
bot.get_command("config").add_command(self.config_levelup_reaction)
bot.get_command("config").add_command(self.config_levelup_reaction_emoji)
+ self.xp_reduction.start()
+
@commands.command(name="enable_xp")
async def config_enable_xp(self, ctx: MyContext, value: bool):
"""Enable or disable the XP system in your server"""
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "enable_xp", value))
-
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "enable_xp", value)
+ )
+
@commands.command(name="noxp_channels")
- async def config_noxp_channels(self, ctx: MyContext, channels: commands.Greedy[discord.TextChannel]):
+ async def config_noxp_channels(
+ self, ctx: MyContext, channels: commands.Greedy[discord.abc.GuildChannel]
+ ):
"""Select in which channels your members should not get any xp"""
if len(channels) == 0:
channels = None
else:
channels = [channel.id for channel in channels]
x = await self.bot.sconfig.edit_config(ctx.guild.id, "noxp_channels", channels)
+ self.xp_channels_cache[ctx.guild.id] = channels if channels is not None else []
await ctx.send(x)
-
+
+ @commands.command(name="xp_reduction")
+ async def config_xp_reduction(self, ctx: MyContext, enabled:bool):
+ """Enable or disable the xp reduction system"""
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "xp_reduction", enabled)
+ )
+
@commands.command(name="levelup_channel")
async def config_levelup_channel(self, ctx: MyContext, *, channel):
"""Select in which channel the levelup messages should be sent
None for no levelup message, any for any channel"""
- if channel.lower() == 'none':
- channel = 'none'
- elif channel.lower() == 'any':
- channel = 'any'
+ if channel.lower() == "none":
+ channel = "none"
+ elif channel.lower() == "any":
+ channel = "any"
else:
channel = await commands.TextChannelConverter().convert(ctx, channel)
channel = channel.id
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "levelup_channel", channel))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "levelup_channel", channel)
+ )
self.xp_channels_cache.pop(ctx.guild.id, None)
-
+
@commands.command(name="levelup_message")
async def config_levelup_message(self, ctx: MyContext, *, message=None):
"""Message sent when a member reaches a new level
Use {level} for the new level, {user} for the user mention and {username} for the user name
Set to None to reset it"""
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "levelup_message", message))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "levelup_message", message)
+ )
@commands.command(name="levelup_reaction")
- async def config_levelup_reaction(self, ctx: MyContext, *, bool: bool=None):
+ async def config_levelup_reaction(self, ctx: MyContext, *, bool: bool = None):
"""If the bot add a reaction to the message or send a message
Set to True for the reaction, False for the message"""
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "levelup_reaction", bool))
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "levelup_reaction", bool)
+ )
@commands.command(name="reaction_emoji")
- async def config_levelup_reaction_emoji(self, ctx: MyContext, emote: discord.Emoji=None):
+ async def config_levelup_reaction_emoji(
+ self, ctx: MyContext, emote: Union[discord.Emoji, str] = None
+ ):
"""Set the emoji wich one the bot will react to message when levelup"""
# check if emoji is valid
- emote = emote if isinstance(emote, discord.Emoji) or emoji.is_emoji(emote) else False
+ emote = (
+ emote
+ if isinstance(emote, discord.Emoji) or emoji.is_emoji(emote)
+ else False
+ )
# if emojis was invalid (couldn't be converted)
if not emote:
await ctx.send(await self.bot._(ctx.guild.id, "sconfig.invalid-emoji"))
@@ -89,18 +130,22 @@ async def config_levelup_reaction_emoji(self, ctx: MyContext, emote: discord.Emo
# convert discord emoji to ID if needed
emote = str(emote.id) if isinstance(emote, discord.Emoji) else emote
# save result
- await ctx.send(await self.bot.sconfig.edit_config(ctx.guild.id, "reaction_emoji", emote))
-
- async def _create_config(self, ctx: MyContext, mentions: bool=False) -> List[Tuple[str,str]]:
+ await ctx.send(
+ await self.bot.sconfig.edit_config(ctx.guild.id, "reaction_emoji", emote)
+ )
+
+ async def _create_config(
+ self, ctx: MyContext, mentions: bool = False
+ ) -> List[Tuple[str, str]]:
"""Create a list of (key,value) for the /config command"""
roles = await self.rr_list_role(ctx.guild.id)
sorted_dict = dict()
for r in roles:
- if role := ctx.guild.get_role(r['role']):
- if r['level'] in sorted_dict:
- sorted_dict[r['level']].append(role)
+ if role := ctx.guild.get_role(r["role"]):
+ if r["level"] in sorted_dict:
+ sorted_dict[r["level"]].append(role)
else:
- sorted_dict[r['level']] = [role]
+ sorted_dict[r["level"]] = [role]
if len(sorted_dict) == 0:
return list()
_lvl = await self.bot._(ctx.guild.id, "xp.card.level")
@@ -113,8 +158,28 @@ async def _create_config(self, ctx: MyContext, mentions: bool=False) -> List[Tup
result.append((f"{_lvl} {k}", " ".join(subroles)))
return result
+
+ ################
+ # XP reduction #
+ ################
+
+ @tasks.loop(hours=24*7)
+ async def xp_reduction(self):
+ """Reduce the xp of all members each week"""
+
+ # Compute the XP to remove each week
+ xp_to_remove = await self.calc_xp(f"Vous savez, moi je ne crois pas qu’il y ait de bonne ou de mauvaise situation. Moi, si je devais résumer ma vie aujourd’hui avec vous, je dirais que c’est d’abord des rencontres. Des gens qui m’ont tendu la main, peut-être à un moment où je ne pouvais pas, où j’étais seul chez moi. Et c’est assez curieux de se dire que les hasards, les rencontres forgent une destinée... Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas l’interlocuteur en face je dirais, le miroir qui vous aide à avancer. Alors ça n’est pas mon cas, comme je disais là, puisque moi au contraire, j’ai pu ; et je dis merci à la vie, je lui dis merci, je chante la vie, je danse la vie... je ne suis qu’amour ! Et finalement, quand des gens me disent « Mais comment fais-tu pour avoir cette humanité ? », je leur réponds très simplement que c’est ce goût de l’amour, ce goût donc qui m’a poussé aujourd’hui à entreprendre une construction mécanique... mais demain qui sait ? Peut-être simplement à me mettre au service de la communauté, à faire le don, le don de soi.")
+
+ # xp_to_remove *= 1
+ for guild in self.bot.guilds:
+ if self.bot.server_configs[guild.id]["xp_reduction"]:
+ for member in guild.members:
+ await self.bdd_set_xp(userID=member.id, points=xp_to_remove, Type="remove", guild=guild.id)
+
+
+
async def get_lvlup_chan(self, msg: discord.Message):
- value = self.bot.server_configs[msg.guild.id]['levelup_channel']
+ value = self.bot.server_configs[msg.guild.id]["levelup_channel"]
if value is None or value == "none":
return None
if value == "any":
@@ -125,24 +190,35 @@ async def get_lvlup_chan(self, msg: discord.Message):
except discord.errors.NotFound:
return None
- async def check_noxp(self, msg: discord.Message):
+ async def check_noxp(self, message: discord.Message):
"""Check if this channel/user can get xp"""
- if msg.guild is None:
+ if message.guild is None:
return False
- if msg.guild.id in self.xp_channels_cache.keys():
- if msg.channel.id in self.xp_channels_cache[msg.guild.id]:
+
+ # get the channel ID (or parent channel id for thread)
+ channel_id = message.channel.parent.id if isinstance(message.channel, discord.Thread) else message.channel.id
+ # get the category
+ category_id = message.channel.category.id if message.channel.category is not None else 0 # dumb ID
+
+ if message.guild.id in self.xp_channels_cache:
+ if channel_id in self.xp_channels_cache[message.guild.id]:
return False
- else:
- chans = self.bot.server_configs[msg.guild.id]['noxp_channels']
- if chans is not None:
+ if category_id in self.xp_channels_cache[message.guild.id]:
+ return False
+ else: # load cache
+ channels = self.bot.server_configs[message.guild.id]["noxp_channels"]
+ if channels is not None:
# convert to a list even if there's only one item
- chans = [chans] if isinstance(chans, str) else chans
- chans = [int(x) for x in chans]
- if msg.channel.id in chans:
+ channels = [channels] if isinstance(channels, str) else channels
+ channels = [int(x) for x in channels]
+
+ self.xp_channels_cache[message.guild.id] = channels
+
+ if channel_id in channels or category_id in channels:
return False
else:
- chans = []
- self.xp_channels_cache[msg.guild.id] = chans
+ self.xp_channels_cache[message.guild.id] = []
+
return True
async def check_cmd(self, msg: discord.Message):
@@ -153,32 +229,38 @@ async def check_cmd(self, msg: discord.Message):
async def check_spam(self, text: str):
"""Checks if a text contains spam"""
# check for usual bots prefixes
- if len(text)>0 and (text[0] in string.punctuation or text[1] in string.punctuation):
+ if len(text) > 0 and (
+ text[0] in string.punctuation or text[1] in string.punctuation
+ ):
return True
d = dict()
# count frequency of letters in the message
for c in text:
- if c in d.keys():
+ if c in d:
d[c] += 1
else:
d[c] = 1
for v in d.values():
# if frequency is too high: spam detected
- if v/len(text) > self.spam_rate:
+ if v / len(text) > self.spam_rate:
return True
return False
- @commands.Cog.listener('on_message')
+ @commands.Cog.listener("on_message")
async def add_xp(self, msg: discord.Message):
"""Assigns a certain amount of xp to a message"""
# check if it's a bot or sent in DM
if msg.author.bot or msg.guild is None:
return
# check if it's in a no-xp channel or if xp is disabled in the server
- if not (await self.check_noxp(msg) and self.bot.server_configs[msg.guild.id]['enable_xp']):
+ if not (
+ await self.check_noxp(msg)
+ and self.bot.server_configs[msg.guild.id]["enable_xp"]
+ ):
return
+
# if xp of that guild is not in cache yet
- if msg.guild.id not in self.cache.keys() or len(self.cache[msg.guild.id]) == 0:
+ if msg.guild.id not in self.cache or len(self.cache[msg.guild.id]) == 0:
await self.bdd_load_cache(msg.guild.id)
# if xp of that member is in cache
if msg.author.id in self.cache[msg.guild.id].keys():
@@ -187,7 +269,11 @@ async def add_xp(self, msg: discord.Message):
return
content = msg.clean_content
# if content is too short or is spammy or is a command
- if len(content) < self.minimal_size or await self.check_spam(content) or await self.check_cmd(msg):
+ if (
+ len(content) < self.minimal_size
+ or await self.check_spam(content)
+ or await self.check_cmd(msg)
+ ):
return
# we calcul xp amount
giv_points = await self.calc_xp(msg)
@@ -197,19 +283,21 @@ async def add_xp(self, msg: discord.Message):
else:
try:
# we check in the database for the previous xp
- prev_points = (await self.bdd_get_xp(msg.author.id, msg.guild.id))
+ prev_points = await self.bdd_get_xp(msg.author.id, msg.guild.id)
if len(prev_points) > 0:
- prev_points = prev_points[0]['xp']
+ prev_points = prev_points[0]["xp"]
else:
# if user not in database, it's their first message
prev_points = 0
- except:
+ except BaseException:
prev_points = 0
# we update database with the new xp amount
- await self.bdd_set_xp(msg.author.id, giv_points, 'add', msg.guild.id)
+ await self.bdd_set_xp(msg.author.id, giv_points, "add", msg.guild.id)
# we update cache with the new xp amount
self.cache.get(msg.guild.id)[msg.author.id] = [
- round(time.time()), prev_points+giv_points]
+ round(time.time()),
+ prev_points + giv_points,
+ ]
# calcul of the new level
new_lvl = await self.calc_level(self.cache[msg.guild.id][msg.author.id][1])
# if new level is higher than previous level higher than 0
@@ -217,20 +305,25 @@ async def add_xp(self, msg: discord.Message):
# we send levelup message
await self.send_levelup(msg, new_lvl)
# refresh roles rewards for that user
- await self.give_rr(msg.author, new_lvl[0], await self.rr_list_role(msg.guild.id))
+ await self.give_rr(
+ msg.author, new_lvl[0], await self.rr_list_role(msg.guild.id)
+ )
- async def calc_xp(self, msg: discord.Message):
+ async def calc_xp(self, msg: discord.Message | str):
"""Calculates the xp amount corresponding to a message"""
- content: str = msg.clean_content
+ if isinstance(msg, str):
+ content = msg
+ else:
+ content: str = msg.clean_content
# we replace custom emojis by their names
matches = re.finditer(r"", content, re.MULTILINE)
for _, match in enumerate(matches, start=1):
content = content.replace(match.group(0), match.group(1))
# we remove links
- matches = re.finditer(r'((?:http|www)[^\s]+)', content, re.MULTILINE)
+ matches = re.finditer(r"((?:http|www)[^\s]+)", content, re.MULTILINE)
for _, match in enumerate(matches, start=1):
content = content.replace(match.group(0), "")
- return min(round(len(content)*self.xp_per_char), self.max_xp_per_msg)
+ return min(round(len(content) * self.xp_per_char), self.max_xp_per_msg)
async def calc_level(self, xp: int):
"""Calculates the level corresponding to a xp amount
@@ -240,42 +333,61 @@ async def calc_level(self, xp: int):
- current lvl = ceil(0.056*current_xp^0.65)
- xp needed for a level: ceil(((lvl-1)*125/7)^(20/13))"""
if xp == 0:
- return [0, ceil((1*125/7)**(20/13)), 0]
- lvl = ceil(0.056*xp**0.65)
- next_step = ceil((lvl*125/7)**(20/13))
+ return [0, ceil((1 * 125 / 7) ** (20 / 13)), 0]
+ lvl = ceil(0.056 * xp**0.65)
+ next_step = ceil((lvl * 125 / 7) ** (20 / 13))
# next_step = xp
# while ceil(0.056*next_step**0.65) == lvl:
# next_step += 1
- return [lvl, next_step, ceil(((lvl-1)*125/7)**(20/13))]
+ return [lvl, next_step, ceil(((lvl - 1) * 125 / 7) ** (20 / 13))]
async def send_levelup(self, msg: discord.Message, lvl: int):
"""Send the levelup message or react with the reaction"""
config = self.bot.server_configs[msg.guild.id]
- print(config['levelup_reaction'])
- if config['levelup_reaction']:
- if config['reaction_emoji'] is None:
+ if config["levelup_reaction"]:
+ if config["reaction_emoji"] is None:
await msg.add_reaction("💫")
else:
- def emojis_convert(s_emoji: str, bot_emojis: List[discord.Emoji]) -> Union[str, discord.Emoji]:
+
+ def emojis_convert(
+ s_emoji: str, bot_emojis: List[discord.Emoji]
+ ) -> Union[str, discord.Emoji]:
if s_emoji.isnumeric():
d_em = discord.utils.get(bot_emojis, id=int(s_emoji))
if d_em is not None:
return d_em
return emoji.emojize(s_emoji, language="alias")
- await msg.add_reaction(emojis_convert(config['reaction_emoji'],self.bot.emojis))
+
+ await msg.add_reaction(
+ emojis_convert(config["reaction_emoji"], self.bot.emojis)
+ )
else:
destination = await self.get_lvlup_chan(msg)
# if no channel or not enough permissions: abort
- if destination is None or (not msg.channel.permissions_for(msg.guild.me).send_messages):
+ if destination is None or (
+ not msg.channel.permissions_for(msg.guild.me).send_messages
+ ):
return
- text = config['levelup_message']
+ text = config["levelup_message"]
if text is None or len(text) == 0:
- text = await self.bot._(msg.channel, 'xp.default_levelup')
- await destination.send(text.format_map(self.bot.SafeDict(user=msg.author.mention,
- level=lvl[0],
- username=msg.author.display_name)))
-
- async def give_rr(self, member: discord.Member, level: int, rr_list: List[Dict[str, int]], remove: bool = False) -> int:
+ text = await self.bot._(msg.channel, "xp.default_levelup")
+ await destination.send(
+ text.format_map(
+ self.bot.SafeDict(
+ user=msg.author.mention,
+ level=lvl[0],
+ username=msg.author.display_name,
+ )
+ )
+ )
+
+ async def give_rr(
+ self,
+ member: discord.Member,
+ level: int,
+ rr_list: List[Dict[str, int]],
+ remove: bool = False,
+ ) -> int:
"""Give (and remove?) roles rewards to a member
rr_list is a list of dictionnaries containing level and role id
put remove as True if you want to remove unneeded roles rewards too"""
@@ -283,46 +395,67 @@ async def give_rr(self, member: discord.Member, level: int, rr_list: List[Dict[s
# List of roles IDs owned by user
has_roles = [x.id for x in member.roles]
# for each role that should be given and not already owned by user
- for role in [x for x in rr_list if x['level'] <= level and x['role'] not in has_roles]:
+ for role in [
+ x for x in rr_list if x["level"] <= level and x["role"] not in has_roles
+ ]:
try:
- r = member.guild.get_role(role['role'])
+ r = member.guild.get_role(role["role"])
if r is None:
continue
# finally add the role, with a reason
- await member.add_roles(r, reason="Role reward (lvl {})".format(role['level']))
+ await member.add_roles(
+ r, reason="Role reward (lvl {})".format(role["level"])
+ )
c += 1
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
# if we don't have to remove roles: stop
if not remove:
return c
# for each role that should be removed and owned by user
- for role in [x for x in rr_list if x['level'] > level and x['role'] in has_roles]:
+ for role in [
+ x for x in rr_list if x["level"] > level and x["role"] in has_roles
+ ]:
try:
- r = member.guild.get_role(role['role'])
+ r = member.guild.get_role(role["role"])
if r is None:
continue
# finally remove the role, with a reason
- await member.remove_roles(r, reason="Role reward (lvl {})".format(role['level']))
+ await member.remove_roles(
+ r, reason="Role reward (lvl {})".format(role["level"])
+ )
c += 1
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
return c
- async def bdd_set_xp(self, userID: int, points: int, Type: str = 'add', guild: int = None):
+ async def bdd_set_xp(
+ self, userID: int, points: int, Type: str = "add", guild: int = None
+ ):
"""Add/reset xp to a user in the database
Set guild=None for global leaderboard"""
try:
+ try:
+ xp = await self.bdd_get_xp(userID, guild)
+ xp = xp[0]["xp"]
+ except IndexError:
+ xp = 0
if points < 0:
raise ValueError("You cannot add nor set negative xp")
- if Type == 'add':
+ if Type == "add":
query = "INSERT INTO xp (`guild`, `userid`,`xp`) VALUES (:g, :u, :p) ON CONFLICT(guild, userid) DO UPDATE SET xp = (xp + :p);"
+ elif Type == "remove":
+ if xp < points:
+ query = "INSERT INTO xp (`guild`, `userid`,`xp`) VALUES (:g, :u, :p) ON CONFLICT(guild, userid) DO UPDATE SET xp = 0;"
+ else:
+ query = "INSERT INTO xp (`guild`, `userid`,`xp`) VALUES (:g, :u, :p) ON CONFLICT(guild, userid) DO UPDATE SET xp = (xp - :p);"
else:
query = "INSERT INTO xp (`guild`, `userid`,`xp`) VALUES (:g, :u, :p) ON CONFLICT(guild, userid) DO UPDATE SET xp = :p;"
- self.bot.db_query(query, {'g': guild, 'u': userID, 'p': points})
+
+ self.bot.db_query(query, {"g": guild, "u": userID, "p": points})
return True
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
return False
async def bdd_get_xp(self, userID: int, guild: int = None):
@@ -330,16 +463,18 @@ async def bdd_get_xp(self, userID: int, guild: int = None):
Set guild=None for global leaderboard"""
try:
query = "SELECT `xp` FROM `xp` WHERE `userid` = :u AND `guild` = :g"
- liste = self.bot.db_query(query, {'u': userID, 'g': guild})
+ liste = self.bot.db_query(query, {"u": userID, "g": guild})
if len(liste) == 1:
if userID in self.cache[guild].keys():
- self.cache[guild][userID][1] = liste[0]['xp']
+ self.cache[guild][userID][1] = liste[0]["xp"]
else:
self.cache[guild][userID] = [
- round(time.time())-60, liste[0]['xp']]
+ round(time.time()) - 60,
+ liste[0]["xp"],
+ ]
return liste
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
return list()
async def bdd_get_nber(self, guild: int = None):
@@ -347,12 +482,12 @@ async def bdd_get_nber(self, guild: int = None):
Set guild=None for global leaderboard"""
try:
query = "SELECT COUNT(*) as count FROM xp WHERE `guild` = :g"
- liste = self.bot.db_query(query, {'g': guild})
+ liste = self.bot.db_query(query, {"g": guild})
if liste is not None and len(liste) == 1:
- return liste[0]['count']
+ return liste[0]["count"]
return 0
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
return 0
async def bdd_load_cache(self, guild: int = None):
@@ -365,9 +500,9 @@ async def bdd_load_cache(self, guild: int = None):
if guild not in self.cache.keys():
self.cache[guild] = dict()
for l in liste:
- self.cache[guild][l['userid']] = [round(time.time())-60, int(l['xp'])]
+ self.cache[guild][l["userid"]] = [round(time.time()) - 60, int(l["xp"])]
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
async def bdd_get_rank(self, userID: int, guild: discord.Guild = None):
"""Get the rank of a user
@@ -381,15 +516,15 @@ async def bdd_get_rank(self, userID: int, guild: discord.Guild = None):
if guild is not None:
users = [x.id for x in guild.members]
for x in liste:
- if guild is None or (guild is not None and x['userid'] in users):
+ if guild is None or (guild is not None and x["userid"] in users):
i += 1
- if x['userid'] == userID:
+ if x["userid"] == userID:
userdata = dict(x)
- userdata['rank'] = i
+ userdata["rank"] = i
break
return userdata
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
async def bdd_get_top(self, top: int = None, guild: discord.Guild = None):
""""""
@@ -399,33 +534,37 @@ async def bdd_get_top(self, top: int = None, guild: discord.Guild = None):
query += f" LIMIT {top}"
return self.bot.db_query(query, (guild.id if guild else None,))
except Exception as e:
- await self.bot.get_cog('Errors').on_error(e)
+ await self.bot.get_cog("Errors").on_error(e)
async def get_xp(self, user: discord.User, guild_id: int = None):
"""Get the xp amount of a user in a guild"""
xp = await self.bdd_get_xp(user.id, guild_id)
if xp is None or (isinstance(xp, list) and len(xp) == 0):
return
- return xp[0]['xp']
+ return xp[0]["xp"]
- async def send_embed(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, levels_info):
+ async def send_embed(
+ self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, levels_info
+ ):
"""Send the !rank command as an embed"""
- LEVEL = await self.bot._(ctx.channel, 'xp.card.level')
- RANK = await self.bot._(ctx.channel, 'xp.card.rank')
+ LEVEL = await self.bot._(ctx.channel, "xp.card.level")
+ RANK = await self.bot._(ctx.channel, "xp.card.rank")
if levels_info is None:
levels_info = await self.calc_level(xp)
emb = discord.Embed(color=self.embed_color)
emb.set_author(name=str(user), icon_url=user.display_avatar)
- emb.add_field(name='XP', value=f"{xp}/{levels_info[1]}")
+ emb.add_field(name="XP", value=f"{xp}/{levels_info[1]}")
emb.add_field(name=LEVEL, value=levels_info[0])
emb.add_field(name=RANK, value=f"{rank}/{ranks_nb}")
await ctx.send(embed=emb)
- async def send_txt(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, levels_info):
+ async def send_txt(
+ self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb, levels_info
+ ):
"""Send the !rank command as a plain text"""
- LEVEL = await self.bot._(ctx.channel, 'xp.card.level')
- RANK = await self.bot._(ctx.channel, 'xp.card.rank')
+ LEVEL = await self.bot._(ctx.channel, "xp.card.level")
+ RANK = await self.bot._(ctx.channel, "xp.card.rank")
if levels_info is None:
levels_info = await self.calc_level(xp)
msg = f"""__**{user.name}**__
@@ -434,7 +573,7 @@ async def send_txt(self, ctx: MyContext, user: discord.User, xp, rank, ranks_nb,
**{RANK}** {rank}/{ranks_nb}"""
await ctx.send(msg)
- @commands.command(name='rank')
+ @commands.command(name="rank")
@commands.guild_only()
@commands.bot_has_permissions(send_messages=True)
@commands.cooldown(3, 10, commands.BucketType.user)
@@ -445,22 +584,22 @@ async def rank(self, ctx: MyContext, *, user: discord.User = None):
user = ctx.author
# if user is a bot
if user.bot:
- return await ctx.send(await self.bot._(ctx.channel, 'xp.bot-rank'))
+ return await ctx.send(await self.bot._(ctx.channel, "xp.bot-rank"))
# if xp is disabled
- if not self.bot.server_configs[ctx.guild.id]['enable_xp']:
- return await ctx.send(await self.bot._(ctx.guild.id, 'xp.xp-disabled'))
+ if not self.bot.server_configs[ctx.guild.id]["enable_xp"]:
+ return await ctx.send(await self.bot._(ctx.guild.id, "xp.xp-disabled"))
# if guild cache not done yet
if ctx.guild.id not in self.cache:
await self.bdd_load_cache(ctx.guild.id)
xp = await self.get_xp(user, ctx.guild.id)
if xp is None:
if ctx.author == user:
- return await ctx.send(await self.bot._(ctx.channel, 'xp.no-xp-author'))
- return await ctx.send(await self.bot._(ctx.channel, 'xp.no-xp-user'))
+ return await ctx.send(await self.bot._(ctx.channel, "xp.no-xp-author"))
+ return await ctx.send(await self.bot._(ctx.channel, "xp.no-xp-user"))
levels_info = None
ranks_nb = await self.bdd_get_nber(ctx.guild.id)
try:
- rank = (await self.bdd_get_rank(user.id, ctx.guild))['rank']
+ rank = (await self.bdd_get_rank(user.id, ctx.guild))["rank"]
except KeyError:
rank = "?"
if isinstance(rank, float):
@@ -470,33 +609,41 @@ async def rank(self, ctx: MyContext, *, user: discord.User = None):
else:
await self.send_txt(ctx, user, xp, rank, ranks_nb, levels_info)
- async def create_top_main(self, ranks: List[Dict[str, int]], nbr: int, page: int, ctx: MyContext):
+ async def create_top_main(
+ self, ranks: List[Dict[str, int]], nbr: int, page: int, ctx: MyContext
+ ):
"""Create the !top page
ranks: data pulled from the database
nbr: number of users to show
page: page number to show"""
txt = list()
- i = (page-1)*20
+ i = (page - 1) * 20
for u in ranks[:nbr]:
i += 1
- user = self.bot.get_user(u['userid'])
+ user = self.bot.get_user(u["userid"])
if user is None:
try:
- user = await self.bot.fetch_user(u['userid'])
+ user = await self.bot.fetch_user(u["userid"])
except discord.NotFound:
- user = await self.bot._(ctx.channel, 'xp.del-user')
+ user = await self.bot._(ctx.channel, "xp.del-user")
if isinstance(user, discord.User):
user_name = discord.utils.escape_markdown(user.name)
if len(user_name) > 18:
- user_name = user_name[:15]+'...'
+ user_name = user_name[:15] + "..."
else:
user_name = user
- l = await self.calc_level(u['xp'])
- txt.append('{} • **{} |** `lvl {}` **|** `xp {}`'.format(i, "__" +
- user_name+"__" if user == ctx.author else user_name, l[0], u['xp']))
+ l = await self.calc_level(u["xp"])
+ txt.append(
+ "{} • **{} |** `lvl {}` **|** `xp {}`".format(
+ i,
+ "__" + user_name + "__" if user == ctx.author else user_name,
+ l[0],
+ u["xp"],
+ )
+ )
return txt, i
- @commands.command(name='top')
+ @commands.command(name="top")
@commands.guild_only()
@commands.bot_has_permissions(send_messages=True)
@commands.cooldown(5, 60, commands.BucketType.user)
@@ -504,24 +651,27 @@ async def top(self, ctx: MyContext, page: typing.Optional[int] = 1):
"""Get the list of the highest levels
Each page has 20 users"""
# if xp is disabled
- if not self.bot.server_configs[ctx.guild.id]['enable_xp']:
- return await ctx.send(await self.bot._(ctx.guild.id, 'xp.xp-disabled'))
+ if not self.bot.server_configs[ctx.guild.id]["enable_xp"]:
+ return await ctx.send(await self.bot._(ctx.guild.id, "xp.xp-disabled"))
# if guild cache not done yet
if ctx.guild.id not in self.cache:
await self.bdd_load_cache(ctx.guild.id)
# get user ranks from db
- ranks = [{'userid': key, 'xp': value[1]}
- for key, value in self.cache[ctx.guild.id].items()]
- ranks = sorted(ranks, key=lambda x: x['xp'], reverse=True)
+ ranks = [
+ {"userid": key, "xp": value[1]}
+ for key, value in self.cache[ctx.guild.id].items()
+ ]
+ ranks = sorted(ranks, key=lambda x: x["xp"], reverse=True)
# cal max page and check the argument
- max_page = ceil(len(ranks)/20)
+ max_page = ceil(len(ranks) / 20)
if page < 1:
- return await ctx.send(await self.bot._(ctx.channel, 'xp.top.low-page'))
+ return await ctx.send(await self.bot._(ctx.channel, "xp.top.low-page"))
elif page > max_page:
- return await ctx.send(await self.bot._(ctx.channel, 'xp.top.high-page'))
+ return await ctx.send(await self.bot._(ctx.channel, "xp.top.high-page"))
# limit to the amount of users we neeed
- ranks = ranks[(page-1)*20:]
- # create leaderboard field while making sure the text fits in it (1024 char)
+ ranks = ranks[(page - 1) * 20 :]
+ # create leaderboard field while making sure the text fits in it (1024
+ # char)
nbr = 20
txt, i = await self.create_top_main(ranks, nbr, page, ctx)
while len("\n".join(txt)) > 1000 and nbr > 0:
@@ -529,39 +679,51 @@ async def top(self, ctx: MyContext, page: typing.Optional[int] = 1):
txt, i = await self.create_top_main(ranks, nbr, page, ctx)
# wait a bit to not overload the bot
await asyncio.sleep(0.2)
- f_name = await self.bot._(ctx.channel, 'xp.top.name', min=(page-1)*20+1, max=i, page=page, pmax=max_page)
+ f_name = await self.bot._(
+ ctx.channel,
+ "xp.top.name",
+ min=(page - 1) * 20 + 1,
+ max=i,
+ page=page,
+ pmax=max_page,
+ )
# author
rank = await self.bdd_get_rank(ctx.author.id, ctx.guild)
if len(rank) == 0:
# user has no xp yet
your_rank = {
- 'name': "__"+await self.bot._(ctx.channel, "xp.top.your")+"__",
- 'value': await self.bot._(ctx.guild, "xp.no-xp-author")
+ "name": "__" + await self.bot._(ctx.channel, "xp.top.your") + "__",
+ "value": await self.bot._(ctx.guild, "xp.no-xp-author"),
}
else:
- lvl = await self.calc_level(rank['xp'])
+ lvl = await self.calc_level(rank["xp"])
lvl = lvl[0]
your_rank = {
- 'name': "__"+await self.bot._(ctx.channel, "xp.top.your")+"__",
- 'value': "**#{} |** `lvl {}` **|** `xp {}`".format(rank['rank'] if 'rank' in rank.keys() else '?', lvl, rank['xp'])
+ "name": "__" + await self.bot._(ctx.channel, "xp.top.your") + "__",
+ "value": "**#{} |** `lvl {}` **|** `xp {}`".format(
+ rank["rank"] if "rank" in rank.keys() else "?", lvl, rank["xp"]
+ ),
}
# title
- t = await self.bot._(ctx.channel, 'xp.top.title')
+ t = await self.bot._(ctx.channel, "xp.top.title")
# final embed
if ctx.can_send_embed:
emb = discord.Embed(title=t, color=self.embed_color)
- emb.set_author(name=self.bot.user.name,
- icon_url=self.bot.user.display_avatar)
+ emb.set_author(
+ name=self.bot.user.name, icon_url=self.bot.user.display_avatar
+ )
emb.add_field(name=f_name, value="\n".join(txt), inline=False)
emb.add_field(**your_rank)
await ctx.send(embed=emb)
else:
- await ctx.send(f_name+"\n\n"+'\n'.join(txt))
+ await ctx.send(f_name + "\n\n" + "\n".join(txt))
async def rr_add_role(self, guildID: int, roleID: int, level: int):
"""Add a role reward in the database"""
- query = "INSERT INTO `roles_levels` (`guild`,`role`,`level`) VALUES (:g, :r, :l);"
- self.bot.db_query(query, {'g': guildID, 'r': roleID, 'l': level})
+ query = (
+ "INSERT INTO `roles_levels` (`guild`,`role`,`level`) VALUES (:g, :r, :l);"
+ )
+ self.bot.db_query(query, {"g": guildID, "r": roleID, "l": level})
return True
async def rr_list_role(self, guild: int, level: int = -1) -> List[dict]:
@@ -570,21 +732,22 @@ async def rr_list_role(self, guild: int, level: int = -1) -> List[dict]:
query = "SELECT rowid AS id, * FROM `roles_levels` WHERE guild = :g ORDER BY level;"
else:
query = "SELECT rowid AS id, * FROM `roles_levels` WHERE guild=:g AND level=:l ORDER BY level;"
- liste = self.bot.db_query(query, {'g': guild, 'l': level})
+ liste = self.bot.db_query(query, {"g": guild, "l": level})
return liste
async def rr_remove_role(self, ID: int):
"""Remove a role reward from the database"""
query = "DELETE FROM `roles_levels` WHERE rowid = ?;"
- self.bot.db_query(query, (ID, ))
+ self.bot.db_query(query, (ID,))
return True
@commands.group(name="roles_levels")
+ @commands.has_permissions(manage_guild=True)
@commands.guild_only()
async def rr_main(self, ctx: MyContext):
"""Manage your roles rewards like a boss"""
if ctx.subcommand_passed is None:
- await ctx.send_help('roles_levels')
+ await ctx.send_help("roles_levels")
@rr_main.command(name="add")
@commands.has_permissions(manage_guild=True)
@@ -592,16 +755,22 @@ async def rr_add(self, ctx: MyContext, level: int, *, role: discord.Role):
"""Add a role reward
This role will be given to every member who reaches the specified level"""
try:
- if role.name == '@everyone':
+ if role.name == "@everyone":
raise commands.BadArgument(f'Role "{role.name}" not found')
l = await self.rr_list_role(ctx.guild.id)
- if len([x for x in l if x['level'] == level]) > 0:
- return await ctx.send(await self.bot._(ctx.guild.id, 'xp.rr.already-exist'))
+ if len([x for x in l if x["level"] == level]) > 0:
+ return await ctx.send(
+ await self.bot._(ctx.guild.id, "xp.rr.already-exist")
+ )
await self.rr_add_role(ctx.guild.id, role.id, level)
except Exception as e:
- await self.bot.get_cog('Errors').on_command_error(ctx, e)
+ await self.bot.get_cog("Errors").on_command_error(ctx, e)
else:
- await ctx.send(await self.bot._(ctx.guild.id, 'xp.rr.added', name=role.name, level=level))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id, "xp.rr.added", name=role.name, level=level
+ )
+ )
@rr_main.command(name="list")
async def rr_list(self, ctx: MyContext):
@@ -611,11 +780,12 @@ async def rr_list(self, ctx: MyContext):
try:
l = await self.rr_list_role(ctx.guild.id)
except Exception as e:
- await self.bot.get_cog('Errors').on_command_error(ctx, e)
+ await self.bot.get_cog("Errors").on_command_error(ctx, e)
else:
LVL = await self.bot._(ctx.guild.id, "xp.card.level")
- desc = '\n'.join(
- ["• <@&{}> : {} {}".format(x['role'], LVL, x['level']) for x in l])
+ desc = "\n".join(
+ ["• <@&{}> : {} {}".format(x["role"], LVL, x["level"]) for x in l]
+ )
if len(desc) == 0:
desc = await self.bot._(ctx.guild.id, "xp.rr.no-rr-2")
title = await self.bot._(ctx.guild.id, "xp.rr.list-title", nbr=len(l))
@@ -630,12 +800,12 @@ async def rr_remove(self, ctx: MyContext, level: int):
try:
l = await self.rr_list_role(ctx.guild.id, level)
if len(l) == 0:
- return await ctx.send(await self.bot._(ctx.guild.id, 'xp.rr.no-rr'))
- await self.rr_remove_role(l[0]['id'])
+ return await ctx.send(await self.bot._(ctx.guild.id, "xp.rr.no-rr"))
+ await self.rr_remove_role(l[0]["id"])
except Exception as e:
- await self.bot.get_cog('Errors').on_command_error(ctx, e)
+ await self.bot.get_cog("Errors").on_command_error(ctx, e)
else:
- await ctx.send(await self.bot._(ctx.guild.id, 'xp.rr.removed', level=level))
+ await ctx.send(await self.bot._(ctx.guild.id, "xp.rr.removed", level=level))
@rr_main.command(name="reload")
@commands.has_permissions(manage_guild=True)
@@ -644,22 +814,43 @@ async def rr_reload(self, ctx: MyContext):
"""Refresh roles rewards for the whole server"""
try:
if not ctx.guild.me.guild_permissions.manage_roles:
- return await ctx.send(await self.bot._(ctx.guild.id, 'xp.cant-manage-roles'))
+ return await ctx.send(
+ await self.bot._(ctx.guild.id, "xp.cant-manage-roles")
+ )
c = 0
rr_list = await self.rr_list_role(ctx.guild.id)
if len(rr_list) == 0:
await ctx.send(await self.bot._(ctx.guild, "xp.rr.no-rr-2"))
return
- xps = [{'user': x['userid'], 'xp':x['xp']} for x in await self.bdd_get_top(top=None, guild=ctx.guild)]
+ xps = [
+ {"user": x["userid"], "xp": x["xp"]}
+ for x in await self.bdd_get_top(top=None, guild=ctx.guild)
+ ]
for member in xps:
- m = ctx.guild.get_member(member['user'])
+ m = ctx.guild.get_member(member["user"])
if m is not None:
- level = (await self.calc_level(member['xp']))[0]
+ level = (await self.calc_level(member["xp"]))[0]
c += await self.give_rr(m, level, rr_list, remove=True)
- await ctx.send(await self.bot._(ctx.guild.id, 'xp.rr.reload', count=c, members=ctx.guild.member_count))
+ await ctx.send(
+ await self.bot._(
+ ctx.guild.id,
+ "xp.rr.reload",
+ count=c,
+ members=ctx.guild.member_count,
+ )
+ )
except Exception as e:
- await self.bot.get_cog('Errors').on_command_error(ctx, e)
+ await self.bot.get_cog("Errors").on_command_error(ctx, e)
+
+ def cog_unload(self):
+ self.xp_reduction.cancel()
+
+config = {}
+async def setup(bot:Gunibot=None, plugin_config:dict=None):
+ if bot is not None:
+ await bot.add_cog(XP(bot), icon="🪙")
+ if plugin_config is not None:
+ global config
+ config.update(plugin_config)
-async def setup(bot):
- await bot.add_cog(XP(bot))
diff --git a/requirements.txt b/requirements.txt
index 8abbad7f..5237d6cd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,13 @@
psutil
aiohttp
-GitPython>=3.1.8
+gitpython >= 3.1.8
python-i18n
-PyYAML
-emoji>=2.0
-feedparser>=6.0
-python-twitter>=3.5
-async_timeout>=3.0
+pyyaml
+emoji >= 2.0
+feedparser >= 6.0
+python-twitter >= 3.5
+async-timeout >= 3.0
argparse
-discord.py==2.0
\ No newline at end of file
+numpy
+discord.py ~= 2.0.0
+lrfutils == 0.1.2
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..6178b5a5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python
+# coding=utf-8
+
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
+import sys
+import os
+import pkg_resources
+import subprocess
+
+accept = ["y", "yes", "yep", "yeah"]
+decline = ["n", "no", "nope", "nah"]
+
+# ________________________________________________________________________________
+# check python version
+
+py_version = sys.version_info
+if py_version.major != 3 or py_version.minor < 10:
+ print("⚠️ \033[33mGipsy require Python 3.10 or more.\033[1m")
+ sys.exit(1)
+
+# ________________________________________________________________________________
+# Detect virtual environment
+
+
+def get_base_prefix_compat():
+ """Get base/real prefix, or sys.prefix if there is none."""
+ return (
+ getattr(sys, "base_prefix", None)
+ or getattr(sys, "real_prefix", None)
+ or sys.prefix
+ )
+
+
+def in_virtualenv():
+ return get_base_prefix_compat() != sys.prefix
+
+
+# ________________________________________________________________________________
+# Check modules
+
+
+def check_libs(verbose=False):
+ """Check if the required libraries are installed and can be imported"""
+ with open("requirements.txt", "r") as file:
+ packages = pkg_resources.parse_requirements(file.readlines())
+ try:
+ pkg_resources.working_set.resolve(packages)
+ except pkg_resources.VersionConflict as e:
+ if verbose:
+ print(f"\n🤕 \033[31mOops, there is a problem in the dependencies.\033[0m")
+ print(f"\n⚠️ \033[33m{type(e).__name__}: {e}\033[0m\n ")
+ return False
+ except Exception as e:
+ if verbose:
+ print(f"\n🤕 \033[31mOops, there is a problem in the dependencies.\033[0m")
+ print(
+ f"\n⛔ \u001b[41m\u001b[37;1m{type(e).__name__}\033[0m: \033[31m{e}\033[0m"
+ )
+ return False
+ return True
+
+
+# ________________________________________________________________________________
+# Setup virtual environment
+
+
+def setup_venv():
+ choice = input(
+ f"\033[34m\n🏗️ Do you want to create a virtual environment? [Y/n]\033[0m"
+ )
+ if choice.lower() not in decline:
+ print("Creating virtual environment...")
+ os.system("python3 -m venv venv")
+ print("Done!")
+ print(
+ "\n🔄️ \033[34mPlease activate the virtual environment using the command below that correspond to your system. Then restart the setup script.\033[0m\n"
+ )
+ print(
+ "\033[32m Linux or MacOS (bash shell)\t:\033[0m source venv/bin/activate"
+ )
+ print(
+ "\033[32m Linux or MacOS (fish shell)\t:\033[0m source venv/bin/activate.fish"
+ )
+ print(
+ "\033[32m Linux or MacOS (csh shell)\t:\033[0m source venv/bin/activate.csh"
+ )
+ print("\033[32m Windows (in cmd.exe)\t\t:\033[0m venv\\Scripts\\activate.bat")
+ print("\033[32m Windows (in PowerShell)\t:\033[0m venv\\Scripts\\Activate.ps1")
+ print(
+ ' ⮩ If you have an error like "cannot run script", you may open a new Powershell in administrator mode and run the following command: Set-ExecutionPolicy RemoteSigned\n'
+ )
+ exit(0)
+ else:
+ print(" Ok, let's stay on global environment.")
+ return False
+
+
+# ________________________________________________________________________________
+# Install dependancies
+
+
+def install_dependencies():
+ """Install all dependencies needed for the bot to work."""
+ choice = input(
+ f"\033[34m\n🏗️ Do you want to install dependencies on the actual environment? [y/N]\033[0m"
+ )
+ if choice.lower() in accept:
+ print("🏗️ Installing dependencies...")
+ os.system("python3 -m pip install -r requirements.txt")
+ print("Done!")
+ return True
+ else:
+ print(" Dependencies not installed.")
+ return False
+
+
+if __name__ == "__main__":
+ verbose = False
+else:
+ verbose = True
+
+if not check_libs(verbose=verbose):
+ print(
+ f"\n🏗️ You need to install the bot dependencies. The automatic script will probably upgrade (or rarely downgrade) some python modules already installed on your machine."
+ )
+ if not in_virtualenv():
+ setup_venv()
+ if install_dependencies():
+ os.system("Restarting...")
+ os.system("python3 setup.py")
+ else:
+ print(
+ "\n⚠️ \033[33mThe bot can't run without it's dependencies. Please install all the required modules with the following command:\033[1m\n"
+ )
+ print(
+ " \u001b[47m\033[30mpython3 -m pip install -r requirements.txt\033[0m\n "
+ )
+ exit(1)
+
+# ________________________________________________________________________________
+# Import modules
+
+import subprocess
+from LRFutils import color
+from core import config
+import sys
+
+# ________________________________________________________________________________
+# Setup script
+
+def main():
+ if not os.path.isdir("plugins"):
+ os.mkdir("plugins")
+
+ config.token_set()
+
+ # Optional settings
+
+ choice = input(
+ f"\n{color.fg.blue}Do you want to configure optional bot settings? [Y/n]:{color.stop} "
+ )
+ if choice.lower() not in decline:
+ config.advanced_setup()
+
+ # End optional settings
+
+ config.setup_plugins()
+
+ print(f"\n{color.fg.green}✅ Setup complete!{color.stop}")
+
+if __name__ == "__main__":
+
+ main()
+
+ # Start bot
+
+ print(
+ f"\n{color.fg.yellow}⚠️ Before starting the bot, you should open the config.yaml file and check that everything is correct.{color.stop} "
+ )
+ choice = input(f"{color.fg.blue}▶️ Do you want to start the bot? [Y/n]{color.stop} ")
+ if choice.lower() not in decline:
+ print(
+ " Starting the bot...\n--------------------------------------------------------------------------------"
+ )
+ subprocess.run([sys.executable, "start.py"])
diff --git a/start.py b/start.py
index abee7e16..6dcbd9c6 100644
--- a/start.py
+++ b/start.py
@@ -1,117 +1,149 @@
#!/usr/bin/env python
# coding=utf-8
-import discord, time, asyncio, logging, json, sys, os, argparse
-from shutil import copyfile
-from utils import Gunibot, setup_logger
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
-# check python version
-py_version = sys.version_info
-if py_version.major != 3 or py_version.minor < 9:
- print("Vous devez utiliser au moins Python 3.9 !", file=sys.stderr)
- sys.exit(1)
+import setup # do not remove this import, it also check the dependencies
+
+import os
+import asyncio
+from utils import Gunibot
+import argparse
+import discord
+from LRFutils import color
+from LRFutils import logs
+
+if not os.path.isdir("plugins"):
+ os.mkdir("plugins")
+
+# Check and dispatch the config to all plugins
+from core import config
+
+config.check()
# Getting global system list
global_systems = []
-for system in os.listdir('./bot/utils/'):
- if os.path.isfile('./bot/utils/' + system) and system[-3:] == '.py':
+for system in os.listdir("./bot/utils/"):
+ if os.path.isfile("./bot/utils/" + system) and system[-3:] == ".py":
global_systems.append("bot.utils." + system[0:-3])
# Getting plugin list
plugins = []
-for plugin in os.listdir('./plugins/'):
- if plugin[0] != '_':
- if os.path.isdir('./plugins/' + plugin):
- plugins.append("plugins." + plugin + '.bot.main')
-
-# Generating docs
-from bot.docs import generate_docs
-generate_docs()
-
-#---------------#
+for plugin in os.listdir("./plugins/"):
+ if plugin[0] not in ["_", "."]:
+ if os.path.isdir("./plugins/" + plugin):
+ plugins.append(f"plugins.{plugin}.{plugin}")
+
+
+def print_ascii_art():
+ """
+ Print GIPSY 2.0 ascii art
+ """
+ # Disable some pylints warning violations in this function
+ # pylint: disable=anomalous-backslash-in-string
+ # pylint: disable=trailing-whitespace
+ print(
+ f"""{color.fg.blue}
+ ___ ____ ____ ___ _ _ __ __
+ / __)(_ _)( _ \/ __)( \/ ) / ) /. |
+ ( (_-. _)(_ )___/\__ \ \ / )( (_ _)
+ \___/(____)(__) (___/ (__) (__)() (_)
+
+ {color.stop}"""
+ )
+
+
+# ---------------#
# M A I N #
-#---------------#
-
+# ---------------#
def main():
-
- # Getting global config
- from bot.config import get_config
- conf = get_config('./config/config', isBotConfig = True)
- if conf == None:
- return 1
-
- # Getting plugins configs
- for plugin in os.listdir('./plugins/'):
- if plugin[0] != '_':
- if os.path.isfile('./plugins/' + plugin + '/config/require-example.json'):
- conf.update(get_config('./plugins/' + plugin + '/config/require', isBotConfig = False))
+ """
+ Main function
+ """
# Creating client
- client = Gunibot(case_insensitive=True, status=discord.Status("online"), beta=False, config=conf)
+ client = Gunibot(
+ case_insensitive=True,
+ status=discord.Status.do_not_disturb,
+ beta=False
+ )
# Writing logs + welcome message
if not os.path.isdir("logs"):
os.makedirs("logs")
- log = setup_logger()
- log.setLevel(logging.DEBUG)
- log.info("Lancement du bot")
-
- print("""
- ___ ____ ____ ___ _ _ ___ ___
- / __)(_ _)( _ \/ __)( \/ ) (__ \ / _ \\
-( (_-. _)(_ )___/\__ \ \ / / _/ ( (_) )
- \___/(____)(__) (___/ (__) (____)()\___/
-
- """)
-
- # Loading extensions (global systems + plugins)
- async def load(client, global_systems, plugins):
+
+ print(" ")
+ logs.info("▶️ Starting Gipsy...")
+
+ # pylint: disable-next=anomalous-backslash-in-string
+ print_ascii_art()
+
+ # Loading extensions (global system + plugins)
+ async def load(bot_client, global_system_list, plugin_list):
loaded = 0
failed = 0
notloaded = ""
- for extension in global_systems + plugins:
+ for extension in global_system_list + plugin_list:
try:
- await client.load_extension(extension)
+ await bot_client.load_extension(extension)
loaded += 1
- except:
- log.exception(f'\nFailed to load extension {extension}')
+ except Exception: # pylint: disable=broad-except
+ logs.error(f"Failed to load extension: {extension}")
notloaded += "\n - " + extension
failed += 1
- if failed > 0:
- raise Exception("\n{} modules not loaded".format(failed) + notloaded)
return loaded, failed
# Printing info when the bot is started
async def on_ready():
"""Called when the bot is connected to Discord API"""
- print('Bot connecté')
- print("Nom : "+client.user.name)
- print("ID : "+str(client.user.id))
+ logs.info(f"{color.fg.green}✅ Bot connected")
+ logs.info("Nom : " + client.user.name)
+ logs.info("ID : " + str(client.user.id))
if len(client.guilds) < 200:
serveurs = [x.name for x in client.guilds]
- print(
- "Connecté sur ["+str(len(client.guilds))+"] "+", ".join(serveurs))
+ logs.info(
+ "Connected on "
+ + str(len(client.guilds))
+ + " servers:\n - "
+ + "\n - ".join(serveurs)
+ )
else:
- print("Connecté sur "+str(len(client.guilds))+" serveurs")
- print(time.strftime("%d/%m %H:%M:%S"))
+ logs.info("Connected on " + str(len(client.guilds)) + " servers")
loaded, failed = await load(client, global_systems, plugins)
- print(f"{loaded} plugins chargés, {failed} plugins en erreur")
- print('------')
+ logs.info(f"{loaded} plugins loaded, {failed} plugins failed")
+ print(
+ "--------------------------------------------------------------------------------"
+ )
+ await client.change_presence(
+ status=discord.Status.online,
+ )
await asyncio.sleep(2)
+ # only load plugins once
+ client.remove_listener(on_ready)
+
client.add_listener(on_ready)
# Check if the bot must run in beta
parser = argparse.ArgumentParser()
- parser.add_argument("-b", "--beta", help="Run with the beta bot token", action='store_true')
+ parser.add_argument(
+ "-b", "--beta", help="Run with the beta bot token", action="store_true"
+ )
args = parser.parse_args()
# Launch bot
- if args.beta:
- client.beta = True
- client.run(conf["token_beta"])
- else:
- client.run(conf["token"])
+ try:
+ client.run(config.get("bot.token"))
+ except discord.errors.LoginFailure:
+ logs.error("⚠️ Invalid token")
+ config.token_set(force_set=True)
+ os.system("python3 start.py")
+ exit()
if __name__ == "__main__":
diff --git a/utils.py b/utils.py
index d52d739f..a27ccf73 100644
--- a/utils.py
+++ b/utils.py
@@ -1,3 +1,10 @@
+"""
+Ce programme est régi par la licence CeCILL soumise au droit français et
+respectant les principes de diffusion des logiciels libres. Vous pouvez
+utiliser, modifier et/ou redistribuer ce programme sous les conditions
+de la licence CeCILL diffusée sur le site "http://www.cecill.info".
+"""
+
import discord
from discord.ext import commands
import logging
@@ -39,8 +46,7 @@ def can_send_embed(self) -> bool:
class Gunibot(commands.bot.AutoShardedBot):
"""Bot class, with everything needed to run it"""
- def __init__(self, case_insensitive=None, status=None, beta=False, config: dict = None):
- self.config = config
+ def __init__(self, case_insensitive=None, status=None, beta=False):
# defining allowed default mentions
ALLOWED = discord.AllowedMentions(everyone=False, roles=False)
# defining intents usage
@@ -48,12 +54,18 @@ def __init__(self, case_insensitive=None, status=None, beta=False, config: dict
intents.message_content = True
intents.members = True
# we now initialize the bot class
- super().__init__(command_prefix=self.get_prefix, case_insensitive=case_insensitive, status=status,
- allowed_mentions=ALLOWED, intents=intents)
- self.log = logging.getLogger("runner") # logs module
- self.beta: bool = beta # if the bot is in beta mode
- self.database = sqlite3.connect('data/database.db') # database connection
+ super().__init__(
+ command_prefix=self.get_prefix,
+ case_insensitive=case_insensitive,
+ status=status,
+ allowed_mentions=ALLOWED,
+ intents=intents,
+ )
+ self.log = logging.getLogger("runner") # logs module
+ self.beta: bool = beta # if the bot is in beta mode
+ self.database = sqlite3.connect("data/database.db") # database connection
self.database.row_factory = sqlite3.Row
+ self.cog_icons = {} # icons for cogs
self._update_database_structure()
async def get_context(self, message: discord.Message, *, cls=MyContext):
@@ -67,39 +79,41 @@ async def get_context(self, message: discord.Message, *, cls=MyContext):
def server_configs(self):
"""Guilds configuration manager"""
return self.get_cog("ConfigCog").confManager
-
+
@property
- def sconfig(self) -> 'Sconfig':
+ def sconfig(self) -> "Sconfig":
"""Return sconfig configuration manager"""
return self.get_cog("Sconfig")
def _update_database_structure(self):
"""Create tables and indexes from 'data/model.sql' file"""
c = self.database.cursor()
- with open('data/model.sql', 'r', encoding='utf-8') as f:
+ with open("data/model.sql", "r", encoding="utf-8") as f:
c.executescript(f.read())
- for plugin in os.listdir('./plugins/'):
- if plugin[0] != '_':
- if os.path.isfile('./plugins/' + plugin + '/data/model.sql'):
- with open('./plugins/' + plugin + '/data/model.sql', 'r', encoding='utf-8') as f:
+ for plugin in os.listdir("./plugins/"):
+ if plugin[0] != "_":
+ if os.path.isfile("./plugins/" + plugin + "/data/model.sql"):
+ with open(
+ "./plugins/" + plugin + "/data/model.sql", "r", encoding="utf-8"
+ ) as f:
c.executescript(f.read())
c.close()
- async def user_avatar_as(self, user, size=512):
+ async def user_avatar_as(
+ self,
+ user: Union[discord.User, discord.Member],
+ size=512,
+ ):
"""Get the avatar of an user, format gif or png (as webp isn't supported by some browsers)"""
- if not hasattr(user, "avatar_url_as"):
- raise ValueError
- try:
- if user.is_avatar_animated():
- return user.display_avatar_as(format='gif', size=size)
- else:
- return user.display_avatar_as(format='png', size=size)
- except Exception as e:
- await self.cogs['Errors'].on_error(e, None)
+ avatar = user.display_avatar.with_size(size) # the avatar always exist, returns the URL to the default one
+ if avatar.is_animated():
+ return avatar.with_format("gif")
+ else:
+ return avatar.with_format("png")
class SafeDict(dict):
def __missing__(self, key):
- return '{' + key + '}'
+ return "{" + key + "}"
async def get_prefix(self, msg):
"""Get a prefix from a message... what did you expect?"""
@@ -110,14 +124,15 @@ async def get_prefix(self, msg):
prefix = "?"
return commands.when_mentioned_or(prefix)(self, msg)
- async def update_config(self, key: str, value):
- """Change a value in the config file
- No undo can be done"""
- self.config[key] = value
- with open("config.json", 'w', encoding='utf-8') as f:
- json.dump(self.config, f, indent=4)
-
- def db_query(self, query: str, args: Union[tuple, dict], *, fetchone: bool=False, returnrowcount: bool=False, astuple: bool=False) -> Union[int, List[dict], dict]:
+ def db_query(
+ self,
+ query: str,
+ args: Union[tuple, dict],
+ *,
+ fetchone: bool = False,
+ returnrowcount: bool = False,
+ astuple: bool = False,
+ ) -> Union[int, List[dict], dict]:
"""Do any query to the bot database
If SELECT, it will return a list of results, or only the first result (if fetchone)
For any other query, it will return the affected row ID if returnrowscount, or the amount of affected rows (if returnrowscount)"""
@@ -143,20 +158,19 @@ def db_query(self, query: str, args: Union[tuple, dict], *, fetchone: bool=False
cursor.close()
return result
-
@property
def _(self) -> Callable[[Any, str], Coroutine[Any, Any, str]]:
"""Translate something"""
- cog = self.get_cog('Languages')
+ cog = self.get_cog("Languages")
if cog is None:
self.log.error("Unable to load Languages cog")
return lambda *args, **kwargs: args[1]
return cog.tr
-
- async def add_cog(self, cog: commands.Cog):
+
+ async def add_cog(self, cog: commands.Cog, icon=None):
"""Adds a "cog" to the bot.
A cog is a class that has its own event listeners and commands.
-
+
Parameters
-----------
cog: :class:`Cog`
@@ -170,16 +184,23 @@ async def add_cog(self, cog: commands.Cog):
CommandError
An error happened during loading.
"""
+
+ self.cog_icons.update({cog.qualified_name.lower(): icon})
+
await super().add_cog(cog)
for module in self.cogs.values():
- if type(cog) != type(module):
- if hasattr(module, 'on_anycog_load'):
+ if not isinstance(cog, type(module)):
+ if hasattr(module, "on_anycog_load"):
try:
module.on_anycog_load(cog)
- except:
+ except BaseException:
self.log.warning(f"[add_cog]", exc_info=True)
-
- def remove_cog(self, cog: str):
+
+ def get_cog_icon(self, cog_name):
+ """Get a cog icon"""
+ return self.cog_icons.get(cog_name.lower())
+
+ async def remove_cog(self, cog: str):
"""Removes a cog from the bot.
All registered commands and event listeners that the
@@ -192,30 +213,35 @@ def remove_cog(self, cog: str):
name: :class:`str`
The name of the cog to remove.
"""
- super().remove_cog(cog)
+ await super().remove_cog(cog)
for module in self.cogs.values():
- if type(cog) != type(module):
- if hasattr(module, 'on_anycog_unload'):
+ if not isinstance(cog, type(module)):
+ if hasattr(module, "on_anycog_unload"):
try:
module.on_anycog_unload(cog)
- except:
+ except BaseException:
self.log.warning(f"[remove_cog]", exc_info=True)
class CheckException(commands.CommandError):
"""Exception raised when a custom check failed, to send errors when needed"""
+
def __init__(self, id, *args):
super().__init__(message=f"Custom check '{id}' failed", *args)
self.id = id
+
def setup_logger():
"""Create the logger module, used for logs"""
# on chope le premier logger
log = logging.getLogger("runner")
# on défini un formatteur
format = logging.Formatter(
- "%(asctime)s %(levelname)s: %(message)s", datefmt="[%d/%m/%Y %H:%M]")
- # ex du format : [08/11/2018 14:46] WARNING RSSCog fetch_rss_flux l.288 : Cannot get the RSS flux because of the following error: (suivi du traceback)
+ "%(asctime)s %(levelname)s: %(message)s", datefmt="[%d/%m/%Y %H:%M]"
+ )
+ # ex du format : [08/11/2018 14:46] WARNING RSSCog fetch_rss_flux l.288 :
+ # Cannot get the RSS flux because of the following error: (suivi du
+ # traceback)
# log vers un fichier
file_handler = logging.FileHandler("logs/debug.log")
@@ -230,7 +256,7 @@ def setup_logger():
stream_handler.setFormatter(format)
# supposons que tu veuille collecter les erreurs sur ton site d'analyse d'erreurs comme sentry
- #sentry_handler = x
+ # sentry_handler = x
# sentry_handler.setLevel(logging.ERROR) # on veut voir que les erreurs et au delà, pas en dessous
# sentry_handler.setFormatter(format)
@@ -246,14 +272,33 @@ def setup_logger():
return log
-CONFIG_OPTIONS: Dict[str, Dict[str, Any]] = {}
-if os.path.isfile('./config/global_options.json'):
- with open('./config/global_options.json') as config:
- CONFIG_OPTIONS.update(json.load(config))
+CONFIG_OPTIONS: Dict[str, Dict[str, Any]] = {}
-for plugin in os.listdir('./plugins/'):
- if plugin[0] != '_':
- if os.path.isfile('./plugins/' + plugin + '/config/options.json'):
- with open('./plugins/' + plugin + '/config/options.json') as config:
- CONFIG_OPTIONS.update(json.load(config))
\ No newline at end of file
+from core import config
+
+CONFIG_OPTIONS.update(
+ {
+ "prefix": {
+ "default": config.get("bot.default_prefix"),
+ "type": "text",
+ "command": 'prefix',
+ },
+ "language": {
+ "default": config.get("bot.default_language"),
+ "type": "text",
+ "command": 'language',
+ },
+ "admins": {
+ "default": config.get("bot.admins"),
+ "type": "categories",
+ "command": None,
+ },
+ }
+)
+
+for plugin in os.listdir("./plugins/"):
+ if plugin[0] != "_":
+ if os.path.isfile("./plugins/" + plugin + "/config/options.json"):
+ with open("./plugins/" + plugin + "/config/options.json") as config:
+ CONFIG_OPTIONS.update(json.load(config))