Google Apps ScriptでWebex APIを操作する【GAS】
コロナ渦を経て、日本の企業でもはやWeb会議システムを導入していない企業はどうなのよ?と言われるほど当たり前の存在になりました。圧倒的にZoomがシェアを握っていて、TeamsやGoogle Meetがそれを追いかける状況で、結構しっかり作られていてREST APIも豊富なのに知名度低いのが「Cisco Webex」。
無償で利用できるのに、Ciscoがあまり推していないのかシェアはいまいちですが、自分は仕事で使っています。ということで今回はこのWebex APIをGoogle Apps Scriptから使ってみました。
目次
今回利用するスプレッドシート等
- Webex API - Google Spreadsheet
- Webex API Reference
上記のリファレンスがサンプルコードやレスポンス、デモが整っているのですが、もう一つリファレンスがあり、後者のほうが実は全てを網羅してたりします(IP電話/クラウドPBXのWebex Callingなどの記載もきっちり乗っています)。今回はこのAPIのOAuth2.0認証およびWebhookを設置するコードを作ってみました。
Webhookの受け口もGASで記述して取得させています。
※自分の場合、Webex Callingの着電をWebhookで取得したかったために今回のコードを作成しています。PCやスマフォ、実機から電話の出来るためコールセンターに向いています。
事前準備
今回のプログラムはOAuth2認証、Webhook設置、Wehookの受け口の3つをGASで作ります。デバッグ含めてポイントがいくつかあるので、以下の作業をしておきましょう。
GAS側の作業
プロジェクトを移動する
ログエクスプローラの使い方等は以下のエントリーを参照してください。
図:プロジェクト変更画面
デプロイを行う
GAS側で処理するためのコードを記述したら、doPostの受け口を有効化するためにデプロイをします。
- スクリプトエディタを開く
- 右上のデプロイをクリック
- 新しいデプロイをクリック
- 種類の選択ではウェブアプリを選択し、次のユーザとして実行は自分にしておきます。
- アクセスできるユーザは、全公開する必要があるので「全員」としておきます(Webhookなので認証有りだとWebex側からアクセス出来ない)
- 末尾がexecで終わるURLが発行される。これを次項のGASのWebhookurlとして指定します。
- 次回以降コードを編集して再デプロイ時はデプロイを管理から同じURLにて、新しいバージョンを指定して発行することが出来ます。
コールバックURLを取得する
コールバックURLとは、認証を完了しAccess Tokenを取得したら戻るべきURLを指定するものです。これは、スクリプトIDをもとに作られているので、スクリプトIDを取得して組み立てます。
- スクリプトエディタのサイドバーより「プロジェクトの設定」を開く
- 情報の中にある「スクリプトID」を控えておく。
- https://script.google.com/macros/d/スクリプトID/usercallback として組み立てる。これがコールバックURLとなる。
図:スクリプトIDはファイル毎に異なるのです。
ライブラリの追加
以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。
- スクリプトエディタを開きます。
- サイドバーよりより「ライブラリ」の+ボタンをクリック
- ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
- 今回はバージョンは43を選択してみます。
- 保存ボタンを押して完了
これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。
図:ライブラリを追加した様子
Webex側での作業
OAuth2.0認証に必要なクライアントIDやシークレットを取得する為に、Webex for Developerにログインしてアプリを作成します。
- Webex for DeveloperのCreate New Appにログインする
- Create an Integrationをクリック
- Integration Nameがアプリの名前になります。
- アイコンは512x512のサイズのpng画像をアップする必要があります。
- App Hub Descriptionがアプリの説明文
- Redirect URLは、前述のコールバックURLを入れます。
- ScopesはAPIを利用して何をするかでオンにしていきます。今回は適当に「spark-compliance:webhooks_read spark:kms spark-compliance:webhooks_write meeting:schedules_read」をオンにします。
- Add Integrationをクリックすると、Client IDおよびClient Secretが手に入ります。
図:IDとSecretを後で利用します。
ソースコード
今回のコードはMeetingをスケジュールしたらWebhookで飛ばすのを設置します。そのWebhookはdoPostで受け取る必要があります。また、OAuth認証ではScopeが必要になります。あとはScopeに応じてWebexの様々なAPIを叩くことが出来るようになります。
OAuth2認証コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
//認証用の各種変数 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を設置する事で、代表電話に掛かってきたイベントを取得する事が出来るようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
//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の内容はログエクスプローラからでないと確認できないので注意が必要です。
1 2 3 4 5 |
//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内のコードで判定して弾くようにすれば安心です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{ "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の場合は非通知なので、その場合の処理も追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
//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に無事に届いた
関連リンク
- Web会議システムのシェア・市場規模を解説!一番選ばれている人気サービスは?
- Webex で ChatGPT API を使った Bot の作り方
- Cisco WebexとAWS LambdaでOpenAI GPT-3のチャットボットを作る
- Cisco Webex TeamsのBotで、投稿されたメッセージにリアルタイムに反応する(4分で実装)
- Webex Calling APIを始めてみよう-3種類のAPIについて概要を把握する
- IOS/IOS-XE : TFTP/FTP/SCP を使用したファイル転送
- Cisco IOS - File System
- 【GAS】Googleカレンダから今日のイベントを拾ってWebex Teamsに通知する
- 【GAS】Googleスプレッドシートで管理している当番表をメンション付きでCisco Webex Teamsに定期的に通知する