2 |
3 | **Pré-requis:** Avoir mis en place au moins un détecteur
4 |
5 | ## ✨ Description d'un automate
6 |
7 | Cette section va vous permettre de créer un automate attaché à un de vos détecteurs.
8 |
9 | Allez sur le menu "Description des Automates" puis appuyez sur "Créer".
10 |
11 |
12 |
13 |
14 |
15 | Ce n'est pas ici que vous allez créer vos actions.
16 |
17 | Un automate ne peut pas être attaché à plus d'un détecteur.
18 | ⚠️ Par contre il est possible de créer un automate sans détecteur, on parlera de routine ou de plugin.
19 |
20 | ## Édition des actions
21 |
22 | Une fois la description de votre automate effectuée, nous vous invitons à revenir sur le menu "Éditeur d'Automate".
23 |
24 | ## Pour aller plus loin
25 |
26 | - [ ] [Mettre en oeuvre une suite d'action à appliquer après la détection](CHAPITRE-6.md)
27 |
--------------------------------------------------------------------------------
/hermes_ui/templates/admin/rediscli/response.html:
--------------------------------------------------------------------------------
1 | {% macro render(item, depth=0) %}
2 | {% set type = type_name(item) %}
3 |
4 | {% if type == 'tuple' or type == 'list' %}
5 | {% if not item %}
6 | Empty {{ type }}.
7 | {% else %}
8 | {% for n in item %}
9 | {{ loop.index }}) {{ render(n, depth + 1) }}
10 | {% endfor %}
11 | {% endif %}
12 | {% elif type == 'bool' %}
13 | {% if depth == 0 and item %}
14 | OK
15 | {% else %}
16 | {{ item }}
17 | {% endif %}
18 | {% elif type == 'str' or type == 'unicode' %}
19 | "{{ item }}"
20 | {% elif type == 'bytes' %}
21 | "{{ item.decode('utf-8') }}"
22 | {% elif type == 'TextWrapper' %}
23 |
{{ item }}
24 | {% elif type == 'dict' %}
25 | {% for k, v in item.items() %}
26 | {{ loop.index }}) {{ k }} - {{ render(v, depth + 1) }}
27 | {% endfor %}
28 | {% else %}
29 | {{ item }}
30 | {% endif %}
31 | {% endmacro %}
32 | {{ render(result) }}
--------------------------------------------------------------------------------
/hermes_ui/templates/admin/model/create.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/master.html' %}
2 | {% import 'admin/lib.html' as lib with context %}
3 | {% from 'admin/lib.html' import extra with context %} {# backward compatible #}
4 | {% import 'admin/adminlte/forms.html' as forms with context %}
5 |
6 | {% block head %}
7 | {{ super() }}
8 | {{ lib.form_css() }}
9 | {% endblock %}
10 |
11 | {% block body %}
12 |
13 |
2 |
3 | ## ✨ Configuration
4 |
5 | Tout le principe d'Hermes est de réagir à une certaine typologie de message électronoqie.
6 | Nous avons alors besoin d'une source de données dans laquelle lire les messages en entrés.
7 |
8 | Le programme permet, clé en main de configurer l'accès à une boite IMAP4 en sachant :
9 |
10 | - Hôte distante (IP ou DNS)
11 | - Nom d'utilisateur pour s'authentifier
12 | - Mot de passe associé à l'utilisateur
13 | - Dossier dans lequel se placer pour lire et analyser les messages, par défaut **INBOX**.
14 |
15 | Pour cela, nous vous invitons à vous rendre dans le menu "Sources de données" puis "Boite aux lettres (IMAP)".
16 | Une fois sur la liste des boîtes, nous vous invitons à cliquer sur "Créer".
17 |
18 | 🔒 Nous vous conseillons de laisser coché "TLS" et "Vérification Certificat" pour plus de sécurité.
19 | ❓ La case **Activation** permet d'autoriser Hermès à inclure cet automate lors de la surveillance de(s) boite(s) IMAP. Inversement pour test(s) uniquement(s).
20 | ⚠️ L'option "Legacy TLS" permet d'essayer de négocier une connexion sur un serveur ayant des protocols déchus. Cette option est déconseillée et risque de ne pas fonctionner selon vos installations locales. (openssl)
21 |
22 | ## Utilisation de variable
23 |
24 | Les champs **Hôte distante**, **Nom d'utilisateur**, **Mot de passe** peuvent contenir des variables au format `{{ ma_variable }}`.
25 |
26 | ## Fournisseurs compatibles
27 |
28 | N'importe quel fournisseur de messagerie est compatible, mais sachez que certain fournisseur exige un niveau
29 | d'authentification plus important que le couple *utilisateur, mot de passe*.
30 |
31 | Pour que GMail soit compatible, il faut d'abord activer l'accès moins sécurisée. (cf. google)
32 |
33 | ## Pour aller plus loin
34 |
35 | - [ ] [Détecter un message électronique](CHAPITRE-4.md)
36 |
--------------------------------------------------------------------------------
/docs/GMAIL.md:
--------------------------------------------------------------------------------
1 |
GMail
2 |
3 | Nous allons vous guidez dans la mise en oeuvre de la connexion à votre compte GMail.
4 |
5 | ## Authentification
6 |
7 | Hôte IMAP : `imap.gmail.com`
8 | Hôte SMTP : `smtp.gmail.com`
9 | utilisateur : `mon_compte@gmail.com`
10 |
11 | mot de passe : **Vous ne pouvez pas utiliser votre mot de passe habituel ! Vous devez en créer un spécialement pour hermes.**
12 |
13 | D'abord recherchez "gmail generate app password" sur Google.
14 |
15 | ```
16 | Create & use App Passwords
17 | Go to your Google Account.
18 | On the left navigation panel, choose Security.
19 | On the "Signing in to Google" panel, choose App Passwords. ...
20 | At the bottom, choose Select app and choose the app you're using.
21 | Choose Select device and choose the device you're using.
22 | Choose Generate.
23 | ```
24 |
25 | [Générer son mot de passe d'Application](https://support.google.com/accounts/answer/185833?hl=fr)
26 |
27 | ## Configuration GMAIL nécessaire
28 |
29 | Google dispose d'une implémentation IMAP4 modifiée et cela risque de poser un problème lors de la suppression des messages.
30 | Cela vient de la manière dont les instructions `DELETE` et `EXPUNGE` sont interprétées.
31 |
32 | Si vous souhaitez pallier à ce problème de message non supprimable depuis Hermès :
33 |
34 | - Connectez-vous sur votre compte gmail depuis votre navigateur internet.
35 | - Une fois sur votre boite de reception, choississez réglages, en haut à droite de la liste des messages. (Roue crantée puis réglages.)
36 | - Choisir l'onglet 'Boîte de réception'
37 | - Cochez le bouton radio 'Laisser le client mail choisir pour la suppression'
38 | - À votre convenance, choisir comment sera interprété la suppression. (i) suppression immédiate (ii) déplacer dans corbeille.
39 |
40 |
41 |
--------------------------------------------------------------------------------
/test/test_detecteur.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from hermes.detecteur import *
3 | from hermes.source import Source
4 |
5 | ma_source = Source(
6 | "#Mesures Relève des températures du mois d'Août le 12/12/2020",
7 | """Réf-091
8 |
9 | Bonjour JOHN DOE !
10 |
11 | Date de mesure : 12/12/2020
12 | Auteur : Ahmed TAHRI
13 |
14 | Nous avons mesurés à 38 reprises la température de votre ville natale.
15 |
16 | Merci de votre attention.
17 | """
18 | )
19 |
20 | mon_detecteur = Detecteur(
21 | "Relève de température"
22 | )
23 |
24 | mon_detecteur.je_veux(
25 | IdentificateurRechercheInteret(
26 | "Recherche de la référence",
27 | "Réf-"
28 | )
29 | )
30 |
31 | mon_detecteur.je_veux(
32 | DateRechercheInteret(
33 | "Recherche date de relève",
34 | "Relève des températures du mois d'Août le"
35 | )
36 | )
37 |
38 | mon_detecteur.je_veux(
39 | ExpressionCleRechercheInteret(
40 | "Recherche d'une phrase à l'identique",
41 | "Nous avons mesurés à"
42 | )
43 | )
44 |
45 | mon_detecteur.je_veux(
46 | LocalisationExpressionRechercheInteret(
47 | "Recherche du nombre de relève température",
48 | "reprises",
49 | "Nous avons mesurés à"
50 | )
51 | )
52 |
53 | mon_detecteur.je_veux(
54 | InformationRechercheInteret(
55 | "Recherche de hashtag",
56 | "Mesures"
57 | )
58 | )
59 |
60 | mon_detecteur.je_veux(
61 | CleRechercheInteret(
62 | "Présence de Auteur",
63 | "Auteur"
64 | )
65 | )
66 |
67 | mon_detecteur.je_veux(
68 | ExpressionDansCleRechercheInteret(
69 | "Vérifier que Ahmed est auteur",
70 | "Auteur",
71 | "Ahmed"
72 | )
73 | )
74 |
75 |
76 | class TestDetecteur(unittest.TestCase):
77 |
78 | def test_detection(self):
79 |
80 | self.assertTrue(
81 | mon_detecteur.lance_toi(ma_source)
82 | )
83 |
84 |
85 | if __name__ == '__main__':
86 | unittest.main()
87 |
--------------------------------------------------------------------------------
/hermes_ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "build-dev": "node_modules/.bin/webpack --config webpack.config.js --mode development",
4 | "build": "node_modules/.bin/webpack --config webpack.config.js --mode production"
5 | },
6 | "dependencies": {
7 | "@babel/core": "7",
8 | "admin-lte": "2.3.8",
9 | "awesomplete": "^1.1.5",
10 | "babel-cli": "^6.26.0",
11 | "babel-core": "^6.26.3",
12 | "babel-loader": "^8.0.6",
13 | "bootstrap": "3",
14 | "clipboard": "^2.0.4",
15 | "css-loader": "^3.2.0",
16 | "cssnano": "^4.1.10",
17 | "datatables.net-bs": "^1.10.20",
18 | "dropzone": "^5.5.1",
19 | "extract-text-webpack-plugin": "^0.8.1",
20 | "fastclick": "^1.0.6",
21 | "file-loader": "^5.1.0",
22 | "font-awesome": "^4.7.0",
23 | "highlight.js": "^10.4.1",
24 | "icheck": "^1.0.2",
25 | "image-webpack-loader": "6.0",
26 | "intro.js": "^2.9.3",
27 | "jquery": "3",
28 | "jquery-sidebar": "^3.3.2",
29 | "jquery-slimscroll": "^1.3.8",
30 | "jquery-ui": "^1.12.1",
31 | "jquery.terminal": "^2.8.0",
32 | "jsoneditor": "^7.1.0",
33 | "jstree": "^3.3.8",
34 | "lodash.template": "^4.5.0",
35 | "manifest-revision-webpack-plugin": "^0.0.5",
36 | "mini-css-extract-plugin": "^0.8.0",
37 | "moment": "^2.24.0",
38 | "optimize-css-assets-webpack-plugin": "^5.0.3",
39 | "postcss-loader": "^3.0.0",
40 | "script-loader": "^0.6.1",
41 | "select2": "^4.0.11",
42 | "serialize-javascript": "^3.1.0",
43 | "style-loader": "^1.0.0",
44 | "sweetalert2": "^8.18.5",
45 | "terser-webpack-plugin": "^2.2.1",
46 | "tunnel-agent": "^0.6.0",
47 | "uglifyjs-webpack-plugin": "^2.2.0",
48 | "url-loader": "^2.2.0",
49 | "webpack": "4",
50 | "webpack-manifest-plugin": "^2.2.0"
51 | },
52 | "name": "hermes",
53 | "version": "1.0.0",
54 | "main": "index.js",
55 | "author": "Hermès",
56 | "license": "NPOSL-3.0",
57 | "devDependencies": {
58 | "webpack-cli": "^3.3.9"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/hermes_ui/templates/security/login_user.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/master.html' %}
2 | {% from "security/_macros.html" import render_field, render_field_with_errors, render_checkbox_field, render_button %}
3 | {% include "security/_messages.html" %}
4 | {% block head %}
5 |
10 | {% endblock head %}
11 | {% block page_body %}
12 |
2 |
3 | Nous développons ici pour chaque type de critère :
4 |
5 | - Une courte description de ce qui est recherché
6 | - Ce qui est capturé (stockable dans une variable)
7 |
8 | ## Identifiant
9 |
10 | La recherche d'identifiant correspond à tout ce qui ressemble à PREFIXE-NUMEROS.
11 |
12 | ```
13 | Ticket D91827631 : Changement majeur de l'infra PBX-FR
14 | ```
15 |
16 | **Exemple :** « On recherche un identifiant commençant par la lettre D dans le titre du message »
17 |
18 | **Capture :** D91827631
19 |
20 | Ici le préfixe est `D`.
21 |
22 | ## Recherche d'expression
23 |
24 | Rechercher une expression localisable
25 |
26 | ```
27 | Bienvenue à Antoine GAUTIER au sein de la ville de Paris
28 | ```
29 |
30 | **Exemple :** « Je recherche une expression comprise entre, ‘Bienvenue à’... et ...’au sein de la ville de Paris’ dans le corps du message »
31 |
32 | **Capture :** `Antoine GAUTIER`
33 |
34 | ## Date
35 |
36 | Trouver une date peu importe le format de représentation sachant le prefixe.
37 | - RFC 3339
38 | - RFC 2822
39 | - Y-m-d
40 | - d-m-Y
41 |
42 | ```
43 | Bilan du 10/11/2020 pour le concours d'excellence
44 | ```
45 |
46 | **Exemple :** « Je recherche une date juxtaposée à l’expression ‘Bilan du‘ dans le titre du message »
47 |
48 | **Capture :** `10/11/2020`
49 |
50 | ## XPath (HTML)
51 |
52 | Trouver un noeud dans un arbre XML soit le corps HTML de votre message.
53 |
54 | ```html
55 |
56 |
57 |
58 | Bonjour !
59 |
60 |
61 |
62 | ```
63 |
64 | **Exemple :** « Je souhaite extraire le contenu de la première div ayant la classe .sujet »
65 |
66 | **Capture :** `Bonjour !`
67 |
68 | ## Clé
69 |
70 | **Brève explication :** Le programme Hermès arrive à trouver automatiquement certaine association explicite dans un texte.
71 | Toute expression sous la forme de A -> B (A associé à B). Exemple : « Contact Externe : Ahmed TAHRI »
72 | Dans cet exemple, « Contact Externe » sera la clé auto-découverte.
73 |
74 | ```
75 | Bonjour Michael,
76 |
77 | Votre ticket de support numéro 761637 est maintenant ouvert.
78 |
79 | Corresp. interne : Dep. RH
80 | Contact Externe : Ahmed TAHRI
81 |
82 | Merci de votre patience.
83 | ```
84 |
85 | **Exemple :** « Je vérifie que le moteur a trouvé la clé ‘Contact Externe’ dans le message »
86 |
87 | **Capture :** `Ahmed TAHRI`
88 |
89 | ## Expression exacte dans la clé
90 |
91 | Permet de vérifier la présence d'un mot ou d'une suite de mot depuis la valeur associée à une clé auto-découverte par
92 | Hermès.
93 |
94 | **Exemple :** « Je vérifie que la clé ‘Contact Externe’ contient bien »
95 |
96 | **Capture :** `Ahmed TAHRI`
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # celery beat schedule file
95 | celerybeat-schedule
96 |
97 | # SageMath parsed files
98 | *.sage.py
99 |
100 | # Environments
101 | .env
102 | .venv
103 | env/
104 | venv/
105 | ENV/
106 | env.bak/
107 | venv.bak/
108 |
109 | # Spyder project settings
110 | .spyderproject
111 | .spyproject
112 |
113 | # Rope project settings
114 | .ropeproject
115 |
116 | # mkdocs documentation
117 | /site
118 |
119 | # mypy
120 | .mypy_cache/
121 | .dmypy.json
122 | dmypy.json
123 |
124 | # Pyre type checker
125 | .pyre/
126 |
127 | .idea/
128 | msg_parser/
129 | configuration.yml
130 | configurations/*.yml
131 |
132 | node_modules/
133 | *.sqlite
134 | manifest.json
135 | .DS_Store
136 |
--------------------------------------------------------------------------------
/hermes_ui/models/session.py:
--------------------------------------------------------------------------------
1 | from hermes_ui.models.automate import Automate, ActionNoeud
2 | from hermes_ui.models.detecteur import Detecteur, RechercheInteret
3 | from hermes_ui.db import db
4 |
5 |
6 | class AutomateExecution(db.Model):
7 |
8 | id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True)
9 |
10 | automate_id = db.Column(db.Integer(), db.ForeignKey(Automate.id, ondelete='CASCADE'), nullable=False)
11 | automate = db.relationship(Automate, cascade='all, save-update, delete, merge')
12 |
13 | sujet = db.Column(db.String(255), nullable=False)
14 | corps = db.Column(db.Text(), nullable=False)
15 |
16 | date_creation = db.Column(db.DateTime(timezone=True), nullable=False)
17 |
18 | detecteur_id = db.Column(db.Integer(), db.ForeignKey(Detecteur.id, ondelete='CASCADE'), nullable=False)
19 | detecteur = db.relationship(Detecteur, cascade='all, save-update, merge')
20 |
21 | validation_detecteur = db.Column(db.Boolean(), default=False, nullable=False)
22 | validation_automate = db.Column(db.Boolean(), default=False, nullable=False)
23 |
24 | explications_detecteur = db.Column(db.Text(), nullable=True)
25 |
26 | logs = db.Column(db.Text(), nullable=True)
27 |
28 | date_finalisation = db.Column(db.DateTime(timezone=True), nullable=False)
29 |
30 | actions_noeuds_executions = db.relationship('ActionNoeudExecution')
31 | recherches_interets_executions = db.relationship('RechercheInteretExecution')
32 |
33 |
34 | class ActionNoeudExecution(db.Model):
35 |
36 | id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True)
37 |
38 | automate_execution_id = db.Column(db.BigInteger(), db.ForeignKey(AutomateExecution.id, ondelete='CASCADE'), nullable=False)
39 | automate_execution = db.relationship(AutomateExecution, cascade='all, save-update, delete, merge')
40 |
41 | action_noeud_id = db.Column(db.Integer(), db.ForeignKey(ActionNoeud.id, ondelete='CASCADE'))
42 | action_noeud = db.relationship(ActionNoeud)
43 |
44 | validation_action_noeud = db.Column(db.Boolean(), default=False, nullable=False)
45 |
46 | args_payload = db.Column(db.Text(), nullable=True)
47 | payload = db.Column(db.Text(), nullable=True)
48 |
49 |
50 | class RechercheInteretExecution(db.Model):
51 |
52 | id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True)
53 |
54 | automate_execution_id = db.Column(db.BigInteger(), db.ForeignKey(AutomateExecution.id), nullable=False)
55 | automate_execution = db.relationship(AutomateExecution)
56 |
57 | recherche_interet_id = db.Column(db.Integer(), db.ForeignKey(RechercheInteret.id))
58 | recherche_interet = db.relationship(RechercheInteret)
59 |
60 | validation_recherche_interet = db.Column(db.Boolean(), default=False, nullable=False)
61 |
62 | payload = db.Column(db.Text(), nullable=True)
63 |
64 |
65 | class AutomateExecutionDataTable:
66 |
67 | def __init__(self, executions):
68 | self.data = executions
69 |
--------------------------------------------------------------------------------
/docs/CHAPITRE-2.md:
--------------------------------------------------------------------------------
1 |
Création des variables globales
2 |
3 | ## ✨ C'est quoi ?
4 |
5 | Juste avant, nous avons parlé des variables *simplifiées* sous Hermes. Pour mémo, les variables peuvent être issus de :
6 |
7 | - Le résultat d'un critère de recherche
8 | - **Une variable accessible globalement**, depuis le menu "Mes variables globales"
9 | - Le résultat d'une action
10 |
11 | Il est parfois très utile de disposer d'une variable partagée peut-importe l'automate, comme par exemple vos identifiants SMTP pour envoyer un message.
12 |
13 | ## Où ?
14 |
15 | La création de vos variables globales est possible depuis le menu "Mes variables globales".
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## Choix de format
24 |
25 | ### Classique
26 |
27 | La désignation représente le nom de votre future variable.
28 |
29 | Imaginons que vous souhaiteriez conserver le nom d'utilisateur et le mot de passe SMTP.
30 |
31 | Vous allez créer deux variables, l'une `identifiant_smtp` et l'autre `mot_de_passe_smtp`.
32 | Qui seront par la suite accessible par la syntaxe `{{ identifiant_smtp }}`.
33 |
34 | Vous remplirez comme suit :
35 |
36 |
37 |
38 |
39 |
40 | ### Avancé
41 |
42 | Si vous le souhaitez, vous pouvez exploiter une valeur plus complexe. Vous pouvez insérer dans *Valeur* :
43 |
44 | - Une chaîne JSON
45 | - Une chaîne YAML
46 |
47 | Reprenons notre cas ci-dessus. Au lieu de créer deux variables `identifiant_smtp` et l'autre `mot_de_passe_smtp`,
48 | créons une seule variable `mon_compte_smtp`.
49 |
50 | Pour ce faire nous constituons une chaîne **JSON** tel que :
51 |
52 | ```json
53 | {
54 | "mon_compte_smtp": {
55 | "identifiant": "abcdef@mon-provider.com",
56 | "mot_de_passe": "azerty"
57 | }
58 | }
59 | ```
60 |
61 | Et donc en remplissant le formulaire de cette manière :
62 |
63 |
64 |
65 |
66 |
67 | Vous allez pouvoir invoquer `{{ mon_compte_smtp.identifiant }}` et `{{ mon_compte_smtp.mot_de_passe }}`.
68 |
69 | ⚠️ Vous remarquerez que le nom de votre variable n'est plus la **désignation** mais le nom de la clé/index racine. Ceci s'applique dans le cas où le format sélectionné est `JSON` ou `YAML`.
70 |
71 | ## Pour aller plus loin
72 |
73 | - [ ] [Mise en place de votre/vos boîte(s) IMAP](CHAPITRE-3.md)
74 |
--------------------------------------------------------------------------------
/hermes_ui/templates/admin/model/inline_list_base.html:
--------------------------------------------------------------------------------
1 | {% macro render_inline_fields(field, template, render, check=None) %}
2 |
3 | {# existing inline form fields #}
4 |
5 | {% for subfield in field %}
6 |
7 | {%- if not check or check(subfield) %}
8 |
25 |
26 | {%- endif -%}
27 | {{ render(subfield) }}
28 |
29 | {% endfor %}
30 |
31 |
32 | {# template for new inline form fields #}
33 |
52 | {% endmacro %}
53 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='hermes',
5 | version='1.0.15',
6 | author='Ahmed TAHRI',
7 | author_email='ahmed.tahri@cloudnursery.dev',
8 | description='Automates programmables à réaction aux échanges électroniques reçus depuis une boîte IMAP4',
9 | license='NPOSL-3.0',
10 | packages=['hermes', 'hermes_ui'],
11 | install_requires=[
12 | 'Flask==1.1.*',
13 | 'requests_html',
14 | 'python-slugify',
15 | 'requests>=2.23,<3.0',
16 | 'prettytable',
17 | 'imapclient>=2.1.0,<3.0',
18 | 'zeep>=3.4,<4.0',
19 | 'tqdm',
20 | 'emails>=0.6.1,<1.0',
21 | 'flask_security',
22 | 'flask_admin',
23 | 'flask_sqlalchemy==2.4.*',
24 | 'flask_migrate',
25 | 'pyyaml',
26 | 'marshmallow>=3.5.2,<4.0',
27 | 'flask_marshmallow>=0.12,<1.0',
28 | 'marshmallow-sqlalchemy>=0.23,<1.0',
29 | 'python-dateutil',
30 | 'jinja2',
31 | 'flask-emails',
32 | 'ruamel.std.zipfile',
33 | 'ics==0.5',
34 | 'olefile>=0.46,<1.0',
35 | 'html5lib',
36 | 'pandas',
37 | 'flask_babel>=1.0.*',
38 | 'records',
39 | 'flask_babel',
40 | 'unidecode',
41 | 'pandas',
42 | 'records',
43 | 'marshmallow-oneofschema>=2.0.1,<2.1',
44 | 'loguru',
45 | 'Flask-Webpack>=0.1,<1.0',
46 | 'mysql-connector-python',
47 | 'werkzeug==1.0.*',
48 | 'sqlalchemy==1.3.*',
49 | 'flask_webpackext==1.0.*',
50 | 'pyopenssl>=19.1.0',
51 | 'msg_parser>=1.1.0',
52 | 'wtforms',
53 | 'dateparser',
54 | 'kiss-headers>=2.0.4,<3.0',
55 | 'email_validator>=1.1.0'
56 | ],
57 | tests_require=[],
58 | keywords=[],
59 | dependency_links=[
60 | 'git+https://github.com/Ousret/python-emails.git#egg=emails'
61 | ],
62 | classifiers=[
63 | 'Development Status :: 5 - Production/Stable',
64 | 'Environment :: Win32 (MS Windows)',
65 | 'Environment :: X11 Applications',
66 | 'Environment :: MacOS X',
67 | 'Intended Audience :: Developers',
68 | 'License :: OSI Approved :: MIT License',
69 | 'Operating System :: OS Independent',
70 | 'Programming Language :: Python',
71 | 'Programming Language :: Python :: 3.6',
72 | 'Programming Language :: Python :: 3.7',
73 | 'Programming Language :: Python :: 3.8',
74 | ],
75 | message_extractors={
76 | 'hermes': [
77 | ('**.py', 'python', None),
78 | ('templates/**.html', 'jinja2', None),
79 | ('assets/**', 'ignore', None),
80 | ('static/**', 'ignore', None)
81 | ],
82 | 'hermes_ui': [
83 | ('**.py', 'python', None),
84 | ('templates/**.html', 'jinja2', None),
85 | ('assets/**', 'ignore', None),
86 | ('static/**', 'ignore', None)
87 | ],
88 | },
89 |
90 | )
91 |
--------------------------------------------------------------------------------
/hermes_ui/assets/styles/AdminLTE-select2.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Plugin: Select2
3 | * ---------------
4 | */
5 | .select2-container--default.select2-container--focus,
6 | .select2-selection.select2-container--focus,
7 | .select2-container--default:focus,
8 | .select2-selection:focus,
9 | .select2-container--default:active,
10 | .select2-selection:active {
11 | outline: none;
12 | }
13 | .select2-container--default .select2-selection--single,
14 | .select2-selection .select2-selection--single {
15 | border: 1px solid #d2d6de;
16 | border-radius: 0;
17 | padding: 6px 12px;
18 | height: 34px;
19 | }
20 | .select2-container--default.select2-container--open {
21 | border-color: #3c8dbc;
22 | }
23 | .select2-dropdown {
24 | border: 1px solid #d2d6de;
25 | border-radius: 0;
26 | }
27 | .select2-container--default .select2-results__option--highlighted[aria-selected] {
28 | background-color: #3c8dbc;
29 | color: white;
30 | }
31 | .select2-results__option {
32 | padding: 6px 12px;
33 | user-select: none;
34 | -webkit-user-select: none;
35 | }
36 | .select2-container .select2-selection--single .select2-selection__rendered {
37 | padding-left: 0;
38 | padding-right: 0;
39 | height: auto;
40 | margin-top: -4px;
41 | }
42 | .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
43 | padding-right: 6px;
44 | padding-left: 20px;
45 | }
46 | .select2-container--default .select2-selection--single .select2-selection__arrow {
47 | height: 28px;
48 | right: 3px;
49 | }
50 | .select2-container--default .select2-selection--single .select2-selection__arrow b {
51 | margin-top: 0;
52 | }
53 | .select2-dropdown .select2-search__field,
54 | .select2-search--inline .select2-search__field {
55 | border: 1px solid #d2d6de;
56 | }
57 | .select2-dropdown .select2-search__field:focus,
58 | .select2-search--inline .select2-search__field:focus {
59 | outline: none;
60 | border: 1px solid #3c8dbc;
61 | }
62 | .select2-container--default .select2-results__option[aria-disabled=true] {
63 | color: #999;
64 | }
65 | .select2-container--default .select2-results__option[aria-selected=true] {
66 | background-color: #ddd;
67 | }
68 | .select2-container--default .select2-results__option[aria-selected=true],
69 | .select2-container--default .select2-results__option[aria-selected=true]:hover {
70 | color: #444;
71 | }
72 | .select2-container--default .select2-selection--multiple {
73 | border: 1px solid #d2d6de;
74 | border-radius: 0;
75 | }
76 | .select2-container--default .select2-selection--multiple:focus {
77 | border-color: #3c8dbc;
78 | }
79 | .select2-container--default.select2-container--focus .select2-selection--multiple {
80 | border-color: #d2d6de;
81 | }
82 | .select2-container--default .select2-selection--multiple .select2-selection__choice {
83 | background-color: #3c8dbc;
84 | border-color: #367fa9;
85 | padding: 1px 10px;
86 | color: #fff;
87 | }
88 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
89 | margin-right: 5px;
90 | color: rgba(255, 255, 255, 0.7);
91 | }
92 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
93 | color: #fff;
94 | }
95 | .select2-container .select2-selection--single .select2-selection__rendered {
96 | padding-right: 10px;
97 | }
98 |
--------------------------------------------------------------------------------
/hermes_ui/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | # this is the Alembic Config object, which provides
12 | # access to the values within the .ini file in use.
13 | config = context.config
14 |
15 | # Interpret the config file for Python logging.
16 | # This line sets up loggers basically.
17 | fileConfig(config.config_file_name)
18 | logger = logging.getLogger('alembic.env')
19 |
20 | # add your model's MetaData object here
21 | # for 'autogenerate' support
22 | # from myapp import mymodel
23 | # target_metadata = mymodel.Base.metadata
24 | from flask import current_app
25 | config.set_main_option(
26 | 'sqlalchemy.url', current_app.config.get(
27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
28 | target_metadata = current_app.extensions['migrate'].db.metadata
29 |
30 | # other values from the config, defined by the needs of env.py,
31 | # can be acquired:
32 | # my_important_option = config.get_main_option("my_important_option")
33 | # ... etc.
34 |
35 |
36 | def run_migrations_offline():
37 | """Run migrations in 'offline' mode.
38 |
39 | This configures the context with just a URL
40 | and not an Engine, though an Engine is acceptable
41 | here as well. By skipping the Engine creation
42 | we don't even need a DBAPI to be available.
43 |
44 | Calls to context.execute() here emit the given string to the
45 | script output.
46 |
47 | """
48 | url = config.get_main_option("sqlalchemy.url")
49 | context.configure(
50 | url=url, target_metadata=target_metadata, literal_binds=True
51 | )
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online():
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 |
65 | # this callback is used to prevent an auto-migration from being generated
66 | # when there are no changes to the schema
67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
68 | def process_revision_directives(context, revision, directives):
69 | if getattr(config.cmd_opts, 'autogenerate', False):
70 | script = directives[0]
71 | if script.upgrade_ops.is_empty():
72 | directives[:] = []
73 | logger.info('No changes in schema detected.')
74 |
75 | connectable = engine_from_config(
76 | config.get_section(config.config_ini_section),
77 | prefix='sqlalchemy.',
78 | poolclass=pool.NullPool,
79 | )
80 |
81 | with connectable.connect() as connection:
82 | context.configure(
83 | connection=connection,
84 | target_metadata=target_metadata,
85 | process_revision_directives=process_revision_directives,
86 | **current_app.extensions['migrate'].configure_args
87 | )
88 |
89 | with context.begin_transaction():
90 | context.run_migrations()
91 |
92 |
93 | if context.is_offline_mode():
94 | run_migrations_offline()
95 | else:
96 | run_migrations_online()
97 |
--------------------------------------------------------------------------------
/hermes_ui/webpack.config.js:
--------------------------------------------------------------------------------
1 | let webpack = require('webpack');
2 | let config = require('./assets/config');
3 | let ManifestPlugin = require('webpack-manifest-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const TerserPlugin = require('terser-webpack-plugin');
6 | let OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
7 | let path = require('path');
8 |
9 | module.exports = {
10 | context: path.join(__dirname, config.build.context),
11 | entry: {
12 | app: "./scripts/app.js",
13 | app_help: './scripts/app_help.js',
14 | app_hermes: './scripts/app_hermes.js'
15 | },
16 | output: {
17 | path: path.join(__dirname, config.build.assetsPath),
18 | filename: 'js/[name].[chunkhash].js',
19 | publicPath: path.join(__dirname, config.build.assetsURL)
20 | },
21 | optimization: {
22 | minimizer: [new TerserPlugin()],
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.css$/,
28 | use: [
29 | {
30 | loader: MiniCssExtractPlugin.loader,
31 | options: {
32 | publicPath: '../',
33 | hmr: process.env.NODE_ENV === 'development',
34 | },
35 | },
36 | //'style-loader',
37 | 'css-loader'
38 | ],
39 | },
40 | {
41 | test: /\.js$/,
42 | loader: 'babel-loader',
43 | exclude: /node_modules/
44 | },
45 | {
46 | test: /\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/,
47 | loader: 'url-loader',
48 | options: {
49 | limit: 10000,
50 | name: 'fonts/[name].[hash:7].[ext]',
51 | publicPath: '../build/',
52 | }
53 | },
54 | {
55 | test: /\.(png|jpg|gif)$/i,
56 | use: [
57 | {
58 | loader: 'url-loader',
59 | options: {
60 | limit: 8192,
61 | },
62 | },
63 | ],
64 | },
65 | ]
66 | },
67 | plugins: [
68 | new ManifestPlugin({
69 | fileName: 'manifest.json',
70 | stripSrc: true,
71 | publicPath: config.build.assetsURL
72 | }),
73 | new MiniCssExtractPlugin(
74 | {
75 | // Options similar to the same options in webpackOptions.output
76 | // all options are optional
77 | filename: '[name].css',
78 | chunkFilename: '[id].css',
79 | ignoreOrder: false, // Enable to remove warnings about conflicting order
80 |
81 | }
82 | ),
83 | new OptimizeCssAssetsPlugin({
84 | assetNameRegExp: /\.css$/g,
85 | cssProcessor: require('cssnano'),
86 | cssProcessorPluginOptions: {
87 | preset: ['default', {discardComments: {removeAll: true}}],
88 | },
89 | canPrint: true
90 | }),
91 | // new webpack.ProvidePlugin({
92 | // jQuery: 'jquery/src/jquery',
93 | // $: 'jquery/src/jquery',
94 | // jquery: 'jquery/src/jquery',
95 | // 'window.jQuery': 'jquery/src/jquery'
96 | // })
97 | ]
98 | };
--------------------------------------------------------------------------------
/test/test_automate.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from hermes.automate import *
3 | from hermes.detecteur import *
4 |
5 |
6 | class FakeSource(Source):
7 |
8 | def __init__(self, titre, corps):
9 | super().__init__(titre, corps)
10 | self._titre = titre
11 | self._corps = corps
12 |
13 | def titre(self):
14 | return self._titre
15 |
16 | def corps(self):
17 | return self._corps
18 |
19 |
20 | ma_source = FakeSource(
21 | "#Mesures Relève des températures du mois d'Août le 12/12/2020",
22 | """Réf-091
23 |
24 | Bonjour JOHN DOE !
25 |
26 | Date de mesure : 12/12/2020
27 | Auteur : Ahmed TAHRI
28 |
29 | Nous avons mesurés à 38 reprises la température de votre ville natale.
30 |
31 | Merci de votre attention.
32 | """
33 | )
34 |
35 | mon_detecteur = Detecteur(
36 | "Relève de température"
37 | )
38 |
39 | mon_detecteur.je_veux(
40 | IdentificateurRechercheInteret(
41 | "Recherche de la référence",
42 | "Réf-",
43 | friendly_name='reference_releve'
44 | )
45 | )
46 |
47 | mon_detecteur.je_veux(
48 | DateRechercheInteret(
49 | "Recherche date de relève",
50 | "Relève des températures du mois d'Août le",
51 | friendly_name='date_releve'
52 | )
53 | )
54 |
55 | mon_detecteur.je_veux(
56 | ExpressionCleRechercheInteret(
57 | "Recherche d'une phrase à l'identique",
58 | "Nous avons mesurés à"
59 | )
60 | )
61 |
62 | mon_detecteur.je_veux(
63 | LocalisationExpressionRechercheInteret(
64 | "Recherche du nombre de relève température",
65 | "reprises",
66 | "Nous avons mesurés à",
67 | friendly_name='nombre_releve'
68 | )
69 | )
70 |
71 | mon_detecteur.je_veux(
72 | InformationRechercheInteret(
73 | "Recherche de hashtag",
74 | "Mesures"
75 | )
76 | )
77 |
78 | mon_detecteur.je_veux(
79 | CleRechercheInteret(
80 | "Présence de Auteur",
81 | "Auteur"
82 | )
83 | )
84 |
85 | mon_detecteur.je_veux(
86 | ExpressionDansCleRechercheInteret(
87 | "Vérifier que Ahmed est auteur",
88 | "Auteur",
89 | "Ahmed"
90 | )
91 | )
92 |
93 | action_a = RequeteHttpActionNoeud(
94 | "Requête sur httpbin",
95 | "https://httpbin.org/post",
96 | "POST",
97 | {
98 | 'nombre_releve': '{{ nombre_releve }}',
99 | 'date_releve': '{{ date_releve }}',
100 | 'id': '{{ reference_releve }}'
101 | },
102 | None,
103 | None,
104 | 200,
105 | friendly_name='reponse_webservice_httpbin'
106 | )
107 |
108 | action_b = ComparaisonVariableActionNoeud(
109 | "Vérifier la cohérence réponse du website",
110 | "{{ reponse_webservice_httpbin.form.id }}",
111 | '==',
112 | '{{ reference_releve }}',
113 | None
114 | )
115 |
116 |
117 | class TestAutomate(unittest.TestCase):
118 | def test_automate_basic(self):
119 | mon_automate = Automate(
120 | "Réaction à la reception des mesures de température",
121 | mon_detecteur
122 | )
123 |
124 | mon_automate.action_racine = action_a
125 | action_a.je_realise_en_cas_reussite(action_b)
126 |
127 | self.assertTrue(
128 | mon_automate.lance_toi(
129 | ma_source
130 | )
131 | )
132 |
133 | self.assertEqual(
134 | 2,
135 | len(mon_automate.actions_lancees)
136 | )
137 |
138 |
139 | if __name__ == '__main__':
140 | unittest.main()
141 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at ahmed.tahri@cloudnursery.dev. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/hermes_ui/incident.py:
--------------------------------------------------------------------------------
1 | from hermes.automate import EnvoyerMessageSmtpActionNoeud
2 | from hermes.source import Source
3 | from hermes.logger import logger, mem_handler
4 |
5 | from .models import Automate
6 |
7 |
8 | class SourceLogger(Source):
9 |
10 | def __init__(self):
11 | super().__init__('', '')
12 |
13 | self._destinataire = 'admin@localhost'
14 | self._raw_content = '\n'.join([str(el.msg) for el in mem_handler.buffer]).encode('utf-8')
15 |
16 | @property
17 | def raw(self):
18 | return self._raw_content
19 |
20 | @property
21 | def nom_fichier(self):
22 | return 'interoperabilite.log'
23 |
24 | @property
25 | def destinataire(self):
26 | return self._destinataire
27 |
28 | @destinataire.setter
29 | def destinataire(self, nouveau_destinataire):
30 | self._destinataire = nouveau_destinataire
31 |
32 | @property
33 | def titre(self):
34 | return 'Traces de votre interopérabilité'
35 |
36 |
37 | class NotificationIncident:
38 |
39 | EMAIL_HOST = None
40 | EMAIL_PORT = None
41 | EMAIL_TIMEOUT = None
42 | EMAIL_USE_TLS = None
43 | EMAIL_HOST_USER = None
44 | EMAIL_HOST_PASSWORD = None
45 | EMAIL_FROM = None
46 |
47 | EMAIL_TO_DEFAULT = None
48 |
49 | @staticmethod
50 | def init_app(app):
51 | """
52 | :param flask.Flask app:
53 | """
54 | NotificationIncident.EMAIL_HOST = app.config.get('EMAIL_HOST', 'localhost')
55 | NotificationIncident.EMAIL_PORT = app.config.get('EMAIL_PORT', 25)
56 | NotificationIncident.EMAIL_TIMEOUT = app.config.get('EMAIL_TIMEOUT', 10)
57 | NotificationIncident.EMAIL_USE_TLS = app.config.get('EMAIL_USE_TLS', False)
58 | NotificationIncident.EMAIL_HOST_USER = app.config.get('EMAIL_HOST_USER', None)
59 | NotificationIncident.EMAIL_HOST_PASSWORD = app.config.get('EMAIL_HOST_PASSWORD', None)
60 | NotificationIncident.EMAIL_FROM = app.config.get('EMAIL_FROM', None)
61 |
62 | NotificationIncident.EMAIL_TO_DEFAULT = app.config.get('INCIDENT_NOTIFIABLE', None)
63 |
64 | @staticmethod
65 | def prevenir(automate, source, titre, description):
66 | """
67 | :param Automate automate:
68 | :param Source source:
69 | :param str titre:
70 | :param str description:
71 | :return:
72 | """
73 |
74 | if NotificationIncident.EMAIL_HOST is None or NotificationIncident.EMAIL_PORT is None or NotificationIncident.EMAIL_USE_TLS is None:
75 | return False
76 |
77 | ma_fausse_source = SourceLogger()
78 | ma_fausse_source.destinataire = NotificationIncident.EMAIL_FROM
79 |
80 | if automate is None:
81 | return False
82 |
83 | mon_action = EnvoyerMessageSmtpActionNoeud(
84 | "Envoyer une notification d'erreur au(x) responsable(s) de l'automate",
85 | str(NotificationIncident.EMAIL_TO_DEFAULT) + ((',' + automate.responsable_derniere_modification.email) if automate.responsable_derniere_modification is not None else ''),
86 | titre,
87 | description,
88 | hote_smtp=NotificationIncident.EMAIL_HOST,
89 | port_smtp=NotificationIncident.EMAIL_PORT,
90 | nom_utilisateur=NotificationIncident.EMAIL_HOST_USER,
91 | mot_de_passe=NotificationIncident.EMAIL_HOST_PASSWORD,
92 | enable_tls=NotificationIncident.EMAIL_USE_TLS,
93 | pj_source=True,
94 | source_pj_complementaire=source,
95 | force_keep_template=True
96 | )
97 |
98 | return mon_action.je_realise(ma_fausse_source)
99 |
--------------------------------------------------------------------------------
/test/test_critere.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from hermes.source import Source
3 | from hermes.detecteur import *
4 |
5 |
6 | ma_source = Source(
7 | "#Mesures Relève des températures du mois d'Août le 12/12/2020",
8 | """Réf-091
9 |
10 | Bonjour JOHN DOE !
11 |
12 | Date de mesure : 12/12/2020
13 | Auteur : Ahmed TAHRI
14 |
15 | Nous avons mesurés à 38 reprises la température de votre ville natale.
16 |
17 | Merci de votre attention.
18 | """
19 | )
20 |
21 |
22 | class TestCriteres(unittest.TestCase):
23 |
24 | def test_identifiant(self):
25 |
26 | critere = IdentificateurRechercheInteret(
27 | "Recherche de la référence",
28 | "Réf-"
29 | )
30 |
31 | self.assertTrue(
32 | critere.tester_sur(ma_source.extraction_interet)
33 | )
34 |
35 | self.assertEqual(
36 | critere.value,
37 | "Réf-091"
38 | )
39 |
40 | def test_date(self):
41 |
42 | critere = DateRechercheInteret(
43 | "Recherche date de relève",
44 | "Relève des températures du mois d'Août le"
45 | )
46 |
47 | self.assertTrue(
48 | critere.tester_sur(ma_source.extraction_interet)
49 | )
50 |
51 | self.assertEqual(
52 | critere.value,
53 | ' 12/12/2020'
54 | )
55 |
56 | def test_expression_exacte(self):
57 |
58 | critere = ExpressionCleRechercheInteret(
59 | "Recherche d'une phrase à l'identique",
60 | "Nous avons mesurés à"
61 | )
62 |
63 | self.assertTrue(
64 | critere.tester_sur(ma_source.extraction_interet)
65 | )
66 |
67 | self.assertEqual(
68 | critere.value,
69 | True
70 | )
71 |
72 | def test_localisation_expression(self):
73 |
74 | critere = LocalisationExpressionRechercheInteret(
75 | "Recherche du nombre de relève température",
76 | "reprises",
77 | "Nous avons mesurés à"
78 | )
79 |
80 | self.assertTrue(
81 | critere.tester_sur(ma_source.extraction_interet)
82 | )
83 |
84 | self.assertEqual(
85 | critere.value,
86 | '38'
87 | )
88 |
89 | def test_information(self):
90 |
91 | critere = InformationRechercheInteret(
92 | "Recherche de hashtag",
93 | "Mesures"
94 | )
95 |
96 | self.assertTrue(
97 | critere.tester_sur(ma_source.extraction_interet)
98 | )
99 |
100 | self.assertEqual(
101 | critere.value,
102 | "Mesures"
103 | )
104 |
105 | def test_cle_recherche(self):
106 |
107 | critere = CleRechercheInteret(
108 | "Présence de Auteur",
109 | "Auteur"
110 | )
111 |
112 | self.assertTrue(
113 | critere.tester_sur(ma_source.extraction_interet)
114 | )
115 |
116 | self.assertEqual(
117 | critere.value,
118 | "Ahmed TAHRI"
119 | )
120 |
121 | def test_expression_exacte_dans_cle(self):
122 |
123 | critere = ExpressionDansCleRechercheInteret(
124 | "Vérifier que Ahmed est auteur",
125 | "Auteur",
126 | "Ahmed"
127 | )
128 |
129 | self.assertTrue(
130 | critere.tester_sur(ma_source.extraction_interet)
131 | )
132 |
133 | self.assertEqual(
134 | critere.value,
135 | "Ahmed"
136 | )
137 |
138 |
139 | if __name__ == '__main__':
140 | unittest.main()
141 |
--------------------------------------------------------------------------------
/docs/CHAPITRE-7.md:
--------------------------------------------------------------------------------
1 |
Tester & Débugger un Automate
2 |
3 | **Pré-requis:** Avoir mis en place au moins un détecteur, la description associée d'un automate ainsi que des actions prêtes.
4 |
5 | ## ✨ Phase finale de conception
6 |
7 | Lors de la création d'un **détecteur** nous avions vu qu'il est possible de le tester au fur et à mesure de la conception.
8 | Il en va de même pour la création des actions d'un automate.
9 |
10 | ### Mode de test et de prodution
11 |
12 | Dans la zone "Choisir Automate" et aussi dans la boîte de dialogue création "Description d'un Automate",
13 | il y a une option "Production".
14 |
15 | Cette checkbox, décochée, permet d'empêcher le moteur de surveillance continue de votre boîte IMAP d'exécuter votre automate.
16 |
17 | Pendant la phase de conception il est recommandé de laisser votre automate en mode test. Donc checkbox décochée.
18 |
19 | ### Lancer l'automate seul
20 |
21 | Depuis la page "Éditeur d'Automate", selectionnez votre automate depuis la zone "Choisir Automate".
22 | Puis une fois dans cet état.
23 |
24 |
25 |
26 |
27 |
28 | Cliquez sur "Tester Automate", puis confirmer le démarrage.
29 |
30 | ⚠️ Une limitation empêche de pouvoir conduire un test autrement que depuis votre boîte IMAP.
31 | Ce qui signifie que vous devez vous assurer que :
32 |
33 | - Votre message type est disponible dans le dossier dans lequel Hermes ira lire les messages
34 | - La boucle de surveillance des messages est suspendue
35 | - Votre automate est en mode test
36 |
37 | Une fois les critères réunis, vous observerez le résultat en temps réel depuis la zone console.
38 |
39 |
40 |
41 |
42 |
43 | ### Résultat d'un automate
44 |
45 | Un automate termine par une réussite si la dernière action de l'arbre se termine correctement.
46 |
47 | ## 📊 Historique des lancements
48 |
49 | Hermes permet de consulter les 50 derniers lancements que ce soit en mode production ou de test depuis la zone
50 | "Historique des exécutions".
51 |
52 | ### 😞 Les erreurs critiques
53 |
54 | Une erreur critique est qqch qui ne se rattrape pas et qui empêche l'automate d'aboutir. Par ex. une variable non résolue.
55 |
56 | ⚠️ Les automates qui se solde par une erreur critique n'apparaissent pas dans l'historique. Néanmoins un message électronique est
57 | envoyé à :
58 |
59 | - Adresse de messagerie `INCIDENT_NOTIFIABLE` parametrée dans **configuration.yml**
60 | - Dernier éditeur de l'automate
61 | - Créateur de l'automate
62 |
63 | Ce rapport contient autant d'information que possible pour assister à la résolution.
64 |
65 | ### Debug
66 |
67 | Pour chaque rapport d'execution existe une ligne dans le tableau "Historique des exécutions".
68 | Ce tableau s'actualise lui aussi avec une latence de plus ou moins cinq secondes.
69 |
70 | Chaque ligne offre un récapitulatif succint de l'exécution.
71 |
72 | 
73 |
74 | Pour consulter les détails de chaque exécution, il est possible de cliquer sur le bouton de la colonne "Info".
75 |
76 | Vous observerez ansi un assistant similaire à celui de la création d'une action.
77 |
78 | **Revoir comment s'est déroulé la détection**
79 | 
80 | **Voir comment s'est exécutée une action et en connaître la réponse**
81 | 
82 |
83 | ⚠️ Les caches sur les images ne représentent pas la réalité, et sont ici pour protéger la confidentialité de mon environnement de production.
84 |
85 |
--------------------------------------------------------------------------------
/hermes_ui/adminlte/views.py:
--------------------------------------------------------------------------------
1 | from flask_admin.contrib import sqla
2 | from flask_security import current_user
3 | from flask import url_for, redirect, request, abort
4 | from flask_admin import menu
5 | from flask_security.utils import hash_password
6 | from flask_security.forms import EqualTo, unique_user_email
7 | from wtforms import fields, validators
8 | from wtforms.fields import html5
9 |
10 |
11 | class FaLink(menu.MenuLink):
12 |
13 | def __init__(self, name, url = None, endpoint = None, category = None, class_name = None, icon_type = "fa",
14 | icon_value = None, target = None):
15 | super(FaLink, self).__init__(name, url, endpoint, category, class_name, icon_type, icon_value, target)
16 |
17 |
18 | class FaModelView(sqla.ModelView):
19 |
20 | def __init__(self, model, session, name = None, category = None, endpoint = None, url = None, static_folder = None,
21 | menu_class_name = None, menu_icon_type = "fa", menu_icon_value = None):
22 | super(FaModelView, self).__init__(model, session, name, category, endpoint, url, static_folder, menu_class_name,
23 | menu_icon_type, menu_icon_value)
24 |
25 |
26 | class BaseAdminView(FaModelView):
27 | required_role = 'admin'
28 | can_export = True
29 | can_view_details = True
30 | can_create = True
31 | can_edit = True
32 | can_delete = True
33 | edit_modal = True
34 | create_modal = True
35 | details_modal = True
36 |
37 | def is_accessible(self):
38 | if not current_user.is_active or not current_user.is_authenticated:
39 | return False
40 |
41 | if current_user.has_role(self.required_role):
42 | return True
43 |
44 | return False
45 |
46 | def _handle_view(self, name, **kwargs):
47 | if not self.is_accessible():
48 | if current_user.is_authenticated:
49 | abort(403)
50 | else:
51 | return redirect(url_for('security.login', next = request.url))
52 |
53 |
54 | class AdminsView(BaseAdminView):
55 | required_role = 'superadmin'
56 |
57 | column_display_all_relations = True
58 | column_editable_list = ['email', 'first_name', 'last_name']
59 | column_searchable_list = ['roles.name', 'email', 'first_name', 'last_name']
60 | column_exclude_list = ['password']
61 | column_details_exclude_list = ['password']
62 | form_excluded_columns = ['password']
63 | column_filters = ['email', 'first_name', 'last_name']
64 | can_export = True
65 | can_view_details = True
66 | can_create = True
67 | can_edit = True
68 | can_delete = True
69 | edit_modal = True
70 | create_modal = True
71 | details_modal = True
72 |
73 | def on_model_change(self, form, model, is_created):
74 | """
75 | :param form:
76 | :param hermes_ui.adminlte.models.User model:
77 | :param is_created:
78 | :return:
79 | """
80 | model.password = hash_password(form.password.data)
81 |
82 | def get_create_form(self):
83 | CreateForm = super().get_create_form()
84 |
85 | CreateForm.email = html5.EmailField(
86 | 'Email',
87 | validators=[
88 | validators.DataRequired(),
89 | validators.Email(),
90 | unique_user_email,
91 | ],
92 | )
93 | CreateForm.password = fields.PasswordField(
94 | 'Password',
95 | validators=[
96 | validators.DataRequired(),
97 | ],
98 | )
99 |
100 | CreateForm.confirm_password = fields.PasswordField(
101 | 'Confirm Password',
102 | validators=[
103 | validators.DataRequired(),
104 | EqualTo('password', message='RETYPE_PASSWORD_MISMATCH'),
105 | ],
106 | )
107 |
108 | CreateForm.field_order = (
109 | 'email', 'first_name', 'last_name',
110 | 'password', 'confirm_password', 'roles', 'active')
111 |
112 | return CreateForm
113 |
--------------------------------------------------------------------------------
/hermes_ui/templates/admin/adminlte/forms.html:
--------------------------------------------------------------------------------
1 | {% import 'admin/lib.html' as lib with context %}
2 |
3 | {% macro form_header(header, icon=None, is_modal=False) %}
4 | {% if is_modal %}
5 |
8 |
{{ header }}
9 | {% else %}
10 |
{% if icon %} {% endif %}{{ header }}
11 | {% endif %}
12 | {% endmacro %}
13 |
14 | {% macro form_body(form, form_opts=None) %}
15 | {% if form.hidden_tag is defined %}
16 | {{ form.hidden_tag() }}
17 | {% else %}
18 | {% if csrf_token %}
19 |
20 | {% endif %}
21 | {% for f in form if f.widget.input_type == 'hidden' %}
22 | {{ f }}
23 | {% endfor %}
24 | {% endif %}
25 |
26 | {% if form_opts and form_opts.form_rules %}
27 | {% for r in form_opts.form_rules %}
28 | {{ r(form, form_opts=form_opts) }}
29 | {% endfor %}
30 | {% else %}
31 | {% for f in form if f.widget.input_type != 'hidden' %}
32 | {% if form_opts %}
33 | {% set kwargs = form_opts.widget_args.get(f.short_name, {}) %}
34 | {% else %}
35 | {% set kwargs = {} %}
36 | {% endif %}
37 | {{ lib.render_field(form, f, kwargs) }}
38 | {% endfor %}
39 | {% endif %}
40 | {% endmacro %}
41 |
42 | {% macro form_view(details_columns) %}
43 |
11 | `;
12 | };
13 |
14 | $(function () {
15 |
16 | // Crappy way of not running this outside of app
17 | if ($('.login-box').length > 0) {return; }
18 |
19 | let current_admin_page = $('.content-header h1').html(),
20 | header_section = $('section.content-header'),
21 | content_section = $('section.content');
22 |
23 | if (current_admin_page.startsWith('Clé'))
24 | {
25 | content_section.prepend(
26 | create_helper_callout(
27 | "Comprendre ce qu'est une Clé",
28 | "
" +
29 | "
Une Clé est découverte automatiquement par le moteur
" +
30 | "
Vous pouvez les découvrir avec l'outil 'Analyse de message' en bas à droite de l'écran
" +
31 | "
La valeur associée a cette clé peut être stockée pour être exploitée
" +
32 | "
"
33 | )
34 | );
35 | header_section.append("Permet de vérifier l'existe d'une Clé dans l'analyse préliminaire de votre source");
36 | }
37 | else if(current_admin_page.startsWith('Identifiant'))
38 | {
39 | header_section.append("Permet de vérifier la présence d'un identifiant au format numérique dans votre source");
40 | }
41 | else if(current_admin_page.startsWith('Recherche d\'expression'))
42 | {
43 | header_section.append("Permet d'extraire un ou des mot(s) sachant au moins soit la partie immédiatement à droite et/ou immédiatement à gauche");
44 | }
45 | else if(current_admin_page.startsWith('Détecteur'))
46 | {
47 | header_section.append("Décrire à l'aide d'une collection de règles un type de source, ou comment identifier une source en tant que");
48 |
49 | content_section.prepend(
50 | create_helper_callout(
51 | "Comprendre ce qu'est un Détecteur",
52 | "
" +
53 | "
Un Détecteur est une définition, elle permet d'identifier une source
" +
54 | "
Cette définition ce précise avec un ensemble de règles, celles-ci sont éditables/créables depuis le sous menu Règles de détection
" +
55 | "
Finalement, votre détecteur sera utile pour déclencher les actions d'un Automate
" +
56 | "
De plus un détecteur comprendra de règle, de plus le taux de faux positif sera faible
" +
57 | "
"
58 | )
59 | );
60 | }
61 | else if(current_admin_page.startsWith('Expression exacte'))
62 | {
63 | header_section.append("Retrouver une expression, une phrase, une suite de mots, dans votre source");
64 |
65 | content_section.prepend(
66 | create_helper_callout(
67 | "Remarques sur la recherche d'expression exacte",
68 | "
" +
69 | "
La recherche ne sera pas sensible à la case
" +
70 | "
Les accents ne sont pas un critère d'égalité, eg. é = e, à = a
" +
71 | "
"
72 | )
73 | );
74 | }
75 | else if(current_admin_page.startsWith('Mes variables globales'))
76 | {
77 | header_section.append("Stocke des variables globales disponibles à tout les automates");
78 |
79 | content_section.prepend(
80 | create_helper_callout(
81 | "Remarques sur le stockage de variable globale",
82 | "
" +
83 | "
Il est possible de stocker des informations structurées au format JSON ou YAML
" +
84 | "
Les variables créées sont disponibles depuis le volet de droite accessible par le bouton en bas à droite
" +
85 | "
Pour un stockage simple sans JSON ou YAML, choissisez AUTRE dans le choix format
" +
86 | "
Pour mémo, les variables sont accessible en les écrivants de la forme suivante: {{ ma_variable }}
" +
87 | "
"
88 | )
89 | );
90 | }
91 | else if(current_admin_page.startsWith('Description des Automates'))
92 | {
93 | header_section.append("Précise le cadre d'un automate");
94 |
95 | content_section.prepend(
96 | create_helper_callout(
97 | "Comprendre ce qu'est une Description d'Automate",
98 | "
" +
99 | "
Vous pouvez ici associer un Automate avec un Détecteur
" +
100 | "
Cette section décrit le comportement de lancement d'un Automate
" +
101 | "
Les Actions lancées sont éditables depuis la page principale de l'application
" +
102 | "
"
103 | )
104 | );
105 | }
106 | else if(current_admin_page.startsWith('Date'))
107 | {
108 | header_section.append("Recherche une date au format français ou anglais ou RFC 3339 ou RFC 2822");
109 | }
110 | else if(current_admin_page.startsWith('Opération sur critères'))
111 | {
112 | header_section.append("Ce critère permet de combiner UN ou PLUSIEUR autre critères et y appliquer un opérateur");
113 |
114 | content_section.prepend(
115 | create_helper_callout(
116 | "Remarque sur Opération sur Critère(s)",
117 | "
" +
118 | "
Vous pouvez utiliser un critère Opération sur Critère(s) en sous-critère sauf lui-même
" +
119 | "
"
120 | )
121 | );
122 |
123 | content_section.prepend(
124 | create_helper_callout(
125 | "Comprendre ce qu'est une Opération sur Critère(s)",
126 | "
" +
127 | "
Ce critère permet de combiner l'existance d'autres critères
" +
128 | "
Ce critère doit contenir au moins UN sous critère
" +
129 | "
Les opérations possibles sont les suivantes : AND, OR, NOT, XOR
" +
130 | "
AND : L'ensemble des sous-critères doivent être validés
" +
131 | "
OR : Au moins UN des sous-critères doit être validé
" +
132 | "
NOT : AUCUN des sous-critères ne doit être validé
" +
133 | "
XOR : Uniquement UN SEUL des sous-critères doit être validé
13 |
14 | > Hermès is a pagan god in Greek mythology - messenger of the gods.
15 |
16 | 
17 |
18 | The names and logos of `iTop` and `Microsoft Exchange` are displayed only as samples.
19 | Any IMAP provider or service works with Hermes, just as `iTop` is only one of the services you can use to send HTTP requests. Hermes is not affiliated with Combodo (iTop) or Microsoft (Exchange).
20 |
21 | ## Contributions
22 |
23 | Please ⭐ this project if you found it useful. Even better, contribute by :
24 | - Reporting issues and problems
25 | - Submitting a fix with a pull request
26 | - Requesting features to benefit everyone
27 |
28 | ## 🍰 Why Hermes ?
29 |
30 | This project was created with a specific use case in mind, which brought up the possibilities of a more open and generic use case.
31 | A company may face this problem :
32 |
33 | **How do we manage the interoperability of services with n-tiers, based only on electronic exchanges?**
34 |
35 | A company was currently using the ITSM iTop program and the Incoming Mail (Mailbox Scanner) functionalities.
36 | The official description of iTop is the following : `This extension runs in the background to scan the defined mail inbox(es) and either create or update tickets based on the content of the incoming emails.`
37 |
38 | With the old solution (Incoming Mail):
39 |
40 | 1) Limited and restricted message identification
41 | 2) Forced to create IMAP files for *n* operations
42 | 3) Scanner actions are limited to basic operations
43 |
44 | They found themselves extremely limited by Incoming Mail's functionalities.
45 |
46 | Hermes offers a complete solution, building on what iTop cannot provide.
47 |
48 | ## ✨ Installation
49 |
50 | Hermes is easily installed and executed in two ways. Requirements:
51 |
52 | - A usable IMAP / SMTP account
53 | - Your choice of a Linux / Unix / Windows environment
54 |
55 | Whatever your preferred method, start by running :
56 |
57 | ```shell
58 | cd $HOME
59 | git clone https://github.com/Ousret/hermes.git
60 | cd ./hermes
61 | cp configuration.dist.yml configuration.yml
62 | ```
63 |
64 | First, modify the configuration with your preferred text editor: `nano`, `vim`, etc..
65 |
66 | ```shell
67 | nano configuration.yml
68 | ```
69 |
70 | ```yaml
71 | PRODUCTION: &production
72 | <<: *common
73 | SECRET_KEY: PleaseChangeThisStringBeforeDeployment # Replace with a long randomly generated string
74 | # *-* smtp configuration *-* used to send error reports
75 | EMAIL_HOST: 'smtp-host'
76 | EMAIL_PORT: 587
77 | EMAIL_TIMEOUT: 10
78 | EMAIL_USE_TLS: True
79 | EMAIL_HOST_USER: 'smtp-user@smtp-host'
80 | EMAIL_HOST_PASSWORD: 'secret_smtp'
81 | EMAIL_FROM: 'smtp-user@smtp-host'
82 | INCIDENT_NOTIFIABLE: 'destination@gmail.com' # Replace with the email to send error reports to
83 | ```
84 |
85 | ### Method 1 : WITH Docker
86 |
87 | If you've already installed `docker` and `docker-compose` on your machine, you can simply run :
88 |
89 | ```shell
90 | docker-compose up
91 | ```
92 |
93 | ### Method 2 : WITHOUT Docker
94 |
95 | Requirements : `python3`, `pip`, `nodejs`, `npm`. Optional : `mariadb-server` and `mariadb-client`.
96 |
97 | These commands may require superuser privileges. (Installing the `yarn` utility)
98 | ```bash
99 | npm install yarn -g
100 | ```
101 |
102 | ```shell
103 | pip install certifi pyopenssl --user
104 |
105 | python setup.py install --user
106 | cd ./hermes_ui
107 | yarn install
108 | yarn build
109 | cd ..
110 | ```
111 |
112 | The second method requires a database implementation. If you're using `mariadb`, connect and create a `hermes` database.
113 |
114 | ```sql
115 | CREATE DATABASE hermes;
116 | ```
117 |
118 | If you don't have `mariadb` installed, you can opt for a lightweight `sqlite` implementation.
119 |
120 | In the `configuration.yml` file, change the following parameter :
121 |
122 | ```yaml
123 | PRODUCTION: &production
124 | <<: *common
125 | SQLALCHEMY_DATABASE_URI: 'mysql://user:mdp@127.0.0.1/hermes'
126 | ```
127 |
128 | If you don't want to use `mariadb`, replace it with :
129 |
130 | ```yaml
131 | PRODUCTION: &production
132 | <<: *common
133 | SQLALCHEMY_DATABASE_URI: 'sqlite:///hermes.sqlite'
134 | ```
135 |
136 | Finally, launch `wsgi.py`.
137 |
138 | ```shell
139 | python wsgi.py
140 | ```
141 |
142 | ### AFTER Method 1 or 2
143 |
144 | Navigate to the following address : [http://127.0.0.1:5000](http://127.0.0.1:5000)
145 | The default user is `hermes@localhost` and the password is `admin`.
146 | It's a good idea to change these after the first connection.
147 |
148 |
149 |
150 |
151 |
152 | ## ⚡ How does it work ?
153 |
154 | 
155 |
156 | Essentially,
157 |
158 | An electronic **message** is received -> we use a **series of criteria** from a **detector** to find the nature of the message while preserving evaluation results -> **A series of actions** defined by the designer will be linked in a binary tree -> each action results in a **success** or a **failure** and takes the appropriate following action.
159 |
160 | ## 👤 Documentation
161 |
162 | This section is a guide to getting started with Hermes quickly.
163 |
164 | - [ ] [Understanding simplified variables with Hermes](docs/CHAPITRE-1.md)
165 | - [ ] [Write / save global variables](docs/CHAPITRE-2.md)
166 | - [ ] [Configure your IMAP box(es)](docs/CHAPITRE-3.md)
167 | - [ ] [Detecting an email message](docs/CHAPITRE-4.md)
168 | - [ ] [Creating a controller in response to a message detection](docs/CHAPITRE-5.md)
169 | - [ ] [Implement an action sequence](docs/CHAPITRE-6.md)
170 | - [ ] [Test and debug the controller](docs/CHAPITRE-7.md)
171 |
172 | Going further :
173 |
174 | - [ ] [Detection criteria](docs/CRITERES.md)
175 | - [ ] [The actions](docs/ACTIONS.md)
176 | - [ ] [Gmail](docs/GMAIL.md)
177 |
178 | ## 🚧 Maintenance
179 |
180 | This program is still in its development stages.
181 | Hermes is stable and available for production and deployment. This project can be improved - ideas for significant refactors are being considered.
182 |
183 | A GitHub Project is open with all the tasks to be carried out to make Hermes even more incredible!
184 |
185 | For now, I'm focusing on bugs and security maintenance, and I re-read and approve all contributions.
186 |
187 | ## ⬆️ Upgrade
188 |
189 | Hermes may require updates. To do so, run the `upgrade.sh` script.
190 |
191 | ```shell
192 | ./upgrade.sh
193 | ```
194 |
195 | ## 📝 License
196 |
197 | **Commercial exploitation is strictly prohibited, however, internal use is authorized.**
198 |
199 | Released under "Non-Profit Open Software License 3.0 (NPOSL-3.0)"
200 |
201 | ## Contributor(s) :
202 |
203 | - Ahmed TAHRI @Ousret, Developer and maintainer
204 | - Didier JEAN ROBERT @SadarSSI, Initial conception and feature brainstormer
205 | - Denis GUILLOTEAU @Dsniss, Initial conception, tester, validator.
--------------------------------------------------------------------------------
/docs/CHAPITRE-6.md:
--------------------------------------------------------------------------------
1 |
Éditer les actions d'un Automate
2 |
3 | **Pré-requis:** Avoir mis en place au moins un détecteur et la description associée d'un automate.
4 |
5 | ## ✨ Éditeur d'un automate
6 |
7 | Une fois la description de votre automate effectuée, nous vous invitons à revenir sur le menu "Éditeur d'Automate".
8 |
9 | Nous vous invitons à choisir votre automate correspondant depuis la zone "Choisir Automate".
10 |
11 |
12 |
13 |
14 |
15 | N'hésitez pas à regarder le volet des "variables" disponibles en appuyant sur le bouton en bas à droite.
16 | Nous constatons que nos deux variables du détecteur sont disponibles. C'est bon signe.
17 |
18 | ## 👁️ Comprendre l'interface d'édition des Automates
19 |
20 | Un assistant visuel vous permet de faire le tour de l'interface étape par étape. Nous vous invitons,
21 | au moins une fois à cliquer sur "Guide de l'interface".
22 |
23 |
24 |
25 |
26 |
27 | ## ✍️ Actions & Scénario
28 |
29 | Hermes remplace vos traitements répétitifs en vous permettant de créer une suite d'actions.
30 |
31 | Cette suite d'actions s'organise en arbre binaire, chaque action possède deux issues, l'une en cas de **réussite**, l'autre en cas **d'échec**.
32 |
33 | Vous pouvez modifier votre arbre d'actions avec les boutons suivants :
34 |
35 | - Nouvelle Action
36 | - Supprimer Action
37 | - Modifier Action
38 | - Remplacer Action
39 |
40 | ## Actions
41 |
42 | Les actions disponibles sur étagère sont les suivantes :
43 |
44 | | Type d'Action | Description |
45 | |------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
46 | | RequeteSqlActionNoeud | Effectuer une requête de type SQL sur un serveur SGDB tel que Oracle, MySQL, PosgreSQL, Microsoft SQL Serveur et MariaDB. |
47 | | RequeteSoapActionNoeud | Effectuer une requête de type SOAP Webservice. |
48 | | RequeteHttpActionNoeud | Effectuer une requête de type HTTP sur un serveur distant. |
49 | | EnvoyerMessageSmtpActionNoeud | Ecrire un message électronique vers n- tiers via un serveur SMTP. |
50 | | TransfertSmtpActionNoeud | Transférer le message électronique d'origine vers n-tiers via un serveur SMTP. |
51 | | ConstructionInteretActionNoeud | Construire une variable intermédiaire. |
52 | | ConstructionChaineCaractereSurListeActionNoeud | Fabriquer une chaîne de caractère à partir d'une liste identifiable. |
53 | | InvitationEvenementActionNoeud | Emettre ou mettre à jour une invitation à un évènement par message électronique. |
54 | | VerifierSiVariableVraiActionNoeud | Vérifie si une variable est Vrai. |
55 | | ComparaisonVariableActionNoeud | Effectue une comparaison entre deux variables de votre choix, nombres, dates, etc.. |
56 | | DeplacerMailSourceActionNoeud | Déplacer le message électronique d'origine dans un autre dossier. |
57 | | CopierMailSourceActionNoeud | Copier le message électronique d'origine dans un autre dossier. |
58 | | SupprimerMailSourceActionNoeud | Supprimer le message électronique d'origine |
59 | | TransformationListeVersDictionnaireActionNoeud | Création d'une variable intermédiaire sachant une liste [{'cle_a': 'val_a', 'cle_b': 'val_b'}] vers {'val_a': 'val_b'}. |
60 | | ItopRequeteCoreGetActionNoeud | Effectuer une requête sur iTop avec l'opération core/get REST JSON |
61 | | ItopRequeteCoreCreateActionNoeud | Effectuer une requête sur iTop avec l'opération core/create REST JSON |
62 | | ItopRequeteCoreUpdateActionNoeud | Effectuer une requête sur iTop avec l'opération core/update REST JSON |
63 | | ItopRequeteCoreApplyStimulusActionNoeud | Effectuer une requête sur iTop avec l'opération core/apply_stimulus REST JSON |
64 | | ItopRequeteCoreDeleteActionNoeud | Effectuer une requête sur iTop avec l'opération core/delete REST JSON |
65 | | ExecutionAutomateActionNoeud | Exécute un autre Automate (routine ou plugin) |
66 |
67 | Chaque action nécessite de remplir n argument(s).
68 | Les arguments communs sont les suivants :
69 |
70 | - designation (Courte description de votre action)
71 | - friendly_name (Précise dans quel nom de variable le résultat doit être stocké)
72 |
73 | ### Scénario fictif
74 |
75 | Pour traiter nos factures iCloud, nous allons employer le scénario suivant :
76 |
77 | - Si le montant de la facture est inférieur à 1.00 EUR on supprime le message immédiatement
78 | - Sinon on effectue une requête http sur un serveur distant pour l'informer de la facture
79 | - Dans le cas ou facture >= 1.00 EUR on la conserve dans le dossier IMAP iCloud
80 |
81 | Pour cela nous allons utiliser les actions de la manière suivante :
82 |
83 | - ComparaisonVariableActionNoeud
84 | - RequeteHttpActionNoeud
85 | - DeplacerMailSourceActionNoeud
86 | - SupprimerMailSourceActionNoeud
87 |
88 | ### Création d'une action
89 |
90 | Pour créer une nouvelle action, nous vous invitons à cliquer sur "Nouvelle Action".
91 |
92 | 1) Choisir le type d'action.
93 | 2) (Optionnel) choisir l'action parente et la branche fils, échec ou réussite.
94 | 3) Renseigner les arguments de l'assistant.
95 |
96 | Captures, dans l'ordre.
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | ⚠️ Les deux dernières captures n'apparaissent que lorsque la première action a déjà été créée. L'action racine n'a pas de parent et elle s'exécute obligatoirement.
105 |
106 | ### Arguments d'une action
107 |
108 | Chaque action nécessite n argument(s) obligatoire(s) et n optionnel(s). Chaque argument peut contenir des variables.
109 |
110 |
111 |
112 |
113 |
114 | Un support de completion automatique est disponible dans la mesure du raisonable.
115 |
116 | ⚠️ Aucun support de marche arrière n'est disponible. Ceci est une limitation de la bibliothèque sweetalert2.
117 | L'assistant de création des actions ne permet pas de revenir à une étape antérieur.
118 |
119 | ### L'arbre des actions
120 |
121 |
122 |
123 |
124 |
125 | Une fois vos actions mises en place. L'éditeur vous proposera une représentation visuelle de votre arbre d'action.
126 |
127 | ### Modifier l'arbre
128 |
129 | Vous pouvez insérer dans l'arbre, supprimer et remplacer.
130 | Sachez qu'Hermes tentera de rééquilibrer l'arbre en priviligiant toujours la branche de **réussite**.
131 |
132 | ## Pour aller plus loin
133 |
134 | - [ ] [Test et debug d'un automate](CHAPITRE-7.md)
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
13 |
14 | > Hermès est une divinité issue de la mythologie grecque. Messager des dieux.
15 |
16 | 
17 |
18 | Les noms et logos `iTop` et `Microsoft Exchange` sont affichés à titre d'exemple uniquement.
19 | N'importe quel service IMAP fonctionne avec Hermes. De même qu'iTop est UN des services sur lequel vous pouvez émettre des requêtes. Hermes n'est pas affilié à Combodo (iTop) ni à Microsoft (Exchange).
20 |
21 | ## Contributions
22 |
23 | Merci d'offrir une ⭐ à ce projet s'il vous a été utile. Encore mieux, participez en :
24 | - Signalant un problème
25 | - Proposant un correctif via le système de pull request
26 | - Proposant des fonctionnalités/idées utiles à tous
27 |
28 | ## 🍰 Quel besoin ?
29 |
30 | Ce projet est né d'un besoin spécifique qui a laissé entrevoir la possibilité d'un cas bien plus ouvert et générique.
31 | Une entreprise peut-être confrontée à cette problématique :
32 |
33 | **Comment gérer une interopérabilité des services avec n-tiers en se basant uniquement sur les échanges électroniques ?**
34 |
35 | L'origine est qu'une entreprise utilisant le programme ITSM iTop et l'Incoming Mail (Scanner de boîte mail).
36 | La description officielle du module iTop est la suivante : `This extension runs in the background to scan the defined mail inbox(es) and either create or update tickets based on the content of the incoming emails.`
37 |
38 | Avec l'ancienne solution (Incoming Mail):
39 |
40 | 1) Identification d'un message très limitée et restreinte
41 | 2) Obligation de créer des dossiers IMAP pour n opération(s)
42 | 3) Les actions du scanner sont limitées à des simples opérations
43 |
44 | Ils se sont retrouvés extrêment limitée par l'Incoming Mail.
45 |
46 | Hermes offre une solution complète à ce qu'iTop ne peux pas fournir.
47 |
48 | ## ✨ Installation
49 |
50 | Le projet Hermes s'installe et s'execute très facilement de deux manières. À condition d'avoir :
51 |
52 | - Un compte IMAP et SMTP utilisable
53 | - Environnement Linux, Unix ou Windows au choix
54 |
55 | Quelque soit votre méthode préférée, commencez par :
56 |
57 | ```shell
58 | cd $HOME
59 | git clone https://github.com/Ousret/hermes.git
60 | cd ./hermes
61 | cp configuration.dist.yml configuration.yml
62 | ```
63 |
64 | Modifions d'abord la configuration à l'aide de votre éditeur préféré, `nano`, `vim`, etc..
65 |
66 | ```shell
67 | nano configuration.yml
68 | ```
69 |
70 | ```yaml
71 | PRODUCTION: &production
72 | <<: *common
73 | SECRET_KEY: MerciDeMeChangerImmediatementAvantPremierLancement # Remplacer par une longue chaîne de caractère aléatoire
74 | # *-* configuration smtp *-* à utiliser pour envoyer les rapports d'erreurs
75 | EMAIL_HOST: 'hote-smtp'
76 | EMAIL_PORT: 587
77 | EMAIL_TIMEOUT: 10
78 | EMAIL_USE_TLS: True
79 | EMAIL_HOST_USER: 'smtp-utilisateur@hote-smtp'
80 | EMAIL_HOST_PASSWORD: 'secret_smtp'
81 | EMAIL_FROM: 'smtp-utilisateur@hote-smtp'
82 | INCIDENT_NOTIFIABLE: 'destinataire@gmail.com' # Remplacer par l'adresse email à laquelle transmettre un rapport d'erreur
83 | ```
84 |
85 | ### Méthode 1 : AVEC Docker
86 |
87 | En ayant déjà installé `docker` et `docker-compose` sur votre machine, vous n'avez plus qu'à lancer :
88 |
89 | ```shell
90 | docker-compose up
91 | ```
92 |
93 | ### Méthode 2 : SANS Docker
94 |
95 | Les pré-requis sont les suivants : `python3`, `pip`, `nodejs`, `npm`. Optionnellement `mariadb-server` et `mariadb-client`.
96 |
97 | Il est possible que cette commande nécessite les droits super-utilisateur. (Installation de l'utilitaire `yarn`)
98 | ```bash
99 | npm install yarn -g
100 | ```
101 |
102 | ```shell
103 | pip install certifi pyopenssl --user
104 |
105 | python setup.py install --user
106 | cd ./hermes_ui
107 | yarn install
108 | yarn build
109 | cd ..
110 | ```
111 |
112 | La seconde méthode nécessite de mettre en oeuvre une base de données. Si vous êtes sous `mariadb`, connectez-vous et créez une base de données `hermes`.
113 |
114 | ```sql
115 | CREATE DATABASE hermes;
116 | ```
117 |
118 | Si vous n'avez pas `mariadb`, vous pouvez opter pour un système léger `sqlite` qui ne nécessite rien de plus.
119 |
120 | Dans le fichier `configuration.yml`, modifiez le paramètre suivant :
121 |
122 | ```yaml
123 | PRODUCTION: &production
124 | <<: *common
125 | SQLALCHEMY_DATABASE_URI: 'mysql://utilisateur:mdp@127.0.0.1/hermes'
126 | ```
127 |
128 | Si vous ne souhaitez pas mettre en place `mariadb`, remplacez par :
129 |
130 | ```yaml
131 | PRODUCTION: &production
132 | <<: *common
133 | SQLALCHEMY_DATABASE_URI: 'sqlite:///hermes.sqlite'
134 | ```
135 |
136 | Pour finir lancer le programme `wsgi.py`.
137 |
138 | ```shell
139 | python wsgi.py
140 | ```
141 |
142 | ### APRÈS Méthode 1 OU 2
143 |
144 | Ouvrir le navigateur à l'adresse suivante : [http://127.0.0.1:5000](http://127.0.0.1:5000)
145 | L'utilisateur par défaut est `hermes@localhost` et le mot de passe associé est `admin`.
146 | Il est bien entendu sage de le modifier rapidement après la 1ere connexion.
147 |
148 |
149 |
150 |
151 |
152 | ## ⚡ Comment ça marche ?
153 |
154 | 
155 |
156 | En bref,
157 |
158 | Un **message** électronique est reçu, nous arrivons, grâce à une suite **de critères** (depuis **un détecteur**) à définir la nature du message tout en conservant les résultats de l'évaluation
159 | des critères. Ensuite **une suite d'actions** déterminées par le concepteur s'enchainera en arbre binaire, chaque action se solde par **une réussite** ou **un échec** et prend la branche correspondante
160 | pour exécuter l'action suivante.
161 |
162 | ## 👤 Documentations
163 |
164 | Cette section vous propose de prendre en main rapidement Hermes.
165 |
166 | - [ ] [Comprendre le mécanisme des variables simplifiées sous Hermes](docs/CHAPITRE-1.md)
167 | - [ ] [Écrire et enregistrer vos variables partagées / globales](docs/CHAPITRE-2.md)
168 | - [ ] [Configurer votre/vos boîte(s) IMAP](docs/CHAPITRE-3.md)
169 | - [ ] [Détecter un message électronique](docs/CHAPITRE-4.md)
170 | - [ ] [Créer un automate en réaction à une détection de message électronique](docs/CHAPITRE-5.md)
171 | - [ ] [Mettre en oeuvre une suite d'action à appliquer après la détection](docs/CHAPITRE-6.md)
172 | - [ ] [Test et debug d'un automate](docs/CHAPITRE-7.md)
173 |
174 | Pour aller encore plus loin :
175 |
176 | - [ ] [Les critères de détection](docs/CRITERES.md)
177 | - [ ] [Les actions](docs/ACTIONS.md)
178 | - [ ] [GMail](docs/GMAIL.md)
179 |
180 | ## 🚧 Maintenance
181 |
182 | Ce programme n'est qu'à ses balbutiements.
183 | Hermès est stable et disponible pour la production. Ce projet peut être amélioré, des idées d'évolutions significatives sont à l'étude.
184 |
185 | Un projet Github est ouvert avec l'ensemble des idées / tâches à réaliser pour rendre ce projet incroyable.
186 |
187 | Pour le moment, j'adresse la maintenance concernant les bugs et la sécurité et je relis et j'approuve les contributions soumises.
188 |
189 | ## ⬆️ Mise à niveau
190 |
191 | Hermès peut être sujet à une mise à jour. Pour ce faire il est possible d'utiliser le script `upgrade.sh`.
192 |
193 | ```shell
194 | ./upgrade.sh
195 | ```
196 |
197 | ## 📝 Droits
198 |
199 | **L'exploitation commerciale est strictement interdite tandis que l'usage interne professionnel est autorisée.**
200 |
201 | Publication sous "Non-Profit Open Software License 3.0 (NPOSL-3.0)"
202 |
203 | ## Contributeur(s) :
204 |
205 | - Ahmed TAHRI @Ousret, Développeur et mainteneur
206 | - Didier JEAN ROBERT @SadarSSI, Conception et expression de besoins
207 | - Denis GUILLOTEAU @Dsniss, Aide à la conception, expression de besoins, testeur, valideur
208 |
--------------------------------------------------------------------------------
/hermes_ui/assets/scripts/Compoments/jquery.asuggest.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery textarea suggest plugin
3 | *
4 | * Copyright (c) 2009-2010 Roman Imankulov
5 | *
6 | * Dual licensed under the MIT and GPL licenses:
7 | * http://www.opensource.org/licenses/mit-license.php
8 | * http://www.gnu.org/licenses/gpl.html
9 | *
10 | * Requires:
11 | * - jQuery (tested with 1.3.x and 1.4.x)
12 | * - jquery.a-tools >= 1.4.1 (http://plugins.jquery.com/project/a-tools)
13 | */
14 |
15 | /*globals jQuery,document */
16 |
17 | (function ($) {
18 | // workaround for Opera browser
19 | if (navigator.userAgent.match(/opera/i)) {
20 | $(document).keypress(function (e) {
21 | if ($.asuggestFocused) {
22 | $.asuggestFocused.focus();
23 | $.asuggestFocused = null;
24 | e.preventDefault();
25 | e.stopPropagation();
26 | }
27 | });
28 | }
29 |
30 | $.asuggestKeys = {
31 | UNKNOWN: 0,
32 | SHIFT: 16,
33 | CTRL: 17,
34 | ALT: 18,
35 | LEFT: 37,
36 | UP: 38,
37 | RIGHT: 39,
38 | DOWN: 40,
39 | DEL: 46,
40 | TAB: 9,
41 | RETURN: 13,
42 | ESC: 27,
43 | COMMA: 188,
44 | PAGEUP: 33,
45 | PAGEDOWN: 34,
46 | BACKSPACE: 8,
47 | SPACE: 32
48 | };
49 | $.asuggestFocused = null;
50 |
51 | $.fn.asuggest = function (suggests, options) {
52 | return this.each(function () {
53 | $.makeSuggest(this, suggests, options);
54 | });
55 | };
56 |
57 | $.fn.asuggest.defaults = {
58 | 'delimiters': '\n ',
59 | 'minChunkSize': 1,
60 | 'cycleOnTab': true,
61 | 'autoComplete': true,
62 | 'endingSymbols': ' ',
63 | 'stopSuggestionKeys': [$.asuggestKeys.RETURN, $.asuggestKeys.SPACE],
64 | 'ignoreCase': false
65 | };
66 |
67 | /* Make suggest:
68 | *
69 | * create and return jQuery object on the top of DOM object
70 | * and store suggests as part of this object
71 | *
72 | * @param area: HTML DOM element to add suggests to
73 | * @param suggests: The array of suggest strings
74 | * @param options: The options object
75 | */
76 | $.makeSuggest = function (area, suggests, options) {
77 | options = $.extend({}, $.fn.asuggest.defaults, options);
78 |
79 | var KEY = $.asuggestKeys,
80 | $area = $(area);
81 | $area.suggests = suggests;
82 | $area.options = options;
83 |
84 | /* Internal method: get the chunk of text before the cursor */
85 | $area.getChunk = function () {
86 | var delimiters = this.options.delimiters.split(''), // array of chars
87 | textBeforeCursor = this.val().substr(0, this.getSelection().start),
88 | indexOfDelimiter = -1,
89 | i,
90 | d,
91 | idx;
92 | for (i = 0; i < delimiters.length; i++) {
93 | d = delimiters[i];
94 | idx = textBeforeCursor.lastIndexOf(d);
95 | if (idx > indexOfDelimiter) {
96 | indexOfDelimiter = idx;
97 | }
98 | }
99 | if (indexOfDelimiter < 0) {
100 | return textBeforeCursor;
101 | } else {
102 | return textBeforeCursor.substr(indexOfDelimiter + 1);
103 | }
104 | };
105 |
106 | /* Internal method: get completion.
107 | * If performCycle is true then analyze getChunk() and and getSelection()
108 | */
109 | $area.getCompletion = function (performCycle) {
110 | var text = this.getChunk(),
111 | selectionText = this.getSelection().text,
112 | suggests = this.suggests,
113 | foundAlreadySelectedValue = false,
114 | firstMatchedValue = null,
115 | i,
116 | suggest;
117 | // search the variant
118 | for (i = 0; i < suggests.length; i++) {
119 | suggest = suggests[i];
120 | if ($area.options.ignoreCase) {
121 | suggest = suggest.toLowerCase();
122 | text = text.toLowerCase();
123 | }
124 | // some variant is found
125 | if (suggest.indexOf(text) === 0) {
126 | if (performCycle) {
127 | if (text + selectionText === suggest) {
128 | foundAlreadySelectedValue = true;
129 | } else if (foundAlreadySelectedValue) {
130 | return suggest.substr(text.length);
131 | } else if (firstMatchedValue === null) {
132 | firstMatchedValue = suggest;
133 | }
134 | } else {
135 | return suggest.substr(text.length);
136 | }
137 | }
138 | }
139 | if (performCycle && firstMatchedValue) {
140 | return firstMatchedValue.substr(text.length);
141 | } else {
142 | return null;
143 | }
144 | };
145 |
146 | $area.updateSelection = function (completion) {
147 | if (completion) {
148 | var _selectionStart = $area.getSelection().start,
149 | _selectionEnd = _selectionStart + completion.length;
150 | if ($area.getSelection().text === "") {
151 | if ($area.val().length === _selectionStart) { // Weird IE workaround, I really have no idea why it works
152 | $area.setCaretPos(_selectionStart + 10000);
153 | }
154 | $area.insertAtCaretPos(completion);
155 | } else {
156 | $area.replaceSelection(completion);
157 | }
158 | $area.setSelection(_selectionStart, _selectionEnd);
159 | }
160 | };
161 |
162 | $area.unbind('keydown.asuggest').bind('keydown.asuggest', function (e) {
163 | if (e.keyCode === KEY.TAB) {
164 | if ($area.options.cycleOnTab) {
165 | var chunk = $area.getChunk();
166 | if (chunk.length >= $area.options.minChunkSize) {
167 | $area.updateSelection($area.getCompletion(true));
168 | }
169 | e.preventDefault();
170 | e.stopPropagation();
171 | $area.focus();
172 | $.asuggestFocused = this;
173 | return false;
174 | }
175 | }
176 | // Check for conditions to stop suggestion
177 | if ($area.getSelection().length &&
178 | $.inArray(e.keyCode, $area.options.stopSuggestionKeys) !== -1) {
179 | // apply suggestion. Clean up selection and insert a space
180 | var _selectionEnd = $area.getSelection().end +
181 | $area.options.endingSymbols.length;
182 | var _text = $area.getSelection().text +
183 | $area.options.endingSymbols;
184 | $area.replaceSelection(_text);
185 | $area.setSelection(_selectionEnd, _selectionEnd);
186 | e.preventDefault();
187 | e.stopPropagation();
188 | this.focus();
189 | $.asuggestFocused = this;
190 | return false;
191 | }
192 | });
193 |
194 | $area.unbind('keyup.asuggest').bind('keyup.asuggest', function (e) {
195 | var hasSpecialKeys = e.altKey || e.metaKey || e.ctrlKey,
196 | hasSpecialKeysOrShift = hasSpecialKeys || e.shiftKey;
197 | switch (e.keyCode) {
198 | case KEY.UNKNOWN: // Special key released
199 | case KEY.SHIFT:
200 | case KEY.CTRL:
201 | case KEY.ALT:
202 | case KEY.RETURN: // we don't want to suggest when RETURN key has pressed (another IE workaround)
203 | break;
204 | case KEY.TAB:
205 | if (!hasSpecialKeysOrShift && $area.options.cycleOnTab) {
206 | break;
207 | }
208 | case KEY.ESC:
209 | case KEY.BACKSPACE:
210 | case KEY.DEL:
211 | case KEY.UP:
212 | case KEY.DOWN:
213 | case KEY.LEFT:
214 | case KEY.RIGHT:
215 | if (!hasSpecialKeysOrShift && $area.options.autoComplete) {
216 | $area.replaceSelection("");
217 | }
218 | break;
219 | default:
220 | if (!hasSpecialKeys && $area.options.autoComplete) {
221 | var chunk = $area.getChunk();
222 | if (chunk.length >= $area.options.minChunkSize) {
223 | $area.updateSelection($area.getCompletion(false));
224 | }
225 | }
226 | break;
227 | }
228 | });
229 | return $area;
230 | };
231 | }(jQuery));
232 |
--------------------------------------------------------------------------------
/docs/CHAPITRE-1.md:
--------------------------------------------------------------------------------
1 |
Comprendre le fonctionnement des variables sous d'Hermes
2 |
3 | ## ✨ La génèse
4 |
5 | Hermes dispose d'un moteur de variables (simplifié).
6 | La syntaxe d'appel des variables est similaire à celle du moteur de template Twig.
7 |
8 | Faire appel à une variable de la manière suivante : `{{ ma_variable }}`.
9 | Une variable commence systèmatiquement par `{{` et se termine par `}}`.
10 | Les espaces ne sont pas obligatoires.
11 |
12 | ## Où ?
13 |
14 | Vous êtes autorisés à utiliser les variables dans vos actions et dans la description de vos boîtes IMAP.
15 | Il n'est pas possible d'utiliser les variables dans les paramètres des critères de détection.
16 |
17 | Les variables disponibles sont accessible par un volet caché à droite.
18 |
19 | 
20 |
21 | Le bouton en bas à droite vous permet de le faire apparaître et disparaître à votre guise.
22 |
23 | Trois sections sont visibles :
24 |
25 | - Les variables locales sont produites par un automate, ses actions ainsi que les critères de selection d'un message.
26 | - Les variables globales sont produites par vos entrés depuis le menu "Mes variables globales".
27 | - Les filtres permettent d'agir sur une variable, pour plus d'information, ci-dessous.
28 |
29 | ## Comment ?
30 |
31 | Vous produisez des variables de *TROIS* manières :
32 |
33 | - Le résultat d'un critère de recherche
34 | - Une variable accessible globalement, depuis le menu "Mes variables globales"
35 | - Le résultat d'une action
36 |
37 | Vous pouvez exploiter les variables seulement :
38 |
39 | - Dans les arguments d'une action
40 |
41 | ## Les filtres
42 |
43 | Pouvoir stocker et réutiliser de l'information c'est bien, pouvoir la transformer c'est mieux.
44 |
45 | | Filtre | Description | Avant | Après |
46 | |----------------------|---------------------------------------------------------------------------------------------------------------|--------------------|--------------|
47 | | escapeQuote | Sécurise une chaîne de caractère pour une insertion dans un JSON. Traite par ex. les doubles chevrons. | Je"suis" | Je\\"suis\\" |
48 | | keys | Liste les clés d'un dictionnaire associatif | {A: 0, B: 1, C: 2} | [A, B, C] |
49 | | int | Conserve UNIQUEMENT les chiffres d'une chaîne de caractère et convertir en entier | ITOP-T-00541 | 541 |
50 | | float | Conserve UNIQUEMENT les chiffres et caractère '.' et ',' et convertir en float | 1,22 € | 1.22 |
51 | | lower | Chaque caractère se transforme en minuscule s'il y a lieu | ITOP-T-00541 | itop-t-00541 |
52 | | upper | Chaque caractère se transforme en majuscule s'il y a lieu | je suis | JE SUIS |
53 | | strip | Retire les espaces d'une chaîne de caractères | je suis | jesuis |
54 | | capitalize | Première lettre en majuscule uniquement | je Suis | Je suis |
55 | | dateAjouterUnJour | Prends une date US et y additionne une seule journée | 2020/01/01 | 2020/01/02 |
56 | | dateAjouterUnMois | Prends une date US et y additionne un mois | 2020/01/01 | 2020/02/01 |
57 | | dateAjouterUneAnnee | Prends une date US et y additionne une année | 2020/01/01 | 2021/01/01 |
58 | | dateRetirerUnJour | Prends une date US et y retire une journée | 2020/01/01 | 2019/12/31 |
59 | | dateRetirerUnMois | Prends une date US et y retire un mois | 2020/01/01 | 2019/12/01 |
60 | | dateRetirerUneAnnee | Prends une date US et y retire une année | 2020/01/01 | 2019/01/01 |
61 | | dateFormatFrance | Prends une date et passe du format US à FR Y-m-d à d-m-Y | 2020/01/01 | 01/01/2020 |
62 | | dateFormatUS | Prends une date et passe du format FR à US d-m-Y à Y-m-d | 01/01/2020 | 2020/01/01 |
63 | | dateProchainLundi | Prends une date FR et remplace cette date par la date du prochain Lundi SI cette date n'est pas déjà un Lundi | | |
64 | | dateProchainMardi | Prends une date FR et remplace cette date par la date du prochain Mardi SI cette date n'est pas déjà un Mardi | | |
65 | | dateProchainMercredi | // | | |
66 | | dateProchainJeudi | // | | |
67 | | dateProchainVendredi | // | | |
68 | | dateProchainSamedi | // | | |
69 | | dateProchainDimanche | // | | |
70 | | slug | Transforme une chaîne de caractères en slug. URL-Safe String. | J'étais là | j-etais-la |
71 | | alNum | Conserve les caractères alphanumériques d'une chaîne | [##BONJOUR1] | BONJOUR1 |
72 | | alpha | Conserve les caractères alpha d'une chaîne | [##BONJOUR1] | BONJOUR |
73 | | remplissage**Zero | Rajoute des zéros en début de chaîne. Remplacer « *** » par « Un,Deux, Trois, Quatre, Cinq, etc.. » | | |
74 |
75 | Imaginons que la variable `{{ ma_variable }}` contienne la valeur `ITOP-T-00541`. Pour en extraire la partie numérique j'applique
76 | le filtre `int` sur celle-ci.
77 |
78 | ```
79 | {{ ma_variable|int }}
80 | ```
81 |
82 | `{{ ma_variable }}` est remplacée par `ITOP-T-00541` tandis que `{{ ma_variable|int }}` est remplacée par `541`.
83 |
84 | ## Les différents types
85 |
86 | Les variables sous Hermes ne sont qu'un *proxy* vers les variables nativements accessibles sous Python.
87 |
88 | Ce qui signifie qu'une variable peut contenir un `int`, `str`, `float` mais aussi un `list` et `dict` !
89 |
90 | `{{ ma_variable }}` peut contenir le `dict` suivant :
91 |
92 | ```json
93 | {
94 | "nom_utilisateur": "john_doe",
95 | "mot_de_passe": "azerty"
96 | }
97 | ```
98 |
99 | Pour acceder à `nom_utilisateur` --> `{{ ma_variable.nom_utilisateur }}`.
100 |
101 | Pour accéder à un niveau plus bas nous séparons les `étages` par un POINT.
102 | Il est également possible d'accéder à `nom_utilisateur` de la manière suivante `{{ ma_variable.0 }}`.
103 |
104 | `{{ ma_variable }}` peut contenir le `list` suivant :
105 |
106 | ```json
107 | [
108 | 'A',
109 | 'B',
110 | 'C'
111 | ]
112 | ```
113 |
114 | Pour acceder à la lettre `C`, on écrit `{{ ma_variable.2 }}`. Les index de liste commence à ZÉRO.
115 |
116 | ## Les variables imbriquées
117 |
118 | Pour les plus aguéris, sachez que vous pouvez invoquer une variable dans une variable. Sans limite.
119 |
120 | Imaginons que la variable `{{ ma_variable }}` contienne :
121 |
122 | ```json
123 | {
124 | "tickets": {
125 | "561": {
126 | "A": 1,
127 | "B": 2
128 | }
129 | }
130 | }
131 | ```
132 |
133 | Nous souhaitons obtenir la valeur associée à `A`, soit `1`. Nous écrirons, naturellement, alors `{{ ma_variable.tickets.561.A }}`.
134 | Néanmoins, partons du principe que nous sachons pas à l'avance que nous souhaitons passer par le niveau `561`.
135 |
136 | Disons que si un de vos critères a réussi à capturer `561` dans la variable `mon_numero_de_ticket`.
137 |
138 | Nous pouvons écrire : `{{ ma_variable.tickets.{{ mon_numero_de_ticket }}.A }}`, sachant qu'il sera traduit par `{{ ma_variable.tickets.561.A }}` puis `1`.
139 | Génial, non ?
140 |
141 | Maintenant si `{{ mon_numero_de_ticket }}` contient `Ticket 561` à la place de `561`, vous n'avez qu'à appliquer le filtre `|int` sur cette variable tel que :
142 | `{{ ma_variable.tickets.{{ mon_numero_de_ticket|int }}.A }}`
143 |
144 | ## Pour aller plus loin
145 |
146 | - [ ] [Écrire et enregistrer vos variables partagées / globales](CHAPITRE-2.md)
147 |
--------------------------------------------------------------------------------