バヅクリCTOの合原です。
ひさびさにqiita投稿です。
いいタイミングでこちらのキャンペーンも開催していたので、相乗りさせていただきました
https://qiita.com/official-campaigns/engineer-festa/2022
やったこと
今回は、Railsアプリケーションからの利用を想定していたため、
Server-to-server OAuthアプリにて、実装Tryしてみました。
※Server-to-server OAuthの事例があまり見つからなかったのもあったので w
--
背景(こちらは読み飛ばしていただいても )
当社の主力事業バヅクリでは、オンラインでのチームビルディングや研修をアソビを取り入れた、非常にユニークなプログラムを用意して、サービスとして提供しています。その際、Zoomミーティング機能を主に活用しています。
詳細は割愛しますが、これまで、イベント開催が確定したタイミングで
イベント運営担当者が、
- 手動でZoom ミーティングを作成
- 作成ZoommミーティングURLを社内管理サイトへ登録
ということが手動で
行われていましたが、
- 効率が悪い
- ヒューマンエラーも起きやすい
ので、
- zoomミーティングの作成の自動化
- 当社DBへ上記ミーティング情報の登録
することで、リスクをZEROにすることができないかと。。。
つまり、zoom APIを利用して、ZoomMeetingの作成を自動化できないか?と検証してみました。
調査
何はともあれ、まずはAPIについて調査です。
ググってみると、まさに、
https://qiita.com/yosuke-sawamura/items/fa036273a161b190478b
こちらの神がかった記事があり、参考にさせていただきました。
あとは、セオリー通り公式ドキュメントを読み読み
- Oauthを使うこと(access_token発行)
- access_tokenを用いて、APIリクエスト
と、ざっくり概要を理解。
Server-to-server OAuth作成
基本的には、こちらを参考に。。。
使用するための設定をポチポチと
※scopeについても、ドキュメントを参考に設定します。
Scopes: meeting:write:admin, meeting:write
まずは、curlでaccess_token取得試す
とてもドキュメントがわかりやすく、こちらを参考に。
credentials情報を元に、
curlコマンドでaccess_token取得、
$ curl -X POST -H 'Authorization: Basic Base64Encoder(clientId:clientSecret)' https://zoom.us/oauth/token?grant_type=account_credentials&account_id={accountId}
下記は、こちらの通り、アプリを作成すれば取得可能です。
- clientId
- clientSecret
- account_id
※秘匿情報なので然るべき管理をお忘れなく
あとは、このaccess_tokenを用いて、APIリクエストをすればいいので、
実装しつつ確認へ。
設計
さて、Railsでどうやるか。まずは設計です。
moduleを適切に用いて、わかりやすく。。。
かつ、ZoomAPIは、豊富にあるので、各種APIごとに使い分けられるように。。。
ということを踏まえ。
zoomsという名前空間に下記のような継承関係のある形で設計。
名前空間の命名は複数形にしておくと、衝突が防げます
$ ~/work/buzzkuri (feat_zooms_modules*) » tree -L 2 app/models/zooms
app/models/zooms
├── app.rb # access_token 管理 ※ApplicationRecordを継承
├── base.rb # access_token の(再)発行、API周りの情報を一元化
├── meeting.rb # 下記eventモデルをこちらで、対象アカウント分キックする
└── meetings
└── event.rb # ZoomMeetingAPIでMeeting作成し、DBへの保存までこちらで。
技術検証用にと、、、若干設計が荒削りですが、、まぁご勘弁ください
実装
class Zooms::Base
class GetAccessTokenApitError < StandardError; end
TIME_ZONE = 'Asia/Tokyo'.freeze
require "net/http"
require "base64"
def access_token
@access_token ||= if token_available?
latest_app.access_token
else
create_access_token!
::Zooms::App.last.access_token
end
end
private
def create_access_token!
response = token_request
body = JSON.parse(response.body)
if response.code == "200"
::Zooms::App.create!(body)
else
#FIXME: Errorの場合のresponseが不明
raise GetAccessTokenApitError, body.message
end
end
def token_available?
latest_app && latest_app.created_at > Time.current.ago(1.hour)
end
def latest_app
@latest_app ||= ::Zooms::App.last
end
def token_request
uri = URI.parse("https://zoom.us/oauth/token")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme === "https"
http.post(uri.path, token_request_query.to_param, token_request_headers)
end
def token_request_query
{
grant_type: "account_credentials",
account_id: account_id,
}
end
def token_request_headers
{ "authorization" => "Basic #{credentials}" }
end
def credentials
Base64.strict_encode64("#{client_id}:#{client_secret}")
end
def account_id
Rails.application.credentials.zoom[Rails.env.to_sym][:api][:account_id]
end
def client_id
Rails.application.credentials.zoom[Rails.env.to_sym][:api][:client_id]
end
def client_secret
Rails.application.credentials.zoom[Rails.env.to_sym][:api][:client_secret]
end
end
class Zooms::Meeting < Zooms::Base
class CreateApiError < StandardError; end
private
def headers
{
Authorization: "Bearer #{access_token}",
"Content-Type": "application/json"
}
end
def api_request_url
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
end
def post_api_request
uri = URI.parse(api_request_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
http.post(uri.path, request_body.to_json, headers)
end
def request_body
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
end
end
class Zooms::Meetings::Event < ::Zooms::Meeting
include ActiveModel::Model
ATTRS = %i[
event
zoom_account
].freeze
ATTRS.each do |a|
attr_accessor a
validates a, presence: true
end
validate :ensure_unready_zoom
validate :ensure_zoom_account
def create!
return raise ArgumentError, errors.full_messages.join(", ") if invalid?
response = post_api_request
body = JSON.parse(response.body)
raise CreateApiError, "#{body['code']}: #{body['message']}" unless response.code == "201"
ActiveRecord::Base.transaction do
event.zoom_url = body["join_url"].split("?").first
event.zoom_meeting_id = body["id"]
event.zoom_passcode = body["password"]
event.event_zoom_accounts.build(zoom_account: zoom_account)
event.save!
end
end
private
def api_request_url
"https://api.zoom.us/v2/users/#{zoom_account.name}/meetings"
end
def request_body
# https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/meetingCreate
{
topic: topic,
type: "2",
start_time: start_time, # yyyy-MM-ddTHH:mm:ss
timezone: ::Zooms::Base::TIME_ZONE,
duration: duration, # 所要時間
password: password,
settings: {
alternative_hosts: alternative_hosts
}
}
end
def start_time
event.start_time.strftime("%Y-%m-%dT%H:%M:%S")
end
def duration
e = event
Integer(e.end_time - e.start_time) / 60
end
def password
random = Random.new
random.rand(9_999_999_999)
end
def ensure_unready_zoom
errors.add :event, " DONT need settings zoom" if event.use_zoom?
end
def topic
"#{!Rails.env.production? ? "[#{Rails.env}]" : ''}【バヅクリ】#{event.membership_business.name}様 #{event.name}"
end
def alternative_hosts
service_managers.join(";")
end
def service_managers
%w[
rintaro.shimoyoshi@buzzkuri.co.jp
miku.araya@buzzkuri.co.jp
yumi.aihara@buzzkuri.co.jp
]
end
def ensure_zoom_account
errors.add :zoom_account, :invalid unless ::ZoomAccount.exists? id: zoom_account&.id
end
end
※injectしているeventオブジェクトは当社オリジナルのものです
class Zooms::App < ApplicationRecord
validates :access_token, :token_type, :expires_in, :scope, presence: true
end
class Events::Zoom
include ActiveModel::Model
ATTRS = %i[
event
zoom_accounts
].freeze
ATTRS.each do |a|
attr_accessor a
validates a, presence: true
end
def create!
return Rails.logger.info "#{self.class.name}: Not found available zoom account!" if creatable_accounts.empty?
creatable_accounts.each do |a|
Zooms::Meetings::Event.new(
event: event,
zoom_account: a
).create!
end
rescue StandardError => e
Rails.logger.info "#{self.class.name}: #{e.message}!"
end
:
end
↑雑ですが
実行
こうして、あとは、Rails consoleで、
$ Events::Zoom.new(event: e).create!
※実際には、active jobを使ってバックグランド実行しています
として、無事、対象のzoomアカウントでリクエストした内容で、ミーティングができました
あとは、現行のイベント確定時の処理に組み込めば、
イベント開催確定時のZoomミーティングの作成の自動化が、できそうなことが十分検証できました。
所感
ZoomAPIの利用は初めてでしたが、とてもドキュメントがわかりやすく、充実していて、
詰まる点がなく、検証ができました。
今回利用したAPIでは、Zoomミーティング作成時にレコーディングONなど、さまざまなオプションも設定できるようなので、うちのプログラムに応じて自動で設定することもでき、より、人的作業の効率化ができそうです。
今後は、ミーティング作成に限らず、他のAPIやSDKも使いつつ、自動化したり、面白いZoomの使い方をビジネスサイドに提案していきたいと思います
恒例のやつですが
当社では、バックエンドサーバーとして主にRails、フロントエンドでは、Nextjsを利用しています。
新しい物(コト)好きの当社では、絶賛、エンジニアを募集中です!
気になる方はお気軽にご連絡いただけましたらと思います!