Zoom Phone の不在着信＆留守電を Slack に通知する

## はじめに
以前の記事では、Zoom Team Chat の Incoming Webhook を中心に紹介してきました。

https://qiita.com/michitakasugi/items/edfad8d8fe033e79922e

https://qiita.com/michitakasugi/items/2130c2e12e67ff34c988

今回は同じ Webhook でも Outgoing Webhook つまり、Zoom から外部サービスへの Webhook 通知を扱ってみたいと思います。

## 下準備

利用するのは Webhook Only と呼ばれる機能で、以前こちらの記事でも紹介したことがあります。

https://qiita.com/yosuke-sawamura/items/a45453f1492a43211b96

そもそも Webhook とは何か、や基本的な仕様、実装準備などについては上記の記事を参照ください。
特に Challenge-response check (CRC) の対応は少し組み込みに時間がかかるかもしれませんが、こちらの記事を参照していただければ、割とすんなり実装できると思います。

この記事に沿って、サーバが指定ポートで listen した状態で、2.2にある追記部分くらいまで完了してください。（今回は録画データを扱わないので、録画が完了したイベント「recording.completed」を受けての処理周りは含めなくてOKです）

## Slack での Incoming Webhook の準備
これもあえてこの記事で説明するほどではありませんが、Manage Apps から Incoming Webhook を検索し、インストール。

![Screenshot 2023-03-03 at 18.36.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/cd3dcbd9-5eee-38fb-7e87-8bc4015bc815.png)

新規の Incoming Webhook を追加して、 Webhook URL を取得します。こちらの記事なんかはわかりやすいですね。

https://qiita.com/ik-fib/items/b4a502d173a22b3947a0

## Webhook Only アプリの追加
今回のアプリは、 Zoom Phone の不在着信や留守電を受け取ったら Slack に通知するという処理なので、Webhook Only アプリの画面の Feature セクション内、Event Subscription で追加する Events （Add Events から追加します）は、画像のように [Callee missed a phone call](https://marketplace.zoom.us/docs/api-reference/phone/events/#operation/phone.callee_missed) と [Voicemail is received](https://marketplace.zoom.us/docs/api-reference/phone/events/#operation/phone.voicemail_received) の2つを選択します。

![Screenshot 2023-03-03 at 18.44.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/45545187-2937-bbe9-e433-04ea7352ce22.png)


## Web サーバ側の実装について

ここからは Node.js 環境での実装になります。（事前に HTTPS 通信が可能な Web サーバの準備が必要です）

以下は準備です。

```javascript
require('dotenv').config();
const https = require('https');
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
const port = 3000;
```

また、bodyParser を明示します。（expressのバージョンによっては、body-parserはdeprecatedになっているようなので、expressだけで良いかもしれません）

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

早速POSTされた時の処理ですが、こちらは上述の記事にあるのでほぼ再掲となります。

```javascript

app.post('/', (req, res) => {
  console.log(req.body);
  console.log("POST");

  var response;

  console.log(req.headers);
  console.log(req.body);
  response = { status: 200 };
  res.status(response.status);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`))
```

ひとまずこれで、このエンドポイントに POST アクセスがあった際に HTTP 200 を返す形になるかと思います。とりあえずなので、どんなリクエストが来ても OK! OK! と返す変なヤツになってますね。

## Secret Token を基に識別
自分が設定した Webhook からの POST イベントのみを識別するため、Webhook Only アプリ作成時に生成された Secret Token を基に識別する仕組みを作ります。あらかじめアプリと同じディレクトリに `.env` ファイルを配置しておきます。

```
ZOOM_WEBHOOK_SECRET_TOKEN="{YOURSECRETTOKEN}"
```
このような内容を記載して保存しつつアプリ側から呼び出し、Signature を生成した上でWebhook のヘッダーに含まれる `x-zm-signature` を比較して識別します。さらに、Zoomからの Challenge-response check (CRC) の対応として、event が endpoint.url_validation の場合に、指定されたレスポンスを返却するよう処理を追加します。

```javascript
if (req.headers['x-zm-signature'] === signature) {
  console.log("SecretTokenVerification : true");
  if(req.body.event === 'endpoint.url_validation'){
      console.log('endpoint.url_validation');
      const hashForValidate = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET_TOKEN).update(req.body.payload.plainToken).digest('hex');
      response = {
        message: {
          "plainToken": req.body.payload.plainToken,
          "encryptedToken": hashForValidate
        },
        status: 200
      };
      console.log(response.message);
      res.status(response.status);
      res.json(response.message);
  }else{
    WriteToLog("SecretTokenVerification : false");
    response = { message: 'Zoom Webhook Demo Response', status: 200 };
    res.status(response.status);
    res.json(response);
  }
};
```

ここまでできたら、今度は event ごとの処理の追加だけですね！switch case を使って一気に追加しましょう。

## イベントごとの処理の追加
今回は、 [Callee missed a phone call](https://marketplace.zoom.us/docs/api-reference/phone/events/#operation/phone.callee_missed) と [Voicemail is received](https://marketplace.zoom.us/docs/api-reference/phone/events/#operation/phone.voicemail_received) の2つですので、これらの Webhook の Body をみながら、パースしてデータを取得していきます。

```javascript
  if (req.headers['x-zm-signature'] === signature) {
    console.log("SecretTokenVerification : true");
    let slackBody = "";
    switch (req.body.event) {
      case 'endpoint.url_validation':
        console.log('endpoint.url_validation');
        const hashForValidate = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET_TOKEN).update(req.body.payload.plainToken).digest('hex');
        response = {
          message: {
            "plainToken": req.body.payload.plainToken,
            "encryptedToken": hashForValidate
          },
          status: 200
        };
        console.log(response.message);
        res.status(response.status);
        res.json(response.message);
        break;
      case 'phone.callee_missed':
        console.log('Webhook Notification: '+'phone.callee_missed');
        response = {status:200};
        res.status(response.status);
        const callerPhoneNumber = req.body.payload.object.caller.phone_number;
        const callType = req.body.payload.object.callee.extension_type;
        const calleePhoneNumber = req.body.payload.object.callee.phone_number;
        const call_end_time = req.body.payload.object.call_end_time;
        slackBody = ":telephone_receiver: Missed call notification. \nCaller: "+callerPhoneNumber+"\nCallee: "+calleePhoneNumber+"\nType :"+callType+"\nTime: "+call_end_time; 
        console.log(slackBody);
        sendToSlack(slackBody);
        break;
      case 'phone.voicemail_received':
        console.log('Webhook Notification: '+'phone.voicemail_received');
        response = {status:200};
        res.status(response.status);
        const vcaller_number = req.body.payload.object.caller_number;
        const vcaller_name = req.body.payload.object.caller_name;
        const vdate_time = req.body.payload.object.date_time;
        slackBody = ":telephone_receiver: Voice mail notification. \nCaller: "+vcaller_number+"\nName: "+vcaller_name+"\nTime: "+vdate_time; 
        console.log(slackBody);
        sendToSlack(slackBody);
        break;
      default:
        break;
      }
    }else{
      console.log("SecretTokenVerification : false");
      response = { message: 'Zoom Webhook Demo Response', status: 200 };
      res.status(response.status);
      res.json(response);
    }
```

Body の詳細はリファレンスに記載があるので、そちらを参照していただきつつ、必要な情報を取り出してください。

## Slack に通知

すでに上のコードにも含まれていますが、 `sendToSlack(slackBody)` の部分を書いていきます。
```javascript

function sendToSlack(slackBody) {
  var data = JSON.stringify({"username":"Zoom Notification","text": slackBody,"icon_emoji":":zoom:"}); 
  //icon_emoji :zoom: は事前にご自身で追加してくださいね！
  var  options = {
    hostname: 'hooks.slack.com',
    port: 443,
    path: '/services/XXXXXXXXXXXXXX',
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(data)
    }
  };

  var req = https.request(options, (res) =>{
    if(res.statusCode===200){
        console.log("OK:"+res.statusCode);
    }else{
        console.log("Status Error:"+res.statusCode);
    }
  });

  req.on('error',(e)=>{
    console.error(e);
  });

  req.write(data);
  req.end();
}
```

以上でコードとしては終了です。（ `app.listen` が最後にきます）

ここまでできたら、Zoom Phoneで実際に不在着信と留守電を試してみましょう。このような形で、着信履歴や留守電の通知がきます。

![Screenshot 2023-03-03 at 17.07.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/ae269d26-7216-1599-be89-b28a217707f5.png)



## サンプル・ドキュメント類について
### 概要 (Using Webhook)：

https://marketplace.zoom.us/docs/api-reference/webhook-reference/

### 各種イベントの詳細 (オフィシャル・ドキュメント)：

https://marketplace.zoom.us/docs/api-reference/phone/events/#overview