Zoom APIとLaravelを使って自動ミーティング作成フォームを構築する。
技術スタック LaravelZOOM

Zoom APIとLaravelを使って自動ミーティング作成フォームを構築する。

2021.05.04 2021.01.10

こんにちはJuneです。年明け早々コロナが本気出してきて、ますます外に出れない日々が続きます。まあエンジニアは家にいてもプログラムでいろんなもの作れるのでいい暇つぶしになります。そこでコロナで株が爆上がりのzoomにAPIがあるということを知って、早速使ってみました。会社でも「zoom APIには金の匂いがする!」とみんなで盛り上がったので是非探索してみます。

zoom API自体は2018年ごろから出ていたらしく、現在はv2がリリースされています。ドキュメントを一通り読んだ所、zoomで行えることは一通りAPIを通じて行えるそうです。zoom APIについてわかった箇所を詳細に説明したいですが、この記事では実際の活用例を説明したいので部分省略します。

しかし、zoom APIを使用する認証フローや仕組みについては簡単に解説します。その説明から始めますので、「webアプリのはよ動き見せろや!」「そんなの知っとんじゃ!」という方は「Laravelを立ち上げる」から読み始めてください。

Zoom APIの概要

詳しくはZoom API レファランスを見ればわかりますが、zoomのGUIでできることは基本的に可能です。ミーティングを作ったり、ウェビナーを開催したり、ユーザー情報を取ったり、開催中のzoomに対してチャットを送るなどなんでもできます。他にもwebhookや独自のコードでブラウザ上に映像・音声を出力できるSDKやエンドポイントもあるみたいです。

これらのAPIはRestAPIであり、特定のルートに対してGET/POST/PUT/DELTEでリクエストし、付属の情報はGETパラメータやPOSTパラメータで送信します。

参考:https://marketplace.zoom.us/docs/api-reference/introduction

認証方法

zoom APIにアクセスするには、JWTとOAuth2.0が使用できます。二者の違いはアクセスできる機能の量とマーケットプレイスに公開できるかが主になります。

JWT

ます。JSONで認証情報をやりとりします。

JWTによる認証はOAuthで使用できる権限範囲より狭く、自分自身で簡単にライトに使用したい場合に使うらしいです。また開発したzoom アプリはマーケットプレイスを通じて公開することができますが、JWT認証の場合はその公開ができません。

独自のwebアプリとZoomを連携させたい場合は次のOAuth2.0認証をお勧めします。

参考:https://marketplace.zoom.us/docs/api-reference/using-zoom-apis#using-jwt

OAuth 2.0

OAuthは自身のwebサービスの資格情報を第三者のサービスへ提供する際に使用される認証フローです。ここでいう「webサービス」は「zoom」で「第三者のサービス」は「私のLaravelアプリ」です。

つまりOAuth認証を用いることで「私のLaravelアプリ」は連携させた人の「zoom」の資格情報を利用できる様になります。よって「私のLaravelアプリ」は連携させた人のzoomのミーティング情報を読み取ったり、作成したり、ユーザー情報を取得することができます。

資格情報を与え、操作権限も付与するのでかなり厳重な認証システムが必要となりそこで、秘密鍵や特定のプロトコルなどを用いたOAuthが使用されます。上記のJWTよりセキュアであり、また様々なAPIを利用できる様になります。

OAuthで実装したzoom APIはマーケットプレイスに出して公開することができます。今回の説明ではこのOAuth認証をLaravelを用いて実装していきます。まだ認証・連携のイメージがつかないと思いますが、読んでいくうちにわかると思います(多分)。

参考:https://marketplace.zoom.us/docs/api-reference/using-zoom-apis#using-oauth

連携の流れ

OAuthでの認証は以下の様に行われます。

もう少し具体的に解説すると

  1. ユーザー(zoomにログイン済み)を認証画面へリダイレクトさせる
  2. 確認後、ユーザーが承認したという証であるcodeを取得。
  3. OAuthプロトコルに従い作成した認証キーとリクエストをzoomに送る
  4. アクセストークンを得る。そのアクセストークン でzoom APIにアクセスする。アクセストークン などはアプリのDBに保存しておく。

こちらも後で実際の画面のスクショ付きで解説しますので、今は「ヘェ〜」程度の理解で大丈夫です。

APIへのアクセス方法

OAuthでアクセストークン を取得したら、そのトークンをリクエストヘッダーに仕込んでAPIにアクセスします。

今回作るアプリ

まずアプリの機能と概要を説明しておきます。今回作るアプリは「匿名ユーザーが入力したフォームの内容に応じてzoomミーティングが作成され、それを管理できるwebアプリ」です。見た目は以下の感じです。

アプリを管理し連携させるzoomアカウントを持っている「管理者」と、フォームに入力する「匿名ユーザー(お客さん)」が存在するとします。

機能の概要

場面としては「zoomでのご相談はこちら」的な会社用のフォームであると思ってください。最初にお客さんはフォームにて名前・アドレス、zoomの希望開始時間を入力します。

フォーム入力内容が正しければ、zoom APIを通じて指定の時間で始まるzoom ミーティングを連携先のアカウントで作成。

APIからのレスポンスよりミーティングURLを取得し、DBに保存すると共にお客さんへミーティング情報をメールで飛ばす。(メールはローカルの環境でできなかったので、実装は割愛。ソースはあります。)

管理者は管理画面にて誰が・いつミーティングを開くのかを一覧で確認できる。またお客さんは時間変更用・削除用URLにアクセスして時間の変更・ミーティングのキャンセルが可能。

以上の様な機能を持たせたいと思います。とりあえずこれらの機能を実装する程度なので、厳密なバリデーションや細かい機能は割愛します。

開発環境

私が慣れているLaravel 6を用いて作成します。Laravelのインストール方法は省略します。一応こちらで作ったDocker開発環境を用いています。またzoomへのHTTPアクセスをよく行うので、PHPのHTTPライブラリであるguzzlehttpをインストールしてください。

また、以下詳細な開発環境情報です。開発したリポジトリも開放してますのでぜひどうぞ。

  • MacOS Catalina 10.15.5
  • Docker 20.10.0
  • Docker-compose 1.27.4
  • (コンテナ内)composer 1.10.19
  • (コンテナ内)Laravel 6.20
  • (コンテナ内)guzzlehttp 7.2
  • (コンテナ内)centod 8
  • (コンテナ内)httpd 2.4.37
  • (コンテナ内)php 7.4.7
  • (コンテナ内・公式イメージ)mysql:5.7

Zoom APIキーを手に入れる

zoom アプリの作成

それでは初めていきましょう。Laravelを実装する前に自身のZoomアカウントにて、zoomアプリを作成していきましょう。zoom マーケットプレイスへ移動します。そして画面上部の「Develop」をクリックし「Build App」をクリックします。

するとzoomアプリを選択する以下の様な画面が表示されますので、「OAuth」の「Create」をクリック

「Create」を押すとアプリの名前などを入力するモーダルが出現します。任意の名前を入力し、アプリのタイプ、マーケットプレイスに公表するかを決定します。とりあえず私は以下の様にしました。

名前とマーケットプレイスへの公開は後から変更できます。ひとまず入力したら「Create」を押します。

諸所の設定を入力

アプリの認証情報

作成後にはアプリの管理画面に飛ばされると思います。そこから作成したアプリを選択して「App Credentials」を選択

Larvel側には 「Client ID」「Client Secret」 が必要となります。後で.envファイルに記載します。ちなみに 「Client Secret」 は絶対に外に漏れてはいけません。

「Redirect URL for OAuth」 にて承認画面からのリダイレクト先を指定しておきます。承認画面でアプリ連携を許可した際にはトークンなどが 「Redirect URL for OAuth」 あてへ送信され、ユーザーもリダイレクトされます。ここの値が異なっているとエラーで認証が進みません。

今は開発環境なのでドメインにlocalhostを指定しています。(私の環境ではlocalhost:9000でLaravelの画面が表示される様に設定しています。php artisan serveなどの場合はlocalhost:8000になると思いますので、ポートの指定に気をつけてください。)

「whitelist URL」はOAuthのリダイレクト先として許可するURLを指定できます。OAuthリダイレクトのURLに完全一致させるか、前方一致させる必要があります。設定したリダイレクト先はhttp://localhost:9000/zoomauth/checkとしていたので、その前方を含める様にhttp://localhost:9000/に設定しておきます。

アプリの公開情報

「Information」にて「Optional」と書かれている箇所以外を入力し記述します。

アプリのスコープ(アクセス範囲)

ここが結構重要です。「Scopes」という箇所ではアプリの操作権限、アクセス範囲を設定できます。ユーザー情報の取得やミーティングの作成にもそれぞれスコープが用意されて、スコープ外の操作へのアクセスは401と認証エラーとなります。「Add Scopes」でスコープを追加します。

よくわからなければ全部追加してもいいですが、「Meeting」「User」のスコープを全て追加しておけば今回のアプリの実装は可能です。

アプリのアクティベート

最後に「Activation」にて確認します。不足箇所は以下の様にオレンジ文字で指摘されるので直しましょう。全てが入力できていれば後は問題ありません。「Install」などは押さなくても問題ありません。

Lravelを立ち上げる(Docker)

それではLaravelを立ち上げましょう。インストールはされており、ユーザーテーブルのマイグレーションを行う前だと仮定します。

Clien ID と Client Secretを設定

Larvelのプロジェクトルートに.envという環境変数を記述するファイルがありますので、そちらに 「App Credentials」 で取得できるClient IDClient Secretを設定します。

.env
APP_NAME=Laravel
APP_ENV=local
...
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

ZOOM_CLIENT_ID=clientid
ZOOM_CLIENT_SECRET=clientsecret

.envを更新したらキャッシュをクリアして反映させます。

php artisan config:clear
php artisan cache:clear

.envに記述することで他のソースコード内でenv('ZOOM_CLIENT_ID')と行った形で出力できます。また.envは基本的に.gitignoreに登録されているので公開リポジトリに秘密鍵が載ってしまうという様な事故を防げます。

User tableをちょっと改造

今回のアプリの管理者は一人ですが、もし複数人に使用してもらいたい時に「ユーザーごとにトークンを分けたいな」と思ったのでその改造をします。初期で用意されているユーザーテーブルを以下の様に書き換えます。

database/migrations/2014_10_12_000000_create_users_table.php
public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
    
        //ここから追加
        $table->longText('zoom_code')->nullable()->default(null);
        $table->longText('access_token')->nullable()->default(null);
        $table->longText('refresh_token')->nullable()->default(null);
        $table->timestamp('zoom_expires_in', 0)->nullable()->default(null);
        $table->rememberToken();
        $table->timestamps();
    });
}

それぞれのカラムの説明はこの通り。

  • zoom_code:連携許可の際に得られる許可コード。
  • access_token:APIにアクセスするためのアクセストークン これを手にしたら勝ち。
  • refresh_token:access_tokenを更新するためのトークン。
  • zoom_expires_in:access_tokenの期限を記録しておく。APIにアクセスする前にこれでチェックする。

ユーザーごとのトークンが管理できる様になり、$user->auth()->access_tokenみたいな感じでトークンを使用できます。

それではマイグレーションをしましょう。(仮ユーザーのseedも忘れずに)

php artisan migrate --seed
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.02 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.01 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.01 seconds)
Seeding: UsersTableSeeder
Seeded:  UsersTableSeeder (0.06 seconds)
Database seeding completed successfully.

フォームと管理画面を適当に作る

詳しくはアップしたリポジトリを見てください。スタイルはbootstrapで調整しています。ルート情報だけ載せておきます。

/                   フォームを表示  
/confirm            フォームの受付完了確認画面
/admin              管理者用ページ
/login              ログインページ
/logout             ログアウトルート
/zoomoatuh/check    zoom OAuthリダイレクト先
/form/alter         ミーティングの時間変更画面
/form/delete        ミーティングのキャンセル画面

Lravel と ZoomのOatuh 2 認証・連携

管理画面から連携確認画面へ誘導とユーザー認証

それではLaravelとZoomの連携処理を実装していきます。連携処理はログインした管理者のアクセス配下で行います。管理画面は/adminです。/adminのコントローラーとビューは以下の通りです。

app/Http/Controllers/AdminController.php
public function index(Request $request){
    $user = auth()->user();
    $noZoomCode = $user->zoom_code == null; //連携を行っているか
    $zoomOuthLink = 'https://zoom.us/oauth/authorize?'.http_build_query([
        'response_type'=>'code',
        'redirect_uri'=>env('APP_URL').'/zoomoatuh/check',
        'client_id'=>env('ZOOM_CLIENT_ID'),
    ]);
    $oauthSuccess=false;
    $meetings = Meeting::all();

    return view('admin',compact('noZoomCode','zoomOuthLink','oauthSuccess','meetings'));
}
resources/views/admin.blade.php
@extends('layouts.layout')

@section('main-content')
    <div class="main-content">
        @if($noZoomCode)
        <div class="alert alert-danger mb-3" role="alert">
            <h4 class="alert-heading">Zoomとの連携が行われていません。</h4>
            <p>このシステムをご利用する場合、Zoomとの連携を行ってください。</p>
            <a href="{{$zoomOuthLink}}" class="btn btn-danger">Zoomと連携</a>
        </div>
        @else
        <h1>予約一覧</h1>
        @endif
    </div>
@endsection

管理画面では管理者がzoomと連携しているかで表示を変更しています。連携しているかはuserテーブルのzoom_codenullかで確認しています。

連携が済んでいない場合はzoomの連携確認画面へ飛ばすリンクボタンを表示させています。

この$zoomOuthLinkの作成はこちらのドキュメントにある通り、ルールがあります。

app/Http/Controllers/AdminController.php
$zoomOuthLink = 'https://zoom.us/oauth/authorize?'.http_build_query([
    'response_type'=>'code',
    'redirect_uri'=>env('APP_URL').'/zoomoatuh/check',
    'client_id'=>env('ZOOM_CLIENT_ID'),
]);

今はOAuthのステップの中で「ユーザー認証」というユーザーへ「このアプリ(Laravel)とzoomを連携してもいい?」とzoomが聞いている段階です。そのユーザー認証にはまずhttps://zoom.us/oauth/authorizeへGETで管理者本人がアクセスします。

その際にGETパラメータにresponse_typeredirect_uriclient_idを入力します。

response_typeAccess response type being requested. The supported authorization workflow requires the value `code`.

とある様にresponse_typeにはcodeという文字を設定します。そしてredirect_uriはzoom アプリ作成時にも設定した通りのURLを入力しますので、http://localhost:9000/zoomauth/checkを設定。client_id.envで設定値をenv()で呼び出します。

それらをGETパラメータとして一つのURLにまとめます。以下の様な感じです。

https://zoom.us/oauth/authorize?response_type=code&redirect_uri=http://localhost:9000/zoomauth/check&client_id=clientid

予めサーバーサイドで作っておき、ボタンのリンクにはめ込んでおきます。画面では以下の様に表示されます。

認証画面からのリダイレクトURLでの処理

ボタンをクリックすると以下の画面が表示されます。(正確には承認画面のGETを叩く)

管理者に対してこのアプリが自身のzoomアカウントに対して、何をするのかが書かれています。管理者はこの「認可」を押すと、redirect_uriのリダイレクト先に飛ばされます。OAuthではこのリダイレクト先の処理が大切です!http://localhost:9000/zoomauth/checkのコントローラーは以下の通りです。(ビューはなし)

app/Http/Controllers/AdminController.php
public function zoomOauth(Request $request){
    $user = auth()->user();

    if($user->zoom_code==null){
        $code = $request['code'];

        $user->zoom_code = $code;
        $user->save();

        $basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));
        $client = new \GuzzleHttp\Client([
            'headers' => ['Authorization' => 'Basic '.$basic]
        ]);
        $res = $client->request('POST','https://zoom.us/oauth/token',[
            'query' => [
                'grant_type'=>'authorization_code',
                'code'=>$code,
                'redirect_uri'=>'http://localhost:9000/zoomoatuh/check'
            ]
        ]);
        $result = json_decode($res->getBody()->getContents());

        $user->access_token= $result->access_token;
        $user->refresh_token= $result->refresh_token;
        $unixTime = time();
        $user->zoom_expires_in= date("Y-m-d H:i:s",$unixTime+$result->expires_in);
        $user->save();

        return redirect()->route('amdin')->with([
            'noZoomCode'=>false,
            'oauthSuccess'=>true
        ]);
    }
}

https://zoom.us/oauth/authorize から http://localhost:9000/zoomauth/check へリダイレクトされると自動的にGETパラメータに?code=~~~~というものが付与されています。このcodeは後の認証に必要になります。

リダイレクトURLからcodeの値を取得します。いったんDBに保存してから、実際にAPIへリクエストするのに必要なアクセストークンの取得処理を行います。そこで以下の様なリクエストを行います。

app/Http/Controllers/AdminController.php
$basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));
$client = new \GuzzleHttp\Client([
    'headers' => ['Authorization' => 'Basic '.$basic]
]);
$res = $client->request('POST','https://zoom.us/oauth/token',[
    'query' => [
        'grant_type'=>'authorization_code',
        'code'=>$code,
        'redirect_uri'=>'http://localhost:9000/zoomoatuh/check'
    ]
]);

Zoomにも書いてある通りの処理ですが、アクセストークンを得る https://zoom.us/oauth/token というルートにアクセスするときは、まずリクエストヘッダーを付与します。リクエストヘッダーは 'headers' => ['Authorization' => 'Basic '.$basic] です。ここに client IDとclient secretをコロンで付けて一つの文字列にし、それをbase64エンコードをします。つまり明示的に処理を表示すると以下の様な感じです。

client_id:cilent_secret //これで一行の文字列
↓
この値を64base encode
↓
si84nf7435934jdfsdfi... //エンコード化された文字。これを送る

そしてそれをリクエストヘッダーに付与します。'headers' => ['Authorization' => 'Basic '.$basic] これを文字列として表示すると、Authorization: Basic si84nf7435934jdfsdfi… みたいな感じです。ちなみに Basicとエンコード文字の間は半角が空いていますので注意。

リクエストヘッダーを付けたら先ほどと似た感じでGETパラメータを以下の様に設定します。

$res = $client->request('POST','https://zoom.us/oauth/token',[
    'query' => [
        'grant_type'=>'authorization_code',
        'code'=>$code,
        'redirect_uri'=>'http://localhost:9000/zoomoatuh/check'
    ]
])

grant_typeauthorization_codeという文字とし、codeにはリダイレクト時についてきた値である$request['code']を用います。redirect_uriは先ほどと同じです。(redirect_uriを別にすると認証が通りません!)

これでセットアップが完了です。実際のURLとしては以下の感じです。

https://zoom.us/oauth/token?grant_type=authorization_code&code=~~~~~&redirect_uri=http://localhost:9000/zoomoatuh/check
(そして直接は見えないですが、リクエストヘッダーには 「Authorization: Basic si84nf7435934jdfsdfi…」 という値がついています!

リクエストが成功するとアクセストークン を含んだレスポンスがJSONで戻ってきます。それを展開してDBへ保存します。zoom_expires_inは現在時刻と足し合わせて、期限切れ時刻を計算してから格納しています。

/*
$resultの中身の例
{
    "access_token": "eyJhbGciOiJIUz...",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJI..",
    "expires_in": 3599,
    "scope": "user:read"
}
*/

$result = json_decode($res->getBody()->getContents());

$user->access_token= $result->access_token;
$user->refresh_token= $result->refresh_token;
$unixTime = time();
$user->zoom_expires_in= date("Y-m-d H:i:s",$unixTime+$result->expires_in);
$user->save();

return redirect()->route('amdin')->with([
         'noZoomCode'=>false,
         'oauthSuccess'=>true
]);

そして最後は管理画面へリダイレクトしてあげます。管理者からしてみるとzoomの画面で「許可」を押すと元のサイトに戻って、グルグルローディングしてるなーと思ったら管理画面に戻ってきた感覚となります。実際の画面ではzoom連携の警告がなくなり以下の様な感じになります。

これでOAuthは完了です。意外と簡単ですね。access_tokenは1時間で切れてしまうので、APIアクセスの際は期限切れでないかをチェック、そしてダメならtokenをリフレッシュする機能が必要となります。次はaccess_tokenのチェック方ら連携した人のユーザー情報を取得するとともに、リフレッシュ機能を実装します。

ユーザー情報を取得

ミーティングを作成したりなどはユーザーIDが必要となります。他のAPIで使用するのでコントローラー内の共通メソッドとして分離しておきましょう。以下の様にします。

app/Http/Controllers/ZoomApiController.php
class ZoomApiController extends Controller
{
    //
    protected function me(){
        $user = auth()->user();
        $client = new \GuzzleHttp\Client([
            'headers' => ['Authorization' => 'Bearer '.$user->access_token]
        ]);
        $res = $client->request('GET','https://api.zoom.us/v2/users/me');
        $result = json_decode($res->getBody()->getContents());
        // dd($result);
        return $result;
    }
...
}

ユーザーテーブルにaccess_tokenがあるので$user = auth()->user();で現在のログインユーザーを取り出して、$user->access_tokenにて出力します。

access_tokenがあればAPIへのアクセスはリクエストヘッダーにトークンを入れるだけでアクセスできます。リクエストヘッダーは'headers' => ['Authorization' => 'Bearer '.$user->access_token]です。さっきはBasicだったのが、Bearer(ベアラー)になっていますのでタイポに注意。

dd($result)を有効にして出力してみると

こんな感じのJSONが返ってきますので、適宜IDなどを使用します。

リフレッシュ機能を実装

access tokenは1時間しか持たないのでもし期限切れになった際にはリフレッシュトークンを使用してトークンを更新します。ちなみにリフレッシュトークンの有効期限は15年です笑私の場合は以下の様に実装しました。

app/Http/Controllers/ZoomApiController.php
protected function checkRefresh(){
    $user = auth()->user();
    $token_expires =  new \DateTime($user->zoom_expires_in);
    $now = new \DateTime();

    if($now >= $token_expires){
        $basic = base64_encode(env('ZOOM_CLIENT_ID').':'.env('ZOOM_CLIENT_SECRET'));
        $client = new \GuzzleHttp\Client([
            'headers' => ['Authorization' => 'Basic '.$basic]
        ]);
        $res = $client->request('POST','https://zoom.us/oauth/token',[
            'query' => [
                'grant_type'=>'refresh_token',
                'refresh_token'=>$user->refresh_token
            ]
        ]);
        $result = json_decode($res->getBody()->getContents());

        $user->access_token= $result->access_token;
        $user->refresh_token= $result->access_token;
        $unixTime = time();
        $user->zoom_expires_in= date("Y-m-d H:i:s",$unixTime+$result->expires_in);
        $user->save();
        return $user;
    }
    return $user;
}

APIリクエストごとにトークンをチェックできる様にしています。有効期限をテーブルに保存してあるのでそれを比較して、現在時刻が有効期限を過ぎていたらリフレッシュ処理を行う様します。そして戻ってきたトークンをテーブルで更新させ、ユーザーモデルをreturnします。

有効期限ないであればそのままユーザーモデルを返却するという感じです。

APIからミーティングを作成

それではフォームから入力された値を元にミーティングを作れる様にしましょう。メール通知は機能してはいませんが、実装したコードはコメントアウトさせてますので、頑張れる人はメールも実装してみてください。

まずフォームから取得したミーティング情報を格納するテーブルを以下の様に定義して、マイグレーションを実施します。

フォーム&ミーティング管理テーブルを作成

database/migrations/2021_01_10_005145_create_meeting.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

class CreateMeeting extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('meeting', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('company_name');
            $table->string('email');
            $table->longText('content');

            $table->timestamp('start_at', 0)->default(DB::raw('CURRENT_TIMESTAMP'));
            $table->longText('hash');
            $table->boolean('is_canceled');

            $table->longText('zoom_meeting_id');
            $table->longText('zoom_join_url');
            $table->longText('zoom_start_url');
            $table->longText('zoom_password');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('meeting');
    }
}

start_atはお客さんが入力した希望zoom開催時間です。本来は管理側の都合を合わせた機能にすべきですが今回はプロトタイプなので割愛。フロントからはdatetime形式で来たものを受け取ります。

hashは後でお客さんがミーティングをキャンセルしたり、時間を変更するときにアクセスするURLに付けるランダムな文字列です。一種のパスワードみたいなものです。後ほど使い方を解説します。

そしてzoomへCreate Meeting API を送信すると、ミーティングURLなどが返ってきますのでそれをzoom_join_urlzoom_start_urlに入れておきます 。他にフォームの内容を格納する箇所を定義してマイグレーションします。

フォームの画面以下の様に実装しました。

バリデーションとmeeting apiをリクエスト

フォームのビューがPOSTリクエストを受けたら以下のコントローラーが実行されます。

app/Http/Controllers/ZoomApiController.php
public function createMeeting(Request $request){
    $validator = Validator::make($request->all(),[
        'email'=>'required|email:rfc',
        'yourname'=>'required',
        'companyname'=>'required',
        'startAt'=>'date|required',
        'content'=>'required|max:1000',
    ]);

    $error = $validator->getMessageBag()->toArray();
    
    //バリデーションエラーがあれば元の画面へ
    if ($validator->fails()) {
        return view('form',compact('error'));
    }
    
    $user = $this->checkRefresh();
    $user = auth()->user();

    $zoom_user = $this->me();

    $url = 'https://api.zoom.us/v2/users/'.$zoom_user->id.'/meetings';
    $client = new \GuzzleHttp\Client([
        'headers' => [
            'Authorization' => 'Bearer '.$user->access_token,
            'Content-Type'=>'application/json'
        ],
    ]);

    $topic = $request->companyname.' '.$request->yourname.'様 ご相談';
    $meeting_password = substr(base_convert(bin2hex(openssl_random_pseudo_bytes(9)),16,36),0,9);
    $res = $client->request('POST',$url,[
        \GuzzleHttp\RequestOptions::JSON => [
            'topic'=>$topic,
            'type'=>2,
            'start_time'=>$request->startAt,
            'password'=>$meeting_password
        ]
    ]);
    $result = json_decode($res->getBody()->getContents());

    $meeting = new Meeting();
    $meeting->name=$request->yourname;
    $meeting->company_name=$request->companyname;
    $meeting->email=$request->email;
    $meeting->content=$request->content;

    $start = new \DateTime($result->start_time);
    $meeting->start_at=$start;
    $meeting->hash=substr(base_convert(bin2hex(openssl_random_pseudo_bytes(64)),16,36),0,64);
    $meeting->is_canceled=false;

    $meeting->zoom_meeting_id=$result->id;
    $meeting->zoom_join_url=$result->join_url;
    $meeting->zoom_start_url=$result->start_url;
    $meeting->zoom_password=$result->password;
    $meeting->save();

    $format = $start->format('Y年m月d日 H時i分');
    // $meeting->start_at = $format;
    // $mail = new ContactMail($meeting);
    // Mail::to($request->email)->send($mail);


    return redirect('/confirm')->with([
        'form_id'=>$meeting->id,
        'name'=>$request->yourname,
        'companyname'=>$request->companyname,
        'content'=>$request->content,
        'start_time'=>$format
    ]);
}

長いですが、バリデーションからAPIのアクセスまで一通り行われています。

有効期限チェックとエンドポイントリクエストの作成

まずは最初の方では有効期限のチェックを行い、そしてミーティングを作成するユーザー情報を取得しています。

app/Http/Controllers/ZoomApiController.php
$user = $this->checkRefresh();
$user = auth()->user();

$zoom_user = $this->me();

$url = 'https://api.zoom.us/v2/users/'.$zoom_user->id.'/meetings';
$client = new \GuzzleHttp\Client([
    'headers' => [
        'Authorization' => 'Bearer '.$user->access_token,
        'Content-Type'=>'application/json'
    ],
]);

ミーティングの作成を行うエンドポイントは https://api.zoom.us/v2/users/{zoom_user_id}/meetingsです。{zoom_user_id}にはmeで取得したuser_id(連携したzoomアカウントのuser id)を挿入します。そしてリクエストヘッダーを付けてひとまず、GuzzleHttpのインスタンスを作成します。

ミーティングパスワードを設定してAPIへリクエスト

app/Http/Controllers/ZoomApiController.php
$topic = $request->companyname.' '.$request->yourname.'様 ご相談';        
$meeting_password = substr(base_convert(bin2hex(openssl_random_pseudo_bytes(9)),16,36),0,9);
$res = $client->request('POST',$url,[
    \GuzzleHttp\RequestOptions::JSON => [
        'topic'=>$topic,
        'type'=>2,
        'start_time'=>$request->startAt,
        'password'=>$meeting_password
    ]
]);
$result = json_decode($res->getBody()->getContents());

お客さんに入力してもらう様のパスワードを生成し、そして指定したエンドポイントへPOSTします。ここでPOSTパラメーター内にミーティング設定情報をJSONで記入します。どんな値が設定できるかはここで確認できます。

上手く想像できない方は以下のGUIで行うzoom画面を参考にするといいです

ここで入力できる値は全て、APIでも入力できますのでリファランスでフォーマットなど確認しながら自分なりの設定をしましょう。私の場合、まずトピックを「{会社名} {客名様} ご相談」として必ず定義しており、そこは'topic'=>$topic,と定義してます。

start_timeは開催日時であり、フォームで入力された値を入れています。passwordは念のため付与しています。そしてAPIをリクエストします。

API処理終了後

app/Http/Controllers/ZoomApiController.php
$result = json_decode($res->getBody()->getContents());

$meeting = new Meeting();
$meeting->name=$request->yourname;
$meeting->company_name=$request->companyname;
$meeting->email=$request->email;
$meeting->content=$request->content;

$start = new \DateTime($result->start_time);
$meeting->start_at=$start;
$meeting->hash=substr(base_convert(bin2hex(openssl_random_pseudo_bytes(64)),16,36),0,64);
$meeting->is_canceled=false;

$meeting->zoom_meeting_id=$result->id;
$meeting->zoom_join_url=$result->join_url;
$meeting->zoom_start_url=$result->start_url;
$meeting->zoom_password=$result->password;
$meeting->save();

$format = $start->format('Y年m月d日 H時i分');
// $meeting->start_at = $format;
// $mail = new ContactMail($meeting);
// Mail::to($request->email)->send($mail);


return redirect('/confirm')->with([
    'form_id'=>$meeting->id,
    'name'=>$request->yourname,
    'companyname'=>$request->companyname,
    'content'=>$request->content,
    'start_time'=>$format
]);

$resultにzoomからのレスポンスがあるので適宜Meetingモデルやメールに格納します。そして最後にユーザーは確認画面が表示されます。

自分のアカウントで実験

では実験してみます。連携した管理者のzoomアカウントでミーティング一覧をみています。リクエスト前はこの様に何もありません。

そこでフォームにこの様に入力していきます。(今回は管理者自身が入力)

そして送信を押すとちょっとロードして、こちらの画面にリダイレクトされます。

そして先ほどのzoom一覧を見てみると、

JUNE様ですね。きちんとミーティングが作られています。(時間がずれているのはコンテナ側のタイムゾーンの設定をすっかり忘れていたからです。)そしてテーブルを見てみると

きちんと作られていました。zoom_join_urlの値にアクセスすると

管理者が直接言っているのでミーティングの開始となっていますが、きちんと有効なミーティングURLを取得し保存できています。本来であればメールでお客様にこのURLとパスワードをお知らせします。

また管理画面では

この様にして一覧で確認ができます。ちなみに「ミーティングを削除」などのボタンはhttp://localhost:9000/form/delete?hash=cgckkwc040okko..へリンクされています。form/deleteでミーティングを削除する確認画面へ飛べます。そして照合のためにhash=cgckkwc040okko..の値を用いています。(途中にあった$hashの値です。)

お客様が匿名であり、ユーザーセッションによる識別ができない時は、予測困難なハッシュ付きURLをお客様だけのメールに渡してミーティングの制御が可能です。

まとめ

以上がアプリの実装の流れです。今回作成したOAuth認証zoom アプリはプライベートなので作った本人しか今は利用できませんが、しっかり実装して審査を受けることでマーケットプレイスに出店して自由に使用してもらうことが可能になります。

OAuthも意外と簡単でしたが、公式のAPIリクエストライブラリが出ているわけでないので本格的な開発の際には独自のzoom APIライブラリを作っておくといいかもしれません。zoomでできることはこれだけでないので、もっと色々調べてみようと思います。ひとまず今回のzoomアプリはここまでとします。一応リポジトリに公開してあるのでぜひクローンして遊んでみてください。

追記 2021 3 31

なんか、これぐらいの規模で特定のアプリでの利用であればJWTでも十分でした汗。JWT編もそのうちやろうと思います。

Copyright © 2021 jun. All rights reserved.