からめもぶろぐ。

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

Microsoft Build Recap Community Day に登壇しました

Microsoft Build Recap Community Day で Microsoft Teams 開発の最新情報について登壇してきました。

資料は Speaker Deck に公開しています。

speakerdeck.com

途中でご紹介したアーキテクチャの資料の元ネタは以下にあります。

docs.microsoft.com

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 アプリケーションじゃ駄目なんですね。マイクロソフト アカウントから作る必要があります。

Microsoft Teams Framework (TeamsFx) が公開されました

Microsoft Build 2021 のタイミングだと思いますが、Microsoft Teams Toolkit が更新され、同時に Microsoft Teams Framework (TeamsFx) が公開されています。

github.com

すべての機能を追い切れていませんが、具体的にどんなものかというと、Microsoft Teams アプリを開発するときにこれまでいろいろと面倒だった手の届かないかゆいところを全部面倒を見てくれます。例えば、

  • localhost でホストをするときの自己証明書のインストールをやってくれる
  • localhost でホストをするときの Azure AD アプリケーションの作成をやってくれる
  • デバッグ時にパッケージのインストール (npm install)、バックエンド サーバー (node.js) の起動、フロントエンド サーバー (react-scripts) の起動、ブラウザーの起動を一連のタスクとしてやってくれる
  • Microsoft Graph への接続 (Microsoft Graph SDK との連携) をやってくれる
  • teamfx のコマンドラインでプロビジョニングやデプロイもやってくれる

多機能すぎて何が何だかよくわからないです。既存のアプリに適用する必要はあまりないかもしれませんが、これから新しく作るアプリではかなり役立ってくれそうな気がします。

Microsoft Bookings で予約がされたときのイベントを Power Automate でトリガーする

Microsoft Bookings にしばらく触っていなかったのですがいつの間にか Microsoft Teams 会議に対応していました。COVID-19 の状況下においてオンライン会議の予約ができるのは嬉しいですね。Microsoft Bookings だけでもだいたいのことは完結するのですが、そこにカスタムでちょっと何かをしたい場合、やはり Power Automate を使って予約がされたときのイベントをトリガーしたいです。

改めて Microsoft Bookings の仕組みを確認してみます。ビジネスを新しく作成すると Azure AD にユーザーが作られます。このユーザーはライセンスを持たないですが Exchange のメールボックスを持っています。そして予約が作成されると、このユーザーを開催者としてイベントが作成され、スタッフに会議出席依頼が出されます。*1 要するに、このユーザーに対してイベントが作成されたときをトリガーすればよさそうです。

Power Automate で接続を作成するにはユーザーにログインする必要があります。しかし自動的に作成するユーザーのパスワードはわからないので一度リセットしてあげる必要があります。*2 トリガーとしては「新しいイベントが作成されたとき」を使用します。このトリガーの出力からは大した情報を得ることはできませんが、そこから Microsoft Graph の Bookings API を呼び出すことで詳細な情報を取得することができます。

docs.microsoft.com

ただし Power Automate には Bookings API のコネクタはないので、HTTP アクション、つまりプレミアム コネクタの利用が必要になります。場合によっては Logic Apps を使うことも検討したほうがいいかと思います。

*1:オンライン会議を有効にしたときはこのタイミングで Microsoft Teams 会議が作成されます。

*2:パスワードをリセットしても動作には影響ありません。

SharePoint Online のモダン サイトでニュースの一覧を表示する

SharePoint Online のモダン サイトで標準のニュース Web パーツがあるのですが、指定した件数しか表示できないので、新しいニュースがあると古いニュースが流れてしまいます。[すべて表示] をクリックすればすべてのニュースが見られるのですが、ひと手間かかるのと、タイトルを非表示にしていると表示されないので、ちょっと面倒だったりします。可能であれば Web パーツとして一覧表示したいです。

結論からいうと、件数の制限がなく表示できるようにするには、ドキュメント ライブラリの Web パーツを使うのがよさそうです。*1 さっそくドキュメント ライブラリの Web パーツを貼り付けてサイト ページを表示してみます。

f:id:karamem0:20210519145930p:plain

なんかいまいち。ファイル名だと何のニュースかわかりません。新しく「すべてのニュース」という名前でビューを作成して「名前」列の代わりに「タイトル」列を出すようにします。

f:id:karamem0:20210519145948p:plain

ほぼよさそうに見えますが少しだけ問題があります。サイト ページには通常のページとニュースのページが混在していますがニュースだけを表示したいです。ニュースかどうかは「昇格された状態 (PromotedState)」列で判断できますが、既定では非表示になっていて UI からはフィルターの設定ができません。よって PowerShell で操作をすることになります。方法としては 2 通りあります。

  1. 一時的に「昇格された状態」列を表示する
  2. ビューの CAML を直接修正する

今回は一発でできる後者の方法をやってみます。スクリプトは以下の通りです。前提として PnP PowerShell を使用します。

$SITE_URL = '{{SiteUrl}}'
$LIST_NAME = '{{ListName}}'
$VIEW_NAME = '{{ViewName}}'
$CAML = @'
<Where>
    <Eq>
        <FieldRef Name="PromotedState" />
        <Value Type="Number">2</Value>
    </Eq>
</Where>
'@
Connect-PnPOnline -Url $SITE_URL -Credentials (Get-Credential)
Get-PnPView -List $LIST_NAME -Identity $VIEW_NAME | Set-PnPView -Values @{ ViewQuery = $CAML }

実行すると今まであった「ホーム」がなくなっていることがわかります。これでニュースの一覧を出すことができました。

f:id:karamem0:20210519151926p:plain

*1:注意点として、ドキュメント ライブラリの Web パーツは自身のサイトのドキュメント ライブラリしか表示できない、という制約が発生します。