システムを設計してみる
前回は社内で定期的に開催しているオンラインもくもく会をGoogle Meetで運用していくために、Google Meet API
を使えるように準備しました。
今回はいよいよ前回の準備を踏まえて、システム構築していこうと思います。
前回準備したことは簡単に言えば、Google Meet APIをたたくためのOAuth2のアクセストークンを取得しただけです。
というわけで、まずはこの取得したアクセストークン(とリフレッシュトークン)を使ってどうやってシステムを作っていくか考えてみます。
前回なんとなく考えていたのは、Google Apps Script(GAS)で定期的にGoogle Meet APIをたたいて、会議参加者を取得すること。
で、取得した情報をSpread Sheetに反映してみんなが見れるようにすること。
この2点でした。
そこで、まずはGASで実装するべき機能を整理してみます。
- アクセストークンの有効期限切れたらリフレッシュトークンでアクセストークンを更新する機能
- 会議スペースを必要分だけ作成する機能
- それぞれの会議スペースでオンライン中の参加者をリストアップする機能
- 作成した会議スペースとそれぞれの会議スペースの参加者をGoogle Spread Sheetへ反映する機能
ちなみに会議スペース(spaces)とはオンライン会議を開催するための仮想の空間のことで、この会議スペース内では会議を1つだけ開催できるようです。
会議スペースがあるだけで会議が開催されるわけではなく、誰かが会議スペース内で会議を開始することで初めて会議レコード(conferenceRecords)が生成されます。
そして会議レコードから参加者がいなくなると会議レコードが破棄されるようです。
なお会議の参加者(conferenceRecords.participants)は会議レコード単位で管理されているため、参加者をリストアップするためには
- まず会議スペース内で現在開催されれている会議レコードを特定する。
- それから会議レコード内の参加者を取得する。
という流れになります。
GASの実装
実装を始める前に前回取得したアクセストークンとリフレッシュトークンをGASのユーザープロパティに保存しておきましょう。
ユーザープロパティはGUI上では編集できないため、GASのプログラム上で設定しなければいけません。
function myFunction() {
PropertiesService.getUserProperties().setProperty('access_token', '{前回取得したアクセストークン}');
PropertiesService.getUserProperties().setProperty('refresh_token', '{前回取得したリフレッシュトークン}');
}
myFunction
を実行すれば、アクセストークンとリフレッシュトークンがユーザープロパティに保存されて後から取り出すことができます。
それからクライアントIDとクライアントシークレットも前回作成しました。
こちらは定数として定義しておきます。
const CLIENT_ID = '{前回作成したクライアントID}';
const CLIENT_SECRET = '{前回作成したクライアントシークレット}';
準備ができたら早速実装を開始しましょう。
まずは「アクセストークンの有効期限切れたらリフレッシュトークンでアクセストークンを更新する機能」からです。
アクセストークン更新
最初はアクセストークンが有効かどうかチェックする関数を作ってみます。
function validateAccessToken(access_token) {
// アクセストークンがないなら問答無用で無効
if (!access_token) return false;
// アクセストークン検証
const url = `https://oauth2.googleapis.com/tokeninfo?access_token=${access_token}`;
const option = {
'muteHttpExceptions': true,
'method': 'GET'
};
const response = UrlFetchApp.fetch(url, option);
// アクセストークンが有効ならステータス200
return response.getResponseCode() == 200;
}
アクセストークンを検証するにはこのURLにGETリクエストを投げます。
https://oauth2.googleapis.com/tokeninfo?access_token=${access_token}
クエリパラメータで指定したaccess_tokenが有効ならステータスコード「200」がレスポンスされます。
逆にaccess_tokenが無効ならステータスコードは「400」が返ってくることになります。
アクセストークンが無効だったらリフレッシュトークンを使ってアクセストークンを取り直す必要があります。
それはたとえばこんなコードになります。
function refreshAccessToken(refresh_token) {
// アクセストークンをリフレッシュ
const url = `https://www.googleapis.com/oauth2/v4/token?refresh_token=${refresh_token}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&grant_type=refresh_token`;
const option = {
'muteHttpExceptions': true,
'method': 'POST'
};
const response = UrlFetchApp.fetch(url, option);
if (response.getResponseCode() == 200) {
// トークンリフレッシュ成功
const data = JSON.parse(response.getContentText());
// 新しいアクセストークンをユーザープロパティに保存しておく
PropertiesService.getUserProperties().setProperty('access_token', data.access_token));
// 新しいアクセストークンを戻り値に指定
return data.access_token;
} else {
// 取得失敗ならリフレッシュトークンが失効しているのでエラーで終了
// この場合はクライアントIDを作り直すところからやり直す必要がある
console.error(response.getAllHeaders());
}
}
アクセストークンのリフレッシュについては前回も少し触れたので、詳しい説明は省略します。
トークンリフレッシュのリクエストが成功すると、こちらもステータスコードは「200」で返ってきます。
そして新しいアクセストークンを含んだJSON形式のテキストがレスポンスボディに入っています。
ここでは新しいアクセストークンはユーザープロパティに保存してから、戻り値にしています。
できあがったvalidateAccessToken
とrefreshAccessToken
を使ってアクセストークンを取得してみます。
function getAccessToken() {
const properties = PropertiesService.getUserProperties();
let access_token = properties.getProperty('access_token');
if (validateAccessToken(access_token)) {
return access_token;
} else {
const refresh_token = properties.getProperty('refresh_token');
return refreshAccessToken(refresh_token);
}
}
getAccessToken
関数はユーザープロパティに保存されているアクセストークンが有効ならその値を返し、無効ならリフレッシュトークンで新しいアクセストークンにして返す関数です。
会議スペース作成
アクセストークンが簡単に更新できるようになったので、次はそのアクセストークンを使って会議スペースを必要分だけ生成する処理を考えてみます。
我々のもくもく会では会議スペースを3~5くらい用意しています。
- メインセッション
- 自習室
- 会議室1 ~ 3
そこでまずは、こんな感じの定義をJSON形式で作ってみました。
保存先はスクリプト プロパティ
にして後から追加、編集できるようにしておきます。
[
{
"name": "<<メインセッション>>",
"description": "もくもく会参加者の方は、最初にこちらのルームへお入りください。"
},
{
"name": "◎自習室",
"description": "基本的にマイクをミュートにしてください。"
},
{
"name": "☆会議室1",
"description": "自由に出入りして、会話してください。"
},
{
"name": "☆会議室2",
"description": "自由に出入りして、会話してください。"
},
{
"name": "☆会議室3",
"description": "自由に出入りして、会話してください。"
}
]
この定義をrooms
というプロパティ名でスクリプト プロパティに登録しておきます。
下記のコードでこの定義を取り出して使うことができます。
JSON.parse(PropertiesService.getScriptProperties().getProperty("rooms"));
ではいよいよ会議スペースを作成する機能を作ってみます。
function createMeetingSpaces(count) {
const access_token = getAccessToken();
const url = 'https://meet.googleapis.com/v2/spaces';
const option = {
'headers': {
'Authorization': 'Bearer ' + access_token,
'Content-Type': 'application/json'
},
'muteHttpExceptions': true,
'method': 'POST'
};
const spaces = [];
for (let i = 0; i < count; i++) {
const response = UrlFetchApp.fetch(url, option);
spaces.push(JSON.parse(response.getContentText()));
}
return spaces;
}
会議スペースの作成はhttps://meet.googleapis.com/v2/spacesというURLにPOSTリクエストを投げるだけです。
もちろん、リクエストヘッダにはAuthorization
としてアクセストークンをセットしておかないといけません。
createMeetingSpaces
の引数には作成する会議スペース数を指定します。
戻り値には作成した会議スペースの情報をリストにして指定しています。
createMeetingSpaces(5);
こんな感じで実行すれば、作成されるはず・・・ですが、失敗しました。
こんな感じで 403 エラー が返ってきてしまいました。
{
"error": {
"code": 403,
"message": "Request had insufficient authentication scopes.",
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
"domain": "googleapis.com",
"metadata": {
"service": "meet.googleapis.com",
"method": "google.apps.meet.v2.SpacesService.CreateSpace"
}
}
]
}
}
前回、クライアントIDを作った時「スコープ」を指定せず、空欄のまま作成しました。
その場合、会議情報を参照しかできない権限になるようです。
そのため、会議スペースを作成する権限がないということでエラーになっています。
そんなわけで、クライアントIDを作り直す必要があります。
クライアントIDを作り直す
GCPコンソールで前回作成したプロジェクト(私は「MokuMokuMeeting」というプロジェクトにしました)を開きます。
- 「API とサービス」の画面へ遷移し、メニューから「OAuth 同意画面」を選択します。
- 前回作成した「もくもく会」というアプリの「アプリを編集」というリンクをクリックします。
- スコープの画面で「スコープを追加または削除」をクリック
- Google Meet API の下記2つのスコープを選択して更新をクリック
「…/auth/meetings.space.created」
「…/auth/meetings.space.readonly」
設定が保存できたら「認証情報」の画面からクライアントIDを作り直します。
スコープを更新したら
クライアントIDを作り直さないと更新されたスコープは反映されない
のでお気を付けください。
クライアントIDの作り方からアクセストークンの取得は前回の記事を確認してみてください。
基本的な手順は前回と同じですが、1点だけ違うのは認可コードを取得するためにブラウザで認可画面にアクセスするところです。
前回はクエリパラメータのscopeに「https://www.googleapis.com/auth/meetings.space.readonly」という値を1つ指定しただけでしたが、今回はもう1つ「https://www.googleapis.com/auth/meetings.space.created」という値を指定する必要があります。
そして、scopeに複数の値を指定する場合は「%20」という区切り文字で連結することになっています。
というわけで具体的にはこんなURLになりました。
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=99999999999-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com&redirect_uri=https://localhost&scope=https://www.googleapis.com/auth/meetings.space.readonly%20https://www.googleapis.com/auth/meetings.space.created&access_type=offline
※クライアントIDの部分はダミーです
ブラウザでこのURLにアクセスして認可コードの取得
→curl コマンドでアクセストークン、リフレッシュトークンを取得
という流れは前回と同じです。
前回の手順通りにやれば、新しいアクセストークンとリフレッシュトークンが取得できるはずです。
アクセストークンとリフレッシュトークンが新しく取得しなおせたら、再びGASのユーザープロパティにセットしておきましょう。
function myFunction() {
PropertiesService.getUserProperties().setProperty('access_token', '{新しく取得したアクセストークン}');
PropertiesService.getUserProperties().setProperty('refresh_token', '{新しく取得したリフレッシュトークン}');
}
これで準備OK!
もう一度、会議スペースの作成処理を実行してみましょう。
createMeetingSpaces(5);
今度はステータスコード200が返ってきて、ちゃんと会議スペースが作成できましたね。
返ってきた会議情報はこんな感じです。
[
{
name: 'spaces/X9XxxXxXX9XX',
meetingUri: 'https://meet.google.com/xxx-xxxx-xxx',
meetingCode: 'xxx-xxxx-xxx',
config: { accessType: 'TRUSTED', entryPointAccess: 'ALL' }
},
{
name: 'spaces/xXXXxxXXxxxX',
meetingUri: 'https://meet.google.com/yyy-yyyy-yyy',
meetingCode: 'yyy-yyyy-yyy',
config: { accessType: 'TRUSTED', entryPointAccess: 'ALL' }
},
・
・
(以下省略)
・
・
]
nameプロパティはいわゆる会議スペースのIDみたいなものですね。
meetingUriプロパティは会議に参加する時のURLです。
meetingCodeプロパティはmeetingUriの末尾と同じ値です。
これらの情報の内 meetingCode を使って会議スペースで現在開催されている会議レコードを取得することになります。
続きは – システム構築 後編 – へ
そんなわけで、続けて会議スペースから meetingCode を使って会議レコードを取得。
そして、会議レコードから現在の参加者をリストアップする。
最後にリストアップした参加者をGoogle Spread Sheetへ反映する。
こんな流れで実装を進めていくわけですが、ちょっと長くなってきたので今回はここまで。
続きは次回にしたいと思います。
良かったら次回「- システム構築 後編 -」の記事も見てみてください。
もくもく会システム開発 シリーズ目次
コメント