catch-img

Zoomの投票作成をAPIで秒処理

こんにちは。経営企画室 プロトタイプビルダーの山下です。
ラーニングサービス部 AWS認定インストラクターの傍ら、主に自分の作業まわりを改善するプロトタイプビルダーを兼任しています。

今日は今年作って、運用しているツールを一つ紹介します。
読んでいただいた方の何かのヒントになれば幸いです。

課題

トレノケートではオンライントレーニングを行う上での配信サービスとして、Zoomミーティングを利用しています。
Zoomミーティングには、投票機能があり、受講者の皆様にアンケートを取ることができます。
ミーティングにはテンプレート機能があるので、ミーティングの設定とあわせて作成しておいた投票を使い回せます。
ですが、トレノケートではミーティングを作成する部門があり、開催するインストラクターが作成するわけではありません。
そして、コース数も約1,400コースあり、それぞれで個別管理をすることが現実的ではないため、テンプレート機能は使いません。
なので、投票などの個別設定はミーティングが作成された後にインストラクターがそれぞれ行います。

そこで例えば10問ほどある投票を作成しようと思うと、毎回同じ質問と選択肢をコピー、ペーストするだけでも、10分以上の時間がかかります。

私はこの作業が面倒で面倒で苦手でした。
この作業が待っていると思うと少しばかり憂鬱になってしまい、美味しいお酒も100%楽しむことができませんでした。

そこで、まずはZoomのAPIのpollsへPostmanを使って、投票内容を書いておいたJsonをPOSTする運用を始めました。

これで毎回、秒で投票作成が完了するようになりました。

すごく便利ですごく嬉しかったので、他のインストラクターにも共有しようと思いましたが、そのためにはZoomのAPI認証情報を共有しなければなりません。
トレノケートではユーザーごとにはAPI認証情報の発行は制御されていて、管理部門から発行してもらったAPI認証情報を使っています。

そこで、以下の要件を満たすツールを作りました。

  • インストラクターはログイン認証して利用する。
  • 投票内容はゼロから書かなくても過去のミーティングから取得可能。
  • 一度使った投票は何度でも呼び出し可能。編集削除も可能。
  • 他のインストラクターの投票は見ない。
  • ZoomのAPI認証情報は安全な場所で管理して使用する。

 

作ったもの

ログイン

[UserName]と[Password] を入力して[Login]ボタンからログインします。

投票作成までの流れ

(1) 投票内容登録
データベースに投票内容を登録します。

(2) 投票作成
データベースに登録した投票を、指定したミーティングIDに作成します。

(1) 投票内容登録

[投票内容]をJson形式で入力して、[Add]ボタンで登録します。

[名前]はこのツールで投票内容を見分けやすくするためのものです。いわば目印です。わかりやすい名前をつけてください。(オプション機能参照)

過去のミーティングから投票内容を呼び出すこともできます。(オプション機能参照)

* Jsonの例
2つの投票で、1つめの投票に2つの質問、2つめの投票は1つの質問の例です。
「好きなメニューは何ですか?」の質問は、複数選択("type": "multiple")です。

{    "polls": [
{
"title": "おはようございます!",
"questions": [
{
"name": "どこから接続されていますか?",
"type": "single",
"answers": [
"自宅から",
"会議室から",
"オフィスから",
"その他"
]
},
{
"name": "好きなメニューは何ですか?",
"type": "multiple",
"answers": [
"和食",
"中華",
"フレンチ",
"イタリアン",
"ファーストフード",
"その他"
]
}
]
},
{
"title": "午後もがんばっていきましょう!",
"questions": [
{
"name": "運動は何かされてますか?",
"type": "single",
"answers": [
"ランニング",
"筋トレ",
"ヨガ",
"その他"
]
}
]
}
]
}

オプション機能 - 名前

名前をつけて一覧で見やすくすることができます。

既存の投票内容にも名前を入力して[Update]できます。

オプション機能 - 呼び出し

過去に使用したミーティングから投票内容を呼び出せます。

過去のZoomのミーティングIDを入力して、[呼び出し]ボタンを押下します。

投票内容に反映されます。
このまま[add]ボタンから新規追加して、[投票作成]することもできます。
もちろん修正して再利用ができます。

(2) 投票作成

登録した投票が上部一覧に表示されますので選択します。
[投票内容]フィールドに選択した投票内容が表示されます。
ZoomのミーティングIDを[ミーティングID]フィールドに入力して、[投票作成]ボタンを押下します。
[ミーティングID]フィールドが空(ブランク)になったら処理終了です。
Zoomの管理画面を確認してください。

次回以降は、(2) 投票作成の操作のみで、投票作成ができます。

投票内容の編集

登録した投票が上部一覧に表示されますので選択します。
[投票内容]フィールドに選択した投票内容が表示されますので修正します。
[Update]ボタンを押下します。

投票内容の削除

登録した投票の右のゴミ箱アイコンから削除します。

構成サービス

Amazon S3

静的なHTML, CSS, JavaScript、画像を配置して配信しています。

Amazon Cognito

ユーザープールを使って、ユーザーの登録、ログイン認証を行っています。
API GatewayのCognitoオーサライザーを使用してログイン認証していないとAPIが実行できないようにしています。

Amazon DynamoDB

投票内容をユーザーごとに保存しています。

Amazon API Gateway

JavaScriptから呼び出すREST APIをセットアップしています。
それぞれLambdaを呼び出しています。
設定はこちらのSwaggerを参考にしていただけますと幸いです。
(長くなるのでOPTIONSは割愛しています)


swagger: "2.0"
info:
  version: "2020-07-12T12:12:55Z"
  title: "ZoomPollsAPI"
host: "abcdefghi.execute-api.us-east-2.amazonaws.com"
basePath: "/v1"
schemes:
- "https"
paths:

/polls:
get: consumes: - "application/json" produces: - "application/json" responses: 200:
description: "200 response" schema:
$ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" security: - ZoomTool: []
x-amazon-apigateway-integration: uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:list_poll/invocations" responses: default: statusCode: "200" responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates: application/json: "{\n \"userId\": \"$context.authorizer.claims.sub\"\ \ \n}" passthroughBehavior: "when_no_templates" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws" post: consumes: - "application/json" produces: - "application/json" parameters: - in: "body" name: "NoteCreateModel" required: true schema:
$ref: "#/definitions/NoteCreateModel" responses:
200:
description: "200 response" schema:
$ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" security: - ZoomTool: []
x-amazon-apigateway-request-validator: "Validate body" x-amazon-apigateway-integration: uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:create_update_poll/invocations" responses: default: statusCode: "200" responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates:
application/json: "{\r\n \"userId\": \"$context.authorizer.claims.sub\"\ ,\r\n \"noteId\": $input.json('$.noteId'),\r\n \"note\": $input.json('$.note'),\r\ \n \"pollname\": $input.json('$.pollname')\r\n}" passthroughBehavior: "when_no_templates" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws"
/polls/addpoll: post: consumes: - "application/json" produces: - "application/json" responses: 200:
description: "200 response" schema:
$ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" security: - ZoomTool: []
x-amazon-apigateway-integration: uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:add_poll/invocations" responses: default: statusCode: "200" responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates:
application/json: "{\r\n \"note\": $input.json('$.note'),\r\n \"mtgid\"\ : $input.json('$.mtgid'),\r\n \"company\": \"trainocate\"\r\n}" passthroughBehavior: "when_no_templates" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws"
/polls/zoompolls: get: consumes: - "application/json" produces: - "application/json" parameters: - name: "mtgid" in: "query" required: false type: "string" responses: 200:
description: "200 response" schema:
$ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" security: - ZoomTool: []
x-amazon-apigateway-integration: uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:get_poll/invocations" responses: default: statusCode: "200" responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates:
application/json: "{\r\n \"mtgid\": \"$input.params(\"mtgid\")\",\r\n\ \ \"company\": \"trainocate\"\r\n}" passthroughBehavior: "when_no_templates" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws" /polls/{id}: delete: consumes: - "application/json" produces: - "application/json" parameters: - name: "id" in: "path" required: true type: "string" responses: 200:
description: "200 response" schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: "string" security: - ZoomTool: []
x-amazon-apigateway-integration: uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:123456789012:function:delete_poll/invocations" responses: default: statusCode: "200" responseParameters: method.response.header.Access-Control-Allow-Origin: "'*'" requestTemplates:
application/json: "{\r\n \"userId\": \"$context.authorizer.claims.sub\"\ ,\r\n \"noteId\": \"$input.params('id')\"\r\n}" passthroughBehavior: "when_no_templates" httpMethod: "POST" contentHandling: "CONVERT_TO_TEXT" type: "aws" securityDefinitions: ZoomTool: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "cognito_user_pools" x-amazon-apigateway-authorizer: providerARNs: - "arn:aws:cognito-idp:us-east-2:123456789012:userpool/us-east-2_niM1Ff0ZV" type: "cognito_user_pools" definitions: Empty: type: "object" title: "Empty Schema" NoteCreateModel: type: "object" required: - "note" properties: note: type: "string" noteId: type: "string" title: "Note Create Model" x-amazon-apigateway-request-validators:
Validate body:
validateRequestParameters: false validateRequestBody: true

 

AWS Lambda

ランタイムはPython3.8です。
コードを記載します。
ロギング、デバッグコードは割愛します。

DynamoDB PUT


import boto3
from botocore.exceptions import ClientError def lambda_handler (event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('zoompoll')
try:
response = table.put_item(
Item={
'userId': event["userId"],
'pollId': event["pollId"],
'poll': event["poll"],
'pollname': event.get('pollname', '')
}
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
if response['ResponseMetadata']['HTTPStatusCode'] == 200: return event["pollId"]
else:
return ""

DynamoDB Query


import boto3
from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError
def lambda_handler (event, context): dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('zoompoll')
try:
response = table.query(KeyConditionExpression=Key("userId").eq(event["userId"]))
except ClientError as e:
print(e.response['Error']['Message'])
else:
return response["Items"]

DynamoDB Delete


import boto3
from botocore.exceptions import ClientError def lambda_handler (event, context): dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('zoompoll')
try:
response = table.delete_item(
Key={
'userId': event["userId"],
'pollId': event["pollId"]
}
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
if response['ResponseMetadata']['HTTPStatusCode'] == 200: return event["pollId"]
else:
return ""

Zoom GET

Zoom APIの認証情報は、Secrets Managerで安全に管理しています。


import os
import json
import requests
import boto3
import base64
from botocore.exceptions import ClientError
secret_name = os.environ.get('SECRET_NAME')
def get_secret (): session = boto3.session.Session()
client = session.client(
service_name='secretsmanager' )
try:
secret_id = secret_name
get_secret_value_response = client.get_secret_value( SecretId=secret_id
)
except ClientError as e:
raise e
else:
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString'] else: secret = base64.b64decode(get_secret_value_response['SecretBinary']) return secret
def lambda_handler (event, context): try:
secret = json.loads(get_secret())
mtg_id = event['mtgid'].replace('-', '').replace(' ', '') url = 'https://api.zoom.us/v2/meetings/{meetingId}/polls'.format( meetingId=mtg_id
)
headers = {
'Authorization': 'Bearer {token}'.format(
token=secret['token']
),
'Accept': 'application/json, application/xml',
'Content-Type': 'application/json' }
response = requests.get(
url,
headers=headers
)
if response.status_code == 200:
polls = json.loads(response.text)
if ('total_records' in polls):
del polls['total_records']
for poll in polls['polls']:
if ('id' in poll):
del poll['id']
if ('status' in poll):
del poll['status']
return polls
else:
return ''
except Exception as e: raise e
 

Zoom POST


import os
import json
import requests
import boto3
import base64
from botocore.exceptions import ClientError
secret_name = os.environ.get('SECRET_NAME')
def get_secret (): session = boto3.session.Session() client = session.client( service_name='secretsmanager' ) try:
get_secret_value_response = client.get_secret_value( SecretId=secret_name
)
except ClientError as e:
raise e
else:
if 'SecretString' in get_secret_value_response: secret = get_secret_value_response['SecretString'] else:
secret = base64.b64decode(get_secret_value_response['SecretBinary']) return secret
def lambda_handler (event, context):
try:
secret = json.loads(get_secret())
mtg_id = event['mtgid'].replace('-', '').replace(' ', '') polls = json.loads(event['poll'].replace('\n', '')) url = 'https://api.zoom.us/v2/meetings/{meetingId}/polls'.format( meetingId=mtg_id
)
headers = {
'Authorization': 'Bearer {token}'.format(
token=secret['token']
),
'Accept': 'application/json, application/xml', 'Content-Type': 'application/json' }
for poll in polls['polls']:
response = requests.post(
url,
data=json.dumps(poll),
headers=headers
)
except Exception as e:
raise e

 

山下 光洋(やました みつひろ)

トレノケート株式会社 講師。AWS Authorized Instructor Champion / AWS認定インストラクター(AAI) / AWS 認定ソリューションアーキテクト - プロフェッショナル /AWS認定DevOpsエンジニア - プロフェッショナル / AWS 認定デベロッパー - アソシエイト / AWS 認定 SysOps アドミニストレーター - アソシエイト / AWS 認定クラウドプラクティショナー / kintone認定 カスタマイズスペシャリスト他。AWS認定インストラクターとしてAWS認定コースを実施。毎年1,500名以上に受講いただいている。AWS 認定インストラクターアワード2018, 2019を日本で唯一受賞。著書『AWSではじめるLinux入門ガイド』(マイナビ出版社)。共著書『AWS認定試験対策 AWS クラウドプラクティショナー』(SBクリエイティブ社)。前職では2016年にAWS Summitにパネラーとして参加。その前はLotus Technical Award 2009 for Best Architectとして表彰されている。また、各コミュニティの運営にも個人的に関わり、勉強会にてスピーカーや参加をしている。

無料ダウンロード

オススメコンテンツ

オススメ記事

プロジェクトマネジメント PMP AWS ビジネススキル Microsoft PMBOKⓇ 田中淳子 IT資格 人材育成 山下光洋 AMA Azure コミュニケーション 人材開発用語集 PMBOK®ガイド入門 クラウド ITスキル 新入社員 横山哲也 PMP試験問題に挑戦 re:Invent セキュリティ 人材育成応援ラジオ DX Cisco PMBOKⓇガイド 第6版 試験体験記 イベント・セミナー PMBOK®ガイド第6版の変更点 人材開発 CCIE CCNA テレワーク ネットワーク リモートワーク 研修 AI(人工知能) GCP PMP(R)試験問題 第6版対応 PMP合格体験記 Windows Server AWS_Q&A Active Directory IT人材 IT資格解説 アセスメント デジタルビジネス ヒューマンスキル リーダーシップ 人気コースランキング 大喜利 部下の育成 Conversations PMの心得 キャリア グローバル人材 新入社員研修といえば DX人材育成 IoT OJT reinvent2022 CCNP Security Windows PowerShell クリエイティビティ プログラミング リスキリング 人材トレンド 試験対策問題 PMP試験対策一問一答 コーチング プロジェクト プロトタイプビルダー 1on1 AWS_DiscoveryDay AWSトレーニングイベント GCP無料セミナー Google Cloud Google Cloud Platform G検定 ITインフラ oVice アワード クリティカルシンキング サンプル問題 ステークホルダー ダイバーシティ ディープラーニング ワーケーション 自律 試験Tips AI人材 Linux PMI Power Platform Python Teams Web会議 kintone アイデア