himanago

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

LINE BOOT AWARDS 2018 ファイナルステージの振り返り

はじめに

前回の記事でも書きましたが、LINE BOOT AWARDS 2018にエントリーし、2018/11/10(土)開催のファイナルステージに出場してきましたので、その報告と振り返りを書いておきたいと思います。
※つくったものとかは前の記事の内容や応募作品ページをご参照ください

結果

まずは結果から。

  • 展示審査にて落選(決勝の決勝、プレゼン審査には進めず…)
  • エーアイさんのAPIパートナー賞受賞

となりました。

振り返り

エントリーについて

前の記事にも書きましたが、

  • Clova Friendsが当たったこと
  • Alexaのアワードを観たこと
  • Cogbot勉強会のLTに触発されたこと

がエントリーのきっかけです。
Clovaが手元にあるから「何か開発したいな」と思え、Alexaのイベントで「自分も機会があれば参加してみたいな」、 LTで「短期間でも応募しようとする人がいる」という話を聞いたという3連コンボで、見事にエントリーまで一直線の気持ちになりました。
ひとつでも欠けていたらエントリーはなかったですし、結果として多少なりとも評価をいただけたので、本当にいい流れだったなと思います。

開発で気を付けたこと

エントリーまでの開発期間は実質4日間のみでしたが、短期といえどきちんと戦略を立てました。それは、「VUI/Botならではのサービス」を意識したことです。
事前にAlexaのアワードのファイナルを見に行っていたこともあり、スマートスピーカーのスキルを作るならば、スマートスピーカーを使う意義が絶対に不可欠であり、それが審査においても重要だと考えていたからです。

特にVUI(Voice UI)は音声のやりとりでシステムとのI/Oを実現するため、使い方によっては著しくUXが下がります。
「それアプリでよくない?」「Webのほうがいい」「チャットボットでやったほうが楽」と思ってしまうようなものをVUIで実装してしまうと、だれも使ってくれなくなります。「手が使えないときはVUIのほうが楽だからVUIのスキルが必要」とかそういう理由が必要ということですね。

また、今回はLINEのアワードですし、評価ポイントにも含まれていた「LINEらしさ」も意識しました。

なので「VUIである必要性」「LINEならではのメリット」は絶対に入れようと思い、逆にそこさえクリアできればある程度のところまでは進めるだろうという確信もありました。
ファイナル進出自体が24/1125ということでしたが、ここまで残れたので今回とった戦略は間違ってなかったんだなと、安心しました。

技術面の工夫

やはりバックエンドをAzureにサーバーレスでつくった点です。
慣れたAzureでの、手軽に開発できるFunctionsのおかげで、短期開発を実現できました。 (ずっと使いたかったCosmos DBも今回はじめてちゃんと触れました。それもよかった点です)

また、VUI開発は非常に制約が多いので、ちょっとした技術的アイディアひとつで他とは一味違うサービスを作ることができるのではとの考えから、Clovaの使い方で技術的な工夫を入れることを意識しました。

そのひとつが今回のサービスの肝だった「Clovaの音声とBotへのメッセージ送信の同期」で(予選突破時のフィードバックでもLINEの方からお褒めの言葉をいただきました)、もうひとつが、予選突破後・決勝前に追加した「Clovaスキルの一時停止&再開」です。

細かい話はここでは触れませんが、技術的に結構おもしろいと思うので、その話は12/23にQiitaの LINEBot&Clova Advent Calendar 2018 に書く予定です。

展示審査のこと

決勝は、プレゼン審査の予選として展示審査がありました。チームごとにブースが用意され、そこで審査員や一般のお客さん(一般投票もあり)に作品を見ていただきます。
展示審査はデモを中心に見せていきました。 が、一般のお客さんも含め、来ていただいた人にはかなり好意的に見ていただけたようです。
こういった経験ははじめてだったので、「すげー!」とか言われるととてもうれしいですね。
結果はここで落選ではありましたが、控えめに言って、展示審査、めちゃくちゃ楽しかったです。

反省点は、展示ブースが地味だったことです。
用意された模造紙を貼ったくらいで、ほかに資料とか飾りとか、なにも用意できなかったところが、ちょっと他とくらべて映えなかったかなと思います。もっとたくさんの人に来てほしかった…。
完全に一人での参加でしたし、しゃべりとデモのみでの勝負という感じで、けっこう厳しいなと思いながらやってました。
準備もぎりぎり。ごはんやトイレの時間もとれず。こういうところはチーム参加のところがうらやましく思えましたね。

最終プレゼンを見て

レベルが違う。それに尽きました。
自分が展示審査まで残ったのが奇跡かと思うくらい、自分のつくったものとは力の入れ具合が違いました。
数日で作ったものを持って行って、勝てるかもとか勝ちたいとか一瞬でも思って申し訳ありません、、、汗

プレゼンも含め本当に素晴らしく、圧巻でした。特にグランプリの高校生。すごすぎです。
プレゼンが高校生とは思えないほどの完成度と話し方で、しかもプロダクトはユーザーテストを重ねに重ねクオリティを上げている。
勝てるわけないですねこんなの。自分もそれを超えるくらいの仕事やプロダクトづくりがしたいなと思いました。大尊敬。

APIパートナー賞受賞について

本当にありがたかったです。
アワードの応募締め切りぎりぎりに「APIを使いたい」という連絡に快く対応いただいた点も含め、すばらしいAPIを提供いただいた株式会社エーアイさんには本当に感謝です。
展示ブースにも来ていただきまして、本当にうれしかったです。ありがとうございました。

今後の展望

LINEの方にもブラッシュアップしてね、と言われたりしたのですが、エーアイさんのAPIが法人利用のみだったり、Azureの運用コストがけっこう高かったりでなかなか正式リリース、とかを考えるのはハードルが高いかなぁと思ってます。
展示審査でも何人かのかたにもうリリースされているんですが?と聞かれ、(リリースできる見込みもないけど)「まだなんです…」と答えたりしてました。
ただ、

ということもあり、ちょっと続けてみようかなと思ってます。
APIの利用期限が間近なため正式リリースはすぐには無理ですが、今回の作品を大きく進化させるなり全く新しいものを作るなりして次のアワードに再挑戦したいと思います。少なくとも、実サービスとして運用しているレベルでないと、勝負にならないということが今回の最大の学びなので、次に挑戦する際にそこは妥協できないなと思います。

おわりに

最後に、11/11を記念して、プリッツ服を着たうちのClovaの写真で締めたいと思います。
プリッツの懸賞で当選させてくれたグリコさんにもこの場を借りて感謝です。

以上です!

LINE BOOT AWARDS 2018にエントリーしてみました&その話を.NET Conf 2018 Tokyoでしてきました

はじめに

ここに書くのはだいぶ久しぶりです…
先日、LINE BOOT AWARDS 2018にClovaスキル&LINE Botを開発してエントリーしたので、そのことと、 それについて.NET Conf 2018 TokyoのUnconference枠で話をしてきたので、それについて書いておこうと思います。
(.NET Confでは10分枠だったのでその補足もかねてその時よりも若干詳しく、スライドベースで書いていきます。スライド資料はここ

スマートスピーカー

f:id:himanago:20181021011841p:plain

ちらほら流行り始めている(気がする)スマートスピーカー
エンジニア界隈では使っている人も見ますが、一般家庭への浸透率ってどの程度なものなんでしょう。

LINE BOOT AWARDSへの挑戦

f:id:himanago:20181021011857p:plain

個人的にも気になっていたスマートスピーカー。LINEの「Clova」がプリッツの懸賞で当たりました。
プリッツ5箱分食べると1回応募できるという具合だったので、この懸賞のためにプリッツを食べ続ける覚悟もしていましたが、2回目の応募で当たったのでかなり運がよかったです…!

f:id:himanago:20181021011910p:plain

せっかくスマートスピーカーが入手できたので、それならばスキルの開発をしてみたいと思うのがエンジニアというもの。

f:id:himanago:20181021011918p:plain

ちょうど、LINE BOOT AWARDS 2018という開発コンテストが開催中でした。
グランプリの賞金が1000万円とものすごい額。そこまでは狙えないにしろ、どこまでいけるか、挑戦してみることにしました。

f:id:himanago:20181021011925p:plain

とはいえ、スケジュールはものすごくタイトでした。 Clovaが届いたのが9/23(日)。アワードのエントリーに締め切りが10/10(水)。
これだけでずいぶん短いですが、Clovaが届いたときには、まだ特にアワードに応募することなんて考えてもいませんでした。

応募するきっかけをくれたのが、2つのイベントです。
(スライドには書いていないのですが)ひとつは9/29(土)に実施された、Amazonが出しているスマートスピーカーである「Alexa」のアワードの最終プレゼンを見に行ったこと。
ここで結構刺激を受けてきまして、いつか自分もこういうアワードに挑戦してみたいなぁと思ったわけです。 alexaskillawards.connpass.com

もうひとつが、こちらはスライドに書いていますが10/2(火)に参加したCogbot勉強会。
ちょうどLINE BOOT AWARDSとのコラボ回だったのですが、ふつうにLINE系開発の情報収集目的で参加しました。
しかしそこでアワードの概要やC#での開発についての話、また最後のLTで短期間でもAzureを利用してアワードに挑戦するという話(これが自分にはいちばん響きました)を聞いて、これは自分もアワードに挑戦すべきなのではないか…?と思わされてしまいました。 cogbot.connpass.com

そして、10/4(木)、突然、シャワーを浴びているときに、作るもののアイディアが降ってきました。
(入浴中とかって結構アイディアを思いつくこと多いですよね。なんででしょう)
思いついたらもう最後、3連休を利用して一気に作ってやろうと決意を固めました。

3連休ではアワードのもくもく会への参加をし、また締め切り日には有休を取得して最後の追い込み。実質4日間での開発でした。

どんなものを作ったか

f:id:himanago:20181021011934p:plain 絵本読み聞かせのシステムです。
特徴としては

  • お話を読んでくれると同時にLINEにその絵が送られてくる
  • 登場人物ごとに異なる声/話し方でしゃべってくれる

というものです。

デモ動画を置いておきます。 www.youtube.com

Clovaのオーディオ再生機能とAITalk Web APIを組み合わせ、音声再生を連続して実行し、同時にLINEに画像を送っています。

仕組みなど

そのほか、アーキテクチャまわりはアワード応募時の技術解説として書いた以下の記事に書いています。
締め切り当日に一気に書いたのであんまりまとまってないですが…。 qiita.com

今回開発したのは、スマートスピーカーのスキル(Clovaスキル)と、LINE Botを連携させています。
Clovaの特徴のひとつとしてLINE Botとの連携があり、それぞれの可能性を拡げてくれます。
ClovaスキルとLINE Botの開発は似ています(ここでは触れませんがClovaやLINE以外のVUI/Botもかなり似通った利用フローや開発方法といえると思います)。

ClovaスキルとLINE Botの開発方法・今回の構成

f:id:himanago:20181021011955p:plain Clovaスキルの場合は「CEK」がデバイスと自分の作成したバックエンド(Extensionサーバー)との仲介をしてくれます。
対話の定義はCEK上で行い、ユーザーの発話の解釈などはすべてやってくれます。

f:id:himanago:20181021012011p:plain 対してBotのほうは「Messaging API」が仲介をしてくれるのですが、こちらはPush/Replyといったメッセージ送受信を中継してくれるだけです。
なので、ユーザーから送られたメッセージの解釈などは自分で開発する必要があります(バックエンドでやる)。

f:id:himanago:20181021012019p:plain さきほどの技術解説記事でも説明していますが、今回の構成図はこのようになっています。
Clova/LINE Botの共通のバックエンドとしてAzure Functionsを用いています。
読み上げる絵本の情報(本文、読み手がだれか、感情やスピードなど各種パラメータ)をCosmos DBにMongoDBの形式で格納し、本文にあわせて送られる絵の画像はBlob Storageに格納しています。

Azure Functionsによるバックエンド

f:id:himanago:20181021012026p:plain Functionsは非常に便利なサーバーレスのサービスですが、今回はサーバーレスらしい従量課金プランではなく、App Serviceプランでの稼働です。
通常のWebアプリと同じように常時稼働させる形式をとることで、従量課金プランの弱点である初回起動の遅さを克服しています。
Clovaスキルは特にタイムアウトが天敵なので、この選択は必須といえるかなと思います。

[※2018/11/7追記]
App Serviceプランにするだけではだめで、常時接続をオンにする必要があります。オフだとしばらくするとサービスが止まってしまい関数起動が遅くなってしまいます。
常時接続オンにできるのはS1以上なのでちょっとお高め。

[※2018/11/23追記]
Azure Functions のv2を使って再作成していますが、触った感触、コールドスタート問題、かなり改善されている…?
もしかしたら従量課金プランでもClovaのバックエンドとして問題なく使えるかも…。検証して再度追記します。
IPアドレスの問題があるので今回の要件ではどちらにしろApp Serviceプランにしないとですが

[※2018/11/25追記]
何度か試しましたが、成功率はかなり高くなっているようです(コールドスタートで3/4回成功)。
とはいえタイムアウトすることもまだあるので、そのままでは本番運用にはまだ厳しいような気がします。
Azure Functionsはv2からランタイムが.NET Core になってパフォーマンス改善された結果なんでしょうか。

azure.microsoft.com

[※2018/12/20追記]
Logic Appsを利用してコールドスタートを回避するパターンを思いついたので、Qiitaに書きました。
これでコールドスタートを気にする必要がなくなります。 qiita.com

(追記おわり)

また、今回は絵本を無料/有料2種類のコンテンツがある想定にしており、有料のものはLINE Payで追加購入できるようにしています。
そのため、LINE Payとの連携も行っているのですが、LINE Payは連携するシステムのサーバーのIPアドレスを事前に登録しておく必要があります。
App ServiceプランではIPアドレスが変わらないので、この点でも必須なのでは、と考えました。
(こちらの記事を参考にさせていただきました) poke-dev.hatenablog.com

Azure Functionsのリモートデバッグタイムアウトになったり迷子になったりするのでなかなか使えず、コンソール出力を使ってのデバッグがメインでした。はじめはつらかったものの、慣れればどうにかなるものでした。

BlobストレージとLINE Botの画像送信

f:id:himanago:20181021012037p:plain このスライドに書いてある通り、Blob StorageとLINE Botの画像送信は相性がいいです。
通常の画像メッセージでも今回使ったイメージマップメッセージでも、画像はバイナリデータではなくURLの指定で行います。
Blob Storageは上げたデータをURLでアクセスさせることができるので、そのまま利用できるという寸法です。

f:id:himanago:20181021012049p:plain さきほども少し触れましたが、今回は通常の画像メッセージではなく「イメージマップ」を使っています。
もともとは画像に対してタップした際のアクションを定義するLINE Botの機能ですが、トーク画面に対してめいっぱいの幅を使って画像を表示させることができるため、今回の絵本の絵の送信に適しています。もくもく会にてアドバイスいただいたのですが、これによって非常に絵本が読みやすくなりました。

このイメージマップですが、デバイス幅にあわせた画像が表示される機能で、利用するには複数の幅の画像を事前に用意しておく必要があります。
Messaging APIリファレンスからの引用ですが、

イメージマップに使える画像の仕様は以下のとおりです。

画像フォーマット:JPEGまたはPNG 画像の幅:240px、300px、460px、700px、および1040px データサイズ:最大1MB 5つの異なるサイズの画像を「baseUrl/{画像の幅サイズ}」の形式でアクセスできるようにします。これにより、デバイスに応じて望ましい解像度でLINEに画像がダウンロードされます。

たとえば、ベースURLが以下の場合を考えます。

https://example.com/images/cats

幅が700pxの画像のURLは以下になります。

https://example.com/images/cats/700

となっており、Blob Storageへの格納は、スライドに書いた通り、これに沿うようにする必要があります。
(ファイル名を拡張子を外した画像の幅サイズの数字にしてディレクトリ構成を工夫するだけ)

C#出のLINE開発について

f:id:himanago:20181021012059p:plain

Cogbot勉強会でもC#には厳しい、という話が…。
完全に個人的な意見ですが、Clovaも(AWSとセットの)Alexaに対抗するなら、Azureを味方につけやすいC#との親和性を高めてもよいのではないかなぁと思います。
公式のSDKがないとはいえ、C#で開発する場合もコミュニティSDKや周辺ツールがあります。
今回の開発でも、Messaging API、CEK、LINE Pay用のC#SDKを使わせていただきました。

今回使った機能(Clovaのオーディオ関連など)で一部SDKが対応していないものもありそこは自前で実装しましたが、ベースとなるものがあるだけ開発のハードルはかなり下がり、かなりありがたかったです。

おわりに

f:id:himanago:20181021012107p:plain

今回は一気に開発しましたが、やはり目標や締め切りがあったことで、かなり真剣に取り組めました。
.NET Confで話したい、というモチベーションもありました。
締め切り後の連絡にもかかわらずUnconference枠で参加させていただき、.NET Confの管理者の方には大変感謝しています。ありがとうございました。

そしてアワードの途中経過について

そして!
今回開発した「Clova&LINEで絵本読み聞かせ」ですが、なんとFinal Stage進出の連絡をいただきました!
このような評価をいただけて大変うれしく思います。
Finalは11/10(土)に実施とのことで、ここでは展示審査もあるようなので、さらなるブラッシュアップをしていきたいと思います。

これからもがんばります!
以上です。

【Xamarin.Forms+Cognitive Services】飯テロ防止Twitterクライアント作ってみた

はじめに

はやいものでもう11月になってしまいました。今年もあと2か月弱。
研修などが土曜に多い関係上、なかなか土曜に休むことができない日々が続き、土曜の面白そうな技術系イベントにあまり出られず、ちょっと最近フラストレーションがたまっています。
その憂さ晴らしついでに、今回はちょっと手を動かしてみました。

今回作ったもの

タイトルにある通り、「飯テロ防止Twitterクライアント」です。
Twitterで食べ物の写真をたくさん上げている人をミュートした」ということを言っている後輩がいて、たしかに食べ物の写真は、深夜だったり気分の悪いときに見るのはきついとは思うですが、ミュートしてしまうことで食べ物写真ツイート以外の他の有益な情報まで目に入らなくなってしまうのはもったいないことです。
だったら、食べ物の写真だけうまいこと表示されないような「飯テロ防止」機能を持ったTwitterクライアントがあればよいのでは?と思ったのがはじまりです。
特に実運用は想定していないので、今回は飯テロ防止に関係がある「タイムライン表示」の部分だけ実装しています。

ソースはGitHubに公開しています。
github.com

名前は「FoodPornPreventer」。
preventerはちょっと微妙かもですが、英語で飯テロに近い言葉は「food porn」と言うそう。

アプリのしくみ

まず、Twitterクライアントなので、スマホなどモバイル端末での利用を前提に、Xamarin.FormsでAndroid/iOS/UWPに対応させています。
そこにTwitterクライアントとしての機能を実装するため、「CoreTweet」というパッケージを導入しています。Twitter連携するアプリを作成するうえでの定番のようです。
CoreTweetはこちら。日本語の説明がありました。 github.com

肝心の「飯テロ防止」は、AzureのCognitive Servicesにある「Computer Vision API」による画像解析を用います。
画像を解析してさまざまなことを教えてくれるサービスです。
azure.microsoft.com

流れは、

  1. CoreTweetでTwitterのタイムラインを取得

  2. 画像を含むツイートの場合、その画像URLをComputer Vision APIに渡して解析

  3. 解析結果に「food」という文字列が含まれていれば“飯テロ”とみなし、警告画像に差し替え、それ以外はそのままタイムラインに表示させる

となります。 では実際に中身を見ていきます。

Twitter連携

CoreTweetを導入するには、NuGetでPCLプロジェクトに「CoreTweet」をインストールします。
CoreTweetを入れると「Newtonsoft.Json」のパッケージも一緒に入るのですが、これが最新の「10.x」だとUWPで動きませんでした。なので、最終的にバージョンはCoreTweetが「0.8.1.394」、Newtonsoft.Jsonが「9.0.1」の組み合わせにしています。
また、ターゲットを変更しないとうまく上記がインストールできなかったので、Profileは「7」に変更しています。

では、CoreTweetによるタイムラインの取得処理です。

以下のように生成したアクセストークンを使って、

// トークンの生成
// 今回は事前にTwitterアカウントで作成しておいたアクセス情報を使用
private Tokens Tokens { get; } = Tokens.Create(Secrets.ConsumerKey, Secrets.ConsumerSecret, Secrets.AccessToken, Secrets.AccessTokenSecret);

タイムライン取得を行います。

var timeline = await Tokens.Statuses.HomeTimelineAsync(count: TweetCount, max_id: maxId);

これはTwitter APIの「statuses/home_timeline」に相当します。CoreTweetはこのように、基本的にTwitter APIをそのままC#で呼べるよう実装されているので、Twitter APIさえわかればそれと同じような呼び出し方で使うことができ、とても楽です。

取得したツイートのデータは適当にクラスに詰めてListViewにバインドさせて表示させています。
引用リツイートとか動画とか、そのあたりは今回の本題から外れるのであまり作りこんでいません。

Computer Vision APIでの画像判定

食べ物かどうかの判定ですが、あらかじめAzureでComputer Vision APIを使うためのキー情報を取得しておきます。
Xamarinから呼び出すのは簡単で、NuGetからMicrosoft.ProjectOxford.Visionを導入すればOK。
これでTagsを調べ、飯テロかどうかを判定します。Tagsの要素内のHintまたはNameはが「food」であれば食べ物画像=飯テロとみなします。

private async Task<bool> IsFoodPornAsync(string url)
{
    try
    {
        // Computer Vision APIで解析してTagsを取得
        var vision = new VisionServiceClient(Secrets.VisionSubscriptionKey, Secrets.VisionApiRoot);
        VisualFeature[] features = { VisualFeature.Tags };
        var result = await vision.AnalyzeImageAsync(url, features.ToList());
        return result.Tags.Any(t => t.Hint == "food" || t.Name == "food");
    }
    catch (Exception ex)
    {
        return false;
    }
}

判定処理はわずか4行。お手軽ですね。

サムネイル画像の差し替え

飯テロ画像だったら警告画像に差し替えます。
今回はConverterを使って、Computer Vision APIによる飯テロ判定のフラグ値で切り分けています。

public class ImageMaskConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var imageInfo = value as TweetImageInfo;

        if (imageInfo == null) return null;

        // 飯テロ画像なら画像を差し替え
        return imageInfo.IsFoodPorn
            ? ImageSource.FromResource("FoodPornPreventer.Images.mark_chuui.png")
            : ImageSource.FromUri(new Uri(imageInfo.Url));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

実際の動作

今回の開発にあたり、食べ物画像BOT@ibukurogさんのツイートをテストに使わせていただきました。
たとえば、このツイート。

このツイートを本アプリを通して見ると…

Android

f:id:himanago:20171105011730p:plain:w300

iOS

f:id:himanago:20171105011745j:plain:w300

UWP

f:id:himanago:20171105001349p:plain:w300

上記のように、うっかり飯テロにあうことを防ぐことができます!
ちなみに差し替えたアイコンはいらすとやさんからいただいたものです。
注意のマーク | かわいいフリー素材集 いらすとや

なお、iOSだけ、ツイートの追加読み込みでスクロール位置が戻ったり項目が上下に動いて一瞬重なって見えたりと、ちょっと変な動きをするのでいずれ直したいです。

苦労したところ

ListViewはネストできない

ツイートをListViewで並べて表示しますが、各ツイートのListItemに画像を並べる際、ListViewをネストできたら動的に画像の数が変わってくれると思ったのですが、ListViewはネストすることができなかったので、断念。Twitterでは1ツイートには画像が最大4枚という仕様なので、しかたなく4枠固定で画像の領域を用意してごまかしました。

iPhoneでの実機確認時のエラー①

iPhoneの実機で動かしたときがけっこう苦戦しました。
まず、ビルド時に以下のようなエラーが。
f:id:himanago:20171104234147p:plain

Invalid architecture: ARMv7. 32-bit architectures are not supported when deployment target is 11 or later.

端末はiPhone8(iOS11)。おそらく、iOS11から完全64bit化したため「32bitは使えないよ」というエラーが出たのだと思われます。ここは、iOSプロジェクトのビルド設定でサポートされているアーキテクチャを「ARMv7 + ARM64」から「ARM64」に変更すると解消しました。 f:id:himanago:20171104234206p:plain

あとその下の「すべての32ビット浮動小数点演算を64ビット浮動小数点演算として実行する」のチェックも入れておく必要がありそう。
f:id:himanago:20171105100753p:plain

iPhoneでの実機確認時のエラー②

もうひとつ、実機デバッグ時のみ、Computer Vision APIを呼び出す際に例外が発生しました。シミュレーターを使ったときやAndroid/UWPでは発生しなかったものだったので、最初は「?」でした。

  at System.Linq.Expressions.Expression.Call (System.Linq.Expressions.Expression instance, System.Reflection.MethodInfo method, System.Collections.Generic.IEnumerable`1[T] arguments) [0x00111] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Linq/Expressions/MethodCallExpression.cs:1239 
  at System.Linq.Expressions.Expression.Call (System.Linq.Expressions.Expression instance, System.Reflection.MethodInfo method, System.Linq.Expressions.Expression[] arguments) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Linq/Expressions/MethodCallExpression.cs:1046 
  at System.Linq.Expressions.Expression.Call (System.Reflection.MethodInfo method, System.Linq.Expressions.Expression[] arguments) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Linq/Expressions/MethodCallExpression.cs:1001 
  at System.Dynamic.ExpandoObject+MetaExpando.BindSetMember (System.Dynamic.SetMemberBinder binder, System.Dynamic.DynamicMetaObject value) [0x00033] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Dynamic/ExpandoObject.cs:863 
  at System.Dynamic.SetMemberBinder.Bind (System.Dynamic.DynamicMetaObject target, System.Dynamic.DynamicMetaObject[] args) [0x00035] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Dynamic/SetMemberBinder.cs:57 
  at System.Dynamic.DynamicMetaObjectBinder.Bind (System.Object[] args, System.Collections.ObjectModel.ReadOnlyCollection`1[T] parameters, System.Linq.Expressions.LabelTarget returnLabel) [0x000c6] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Dynamic/DynamicMetaObjectBinder.cs:90 
  at System.Runtime.CompilerServices.CallSiteBinder.BindCore[T] (System.Runtime.CompilerServices.CallSite`1[T] site, System.Object[] args) [0x00019] in <773264786149499a986a13db6a7d46fe>:0 
  at System.Runtime.CompilerServices.CallSiteOps.Bind[T] (System.Runtime.CompilerServices.CallSiteBinder binder, System.Runtime.CompilerServices.CallSite`1[T] site, System.Object[] args) [0x00000] in <773264786149499a986a13db6a7d46fe>:0 
  at (wrapper managed-to-native) System.Reflection.MonoMethod:InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&)
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00032] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/corlib/System.Reflection/MonoMethod.cs:305 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:151 
  at System.Linq.Expressions.Interpreter.ExceptionHelpers.UnwrapAndRethrow (System.Reflection.TargetInvocationException exception) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/Utilities.cs:174 
  at System.Linq.Expressions.Interpreter.MethodInfoCallInstruction.Run (System.Linq.Expressions.Interpreter.InterpretedFrame frame) [0x00035] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs:333 
  at System.Linq.Expressions.Interpreter.Interpreter.Run (System.Linq.Expressions.Interpreter.InterpretedFrame frame) [0x00015] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/external/corefx/src/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/Interpreter.cs:63 
  at System.Linq.Expressions.Interpreter.LightLambda.Run3[T0,T1,T2,TRet] (T0 arg0, T1 arg1, T2 arg2) [0x00038] in <773264786149499a986a13db6a7d46fe>:0 
  at Microsoft.ProjectOxford.Vision.VisionServiceClient+<AnalyzeImageAsync>d__19.MoveNext () [0x00053] in <26ced5c9a33f420b960f06b6963aca5a>:0 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:151 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00037] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <a89624c267f94034b6cf9aa0c56f8864>:0 
  at FoodPornPreventer.MainPage+<IsFoodPornAsync>d__11.MoveNext () [0x00050] in C:\Users\hoge\Documents\Visual Studio 2017\Projects\FoodPornPreventer\FoodPornPreventer\FoodPornPreventer\MainPage.xaml.cs:116 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:151 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00037] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/11.2.0.11/src/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:357 
  at FoodPornPreventer.MainPage+<AddTweets>d__10.MoveNext () [0x003a2] in C:\Users\hoge\Documents\Visual Studio 2017\Projects\FoodPornPreventer\FoodPornPreventer\FoodPornPreventer\MainPage.xaml.cs:85 

調べてみると、どうやらリンカーの動作を「Don't Link」にすると解決するらしい、ということがわかりました。 www.syncfusion.com github.com

iOSプロジェクトのプロパティで以下のように変更。 f:id:himanago:20171105095756p:plain

すると、無事実機デバッグできるようになりました。
ただこれだとアプリのサイズが大きくなってしまうと思われるため、以下のようにほかの回避方法もありそうですし、もう少し調べてみたいなと思っています。 spiratesta.hatenablog.com

その他

Xamarinの開発をするときは、普段メインPCをWindowsでやっていますが(iOSやるときはMac Book Airをリモートでつないでる)、実機デバッグが楽なのはUWP>AndroidiOSなので、どうしてもiOSの動作確認が最後になってしまい、個人的にそこで詰まるケースが結構多いです。普段使っているスマホiPhoneなので、ちゃんとiPhoneでも使えるようにしたいところではあるのですが。

あと、今回はなるべくシンプルに作りたかったのでコードはほとんどコードビハインドに書いてみました。ただ、これが失敗だったのか、結局コードビハインドでいろいろ頑張ることになってしまい、シンプルというよりは見通しが悪くなってしまったような印象。
拡張性とかも考えて、ちゃんとMVVMでやったほうがむしろ「シンプル」できれいになるのかも、と思いました。のちほど、Prismを使って再実装してみようかとも思います。

JAZUG 7周年総会で「社内でIoT&AIな農業系チャットボット開発講座をやってみて」というテーマで登壇してきました

はじめに

先日、9/9(土)に行われたJAZUG 7周年総会にて、50分のセッションで登壇させていただきました。
そしてこのたび、そのときのセッション動画が公開されましたので、少し間が空いてしまいましたが、簡単に内容の紹介と振り返りを書いておきたいと思います。

イベントの概要

JAZUG 7周年総会のイベントページ(connpass)はこちらです。 jazug.connpass.com

Microsoft AzureのユーザーコミュニティであるJAZUG(Japan Azure User Group)の7周年を記念したイベントということもあり、たくさんの方が参加されたイベントでした。

仕事以外のコミュニティで話をするのは4月にやったJXUG関連の初心者LT以来2度目でしたが、今回は参加人数も多く、Azureを長く使われている方も多くいる中での登壇でしかもロングの本編登壇だったので、しっかり準備しなければいけないと思い、内容については前日ぎりぎりまで悩みながら資料を作りました。

発表内容と資料・動画

タイトルは「社内でIoT&AIな農業系チャットボット開発講座をやってみて」
内容はタイトルどおり、社内でIoTやAIに絡めた講座をやった際に使ったAzureのサービスに関することを中心にお話しさせていただきました。

資料はSlideShareに上げています。

www.slideshare.net

動画はこちらです。 crash.academy

社内でやったこの講座では、植物に取り付けたIoTデバイスRaspberry Piなどで作成)が湿度データと植物を撮影した写真データを定期的にAzureに送り、そのデータを使ってLINE Botを介してユーザーと会話をする、というシステムを作りました。
f:id:himanago:20170924221756p:plain

使ったAzureのサービスも多種多様で、以下のような構成です。 f:id:himanago:20170924221802p:plain

その他、細かい内容は資料・動画を見ていただくとして、ここからは登壇にあたってどういうことを考えて準備したかや、実際に登壇してみての感想・反省などの振り返りをメインに書いていきます。

登壇のきっかけ

7月のJAZUGの勉強会に参加させていただいた際、登壇面白そうだなと思ったので、アンケートで「登壇希望」と書きました。

ちょうど昨年11月~今年3月にかけて社内でAzureのサービスを多く利用したIoT講座をやっていたりしたので、それがネタになるだろうと思ってのことです。
その後、JAZUGの方から連絡があり、予定するセッション内容などをお伝えしたところ、8月のお盆休み中に、JAZUGの7周年総会でセッションを担当できないか、と正式な依頼をいただきました。

7周年という大きな節目のイベント、しかもあまり準備日数がなさそうということで、少し迷いましたが、むしろこんな舞台でセッションをさせていただける機会はめったにないと思い、ありがたく登壇させていただくことにしました。

準備(ターゲット設定)

まず、今回登壇させていただくにあたり、たくさんの方が参加されるイベントなので、きっちり話す内容を検討しないといけないと思い、以下のようなターゲット設定を行いました。

ターゲット①:Azure歴の長い方

7周年総会ということで、当然、Azure歴の長い方が多く参加されると思いました。そういった方にも飽きずに聞いていただけるよう、以下のような話を盛り込むことにしました。

  1. Azure以外のネタ
  2. Azureのよく使うサービスだけど意外と知らないかもしれないこと
  3. Azure歴の短い人間から見たAzureの進化

1.は、今回UI部分にLINE Botを使ったので、Azure FunctionsやCognitive Servicesを使ってLINE Botを作れるという点、またLINE Beaconなどを紹介することで満たせました。
特にCognitive Services(LUIS、Computer Vision API)がLINE Botと相性が良く、事例として紹介したいと思っていたところでした。

2.については、Cognitive ServicesのComputer Vision APIのサムネイル生成機能あたりがそれに該当します。Computer Vision APIは画像の分析が注目されるAPIなので、自分自身もサムネイル生成もできるというのは少し意外だと感じたためです。
この機能は今回の教材システムのなかでも大きな役目を担っていますが、スライドでいうとこの部分。 f:id:himanago:20171001212606p:plain

少しここで補足しておくと、LINE Botでは、画像をユーザーに送る際には、いくつか注意点があります。
まず、画像の「URL」を教えてあげる必要があるという点です(公開されている必要がある)。今回送りたい画像(植物の写真)はBlob Storageに入れていましたが、Blob StorageではそれぞれのデータにURLが割り当てられるので、LINE Botで送信可能でした。

もうひとつは、LINE Botで画像を送る場合はオリジナルの画像URLとは別にサムネイルの画像URLも必要だという点です。
オリジナル画像のみでも画像の送信は可能ですが、サムネイルをセットで渡してあげないと、ユーザー側のLINEのトーク画面に、重いオリジナル画像がそのまま表示されてしまいます。これでは、画像のサイズが大きいときにトーク画面になかなか画像が表示されずストレスになってしまいます。そこで、トーク画面にはサイズの軽いサムネイルを出し、それをタップすると高解像度のオリジナル画像が表示される、というようにしてあげる必要があるのです。

今回、それはComputer Vision APIを組み合わせることで実装することができました。
LINEから「写真みせて」とメッセージを受け取ったら、Blob Storageに格納してある写真のオリジナルデータのURLをComputer Vision APIに渡し、サムネイル生成を依頼します。するとサムネイルの画像データを送り返してくるので、それをBlob Storageのサムネイル用コンテナー(オリジナル画像の入っているコンテナーとは別)に入れ、そのサムネイル画像のURLと、オリジナル画像のURLをセットでLINEのMessaging APIに送り返します。そうすることで、Messaging APIからユーザーのLINEに画像がサムネイルつきで送られることになります。Blob Storage+Computer Vision APIのサムネイル生成がどんぴしゃでこの実装に使えて、当時はちょっと感動しました。

3.は、Functions開発の進化についてです。
講座当時にはなかったVisual Studio 2017との連携について触れ、「わずか7ヶ月で進歩が感じられるほどAzureは常に進化している」ということを(確認的に)伝え、Visual StudioでのFunctions開発をデモすることにしました。

ターゲット②:今回触れるAzureサービスにそこまでなじみがない方

イベントに参加されるのはAzureに詳しい方だけとは当然限らないので、今回話に出てくるAzureのサービスをあまり知らない/使ったことがない人に向けても、「そのサービスの概要」「使う利点」などを伝えられればと思いました。
こういうコミュニティの参加者の前提知識をどこに設定するかというのは難しい問題だと思いますが、あまり基本的な話をしすぎると最初のターゲットと設定した人が退屈に感じてしまうので、今回は話の中で登場するAzureのサービスについてはひとこと説明をスライドに載せておくことで対応することにしました。

反省

「つかみ」は大事

今回、スライドのデザインはJAZUGでよく使われているものをカスタマイズさせていただいたのですが、これに関連して、 「農業関連の話なのでスライドの下のほうに草生えてます」と最初のつかみで言うのを忘れてしまいました…。

ウケるにしてもウケないにしても、こういう「つかみ」のネタをやって空気感の確認をやっておくと、話す側としてもペースをつかみやすくなるんですよね。 今回はそれができなかったこともあって、いつも通りな話し方のペースをつかむまで、少し時間がかかってしまったような気がします。 なので、前半ちょっと余計なことを話したりその場で思いついたことを話したりしてしまい、後半で少し走り気味になってしまったように思っていましたが、動画をあらためて見てもそこまで走っている感じはしなかったので、許容範囲に収まってよかったかな、と思います。
とはいえ、やはり最初に会場の雰囲気と自分の立ち位置を把握するため、しっかりと「つかみ」を大事にしないといけないな、と改めて感じました。

小道具が活かせなかった

「Spotlight」という、画面の一部をスポットライトで照らしたように見せる機能を持ったクリッカーバイスを持ってきていたのですが、あまり使う必要のあるスライドが少なく、一度だけ申し訳程度に使っただけで終わってしまいました。結局ずっとPCの前で操作をしていましたし、クリッカーとしてのも出番もなく。。
Spotlightはこれです。 www.logicool.co.jp

7周年総会の最後のLT大会で同じ道具を使っている方がいらっしゃったのですが、ものすごくウケていたのでちょっと悔しかったです^^;
次回はスライドをSpotlight使用前提で作って、さらに自分自身もPCから離れて動きながら話してみたいなと思いました。

資料作成がぎりぎり

社内で講師をするときもそうですが、本当にいつも登壇前日まで資料作成をやっている気がします…。余裕をもって準備できていないとリハーサルなども入念にできないので、早め早めの準備は心がけたいところです。
リハーサルをもっとしっかりできていれば上記2点のような反省点も出なかったはずなので、次回はきっちりと余裕をもっていきたいと思います。

振り返りブログが遅い

冒頭に「動画が公開されたので」というふうに書きましたが、それは嘘です…。
本当は登壇直後に公開したかったのですが、内容も振り返りも…といろいろ考えていたら文章のまとまりがなくなり、収拾がつかないまま時間だけが過ぎてしまいました。
動画が公開されたので、このタイミングでこそ出さなければ…!と、なんとか振り返り中心にまとめて公開までこぎつけた次第です。
次回もし登壇をした場合はもっと早く出せるよう、事前に準備しておきたいなと思います。

LINE Messaging APIに関して若干の補足

登壇時、しゃべり損ねてしまったのですが(動画を見直して気づきました)、今回使ったLINE Botの機能は「Reply API」という、ユーザーからのメッセージに応答(返信)するためのAPIで、LINE Botにはもうひとつ、「Push API」というBotからユーザーにメッセージを送るためのAPIが用意されています。
Reply APIはフリープランでも使えますが、Push APIではプロ版(月額料金がかかる)か開発者版(友だち数に制限がある)でしか使えません。そのため、今回はReply APIのみでシステムを構築しています。

まとめ

ということで、社内の講座のためにAzureを触り始め、もうすぐ8か月くらいになります。IoTやAIなんていうと大それた言葉ですが、Azure(とLINE Bot)でうまく実現でき、その可能性に大きな魅力を感じました。今回、JAZUGでの登壇を通して、その魅力を多くの方に伝えられていたらいいなと思います。
現在Azureを業務でも使えるところに取り入れられたらと思い、いろいろ試しているのでこれからも勉強していこうと思います。引き続きJAZUGには参加させていただこうと思いますので、どうぞよろしくお願いいたしますm(_ _)m

「BLOB」の読み方は?

はじめに

普段「BLOB」のことを「ビーロブ」と発音していたのですが、最近「ブロブ」と発音する人が多いことを知りました。
人前でしゃべることも多いので、正しい(もしくは多数派の)発音を知っておきたいと思い、ちょっと調べてみました。

BLOBとは何か

Binary Large OBjectの略。データベースなどでバイナリデータを格納する際のデータ型のことを言います。
似た概念に、大きなテキストデータを格納するためのデータ型であるCLOB(Character Large OBject)というものもあります。
参考:バイナリ・ラージ・オブジェクト - Wikipedia

発音の派閥

「ビーロブ」派

Oracleのドキュメント
BLOBの読み方が「ビーロブ」であると明記されています。
当然、CLOBは「シーロブ」となっています。
用語集

自分が最初に出会ったRDBMSOracleだったため、その影響で「ビーロブ」と発音するようになったようです。

ブロブ」派

・IT用語辞典 e-Words
読み方に「ビーエルオービー/ブロブ」と書かれています。
BLOBとは - IT用語辞典

しかし、CLOBのほうは「シーロブ/シーエルオービー」と書かれています。統一されていない…。
CLOB(キャラクタラージオブジェクト)とは - IT用語辞典

英語圏では?

Channel9の動画(Azure Blob Storageの話が出てくるもの)で英語スピーカーが何と言ってるかを聞いてみました。

channel9.msdn.com
channel9.msdn.com

めっちゃ「ブラーブ」と言ってますね…。
日本語っぽい発音にしたら「ブロブ」、ということになりますね。

結論

英語圏の人もそうですし日本でもそう言う人が多いので、「ブロブ」と発音したいと思います。
ただ、Oracleの文脈ではドキュメントの読み方に従い、今後も「ビーロブ」と言っていこうと思います。

Grails3.3でrest-apiプロファイルを試す(Spring Securityでの認証つき)

はじめに

長いこと業務でGrailsを使っているので、Grailsの動向が気になっています。

Grailsとは、GroovyというJavaを強力に進化させたスクリプト言語を用いたフルスタックなWebアプリケーションフレームワークです。

Ruby on Railsのように「CoC(設定より規約)」「DRY(Don’t Repeat Yourself)」を信条とし、煩わしい作業や無駄な記述を一切排除し素早くWebアプリを開発できる、高い生産性が魅力です。

そんなGrailsですが、つい最近、3.3が正式リリースになりました。

社内の認証プロキシがどうにも突破できずうまく開発できないので現在の業務では2.5を使って開発していますが、ビルドシステムがGradleになり大きく構成の変わった3.xはずっと気になっていました。今回3.3が出たのでちょっと試してみたところ、プロキシ突破できたので、3.x系に乗り換えようか悩み中。。
※プロキシ突破できた話はメモとしてQiitaに書きました

qiita.com

とりあえず、いま作ってるアプリはそのまま2.5で作って、そのあと徐々に3.x系に移行していけばいいかな、と思っています。

さて、今回はGrails 3の出番に備え、リリースしたての3.3を使って、認証機能つきのWeb APIを作ってみたいと思います。 業務で作っているシステムは複数システムを連携ありで作る想定なので、Web APIで連携しやすくすることが重要になるため、ここの検証をしておきます。今回作成したものはGithubに上げています。

github.com

以下、Grailsの簡単な説明を交えつつのやったことの流れです。

「rest-api」プロファイルでプロジェクトを作成

Grailsではコマンドを用いて開発を進めます。アプリの新規作成は「create-app」コマンドで行いますが、Grails 3になると、作りたいアプリの構成ごとに「プロファイル」が用意されており、作成時に指定することができます。プロファイルを指定せずに「create-app」コマンドでアプリを新規作成すると、デフォルトのプロファイルである「web」プロファイルが用いられ、通常のWebアプリケーションのプロジェクトが作成されます。
一方、

grails create-app api-sample --profile=rest-api

とオプションで「rest-api」プロファイルを指定すると、RESTで叩けるWeb APIを作るための雛形プロジェクトを作ってくれます。

ドメインクラスとScaffold

GrailsMVCモデルのフレームワークです。
モデル(DBのテーブルに相当し、Grailsではドメインクラスと呼ばれます)、ビュー(WebアプリではHTMLのレンダリングを司る)、コントローラー(双方の仲立ち)により構成されます。

Grailsでは「設定より規約」により、モデル、ビュー、コントローラーのファイル名を規約通りにつけることで、それぞれが連動し複雑な設定なしでWebアプリとして動いてくれます。具体的には、たとえば書籍を管理するWebアプリの場合「Book.groovy」というモデル(ドメインクラス)、「BookController.groovy」というコントローラークラスを作り、「BookController.groovy」には「index」というpublicなメソッドを作ります(このメソッドを「アクション」と呼びます)。そして、アクションと同じ名前のビュー「index.gsp」というファイルをディレクトリ名がドメイン名と同じであるディレクトリに作ります。つまり、以下のような構成です。

domain
└ Book.groovy
controllers
└ BookController.groovy
views
└ book
   └ index.gsp

この状態でWebアプリを起動し、「http://xxxx/sampleapp/book/index」というURLにアクセスすると、URLに書かれたとおりのコントローラークラスのアクションメソッドが実行され、同様にビューが表示されます。このあたりの仕組みはRuby on Railsと同じですね。

そして、このコントローラーとビューは、ドメインクラスをもとに自動生成することが可能で、それが「Scaffold」です。

ドメインクラスを

package rest.s2.sample

class Book {
    // 属性
    String name
    String author

    // 制約
    static constraints = {
        name blank: false
        author blank: false
    }
}

と定義します。そして、 ‘’‘ grails generate-all rest.s2.sample.Book ’‘’ と実行すると、ドメインクラスに定義した通りの「Book」データをDBと連携し一覧表示/登録/編集/削除ができるようなコントローラーとビューを生成してくれます。これがScaffoldです。自動生成されたコードは実際にそのまま本番で使用することは少ないと思いますが、基本的な動作の書き方の参考にしたり、カスタマイズして流用したりすることができ、開発効率向上に一役買ってくれます。

rest-api の場合の Scaffold

さて、こんなScaffoldですが、「rest-api」プロファイルで作成されたプロジェクトで行うと、ちゃんとRESTで呼べるものが自動生成されます。 特に重要なのはビューで、通常は拡張子が「.gsp」であるGSPファイル(Groovy Server Pages。HTMLをレンダリングするためのファイル)が生成されますが、rest-apiの場合に生成されるのが「.gson」のGSONファイルです。これは、その名の通りGSPのJSON版で、リクエストに対しJSONのレスポンスを返すための情報を記載するファイルです。

起動と動作確認

Scaffoldまでできたら、「run-app」コマンドでアプリを起動します。 APサーバー、DBがあらかじめ含まれているのですぐ動作確認できます(DRYな特徴のひとつ)。 今回はrest-apiなのでブラウザではなくコマンドから確認します。Windows環境なので、PowerShellInvoke-RestMethodを使ってみます。この方法だと、送信するデータをスクリプトで組み立てられるため、RESTの動作確認におすすめです。

RESTということなので、今回、URLにアクションは含めません。UrlMappingsという設定ファイルで、HTTPメソッドとアクションメソッドの対応が定義されており、Grailsのデフォルトの規約を上書きしています。rest-apiでは、規約より設定、ということでしょうか。

package grails3.rest.api.s2.sample

class UrlMappings {

    static mappings = {
        delete "/$controller/$id(.$format)?"(action:"delete")
        get "/$controller(.$format)?"(action:"index")
        get "/$controller/$id(.$format)?"(action:"show")
        post "/$controller(.$format)?"(action:"save")
        put "/$controller/$id(.$format)?"(action:"update")
        patch "/$controller/$id(.$format)?"(action:"patch")

        "/"(controller: 'application', action:'index')
        "500"(view: '/error')
        "404"(view: '/notFound')
    }[f:id:himanago:20170803094920p:plain]
}

実行結果は以下のようになりました。まず登録。

PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"}

id name        author
-- ----        ------
 1 Sample Book Sample Author

そして一覧表示です。

PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method GET

id name        author
-- ----        ------
 1 Sample Book Sample Author

たったこれだけで、RESTなWeb APIを作成することができました。

認証機能をつけるプラグイン

では、次にこのAPIに認証機能を付けてみたいと思います。Grailsには、認証機能をつけるための「Spring Security」がプラグインとして用意されています。プラグインは、Grailsの大きな利点のひとつで、さまざまな機能を追加したり、またよく使う機能を自前のプラグインとして外部化することもできます。

それでは、Spring Securityプラグインを使って認証機能を追加します。 rest-apiの場合、使うのはSpring Security CoreSpring Security RESTの2つ。

プラグインは、build.gradle内のdependenciesに以下の行を追加するだけで導入できます。

    compile 'org.grails.plugins:spring-security-core:3.2.0.M1'
    compile "org.grails.plugins:spring-security-rest:2.0.0.M2"

ちなみに認証の流れはSpring Security RESTのUser guideに載っているこの図がわかりやすいです。 rest

s2-quickstart

Spring Security Coreには、コマンド一発で認証に必要なユーザーとロールのドメインを作ってくれる「s2-quickstart」コマンドが用意されています。

grails s2-quickstart rest.s2.sample.auth User Role

次に、できあがったクラス(User、Roleとその関連を管理するUserRole)に対してgenerate-allでコントローラー/ビューを生成しておきます。

grails generate-all rest.s2.sample.auth.User
grails generate-all rest.s2.sample.auth.Role
grails generate-all rest.s2.sample.auth.UserRole

権限設定

Spring Securityでは、コントローラーのクラス単位/アクション単位でSecuredアノテーションを付けることできめ細かく権限制御をすることができます。ログイン有無や、Userに紐付くRoleごとの制御ができます。今回は、BookControllerのアクションを「ROLE_ADMIN」というRoleを持つUserでログインしていないと実行できないようにしてみます。

記述はクラス名の上に「@Secured(‘ROLE_ADMIN’)」をつけるだけです。もちろん、importは必要になります(この参照がうまくいかない場合はGradleプロジェクトのリフレッシュを行います)。

package rest.s2.sample

import grails.plugin.springsecurity.annotation.Secured
...

@Secured('ROLE_ADMIN')
class BookController {

}

各種設定

初期ユーザー登録

init/Bootstrap.groovyに、起動時処理として初期ユーザーの登録処理を書いておきます。今回は、このユーザーで認証を行います。

package grails3.rest.api.s2.sample

import rest.s2.sample.auth.Role
import rest.s2.sample.auth.User
import rest.s2.sample.auth.UserRole

class BootStrap {

    def init = { servletContext ->
        def user = new User(username: 'admin', password: 'admin')
        user.save(flush: true)
        def role = new Role(authority: 'ROLE_ADMIN')
        role.save(flush: true)
        new UserRole(user: user, role: role).save(flush: true)
    }
    def destroy = {
    }
}

設定ファイルの変更

今回はconf/application.ymlに設定を書きます。

grails:
    ....

    plugin.springsecurity:
        userLookup.userDomainClassName: 'rest.s2.sample.auth.User'
        userLookup.authorityJoinClassName: 'rest.s2.sample.auth.UserRole'
        authority.className: 'rest.s2.sample.auth.Role'
        filterChain.chainMap:
            #Stateless chain
            - {pattern: '/**', filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'}
        rest.token:
                validation:
                    useBearerToken: false
                    headerName: 'X-Auth-Token'

application.groovyにあるSpring Security関連の設定はすべてコメントアウトします。

実行結果を確認

では、実行確認です。run-appして、Invoke-RestMethodコマンドを実行していきます。

認証情報のない状態で、先ほどと同じようにbookの登録を行おうとすると、403エラーになります。

PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"}
Invoke-RestMethod : リモート サーバーがエラーを返しました: (403) 使用不可能
発生場所 行:1 文字:1
+ Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Bod ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

なのでまずは認証をし、アクセストークンを得ます。実行したスクリプト

$user = @{
    username='admin'
    password='admin'
}
$json = $user | ConvertTo-Json
$response = Invoke-RestMethod 'http://localhost:8080/api/login' -Method POST -Body $json -ContentType 'application/json'
echo $response.access_token

<実行結果>

PS C:\> $user = @{
>>     username='admin'
>>     password='admin'
>> }
PS C:\> $json = $user | ConvertTo-Json
PS C:\> $response = Invoke-RestMethod 'http://localhost:8080/api/login' -Method POST -Body $json -ContentType 'application/json'
PS C:\> echo $response.access_token
eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTdlVcL2JRQlJcL1RoTlJnVlNnRWtnZFlBRTI1RWgwek1SbjFjb05xR2tXa0VBWCsrRWVu
5cGFKRXFkZTNJZjlJdVwvUU5RTzNSbDd0cDNodURBZ25xVFwvZTduMzlmenhSVlVqSWJuc1daY0dEOFZXY3lsYjFMTlpXd3d6RFMzSFQ4enFDTzBPZUpGRG16U0JLNlBW
4cXdvbTQrcG02d0JEVzJ0cldGSTZ2bUhjMXl6QkU2VVBcL1Z2dVVHbThJMUJRZTk5S01MSU5reXdNVlNadFhjbjFkc28xUnRzd1Vjd0NGUjY2MFZSSU55Z3RaOElNUTBk
sUTVHZ3ZqMTJZenkwVzFnYllXd09PVUdVUHU3aVZwV0dmZDNUdWJraEljd1hzb3QxT1BEblczNEtDKzRcL0ZYbFJDVW1pdHA1cHN5VVJIZjUwNmMrTHN6WnpcLzdYN3ZO
wMHYrXC8rbmMyTDlrSUwwMFBXQzFpdG5aS2J5WUw1clVhbmZQbGw2OVA1MVllZFI2VHNFQnZcL3Y0XC81NVp2bU9xc3FTWmxtVmczdGlHaFB5dTZaeUZjZUpoOXNvZU0z
waWlsdldTZ3o2dGpENlpqTlkzMXRlZVwvMnk3bDRyTEVxNEpOVW5lV2kzTFQ5UXRLdmVuNDhcLytuT1wvaU9FVlZJNlp5SkE2bnloQTlTeHBvVDY5T0o4WitcL3k3bHlj
SQXdBQSIsInN1YiI6ImFkbWluIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJleHAiOjE1MDE2NTI5ODcsImlhdCI6MTUwMTY0OTM4N30.P2n42_q6WSNY9weZad23aRSP

次にBookの登録です。

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("X-Auth-Token", $response.access_token)
Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"} -Headers $headers

<実行結果>

PS C:\> $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
PS C:\> $headers.Add("X-Auth-Token", $response.access_token)
PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"} -Headers $headers

id name        author
-- ----        ------
 1 Sample Book Sample Author

ちなみに、アクセストークンが誤っていた場合は、401エラーになります。

ということで無事、認証つきでRESTのAPIをたたくことができました。
これだけ手軽に認証機能つきのWeb APIが作れるようになり、Grailsはとても便利に進化していると思います。

画面ロックのショートカットをタスクバーにピン留めする(Windows 10)

はじめに

こんにちは。
今回は画面ロックをタスクバーからかんたんに行えるようにする小技を紹介します。

経緯

(本題とは関係ないので読み飛ばして結構です) 最近会社のメインPCが変わりました。念願のWindows10です。
会社では長らく開発用(Win8.1)とメイン(Win7)の2台体制で仕事していましたが、今回メイン機が7→10と入れ替わったかたち。

この2台はInput Directorというツールでマウス・キーボードを共有しています。

Input Directorは、PCを同一ネットワーク上にある別のPCから操作するためのツールです。
マスター(操作する側のPC)のマウスとキーボードで、スレーブ側(操作される側のPC)を操作できる優れものです。

なのですが、1点、不便なのが画面ロックです。
画面ロックは通常、Winキー+Lやファンクションキーの組み合わせなどで行えますが、マウス・キーボード共有している状態でスレーブ側を画面ロックしようとしても、マスター側がロックされるだけで、スレーブ側はとロックされません。
つまり、キーボード共有した状態で画面ロックのショートカットキーを入力すると、スレーブ側ではなくマスター側がロックされてしまうので、スレーブ側をロックするには別の手段を用いる必要が出てきます。
そこで使ったのが、今回の「タスクバーに画面ロックショートカットをピン留めする」という方法です。

画面ロックをコマンドで行う

画面ロックをタスクバーから実行するために、まず考えたのはバッチ化する、ということでした。
バッチ化するために、コマンドから画面ロックを実行する方法を調べたところ、下記の記事がヒットしたので、使わせていただきました。

www.projectgroup.info

ここに書かれている通り、「rundll32.exe user32.dll,LockWorkStation」と書いたファイルを作成し、拡張子を「bat」すればOKです。
このファイルをダブルクリックなどで実行すると、しっかり画面ロックされます。 (今回は「ロック.bat」というファイルにしました)

ショートカット化する

バッチファイルは、そのままではタスクバーにピン留めできません。 f:id:himanago:20170729230928p:plain

そこで、作ったバッチファイルをショートカット化します。
バッチファイルを右クリック→ショートカットの作成、とするとショートカットができます(ショートカットの名前は「ロック」にしておきます)。
これだけではまだタスクバーにピン留めできないので、これをコマンドプロンプト経由で呼ぶよう、変更します。 今回はバッチファイルは「C:\bat」のフォルダに保存しているので、作成したショートカットのリンク先は「C:\bat\ロック.bat」となっています。
これを、「C:\bat\ロック.bat」→「C:\Windows\System32\cmd.exe /C C:\bat\ロック.bat」と変更することで、コマンドプロンプト経由で呼ばれるようになります。
f:id:himanago:20170729231714p:plain:w300

こうすると、バッチファイルはタスクバーにピン留めできるようになります。

f:id:himanago:20170729234003p:plain:w400

コマンドプロンプトのアイコンだと味気ないので、アイコンも変更します。
アイコンはフラットデザインでよさげなものを選択。今回はこちらの「PNG」「背景なし」を使わせていただきました。

flat-icon-design.com

これをicoに変換すると、ショートカットアイコンとして使えるようになります。
今回はオンラインで変換できる下記のサイトを使用。

app.tree-web.net

アイコンの準備ができたら、再びショートカットのプロパティを開き、アイコンを変更し、タスクバーにピン留めしなおします。
これで、ちょっとおしゃれな画面ロックショートカットをタスクバーにピン留めすることができました。

f:id:himanago:20170730011353p:plain:w200

もう一工夫

これだけでもいいのですが、この状態でピン留めしたショートカットから画面ロックをしようとすると、コマンドプロンプトの画面が一瞬出てしまいます。
そこで、コマンドプロンプトが出ない方法を調べると、VBScriptで記述すればよい、ということがわかりました。

d.hatena.ne.jp

拡張子を変更し、「ロック.vbs」というファイル名にします。 中身は以下のコードに変更します。

Dim oShell
Set oShell = WScript.CreateObject ("WSCript.shell")
oShell.run "rundll32.exe user32.dll,LockWorkStation",0
Set oShell = Nothing

ショートカットのプロパティのリンク先も、VBScriptになったので以下のように実行するexeを変更します。

C:\Windows\System32\wscript.exe //B C:\bat\ロック.vbs

最後に、ショートカットをタスクバーにピン留めしなおします。
これで、ワンクリックできれいにロックがかかるようになりました!
快適な2PC生活が送れそうです。