# Intégration externe - Free Mobile

**URL:** <https://community.gladysassistant.com/t/integration-externe-free-mobile/10366>\
**Category:** Développement\
**Tags:** integration-externe\
**Created:** [Juillet 21, 2026, 10:49 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366 "2026-07-21T10:49:55Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 21, 2026, 10:49 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/1 "2026-07-21T10:49:55Z")

</div>

@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 `type`s 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 appelle `linkContact(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 : _«&nbsp;The user then sends it to the bot in the external  
channel, and the integration calls POST /contact/link with it.&nbsp;»_ Alphabet choisi pour être _«&nbsp;typed in a chat&nbsp;»_.
- `externalIntegration.linkContact.js` — le code **doit** être présent dans le cache (créé par l’UI) et non expiré,  
sinon `NotFoundError('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 :

1. **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.
2. **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 :

1. **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.
2. Le seul contournement compatible avec le cœur actuel est une **action de manifest** (« Lier mon compte ») avec un  
champ code : l’utilisateur copie `ABCD2345` depuis la carte n°1 et le colle dans l’action quelques centimètres  
plus bas, ce qui appelle `linkContact(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"` :

```jsonc
{
  "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 :
  1. **masque la `LinkAccountCard` native** — le flux de code n’a pas de sens ici ;
  2. 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 ;
  3. tout le reste en aval est inchangé — `onSendMessage(contactId, message)` se déclenche normalement ;  
`publishMessage` / le routage entrant n’ont simplement jamais lieu.

### 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-link` ne 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-link` est scopé au propriétaire de la config `req`, 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`)

```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

1. **Forme de l’endpoint** — `POST /contact/self-link { contact_id }` gardé par `messaging_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 le `contact_id` ; l’implicite demande encore moins de code à  
l’intégration. Préférence ?
2. **Nommage** — `messaging_mode: "outbound"` vs. un booléen `inbound: false` vs. une liste de capacités.  
`messaging_mode` se lit le mieux si un futur troisième mode (entrant-seul ?) apparaît un jour.

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [Juillet 21, 2026, 4:31 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/2 "2026-07-21T16:31:02Z")

</div>

Très bon retour @Will_71, je vais réfléchir au sujet !

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [Juillet 23, 2026, 2:02 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/3 "2026-07-23T14:02:36Z")

</div>

Salut @Will_71, j’ai géré le cas des intégrations mono-directionnelles dans le SDK v0.9.0 !

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 23, 2026, 2:10 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/4 "2026-07-23T14:10:01Z")

</div>

Ok 👍 je me remets dessus ce soir

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 23, 2026, 5:38 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/5 "2026-07-23T17:38:23Z")

</div>

J’adore

![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/d/d/dd9a6523bb17c7718bca9d97306ea7f9e0eadc1c.png)

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [Juillet 23, 2026, 5:39 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/6 "2026-07-23T17:39:00Z")

</div>

Yeees !! Hâte de voir l’intégration 😁

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 23, 2026, 8:57 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/7 "2026-07-23T20:57:23Z")

</div>

Ça avance mais ça ne marche pas encore

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/9/3/93e2d21a2eaf4ea27d657bfd477efef61e5b4318.png)

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/3/0/301e2cb4630f1671d646dad41dfa40162238285f.png)

 ![image](https://gladys-assistant-community.b-cdn.net/uploads/default/original/3X/0/6/06984fef587b920490eb4a8cefc7fd579c841082.png)

Edit: j’ai trouvé pourquoi cela ne fonctionne pas.  
J’ai créé une scène et exécuté l’action «&nbsp;Envoyer un SMS&nbsp;», en regardant les logs on voit qu’il utilise l’app Free Mobile natif et comme je ne l’ai pas configuré, j’ai une erreur 404.

Si j’utilise l’action «&nbsp;Envoyer un message&nbsp;», là ça fonctionne très bien. Par contre, si je mets Telegram par exemple, comment sélectionner sur quel service on envoie le message ?

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 23, 2026, 9:17 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/8 "2026-07-23T21:17:24Z")

</div>

Ou alors il faudrait faire évoluer sms.send pour cibler une intégration externe.

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [Juillet 27, 2026, 9:26 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/9 "2026-07-27T09:26:09Z")

</div>

Génial ! 🎉

De mon côté, je pense qu’on pourrait déprécier l’action de scène **“Envoyer un SMS”** et la remplacer par **“Envoyer un message”**.

L’action **“Envoyer un message”** envoie automatiquement le message via le canal configuré par l’utilisateur.

Et si le besoin se fait sentir par la suite, on pourra toujours ajouter un sélecteur de canal dans l’action. Mais l’approche automatique me semble être un très bon comportement par défaut. 🙂

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 27, 2026, 10:29 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/10 "2026-07-27T10:29:05Z")

</div>

Oui je pense qu’il faut rajouter le selecteur car j’utilise les 2 perso

---

<div class="post-metadata">

**Author:** ![pierre-gilles](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/pierre-gilles/32/5886_2.png) [@pierre-gilles](https://community.gladysassistant.com/u/pierre-gilles)\
**Post date:** [Juillet 27, 2026, 11:03 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/11 "2026-07-27T11:03:59Z")

</div>

Tu peux créer une demande de fonctionnalité ?

Je lancerais Claude dessus

---

<div class="post-metadata">

**Author:** ![Will\_71](https://gladys-assistant-community.b-cdn.net/user_avatar/community.gladysassistant.com/will_71/32/9582_2.png) [@Will\_71](https://community.gladysassistant.com/u/Will_71)\
**Post date:** [Juillet 27, 2026, 11:21 UTC](https://community.gladysassistant.com/t/integration-externe-free-mobile/10366/12 "2026-07-27T11:21:30Z")

</div>

Voilà  
[Ajout selecteur dans l'action Envoyer un message](https://community.gladysassistant.com/t/ajout-selecteur-dans-laction-envoyer-un-message/10383)
