からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

Microsoft Bookings API と Microsoft Bot Framework v4 を使って予約ボットを作ってみた

Microsoft Graph にはまだプレビューですが Microsoft Bookings API というものがあるので、Microsoft Bookings で提供されている公開ページ以外でも予約をすることができます。カスタムのページを作ることもできますが、今回はせっかくなので Microsoft Bot Framework と Azure Bot Service を使って LINE から予約できるようにします。

サンプル コード

github.com

Microsoft Bookings API について

Microsoft Bookings API を見てみると、すべてのエンドポイントはアプリケーションのアクセス許可が使えず、委任されたアクセス許可しか使うことができません。バックエンドで動作するアプリケーションを作りたいので、この仕様では非常に困るのですが、仕方がないのでビジネスに対して管理者として設定されているユーザーで OAuth の Resource Owner Password Credentials Grant によってトークンを取得します。これは推奨されていない方法なので注意が必要です。

docs.microsoft.com

とはいえほかに方法がないので仕方ないということにします。ここは GA になったら改善してほしいですね。

Microsoft Graph にはベータ版の SDK を使ってアクセスしますが、Resource Owner Password Credentials Grant に対応するいい感じの AuthenticationProvider が提供されていないので、自作することにします。ボットは長時間動きっぱなしになるのでトークンの有効期限を考えないと痛い目に合います。

private class UsernamePasswordProvider : IAuthenticationProvider
{

    private readonly IPublicClientApplication application;

    private readonly NetworkCredential credentials;

    private AuthenticationResult authenticationResult;

    public UsernamePasswordProvider(IPublicClientApplication application, NetworkCredential credentials)
    {
        this.application = application;
        this.credentials = credentials;
    }

    public async Task AuthenticateRequestAsync(HttpRequestMessage request)
    {
        if (this.authenticationResult == null ||
            this.authenticationResult.ExpiresOn <= DateTime.UtcNow)
        {
            this.authenticationResult =
                await application
                    .AcquireTokenByUsernamePassword(
                        null,
                        this.credentials.UserName,
                        this.credentials.SecurePassword)
                    .ExecuteAsync();
        }
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.authenticationResult.AccessToken);
    }

}

Microsoft Bot Framework について

Microsoft Bot Framework v4 については日本マイクロソフトの中村憲一郎さんの記事がまとまっています。ぶっちゃけこれさえ見ればだいたいわかります。

qiita.com

今回のようにフローにしたがってボットを動かしたいときは Microsoft Bot Framework v4 の作法では WaterfallDialog を使うみたいです。WaterfallStep にあらかじめ定義されたメソッドを追加しておくとその順番でメソッドが処理されます。各メソッドでは「直前にユーザーから送られたメッセージを受信する」ことと「次にユーザーに対してメッセージを送信する」ことを行います。本当は 1 つのメソッドに 2 以上の責務が含まれているのは望ましくないのでちゃんと作るときはさらに別のメソッドに分割したほうがいいと思います。いったんサンプルということでそのままにしています。

実際にやっている流れは以下の通りです。

  • Microsoft Bookings のビジネスの一覧を選択させる
  • 選択されたビジネスで提供されるサービスの一覧を選択させる
  • 予約対象の日付を選択させる
  • 予約対象の時間を選択させる
  • 名前を入力させる
  • メール アドレスを入力させる
  • 予約内容を確認させる

日時に関しては、Microsoft Bookings には営業時間やサービスごとの時間の設定もあるので、内部的には細かいことをやっていますが、このあたりはもうちょっと API で処理してくれるとありがたいなと思いました。

最終的に収集したデータを使って予約を作成します。docs を見ると登録内容としてたくさんの項目がありますが最低限の内容でも登録できます。注意したいのは以下の 2 点です。

  • CustomerId を指定するようにします。指定しないと同じメール アドレスで顧客データが複数作られてしまいます。事前にメール アドレスをキーにして顧客データを検索する必要があります。
  • StaffMemberIds を指定するようにします。指定しないと Microsoft Teams 会議が作成されません。今回はスタッフを指定しないのでいったん実行ユーザーの ID を指定します。
private async Task<DialogTurnResult> SubmitBookingAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // プロファイルを取得する
        var bookingProfile = await this.bookingProfileAccessor.GetAsync(stepContext.Context, () => new BookingProfile(), cancellationToken);
        try
        {
            // 予約を作成する
            await this.graphServiceClient
                .BookingBusinesses[bookingProfile.BookingBusinessId]
                .Appointments
                .Request()
                .AddAsync(new Graph.BookingAppointment()
                {
                    CustomerId = bookingProfile.BookingCustomerId,
                    CustomerName = bookingProfile.BookingCustomerName,
                    CustomerEmailAddress = bookingProfile.BookingCustomerEmail,
                    End = new Graph.DateTimeTimeZone()
                    {
                        DateTime = bookingProfile.BookingEndTime.ToUniversalTime().ToString("s"),
                        TimeZone = "UTC",
                    },
                    ServiceId = bookingProfile.BookingServiceId,
                    ServiceName = bookingProfile.BookingServiceName,
                    Start = new Graph.DateTimeTimeZone()
                    {
                        DateTime = bookingProfile.BookingStartTime.ToUniversalTime().ToString("s"),
                        TimeZone = "UTC",
                    },
                    StaffMemberIds = new[] { bookingProfile.BookingStaffMemberId }
                });
            // メッセージを送信する
            await stepContext.Context.SendActivityAsync(MessageFactory.Text(StringResources.CompleteBookingMessage));
        }
        catch (Exception ex)
        {
            // メッセージを送信する
            await stepContext.Context.SendActivityAsync(MessageFactory.Text(ex.Message));
        }
    }
    else
    {
        // メッセージを送信する
        await stepContext.Context.SendActivityAsync(MessageFactory.Text(StringResources.CancelBookingMessage));
    }
    // ダイアログを終了する
    return await stepContext.EndDialogAsync(null, cancellationToken);
}

LINE からの実行

作成したコードを Azure App Service にデプロイします。普通にデプロイすればいいですが、時刻を扱うため、WEBSITE_TIME_ZONE を指定するのを忘れないようにします。

f:id:karamem0:20210606135248p:plain

Azure Bot Service を作成して Azure App Service に関連付けます。*1 LINE Developer に登録して設定すれば完成です。

docs.microsoft.com

実際に実行してみます。


f:id:karamem0:20210605220815p:plain
f:id:karamem0:20210605221401p:plain

予約が完了するとメールが送信されます。

f:id:karamem0:20210606135522p:plain

Microsoft Bookings からも予定が確認できます。

f:id:karamem0:20210606135833p:plain

まとめ

Microsoft Bookings API はプレビューのため、まだまだできないことが多く、本番運用には向かないかもしれません。ただし API の GA が予告されているため今後については期待ができます。COVID-19 の影響でオンサイトやオンラインにかかわらず予約の需要は高まっていますので活用していただければと思います。

techcommunity.microsoft.com

*1:余談ですが Azure Bot Service で使う Microsoft App Id と Microsoft App Password って自分のテナントで作成した Azure AD アプリケーションじゃ駄目なんですね。マイクロソフト アカウントから作る必要があります。