Telefonische Statusabfrage

Mick
10.01.2022 0 12:05 min

 

Was ist sipgate.io?

sipgate.io ist eine Sammlung von APIs, die es Kund:innen von sipgate ermöglicht, flexible Integrationen für ihre individuellen Bedürfnisse zu erstellen. Sie bietet unter anderem Schnittstellen zum Senden und Empfangen von Textnachrichten oder Faxen, zur Überwachung der Anrufhistorie sowie zum Initiieren und Manipulieren von Anrufen. In diesem Tutorial werden wir die Push-API von sipgate.io nutzen, um einen Anruf anzunehmen und einen IVR-Prozess zu starten. Anrufer:innen können dann mit Hilfe von DTMF-Tönen ihre Kundennummer übertragen, um zu erfahren, wie weit ihr Bearbeitungsstatus ist.

In diesem Tutorial

Wir werden in diesem Tutorial ein Projekt schreiben, dass zwei Services startet. Der erste Service ist eine Datenbank, in der Kundendaten gespeichert sind. Der zweite Service ist ein Webserver, der auf Push-API Aufrufe von sipgate.io reagiert und diese beantwortet. Ruft jemand unsere sipgate-Telefonnummer an, wird der Webserver diesen Anruf entgegennehmen und einen IVR-Prozess abspielen. Unser IVR-System besteht aus zwei Phasen:

  1. Begrüßungsphase: Eine Audiodatei begrüßt Anrufer:innen und fragt sie nach deren Kundennummer.
  2. Auskunftsphase: Sobald die Anrufer:innen ihre Kundennummern übermittelt haben, schaut das System in der Datenbank nach einem entsprechenden Eintrag. Anschließend erhalten die Anrufer:innen Auskunft über ihren Bearbeitungsstatus in Form einer weiteren Audiodatei.

Voraussetzungen: Sie haben Node.js and NPM auf ihrem Computer installiert.

In diesem Projekt liegt der Fokus auf dem Abgleichen der Kundennummer mit der Datenbank. Sollten Sie sich mehr über den IVR-Prozess informieren wollen, sollten Sie einen Blick auf den Blog-Eintrag Komplexe IVRs für Dein CRM selbst erstellen werfen.

Erste Schritte

Aufsetzen des Projekts

Wir nutzen Node.js gemeinsam mit der offiziellen sipgate.io-Bibliothek. Der erste Schritt ist ein neues Node.js Projekt zu erstellen, welches eine index.ts Datei enthält, in der das Webserver-Skript geschrieben wird. Darüberhinaus werden die Dateien src/entities/Customer.ts und src/entities/CreateCustomerTable.ts für die Datenbank benötigt.

npm init -y
mkdir src
mkdir src/entities
mkdir src/migrations
touch src/index.ts
touch src/entities/Customer.ts
touch src/migrations/CreateCustomerTable.ts

Mit dem folgenden Befehl installiert uns der Paketmanager npm die benötigten Pakete:

npm i sipgateio dotenv ts-node typeorm mysql
npm i -D typescript
  • sipgateio: Eine von sipgate entwickelte Library, mit der wir einen Server aufbauen können und Antworten auf Webhooks entwerfen können.
  • dotenv: Erlaubt, die in der .env-Datei gespeicherten Variablen zu lesen.
  • ts-node &typescript: Hilft, TypeScript-Dateien ausführen zu können (typescript wird als Entwickler Abhängigkeit (dev dependency) installiert).
  • typeorm & mysql: Ermöglicht einen einfachen Umgang mit der MySQL Datenbank.

Weitere Konfigurationen

Die nächsten beiden benötigten Dateien erstellen wir mit:

touch .env tsconfig.json

Die .env-Datei enthält einige Umgebungsvariablen, die vor dem Starten des Projektes festgelegt werden müssen. Für den Server werden die Serveradresse (die wir später noch hinzufügen werden) und der Port, auf den wir hören, benötigt. Die Datenbank braucht die Verbindungsdaten bestehend aus dem Host, dem Port, einem Namen, einem User mit Passwort und dem root-Passwort. Die Variablen DATABASE_USER, DATABASE_PASSWORD und DATABASE_ROOT_PASSWORD können beliebig gesetzt werden.

# Webhook server address
SIPGATE_WEBHOOK_SERVER_ADDRESS=
# Webhook server port
SIPGATE_WEBHOOK_SERVER_PORT=8080
# Database
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_NAME=io-labs-telephone-status-request
DATABASE_USER=
DATABASE_PASSWORD=
DATABASE_ROOT_PASSWORD=

Die tsconfig.json gibt die Root-Dateien und die Compileroptionen an, die zum Kompilieren eines TypeScript-Projekts erforderlich sind:

{
    "compilerOptions": {
        "target": "es2017",
        "module": "commonjs",
        "lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
        "skipLibCheck": true,
        "sourceMap": true,
        "outDir": "./dist",
        "moduleResolution": "node",
        "removeComments": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "noImplicitThis": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "resolveJsonModule": true,
        "baseUrl": "."
    },
    "exclude": ["node_modules"],
    "include": ["./src/**/*.ts"]
}
  

Zuletzt müssen wir noch Änderungen an der automatisch generierten package.json vornehmen. Füge die folgenden Zeilen für die Ausführung des Projekts hinzu. Es wird die main-Datei des Projektes definiert und zwei Skripte geschrieben. Das Skript start wird den Web-Server starten und das database:init wird die Datenbank mit einigen Testdaten befüllen.

"main": "src/index.ts",
"scripts": {
    "start": "ts-node ."
    "database:init": "ts-node ./node_modules/.bin/typeorm migration:run",
}
    

Erstellen der Datenbank

Als erste Komponente des Projektes wird die Datenbank erstellt. Wir nutzen eine MySQL Datenbank, die mithilfe von Docker Compose gestartet wird. Stellen Sie also zunächst sicher, dass Docker Compose und Docker bei Ihnen am Gerät installiert sind. Für nähere Informationen dazu siehe hier.
Sobald Docker installiert ist, können wir eine docker-compose.yml Datei erstellen

touch docker-compose.yml

und in diese Datei folgender Inhalt eingefügt werden. Die docker-compose.yml wird ein Container erstellen auf dem ein MySQL Server läuft. Der User, das Passwort, das Root-Passwort und der Datenbankname werden so sein wie in der .env-Datei spezifiziert. Die Datenbank wird über den Port 3306 erreichbar sein.

version: "3"
services:
  db:
    image: mysql
    restart: always
    command: --default-authentication-plugin=mysql_native_password
    container_name: io-labs-telephone-status-request-db
    ports:
      - 127.0.0.1:3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=${DATABASE_ROOT_PASSWORD}
      - MYSQL_DATABASE=${DATABASE_NAME}
      - MYSQL_USER=${DATABASE_USER}
      - MYSQL_PASSWORD=${DATABASE_PASSWORD}
  

Als nächstes befüllen wir die Datenbank mit Daten. Dafür nutzen wir die Bibliothek typeorm. Zur Einrichtung der Datenbank wird Folgendes benötigt: Die Datei ormconfig.js und die Ordner entities und migrations.

ormconfig.js

Zu Beginn werden der eingetragene Host, Port, Name, User und das Passwort aus der .env Datei mit dem dotenv-Package eingelesen. Diese Variablen werden dann zur Initialisierung der Datenbank verwendet. Das Datenbankschema wird anhand der Entitäten beschrieben, welche in TypeScript-Dateien definiert werden. Der Pfad für diese ist gegeben als src/entities/**/*.ts. Der Inhalt der Datenbank wird in sogenannten Migrationen angegeben. Diese werden in Dateien mit dem Pfad src/migrations/**/*.ts definiert. In unserem Fall wird das Datenbankschema in der Customer.ts beschrieben.

require("dotenv").config();

const {
  DATABASE_HOST,
  DATABASE_PORT,
  DATABASE_NAME,
  DATABASE_USER,
  DATABASE_PASSWORD,
} = process.env;

module.exports = {
  type: "mysql",
  host: DATABASE_HOST,
  port: DATABASE_PORT,
  username: DATABASE_USER,
  password: DATABASE_PASSWORD,
  database: DATABASE_NAME,
  synchronize: false,
  entities: ["src/entities/**/*.ts"],
  migrations: ["src/migrations/**/*.ts"],
};

Entities

Anhand der Entitäten wird wie vorhin beschrieben das Datenbankschema definiert. Dabei erhält unsere Datenbank folgende Spalten: zum einen benötigen wir eine eindeutige Kundennummer, welche wir als Primärschlüssel festlegen. Dies lässt sich mithilfe der Annotation @PrimaryGeneratedColumn() Im Anschluss legen wir noch einen Order Status an. Dieser wird mithilfe eines Enums mit den Werten RECEIVED, PENDING, FULFILLED und CANCELLED angelegt.

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

export enum OrderStatus {
  RECEIVED,
  PENDING,
  FULFILLED,
  CANCELED,
}

@Entity()
export default class Customer {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    type: "enum",
    enum: OrderStatus,
  })
  orderStatus: OrderStatus;
}

Migration

In der Migrations-Datei wird nochmals die Tabelle definiert. Auch hier wird wieder festgelegt, dass es sich bei der Kundennummer um einen Primärschlüssel handelt.


import { MigrationInterface, QueryRunner, Table } from "typeorm";

import Customer, { OrderStatus } from "../entities/Customer";

export default class CreateCustomerTable
  implements MigrationInterface {
  private customerTable = new Table({
    name: "customer",
    columns: [
      {
        name: "id",
        type: "integer",
        isPrimary: true,
      },
      {
        name: "orderStatus",
        type: "integer",
      },
    ],
  });

Nachdem die Tabelle definiert wurde, wird die Tabelle mit Testtupeln, bestehend aus ID und orderStatus, befüllt. Die IDs repräsentieren die Nummern, die später während des Anrufes eingegeben werden können und der orderStatus stellt den Bestellstatus zu dieser Nummer dar.


  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(new Table(this.customerTable), true);

    const customerData = [
      {
        id: 12345678,
        orderStatus: OrderStatus.RECEIVED,
      },
      {
        id: 87654321,
        orderStatus: OrderStatus.PENDING,
      },
    ];

    await Promise.all(
      customerData.map((data) => {
        const customer = new Customer();
        customer.id = data.id;
        customer.orderStatus = data.orderStatus;
        return queryRunner.manager.save(customer);
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable(this.customerTable);
  }
}

Aufbau des Skripts

Im folgenden wird nun erläutert, wie das eigentliche Programm für den Webserver aufgebaut ist. Dazu gehen wir das Programm Stück für Stück durch.

In der Methode getAnnouncementByOrderStatus() befinden sich die URLs zu den Soundfiles. Diese werden durch ein Switch-Case-Konstrukt, je nach Order-Status ausgewählt. Im Anschluss gibt die Methode die URL als String zurück. Hierbei ist zu beachten, dass die Soundfiles in diesem Fall öffentlich zugänglich seien müssen. Ansonsten können die Soundfiles nicht abgespielt werden.

const getAnnouncementByOrderStatus = (
  orderStatus: OrderStatus | null,
): string => {
  switch (orderStatus) {
    case OrderStatus.RECEIVED:
      return "https://github.com/sipgate-io/io-labs-telephone-status-request/blob/main/static/orderstatus_received.wav?raw=true";
    case OrderStatus.PENDING:
      return "https://github.com/sipgate-io/io-labs-telephone-status-request/blob/main/static/orderstatus_pending.wav?raw=true";
    case OrderStatus.FULFILLED:
      return "https://github.com/sipgate-io/io-labs-telephone-status-request/blob/main/static/orderstatus_fulfilled.wav?raw=true";
    case OrderStatus.CANCELED:
      return "https://github.com/sipgate-io/io-labs-telephone-status-request/blob/main/static/orderstatus_canceled.wav?raw=true";
    default:
      return "https://github.com/sipgate-io/io-labs-telephone-status-request/blob/main/static/error.wav?raw=true";
  }
};

In der Funktion getAnnouncementByCustomerId wird geprüft, ob die angegebene Kundennummer in der Datenbank vorhanden ist. Wenn dies der Fall ist, wird die Audiodatei zum jeweiligen Status abgespielt. Liegt die Kundennummer jedoch nicht in der Datenbank, wird eine Fehlermeldung in der Konsole ausgegeben und es wird eine Audiodatei abgespielt, um dies zu signalisieren.


const getAnnouncementByCustomerId = async (
  customerId: string,
): Promise<string> => {
  const customer = await getDatabaseRepository(Customer).findOne(customerId);
  if (!customer) {
    console.log(`Customer with Id: ${customerId} not found...`);
    return getAnnouncementByOrderStatus(null);
  }
  return getAnnouncementByOrderStatus(customer.orderStatus);
};

Im folgenden Codeblock stellen wir die Datenbankverbindung her und starten unseren Webserver. Da wir das typeorm-Paket für die Datenbankverbindung und das sipgateio-Paket für die Interaktion mit der Push-API verwenden, lassen sich diese Aufgaben beinahe als Einzeiler lösen. Die Methode createConnection aus dem typeorm-Paket stellt die Verbindung zur Datenbank her. Sobald diese aufgebaut ist, wird mit der Methode createWebhookModule().createServer(…) der Server erstellt.


createConnection().then(() => {
  console.log("Database connection established");
  createWebhookModule()
    .createServer({
      port: PORT,
      serverAddress: SERVER_ADDRESS,
    })
    .then((webhookServer) => {
      console.log("Ready for new calls...");
      // TODO
    }

Damit unser Webserver auf Anrufe reagieren kann, sagen wir ihm, wie er auf einen eingehenden Anruf (onNewCall) bzw. auf ankommende DTMF-Daten (onData) reagieren soll.
Wenn ein Anruf eingeht, wollen wir, dass eine Audiodatei abgespielt wird und von Anrufer:innen die Kundennummer eingegeben wird. Dafür können wir die Methode WebhookResponse.gatherDTMF(…) nutzen. Als Parameter übergeben wir die maximale Anzahl von DTMF-Ziffern an, die wir erwarten, die Länge des Timeouts (bei uns 5 Sekunden) und die URL, unter der die Audiodatei liegt. Nachdem die Audiodatei abgespielt wurde, gibt das System den Anrufer:innen noch 5 Sekunden Zeit, um ihre Kundennummer einzutippen. Sobald diese abgelaufen ist, werden die DTMF-Daten übertragen und das onData-Event wird aufgerufen. Sollte der Kunde schon früher, die maximale Anzahl an Ziffern eingegeben haben, wird das Event schon früher abgeschickt.

webhookServer.onNewCall((newCallEvent) => {
  console.log(`New call from ${newCallEvent.from} to ${newCallEvent.to}`);
  return WebhookResponse.gatherDTMF({
    maxDigits: MAX_CUSTOMERID_DTMF_INPUT_LENGTH,
    timeout: 5000,
    announcement:
    "https://github.com/sipgate-io/io-labs-telephone-status-request/blob/main/static/request_customerid.wav?raw=true",
  });
});

In diesem Codeblock wird geprüft, ob die maximale DTMF Länge erreicht wurde. Sobald dies eintritt, wird die Kundennummer an getAnnouncementByCustomerId weitergegeben, um diese zu prüfen.


webhookServer.onData(async (dataEvent) => {
  const customerId = dataEvent.dtmf;
  if (customerId.length === MAX_CUSTOMERID_DTMF_INPUT_LENGTH) {
    console.log(`The caller provided a customer id: ${customerId} `);
    return WebhookResponse.gatherDTMF({
      maxDigits: 1,
      timeout: 0,
      announcement: await getAnnouncementByCustomerId(customerId),
    });
  }
  return WebhookResponse.hangUpCall();
});

Ausführung des Projekts

Um das Projekt lokal auszuführen sollten folgende Schritte befolgt werden:

1. In einem Terminal den Befehl ssh -R 80:localhost:8080 nokey@localhost.run ausführen.

2. Von dort die letzte URL kopieren und das Terminal geöffnet lassen.

3. Nun muss die .env.example kopiert werden und in .env umbenannt werden

4. Die URL von Schritt 2 nach SIPGATE_WEBHOOK_SERVER_ADDRESS kopieren, sodass die .env Datei dem folgenden Schema entspricht

SIPGATE_WEBHOOK_SERVER_ADDRESS=https://d4a3f97e7ccbf2.localhost.run
SIPGATE_WEBHOOK_SERVER_PORT=8080

5. Die Felder DATABASE_USER, DATABASE_PASSWORD und DATABASE_ROOT_PASSWORD können mit eigenen Werten ausgefüllt werden.

6. Jetzt muss die Webhook-URL im sipgate Account gesetzt werden, dies wird über das sipgate app-web gemacht.

7. Anschließend muss die Datenbank mit dem Befehl docker-compose up -d gestartet werden.

8. Wenn der Docker Container mit der dazugehörigen Datenbank gestartet wurde, muss die Datenbank mittels npm run database:init initialisert werden.

9. Um das Programm zu starten, muss nun noch npm start aus dem Hauptverzeichnis des Projektes ausgeführt werden.

Nun können Sie Ihre sipgate Telefonnummer anrufen um die Anwendung zu Testen. Sobald der Anruf erfolgreich übermittelt wurde, sehen Sie eine Ausgabe in Ihrer Konsole.

Aussicht

Wenn Sie bis hierher durchgehalten haben, herzlichen Glückwunsch! In diesem Tutorial haben Sie gelernt, wie Sie über eine einfache IVR automatisch einen Status telefonisch abfragen können. Dieses Projekt muss allerdings noch nicht das Ende sein, es kann beliebig erweitert werden und die Audiodateien können personalisiert werden.

Das vollständige Projekt finden Sie in unserem GitHub-Repository.

Wenn Sie mehr über die Möglichkeiten unserer sipgate.io-Bibliothek erfahren möchten, schauen Sie sich unsere anderen Tutorials an, z.B. über die Erstellung eines komplexen IVRs.

Keine Kommentare


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.