架電ブロックリストを Zoom Phone APIに流し込む話

# はじめに
Zoom Phone の日本での本格展開が進んでいます。クラウド PBX の機能も備えている Zoom Phone は非常に多機能なことから、現在お客様からもかなりの引き合いをいただいている状況で、先日はテレビ番組でもご紹介いただきました！

<br />
<iframe width="480" height="360" src="https://www.youtube.com/embed/Y1pVAdNL7hE" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" loading="lazy" allowfullscreen></iframe>
<br />
<br />

ただ、既存の他社製 PBX 等をご利用のお客様より、何度か「『架電禁止のブラックリスト』を一括で取り込むことができないか」というお問合せをいただいており、2022年12月現時点では一括登録の口は Web ポータル画面にご用意がないことから、ひとまず API でできるか試してみよう！と考え記事にしてみました。

## そもそもAPIって？
私が語るのもおこがましいですが、API は Application Programming Interface の略で、アプリを作る際に外部サービスと連携するために用意されている、外部サービス側の情報の受け渡し口、といったものになります。
例えば...
1. レストランでお客さんが「ピザを1枚ください」とウェイターに告げます
1. ウェイターはその注文をバックヤードにいるシェフに伝えます
1. シェフはピザ生地に必要な具材を載せて焼き、ピザが出来上がります
1. ウェイターは出来上がったピザをお客さんのテーブルに運びます

これを図式化するとこんな感じです。
![api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/c77237e8-20ec-fb48-b23a-602b7416e68f.png)


ここでいう、「注文」が API リクエスト、出来上がってテーブルに運ばれたピザがレスポンスに相当します。ピザの具材がリクエスト対象のデータだと思ってください。つまり、
1. レストラン（外部サービス）でお客さん（クライアントアプリ）が「ピザを1枚ください」とウェイター（API）に告げます（APIリクエスト）
1. ウェイター（API）はその注文（APIリクエスト）をバックヤードにいるシェフ（バックエンドサーバ）に伝えます
1. シェフはピザ生地に必要な具材（リクエストされたデータ）を載せて焼き、ピザ（レスポンス）が出来上がります
1. ウェイター（API）は出来上がったピザ（レスポンス）をお客さんのテーブル（クライアントアプリ）に運びます

こんな感じで、外部サービスにあるデータを、あらかじめ決められた手続きに沿って要求することで入手することができます。もちろん、データを入手するだけでなく、場合によっては外部サービスにあるデータを変更したり、追加したり、削除したりすることもできるものもあります。

## Zoom API について
Zoom には、そうした API によって操作できる仕組みが沢山用意されています。今後様々なAPIを取り上げていきますが、今回はその中でも Zoom Phone API を操作して、あらかじめ用意された「架電禁止のブラックリスト」を取り込んで一括で追加してみることにしましょう。

# 事前準備
「架電禁止のブラックリスト」の一括登録をする前に、まずは事前準備を進めていきましょう。必要な準備項目は以下の通り。
- [x] 管理者からデータ操作の許可をもらう
- [x] 操作権限（認証トークン）の取得
- [x] 架電禁止のブラックリスト CSV の準備
- [x] API で実際にブロックリストを登録
- [x] 対象データの件数の確認

以下で順番に進めていきましょう。

## 管理者からデータ操作の許可をもらう＆操作権限の取得
会社の Zoom Phone のデータ操作を行うので、あらかじめ API を利用してこういう操作をします、という許可をとっておきましょう。管理者は Web ポータル画面にてユーザーへの操作許可を与える設定をしておく必要があります。
今回は管理者権限でアカウント内のデータを操作できる「サーバ間 OAuth アプリ」という権限で進めていきます。
このやり方は、 @yosuke-sawamura による以下の記事で紹介されていますので、まずはそちらを参照ください。

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

本ケースでは、Zoom Phone の Blocked List 操作になるので、以下の画像にある
- `phone:read:admin`
- `phone:write:admin`

の2つの Scope を有効にしてください。
![Screen Shot 2022-12-05 at 20.17.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/af06e8c1-6c5d-25e4-e6b5-bb6d7f249dfe.png)
Activateできたら、**App credentials** にある
* Account ID
* Client ID
* Client secret

の3つをメモしておいてください。

## 架電禁止のブロックリストCSVの準備
次に、架電禁止のブロックリストですが、まずは一度 Web ポータル画面でどうやってブロックリストを追加するかを確認してみましょう。設定のありかは、*Zoom Web ポータル画面＞管理者＞電話システム管理＞会社情報＞アカウント設定＞ブロックリスト* です。

![2022-12-05 19.50.36.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/cf3652a0-bcdf-bf5f-d6da-7049e6995336.gif)

内容を見てみると、
- ブロック対象の番号
- マッチタイプ（プリフィクスのマッチ、または電話番号のマッチ）
- タイプ（発信または着信）
- コメント
- ステータス（有効または無効）

といった感じになっているようです。同じことを API で実現するとしたらどうでしょうか。API リファレンスページを見てみましょう。

https://marketplace.zoom.us/docs/api-reference/phone/methods/#operation/addAnumberToBlockedList

これをみると、必要なデータは

- block_type (inbound OR outbound)
- comment
- country
- match_type (phoneNumber OR prefix)
- phone_number
- status (active OR inactive)

となっています。これをみる限り、内容としては Web 管理画面で入力する内容とほぼ同じようです。
country については、 Abbreviation List に記載がありました。

https://marketplace.zoom.us/docs/api-reference/other-references/abbreviation-lists/#countries

日本の場合は `JP` で良いみたいです。
ということで、用意するのはこれら6つのデータをまとめた表、ということになりそうです。Web ポータル画面から削除することも可能なので、一度テストデータを作って試してみることにしましょう。
今回はこんなデータを用意してみました。
![Screen Shot 2022-12-05 at 20.04.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/0f79a114-1c7c-112f-cd63-ab8a194971af.png)
この後の処理の都合上、今回は一度このデータを Google Drive にアップしておきます。


:::note warn
電話番号の頭につく `0` はあらかじめ削除しておいてください。 Country = JP とすることで、登録後は自動的に `+81` で始まる番号として登録されます。
:::


## API で実際にブロックリストを登録
さて、上で仕込みが完了したので、ここからは実際に番号をブロックリストに登録していきます。
今回は簡単にサーバレスで、かつサクッと実現したいので Google が用意している Google Apps Script を利用します。

https://script.google.com/home/

ここで `新しいプロジェクト` をクリックすると、コード入力画面が表示されます。誌面の都合上、ここでは Google Apps Script の詳しい説明は省略させていただきます。よく使われるサービスなので、多分検索すると説明が沢山出てくると思います。

で、このコード入力画面に最初から入力されているものを削除しつつ、以下を入力します。
コード内の A と B の部分、Account ID、Client ID、Client secret や対象となるスプレッドシートの URL とシートタブ名の部分は、書き換えてください。



```javascript
//A. 先ほどメモした Account ID、Client ID、Client secret をここに入力します
const grant_type = "account_credentials";
const account_id = "INPUT_ACCOUNT_ID_HERE";
const clientId = "INPUT_CLIENT_ID_HERE";
const clientSecret = "INPUT_CLIENT_SECRET_HERE"; 

//API エンドポイント
const oauthEndpoint = "https://zoom.us/oauth/token" //S2SOauth Token取得
const blacklistEndpoint = "https://api.zoom.us/v2/phone/blocked_list" //Phone Blocked List

// Server-to-Server OAuth リクエストトークン取得の処理
function requestToken() {
  let url = oauthEndpoint + "?grant_type=" + grant_type + "&account_id=" + account_id;
  let options = {
    "method" : "POST",
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : "Basic " + Utilities.base64Encode(clientId + ":" + clientSecret),
    },
    "muteHttpExceptions" : false 
    //エラーが発生した場合、レスポンスにエラー全文を出力する
  }
  let response = UrlFetchApp.fetch(url, options);
  let responseCode = response.getResponseCode();
  let responseBody = response.getContentText();
    //Zoom側から正常なレスポンス以外が返ってきた場合に、エラーコードとエラー内容を取得
  if ([200, 201].indexOf(responseCode)<0){
    throw new Error(responseCode + " : " + responseBody);
  }
  let json = JSON.parse(responseBody);
  Logger.log("token: "+ json["access_token"]);
  Logger.log("expires: " +json["expires_in"]);
    //取得したリクエストトークンと有効期限をログに出力する
  const access_token = json["access_token"];
  bulkAddBlackList(access_token);
}

// 事前準備したデータから一括追加
function bulkAddBlackList(access_token){
  //B. 対象となるスプレッドシートのURLとシートタブ名をここに入力します
  var ss = SpreadsheetApp.openByUrl('INPUT_SPREADSHEET_URL_HERE');
  var sheet = ss.getSheetByName("INPUT_SHEETNAME_HERE");
  var lastRaw = sheet.getLastRow();
  
  for(i=2;i<=lastRaw;i++){
    var targetLine = sheet.getRange(i,1,1,6).getValues();
    //A列からF列まで1行単位のデータを取得し記憶させる
    var phone_number = String(targetLine[0][4]);
    // Body
      let body = {
        "block_type": targetLine[0][0],
        "comment": targetLine[0][1],
        "country": targetLine[0][2],
        "match_type": targetLine[0][3],
        "phone_number": phone_number,
        "status": targetLine[0][5]
      }
    // リクエストヘッダー
    let bulkAddBlackListOption = {
      "method" : "POST",
      "headers" : {
        "Content-Type" : "application/json",
        "Authorization" : "Bearer " + access_token,
      },
      "payload" : JSON.stringify(body),
      muteHttpExceptions : false
    }
    var bulkAddBlackListResponse = UrlFetchApp.fetch(blacklistEndpoint,bulkAddBlackListOption);
    var bulkAddBlackListResponseCode = bulkAddBlackListResponse.getResponseCode();
    var bulkAddBlackListResponseText = bulkAddBlackListResponse.getContentText();
    console.log("Code: " + bulkAddBlackListResponseCode + " Message: " + bulkAddBlackListResponseText);
    let resultCell = sheet.getRange(i,7);
    if(bulkAddBlackListResponseCode==201){
      resultCell.setValue("OK")
    }else if(bulkAddBlackListResponseCode==429){
      resultCell.setValue("APIの処理上限にあたったため、以降の処理を中断しました")
      return false
      //HTTP429 Too many requests が発生した場合に以降の処理を中断する
    }else{
      resultCell.setValue(bulkAddBlackListResponse+" : "+bulkAddBlackListResponseText)
      //それ以外のエラーが発生した場合、エラー内容をcsvに追記し、次の行へ移動する
    }
  }
}
```

こんな感じで処理を書いてみました。早速登録を試してみましょう。

![2022-12-05 20.42.29.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/795363ba-5cf1-b305-796b-ce46d1f2932e.gif)

こんな感じで、5件とも登録することができました。F列にOKと記載されたので、問題なく登録されているようです。（Web ポータル画面の *Zoom Web ポータル画面＞管理者＞電話システム管理＞会社情報＞アカウント設定＞ブロックリスト* からも確認可能です）

:::note warn
ブロックリストに登録できる上限は 7,000 件までとなっております。
:::

# （おまけ）Google Form で投稿する方法

一括登録が終わった後も、随時登録することがあるかもしれません。その際は、Google Form を使って同じようなことができます。

まずは [Google Form](https://forms.google.com/) へアクセスし、新しい Google Form を作成します。内容はどんな形でも構いませんが、例えばこんな形にします。
![Screen Shot 2022-12-07 at 12.43.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/8acec2cb-8aad-bfce-1e28-c26b6e502205.png)

:::note warn
電話番号は、ゼロ抜きハイフンなしのため、「三点リーダ＞回答の検証＞正規表現（一致する）」を選択し、 `[1-9]{1}[0-9]{8,10}` という内容を設定しています。
:::


フォームが準備できたら、画像にある通りページ右上の三点リーダより、スクリプトエディタを開きます。

先ほどと同じように、あらかじめ書かれているものは削除した上で、以下のようなスクリプトを追記します。
先ほどと同じように、AccountID、Client ID、Client Secret を入力してください。
```javascript
//A. 先ほどメモした Account ID、Client ID、Client secret をここに入力します
const grant_type = "account_credentials";
const account_id = "INPUT_ACCOUNT_ID_HERE";
const clientId = "INPUT_CLIENT_ID_HERE";
const clientSecret = "INPUT_CLIENT_SECRET_HERE"; 

//API エンドポイント
const oauthEndpoint = "https://zoom.us/oauth/token" //S2SOauth Token取得
const blacklistEndpoint = "https://api.zoom.us/v2/phone/blocked_list" //Phone Blocked List

// Server-to-Server OAuth リクエストトークン取得の処理
function onFormSubmit(e) { // フォーム投稿時に稼働
  var responseData = e.response.getItemResponses(); //投稿データを取得する
  for (var i = 0; i < responseData.length; i++){    
    var data = responseData[i];
    var title = data.getItem().getTitle();
    var res = data.getResponse();
    switch (title) {
      case "登録担当者":
        var name = res;
        break;
      case "電話番号":
        var phone = res;
        break;
      case "コメント":
        var comment = res;
      default:
        break;
    }
  }
  var note = name + " : " + comment; //投稿者のメールアドレスとコメントをバインドしてブロックリストのコメント欄に記録
  let url = oauthEndpoint + "?grant_type=" + grant_type + "&account_id=" + account_id;
  let options = {
    "method" : "POST",
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : "Basic " + Utilities.base64Encode(clientId + ":" + clientSecret),
    },
    "muteHttpExceptions" : false 
    //エラーが発生した場合、レスポンスにエラー全文を出力する
  }
  let response = UrlFetchApp.fetch(url, options);
  let responseCode = response.getResponseCode();
  let responseBody = response.getContentText();
    //Zoom側から正常なレスポンス以外が返ってきた場合に、エラーコードとエラー内容を取得
  if ([200, 201].indexOf(responseCode)<0){
    throw new Error(responseCode + " : " + responseBody);
  }
  let json = JSON.parse(responseBody);
  Logger.log("token: "+ json["access_token"]);
  Logger.log("expires: " +json["expires_in"]);
    //取得したリクエストトークンと有効期限をログに出力する
  const access_token = json["access_token"];
  addBlackList(access_token,note,phone);
}

// 新規追加
function addBlackList(access_token,note,phone){
  // Body
    let body = {
      "block_type": “outbound”,   //固定値として設定
      "comment": note,            //Formから取得
      "country": “JP”,            //固定値として設定
      "match_type": “phoneNumber”,//固定値として設定
      "phone_number": phone,      //Formから取得
      "status": “active”          //固定値として設定
    }
    console.log(body);
  // リクエストヘッダー
  let addBlackListOption = {
    "method" : "POST", // POST, GET, PUT, PATCH, DELETE
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : "Bearer " + access_token,
    },
    "payload" : JSON.stringify(body),
    muteHttpExceptions : false //エラーが発生した場合、レスポンスにエラー全文を出力する
  }
  var addBlackListResponse = UrlFetchApp.fetch(blacklistEndpoint,addBlackListOption)
  var addBlackListResponseCode = addBlackListResponse.getResponseCode()
  var addBlackListResponseText = addBlackListResponse.getContentText()
  console.log("Code: " + addBlackListResponseCode + " Message: " + addBlackListResponseText)
}
```
ここまで出来たら、Google Form 投稿時にこの処理が走るようにしましょう。スクリプト画面の左側、トリガーを選択し `トリガーを追加` ボタンをクリックします。
![Screen Shot 2022-12-07 at 12.59.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/6ccf88f0-3c27-3e3e-9797-139e4148c503.png)
![Screen Shot 2022-12-07 at 12.51.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/61d185ca-25fd-7057-8791-a72c3d3c81b5.png)
この画像にあるように、フォーム送信時に稼働するように設定します。その後警告画面が出てくると思いますので、画像の手順で実行許可を与えます。
![Screen Shot 2022-12-07 at 12.27.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/87b22574-eeca-aada-ae95-22ef8fa63e7e.png)
![Screen Shot 2022-12-07 at 12.28.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/3b8587b7-4e9f-e080-9da0-0642d324f855.png)
![Screen Shot 2022-12-07 at 12.28.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/3c6cc630-9ed8-a3a5-21da-9281072ae3c9.png)


これで準備完了です。
![Screen Shot 2022-12-07 at 12.56.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/077e30fc-4e19-4ea1-1735-c8b02c1964e1.png)


実際にこのボタンでフォームにアクセスし、テストで投稿してみましょう。こんな感じで追記されていることが確認できれば成功です。
![Screen Shot 2022-12-07 at 12.57.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/b770df80-0c99-e23f-33af-5a716f126810.png)

# まとめ
今回は Zoom API の中でも Phone の API を活用して「架電禁止のブラックリスト」を一括登録する、という処理を試みてみました。普段お使いの Zoom サービスをより便利に活用できる API を使うと、Web 管理画面だけでは難しい操作もできる場合があります。

また Zoom API には他にも Meeting で使える API や、Webinar、Team Chat、などなど様々あります。

https://zine.qiita.com/interview/202205-zoom/

今後もいろいろとご紹介していけたらと思っています。どうぞよろしくお願いいたします！