Change your Slack status while in a call ☎

Julian
15.01.2021 0 11:52 min

What is sipgate.io?

sipgate.io is a collection of APIs, which enables sipgate’s customers to build flexible integrations matching their individual needs.
Among other things, it provides interfaces for sending and receiving text messages and faxes, monitoring the call history, as well as initiating and manipulating calls.
In this tutorial, we make use of the webhook functionality, which allows us to react to certain events like a new call getting established or hung up.

Motivation

We at sipgate use Slack for internal communication.
If you want to call your colleagues it is always helpful to know whether the person is currently available or not. On the other hand, if you are on a call it is useful for your colleagues to know that you can not answer them immediately. Thus, the Slack status is a nice way to visualize your availability.

Especially if you make phone calls on a daily basis it can become quite tedious to manually change the status every time you enter or leave a call.

In this tutorial, we provide a small example that sets the Slack status of you and your co-workers automatically when initiating or receiving a call.

Setup

To get started you need a sipgate account with a sipgate.io package booked. Further information about what account suits you best and how to add sipgate.io can be found in our get-started guide.

Slack Setup

In our example, we assume that you have booked at least the Standard Plan for your Slack workspace since only with a paid plan the admin of a workspace can change other peoples statuses.

In our example, we use an OAuth-Access-Token with the desired permissions to change the status message of people in your workspace. For this purpose, we create a custom Slack App.

There are four roles in a Slack workspace with the hierarchy:

Primary Owner >
Workspace Owner > Workspace Admin > Full Member

The scope of people, whose status can be changed, depends on the role of the slack app’s creator.

For example, a custom Slack App created by a Workspace Owner can change his own status and the status of all Workspace Admins and Full Members but not the status of other Workspace Owners or the Primary Owner.

As there is always exactly one Primary Owner we recommend creating the App
with this account.

Custom Slack App

Create an App and on the left menu click on the OAuth & Permissions tab.
Scroll down to the User Token Scopes and add the OAuth scopes users.profile:read and users.profile:write, then scroll back up and click Install to workspace
and install the App. You will receive the OAuth-Access-Token to authenticate with the Slack API. To save the token for later use, we create a .env file in the root directory of our project and save it like this:

export SLACK_TOKEN=<YOUR_OAUTH_ACCESS_TOKEN>

Setup a node project

Now that the Slack setup is ready, we can create our node project.

To initialize our project we can run npm init -y, which creates a package.json file in which we can specify our dependencies.

For this project, we will be using the official sipgate.io Node.js library, which makes working with the sipgate API much easier while also providing a convenient way to set up a webhook server. To add it to our project, run npm install sipgateio.

We also make use of typescript, so we’ll need to add that to our dev-dependencies using npm install -D typescript ts-node @types/node. For our convenience, we also add

  "start": "ts-node src/main.ts",

to the scripts-section in the package.json so that we can use npm start to run the application.

Let’s get coding!

In our start script, we specified src/main.ts as our entrypoint for the application, so we’ll need to create it.

Receiving webhook events

To receive webhook events from sipgate we need a webserver that is publicly accessible.

When using webhooks in production you have to run your code on a proper webserver with a proper address.
However, for development, we recommend using a service that makes your local environment accessible via the internet.
This makes it much easier to quickly test out your written code.

Various free services can be used to accomplish this.
Some examples are localhost.run or ngrok.
Either one supplies you with a public URL that can be used to receive webhooks from sipgate.
Just make sure that you forward the correct port (in this tutorial we’ll be using port 8080) and that the chosen provider offers secure connections through HTTPS.

For example, to forward your local port 8080 to a public address by using localhost.run, execute the following command:

ssh -R 80:localhost:8080 ssh.localhost.run

and copy the domain that gets printed.

To let sipgate know where the webhooks should be sent to, you’ll need to configure the webhook URLs in the console dashboard. Make sure to include the https://, localhost.run does not include it in its output.

We also need to save the server address into an environment variable. We can just add a new one to our .env file, like this:

export SIPGATE_WEBHOOK_SERVER_ADDRESS=<YOUR_SERVER_ADDRESS>

Now let’s create a webserver. To do so we can make use of the sipgate.io library.

import { createWebhookModule, AnswerEvent, HangUpEvent } from "sipgateio";

const webhookModule = createWebhookModule();

const webhookServerPort = 8080;
const webhookServerAddress = process.env.SIPGATE_WEBHOOK_SERVER_ADDRESS;

if (!webhookServerAddress) {
  throw new Error(
    "SIPGATE_WEBHOOK_SERVER_ADDRESS environment variable not set"
  );
}

webhookModule
  .createServer({
    port: webhookServerPort,
    serverAddress: webhookServerAddress,
  })
  .then((server) => {
    console.log("Listening on port", webhookServerPort);

    server.onAnswer(handleAnswer);

    server.onHangUp(handleHangUp);
  });

async function handleAnswer(answerEvent: AnswerEvent) {
  console.log(answerEvent);
}

async function handleHangUp(hangUpEvent: HangUpEvent) {
  console.log(hangUpEvent);
}

The code creates a webhook server on the port 8080 and provides several functions to receive events like the AnswerEvent and the HangUpEvent.
We created two functions to handle those events, which are currently just logged.

Identifying phone numbers

The next step is to actually modify the slack status.
For that, we need a way to link a phone number to a slack member id.

We solved this by providing a mappings.json file which looks like this:

{
  "+4912345678": {
    "slackMemberId": "U01Jxxxxxx"
  }
}

You can get the member id of Slack users in their profile. Open a user profile and click on „More“, there should be a button to copy the member id.
The phone numbers should have the same format as shown above.

To access the mappings in our code, we need to import the file. The slackMemberId will be stored in a custom interface. Insert the following code after the imports.

interface SlackUserInfo {
  slackMemberId: string;
}

const MAPPINGS: Record<string, SlackUserInfo> = require("../mappings.json");

Now that we have access to the mappings, let’s focus on the handleAnswer function.

An AnswerEvent contains the direction of the call, i.e. whether it is incoming or outgoing, the number of the caller/callee, and some metadata.

By checking the call direction we can find out which slackMemberId belongs to the phone number and consequently set the slack status.
In case of an outgoing call, we need the caller number and for an incoming call the callee number.
Let’s write a function that extracts this number from an AnswerEvent.

function getRelevantNumber(answerEvent: AnswerEvent | HangUpEvent): string {
  return answerEvent.direction === "out"
    ? answerEvent.from
    : answerEvent.answeringNumber;
}

We can use this function in the handleAnswer function.
First of all, we want to ignore voicemail calls so we check if the call is one of them.
Now we use the variable relevantNumber to obtain the phone number from the previous code to find the Slack user information from the mappings file.
Then we validate if a mapping is available or not.

async function handleAnswer(answerEvent: AnswerEvent) {
  if (answerEvent.user === "voicemail") return;

  const relevantNumber = getRelevantNumber(answerEvent);
  const slackUserInfo: SlackUserInfo | undefined = MAPPINGS[relevantNumber];

  if (!slackUserInfo) {
    console.warn(
      `[answerEvent] No slack user mapped for number ${relevantNumber}`
    );
    return;
  }

  // set status
}

The handleHangUp function looks pretty similar. Here we ignore all forwarded hang-ups.

async function handleHangUp(hangUpEvent: HangUpEvent) {
  if (hangUpEvent.cause === HangUpCause.FORWARDED) return;

  const relevantNumber = getRelevantNumber(hangUpEvent);
  const slackUserInfo: SlackUserInfo | undefined = MAPPINGS[relevantNumber];

  if (!slackUserInfo) {
    console.warn(
      `[hangupEvent] No slack user mapped for number ${relevantNumber}`
    );
    return;
  }

  // reset status
}

Connect to the Slack API

To interface with the Slack Web API we’re gonna use Slack’s official node package:

npm install @slack/web-api

Create a new file in the src directory called slack.ts and insert the following:

import { WebClient } from "@slack/web-api";

const token = process.env.SLACK_TOKEN;
if (!token) throw new Error("SLACK_TOKEN environment variable not set!");

const web = new WebClient(token);

This code snippet creates a Slack WebClient with the token from the environment variable, configured earlier. In a real-world project, you would probably not want to have a global variable for the slack client, but for a tutorial this solution is sufficient.

Now we are authorized and can communicate with the API.

Let’s create a function to set the status of a user:

export interface Status {
  status_text: string;
  status_emoji: string;
}

export async function setStatus(
  slackMemberId: string,
  status: Status
): Promise<void> {
  const response = await web.users.profile.set({
    user: slackMemberId,
    profile: JSON.stringify(status),
  });

  if (!response.ok) throw Error(response.error);
}

The function takes a Slack member id and a Status object. This object contains the information to which the status should be set.

To save the status of a Slack member before updating it, we need a function to get the current status.
We will return the status to its previous state later.

export async function getStatus(slackMemberId: string): Promise<Status> {
  const response = await web.users.profile.get({ user: slackMemberId });
  if (!response.ok) throw Error(response.error);

  const profile = response.profile as Status;
  return {
    status_text: profile.status_text,
    status_emoji: profile.status_emoji,
  };
}

This function returns a Status object.

Integrating the Slack logic

Now that we have created the necessary functions to modify and set the Slack status, we can go ahead and implement the remaining logic of our event handler in src/main.ts. To accomplish this we first import the functions from slack.ts and create an object to hold the previous state.

When a call is answered, before replacing the user’s status we store the old one in previousStatuses.

import { Status, getStatus, setStatus } from "./slack";

// map from slackUserId to status before AnswerEvent
const previousStatuses: Record<string, Status> = {};

// ...

async function handleAnswer(answerEvent: AnswerEvent): Promise<void> {
  // ...

  if (previousStatuses[slackUserInfo.slackMemberId]) {
    console.warn(`[answerEvent] Status of ${relevantNumber} was already set.`);
    return;
  }

  const previousStatus = await getStatus(slackUserInfo.slackMemberId);
  previousStatuses[slackUserInfo.slackMemberId] = previousStatus;

  const inCallStatus = {
    status_emoji: ":phone:",
    status_text: "Currently in a call",
  };
  await setStatus(slackUserInfo.slackMemberId, inCallStatus);

  console.log(
    `[answerEvent] setting status of ${relevantNumber} to ${inCallStatus}`
  );
}

We extended the handleAnswer function to check for a previous status. If there is no status we can continue.

The next step is to store the current status, so we can restore it on a hang-up when the user is available again.
After storing it, we modify the status, so that everyone can see the person is currently in a call.

To restore the users status on an HangUpEvent we also need to adapt the handleHangUp function.

async function handleHangUp(hangUpEvent: HangUpEvent): Promise<void> {
  // ...

  const previousStatus: Status | undefined =
    previousStatuses[slackUserInfo.slackMemberId];

  if (!previousStatus) return;

  console.log(
    `[hangupEvent] setting status of ${relevantNumber} back to ${previousStatus}`
  );

  await setStatus(slackUserInfo.slackMemberId, previousStatus);
  delete previousStatuses[slackUserInfo.slackMemberId];
}

This function checks whether the previous status of a Slack member has been empty or not.
If a previous status exists, it will restore it and deletes the entry in the previousStatuses record.

Run the server

That’s it. Our code is ready to execute. Make sure that you configured the correct mappings in the mappings.json.
Then run npm start to test our code.

You should now see a log like this:

Listening on port 8080

This is a good sign. The server is now ready for calls.

When you receive a call or call someone yourself, you should see the following log in the console:

[answerEvent] setting status of +491234xxxx to { status_text: 'Currently in a call', status_emoji: ':phone:' }

You can now go to slack and check if the status was set correctly.
When the call is hung up, there should be this log message in the console:

[hangupEvent] setting status of +491234xxxx back to { status_text: '', status_emoji: '' }

Now the status should be restored.

You can find the whole code for the tutorial in this GitHub repository.

Keine Kommentare


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert