Google Apps ScriptでWebex APIを操作する【GAS】

コロナ渦を経て、日本の企業でもはやWeb会議システムを導入していない企業はどうなのよ?と言われるほど当たり前の存在になりました。圧倒的にZoomがシェアを握っていて、TeamsやGoogle Meetがそれを追いかける状況で、結構しっかり作られていてREST APIも豊富なのに知名度低いのが「Cisco Webex」。

無償で利用できるのに、Ciscoがあまり推していないのかシェアはいまいちですが、自分は仕事で使っています。ということで今回はこのWebex APIをGoogle Apps Scriptから使ってみました。

今回利用するスプレッドシート等

上記のリファレンスがサンプルコードやレスポンス、デモが整っているのですが、もう一つリファレンスがあり、後者のほうが実は全てを網羅してたりします(IP電話/クラウドPBXのWebex Callingなどの記載もきっちり乗っています)。今回はこのAPIのOAuth2.0認証およびWebhookを設置するコードを作ってみました。

Webhookの受け口もGASで記述して取得させています。

※自分の場合、Webex Callingの着電をWebhookで取得したかったために今回のコードを作成しています。PCやスマフォ、実機から電話の出来るためコールセンターに向いています。

事前準備

今回のプログラムはOAuth2認証、Webhook設置、Wehookの受け口の3つをGASで作ります。デバッグ含めてポイントがいくつかあるので、以下の作業をしておきましょう。

GAS側の作業

プロジェクトを移動する

まずは、GCP側を開いて対象のプロジェクトを開く。Google Apps Scriptとプロジェクトを紐付けにする必要があります。doPostがWebhookの受け口になるため、doPostの受け取った内容をデバッグするため、GCPのログエクスプローラからデバッグ内容を見るために必要です。

ログエクスプローラの使い方等は以下のエントリーを参照してください。

Google Apps Scriptでデバッグするイロハ集【GAS】

連結する手順は以下の通り

  1. Google Cloud Consoleを開く
  2. 左上にある▼をクリックする
  3. ダイアログが出てくるので、新規プロジェクトを作るか?既存のプロジェクトを選択する。この時、Google Workspaceであれば選択元は「自分のドメイン」を選択する必要があります。
  4. プロジェクト情報パネルから「プロジェクト番号」をコピーする
  5. 対象のGoogle Apps Scriptのスクリプトエディタを開く
  6. サイドバーからプロジェクト設定を開く
  7. プロジェクトを変更ボタンをクリック
  8. GCPのプロジェクト番号に、4.の番号を入れてプロジェクトを設定をクリック

図:プロジェクト番号をコピーしておきます

図:プロジェクト変更画面

Google Cloud Consoleを弄ってみる

デプロイを行う

GAS側で処理するためのコードを記述したら、doPostの受け口を有効化するためにデプロイをします。

  1. スクリプトエディタを開く
  2. 右上のデプロイをクリック
  3. 新しいデプロイをクリック
  4. 種類の選択ではウェブアプリを選択し、次のユーザとして実行は自分にしておきます。
  5. アクセスできるユーザは、全公開する必要があるので「全員」としておきます(Webhookなので認証有りだとWebex側からアクセス出来ない
  6. 末尾がexecで終わるURLが発行される。これを次項のGASのWebhookurlとして指定します。
  7. 次回以降コードを編集して再デプロイ時はデプロイを管理から同じURLにて、新しいバージョンを指定して発行することが出来ます。

コールバックURLを取得する

コールバックURLとは、認証を完了しAccess Tokenを取得したら戻るべきURLを指定するものです。これは、スクリプトIDをもとに作られているので、スクリプトIDを取得して組み立てます。

  1. スクリプトエディタのサイドバーより「プロジェクトの設定」を開く
  2. 情報の中にある「スクリプトID」を控えておく。
  3. https://script.google.com/macros/d/スクリプトID/usercallback として組み立てる。これがコールバックURLとなる。

図:スクリプトIDはファイル毎に異なるのです。

ライブラリの追加

以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。

  1. スクリプトエディタを開きます。
  2. サイドバーよりより「ライブラリ」の+ボタンをクリック
  3. ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
  4. 今回はバージョンは43を選択してみます。
  5. 保存ボタンを押して完了

これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。

図:ライブラリを追加した様子

Webex側での作業

OAuth2.0認証に必要なクライアントIDやシークレットを取得する為に、Webex for Developerにログインしてアプリを作成します。

  1. Webex for DeveloperのCreate New Appにログインする
  2. Create an Integrationをクリック
  3. Integration Nameがアプリの名前になります。
  4. アイコンは512x512のサイズのpng画像をアップする必要があります。
  5. App Hub Descriptionがアプリの説明文
  6. Redirect URLは、前述のコールバックURLを入れます。
  7. ScopesはAPIを利用して何をするかでオンにしていきます。今回は適当に「spark-compliance:webhooks_read spark:kms spark-compliance:webhooks_write meeting:schedules_read」をオンにします。
  8. Add Integrationをクリックすると、Client IDおよびClient Secretが手に入ります。

図:IDとSecretを後で利用します。

ソースコード

今回のコードはMeetingをスケジュールしたらWebhookで飛ばすのを設置します。そのWebhookはdoPostで受け取る必要があります。また、OAuth認証ではScopeが必要になります。あとはScopeに応じてWebexの様々なAPIを叩くことが出来るようになります。

OAuth2認証コード

//認証用の各種変数
var appid = 'ここにクライアントIDを入れる';
var appsecret='ここにクライアントシークレットを入れる';
var authurl = "https://webexapis.com/v1/authorize?"
var tokenurl = "https://webexapis.com/v1/access_token"

//スコープを半角スペース区切りで入れる
var scope = "spark-compliance:webhooks_read spark:kms spark-compliance:webhooks_write meeting:schedules_read"

function startoauth(){
  //UIを取得する
  var ui = SpreadsheetApp.getUi();
  
  //認証済みかチェックする
  var service = checkOAuth();
  if (!service.hasAccess()) {
    //認証画面を出力
    var output = HtmlService.createHtmlOutputFromFile('template').setHeight(450).setWidth(500).setSandboxMode(HtmlService.SandboxMode.IFRAME);
    ui.showModalDialog(output, 'OAuth2.0認証');
  } else {
    //認証済みなので終了する
    ui.alert("すでに認証済みです。");
  }
}

//アクセストークンURLを含んだHTMLを返す関数
function authpage(){
  var service = checkOAuth();
  var authorizationUrl = service.getAuthorizationUrl();
  var html = "<center><b><a href='" + authorizationUrl + "' target='_blank' onclick='closeMe();'>アクセス承認</a></b></center>"
  return html;
}

//認証チェック
function checkOAuth() {
  return OAuth2.createService("webex")
    .setAuthorizationBaseUrl(authurl)
    .setTokenUrl(tokenurl)
    .setClientId (appid)
    .setClientSecret(appsecret)
    .setCallbackFunction ('authCallback')
    .setPropertyStore (PropertiesService.getUserProperties())
    .setScope(scope);
}
  • クライアントIDとシークレットを記述しておきます。
  • Scopeは半角スペースで区切って複数指定することが可能です。
  • ライブラリで認証の一連の流れを作っておきます。
  • 認証が成功するとAccess Tokenが「ユーザプロパティ」に格納されます。

図:認証を実行中

Webhook設置

Webhookを設置した場合、OAuth認証時のアカウントに対してのイベントだけをWebhookしますので、他のアカウントや電話番号に対してのイベントをhookしたい場合はアカウントの数だけ、認証をしてWebhookを飛ばすようにする必要があります。

代表電話をWebex Callingのアカウントを割り振った場合には、そのアカウントでOAuth認証を実行してWebhookを設置する事で、代表電話に掛かってきたイベントを取得する事が出来るようになります。

//webhookurl
let targetUrl = "ここに公開したウェブアプリケーションURLを記入する";

//webhook making
function createWebhook() {
  //ui
  let ui = SpreadsheetApp.getUi();

  //プロパティ
  let prop = PropertiesService.getScriptProperties();
  let webhookid = prop.getProperty("webhookid");

  //トークン確認
  var service = checkOAuth();

  //認証チェック
  if(service.hasAccess()) { 
    //webhookidがある場合はまずdelete
    if(webhookid == "" || webhookid == undefined){
      //スルーする
    }else{
      //webhookidを削除する
      let ret = deleteWebhook();

      switch(ret){
        case 0:
          //削除完了してるのでスルー
          break;
        case 1:
          //削除時エラー
          ui.alert("削除リクエスト時にエラー");
          return;
        case 2:
          ui.alert("認証が実行されていませんよ。");
          break;
      }
    }

    //リクエストヘッダ
    let header = {
      "Authorization": 'Bearer ' + service.getAccessToken(),
      "Content-Type" : "application/json"
    }

    //リクエストボディ
    let payload = {
      name : "m_webhook",
      targetUrl : targetUrl,
      resource : "meetings",
      event : "created"
    }

    //リクエストオプション
    let options = {
      method: "POST",
      headers: header,
      payload : JSON.stringify(payload),
      muteHttpExceptions: true
    }

    //エンドポイントURLを構築
    let endpoint = "https://webexapis.com/v1/webhooks";

    //レスポンスデータ
    var response = UrlFetchApp.fetch(endpoint, options);

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    //idを追加する
    prop.setProperty("webhookid",result.id)

    //レスポンスコードを取得する
    let status = response.getResponseCode();

    //レスんポンスコードで判定
    if(status == 200){
      //削除完了
      ui.alert("Webhookを設置完了しました。");
      return 0;
    }else{
      ui.alert("webhook設置に失敗しました。");
    }

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}

//webhookを削除する
function deleteWebhook(){
  //トークン確認
  var service = checkOAuth();

  //プロパティ
  let prop = PropertiesService.getScriptProperties();
  let webhookid = prop.getProperty("webhookid");
  
  if(service.hasAccess()) { 
    //リクエストヘッダ
    let header = {
      "Authorization": 'Bearer ' + service.getAccessToken(),
    }

    //リクエストオプション
    let options = {
      method: "DELETE",
      headers: header,
      muteHttpExceptions: true
    }

    //エンドポイントURLを構築
    let endpoint = "https://webexapis.com/v1/webhooks/" + webhookid;

    //レスポンスデータ
    var response = UrlFetchApp.fetch(endpoint, options);

    //リクエスト結果を取得する
    const result = response.getContentText();

    //レスポンスコードを取得する
    let status = response.getResponseCode();

    //レスんポンスコードで判定
    if(status == 200 || status == 201 || status == 204){
      //削除完了
      prop.setProperty("webhookid","");
      return 0;
    }else{
      return 1;
    }
  }else{
    //認証されていない場合
    return 2;
  }
}
  • targetUrlには前述の準備で取得しておいたデプロイした末尾がexecのURLを入力します。
  • Webhookは作ったら有効になりますが、作りっぱでは次に作ったときにも前のものが残り続けてしまいます。ので、deleteするコードも必要になります。
  • webhookを作ったらそのIDをプロパティに格納しておき、削除時にもそれを使って削除を行います。
  • payloadの中身が最低限必要なリクエストボディで、resourceがmeetingsならWebex Meeting、telephony_callsがWebex Callingのイベント指定。これらのイベントで何かあるとWebhookが飛んでいく仕組みです。(telephony_callsはscopeでspark:calls_readが別途必要
  • eventはcreatedが作成時や着信時、updateが更新時に発火するという指定になります。
  • payloadはJSON.stringifyで括って送りつける必要があります。

Webhookの受け口

doPostのコード

doPostでなければWebhookとして受け取れません。また、前述でGCPのログエクスプローラを使う為の準備をしましたが、こうすることで、doPostで受け取ったデータをconsole.logでデバッグすることが出来ます。Webhookデータはe.postData.contentで取得する事が可能で、受け取ったデータを元に処理をすることが可能です。但し、doPostのconsole.logの内容はログエクスプローラからでないと確認できないので注意が必要です。

Google Apps Scriptでデバッグするイロハ集【GAS】

//webhookurl
function doPost(e){
  let ret = e.postData.content;
  console.log(ret)
}

図:doPostの中身をデバッグ中

受信したWebhookの中身

e.postData.contentで内容を受け取ると今回はWebex Meetingでeventを指定してるので以下のような内容を取得出来ます。複数テナントがある場合には、組織のIDを元にWebhookの作成時にリクエストオプションとしてfilterにてorgId="組織のID"を加えておくと、Webhookを飛ばす内容を事前に絞り込めます。

また、今回のdoPostのURLは公開URLなので、事前にリクエストオプションにsecretにて値を入れておき、Webhookを設置。レスポンスデータ内にそれがあるかどうかをdoPost内のコードで判定して弾くようにすれば安心です。

{
    "id":"WebhookのID",
    "name":"m_webhook",
    "targetUrl":"Webhookの受け口のURL",
    "resource":"meetings",
    "event":"created",
    "orgId":"Webhookを作った人の組織のID",
    "createdBy":"Webhookを作った人のID",
    "appId":"アプリの固有のID",
    "ownedBy":"creator",
    "status":"active",
    "created":"2023-03-17T11:54:16.362Z",
    "data":{
        "id":"ミーティングの固有のID",
        "meetingNumber":"ミーティングナンバー",
        "meetingType":"ミーティングのタイプ",
        "timezone":"UTC",
        "start":"2023-03-18T12:05:00Z",
        "end":"2023-03-18T12:45:00Z",
        "hostUserId":"ユーザのID",
        "state":"active",
        "hostEmail":"ミーティングをセットした人のメアド",
        "siteUrl":"ミーティングのサイトURL",
        "orgId":"組織のIDがここに入ってくる",
        "hostType":"1001001"
    }
}

Webex Callingの場合

対象のアカウントに掛かってきた電話に対して、電話番号はわかるようになってるので、これを基準にどこからの電話だよというのは区別をつけようと思えば付けられる。また、受電する側はWebhookを設置したアカウントで電話番号は割り振られてるので、いわゆる鳴分けの元になる処理はここで必要な情報は揃ってる状態。

実際にこれを元に、鳴分けやらナンバーディスプレイ装備をする場合にはGASで受け取っても意味がないので、外部にNode.jsのSocket.ioのサーバなどを用意してそちらで処理をやらせる必要があります。

単純に通知とログのみでオッケーであるならば外部から叩ける状態にして、GASで受け取りWebhookの内容を例えばGoogle ChatのWebhookに投げるといった事で、着電ログを残したり着電時に内容をスマフォで通知を受け取ったり、またスマフォからの場合はチャット内容から電話をそのまま掛けるといったことが可能になります。Google Chatに通知を送るサンプルコードは以下のようなものになります。

着電する電話番号と紐付いてるアカウント個々でOAuth認証を行う必要があります。

telnumがundefinedの場合は非通知なので、その場合の処理も追加しています。

//Google Chat Webhook
var chatwebhook = "ここにスペースのWebhookのURLを入力"

//書き出し先スプレッドシートのID
var ssid = "書き出し先スプレッドシートのIDをここにに入れる";

//着電先電話番号
let calling = "ここは着電先の電話番号を入れる"

//webhookで受信した場合
function doPost(e){
  let ret = e.postData;
  
  let json = JSON.parse(ret.contents)
  
  //電話番号を取得
  let telnum = String(json.data.remoteParty.name);
  let timestamp = formatdate(json.data.eventTimestamp);
  let callType = json.data.remoteParty.callType;

  //Chatに送信
  let target = calling + "に着電" + "\n"

  //非通知対応
  if(telnum == undefined){
    telnum = "非通知"
  }

  let msg = target + timestamp + "\n\n【*" + telnum + "*】からお電話ですよ"
  let result = pushWebhook(msg);

  //配列にまとめる
  let array = [calling,telnum,timestamp,callType];
  
  //スプレッドシートに書き出し
  var ss = SpreadsheetApp.openById(ssid).getSheetByName("シート1");
  ss.appendRow(array)
}

function formatdate(date){
  var tempdate = new Date(date);

  var fullyear = tempdate.getFullYear();
  var monthman = paddingZero(tempdate.getMonth() + 1);
  var dateman = paddingZero(tempdate.getDate());
  var hourman = paddingZero(tempdate.getHours())
  var minman = paddingZero(tempdate.getMinutes());
  var secman = paddingZero(tempdate.getSeconds());

  //文字列を結合する
  var retdate = fullyear + "/" + monthman + "/" + dateman + "  " + hourman + ":" + minman + ":" + secman;

  return retdate;
}

//頭に0をつける
var paddingZero = function(n) {
  return (n < 10)  ? '0' + n : n;
};

//Webhookに対してメッセージを送り込む
function pushWebhook(msg) {
  //メッセージを作成
  let message = {
    'text' : msg
  };

  //送信オプション
  let options = {
    'payload' : JSON.stringify(message),
    'method': 'post',
    'contentType' : 'application/json'
  };

  //POSTにて送信する
  let response = UrlFetchApp.fetch(chatwebhook,options);

  return response;
}

図:numberにどこからの電話なのかが記録

図:Google Chatに無事に届いた

Google Apps ScriptでGoogle Chatにメッセージを送る【GAS】

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)