Automatisierter Faxversand

Tobias
07.02.2022 0 15:50 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– und REST-APIs von sipgate.io nutzen, um automatisch auf Anrufe zu reagieren und nach erfolgter Authentifizierung ein Fax zu versenden.

In diesem Tutorial

In diesem Projekt wird ein einfacher Webserver eingerichtet, der auf einem lokalen Rechner läuft. Sobald ein Anruf eingeht, nimmt dieser Webserver diesen entgegen und spielt eine Audiodatei ab, die den Benutzer auffordert, eine gültige Kunden-ID einzugeben. Anhand der Kunden-ID wird eine Voicemail abgespielt, ein PIN aus der Datenbank geholt und als PIN per SMS an den Nutzer zurückgesendet. Wenn der Benutzer diese PIN korrekt als DTMF eingibt, wird ein Fax gesendet. Unsere Anwendung besteht aus fünf Phasen:

  1. Die Anwendung beantwortet einen Anruf mit einer Sprachansage, welche den Kunden dazu auffordert, eine gültige Kundennummer einzugeben.
  2. Nachdem eine gültige Eingabe gemacht wurde, werden die Kundendaten aus einer Datenbank (MySQL) geholt.
  3. Wenn die Kundennummer vorhanden ist, wird eine Sprachansage abgespielt und eine SMS mit der PIN an den Kunden geschickt.
  4. Der Kunde sendet die PIN an die Anwendung zurück, um seine Identität zu bestätigen.
  5. Wenn die PIN korrekt eingegeben wurde, wird das Fax an einen ausgewählten Empfänger versendet.

Voraussetzungen

Um dieses Projekt zu implementieren, wird die Laufzeitumgebung Node.js, der Paketmanager NPM und Docker benötigt.

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über hinaus wird die Datei src/entities/Customer.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

Mit dem folgenden Befehl installiert uns 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
  • 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 und der Port, auf den wir zugreifen, 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-sendfax
DATABASE_USER=
DATABASE_PASSWORD=
DATABASE_ROOT_PASSWORD=
# SMS
SIPGATE_TOKEN_ID=
SIPGATE_TOKEN=
SIPGATE_SMS_EXTENSION=
# Fax
SIPGATE_FAX_EXTENSION=
SIPGATE_FAX_RECIPIENT=

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 erstellt einen Container 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-sendfax-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"],
};

Entitäten

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() anzeigen. Im Anschluss legen wir noch eine Spalte für den einzugebenden PIN an.

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

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

  @Column()
  pin: number;
}

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 from "../entities/Customer";

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

Nachdem die Tabelle definiert wurde, wird die Tabelle mit Testdaten, bestehend aus ID und PIN, befüllt. Die IDs repräsentieren die Nummern, die später während des Anrufes eingegeben werden können und der PIN stellt die Authentifizierung sicher.

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

    const customerData = [
      {
        id: 12345678,
        pin: 12345,
      },
      {
        id: 87654321,
        pin: 11223,
      },
      {
        id: 11111111,
        pin: 32145,
      },
      {
        id: 22222222,
        pin: 45612,
      },
      {
        id: 33333333,
        pin: 98745,
      },
    ];

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

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

Aufbau des Skripts

Im folgenden wird erläutert, wie die eigentliche Anwendung auf dem Webserver aufgebaut ist. Dazu gehen wir den Programmcode Stück für Stück durch.

In der Methode getAnnouncementByOrderStatus() befinden sich die URLs zu den Audiodateien. 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 Audiodateien in diesem Fall öffentlich zugänglich seien müssen. Ansonsten können die Audiodateien nicht abgespielt werden.

const getAnnouncementByOrderStatus = (
  sendFaxStatus: Status | null,
): string => {
  switch (sendFaxStatus) {
    case OrderStatus.CUSTOMER_ID_INPUT:
      return "https://github.com/sipgate-io/io-labs-sendfax/blob/main/static/request_customerid.wav?raw=true";
    case OrderStatus.PIN_SENT:
      return "https://github.com/sipgate-io/io-labs-sendfax/blob/main/static/pin_sent.wav?raw=true";
    case OrderStatus.FAX_SENT:
      return "https://github.com/sipgate-io/io-labs-sendfax/blob/main/static/fax_sent.wav?raw=true";
    case OrderStatus.INVALID_ID:
      return "https://github.com/sipgate-io/io-labs-sendfax/blob/main/static/invalid_id.wav?raw=true";
    case OrderStatus.INVALID_AUTH:
      return "https://github.com/sipgate-io/io-labs-sendfax/blob/main/static/auth_error.wav?raw=true";
    default:
      return "https://github.com/sipgate-io/io-labs-sendfax/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 der PIN als SMS gesendet und eine Audiodatei zur Bestätigung abgespielt. Liegt die Kundennummer jedoch nicht in der Datenbank, wird eine Fehlermeldung in der Konsole ausgegeben und es wird eine Audiodatei abgespielt.

const getAnnouncementByCustomerId = async (
  customerId: string, status: Status, recipient: string
): Promise<string> => {
  const customer = await getDatabaseRepository(Customer).findOne(customerId);
  if (!customer) {
    console.log(`Customer with Id: ${customerId} not found...`);
    return getAnnouncementByOrderStatus(null);
  }
  sendSmsPin(
  personalAccessTokenId,
  personalAccessToken,
  recipient,
  customer.pin,
  smsExtension,
  );
  return getAnnouncementByOrderStatus(status);
};

In der Funktion getAnnouncmentByCustomerPIN wird geprüft, ob der übermittelte PIN mit der hinterlegten PIN in der Datenbank übereinstimmt. Wenn die der Fall ist, wird ein Fax versendet und eine Audiodatei zur Bestätigung abgespielt. Stimmt die PIN jedoch nicht überein, wird eine Fehlermeldung in der Konsole ausgegeben und wiederum eine Audiodatei abgespielt.

const getAnnouncmentByCustomerPIN = async (
  customerId: string,
  pin: string,
  status: Status,
): Promise => {
  const customer = await getDatabaseRepository(Customer).findOne(customerId);
  if (!customer) {
    console.log(`Customer with Id: ${customerId} not found...`);
    return getAnnouncementByOrderStatus(null);
  }
  if (customer.pin === Number(pin)) {
    sendFaxPin(personalAccessTokenId, personalAccessToken, faxlineId, to);
    return getAnnouncementByOrderStatus(status);
  }
  return getAnnouncementByOrderStatus(null);
};

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 createDatabaseConnection aus dem typeorm-Paket stellt die Verbindung zur Datenbank her. Sobald diese aufgebaut ist, wird mit der Methode createServer() der Server erstellt.

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

Wenn ein Anruf eingeht (onNewCall), wird eine Audiodatei wiedergegeben und der Anrufende gebeten, eine Kundennummer einzugegeben. Dies passiert in der WebhookResponse.gatherDTMF() Methode. 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 dem Anrufenden noch 5 Sekunden Zeit, um seine Kundennummer einzutippen.

webhookServer.onNewCall((newCallEvent) => {
  console.log(`New call from ${newCallEvent.from} to ${newCallEvent.to}`);
  stage.set(newCallEvent.callId, {
    faxStatus: Status.CUSTOMER_ID_INPUT,
    recipient: newCallEvent.from,
    customerId: '',
  });

  return WebhookResponse.gatherDTMF({
    maxDigits: CUSTOMERID_DTMF_INPUT_LENGTH,
    timeout: 5000,
    announcement:
      'https://github.com/sipgate-io/io-labs-sendfax/blob/main/static/request_customerid.wav?raw=true',
  });
});

Sobald DTMF-Daten übertragen werden (onData), geht die Anwendung zum nächsten Schritt über. Im Falle einer übergebenen Kundennummer durch den Anrufenden, wird diese gegen die Datenbank verifiziert und im Folgeschritt eine PIN per SMS an den Anrufenden gesendet.
Den Code dazu, findet man unter src/utils/sendSms.ts.

import * as dotenv from "dotenv";
import * as dotenv from "dotenv";
import { createSMSModule, sipgateIO } from "sipgateio";
dotenv.config();

export const sendSmsPin = (
  personalAccessTokenId: string,
  personalAccessToken: string,
  to: string,
  pin: number,
  smsId: string,
) => {
  const client = sipgateIO({
    tokenId: personalAccessTokenId,
    token: personalAccessToken,
  });

  const message: string = `Your Pin is: ${pin}`;

  const shortMessage = {
    message,
    to,
    smsId,
  };
  const sms = createSMSModule(client);

  sms
    .send(shortMessage)
    .then(() => {
      console.log(`Sms sent to ${to}`);
    })
    .catch(console.error);
};

Im Falle einer PIN-Eingabe durch den Anrufenden, wird diese ebenfalls verifiziert und im Folgeschritt ein Fax an die hinterlegte Adresse in der .env-Datei geschickt.

Versenden des Fax

Um das Fax zu senden, wird der folgende Code benötigt, welcher in src/utils/sendFax.ts zu finden ist.

import * as dotenv from "dotenv";
import * as fs from "fs";
import * as path from "path";
import { createFaxModule, sipgateIO } from "sipgateio";
dotenv.config();

export const sendFaxPin = async (
  personalAccessTokenId: string,
  personalAccessToken: string,
  faxlineId: string,
  to: string,
): Promise => {
  const client = sipgateIO({
    tokenId: personalAccessTokenId,
    token: personalAccessToken,
  });

  const filePath: string = "./src/utils/testpage.pdf";
  const { name: filename } = path.parse(path.basename(filePath));
  const fileContent: Buffer = fs.readFileSync(filePath);

  const fax = createFaxModule(client);

  const faxSendResponsePromise = fax.send({
    fileContent,
    filename,
    to,
    faxlineId,
  });

  faxSendResponsePromise
    .then((sendFaxResponse) => {
      console.log(`Fax sent with id: ${sendFaxResponse.sessionId}`);
      const faxStatusPromise = fax.getFaxStatus(sendFaxResponse.sessionId);
      faxStatusPromise
        .then((faxStatus) => {
          console.log(`Fax status: ${faxStatus}`);
        })
        .catch((error) => {
          console.error("Fax status could not be retrieved: ", error);
        });
    })
    .catch((error) => {
      console.error("Fax could not be sent with Error: ", error);
    });
};

Dieses Code-Snippet verwendet die createFaxModule()-Methode aus der sipgate.io-Library, um auf die REST-API zuzugreifen und Faxe zu versenden. Die .send()-Methode des Fax-Moduls sendet das Fax. Dazu wird an die Methode ein Buffer übergeben, welcher den Inhalt der PDF-Datei enthält, die versendet werden soll. In diesem Fall liegt die PDF-Datei unter src/utils/testpage.pdf.

Ausführung des Projekts

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

1. Führen Sie in einem Terminal den Befehl ssh -R 80:localhost:8080 nokey@localhost.run aus.

2. Kopieren Sie von dort die letzte URL und lassen Sie das Terminal geöffnet.

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

4. Kopieren Sie die URL von Schritt 2 nach SIPGATE_WEBHOOK_SERVER_ADDRESS, so dass 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 die sipgate Accounteinstellungen gemacht.

7. Unsere Anwendung benötigt zudem ein Personal Access Token mit sessions:sms:write, sessions:fax:write und history:read Berechtigungen. Dieses Token wird in den Einstellungen erzeugt und muss in der .env-Datei hinterlegt werden.

8. Setzen Sie die SIPGATE_SMS_EXTENSION-Variable in der .env-Datei:

SIPGATE_SMS_EXTENSION=s1

Eine Web SMS Extension besteht aus dem Buchstaben „s“ gefolgt von einer Zahl (z.B. „s0“). Die sipgate API verwendet das Konzept der Web SMS Extensions, um Geräte innerhalb Ihres Kontos zu identifizieren, die in der Lage sind, SMS zu versenden. In diesem Zusammenhang bezieht sich der Begriff „Gerät“ nicht unbedingt auf ein Hardware-Telefon, sondern auf einen virtuellen Anschluss. Mit der REST-API finden wir heraus, welche die SMS Extension ist.
Zum Beispiel:curl –user tokenId:token https://api.sipgate.com/v2/{userId}/sms
Ersetzen Sie tokenId und token durch die sipgate-Anmeldedaten und userId durch die sipgate User-ID. Die User-ID besteht aus dem Buchstaben ‚w‘ gefolgt von einer Zahl (z.B. ‚w0‘). Sie kann wie folgt ermittelt werden: Loggt man sich auf der sipgate Seite ein, sollte die URL der Seite die Form https://app.sipgate.com/{userId}/… haben, wobei {userId} die User-ID ist.

9. Um eine Faxnachricht senden zu können, muss eine Faxline, bzw. ein virtuelles Faxgerät erstellt werden. Dieses kann über den Punkt Telefonie in den sipgate Accounteinstellungen eingerichtet werden. Alternativ kann es auch über die REST-API konfiguriert werden. Nachdem Sie das Faxgerät erstellt haben, müssen Sie dessen ID in der .env-Konfigurationsdatei unter der Variablen SIPGATE_FAX_EXTENSION hinterlegen. In diesem Beispiel wird die Faxline mit der ID f5 verwendet.

SIPGATE_FAX_EXTENSION=f5

10. Die Empfänger Rufnummer kann durch das Setzen der Variable SIPGATE_FAX_RECIPIENT eingestellt werden.

11. Anschließend muss die Datenbank mit dem Befehl gestartet werden:

docker-compose up -d

12. Wenn der Docker Container mit der dazugehörigen Datenbank gestartet wurde, kann die Datenbank durch ein NPM-Skript initialisert werden. Dazu wird folgender Befehl auf der Kommandozeile ausgeführt:

npm run database:init

13. Um das Programm zu starten, muss nur noch das Start-Skript aus dem Hauptverzeichnis des Projektes ausgeführt werden:

npm start

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

In diesem Tutorial haben Sie gelernt, wie Sie über eine einfache IVR automatisch eine PIN abfragen und ein Fax versenden 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.