@pierre-gilles, j’ai crée un sujet à part pour ne pas trop pollué ton post sur les intégrations externe.
J’ai tout donné à Claude et voilà son analyse concernant Free Mobile:
1. Contexte
Gladys se dote des intégrations externes : des programmes tournant dans des conteneurs Docker isolés, supervisés
par Gladys, dialoguant avec lui via une API hôte (REST) + un WebSocket sortant, enveloppés par
@gladysassistant/integration-sdk. Deux types de manifest existent aujourd’hui (manifest.schema.json, l.16-19) :
device— expose des appareils via les écrans Appareils / Découverte / Configuration.communication— canaux de messagerie (bots à la Telegram) : pas d’écran d’appareils, l’utilisateur lie son
compte depuis l’UI Gladys, et l’intégration échange des messages via l’API hôte.
Je porte le service historique Free Mobile SMS, jusqu’ici intégré au cœur de Gladys, vers une intégration
externe. Free Mobile est un canal de communication, donc type: "communication" est le choix naturel — mais cela a
révélé un trou structurel dans le modèle de communication actuel. Ce document explique ce trou, le prouve contre
le code, et propose un correctif minimal et rétrocompatible.
2. Comment fonctionne le modèle de communication aujourd’hui
Une intégration de communication ne parle à Gladys que de contacts liés. Gladys ne dit jamais « envoie à
William » ; il dit « envoie au contact X », où X est un identifiant que l’intégration elle-même a créé
pendant la liaison. Trois briques (README integration-sdk + contrôleur de l’API hôte) :
- Liaison (consentement) — l’utilisateur clique « Lier mon compte » dans l’UI Gladys, ce qui génère un code
court (usage unique, TTL 15 min). L’utilisateur envoie ce code au bot dans le canal externe ; l’intégration le
reçoit et appellelinkContact(code, contactId)→POST /api/integration/v1/contact/link. - Entrant —
publishMessage(contactId, text)→POST /api/integration/v1/message. Un contact inconnu (non
lié) déclenche un 404. - Sortant —
onSendMessage(contactId, message): réponses du brain et notifications transférées, délivrées par
l’intégration dans le canal externe.
Le code existe pour prouver un trajet entrant
Toute la raison d’être du code est qu’il transite par le canal externe : en le renvoyant au bot, la personne
dans Telegram/Signal prouve qu’elle est bien l’utilisateur Gladys qui l’a généré. C’est confirmé par le code
lui-même :
externalIntegration.createLinkCode.js— commentaire : « The user then sends it to the bot in the external
channel, and the integration calls POST /contact/link with it. » Alphabet choisi pour être « typed in a chat ».externalIntegration.linkContact.js— le code doit être présent dans le cache (créé par l’UI) et non expiré,
sinonNotFoundError('INVALID_LINK_CODE'). Il n’existe aucun autre moyen de créer un contact. Aucun chemin
d’auto-liaison n’existe.
Le modèle présuppose donc un canal bidirectionnel.
3. Le trou : les canaux sortants-seuls
Toute une classe d’intégrations de communication n’a aucun canal entrant — elles ne peuvent que pousser une
notification :
- Free Mobile SMS (envoi d’un SMS vers son propre numéro via un webhook),
- Pushover, ntfy, Gotify,
- webhooks entrants Discord / Slack,
- e-mail SMTP.
Pour toutes, deux choses sont vraies simultanément :
- Le code ne peut pas transiter. Il n’y a nulle part où l’envoyer — pas de bot, pas d’endpoint entrant. L’étape
centrale du flux de liaison du cœur est physiquement impossible. - Il n’y a aucune identité à prouver par aller-retour. L’utilisateur saisit sa propre destination — sa clé
API, son URL de webhook, son adresse e-mail — dans sa propre page de config Gladys, alors qu’il est déjà
authentifié dans Gladys. Le consentement est l’acte de remplir et d’enregistrer ce champ. Un code prouvant
« c’est bien vous dans le canal externe » répond à une question que personne ne pose.
Ce que l’utilisateur vit réellement aujourd’hui (Free Mobile)
La page de config communication (PR #2665, config-page/ConfigTab.jsx l.95) affiche LinkAccountCard sans
condition pour toute intégration de communication. L’utilisateur Free Mobile voit donc :
- La carte native « Lier mon compte » avec un bouton « Générer un code » → affiche par ex.
ABCD2345, et
(son texte i18n) demande d’« envoyer ce code au bot dans le canal externe » — factuellement faux : Free
Mobile n’a pas de tel canal. - Le seul contournement compatible avec le cœur actuel est une action de manifest (« Lier mon compte ») avec un
champ code : l’utilisateur copieABCD2345depuis la carte n°1 et le colle dans l’action quelques centimètres
plus bas, ce qui appellelinkContact(code, username).
C’est une cérémonie vide : l’utilisateur copie un code depuis une carte et le recolle dans une autre, pour
prouver un trajet qui n’existe pas. C’est déroutant et ça a l’air cassé. C’est le problème à corriger.
4. Proposition : un champ de manifest optionnel messaging_mode
Ajouter un champ de manifest de premier niveau optionnel, n’ayant de sens que lorsque type: "communication" :
{
"type": "communication",
"messaging_mode": "outbound", // "bidirectional" (défaut) | "outbound"
}
"bidirectional"(défaut) — le comportement actuel, inchangé. Les bots Telegram / Signal / Matrix ne
déclarent rien et continuent de fonctionner exactement comme avant. Pleinement rétrocompatible."outbound"— l’intégration ne fait que délivrer des messages, elle n’en reçoit jamais. Le cœur alors :- masque la
LinkAccountCardnative — le flux de code n’a pas de sens ici ; - expose un endpoint de l’API hôte pour lier le propriétaire de la config sans code
(POST /api/integration/v1/contact/self-link { contact_id }) : il lie l’utilisateur authentifié de la
config àcontact_id. Consentement = l’utilisateur a rempli et enregistré sa propre destination dans sa
propre page de config ; - tout le reste en aval est inchangé —
onSendMessage(contactId, message)se déclenche normalement ;
publishMessage/ le routage entrant n’ont simplement jamais lieu.
- masque la
Pourquoi c’est le bon découpage
- Minimal & rétrocompatible. Un seul champ enum optionnel. Le contrat bidirectionnel (B.15) est intact ; les
fichiers du flux de code (createLinkCode.js,linkContact.js) ne sont pas modifiés — le sortant est un chemin
parallèle, pas une réécriture. - Résout toute la famille, pas seulement Free Mobile (Pushover, ntfy, webhooks, SMTP…).
- UX honnête. Pas d’instruction trompeuse « envoyez ce code au bot », pas de cérémonie de copier-coller. L’UX de
Free Mobile se réduit à remplir identifiant + clé API → enregistrer → fini, soit le comportement historique
intégré au cœur. - La sécurité n’est pas affaiblie.
self-linkne lie jamais que l’utilisateur déjà authentifié de la page
de config à une destination qu’il a saisie lui-même. Pas de liaison inter-utilisateurs, pas d’escalade de
privilèges. C’est exactement le consentement que fournit le flux de code (« cet utilisateur Gladys est
d’accord »), moins l’aller-retour qui n’a de sens que pour un canal bidirectionnel. (Une intégration sortante ne
peut pas non plus usurper un autre utilisateur :self-linkest scopé au propriétaire de la configreq, et il
n’y a aucun chemin entrant pour recevoir des réponses avec l’autorité de quelqu’un d’autre.)
5. Changements concrets (points d’ancrage dans la PR #2665)
| # | Couche | Fichier (PR #2665) | Changement |
|---|---|---|---|
| 1 | Schéma manifest | server/lib/external-integration/manifest.schema.json |
ajouter un enum messaging_mode optionnel ["bidirectional","outbound"], défaut "bidirectional". Le même fichier sert à l’indexeur du store, donc les manifests outbound valident partout. |
| 2 | Cœur — lib link | server/lib/external-integration/externalIntegration.selfLinkContact.js (nouveau) |
lier l’utilisateur propriétaire de la config à contact_id sans code ; réutiliser le stockage CONTACT_VARIABLE existant (même forme que linkContact, sans le lookup de code). |
| 3 | Cœur — API hôte | server/api/controllers/integrationHost.controller.js + server/api/routes.js |
nouvelle route post /api/integration/v1/contact/self-link + méthode de contrôleur, à côté du contact/link existant (routes.js groupe les routes integration/v1/* ensemble). |
| 4 | Cœur — UI | front/.../config-page/ConfigTab.jsx (l.95) |
n’afficher LinkAccountCard que lorsque messaging_mode !== 'outbound' (lu depuis integration.manifest). |
| 5 | SDK | @gladysassistant/integration-sdk (lib/gladys-integration.js, index.d.ts, README) |
ajouter selfLinkContact(contactId) (POST /contact/self-link) à côté de linkContact ; documenter messaging_mode. |
Rien d’autre ne change. onSendMessage, getContacts, unlinkContact, la machine à états du conteneur, le
protocole WebSocket — tous intacts.
Ébauche du nouveau lib du cœur (calquée sur linkContact.js)
// externalIntegration.selfLinkContact.js
const { CONTACT_VARIABLE } = require('./constants');
const { BadParameters } = require('../../utils/coreErrors');
/**
* @description Link the config-owner user to an external contact WITHOUT a code
* (outbound-only communication integrations): consent is the user filling their
* own destination in their own config page. Stored like linkContact.
* @param {object} service - The external integration service.
* @param {string} userId - Id of the authenticated config-owner user.
* @param {object} body - { contact_id, contact_name? }.
* @returns {Promise<object>} { user: { selector, first_name, language } }.
*/
async function selfLinkContact(
service,
userId,
{ contact_id: contactId, contact_name: contactName } = {},
) {
if (typeof contactId !== 'string' || contactId.length === 0) {
throw new BadParameters('contact_id: must be a non-empty string');
}
// (guarded by the host API: service.manifest.messaging_mode must be 'outbound')
await this.variable.setValue(
CONTACT_VARIABLE,
JSON.stringify({
contact_id: contactId,
contact_name: contactName || null,
linked_at: new Date().toISOString(),
}),
service.id,
userId,
);
// return the user, like linkContact
}
6. Questions ouvertes pour le mainteneur
- Forme de l’endpoint —
POST /contact/self-link { contact_id }gardé parmessaging_mode === 'outbound', vs.
auto-liaison au premier enregistrement de config (pas d’appel SDK explicite). L’appel explicite est plus
prévisible et laisse l’intégration choisir lecontact_id; l’implicite demande encore moins de code à
l’intégration. Préférence ? - Nommage —
messaging_mode: "outbound"vs. un booléeninbound: falsevs. une liste de capacités.
messaging_modese lit le mieux si un futur troisième mode (entrant-seul ?) apparaît un jour.