himanago

Azure・C#などのMS系技術やLINE関連技術など、好きな技術について書くブログ

Logic App から Azure AD のユーザー追加を行う

Azure ユーザーの追加処理を Logic Apps で作ったのでメモ。

Azure AD の ユーザー作成&グループ追加

Azure AD のアクションは、Logic App のデザイナーで「Azure AD」と検索すれば出てきます。 以下のように細かくアクションを用意してくれています。

f:id:himanago:20200106143536p:plain

ユーザー追加のほかにもいろいろありますが、今回はユーザーを作ったあとに指定したグループに入れてみます。
フローはこんなかんじ。Logic Apps なら(たとえば HTTP リクエストで受け取った)パラメータを使ったり、作成した直後のユーザーを参照したりすることも簡単です。

f:id:himanago:20200106150809p:plain

アクションの実行に使用するユーザーとロール

Logic App で Azure AD の操作をする際、そのアクションを動かす Azure アカウントを指定する必要があり、作成時にサインインする必要があります。

f:id:himanago:20200106145040p:plain

ここで使用するアカウントは普段使っているアカウントではなく、Logic App から使う専用アカウントを作っておくほうがセキュリティ・運用的によいと思います。

Azure AD を操作するための権限が必要なので、適切なロールを付与しておかなければなりません。ユーザー作成とグループへの追加は、「ユーザー管理者」をつけておけば動きました。

f:id:himanago:20200106145705p:plain

ちなみに成功すると、その接続情報が Logic App と同じリソースグループに増えてます。

f:id:himanago:20200106145813p:plain

おわりに

Logic Apps を使うとAzure AD の操作もデザイナーをポチポチするだけで実現できるので便利ですね。

【Uno Platform】WebAssembly 実行時の JavaScript 相互運用

Uno Platform は、UWP を iOS/Android や WebAssembly で動くようにするものですが、WebAssembly で Uno を動かすときには、C#/XAML で足りない部分を補ったり、既存の JavaScript ライブラリ/コードを併用したりするために、C#JavaScript を相互に呼び合いたい場面が出てきます。

同じく C# で WebAssembly を実現する Blazor でも JavaScript との相互運用機能 が用意されていますが、Uno Platform でもできるようなので試してみました。

JavaScript ファイルの使用

Wasm のプロジェクトにある WasmScripts フォルダに js ファイルを「埋め込みリソース」として配置すれば、読み込んでくれます。既存のライブラリなどを読み込むこともできますね。

f:id:himanago:20191229120655p:plain

C# コードから JavaScript の呼び出し

JavaScript を配置できるとしても、それを C# 側から呼べないと意味がありません。
C# から呼ぶには、Uno.Foundation 名前空間にある WebAssemblyRuntime.InvokeJS メソッドを使います。

さきほどの JavaScript ファイルの中身に、こんなかんじの適当な関数を定義しておきます。

function sayHello() {
    alert('Hello from JS!');
}

XAML にボタンを一つだけ置いて、コードビハインドを以下のように書いてみます。
if ディレクティブで WASM のときだけ JavaScript 呼び出しで、その他は普通に MessageDialog です。

using System;
using System.Threading.Tasks;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#if __WASM__
using Uno.Foundation;
#endif

namespace UnoJavaScriptInteropSample
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private async void callJsButton_Click(object sender, RoutedEventArgs e)
        {
#if __WASM__
            WebAssemblyRuntime.InvokeJS("sayHello();");
#else
            await new MessageDialog("Hello from MessageDialog").ShowAsync();
#endif
        }
    }
}

UWP だとこうなりますが、

f:id:himanago:20191229145334p:plain

WebAssembly で実行するとこのようになります。

f:id:himanago:20191229144325p:plain

ちゃんと JavaScript コードが呼ばれていますね。

特定要素に対する処理を JS にやらせる

JavaScript のコードを書く際、C#/XAML で生成した特定のコントロールJavaScript に操作させたくなります。

そのために使えるのがコントロールHtmlId プロパティで、生成された HTML 要素の id 属性として使われる値がとれます。これを JavaScript に渡せば特定のコントロールに対する DOM 操作などを簡単に書けます。

たとえば、TextBoxIsReadOnly プロパティは WebAssembly では対応しておらず、True にしても UWP では当然読み取り専用になるので、これと WebAssembly の動きをそろえたい…という場合に以下のようにテキストボックスを readonly にする関数に対し id を渡すよう、以下のように実装すれば対応可能です。

#if __WASM__
        protected override void OnLoaded()
        {
            WebAssemblyRuntime.InvokeJS($"toReadonly({textBox.HtmlId});");
        }
#endif

テキストボックスを readonly にする関数のほうの実装ですが、C# 側の TextBox の HtmlId でとれる値が、実は input 要素のものではなく、それを囲む外側の div である点を考慮する必要があります。

Uno Platform では、UWP のコントロールと対応する見た目の HTML 要素を WebAssembly で生成していますが、結果的に生成されるのが単一のタグで構成されたものであるとは限りません。そのため、一度どのようなタグの要素が生成されるのかを確認してから実装をする必要があります(そのため JavaScript では以下のように2段階で input 要素を探す)。

function toReadonly(id) {
    const box = document.getElementById(id);
    box.getElementsByTagName('input')[0].setAttribute('readonly', 'readonly');
}

イベントの連携

あとおもしろいのはイベントです。以下のドキュメントにも記載がありますが、この機能は JavaScript 側から dispatchEvent を実行することで C# 側に用意したイベントハンドラを呼び出すことができます。

(Wasm) Handling custom HTML events

C# で using に Uno.Extensions を追加して、RegisterHtmlEventHandler/RegisterHtmlCustomEventHandler すると、そこで登録したイベントハンドラJavaScriptdispatchEvent を受け取れます。

気を付けたいのは、JavaScript 側から dispatchEvent する要素と、C# 側で RegisterHtmlEventHandler/RegisterHtmlCustomEventHandler するコントロールを合わせなければならないということです。これも HTML 側でどんな要素が対応しているか確認してしっかり一致させておかないとうまく動きません。

下記のコードは、テキストボックスのダブルクリックで C# 側のイベントを呼ぶ処理です。

外側の div 要素と C#/XAML 側の TextBox コントロールが一致するので、テキストボックスをダブルクリックしてイベントを走らせたければ、JavaScript で input 要素のダブルクリックイベントの処理内で、外側の div から dispatchEvent するのがポイントです。

#if __WASM__
        protected override void OnLoaded()
        {
            WebAssemblyRuntime.InvokeJS($"addDblclickEventListener({textBox.HtmlId});");
            textBox.RegisterHtmlCustomEventHandler("dblclickEvent", OnDblclickEvent, isDetailJson: false);
        }

        private async void OnDblclickEvent(object sender, HtmlCustomEventArgs e)
        {
            await new MessageDialog(e.Detail).ShowAsync();
        }
#endif
function addDblclickEventListener(id) {
    const box = document.getElementById(id);
    const tb = box.getElementsByTagName('input')[0];

    tb.addEventListener('dblclick', (e) => {
        box.dispatchEvent(new CustomEvent("dblclickEvent", { detail: e.target.id }));
    });
}

JavaScript から C# 側に文字列や JSON を渡すことができるので、わりと幅広いことができそうです。

まとめ

Uno における C#JavaScript の相互運用をみてみました。イベントの連携などでいろいろできそうですが、あまり複雑なことをしすぎるとコードがごちゃごちゃしてしまいますし、せっかく C#クロスプラットフォーム対応できるという利点が薄れてしまうので多用しすぎないように注意したいところです。

上記検証に使ったサンプルは下記に置きました。

GitHub - himanago/UnoJavaScriptInteropSample

#技術書典 8 にて「牛めし警察で学ぶ Uno Platform」を頒布します!

ひらりん, ちょまど, かずき」というサークル名で、 技術書典8 (IT や機械工作などの技術書を専門とする同人誌即売会) で本を出します!

techbookfest.org

サークル名の由来

f:id:himanago:20191219232136j:plain

ご覧のとおりです!私(ひらりん)と、Microsoftちょまどさん、かずきさん。
※順番は、サークル加入順

いつも本当にお世話になっているお二人と、前回に続き一緒にサークルできるのがとても楽しみです。
競争率の高い技術書典、無事に当選しとてもうれしいです。

サークルカット

仮ですが、すでにちょまどさんに C# ちゃんのサークルカットを用意してもらっています!
背景の C# コードが、実はかずきさんがささっと書いたもの(8.0 の新機能 をちゃんと入れている!)だったりします。

f:id:himanago:20191219234234j:plain

技術書典8 について

今回は2日間開催ですが、その2日目である 3/1(日) に参加します!

techbookfest.org

経緯

前回の技術書典7 で、個人で参加した際にお二人に手伝っていただきました。

3人でサークルを回したのがとても楽しかったので、今度は書くところから3人で参加したい!と、今回のサークル参加が実現しました。

どんな本?

牛めし警察で学ぶ Uno Platform』と題し、最近話題の「Uno Platform」を使って、ちょまどさんの伝説のアプリ「松屋警察」をリメイクします!

Uno Platform は、デスクトップアプリ(UWP)、モバイルアプリ(Android/iOS)、Web アプリ(WebAssembly)として動くものが C#XAML で作れるいま注目のフレームワークです。

platform.uno

オリジナルは Xamarin.Forms で Custom Vision の画像判定(松屋牛めし吉野家の牛丼を見分ける)を行うモバイルアプリでしたが、今回は Uno Platform でそれを Webアプリとしても動くようにします。

この本では、Uno Platform の概要から、それを使ってアプリを作るところまで一通り解説していく予定です。
ちょまどさんには表紙や挿絵でかわいらしいイラストを描いていただきつつ、アプリデザインも担当いただきます!
そして、かずきさんには圧倒的な技術力と安定感でコードや本文の記述の正確性・質を高めてくださることでしょう。

もうこれだけで、楽しくて役に立つ本になること間違いなしだと思っています。
(ちなみに私は実際のアプリを組み立てていく部分の執筆を担当する予定です!)

メンバーよりひとこと

ひらりん (@himarin269)

大尊敬するお二人とまたご一緒できて、そしてその成果をみなさまにお届けできることとなりとてもうれしいです。
扱うテーマも Uno Platform というとても Hot な技術なので、その楽しさ・すばらしさをたくさん学んで、多くの方に伝えられたらなと思います。
牛めし警察で学ぶ Uno Platform』、どうぞよろしくお願いします!

ちょまどさん (@chomado)

貴重な機会をたまわりまして、誠にありがとうございます。
同人誌を書くのは数年ぶりとなり、大変嬉しく思います。
3人で楽しく本を書いて (描いて) まいりますので、皆さまと 3/1 (日) 技術書典 day 2 でお会いできるのを楽しみにしております!
(゚▽゚ っ)З

かずきさん (@okazuki)

Uno Platform は C# + XAMLVisual Studio による強力なツールサポートをフル活用して UWP, Android, iOS, WebAssembly のアプリを開発出来る凄いプラットフォームです。 少し Uno Platform のソースコードを見たのですが非常に頑張っててソースコードの内容でも好感を持ちました。面白そうなテクノロジーなので興味がありましたら頑張って書くので是非お手に取ってみてください。


みなさんに楽しい本をお届けできるよう、これから 3人で頑張って書いていきます!
3/1(日) 、技術書典8(2日目)でお会いしましょう!よろしくお願いします!!

Azure Functions の DI でエラー

この記事は Microsoft Azure Advent Calendar 2019 の 17日目の記事…ではありません(笑)

qiita.com

アドカレ記事は別途書いてます。今日中には上げます!

[追記] 書きました↓
Durable Functions を使ってコードのみでアンケート回答に便利な LINE Bot を簡単につくる - Qiita

で、この記事は、アドカレ記事を書いていたときに遭遇したエラーについてのメモです。

エラーの内容

前提としては、

  • Azure Functions で LINE Bot を作っていた。
  • Startup で Messaging API SDK 関連のクラスを2つ DI しようとした

という状況。

以下のように Startup を書いたら…

using System;
using LineDC.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(Sample.LineBot.Startup))]
namespace Sample.LineBot
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddSingleton<IDurableWebhookApplication, EnqBotApp>(_ =>
                new EnqBotApp(LineMessagingClient.Create(
                    Environment.GetEnvironmentVariable("ChannelAccessToken")),
                    Environment.GetEnvironmentVariable("ChannelSecret")));
        }
    }
}

なぜかこんなエラーが。

2019-12-17T03:21:41.080 [Error] Executed 'HttpStart' (Failed, Id=5cf2c546-9406-4870-89cc-bae17e4e4e5d)
System.InvalidOperationException : Unable to resolve service for type 'Sample.LineBot.EnqBotApp' while attempting to activate 'Sample.LineBot.DurableEnqFunctions'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp,Type type,Type requiredBy,Boolean isDefaultParameterRequired)
   at lambda_method(Closure ,IServiceProvider ,Object[] )
   at Microsoft.Azure.WebJobs.Host.Executors.DefaultJobActivator.CreateInstance[T](IServiceProvider serviceProvider) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\DefaultJobActivator.cs : 37
   at Microsoft.Azure.WebJobs.Host.Executors.DefaultJobActivator.CreateInstance[T](IFunctionInstanceEx functionInstance) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\DefaultJobActivator.cs : 32
   at Microsoft.Azure.WebJobs.Host.Executors.ActivatorInstanceFactory`1.<>c__DisplayClass1_1.<.ctor>b__0(IFunctionInstanceEx i) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\ActivatorInstanceFactory.cs : 20
   at Microsoft.Azure.WebJobs.Host.Executors.ActivatorInstanceFactory`1.Create(IFunctionInstanceEx functionInstance) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\ActivatorInstanceFactory.cs : 26
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker`2.CreateInstance(IFunctionInstanceEx functionInstance) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionInvoker.cs : 44
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ParameterHelper.Initialize() at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 846
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsyncCore(IFunctionInstanceEx functionInstance,CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 116

メッセージで調べるといろいろ出てくるのですが、根本的な原因・解決策はすぐにはわからず。

解決策

とりあえずいろいろやってみましたが、以下のように記述を変えたらエラーが出なくなることがわかりました。

using System;
using LineDC.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(Sample.LineBot.Startup))]
namespace Sample.LineBot
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var client = LineMessagingClient.Create(Environment.GetEnvironmentVariable("ChannelAccessToken"));

            builder.Services
                .AddSingleton<ILineMessagingClient>(_ => client)
                .AddSingleton<EnqBotApp>(_ => new EnqBotApp(client, Environment.GetEnvironmentVariable("ChannelSecret")));
        }
    }
}

このエラーが出たら DI のしかたを見直してみるといいのかも。

※ 追記 ※

WebhookApplication クラスのスコープがまずかったので修正。

using System;
using LineDC.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(Sample.LineBot.Startup))]
namespace Sample.LineBot
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services
                .AddSingleton<ILineMessagingClient>(_ => LineMessagingClient.Create(Environment.GetEnvironmentVariable("ChannelAccessToken")))
                .AddTransient<EnqBotApp>(s => new EnqBotApp(s.GetService<ILineMessagingClient>(), Environment.GetEnvironmentVariable("ChannelSecret")));
        }
    }
}

今回またかずきさんに DI に関するご指摘をいただきました(技術書典7 に続き3か月ぶり2回目)。 s.GetService<T>() で DI コンテナに登録しているサービスをとれるとのことも教えていただいた。知らなかった。

DI まわりはちゃんと勉強して毎回ちゃんとスコープに気を付けないとあぶないですね。

Microsoft Open Tech Night #1 で LT をしました

少し間があいてしまいましたが、以下のイベントで LT しました。

msdevjp.connpass.com

タイトルは「Azure Application Gatewayでオンプレ DMZクラウドへ拡張する」。

勉強会のテーマにあわせ、いつもと違ってインフラ系での登壇。

ちょうど業務でやっている外部公開な Web アプリの Azure 移行についての話をしました。

内容

L7 ロードバランサの Application Gateway を使って、オンプレ DMZ にある複数のアプリを段階的に移行していく、という話です。

まず Application Gateway を立てて f:id:himanago:20191210233814p:plain

バックエンドプールに Web App 追加 f:id:himanago:20191210233852p:plain

アプリをひとつずつ移行 f:id:himanago:20191210233911p:plain

という流れで移行します。実際に業務でやっている内容なので、けっこう試行錯誤したところも多かったのですが、当日のツイートで

というのがあり、「おなじことやっている方がいたのかー!」とうれしかったのと同時に、「MVP になると答え合わせの対象として見られるのか…」とちょっとドキッとしました。

いままで以上に、下手なことは言えないな…と思ったので、これからもっと技術にきちんと向き合っていかなければ、とあらためて思いました。

資料

当日の資料はこちらです。

www.slideshare.net

雑記

今回のイベントは代官山に新しくできた「Azure Daikanyama Base」という施設で行われました。はじめての施設でしたが、相性が悪かったのか何なのか、なぜかどうやっても自分の PC が Wi-Fi につながらず。

Zoom での配信もあったため、登壇者のネット接続はマスト。しかたなく、おなじくメイン登壇のひとりだったちょまどさんの Surface Go を借りて登壇しました。

で、そこで問題が。発表者席は教壇的なものではなく、ふつうの椅子に座って使う机だったので、立ちでしゃべると Surface Go の画面が小さすぎて見えない!
そのため発表はだいぶ手間取り、ぎこちないかんじになってしまいました。。

立ちで発表するこだわりは捨てて、いっそ座っちゃえばよかったかも。ちょっと反省。

とりあえず LT はなんとかなったのでよかったんですが、この Azure Daikanyama Base、これから先ちょくちょく利用することになるはずのところ、今後ずっとネットにつながらないままなんだろうか。。

進化した LIFF v2 によるサイト導線強化について

最近、個人的に LIFF v2 が熱いと思っています。

LIFF は LINE Front-end Framework の略で、LINE 内に埋め込める Web アプリを作れるフレームワークです。
主に LINE Bot に Web のインターフェースを組み合わせる用途で使われてきました。

LINE Front-end Framework

LIFF v2

10月の v2 リリースを皮切りに、大きなアップデートが続いています。

リリースノート

いくつかのトピックがありますが、中でも Web サイトから LINE Bot 連携サービスへの導線強化に大きな意味があるものが目立ちます。今回はその観点で変更点を見ていきます。

外部ブラウザで動作するようになった

これまでは、LIFF で作った Web アプリは LINE アプリ内のみでの動作でしたが、PC・スマホ等の一般的な Web ブラウザで動かすことができるようになりました。

デバッグがしやすい

通常のブラウザで動かせるというのは、開発者にとってはデバッグがしやすくなるという利点があります。LINE アプリ内のブラウザには開発者ツールがないのでデバッグがつらかったですが、そこが解消されます。

LINE アプリ内でしか動かない API(メッセージ送信や QR コード読み取りなど)もあるので、そのあたりはこれまで通り alert デバッグ等でなんとかしないといけないのかなとは思いますが、初期化処理や CSS 操作での見た目の問題などベーシックな部分の開発を Web の世界だけで完結できるのはかなり効率が良くなりそう。

エンドポイント URL そのままで動く

LIFF は line://app/xxxxx というような URL を使って LINE 内で開いてきましたが、外部ブラウザで動作させる場合はエンドポイント URL にそのままアクセスすれば OK です。

f:id:himanago:20191208060506p:plain

これは、つまり 既存の Web サイト / Web アプリに LIFF の SDK を何も考えずそのまま導入できる ということを意味します。

すでに稼働している Web サイトを LIFF 対応したい場合のハードルが大きく下がる気がしています。

HTTPS のURLスキームと Deep Link

これが大きい!

外部ブラウザ動作用の URL としてはエンドポイント URL そのままを利用しますが、LIFF v2 では URL として、Deep Link(端末にインストールされているアプリへの遷移)をサポートするようになりました。

スキームはドキュメントにありますが、これまで line://app/{liffId} であった LIFF へアクセスするための URL を、 https://liff.line.me/{liffId}/ とすることで以下の動きをする Deep Link 対応の URL となります。

  • LINE がインストールされている端末(主にスマホを想定)→LINE アプリが起動しその中で LIFF ページが開く
  • LINE がインストールされていない端末(主に PC を想定)→エンドポイント URL にリダイレクト

参考:LINE URLスキームを使う

line://~~ が非推奨になっていることからも、LINE Developers で LIFF 作成時に表示される LIFF URL も https のものに変更されるものと予想しています。

Deep Link の注意点としては、関係のないアカウントとのトーク画面を開いている状態で踏むと、その関係ないトーク画面上でそのまま LIFF が開くので、メッセージ送信の API などを使う場合は気を付けないといけません。
ユーザーの誤操作・誤爆を誘発してしまい予期せぬクレームにも発展しかねないので、LIFF からのメッセージ送信の設計は慎重に行う必要があると考えられます。

逆に Flex Message などを友だちとのトークで使えるような LIFF 便利アプリなんかも作ることができそうではあります。

ログイン強化

LINE ログインとの互換性向上

ユーザー情報の取得が簡単になり、また外部ブラウザでの動作時にも LINE ログインの機能が使えます。
これにより、Web アプリと LINE の連携がかなりしやすくなってきています。

LINE ログインのチャネルにしか作れないようになる

LIFF はこれまで Messaging APIBot)のチャネルの下に作ることが多かったですが、今後は「LINE ログイン」チャネルのににしか作れなくなるようです。

f:id:himanago:20191208061237p:plain

※ LIFF は何か別のチャネルの下にサブで追加するもの

LIFF はこれまで Bot の入力インターフェースの補完という印象が強かったですが、今後は LINE ログイン強化や Deep Link により、Web の世界から LINE へつなぐ役割を大きく担う存在になっていきそうです。

Bot の友だち追加もできる

LIFF の使用を開始するとき、ユーザーに連携する LINE 公式アカウント(Bot など)を友だち追加してもらうことができます。 このあたりの設定は LIFF の設定画面の「ボットリンク機能」から行え、ON にすると認証時に友だち追加をあわせて行えます(Normal と Aggressive はタイミングの違い)。

f:id:himanago:20191208064100p:plain

なので、LINE ログインチャネルにしか作れなくなっても、従来通りの Bot に対する補完としても LIFF は使っていくことができます。

ユーザーの Bot への導線強化に期待大

以上に見てきた機能強化から、LIFF v2 を使うとWeb やメール、他のアプリから LINE 公式アカウントへ誘導することが驚くほど簡単にできてしまいます。
なぜなら、https の LIFF URL にアクセスさせるだけでサービスへの LINE ログイン&Bot 友だち追加を行ってもらえるから です。

URL ひとつでこれだけできるのは、実はとんでもないことなのでは!?と思います。Web サイトと LINE 公式アカウント(Bot)を両方運営している/しようとしている方は、その連携を大きく強化できるので、LIFF v2 の導入を検討したほうが良いと思います。

まとめ

WebからLINE(Bot)への導線をしっかり確保できるので、きちんと設計すれば間違いなくかなりの武器になると思われます。

業務で複数の LINE Bot を開発していますが、LIFF v2 の進化は業務ニーズにこたえる正当な進化という印象で、今回の v2 はとてもうれしく思っています。

ほかにも機能追加など細かい部分でアップデートがあるので、開発や運営を進める中で感じたことなどあればまた書いていこうと思います。

C# によるスマートスピーカースキルのクロスプラットフォーム開発ライブラリ「XPlat.VUI」の紹介

本記事は、Qiita「スマートスピーカー Advent Calendar 2019」の3日目のエントリです。 qiita.com

この記事について

現在個人的に開発をして公開している、C#Googleアシスタント、Alexa、Clova に対応したクロスプラットフォームなスキルを効率よく開発できるようになるライブラリ「XPlat.VUI」を紹介します。

XPlat.VUI は、Googleアシスタント、Alexa、Clova に対応したスキル開発における処理の共通化が可能で、HTTPのリクエストをそのまま渡すだけで、各プラットフォームの起動およびインテントリクエストに対応したレスポンスを作って返せます。

背景

まずは簡単に、「XPlat.VUI」ライブラリを開発した背景について説明します。

C# で開発するメリット

C#スマートスピーカースキルを開発するメリットとして、

  • LINQ、async / await、非同期ストリームなど C# 言語そのものの強み
  • Visual StudioIDE)による強力な開発支援機能
  • 豊富な NuGet パッケージ
  • ASP.NET Core や Xamarin など C# を用いた他のプロダクトとのコード共有
  • Microsoft Azure との親和性

などが挙げられます。

特に Azure では、Azure Functions(Azure におけるサーバーレスなコード実行基盤サービス)を使用することが多く、スキルのバックエンドを効率よく実装できます。

また、スキル開発に C# / Azure Functions を使用する理由の一つに、Durable Functions があります。
Durable Functions は サーバーレスでありあがらステートフルな処理をコードのみで実現する Azure Functions の拡張機能です。
これを用いるとスマートスピーカースキルに多く存在する制約を超えていくことができます(今回は本題からずれるので詳細は割愛)。

クロスプラットフォーム対応の苦しみ

Alexa, Googleアシスタント, Clova では、それぞれカスタムスキル(アクション)を実行するときにやりとりされる JSONスキーマが異なります。
また、それぞれの SDK のしくみや使い方も違います。C# SDK においても、それは同様です。

そのため、3プラットフォームに同じ内容のスキルをリリースしたいと思った場合、ロジック部分はある程度共通化できても、それぞれで異なるライブラリを使用し異なるデータのやりとりを実装しなければなりません。

スマートスピーカースキルは使用感の軽いものが多く、スキル内容のコーディングそのものは簡単に済んでしまうケースが多いはずですが、3つのプラットフォームに対応させようとすると、その差異への対応のほうに多く時間を取られる結果となってしまいます。そのため、単一のプラットフォームでの提供のみで終わってしまうケースも多くあると思われます。

XPlat.VUI とは

そこで、XPlat.VUI を開発しました。

NuGet: www.nuget.org

ソースコードgithub.com

XPlat.VUIは、C# を使って Alexa, Googleアシスタント, Clova それぞれのプラットフォームに対応したスキルを共通の実装で開発できるようにしたライブラリです。
これを使えばそれぞれのプラットフォームでの差異を意識することなく、スキルの処理本体の実装に注力することができます。

基本的な使い方(Azure Functions の例)

Azure Functions でのスキル実装の例を見ていきます。

準備

HTTP トリガーの関数を作成

Visual Studio で Azure Functions のプロジェクトを作成し、HTTPトリガーの関数を作ります。言語は C# です(詳細は省略)。
NuGet より XPlat.VUI のパッケージをプロジェクトに追加しておきます。

AssistantBase 抽象クラスを継承したクラスを用意

以下のように、XPlat.VUI では AssistantBase を継承したクラスを作ります。
このクラス内で、スキルがいつ・どんな応答をするかを組み立てていきます。

public class MyAssistant : AssistantBase
{
}

ロガーなど処理の中で使用したいものをプロパティとして持たせたい場合は、IAssistant インターフェースを使って拡張します。

public interface ILoggableAssistant : IAssistant
{
    ILogger Logger { get; set; }
}

public class MyAssistant : AssistantBase, ILoggableAssistant
{
    public ILogger Logger { get; set; }
}

DI の設定

AssistantBase を継承したクラスをスキルのエンドポイントで使用するのですが、単純に

var Assistant = new MyAssistant();

とするよりも DI(Dependency Injection)によって DI コンテナからもらってくるのがおすすめです。
スキルの実装がエンドポイントと依存せず、実装・テストがしやすくなり、他のサービスクラスとの連携もしやすくなります。

なお Azure Functions で DI するには

が必要なので NuGet で追加・更新しておきます。

Azure Functions での DI は、以下のような Startup クラスを定義します。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using XPlat.VUI;

[assembly: FunctionsStartup(typeof(Sample.Startup))]
namespace Sample
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddAssistant<ILoggableAssistant, MyAssistant>();
        }
    }
}

エンドポイント関数の実装

以下のようにエンドポイントとなる関数を実装します。
Alexa, Googleアシスタント, Clova それぞれで異なるエンドポイントを用意するため、関数メソッドは3つ用意しています。
また、DI を使用するので クラス、メソッド双方から static を外し、コンストラクタで AssistantBase 継承クラスを受け取っています。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using XPlat.VUI;
using XPlat.VUI.Models;

namespace Sample
{
    public class Endpoints
    {
        private ILoggableAssistant Assistant { get; }

        public Endpoints(ILoggableAssistant assistant)
        {
            Assistant = assistant;
        }

        [FunctionName(nameof(GoogleEndpoint))]
        public async Task<IActionResult> GoogleEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            var response = await Assistant.RespondAsync(req, Platform.GoogleAssistant);
            return new OkObjectResult(response.ToGoogleAssistantResponse());
        }

        [FunctionName(nameof(AlexaEndpoint))]
        public async Task<IActionResult> AlexaEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            var response = await Assistant.RespondAsync(req, Platform.Alexa);
            return new OkObjectResult(response.ToAlexaResponse());
        }

        [FunctionName(nameof(ClovaEndpoint))]
        public async Task<IActionResult> ClovaEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            var response = await Assistant.RespondAsync(req, Platform.Clova);
            return new OkObjectResult(response.ToClovaResponse());
        }

    }
}

それぞれのエンドポイントで、

  • ロガーの受け渡し(任意)
  • RespondAsync メソッドのコール(プラットフォーム種別を指定)
  • 各プラットフォームに合わせたレスポンス変換・返却

を行っています。この部分は基本的に定型なので、毎回同じようなコードになるはずです。

スキル処理本体の実装

続いて、スキルの応答の中身を作っていきます。この部分がスキル構築の核になります。

リクエスト/イベントベースの処理

AssistantBase 継承クラスでのスキル実装は、起動やインテントリクエストなど、タイミングごとに用意された仮想メソッド(RespondAsync 内で実行される)をオーバーライドして行います。

例えば、起動時とインテントリクエストで何か処理を行いたい場合は以下のように OnLaunchRequestAsyncOnIntentRequestAsync をオーバーライドし、それぞれ処理を実装します。

public class MyAssistant : AssistantBase
{
    protected override Task OnLaunchRequestAsync(
        Dictionary<string, object> session, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    protected override Task OnIntentRequestAsync(
        string intent, Dictionary<string, object> slots, Dictionary<string, object> session,
        CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

OnIntentRequestAsync では インテント名やスロット(エンティティ)の値が引数から受け取れるので、処理の中で使用することができます。

XPlat.VUI では、3つのプラットフォームそれぞれで異なるリクエスト種別、イベントのタイミングをなるべく共通化して差異を吸収し、以下のメソッドを用意しています。

メソッド タイミング 備考
OnLaunchRequestAsync 起動時
OnIntentRequestAsync インテントリクエス Googleアシスタントは起動時以外
OnAudioPlayStartedAsync AudioPlayer での再生開始時 Alexa, Clova のみ
OnAudioPlayPausedOrStoppedAsync AudioPlayer での一時停止または停止時 Alexa, Clova のみ
OnAudioPlayNearlyFinishedAsync AudioPlayer での再生終了直前 Alexa のみ
OnAudioPlayFinishedAsync AudioPlayer での再生終了時 Alexa, Clova のみ

レスポンスの実装

オーバーライドメソッドの中では、スキルの応答内容を定義します。応答内容は Response プロパティに対して追加していきます。応答内容の追加に使用できるメソッドには、以下のものがあります。

Speak

スマートスピーカーが読み上げる、通常のテキストのレスポンスを定義します。

Response.Speak("こんにちは");
Play

mp3 オーディオファイルを再生します。効果音などでの使用を想定しています。

Response.Play("https://dummy.domain/myaudio.mp3");
Break

レスポンスの読み上げ・再生の間の休止(無音の空白)を「秒」で指定します。

Response
    .Speak("こんにちは")
    .Break(3)
    .Speak("サンプルスキルへようこそ");

Speak, Play, Break は、Clova を除き内部的に SSML に変換してそれぞれのレスポンスを作っていますが、Clova は SSML に対応していません。

そのため、特に break については標準機能に存在しないため、Clova の場合は指定秒数無音 mp3 を流すレスポンスを返すことで疑似的に SSML の break と同様の機能を実現しています。
ここで使用している無音 mp3 ファイルは silent-mp3リポジトリGitHub Pages として公開したものを使用しているので、スキルのコードやバックエンドサーバーに配置する必要がありません。

KeepListening

スキルの応答後、さらにユーザーによる返答を待つように指定します。
Alexa, Clova では ShouldEndSession の切り替えをし、Googleアシスタントでもレスポンスの expectUserResponse を切り替えています。

引数で Reprompt のメッセージを指定することができます。

Response
    .Speak("お元気ですか?")
    .KeepListening("元気かどうか教えてください。");
メソッドチェーン

これらは以下のようにメソッドチェーンを用いてワンライナーで書くことができます。

Response
    .Speak("お元気ですか?")                     // Speek simple text
    .Break(3)                                   // Pause for 3 seconds.
    .Play("https://dummy.domain/myaudio.mp3");  // Play mp3 audio.

ユーザーID/デバイスID

Request.UserId でユーザーID を、Request.DeviceId でデバイスID を取得できます。

Googleアシスタントでは、ユーザーID は XPlat.VUI で生成した文字列を疑似的なユーザーID として使用しています。また、Googleアシスタントでは現在デバイスID の取得には対応していません。

応用的な機能

そのほか、スキル開発の幅を広げる応用的な機能も持ちます。

プラットフォームごとに異なる処理を行う

3プラットフォームでコードを共有し処理を共通化しても、どうしても各プラットフォームで異なることをしたいことがあります。

たとえば、プラットフォームごとに異なるレスポンスを返したいことがあります。しゃべらせたいセリフを変えたり、流す効果音を変えたりすることが考えられます。
サウンドライブラリーなどはプラットフォームごとに異なるものが用意されていますし、しゃべり方もプラットフォームごとに違うため、言い回しを変えたりすることが考えられます。

そこで、Speak, Play, Break, PlayWithAudioPlayer では引数に Platform.GoogleAssistant などを渡すことで、指定したプラットフォームのみに含まれるレスポンスを定義することができます。
これも通常のレスポンス同様メソッドチェーンで書くことができるので、プラットフォームで異なるレスポンスを直観的に定義できます。

Response
    .Speak("私はグーグルです。", Platform.GoogleAssistant)
    .Speak("私はアレクサです。", Platform.Alexa)
    .Speak("私はクローバです。", Platform.Clova);

また、それ以外にロジックそのものを動作しているプラットフォームで切り替えたい場合は、Request.CurrentPlatform で分岐させることができます。
以下は、Clova の場合だけ LINE Bot(Messaging API)で Push メッセージを送信する処理を実装する方法です。

if (Request.CurrentPlatform == Platform.Clova)
{
    await lineMessagingClient.PushMessageAsync(Request.UserId, "Hello!");        
}

オーディオ再生(AudioPlayer / Media responses)

PlayWithAudioPlayer を使うと、AudioPlayer(Alexa, Clova)や Media responses(Googleアシスタント)を使ってオーディオコンテンツの再生が可能です。
通常レスポンスの Play でも mp3 再生は可能ですが、通常レスポンスでは再生時間の制限があるため専用のオーディオ再生機能がそれぞれのプラットフォームで用意されています。この機能はそれを共通化したものです(Googleアシスタントの Media responses でのオーディオ再生レスポンスの実装が特に辛かった…)。

Response.PlayWithAudioPlayer(
    "sample-id",                          // audio item id
    "https://dummy.domain/myaudio.mp3",   // audio url
    "Sample Title", "Sample Subtitle").   // タイトルなど

この部分は特に3プラットフォームでの差異が激しいので、複雑なイベント制御によるスキル構築をコード共有することは現状難しいです。
そもそも Googleアシスタントでは再生イベントのハンドリングは XPlat.VUI でも対応していないので、個別にインテントを作って処理する必要があります。

今後の機能追加予定

エンドポイントの共通化

現在3プラットフォームごとにエンドポイントとなる関数を別々に定義する必要がありますが、これを共通して1つで済ませられるようにできたらと考えています。

使用イメージとしては以下のようになります(RespondAsync内で送信元のプラットフォームを判別し、適切なレスポンスを返却する)。

namespace Sample
{
    public class Endpoints
    {
        private ILoggableAssistant Assistant { get; }

        public Endpoints(ILoggableAssistant assistant)
        {
            Assistant = assistant;
        }

        [FunctionName(nameof(CommonEndpoint))]
        public async Task<IActionResult> CommonEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            return await Assistant.RespondAsync(req);
        }
    }
}

プロジェクトテンプレート

DI 部分とエンドポイントの実装をあらかじめ済ませた状態のテンプレートを作っておくことで、スキル開発者が 100% スキルの処理の実装に集中できるようにしたいと考えています。
まずは Azure Functions のテンプレートから作ろうと思いますが、折を見て ASP.NET Core や、AWS Lambda 用のものなども作れたらいいなと思います。

画面対応

将来的には Echo Show や Nest Hub, Clova Desk などのスマートディスプレイ対応も共通化できたらおもしろいなと思っています。Clova はまだ SDK が公開されていなかったり、どのような実装になるかは全く見当もつきませんが、いつかやってみたいと考えています。

まとめ

XPlat.VUI を使えば、簡単に3プラットフォーム対応のスキルを開発できます。

また、C# と Azure を用いることで、言語やクラウドの便利なところはもちろん、Visual Studio による恩恵によってもとても快適な開発が可能です(もちろん VS Code でもOK)。Durable Functions 等による応用的なスキルも開発できます。

スキル開発者は、作りたいものを形にすることに注力し、3プラットフォームに展開することでより多くのユーザーにスキルを使ってもらうことができます。

ぜひ XPlat.VUI を用いて様々なスキル開発に挑戦してほしいなと思います。不具合や要望、プルリクなども受け付けておりますので、ぜひ C# でのスキル開発に興味がある方は使ってみていただけるとうれしいです。