IT・ビジネススキル・人材育成の情報を掲載 | 人材育成のトレノケート【公式ブログ】

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研修(AWS認定トレーニング)

トレノケートのAWS認定トレーニングでは、AWS社の厳格なテクニカルスキル及びティーチングスキルチェックに合格した認定トレーナーがコースを担当します。AWS初心者向けの研修や、AWS認定資格を目指す人向けの研修をご提供し、皆様のAWS知識修得のサポートをいたします。
トレノケートのAWS研修(AWS認定トレーニング)はこちら


▼AWS初心者の方は、 AWS Cloud Practitioner Essentialsから!
座学中心の研修で、AWSを初めて学ぶ方や、営業などで提案に関わる方におすすめです。
「AWS Certified Cloud Practitioner」資格取得を目指す方の基礎知識修得にも最適です。
→ AWS Cloud Practitioner Essentials 詳細・日程はこちらから