Externe Integration - Free Mobile

@pierre-gilles, ich habe ein separates Thema erstellt, um deinen Post zu externen Integrationen nicht zu sehr zu überladen.

Ich habe alles an Claude weitergegeben und hier ist seine Analyse zu Free Mobile:

1. Kontext

Gladys erhält externe Integrationen: Programme, die in isolierten Docker-Containern laufen, von Gladys überwacht werden
und mit ihm über eine Host-API (REST) + ein ausgehendes WebSocket kommunizieren, eingewickelt in
@gladysassistant/integration-sdk. Zwei types von Manifesten existieren heute (manifest.schema.json, Zeile 16-19):

  • device — stellt Geräte über die Bildschirme Geräte / Entdeckung / Konfiguration bereit.
  • communication — Messaging-Kanäle (Bots wie Telegram): keine Geräte-Bildschirme, der Benutzer verbindet sein
    Konto über die Gladys-Benutzeroberfläche, und die Integration tauscht Nachrichten über die Host-API aus.

Ich bringe den historischen Dienst Free Mobile SMS, bisher in den Kern von Gladys integriert, zu einer externen Integration. Free Mobile ist ein Kommunikationskanal, daher ist type: "communication" die natürliche Wahl — aber dies hat ein strukturelles Loch im aktuellen Kommunikationsmodell aufgedeckt. Dieses Dokument erklärt dieses Loch, beweist es anhand des Codes und schlägt einen minimalen und rückwärtskompatiblen Fix vor.


2. Wie funktioniert das Kommunikationsmodell heute

Eine Kommunikationsintegration spricht mit Gladys nur über verknüpfte Kontakte. Gladys sagt nie „sende an
William“; es sagt „sende an den Kontakt X“, wobei X eine Kennung ist, die die Integration selbst während der Verknüpfung erstellt hat. Drei Bausteine (README integration-sdk + Host-API-Controller):

  • Verknüpfung (Zustimmung) — der Benutzer klickt „Mein Konto verknüpfen“ in der Gladys-Benutzeroberfläche, was einen kurzen Code
    generiert (einmalige Nutzung, TTL 15 min). Der Benutzer sendet diesen Code an den Bot im externen Kanal; die Integration empfängt ihn und ruft linkContact(code, contactId) → POST /api/integration/v1/contact/link auf.
  • Eingehend — publishMessage(contactId, text) → POST /api/integration/v1/message. Ein unbekannter (nicht verknüpfter) Kontakt löst einen 404 aus.
  • Ausgehend — onSendMessage(contactId, message): Antworten des Gehirns und Benachrichtigungen, die von der Integration in den externen Kanal geliefert werden.

Der Code existiert, um einen eingehenden Weg zu beweisen

Der ganze Sinn des Codes ist, dass er über den externen Kanal läuft: indem er ihn an den Bot zurücksendet, beweist die Person
in Telegram/Signal, dass sie der Gladys-Benutzer ist, der ihn generiert hat. Dies wird durch den Code selbst bestätigt:

  • externalIntegration.createLinkCode.js — Kommentar: « The user then sends it to the bot in the external
    channel, and the integration calls POST /contact/link with it. »
    Alphabet gewählt, um « typed in a chat » zu sein.
  • externalIntegration.linkContact.js — der Code muss im Cache (erstellt von der Benutzeroberfläche) vorhanden sein und nicht abgelaufen sein,
    sonst NotFoundError('INVALID_LINK_CODE'). Es gibt keine andere Möglichkeit, einen Kontakt zu erstellen. Kein Auto-Verknüpfungsweg existiert.

Das Modell unterstellt also einen bidirektionalen Kanal.


3. Das Loch: nur ausgehende Kanäle

Eine ganze Klasse von Kommunikationsintegrationen hat keinen eingehenden Kanal — sie können nur eine
Benachrichtigung pushed:

  • Free Mobile SMS (SMS an die eigene Nummer über einen Webhook),
  • Pushover, ntfy, Gotify,
  • eingehende Discord / Slack Webhooks,
  • E-Mail SMTP.

Für alle gilt gleichzeitig:

  1. Der Code kann nicht transitieren. Es gibt keinen Ort, an den man ihn senden kann — kein Bot, kein eingehender Endpunkt. Der zentrale Schritt des Verknüpfungsflusses des Kerns ist physisch unmöglich.
  2. Es gibt keine Identität, die durch Hin- und Herbewegungen bewiesen werden muss. Der Benutzer gibt seine eigene Zieladresse — seinen API-Schlüssel, seine Webhook-URL, seine E-Mail-Adresse — in seiner eigenen Gladys-Konfigurationsseite ein, während er bereits in Gladys authentifiziert ist. Die Zustimmung ist die Handlung, dieses Feld auszufüllen und zu speichern. Ein Code, der „das sind Sie im externen Kanal“ beweist, beantwortet eine Frage, die niemand stellt.

Was der Benutzer heute tatsächlich erlebt (Free Mobile)

Die Konfigurationsseite communication (PR #2665, config-page/ConfigTab.jsx Zeile 95) zeigt LinkAccountCard ohne Bedingung für jede Kommunikationsintegration. Der Free Mobile-Benutzer sieht also:

  1. Die native Karte „Mein Konto verknüpfen“ mit einem Button „Code generieren“ → zeigt z. B. ABCD2345 an und
    (sein i18n-Text) bittet darum, „diesen Code an den Bot im externen Kanal zu senden“ — faktisch falsch: Free Mobile hat keinen solchen Kanal.
  2. Die einzige Umgehung, die mit dem aktuellen Kern kompatibel ist, ist eine Manifest-Aktion („Mein Konto verknüpfen“) mit einem Code-Feld: Der Benutzer kopiert ABCD2345 von Karte Nr. 1 und fügt ihn in die Aktion einige Zentimeter weiter unten ein, was linkContact(code, username) aufruft.

Es ist eine leere Zeremonie: Der Benutzer kopiert einen Code von einer Karte und fügt ihn in eine andere ein, um einen Weg zu beweisen, der nicht existiert. Es ist verwirrend und sieht kaputt aus. Das ist das Problem, das behoben werden muss.


4. Vorschlag: ein optionales Manifest-Feld messaging_mode

Ein optionales Manifest-Feld der obersten Ebene hinzufügen, das nur Sinn ergibt, wenn type: "communication":

{
  "type": "communication",
  "messaging_mode": "outbound", // "bidirectional" (Standard) | "outbound"
}
  • "bidirectional" (Standard) — das aktuelle Verhalten, unverändert. Telegram-/Signal-/Matrix-Bots
    melden nichts und funktionieren weiterhin genau wie zuvor. Vollständig abwärtskompatibel.
  • "outbound" — die Integration liefert nur Nachrichten, sie empfängt nie welche. Der Kern ist dann:
    1. versteckt die native LinkAccountCard — der Code-Fluss hat hier keinen Sinn;
    2. stellt ein API-Endpoint des Hosts bereit, um den Besitzer der Konfiguration ohne Code zu verknüpfen
      (POST /api/integration/v1/contact/self-link { contact_id }): Es verknüpft den authentifizierten
      Benutzer der Konfiguration
      mit contact_id. Zustimmung = der Benutzer hat seine eigene
      Zieladresse auf seiner eigenen Konfigurationsseite ausgefüllt und gespeichert;
    3. alles andere downstream bleibt unverändert — onSendMessage(contactId, message) wird normalerweise
      ausgelöst; publishMessage / der eingehende Routing findet einfach nie statt.

Warum das die richtige Aufteilung ist

  • Minimal & abwärtskompatibel. Ein optionales Enum-Feld. Der bidirektionale Vertrag (B.15) ist intakt; die
    Code-Flow-Dateien (createLinkCode.js, linkContact.js) werden nicht geändert — der ausgehende ist ein paralleler
    Pfad
    , keine Neuimplementierung.
  • Löst die ganze Familie, nicht nur Free Mobile (Pushover, ntfy, Webhooks, SMTP…).
  • Ehrliche UX. Keine irreführende Anweisung „senden Sie diesen Code an den Bot“, kein Kopiervorgang. Die UX von
    Free Mobile reduziert sich auf Identifikator + API-Schlüssel ausfüllen → speichern → fertig, also das
    historische Verhalten, das im Kern integriert ist.
  • Die Sicherheit wird nicht geschwächt. self-link verknüpft immer nur den bereits authentifizierten Benutzer der
    Konfigurationsseite mit einer Zieladresse, die er selbst eingegeben hat. Keine Benutzer-zu-Benutzer-Verknüpfung,
    kein Privilegieneskalation. Es ist genau die Zustimmung, die der Code-Flow bietet (« dieser Benutzer Gladys ist
    einverstanden »), minus der Hin- und Rückweg, der nur für einen bidirektionalen Kanal Sinn macht. (Eine
    ausgehende Integration kann auch nicht einen anderen Benutzer usurpieren: self-link ist auf den
    Besitzer der Konfiguration req begrenzt, und es gibt keinen eingehenden Pfad, um Antworten mit der
    Autorität von jemand anderem zu empfangen.)

5. Konkrete Änderungen (Ankerpunkte im PR #2665)

# Schicht Datei (PR #2665) Änderung
1 Manifest-Schema server/lib/external-integration/manifest.schema.json optionales Enum messaging_mode hinzufügen ["bidirectional","outbound"], Standard "bidirectional". Die gleiche Datei wird vom Store-Indexer verwendet, daher validieren outbound-Manifests überall.
2 Kern — Link-Bibliothek server/lib/external-integration/externalIntegration.selfLinkContact.js (neu) Verknüpfe den Besitzer der Konfiguration mit contact_id ohne Code; wiederverwende die bestehende CONTACT_VARIABLE-Speicherung (gleiche Form wie linkContact, ohne Code-Lookup).
3 Kern — Host-API server/api/controllers/integrationHost.controller.js + server/api/routes.js neue Route post /api/integration/v1/contact/self-link + Controller-Methode, neben dem bestehenden contact/link (routes.js gruppiert die Routen integration/v1/* zusammen).
4 Kern — UI front/.../config-page/ConfigTab.jsx (l.95) LinkAccountCard nur anzeigen, wenn messaging_mode !== 'outbound' (aus integration.manifest gelesen).
5 SDK @gladysassistant/integration-sdk (lib/gladys-integration.js, index.d.ts, README) selfLinkContact(contactId) (POST /contact/self-link) neben linkContact hinzufügen; messaging_mode dokumentieren.

Nichts anderes ändert sich. onSendMessage, getContacts, unlinkContact, die Zustandsmaschine des Containers, das
WebSocket-Protokoll — alles intakt.

Entwurf der neuen Kernbibliothek (basierend auf linkContact.js)

// externalIntegration.selfLinkContact.js
const { CONTACT_VARIABLE } = require('./constants');
const { BadParameters } = require('../../utils/coreErrors');

/**
 * @description Verknüpfe den Konfigurationsbesitzer-Benutzer mit einem externen Kontakt OHNE Code
 * (nur ausgehende Kommunikationsintegrationen): Die Zustimmung ist der Benutzer, der seine
 * eigene Zieladresse auf seiner eigenen Konfigurationsseite ausfüllt. Gespeichert wie linkContact.
 * @param {object} service - Der externe Integrationsdienst.
 * @param {string} userId - Id des authentifizierten Konfigurationsbesitzer-Benutzers.
 * @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: muss eine nicht-leere Zeichenkette sein');
  }
  // (geschützt durch die Host-API: service.manifest.messaging_mode muss 'outbound' sein)
  await this.variable.setValue(
    CONTACT_VARIABLE,
    JSON.stringify({
      contact_id: contactId,
      contact_name: contactName || null,
      linked_at: new Date().toISOString(),
    }),
    service.id,
    userId,
  );
  // gebe den Benutzer zurück, wie linkContact
}

6. Offene Fragen für den Maintainer

  1. Form des Endpoints — POST /contact/self-link { contact_id } geschützt durch messaging_mode === 'outbound', vs.
    automatische Verknüpfung bei der ersten Konfiguration (kein expliziter SDK-Aufruf). Der explizite Aufruf ist
    vorhersehbarer und lässt die Integration den contact_id wählen; das implizite erfordert noch weniger Code
    von der Integration. Vorzug?
  2. Benennung — messaging_mode: "outbound" vs. ein Boolean inbound: false vs. eine Liste von Fähigkeiten.
    messaging_mode liest sich am besten, wenn ein zukünftiger dritter Modus (nur eingehend?) eines Tages erscheint.

Sehr guter Punkt, @Will_71, ich werde darüber nachdenken!

Hallo @Will_71, ich habe den Fall der unidirektionalen Integrationen im SDK v0.9.0 behandelt!

Ok :+1: ich mach mich heute Abend wieder dran

Ich liebe es

image

Jaaaa !! Kann es kaum erwarten, die Integration zu sehen :grin:

Es geht voran, aber es funktioniert noch nicht

Edit: Ich habe herausgefunden, warum es nicht funktioniert.
Ich habe eine Szene erstellt und die Aktion „SMS senden“ ausgeführt. In den Logs sieht man, dass es die native Free Mobile-App verwendet und da ich diese nicht konfiguriert habe, gibt es einen Fehler 404.

Wenn ich die Aktion „Nachricht senden“ verwende, funktioniert das sehr gut. Aber wenn ich zum Beispiel Telegram verwende, wie wähle ich dann aus, auf welchem Dienst die Nachricht gesendet wird?

Oder man müsste sms.send weiterentwickeln, um eine externe Integration anzusteuern.

Toll! :tada:

Ich denke, wir könnten die Aktion „SMS senden“ abwerten und durch „Nachricht senden“ ersetzen.

Die Aktion „Nachricht senden“ sendet die Nachricht automatisch über den vom Benutzer konfigurierten Kanal.

Und falls später Bedarf besteht, können wir immer noch einen Kanalauswahler in der Aktion hinzufügen. Aber der automatische Ansatz scheint mir ein sehr gutes Standardverhalten zu sein. :slightly_smiling_face:

Ja, ich denke, man sollte den Selektor hinzufügen, weil ich beide nutze.

Kannst du ein Feature-Request erstellen?

Ich würde Claude darauf ansetzen.

Hier
Ajout selecteur dans l'action Envoyer un message