2023-03-31 04:09 PM 2023-04-17 01:20 PM 更新
Webex Meetingsサービスでは会議中に映像と音声をレコーディング(録画)したり、会話の内容を文字起こししてテキストデータ(トランスクリプト)を生成する機能があります。通常の使い方では会議終了後からレコーディングのデータの変換や、そのレコーディングデータからの文字起こし処理が逐次実行され、主催者や共同主催者に完了の通知がされます。主催者などはその通知からレコーディングを再生(リプレイ)したり、ダウンロードすることができます。もちろん他の参加者などに共有することも可能です。
一般ユーザはWebexのアプリやWebポータルでこのような作業が楽に行えるようになっているために、レコーディングに関わるこのような作業などに苦痛を感じることはあまりないとは思います。むしろかなり使いやすく自然と使いこなせるようになっています。
しかし、大企業の管理者にとってはこのような操作を運用チーム側で担って全ユーザ分のデータを一括で処理したいというケースがあります。
ダウンロードを自動化するユースケースの例:
これらのユースケースは従業員の数が多い企業においては手動ではやっていられません。そこでAPIを利用して自動化する必要があります。
WebexのAPIはレコーディングやトランスクリプトの生成が終了したら即時に主催者や管理者のアプリケーションへ通知する機能(Webhook)があります。Webhookにはレコーディングやトランスクリプトなどを識別するIDが含まれていますので、通知を受けたアプリケーションはそのIDをキーとしてダウンロード用のURLを含む詳細情報(会議時間、主催者情報、ファイル形式など)を取得することができます。
今回はそのような動作をする簡素なデモアプリケーション を作成いたしましたので、デモを構成するサンプルコードなどをご紹介しながら自動化を実現するシステムをどのように開発すれば良いのかの参考になるような情報をご提供できたらと思いこちらの投稿を作成しております。Webhookの使い方、Webhookを受信した後にダウンロードURLを取得する際のポイントなどをトピックとしております。
Webex APIのWebhook はWebexの各種サービス (Meetings&Webinar, Calling, Messaging, Contact Centerなど) におけるユーザやコンテンツ(アセットともいいます)のアクティビティに関しての通知を他のサービスにHTTPで通知する仕組みです。WebhookはSaaSサービスなどで一般的に使われている標準的な仕組みです。
Webex APIのWebhookで例えば次のようなイベントを受け取ることができます。
今回はレコーディングとトランスクリプトの生成が完了した際に送信されるイベントWebhookをを受信することをトリガーとしてダウンロードURLを取得するという処理に中心にご説明します。
流れを簡単に説明するシーケンス図を上のように描いてみました。 (ChatGPTにマークダウンテキストを作成してもらいました! その後にテキストを描画ツールに入力して画像を出力しています。)
今回作成したデモではレコーディング生成のイベントを受信してダウンロードURLを取得するところのみを実装していますが、実際には会議終了のイベントも受信してストレージの準備をしたり、ダウンロードを処理を行う別のプロセスなどを実装するなどが必要かとは思いますが、今回はそれらの部分については割愛いたします。
今回のデモはNode.jsで実装しています。Webex APIのWebhookから送られてくるHTTP POSTリクエストを受信するメソッドを以下のように実装しています。今回はレコーディングの生成完了のイベントを受信したときのみにダウンロードURLを取得する処理を行いたいのですが、アプリケーションは前述の通りさまざまなイベントをWebhookで受信可能ですので、そういった多様なイベントの中でどのイベントについてそれぞれについてどのような処理をしたいのかをここで判定して適切な処理を選択する必要があります。
実装方法はサイジングも考慮するといろんなやり方があると思います。今回はレコーディング制裁完了済みのイベントであった場合は即時にダウンロードURLの取得と記録までを連続して処理しています。(わかりやすくするために)
実際には従業員(ユーザ)数が多くてWebhookのリクエスト量が膨大でこのように連続してダウンロードURLの取得までを実装することが難しい場合があります。その場合はここではレコーディングのIDの記録までとし、ダウンロードURLの取得以降は他のスレッドやプロセスなどに委譲するような実装になると思います。
/*
* Webhookの登録が完了し、イベントが発生するとこのURIにWebhookがプッシュされます
* webhookのtargetURLとして設定されているからです
*/
app.post("/webhook", function (req, res, next) {
// Webex API側にはなるべく早く200 OKを返します。secretなどが不正な場合などは適切な対応をします。
res.status(200);
console.log("WEBHOOK PUSHED ***");
console.log(req.body);
console.log("WEBHOOK **********");
if (req.body.resource) {
if (req.body.resource === "recordings" && req.body.event === "created") {
// Webex Meetingsの録音生成完了のイベントだった場合
if (req.body.data) {
recordingIdList.push(req.body.data.id);
// 録音ファイルをダウンロードするURLなど詳細を取得します
axios
.get("/v1/recordings/" + req.body.data.id, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
.then((response) => {
if (response.status === 200) {
let recordingInfo = {
downloadUrl: response.data.downloadUrl,
password: response.data.password,
temporaryDirectDownloadLink:
response.data.temporaryDirectDownloadLinks
.recordingDownloadLink,
};
console.log(JSON.stringify(recordingInfo));
// ダウンロード用のURLなどを保存します
// 実際のユースケースではダウンロードとストレージ管理のワーカースレッドに渡す方が良いでしょう
recordingInfoList.push(recordingInfo);
}
})
.catch((error) => {
if (error.response) {
console.log(error.response.status); // 例:400
console.log(error.response.statusText); // Bad Request
}
console.log(error.message);
});
}
}
}
});
レコーディングの詳細を取得するエンドポイントへアクセスし、downloadURLやtemporaryDirectDownloadLinksなどを取得しています。これをリストに追記しています。実際にはデータベースなどに記録するケースが多いのではないでしょうか。
なお、レコーディングではなくトランスクリプトについても同様の方法でWebhookの受信と詳細の取得を行えます。
Webex APIではWebhookを送信するURLや送信するイベントの種類などの設定はWebhookを受信するアプリケーションから自発的にWebex APIのCreate a Webhookエンドポイントへ要求して行う必要があります。
SaaSのサービスによっては管理者ポータルのUIで設定を行えるものもありますが、Webexではアプリケーション側がOAuth認証を経てWebhookの作成を行う手順を必要としております。
app.get("/create_webhook", function (req, res, next) {
axios
.post(
"/v1/webhooks",
{
// Webex Meetingsのレコーディング作成イベントを取得する場合は以下のように指定します
name: "wh_meetings_recordings",
targetUrl: "https://example.com/webhook", // Webhookを送信してほしい宛先のURLをここで指定
resource: "recordings", // レコーティングに関するイベントのみのプッシュを希望
event: "created", // 生成時のイベントのみを希望するのでcreatedを指定
//, filter: "" // Webhookイベントを最小限にするためにぜひfilterを有効にご利用ください
//, secret: "" // Webhook POST要求がWebexクラウドから送信されていることを検証するための署名を利用するためのキーですが、積極的にご利用ください
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
}
)
.then((response) => {
if (response.status === 200 || response.status === 201) {
let webhookInfo = {
id: response.data.id, // Webhookを識別するID
name: response.data.name,
status: response.data.status,
created: response.data.created,
};
webhookList.push(webhookInfo);
res.status(200).render("create_webhook_success", {
webhookInfo:
"レコーディングの作成イベントに関連するWebhookを追加しました:\n" +
webhookInfo.name +
" -> https://example.com/webhook",
});
}
})
.catch((error) => {
if (error.response) {
if (error.response.status === 401) {
res.render("error401", {
oauth_auth_url: process.env.OAUTH_AUTH_URL,
});
} else if (error.response.status === 409) {
// 名前とtargetUrlなどが同じWebhookの作成を試みた場合は409が返る
}
}
console.log(error.message);
});
});
Webhookを受信した場合は速やかに200 OKをWebex側に返す必要があります。ここでエラーコード(404 Not Foundなど)を返したりタイムアウトした場合はWebex側でWebhookを無効化(ステータスをactiveからinactiveに変更してWebhookを送信しなくなるなど)するケースも想定してアプリケーション側でWebhookの有効性を確認する必要があります。
実装方法はいろいろあるかと思いますが、このデモではその辺りの処理はひとまずシンプルに以下のように30秒間隔でポーリングして必要があればステータスの変更や再登録を行うようにしています。
// 30秒間隔でWebhookの有効性をWebex APIに対してチェック
var webhookCheckTimer = setInterval(() => {
webhookList.forEach((wh) => {
axios
.get("/v1/webhooks/" + wh.id, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
.then((response) => {
if (response.status === 200) {
if (response.data.status == "active") {
console.log("Webhookのステータスはactiveです");
} else {
console.log("Webhookのステータスはinactiveです");
// ステータスをactiveへ変更します
axios
.update(
"/v1/webhooks/" + wh.id,
{
name: wh.name,
targetUrl: wh.targetUrl,
status: "active",
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${req.session.accessToken}`,
},
}
)
.then((response) => {
if (response.status === 200) {
}
})
.catch((error) => {
if (error.response) {
console.log(error.response.status); // 例:400
console.log(error.response.statusText); // Bad Request
}
});
}
} else {
console.log("Webhookは未登録");
// webhookListから未登録になってしまったWebhookを除外するなど同期の処理や管理者への通知などをここで実行
}
})
.catch((error) => {
if (error.response) {
console.log(error.response.status); // 例:400
console.log(error.response.statusText); // Bad Request
if (response.status === 401) {
// Access Tokenの期限切れが疑われる場合はRefresh Tokenを利用して新しいAccess Tokenを再取得します
}
}
});
});
}, 30000);
2023/04/17 追記:
下記のようにOAuthのプロセスを処理するためのUIを実装する必要がなくなりました。
Webex Service Appという、インテグレーション (Integration) 型とは違うタイプのアプリケーションを定義し、ユーザのサインインを都度行わなくてもアクセストークンとリフレッシュトークンを得ることができるようになりました。詳しくは こちらの投稿 をご覧ください。
アクセストークンを最も手軽に入手する方法はdeveloper.webex.comにてパーソナルなアクセストークンをコピーするというものです。ですがこのトークンのライフタイムは12時間ですので、今回のように永続的にダウンロードURLを取得し続けたり、Webhookの登録をメンテナンスし続けるという用途には向きません。(12時間以内に取得を手動で繰り返す必要があります。)
永続的にアクセスが必要な場合は、OAuthのプロセスを経てアクセストークンとリフレッシュトークンの二つを取得し、リフレッシュトークンを使って有効なアクセストークンに更新し続けるという処理を実装する必要があります。
/*
* Webex APIにユーザとしてログインし、アクセストークンを受信する際にAPIからのリダイレクト要求を処理します
* startauth.pugでレンダリングされるページからWebex.comのログイン画面へ遷移し、パスワード入力後にこのURIへリダイレクトされます
* OAuth認証時にリダイレクトされるURIはdeveloper.webex.comにて設定を行います
*/
app.get("/oauth", function (req, res, next) {
console.log("GET /oauth 受信");
req.session.authorizationCode = req.query.code;
console.log("code:" + req.query.code);
// トークン取得のためにhttps://webexapis.com/v1/access_token エンドポイントへPOST送信
let params = new URLSearchParams();
params.append("grant_type", "authorization_code");
params.append("client_id", process.env.CLIENT_ID);
params.append("client_secret", process.env.CLIENT_SECRET);
params.append("code", req.session.authorizationCode);
params.append("redirect_uri", process.env.REDIRECT_URI);
axios
.post("/v1/access_token", params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then((response) => {
if (response.status === 200) {
/*
上記のclient_id, client)secret, code, redirect_uriなどのP内容が適切であり
POST要求が成功するとAccess Tokenなどを取得でき、以後APIにユーザの代理としてアクセスが
可能となります。本来はここでエンタープライズアプリケーション側のユーザ識別情報と共にDBなどの
何らかのストアにここで得られたトークンの内容を補完し、以後のユーザからのアクセスに応じてその
ユーザの正しいトークンを利用してWebex APIにアクセスすべきです。
*/
// このモックアップではセッションの中に単純にトークンを一時的に保存するだけの処理を行なっています。
req.session.accessToken = response.data.access_token;
req.session.refreshToken = req.body.refresh_token;
// Webex APIには200 OKを返す必要があります。その内容はクライアントのブラウザに表示されます。
// ここではサンドボックスの /views/auth_success.pug をレンダリングしてHTMLとして返しています。
res.status(200).render("auth_success", {
webhook_start_url:
process.env.CODESANDBOX_BASE_URL + "/create_webhook",
});
accessToken = response.data.access_token;
refreshToken = response.data.refresh_token;
}
})
.catch((error) => {
if (error.response) {
console.log(error.response.status); // 例:400
console.log(error.response.statusText); // Bad Request
}
if (error.request) {
console.log(error.request);
}
console.log(error.message);
});
});
このアプリケーションの実行を開始すると、Webexのユーザ認証画面へリダイレクト(SSOの統合が設定されている場合はSSOのIdPが提供する認証画面へも)されますので、その認証が成功するとこのアプリケーションにアクセストークンとリフレッシュトークンが付与されます。(認証完了後にアプリケーションを使うユーザのWebブラウザが /oauth URIにリダイレクトされますが、その際に得られる認証コードと事前に共有しているクライアントシークレットなどを送信してアクセストークンとリフレッシュトークンが得られます。)
アクセストークンの有効期限が過ぎた場合に401エラーなどがAPIから返されますが、その際にリフレッシュトークンを使って新しいトークンを得るなどのような処理を行なって新しいアクセストークンを得ることができます。そしてWebex APIへのアクセスを継続できるのです。
さて、今回は以上となりますがいかがだったでしょうか。
ユーザ数のような規模やユースケースの複雑さなどによって実装においてはいろんな工夫が求められるとは思いますが、レコーディングやトランスクリプトの自動的なダウンロードについての相談は私の実際の営業活動の中で増えて来ていると感じておりますので、この記事が参考になったという方が増えてほしいなと思っています。
アプリケーションのコーディングに関する具体的なご質問(どんな条件分岐を記述するか、どんな構造でデータを保持するべきか、どんな例外処理を実装すべきかなど)については回答が難しい立場(営業チームのSEでございます)にありますのでお答えできそうにありませんが、どのようなAPIがあるのかや、Webexにどんな機能があるのかなどのご質問などはお答えできることが多いと思いますので、ユースケース(おおざっぱでかまいません)を共にコメントなどしていただけるとうれしいです。新しいネタを思いついて関連ありそうな投稿もできたりすると思います。
検索バーにキーワード、フレーズ、または質問を入力し、お探しのものを見つけましょう
シスコ コミュニティをいち早く使いこなしていただけるよう役立つリンクをまとめました。みなさんのジャーニーがより良いものとなるようお手伝いします
下記より関連するコンテンツにアクセスできます