Sende und empfange SMS mit Twilio und dem SAP Cloud Application Programming Model

February 23, 2022
Autor:in:
Prüfer:in:

Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von How to Send and Receive SMS with SAP CAP using Twilio.

Das SAP Cloud Application Programming Model (CAP) ist laut Produktseite ein Framework von Programmiersprachen, -bibliotheken und Werkzeugen zum Bauen von Geschäftsanwendungen und -services. Zu den Hauptmerkmalen von dem Framework zählt die Datenbankmodellierung mit Core Data Services und andere geschäftskritische Anforderungen wie Lokalisierung, Datenschutz, Autorisierung und die Verarbeitung von Business-Events. Hierfür bietet das Framework einen hohen Abstraktionsgrad, um den Entwicklern so viel Programmieraufwand wie möglich abzunehmen. CAP bietet aber auch die nötige Flexibilität, mit der man beliebige JavaScript-Bibliotheken integrieren kann, um beispielsweise die User Experience zu verbessern. In diesem Artikel erfährst du, wie du in einer CAP Anwendung SMS versenden und empfangen kannst. Um den Twilio Service nutzen zu können, werden wir Webhooks und JavaScript Module verwenden.

Was werden wir heute bauen?

Wir werden in diesem Beitrag das Rad nicht neu erfinden und orientieren uns am altbekannten Hello-World-Szenario von anderen CAP-Projekten: dem Buchladen. Es ist kein Problem, wenn du dieses Szenario noch nie verwendet hast und nicht kennst. Die Buchladenbeispiele sind kleine CRUD-Webanwendungen, die Entitäten aus einer Buchhandlung wie zum Beispiel Bücher, Autoren und Bestellungen verwenden. Das Projekt, das du erstellen wirst, hat nur eine einzige Entität: Bücher. Alle Datensätze dieser Entität werden über einen schreibgeschützten REST-Endpunkt verfügbar gemacht. Es handelt sich also eher um eine RU-Webanwendung (Read and Update) ohne Erstellungs- und Löschvorgänge. Darüber hinaus akzeptiert die fertige Anwendung HTTP-POST-Anforderungen, um eingehende Bestellungen entgegenzunehmen, wodurch das Feld stock des bestellten Buchs reduziert wird. Mit etwas Twilio-Magie stellen wir sicher, dass die Buchladenmanager benachrichtigt werden, wenn der Vorrat eines bestimmten Buchs zur Neige geht. Dazu bietet die Anwendungen eine einfache Möglichkeit, um weitere Bücher bei den Lieferanten zu bestellen.

Zur Vorbereitung:

Für dieses Tutorial brauchst du Folgendes:

Erstelle dein Buchladenprojekt

Die CLI verfügt über einen hilfreichen init Befehl um neue Projekte zu generieren. Führe die folgenden Befehle aus:

cds init bookshop --add samples
cd bookshop
npm install

Im nächsten Schritt rufst du einen Befehl auf, um den Webserver auf deinem lokalen Rechner zu starten. Dieser Befehl verwendet die folgenden drei Optionen:

Die in-memory-Option stellt sicher, dass das Datenbankschema in der SQLite-Datenbank erstellt wird, während die to-Option das Webprotokoll bestimmt, das verwendet wird, um die API-Schnittstelle bereitzustellen. Die watch-Option stellt sicher, dass der Server automatisch neu startet, wenn Dateiänderungen auftreten. Für die letzte Option musst du noch ein neues Modul hinzufügen.

npm add -D @sap/cds-dk # installiert extra Modul für die `watch`-Option
cds serve all --watch --in-memory --to rest

Jetzt ist es an der Zeit, die erste Version des Projekts zu testen, indem du eine HTTP-GET-Anfrage an http://localhost:4004/catalog/Books sendest. Ich empfehle, dies mit der REST-Client-Erweiterung für Visual Studio Code und einer neuen Datei namens requests.http zu tun:

### Bestandsabfrage des Katalogs
GET http://localhost:4004/catalog/Books

VS Code wird über dieser Definition einen klickbaren „Send Request“-Text anzeigen. Nach dem erfolgreichen Senden der Anfrage solltest du die folgende Antwort sehen.

Visual Studio Code mit der Rest Client Erweiterung

Alternativ kann du auch curl verwenden, um diese Anfrage zu senden.

curl http://localhost:4004/catalog/Books

Im nächsten Schritt fügst du eine action hinzu, die einen POST-Endpunkt für deine HTTP-Schnittstelle definiert. Dazu musst du die folgende Zeile in der Katalogdefinition von srv/cat-services.cds hinzufügen.

using my.bookshop as my from '../db/data-model';

service CatalogService {
   @readonly
   entity Books as projection on my.Books;

   action submitOrder(book : Books:ID, quantity : Integer);
}

Implementiere diese action in einer neuen Datei namens srv/cat-service.js. Wie bereits erwähnt, liest diese Aktion die eingehende Anfrage und verwendet die Parameter, um den Wert des Felds Stock zu reduzieren. Sollte der Wert dieses Felds negativ werden, muss die Anfrage fehlschlagen:

const cds = require("@sap/cds");
class CatalogService extends cds.ApplicationService {
init() {
  const { Books } = cds.entities("my.bookshop");
  // Reduce stock of ordered books if available stock suffices
  this.on("submitOrder", async (req) => {
    const { book, quantity } = req.data;
    let { stock, title } = await SELECT`stock, title`.from(Books, book);
    const remaining = stock - quantity;
    if (remaining < 0) {
      return req.reject(409, `${quantity} übersteigt den Bestand von Buch #${book}`);
    }
    await UPDATE(Books, book).with({ stock: remaining });
    return { ID: book, stock: remaining };
  });
  return super.init();
}
}
module.exports = { CatalogService };

Dank der watch-Option wird der Webdienst automatisch neu gestartet, wenn du die Datei speicherst.

Hänge die folgenden Zeilen an die requests.http Datei, um die zweite Anfrage zu definieren:

### Bestandsabfrage des Katalogs
GET http://localhost:4004/catalog/Books

### Sende eine Bestellung
POST http://localhost:4004/catalog/submitOrder HTTP/1.1
Content-Type: application/json

{
  "book": 1,
  "quantity": 95
}

Du kannst die Anfrage auch von deinem Terminal aus mit curl senden.

curl -X POST -d '{"book":1,"quantity":95}'  -H 'Content-Type: application/json' http://localhost:4004/catalog/submitOrder

Die erste Anfrage gibt eine erfolgreiche, aber leere Antwort zurück. Weitere Anfragen schlagen fehl, da der Bestand zu niedrig sein wird.

 

Sende eine SMS

In diesem Abschnitt fügst du deinem Projekt eine SMS-Sendefunktion hinzu. Die Twilio-API ermöglicht dir das Senden von SMS, und der Twilio-Client ermöglicht das Aufrufen der API mit einer einzigen Codezeile.

Daher musst du den Twilio Node.js-Client als Abhängigkeit zu deinem Projekt hinzufügen.

npm add twilio

Um dieses Projekt mit deinem Twilio-Konto zu verbinden, musst du die Konto-SID und das Auth-Token angeben. Es ist wichtig, diese Informationen geheim zu halten und sie von der Codebasis zu trennen. Keinesfalls sollten sie in ein Git-Repository eingecheckt werden. Daher ist es sinnvoll, diese Informationen in den Umgebungsvariablen des Projekts zu halten. In jedem CAP-Projekt ist die Datei default-env.json der perfekte Ort für diese Geheimnisse, da sie bereits auf der .gitignore-Liste steht und deren Inhalt beim Start automatisch in die Umgebungsvariablen geladen wird. Füge diese Zeilen deiner default-env.json Datei zu und ersetze alle Platzhalter wie die Absender- und Empfängernummer der Textnachrichten:

{
 "TWILIO_ACCOUNT_SID": "<Account SID>",
 "TWILIO_AUTH_TOKEN": "<Auth Token>",
 "TWILIO_SENDER": "<Telefonnummer in dieser Form: +4915100000000>", 
 "TWILIO_RECEIVER": "<Telefonnummer in dieser Form: +4915100000000>"
}

Nachdem du die Laufzeitumgebung vorbereitet hast, ist es an der Zeit, den Twilio-Client zu initialisieren. Sende eine Warnmeldung, wenn der Schwellenwert erreicht ist. Füge dazu die markierten Zeilen zur Datei srv/cat-service.js hinzu:

const cds = require("@sap/cds");
const twilio = require("twilio");

const twilioClient = twilio();

class CatalogService extends cds.ApplicationService {
init() {
  const { Books } = cds.entities("my.bookshop");
  // Reduce stock of ordered books if available stock suffices
  this.on("submitOrder", async (req) => {
    const { book, quantity } = req.data;
    let { stock, title } = await SELECT`stock, title`.from(Books, book);
    const remaining = stock - quantity;
    if (remaining < 0) {
      return req.reject(409, `${quantity} übersteigt den Bestand von Buch #${book}`);
    }
    await UPDATE(Books, book).with({ stock: remaining });
   
    if (remaining < 10) {
     twilioClient.messages
       .create({
         body: `Ein Kunde bestellte ${quantity}x "${title}". Es sind nur noch ${remaining} Exemplare auf Lager.`,
         from: process.env.TWILIO_SENDER,
         to: process.env.TWILIO_RECEIVER,
       })
       .then((message) =>
         console.log(`Die Nachricht ${message.sid} wurde zugestellt.`)
       )
       .catch((message) => console.error(message));
   }

    return { ID: book, stock: remaining };
  });
  return super.init();
}
}
module.exports = { CatalogService };

Send eine Bestellung, indem du die zweite HTTP Anfrage erneut abschickst.

Visual Studio Code nachdem die zweite Anfrage erfolgreich versendet wurde.

Du solltest die folgende Nachricht auf deinem Telefon empfangen:

“Ein Kunde bestellte 95x ‘Wuthering Heights’. Es sind nur noch 5 Exemplare auf Lager.”

Ein Screenshot von einem Smartphone der die zugestellte SMS zeigt.

Reagiere auf eingehende SMS

Der vorherige Abschnitt hat einen unidirektionalen Kommunikationskanal von deinem Projekt zum Handy eines Buchladenmanagers eingerichtet. Dieser letzte Abschnitt verwandelt ihn in einen bidirektionalen Kanal, der die Antworten von den Managern lesen kann. In der Twilio-Konsole kannst du festlegen, was passiert, wenn Twilio eine SMS erhält. Es gibt mehrere Möglichkeiten, auf dieses Ereignis zu reagieren. Du kannst eine statische Antwort zurücksenden, die Nachricht dynamisch mithilfe einer serverlosen Funktion verarbeiten oder den Inhalt der SMS per HTTP-Anfrage an eine Webschnittstelle (webhook) deiner Anwendung weiterleiten. In unserem Fall ist die letzte Option am sinnvollsten. Du wirst eine Middleware verwenden, um diese Webhook zu implementieren und die SMS-Antwort zu verarbeiten. Da die Anwendung derzeit auf localhost läuft, musst du auch einen Tunnel öffnen. Dieser Tunnel wird mit ngrok aufgebaut und verbindet das Twilio-Rechenzentrum mit deinem Computer.

Dieser Abschnitt hilft dir, diese Webhook Schritt für Schritt zu erstellen, um genau zu verstehen, was getan werden muss.

Teile den Managern zunächst mit, wie sie auf die initiale Nachricht reagieren können. Ändere hierfür die folgende Zeile in der srv/cat-service.js Datei:

twilioClient.messages
         .create({
           body: `Ein Kunde bestellte ${quantity}x "${title}". `+
           `Es sind nur noch ${remaining} Exemplare auf Lager.`+
           `Antworte mit "Ja" falls du zusätzliche Exemplare bestellen möchtest.`,
           from: process.env.TWILIO_SENDER,
           to: process.env.TWILIO_RECEIVER,
         })

Um eine Middleware für CAP-Anwendungen zu verwenden, musst du nur eine Datei srv/server.js erstellen und auf das bootstrap-Ereignis warten, bevor du den Twilio-Client initialisieren kannst. Dazu sollte die Middleware twilio.webhook() verwendet werden, um Missbrauch zu verhindern. Diese Funktion stellt sicher, dass nur Server aus den Twilio-Rechenzentren diese Webhook aufrufen können. Wenn der Service lokal entwickelt wird, wird dieser Check übersprungen.

const cds = require("@sap/cds");
var bodyParser = require("body-parser");
const twilio = require("twilio");

const MessagingResponse = twilio.twiml.MessagingResponse;

cds.on("bootstrap", (app) => {
 const twilioClient = twilio();

 app.use(bodyParser.urlencoded({ extended: true }));

 app.post(
   "/twilioWebhook",
   twilio.webhook({ validate: process.env.NODE_ENV === "production" }), // Don't validate in test mode
   async (req, res) => {
     req.res.writeHead(200, { "Content-Type": "text/xml" });
     console.log(`Received message ${req.body.SmsMessageSid}.`)
     res.end({ ok: 200 });
   }
 );
});

Implementiere die Middleware, um das betroffene Buch in der Datenbank zu finden, aktualisiere dessen Datenbankeintrag und informiere den Buchladenmanager, ob es funktioniert hat:

 app.post(
   "/twilioWebhook",
   twilio.webhook({ validate: process.env.NODE_ENV === "production" }), // Don't validate in test mode
   async (req, res) => {
    req.res.writeHead(200, { "Content-Type": "text/xml" });
     console.log(`Received message ${req.body.SmsMessageSid}.`)
     const twiml = new MessagingResponse();

     if (req.body.Body.includes("Ja")) {
       const parsed = await collectBookDetails(req.body.From, req.body.Body);
       if (parsed?.book?.ID && parsed?.book?.stock) {
         const newStock = parsed?.book.stock + parsed.restock;
         await cds.update("Books").where({ ID: parsed?.book.ID }).with({
           stock: newStock,
         });

         twiml.message(
           `Erledigt ✅, dein Lieferant wurde kontaktiert und ab morgen sind ${newStock} Exemplare auf Lager.`
         );
       } else {
         twiml.message("Oh nein, etwas ging schief. 😣");
       }
     } else {
       twiml.message(
         `Entschuldige, I kann diese Antwort leider nicht verstehen. Du kannst beispielsweise mit "Ja" oder "Ja, bestelle 60 zusätzliche Bücher" antworten.`
       );
     }
     res.end(twiml.toString());

   }
 );

Wahrscheinlich ist dir schon aufgefallen, dass du eine fehlende Funktion aufgerufen hast.
Ändere das, indem du die Funktion collectBookDetails zu srv/server.js hinzufügst. Diese Funktion liest die Kontextdaten aus der letzten Nachricht, die an den Bookshop-Manager gesendet wurde. Füge die neue Funktion direkt nach der Deklaration des twilioClient hinzu, um sicherzustellen, dass sie im richtigen Scope ist.

 const twilioClient = twilio();

 async function collectBookDetails(sender, message) {
   const lastMessages = await twilioClient.messages.list({
     limit: 1,
     from: process.env.TWILIO_SENDER,
     to: sender,
   });
   const lastMessage = lastMessages[0]?.body;

   if (lastMessage) {
     const restockPattern = /\d+/;
     const lastOrderPattern = /(\d+)x/;
     const titlePattern = /"(.*?)"/;

     const restock = message.match(restockPattern)
       ? +message.match(restockPattern)[0]
       : undefined;

     try {
       const lastOrder = +lastMessage.match(lastOrderPattern)[1];
       const title = lastMessage.match(titlePattern)[1];
       const books = await cds.read("Books").where({ title });

       return {
         restock: restock || lastOrder,
         book: books[0],
       };
     } catch (err) {
       //regex didn't find a last order or book title
       return {};
     }
   }
 }

 app.use(bodyParser.urlencoded({ extended: true }));

Bevor du den kompletten Ablauf von Ende zu Ende testest, führe ihn zunächst lokal aus. Dazu musst du die folgende Anfrage zur Datei requests.http hinzufügen; denke daran, den Platzhalter durch deine Telefonnummer zu ersetzen, bevor du die HTTP-Anfrage sendest.

Beachte dabei, dass du das Pluszeichen mit %2b kodieren musst.

### Teste den Webhook um Bücher nachzubestellen
POST http://localhost:4004/twilioWebhook HTTP/1.1
Content-Type: application/x-www-form-urlencoded

Body=Yes 400&From=%2b49151000000

Du solltest jetzt eine TwiML-Antwort (Twilio Markup Language) sehen. Dieses Markup beinhaltet alle nötigen Parameter, damit die Twilio Server einer SMS-Antwort an den Absender zurücksenden können.

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Message>Erledigt ✅, dein Lieferant wurde kontaktiert und ab morgen sind 500 Exemplare auf Lager.</Message>
</Response>

Benutze ngrok um einen Tunnel vom Internet zu Port 4004 zu öffnen.

ngrok http 4004

Die Konsolenausgabe von ngrok

Öffne zunächst dieTwilio-Konsole und navigiere dann zu deiner Telefonnummer. Dort kannst du die HTTPS-URL, die im vorherigen Schritt von ngrok ausgegeben wurde, bei „A message comes in“ hinzufügen. Beachte, dass das Suffix /twilioWebhook an die URL angehängt werden muss.

Die Twilio Konsole mit der gesetzten Webhook

Nun sind wir bereit, den kompletten Ablauf von Ende zu Ende zu testen. Antworte mit „Ja, bestelle 100 weitere Bücher“ auf die Nachricht, die du vor wenigen Minuten erhalten hast. Nach der Bestätigungs-SMS kannst du nun erneut den aktuellen Bestand, mit der ersten HTTP-Anfrage, abfragen.

Ein Screenshot von Visual Studio Code und  einem Smartphone der den SMS Verlauf anzeigt.

Wie geht es weiter?

Glückwunsch, du hast es geschafft! Du hast einen bidirektionalen SMS-Kommunikationskanal zu einer CAP-Anwendung hinzugefügt und damit die User Experience verbessert. Zum Abgleich findest du den gesamten Quellcode auch auf GitHub.

Von hier aus kannst du die Beispielanwendung auf verschiedene Weise erweitern. Du kannst beispielsweise mehr über das SAP Cloud Application Programming Model erfahren, um eine komplexe Unternehmensanwendungen zu erstellen oder zusätzliche Kommunikationskanäle wie E-Mail, WhatsApp, Anrufe (Twilio Voice) oder Video hinzuzufügen. Oder du kannst den Anmeldeprozess mit Twilio Verify absichern, um deine Webanwendung vor Bots zu schützen.

Wenn du Fragen zu dieser Anwendung oder einem verwandten Thema hast, kannst du mich gerne auf einem der folgenden Kanäle kontaktieren: