はじめての Zoom Webhook

<h2>■ 前置き</h2>
ご利用のZoomアカウント内で発生した様々なイベントをサードパーティ製のWebアプリケーションへ通知するWebhook機能をご利用いただけます。。<br>
Webhookを利用いただくことで、イベントが発生した際に準備したWebサーバ側で通知を受け別の動線へと繋げていただけます。Webhook通知をトリガーとしての様々な用途で活用するための実装を実現いただけます。
<br>

![スクリーンショット 2022-11-09 13.12.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/f8432163-6f54-9c49-a4ab-6294a159f675.png)

<h5>WebHook の特徴</h5>

- https通信を許容します
- 通知後200/204レスポンスが確認されない場合は到達不可としてZoomからリトライが発生
- Zoomからのリトライは3回まで実施
- ヘッダーに含まれるx-zm-signatureを元に識別することが可能
- 受取側の実在確認のためchallenge-response check (CRC) への対応が必要

<h2>■ 実装前の準備について</h2>

1. https://marketplace.zoom.us/ へサインインします。
2. 右上プルダウンより 「Build App」 をクリック
 
![スクリーンショット 2022-11-09 14.08.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/b2356ed1-a047-1ddb-7eb2-6952efe4a1aa.png)

３. 「Webhook Only」 > 「Create」 をクリック 

![スクリーンショット 2022-11-09 14.09.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/3398f85a-43f3-8cdc-9d00-1ba008ba04a8.png)

4. 「App Name」に自身で識別可能な名前を入力します。ここでは、「Sample Webhook」とし 「Create」 をクリックして遷移します。

![スクリーンショット 2022-11-09 14.11.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/d9fa2b71-db5d-2408-d334-d1ca52afd800.png)


5. 必須項目となる「Company Name」、「Name」、「Email address」を入力します。

![スクリーンショット 2022-11-09 14.12.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/e9216d2f-ebac-048f-fe80-a966c3dd2e5c.png)

6. 「Secret Token」 は、実装で使用するため控えておきます。

![スクリーンショット 2022-11-09 14.13.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/6e6a5863-afff-9918-f49b-2c83a83ad802.png)

「Event Subscription」のトグルを右に有効にし 「Add Event Sunscription」をクリックします。

![スクリーンショット 2022-11-09 14.15.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/0f039cea-8ac0-78e1-6f7c-099aed9b7772.png)

必須項目の「Event notification endpoint URL」（Webhookを受ける自身のWebサーバ）を入力します。

![スクリーンショット 2022-11-09 14.16.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/9ebf460f-9c70-d02f-1285-d116c866a5ea.png)

「Add Events」をクリックし必要な通知を選択します。ここでは「Recording」の「All Recording have completed」を選択して「Done」で閉じ「Continue」で遷移します。

![スクリーンショット 2022-11-09 14.16.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/a2d9d58d-bcfe-243d-b206-618b765e1375.png)


<h2>■ Webサーバ側の実装について</h2>

以下Node.jsの環境で解説していきます。
1-1. まず、https通信が可能なWebサーバを準備します。

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

//HTTPS ON EXPRESS FRAMEWORK
const options = {
  key: fs.readFileSync('＜自身のKEY.pem＞'),
  cert: fs.readFileSync('＜自身のCERT.pem＞'),
  ca: fs.readFileSync('＜自身のCHAIN.pem＞')
};
const server = https.createServer(options,app);
```
1-2. 次にPOSTされてくる内容を確認したいのでbodyParserを明示します。

```javascript
app.use(bodyParser.json());
```
1-3. では、早速POSTされた時の処理をレスポンスコードを返答するよう指定していきます。
```javascript
//FOR POST REQUEST
app.post('/webhook', (req, res) => {
  console.log("POST");
  var response;

  console.log(req.headers);
  console.log(req.body);
  response = { message: 'Zoom Webhook Demo Response', status: 200 };
  res.status(response.status);
  res.json(response);

});
```
1-4. 最後にlistenすることで起動させます。
```javascript
//RUN HTTPS SERVER
server.listen(port, () => console.log(`Webhook Demo listening on port ${port}!`));
```
以上でPOSTを受ける基本的な構成となりますので、上記を起動することですぐにWebhookの通知を受けれるようになります。

ただし、上記だけですと設定したWebhook以外のPOSTイベントも全て処理対象としてしまうため、自身が設定したWebhookを
「Secret Token」を元に識別する仕組みを入れていきます。

2-1. "1-3."での処理内でヘッダー内の「x-zm-request-timestamp」と内容を「Secret Token」でsignatureを生成してきます。
```javascript
  //VERIFY WEBHOOK EVENT SIGNATURE
  const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
  const hashForVerify = crypto.createHmac('sha256','＜自身のWEBHOOK_SECRET_TOKEN＞').update(message).digest('hex');
  const signature = `v0=${hashForVerify}`;
```
2.2. 上記で生成したsigantureとヘッダーに含まれている「x-zm-signature」を比較して同じであれば、自身が設定したWebhookの通知として識別できます。
```javascript
  if (req.headers['x-zm-signature'] === signature) {
    WriteToLog("SecretTokenVerification : true");
  }
```

ここまで、きたら実際のイベントの内容を元に識別して処理に繋げれるようになります。
下記では録画が完了したイベント「recording.completed」を受け取ったらサーバ側でダウンロードしてファイルとして保存する例となります。
追記）Webhookサーバの実在確認が必要となりますので「endpoint.url_validation」を受け取った際に正常応答できるように受信時の実装も忘れずに実装する必要があります。
```javascript
app.post('/', (req, res) => {
  console.log("POST");

  var response;

  //VERIFY WEBHOOK EVENT SIGNATURE
  const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
  const hashForVerify = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET_TOKEN).update(message).digest('hex');
  const signature = `v0=${hashForVerify}`;

  if (req.headers['x-zm-signature'] === signature) {
    WriteToLog("SecretTokenVerification : true");
   if(req.body.event === 'endpoint.url_validation') {
      WriteToLog('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{
      switch (req.body.event){
        case 'recording.completed':
          WriteToLog('recording.completed');
          let download_url = req.body.payload.object.recording_files[0].download_url;
          let token = req.body.download_token;
          let url = download_url + "?access_token="+token;
          let filename = req.body.payload.object.recording_files[0].recording_start;
          let dir = path.join(__dirname, './download/') + filename + ".mp4";
          Download(url, dir);
          break;
        default:
          WriteToLog("Webhook notification received but no match");
      }
      response = { message: 'Zoom Webhook Demo Response', status: 200 };
      res.status(response.status);
      res.json(response);
    }
  }else{
    WriteToLog("SecretTokenVerification : false");
    response = { message: 'Zoom Webhook Demo Response', status: 200 };
    res.status(response.status);
    res.json(response);
  }
});

//DOWNLOAD CLOUD RECORDED FILE
function Download(url, p){
  console.log("Download started");
  const download = (url, p, callback) => {
    request.head(url, (err, res, body) => {
      request(url)
        .pipe(fs.createWriteStream(p))
        .on('close', callback)
    })
  }
  download(url, p, () => {
    console.log("Download completed: " + p);
  })
};
```

<h2>■ サンプル・ドキュメント類について</h2>
<b>サンプル：</b>

https://github.com/ysawamura-zoom/Workshop/tree/main/Webhook

<b>概要 (Using Webhook)：</b>
　https://marketplace.zoom.us/docs/api-reference/webhook-reference/

<b>各種イベントの詳細 (オフィシャル・ドキュメント)：</b>
　https://marketplace.zoom.us/docs/api-reference/zoom-api/events/#overview
　https://marketplace.zoom.us/docs/api-reference/phone/events/#overview
　https://marketplace.zoom.us/docs/api-reference/app/events/#overview
　https://marketplace.zoom.us/docs/api-reference/chat/events/#overview

<h2>■ 補足</h2>
「Webhook Only」で登録いただいた場合はMarketplaceの管理画面にてZoomから送信された通知の状況を確認いただけます。<br>
Marketplaceトップ画面「Manage > Call Logs > Webhook Logs」より確認いただけます。

![スクリーンショット 2022-11-09 14.18.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/4d7a0571-bb36-c20b-5460-9bdc517f2e42.png)

![スクリーンショット 2022-11-09 14.19.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2123842/c86dafb9-0e88-ebbb-d80c-6763b76ada39.png)