はじめに
コロナ禍の影響でWebMeetingツールの利用ニーズは急拡大しています。ビジネスでの利用シーンが増えるにつれて、運用を楽にするための仕組みの重要性も高まってきているのではないでしょうか。そこで、今回はWebMeetingツールの代表的な製品の一つであるZoomを対象に、運用の効率化を考えてみたいと思います。Zoomには、公開されているAPIとしてZoom APIがあります。日々の利用において、繰り返し実行する操作を、WebやスマホのUIからの操作ではなく、APIを通じて実行できるようになると、ちょっとした操作を省略できたり、関連する他の作業とまとめて一括で実行することができるようになったりと、うれしさがでてきます。Zoom APIの活用例としては、Meetingの作成等、ユーザ側の作業を対象としたものを中心に多くの方々が執筆されています。そこで、今回は管理者側の作業を対象としてAPIを使った効率化に取り組んでみます。具体的には、テナントに登録済みのユーザへの、有償ライセンス(Proライセンス)の割り当てと割り当ての解除を対象とします。
準備・前提
今回ご紹介するサンプルコードを作成した際の、準備・前提事項についてまとめておきます。
- Zoomアカウント Zoomを利用するためのアカウントが必要です。今回は登録済みの他のユーザの設定変更を行うため、アカウントにはしかるべき権限が必要です。この記事では「オーナー」という特権アカウントを使っていますが、作業の内容によっては権限を絞ったアカウントで対応できるものもあるかもしれません。
- Zoomライセンス 割り当て対象となるライセンス契約が必要です。ユーザ全員分の有償ライセンスを持っていれば、都度割り当てを行う必要はありませんが、有償ライセンスが必要となる会議の開催頻度はそれほど高くないため、必要に応じてMeetingのホストとなるユーザにProライセンスを割り当てる運用を行っています。
- スクリプト実行環境 今回の取り組みでは、Windows 10上のpython3.9.0を用いてスクリプトを作成・実行しています。
事前調査
API確認①ユーザ更新
Zoom API Referenceでユーザデータの更新を行うためのAPIを確認します。UsersのAPI群の中に、Update a UserというAPIが提供されているので、今回はこのAPIを用いることにします。操作対象となるユーザのデータ構造は以下のように規定されています。
{
"first_name": "string",
"last_name": "string",
"type": "integer",
"pmi": "integer",
"timezone": "string [date-time]",
"dept": "string",
"vanity_name": "string",
"host_key": "string",
"cms_user_id": "string",
"job_title": "string",
"company": "string",
"location": "string",
"phone_number": "string",
"phone_country": "string"
}
ライセンス割り当ての設定は、「type」の値で制御することができます。1がBasic(無償版利用可能)、2がLicensed(有償版利用可能)となります。
このAPIのエンドポイントは、https://api.zoom.us/v2/users/{userid} とされています。したがって、このAPIを使うためには操作対象ユーザのuseridを特定する必要があります。
API確認②ユーザID紹介
useridは、Zoomのサインインもちいるアカウント名(メールアドレス)とは別物です。内部的に生成されている一意の識別子のため、一般ユーザーがマイページどから参照することはできません。そこで、今回はユーザの一覧取得するList UsersのAPIを用いて、既存ユーザの情報を引いてくることにします。
List Usersで返されるデータの構造は以下のようになっています。
{
"page_count": 1,
"page_number": 1,
"page_size": 30,
"total_records": 1,
"users": [
{
"id": "z8yAAAAA8bbbQ",
"first_name": "Melina",
"last_name": "Ghimire",
"email": "mel@jfggdhfhdfj.djfhdsfh",
"type": 2,
"pmi": 581111112,
"timezone": "America/Los_Angeles",
"verified": 1,
"dept": "",
"created_at": "2018-11-15T01:10:08Z",
"last_login_time": "2019-09-13T21:08:52Z",
"last_client_version": "4.4.55383.0716(android)",
"pic_url": "https://lh4.googleusercontent.com/-someurl/photo.jpg",
"im_group_ids": [
"Abdsjkfhdhfj"
],
"status": "active"
}
]
}
usersとしてリストで返される各ユーザの情報の中に含まれる、id(userid)を取得してUpdate a UserのAPIをたたきに行くことにします。操作対象のユーザを特定するキーとして、今回はemailを用いることにします。
JWT token
pythonスクリプトからAPIをたたきに行くためのtokenを準備します。JWT tokenの生成方法については、すでにいろいろな紹介記事がありますので、ここでの詳細な説明は割愛させていただきます。こちらの記事などを参考としてください(Zoom APIを試してみる JWT編)
実装
処理の流れ
引数① emailアドレス
引数② 設定するライセンス割り当て状態(1:Basic 2:Licensed)
- List User APIを使って、ユーザ一覧の情報を取得する
- 取得した情報から、emailをキーとして操作対象ユーザを特定し、useridを取得する
- Update a User APIを使って、操作対象ユーザのライセンス割り当て状態を変更する
コードサンプル
Zoom API Referenceで提供されているサンプルコードを元に必要な処理を加えて修正したものです。※とりあえず動くレベルの内容なので、より良い実装方法がある場合には、コメントいただけると幸いです。
import sys
import http.client
import json
# 引数の取得
args = sys.argv
target_email = args[1]
lic_type = args[2]
print(f'target email:{target_email}')
print(f'license type:{lic_type}')
conn = http.client.HTTPSConnection("api.zoom.us")
# JWT tokenによる認証を含むリクエストヘッダの設定
headers = {
'authorization': "Bearer <JWT token>",
'content-type': "application/json"
}
# List User APIによるユーザ一覧情報の取得。page_sizeはデフォルト30なので、アカウント登録数に見合う最大値を設定する
conn.request("GET", "/v2/users?page_size=200", headers=headers)
# レスポンスを変数に格納
res = conn.getresponse()
# utf-8で読み出し
res_string = res.read().decode("utf-8")
# json(辞書)型として抽出
res_dict = json.loads(res_string)
# usersの格納値をリストとして抽出
user_list = res_dict["users"]
# 引数で指定したemailを持つユーザのuseridを取得
for i in range(len(user_list)):
user = user_list[i]
if user['email'] == target_email:
target_id = user['id']
print(f'target id:{target_id}')
break
# Update a User APIに渡すペイロードの設定。変更箇所のみでよい。
payload = f'{{"type":{lic_type}}}'
print(f'payload:{payload}')
# Update a User APIによるライセンス割り当ての変更
url = f'/v2/users/{target_id}'
print(f'url:{url}')
## 変更前のユーザデータ確認
conn.request("GET", url , headers=headers )
res = conn.getresponse()
data = res.read()
print("変更前データ")
print(data.decode("utf-8"))
## 変更
conn.request("PATCH", url, payload, headers)
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
## 変更後のユーザデータ確認
conn.request("GET", url , headers=headers )
res = conn.getresponse()
data = res.read()
print("変更後データ")
print(data.decode("utf-8"))
動作確認
動作確認時の出力内容を記載します(個人情報などが出力されている部分はxxxに置換してあります)
Basicライセンスユーザに有償ライセンスを割り当てる(type:1→2)
変更前に"type:1"
だったライセンス状態が、変更後には"type":2
に変わっています。
target email:xxx
license type:2
target id:xxx
payload:{"type":2}
url:/v2/users/xxx
変更前データ
{"id":"xxx","first_name":"xxx","last_name":"xxx","email":"xxx","type":1,"role_name":"Owner","pmi":xxx,"use_pmi":false,"personal_meeting_url":"https://us02web.zoom.us/j/xxx","timezone":"Asia/Tokyo","verified":1,"dept":"","created_at":"yyyy-mm-ddTHH:MM:SSZ","last_login_time":"yyyy-mm-ddTHH:MM:SSZ","last_client_version":"xxx","host_key":"xxx","cms_user_id":"","jid":"xxx","group_ids":[],"im_group_ids":[],"account_id":"xxx","language":"jp-JP","phone_country":"","phone_number":"+null ","status":"active","job_title":"","location":""}
変更後データ
{"id":"xxx","first_name":"xxx","last_name":"xxx","email":"xxx","type":2,"role_name":"Owner","pmi":xxx,"use_pmi":false,"personal_meeting_url":"https://us02web.zoom.us/j/xxx","timezone":"Asia/Tokyo","verified":1,"dept":"","created_at":"yyyy-mm-ddTHH:MM:SSZ","last_login_time":"yyyy-mm-ddTHH:MM:SSZ","last_client_version":"xxx","host_key":"xxx","cms_user_id":"","jid":"xxx","group_ids":[],"im_group_ids":[],"account_id":"xxx","language":"jp-JP","phone_country":"","phone_number":"+null ","status":"active","job_title":"","location":""}
有償ライセンスユーザをBasicに戻す(type:2→1)
変更前に"type:2"
だったライセンス状態が、変更後には"type":1
に変わっています。
target email:xxx
license type:1
target id:xxx
payload:{"type":1}
url:/v2/users/xxx
変更前データ
{"id":"xxx","first_name":"xxx","last_name":"xxx","email":"xxx","type":2,"role_name":"Owner","pmi":xxx,"use_pmi":false,"personal_meeting_url":"https://us02web.zoom.us/j/xxx","timezone":"Asia/Tokyo","verified":1,"dept":"","created_at":"yyyy-mm-ddTHH:MM:SSZ","last_login_time":"yyyy-mm-ddTHH:MM:SSZ","last_client_version":"xxx","host_key":"xxx","cms_user_id":"","jid":"xxx","group_ids":[],"im_group_ids":[],"account_id":"xxx","language":"jp-JP","phone_country":"","phone_number":"+null ","status":"active","job_title":"","location":""}
変更後データ
{"id":"xxx","first_name":"xxx","last_name":"xxx","email":"xxx","type":1,"role_name":"Owner","pmi":xxx,"use_pmi":false,"personal_meeting_url":"https://us02web.zoom.us/j/xxx","timezone":"Asia/Tokyo","verified":1,"dept":"","created_at":"yyyy-mm-ddTHH:MM:SSZ","last_login_time":"yyyy-mm-ddTHH:MM:SSZ","last_client_version":"xxx","host_key":"xxx","cms_user_id":"","jid":"xxx","group_ids":[],"im_group_ids":[],"account_id":"xxx","language":"jp-JP","phone_country":"","phone_number":"+null ","status":"active","job_title":"","location":""}
今後にむけて
今回は、Zoom提供のサンプルコードを元に、とりあえず動くというところまでコードを書きました。が、業務で利用するにはエラーハンドリングをある程度しっかり作りこみたいところです。また、今回の実装ではJWTトークンで認証していますが、JWTトークンには有効期限があるため、長期間の有効期限を持つJWTトークンを保持するのではなく、必要なタイミングで短い有効期限のJWTトークンを都度取得できるような作りにしておく方が、安心・安全な気がしています。ZoomからはMeetingの作成や開始等をトリガーとするWebhookも提供されているので、有償ライセンスが必要な長時間のMeetingが開始されたことをトリガーとしてMeetingのホストに有償ライセンスを割り当てたり、Meetingの終了をトリガーとしてホストへのライセンス割り当てを解除したりするような自動化をつくりこめると、管理者してのお仕事がもっと楽にるような気がしています。