Azure Functionsを使ってLINEで数式のやりとりを可能にする

# はじめに
この記事は [Microsoft Azure Advent Calendar 2017](https://qiita.com/advent-calendar/2017/azure) の4日目の記事です。
今回は、Azure Functionsを使うと手軽にLINE Botが作れる、という話を書いていこうと思います。

# 今回つくるもの
## 数式をきれいに表示してくれるBot
LINEで会話をしているとき、複雑な数式を送りたいと思うことが（ごくまれに）あるかと思います（？）。
たとえば、こんな式をLINEで相手に伝えたいと思っても、これをテキストで打ち込んで送るのはちょっと厳しいものがあります。

![googlechartsample.png](https://qiita-image-store.s3.amazonaws.com/0/194664/a66db542-fa77-2121-c512-34b18e5447a2.png)

通常は写真で撮って送ればいいだけの話なんですが、今回は写真を使わず、相手にきれいな数式を送ってくれるBotを、Azureを使って作ってみたいと思います。

## TeXで数式のやりとり
今回、数式はTeX（てふ）を使います（TeXについては[Wikipedia](https://ja.wikipedia.org/wiki/TeX)を参照ください）。

Botの利用イメージは、

* 数式を送りたい相手とのLINEの会話にBotを参加させる（グループトーク）
* TeXで書かれた数式を含むメッセージを送ると、Botがその数式を画像化し、送り返してくる
* メッセージ内の数式は「$」で囲まれた部分とし、複数書ける（複数数式があればその数の画像を返信する）

というようなものになります。

# 技術仕様
## 使った技術など
* [LINE Messaging API](https://developers.line.me/ja/services/messaging-api/)：LINE Bot作成のためのAPIです。
* [Azure Functions](https://azure.microsoft.com/ja-jp/services/functions/)：今回はMessaging APIと他サービスの仲介を行ってもらいます。
* [Infographics (Google Chart API)](https://developers.google.com/chart/infographics/docs/formulas?hl=en)：TeXの数式を画像化してくれます。※ただしこの機能は現在非推奨。。
* [filestack](https://www.filestack.com/)：画像形式を変換してくれるAPIがあるので、これを利用します。


## Botの仕組み
おおまかな処理の流れは以下のようになります。

![流れ](https://qiita-image-store.s3.amazonaws.com/0/194664/b30fb30d-15f2-dad0-6cbf-e6b19d98e7e6.png)

今回は、LINE Messaging APIのReply API（ユーザーがBotに対して送ったメッセージに対する返信をさせるための機能）を使います。
Reply APIで画像を送り返すには、JPEG画像のURLを指定してあげる必要があるという特徴があります。
Google Chart APIで得られた画像はPNG形式なので、LINEに合わせるためJPEGに変換してあげる必要があり、URLのみで元画像、変換形式の指定ができるfilestackの画像形式変換機能を間に挟むことで、URL１本でTeXからJPEGを得ることができるようになっています。

filestackには事前にサインアップしておいて、APIキーを取得しておきます。
このサービスはURLに指定するだけで使うことができ、画像送信URLを使用するMessaging APIと相性抜群なため、ほかの用途にも使えそうです。

#作成の流れ
## 手順①：Visual StudioでAzure Functions用の関数クラスを作成
今回はC#でコードを書きます。
Azure Functionsのテンプレートでプロジェクトを作成し、プロジェクトの右クリック＞追加＞新しい項目から「Azure 関数」クラスを作成します（今回プロジェクト名もクラス名も「TeXBot」としました）。できあがった「TeXBot.cs」の中身を、下記のようにします。

### コード
```C#:TeXBot.cs
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace TeXBot
{
    public static class TeXBot
    {
        private const string lineChannelAccessToken = "(LINE Botのアクセストークン)";
        private const string filestackApiKey = "(FileStackのAPIキー)";

        [FunctionName("TeXBot")]
        public static async Task<object> Run([HttpTrigger(WebHookType = "genericJson")]HttpRequestMessage req, TraceWriter log)
        {
            var jsonContent = await req.Content.ReadAsStringAsync();
            var eventObj = JsonConvert.DeserializeObject<LineEventObject>(jsonContent);
            var data = eventObj.Events[0];

            if (data.Type == "message")
            {
                // $で囲まれた部分を数式とみなす（$でSplitして奇数番目が数式化対象）
                var replyMessages = data.Message.Text.Split('$').Where((str, idx) => idx % 2 == 1).Select(f =>
                {
                    // ￥マークをバックスラッシュに置換したうえでURLエンコード
                    var texText = Uri.EscapeUriString(f.Replace("\x00A5", "\x005C"));

                    var url = $"https://process.filestackapi.com/{filestackApiKey}/output=format:jpg/https://chart.apis.google.com/chart?cht=tx&chl={texText}";

                    return new ReplyMessage()
                    {
                        Type = "image",
                        OriginalContentUrl = url,
                        PreviewImageUrl = url
                    };
                }).ToArray();

                using (var client = new HttpClient())
                {
                    // 認証情報
                    client.DefaultRequestHeaders.Add("Authorization", $"Bearer {{{lineChannelAccessToken}}}");

                    // LINE側へメッセージ送信依頼
                    var res = await client.PostAsJsonAsync("https://api.line.me/v2/bot/message/reply",
                        // 送信データを作成
                        new ReplyMessageObject()
                        {
                            ReplyToken = data.ReplyToken,
                            Messages = replyMessages
                        }
                    );
                }
            }
            return req.CreateResponse(HttpStatusCode.OK);
        }
    }

    public class LineEventObject
    {
        [JsonProperty("events")]
        public Event[] Events { get; set; }
    }

    public class Event
    {
        [JsonProperty("replyToken")]
        public string ReplyToken { get; set; }
        [JsonProperty("type")]
        public string Type { get; set; }
        [JsonProperty("timestamp")]
        public long Timestamp { get; set; }
        [JsonProperty("source")]
        public Source Source { get; set; }
        [JsonProperty("message")]
        public EventMessage Message { get; set; }
    }

    public class Source
    {
        [JsonProperty("type")]
        public string Type { get; set; }
        [JsonProperty("userId")]
        public string UserId { get; set; }
    }

    public class EventMessage
    {
        [JsonProperty("id")]
        public string Id { get; set; }
        [JsonProperty("type")]
        public string Type { get; set; }
        [JsonProperty("text")]
        public string Text { get; set; }
    }

    public class ReplyMessageObject
    {
        [JsonProperty("replyToken")]
        public string ReplyToken { get; set; }
        [JsonProperty("messages")]
        public ReplyMessage[] Messages { get; set; }
    }

    public class ReplyMessage
    {
        [JsonProperty("type")]
        public string Type { get; set; }
        [JsonProperty("text")]
        public string Text { get; set; }
        [JsonProperty("packageId")]
        public string PackageId { get; set; }
        [JsonProperty("stickerId")]
        public string StickerId { get; set; }
        [JsonProperty("originalContentUrl")]
        public string OriginalContentUrl { get; set; }
        [JsonProperty("previewImageUrl")]
        public string PreviewImageUrl { get; set; }
    }
}
```

### 注意点など
#### JSONデータの格納用クラスについて
[LINEのMessaging APIのリファレンス](https://developers.line.me/ja/docs/messaging-api/reference/)を見て、LINEからどういったデータが来て、LINE側にどういうデータを送り返せばいいのかを確認し、そのとおりに実装する必要があります。
LINEとのデータのやりとりはJSONで行われるので、C#のプログラム内で効率よく扱うため、あらかじめ用意したクラスとJSONを相互変換するようにしています。

JSONのデータを入れるためのクラスは、Visual Studioの機能（編集＞形式を選択して貼り付け＞JSONをクラスとして貼り付け）でも作れるほか、Webサービスの[quicktype](https://quicktype.io/)を利用することでJSONデータ格納用クラスの定義を一発で得ることができます。前者は自動で生成された直後はプロパティ名が小文字になってしまったり、ルートのクラス名が適当だったりと微妙な点も多いですが、後者ではより実用的なクラスが生成されます。
APIリファレンスに書いてあるサンプルのJSONを、上記の機能に使うことですぐにJSONデータ格納用のクラスの定義が生成できるので、とても便利です。

別にクラスを用意せず進めても構わないとは思いますが、入出力のJSONの内容が決まっている外部サービスとの連携では、JSONをもとにクラスを作ったほうが、送受信するデータに抜けや漏れ、ずれが起きにくくなるので、圧倒的に効率よく開発ができると思います。

## 手順②：LINE Messaging APIの開始
詳細な手順は割愛しますが、LINE Developersで、Messaging APIを利用するためのアカウント、チャンネルの作成を行います。今回使うのはReply APIのみなので、フリープランで構いません。
チャンネル設定の画面で、

* アクセストークンの発行
* 「Webhook送信」を「利用する」に変更
* 「Botのグループトーク参加」を「利用する」に変更
* 「自動応答メッセージ」を「利用しない」に変更

をしておきます。

## 手順③：接続情報をコードに反映しAzureにデプロイ
LINE Developersより得られたチャンネルアクセストークンと、filestackのAPIキーをFunctionsのコードに設定します。
この設定ができたら関数は完成ですので、Azureへ「発行」します。
Visual StudioからAzureに接続した状態で、「ビルド」＞「(ソリューション名)の発行」から開く画面で「発行」すればOKです。


## 手順④：デプロイした関数のURLをMessaging APIに設定
関数がAzure上に発行できたら、LINE側にFunctionsのURLを教えてあげましょう。「メッセージ送受信設定」の「Webhook URL」に入力します。
ここがうまく設定できれば、FunctionsでLINE Botに送られたメッセージを参照することができるようになります。

# 実際に使ってみる
数式の書き方は、以下のページが参考になりました。
[Google Chartを使った数式の書き方](http://www.geisya.or.jp/~mwm48961/electro/parameter5.htm)

Messaging APIのQRコードから自分のLINEの友だちに追加し、Botと１対１で会話してみます。
$で囲まれた数式を２つ、メッセージに入れてみました。
結果はこうなりました。

<img width="375" alt="IMG_8441.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/194664/c345e1d3-e298-3cb3-ab7d-1e03e030c50b.jpeg">

若干荒いですが、成功です！ちなみにグループトークでも、数式を投げたらちゃんと変換してくれます。
これで、友だちにきれいな数式が気軽に送れるようになりましたね！（TeXさえマスターすれば）

# まとめ
## Visual Studio 2017＋C#でのFunctions開発は快適
つくってみて思いましたが、Visual Studio 2017でFunctions開発ができるようになったことで、「普通に」C#で書けるのと、Azureへの発行が１クリックでできるのがとても楽でよかったです。ローカル実行やリモートデバッグもありますし、過去のFunctions開発でポータル上でC#スクリプト（.csx）をあーでもないこーでもない言いながらいじっていたときと比べると段違いに楽だし、楽しいです。
それと、やはりC#はLINQが使えるのがいいですね。今回でいうと、送信メッセージから数式を探して画像送信依頼のJSONに変換する処理にLINQを使いましたが、ちょっとの工夫でコード量を抑えることができ、こういうのは、書いていて楽しいなと改めて思いました。

## Azure＋LINE Botの可能性
（実用性は置いておくとしても）今回のようなちょっとしたアイディアをすぐ形できるのが、Azureの魅力の一つではないかと思っています。
Botというと、Bot Frameworkなどを使ったり、プログラムを組んだりするイメージをもたれることも多いかと思いますが、今回紹介したもののような単純応答系のBotであれば、AzureならFunctionsで手軽に作成することができます（ほかにもLogic Appを使えばプログラミングなしでBotが作れます）。
Cognitive ServicesのLUISなどを使えば自然言語による会話も実現できますし、ほかにもBotで使えるサービスが数多くあります。
これらのことから、AzureはLINEなど既存のメッセージングツールをインタフェースにしていろいろなAPIを組み合わせたサービスを作る際の、「核」を担う要素の有力な選択肢のひとつになるのではないかな、と思います。