Telephone status request

Jimmy
11.01.2022 0 11:51 min

What is sipgate.io?

sipgate.io is a collection of APIs that enables sipgate customers to create flexible integrations for their individual needs. Among other things, it offers interfaces for sending and receiving text messages or faxes, for monitoring the call history as well as for initiating and manipulating calls. In this tutorial we will use the Push-API from sipgate.io to answer a call and start an IVR process. Callers can then transmit their customer number using DTMF tones to find out what their processing status is.

In this tutorial

In this tutorial, we will write a project that starts two services. The first service is a database that stores customer data. The second service is a web server that responds to Push-API calls from sipgate.io and answers them. If someone calls our sipgate phone number, the web server will answer this call and starts an IVR process. Our IVR system consists of two phases:

  1. Welcome phase: An audio file greets callers and asks them for their customer number.
  2. Information phase: As soon as the callers have transmitted their customer numbers, the system looks in the database for a corresponding entry. The callers then receive information about their processing status in the form of another audio file.

Prerequisites: You have Node.js, NPM and Docker installed on your computer.

In this project, the focus is on matching the customer number with the database. If you want to learn more about the IVR process, you should take a look at the blog entry Create complex IVRs for your CRM yourself.

First steps

Setting up the project

We use Node.js together with the official sipgate.io library. The first step is to create a new Node.js project which contains an index.ts file where the web server script is written. In addition, the files src/entities/Customer.ts and src/entities/CreateCustomerTable.ts are needed for the database.

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

With the following command the package manager NPM installs the needed packages:

npm i sipgateio dotenv ts-node typeorm mysql
npm i -D typescript
  • sipgateio: A library developed by sipgate that allows us to build a server and design responses to webhooks.
  • dotenv: Allows to read the variables stored in a .env file.
  • ts-node and typescript: Allows us to run TypeScript directly.
  • typeorm and mysql: Enables easy handling of the MySQL database.

Further configuration

We create the next two necessary files with:

touch .env tsconfig.json

The .env file contains some environment variables that must be set before starting the project. For the server, the server address (which we will add later) and the port are needed. The database needs the connection data consisting of the host, the port, a name, a user, a password and the root password. The variables DATABASE_USER, DATABASE_PASSWORD and DATABASE_ROOT_PASSWORD can be set arbitrarily.

# 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=

The tsconfig.json specifies the root files and compiler options required to compile a Typescript project:

{
    "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"]
}
  

Lastly, we need to make changes to the automatically generated package.json. Add the following lines for the execution of the project. They define the entry point of the project and add two scripts. The start script will start the web server and the database:init script will populate the database with some test data.

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

Create the database

The first component of the project is the database. We use a MySQL database that is started using docker-compose. So first make sure that docker-compose and docker are installed on your machine. For more information on this, see here.

Once Docker Compose is installed, we can create a docker-compose.yml file.

touch docker-compose.yml

Add the following content to the file. The docker-compose.yml will create a container in which a MySQL server is running. The user, password, root password and database name will be as specified in the .env file. The database will be accessible via port 3306.

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}
  

Next, we fill the database with data. For this, we use the typeorm library. To set up the database, we need the following: the file ormconfig.js and the folders entities and migrations.

ormconfig.js

First, the specified host, port, name, user and password are read from the .env file with the dotenv package. These variables are then used to initialize the database. The database schema is described using the entities, which are defined in TypeScript files. The path for these is given as src/entities/**/*.ts. The contents of the database are specified in migrations. These are defined in files with the path src/migrations/**/*.ts. In our case the database schema is described in Customer.ts.

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

Based on the entities, the database schema is described as before. Thereby our database gets the following columns. On the one hand, we need a unique customer number, which we define as the primary key. This can be done with the help of the annotation @PrimaryGeneratedColumn(). Following that we will add the orderStatus attribute implemented as an enum using the values RECEIVED, PENDING, FULFILLED, and CANCELLED.

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 the migration file the table is defined. Again it is defined that the customer number is a primary key.


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",
      },
    ],
  });

After the table has been defined, the table is filled with test tuples consisting of ID and orderStatus. The IDs represent the numbers that can be entered later during the call and the orderStatus represents the order status for this customer.


  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);
  }
}

Structure of the script

In the following, we will explain how the actual program for the web server is structured. For this, we go through the program piece by piece.

In the method getAnnouncementByOrderStatus() are the URLs to the sound files. These are selected by a switch-case construct, depending on the order status. Afterward, the method returns the URL as a string. Please note, that the sound files must be publicly accessible from the internet. Otherwise, the sound files cannot be played by sipgate.

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 the getAnnouncementByCustomerId function the existence of a specified customer is checked. If this is the case, the respective status is announced. However, if the customer number is not in the database, an error message is displayed in the server console and an audio file is played to indicate this.


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);
};

In the following code block, we establish the database connection and start our web server. Since we use the typeorm package for the database connection and the sipgateio package for interacting with the Push API, these tasks can be accomplished almost as a one-liner. The createConnection method from the typeorm package establishes the connection to the database. After that, the createWebhookModule().createServer(…) method creates the web server.


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

In order for our web server to respond to calls, we tell it how to respond to an incoming call or to incoming DTMF data using the onNewCall or the onData callbacks.
When a call comes in, we want an audio file to be played and the caller to enter the customer number. For this, we can use the WebhookResponse.gatherDTMF(…) method. As parameters, we pass the maximum number of DTMF digits we expect, the length of the timeout (5 seconds in our case), and the URL where the audio file of the announcement is located. After the audio file is played, the system gives the caller another 5 seconds to type in their customer number. As soon as this has expired, the DTMF data is transmitted and the onData event is called. If the customer has already entered the maximum number of digits, the event will be sent earlier.

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",
  });
});

This code block checks if the provided DTMF sequence has the right length. Then, the customer number is passed to getAnnouncementByCustomerId to check it.


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();
});

Running the project

To run the project locally, follow these steps:

  1. execute the command ssh -R 80:localhost:8080 nokey@localhost.run in a terminal.
  2. Copy the URL from the terminal output and leave the terminal open.
  3. Now, copy the .env.example to .env
  4. Copy the URL from step 2 to SIPGATE_WEBHOOK_SERVER_ADDRESS so that the .env file matches the following scheme
    SIPGATE_WEBHOOK_SERVER_ADDRESS=https://d4a3f97e7ccbf2.localhost.run
            SIPGATE_WEBHOOK_SERVER_PORT=8080
            
  5. The fields DATABASE_USER, DATABASE_PASSWORD and DATABASE_ROOT_PASSWORD can be filled with your own values.
  6. Now the Webhook URL must be set in the sipgate account, which can be done via the sipgate account settings.
  7. Now, start the database with this command: docker-compose up -d.
  8. Once the Docker container has been started with the associated database, you can initialize the database using npm run database:init.
  9. To start the program, you have to execute npm start from the main directory of the project.

Now you can call your sipgate phone number to test the application. Once the call has been successfully transferred, you will see output in your console.

Outlook

If you’ve made it this far, congratulations! In this tutorial, you have learned how to automatically request a status over the phone using a simple IVR. However, this project doesn’t have to be the end, it can be extended as much as you like and the audio files can be personalized.

You can find the complete project in our GitHub repository.

If you want to learn more about the possibilities of our sipgate.io library, have a look at our other tutorials, e.g. about creating a complex IVR system.

Keine Kommentare


Schreibe einen Kommentar

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