LoginSignup
3
3

More than 1 year has passed since last update.

ZoomAPI(Server-to-server OAuth)でミーティング作成の自動化

Last updated at Posted at 2022-07-07

バヅクリCTOの合原です。
ひさびさにqiita投稿です。

いいタイミングでこちらのキャンペーンも開催していたので、相乗りさせていただきました :sweat_smile:
https://qiita.com/official-campaigns/engineer-festa/2022

やったこと

今回は、Railsアプリケーションからの利用を想定していたため、
Server-to-server OAuthアプリにて、実装Tryしてみました。
※Server-to-server OAuthの事例があまり見つからなかったのもあったので w

--

背景(こちらは読み飛ばしていただいても:smiley_cat::ok_hand:

当社の主力事業バヅクリでは、オンラインでのチームビルディングや研修をアソビを取り入れた、非常にユニークなプログラムを用意して、サービスとして提供しています。その際、Zoomミーティング機能を主に活用しています。

詳細は割愛しますが、これまで、イベント開催が確定したタイミングで
イベント運営担当者が、

  • 手動でZoom ミーティングを作成
  • 作成ZoommミーティングURLを社内管理サイトへ登録

ということが手動で行われていましたが、

  • 効率が悪い
  • ヒューマンエラーも起きやすい :cry:

ので、

  1. zoomミーティングの作成の自動化
  2. 当社DBへ上記ミーティング情報の登録

することで、リスクをZEROにすることができないかと。。。
つまり、zoom APIを利用して、ZoomMeetingの作成を自動化できないか?と検証してみました。

調査

何はともあれ、まずはAPIについて調査です。
ググってみると、まさに、
https://qiita.com/yosuke-sawamura/items/fa036273a161b190478b
こちらの神がかった記事があり、参考にさせていただきました。

あとは、セオリー通り公式ドキュメントを読み読み :pencil:

  • Oauthを使うこと(access_token発行)
  • access_tokenを用いて、APIリクエスト

と、ざっくり概要を理解。

Server-to-server OAuth作成

基本的には、こちらを参考に。。。

使用するための設定をポチポチと :mouse:

※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

※秘匿情報なので然るべき管理をお忘れなく :sweat_smile: :star2:

あとは、このaccess_tokenを用いて、APIリクエストをすればいいので、
実装しつつ確認へ。

設計

さて、Railsでどうやるか。まずは設計です。
moduleを適切に用いて、わかりやすく。。。
かつ、ZoomAPIは、豊富にあるので、各種APIごとに使い分けられるように。。。

ということを踏まえ。
zoomsという名前空間に下記のような継承関係のある形で設計。

名前空間の命名は複数形にしておくと、衝突が防げます:star2:

$ ~/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への保存までこちらで。

技術検証用にと、、、若干設計が荒削りですが、、まぁご勘弁ください :sweat:

実装

app/models/zooms/base.rb
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
app/models/zooms/meeting.rb
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


app/models/zooms/meetings/event.rb
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オブジェクトは当社オリジナルのものです :sweat:

app/models/zooms/app.rb
class Zooms::App < ApplicationRecord
  validates :access_token, :token_type, :expires_in, :scope, presence: true
end
app/models/events/zoom.rb
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

↑雑ですが :sweat:

実行

こうして、あとは、Rails consoleで、

$ Events::Zoom.new(event: e).create! 

※実際には、active jobを使ってバックグランド実行しています

として、無事、対象のzoomアカウントでリクエストした内容で、ミーティングができました :sunglasses:

スクリーンショット 2022-07-07 12.28.01 (1).png

あとは、現行のイベント確定時の処理に組み込めば、
イベント開催確定時のZoomミーティングの作成の自動化が、できそうなことが十分検証できました。

所感

ZoomAPIの利用は初めてでしたが、とてもドキュメントがわかりやすく、充実していて、
詰まる点がなく、検証ができました。

今回利用したAPIでは、Zoomミーティング作成時にレコーディングONなど、さまざまなオプションも設定できるようなので、うちのプログラムに応じて自動で設定することもでき、より、人的作業の効率化ができそうです。

今後は、ミーティング作成に限らず、他のAPIやSDKも使いつつ、自動化したり、面白いZoomの使い方をビジネスサイドに提案していきたいと思います :sunglasses:

恒例のやつですが :sweat:

当社では、バックエンドサーバーとして主にRails、フロントエンドでは、Nextjsを利用しています。
新しい物(コト)好きの当社では、絶賛、エンジニアを募集中です!

気になる方はお気軽にご連絡いただけましたらと思います!

スクリーンショット 2022-08-02 13.25.03.png

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3