最近Nimがキテるらしいので、LINE Botで試してみた

最近、Nimが流行りつつあるとかないとか。

[至高の言語、Nimを始めるエンジニアへ](https://qiita.com/rigani/items/6e87c7cee6903ed65ed2) を書いたエンジニアが、
「Nimいいよ！速いし綺麗だし！とりあえず一回書いてみ？」とアピールが凄いので、とりあえず一回触ってみました。

テーマは、初めて書く言語なら丁度いいであろうLINE BOTです。

## なにはともあれインストール

`brew` でいけます。楽ですね。
ただ、僕の場合はXcodeのバージョンが低くて怒られたので、言われたとおりにアップデートすればOKです。

```shell
$ brew install nim
Error: Your Xcode (8.2.1) is too outdated.
Please update to Xcode 9.2 (or delete it).
Xcode can be updated from the App Store.
```
インストールが終わると、`nim`コマンドと同時に `nimble` というライブラリマネージャも使えるようになりました。

```shell
$ nim -v
Nim Compiler Version 0.17.2 (2017-09-18) [MacOSX: amd64]
Copyright (c) 2006-2017 by Andreas Rumpf

active boot switches: -d:release -d:useLinenoise
$ nimble -v
nimble v0.8.8 compiled at 2017-09-18 21:04:35
```

エディタは、VSCodeを使います。
VSCode Extensionに [Nim](https://marketplace.visualstudio.com/items?itemName=kosz78.nim) があるのでささっと入れます。

## LINE BOT
ここから少し、LINE BOTの設定のお話です。

[LINE Developers](https://developers.line.me/ja/) へアクセスして、 `Messaging API（ボット）をはじめる` を選択してください。
※ LINEのログインが求められます。

<img width="910" alt="LINE_Developers.png" src="https://qiita-image-store.s3.amazonaws.com/0/92359/fd11ea96-dff3-b3d8-48fc-0b8d725ed243.png">

### 新規作成

**【STEP1　プロバイダーを選択してください】**

プロバイダー名を適当に入れてください。

<img width="900" alt="LINE_Developers 4.png" src="https://qiita-image-store.s3.amazonaws.com/0/92359/be24c7eb-29e2-fc90-d9a2-f1faa3c0bc02.png">

**【STEP2　Messaging APIの情報を入力してください】**

大事なアプリ名や業種を入力します。
プランは `Developer Trial` を使います。

<img width="900" alt="LINE_Developers 5.png" src="https://qiita-image-store.s3.amazonaws.com/0/92359/30e95494-df01-ffd5-983d-28dcd08d831c.png">

**【STEP3　入力内容をご確認いただき作成ボタンで完了してください】**

特にここまでは問題ないと思います。

### Channel基本設定

作成し終えるとBotの `Channel基本設定` が確認できると思います。

今回使う情報は、以下の２つです。メモしておいてください。
※ 初期状態だとアクセストークンは生成されていないと思うので、その時は再発行をしてください。

- Channel Secret
- アクセストークン（ロングターム）

## Webhookを用意

いよいよNimで、ユーザが送信したメッセージを受け取ってみます。
まずは最小限のサーバを立てるために、[asynchttpserver](https://nim-lang.org/docs/asynchttpserver.html)を使います。

とりあえず確認するだけのコード。

```app.nim
import asynchttpserver, asyncdispatch, json

proc callback(req: Request) {.async.} =
  if req.url.path == "/webhook":
    echo "Request body: ", req.body
    await req.respond(Http200, req.body, nil)
  else:
    await req.respond(Http404, "Not Found")

# Run server
var server = newAsyncHttpServer()
waitFor server.serve(Port(8888), callback)
```
ドキュメントが少ないとか言われてますが、サンプルは簡潔で
ライブラリのコードも追いやすいと思います。

`proc`はプロシージャと呼ばれるもので、他の言語でいう関数とかと同じものです。
その後ろに `{.`と`.}`で囲まれた部分があると思います。これはプラグマと呼ばれるもので、コンパイラに情報を追加で与えたり、多言語と連携する時に直接コードを埋め込んだりできるらしいです。
ここでは、`async`をつけることで、このプロシージャを非同期化します。
また、非同期プロシージャの処理でレスポンスを同期的に行うために、 `await` を付与します。

それでは、コンパイル&実行します。

```shell
$ nim c -r app.nim
```

別コンソールからcurlで試すと、

```shell
$ curl -X POST http://localhost:8888/webhook -d '{"name": "enta0701"}'
{"name": "enta0701"}%
$ curl -X POST http://localhost:8888 -d '{"name": "enta0701"}'
Not Found%
```
大丈夫そうですね。


## LINE BOTにWebhookを登録する

Webhook URL は `https` でなければいけません。
開発中は、[ngrok](https://ngrok.com/) を使うといいです。

以下のコマンドで発行しましょう。
※ ポートは適宜合わせてください。

```shell
$ ngrok http 8888
```

<img width="585" alt="2__ngrok_http_8888__ngrok_.png" src="https://qiita-image-store.s3.amazonaws.com/0/92359/51a69dd2-f361-f981-6346-6bb33cded7b2.png">

LINE BOT側の設定で、以下の２つを変更します

- Webhook送信を「利用する」に変更
- Webhook URL を `ngrok`で発行されたもの + パスを指定

<img width="887" alt="LINE_Developers 6.png" src="https://qiita-image-store.s3.amazonaws.com/0/92359/c9c91f0c-4836-d563-41da-730b43d0869c.png">

`接続確認`をクリックすると、立ち上げたサーバにJSONが送られてきます。

<img width="828" alt="LINE_Developers.png" src="https://qiita-image-store.s3.amazonaws.com/0/92359/5bc67913-50fd-146f-7057-7000a9427029.png">

```json
{
  "events": [
    {
      "replyToken": "00000000000000000000000000000000",
      "type": "message",
      "timestamp": 1519138980059,
      "source": {
        "type": "user",
        "userId": "Udeadbeefdeadbeefdeadbeefdeadbeef"
      },
      "message": {
        "id": "100001",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "ffffffffffffffffffffffffffffffff",
      "type": "message",
      "timestamp": 1519138980059,
      "source": {
        "type": "user",
        "userId": "Udeadbeefdeadbeefdeadbeefdeadbeef"
      },
      "message": {
        "id": "100002",
        "type": "sticker",
        "packageId": "1",
        "stickerId": "1"
      }
    }
  ]
}
```
それでは、LINE BOTを友達に登録して、メッセージ(ほげ)を送ってみましょう。

```
{"events":[{"type":"message","replyToken":"4055b0e902598b7870aac44bbedf8d10","source":{"userId":"Ucf214e6699ca85b64e569e9e09d3a988","type":"user"},"timestamp":1519139249667,"message":{"type":"text","id":"5572675039033","text":"ほげ"}}]}
```

来ましたね。
あとは、これをパースしてゴニョゴニョすればOKですね。


## オウム返し

先にコードを。

```app.nim
import asynchttpserver, asyncdispatch, json, httpclient
import dotenv, os

let env = initDotEnv()
env.load()

const
  lineApiMessageReplyEndpoint = "https://api.line.me/v2/bot/message/reply"

proc callback(req: Request) {.async.} =
  if req.url.path == "/webhook":
    echo "Request body: ", req.body

    let events = parseJson(req.body)["events"]
    for event in events:
      if event["type"].str == "message":
        let client = newHttpClient()
        client.headers = newHttpHeaders({
          "Content-Type": "application/json",
          "Authorization": "Bearer " & getEnv("AUTHORIZATION_KEY")
        })
        let body = %*{
          "replyToken": event["replyToken"],
          "messages": [{
            "type": "text",
            "text": event["message"]["text"]
          }]
        }
        let response = client.request(lineApiMessageReplyEndpoint,
                                      httpMethod = HttpPost,
                                      body = $body)
        echo response.status
      else:
        echo "Type is not 'message'"
    await req.respond(Http200, "", nil)
  else:
    await req.respond(Http404, "Not Found")

# Run server
var server = newAsyncHttpServer()
waitFor server.serve(Port(8888), callback)
```

先程のコードに所々追記をしています。

以下の部分は秘匿情報を扱うため、 `.env` に環境変数を記載してよしなに読み込んで扱うためのライブラリの初期化です。

```nim
let env = initDotEnv()
env.load()
```

```.env
AUTHORIZATION_KEY="[アクセストークン]"
```

ただ、標準ライブラリではないので、 `nimble` で取ってきましょう。

```shell
$ nimble install dotenv
```

ここまで来たら、サーバを再起動します。
※ ngrokはそのままでOKです。もし閉じてしまった場合は、再度LINE BOTへ設定し直してください。


…はい、止まりましたね。
こんなエラーが出てるかと思います。

```
Error: unhandled exception: SSL support is not available. Cannot connect over SSL.
```
ドキュメントにも、 `httpclinet`でSSLを使用する時は、コンパイルオプションを指定してね、ってちゃんと書いてありました。
https://nim-lang.org/docs/httpclient.html#ssl-tls-support

ので、改めてコンパイル&実行します。

```shell
$ nim c -r -d:ssl app.nim
```

いかがでしょう？ オウム返しできたと思います！ :sparkles: 

## おまけ

もうここまで出来たら、あとはアイデア次第！ですが、
APIのドキュメントに署名の検証があったので、ついでにやっておきましょう。
https://developers.line.me/ja/docs/messaging-api/reference/#signature-validation

まずは `hmac` をnimbleで取ってきます。

```
$ nimble install hmac
```

リクエスト後の処理に、以下のコードを追記します。

```nim
  import ..., hmac, base64

  let signature = req.headers.getOrDefault(key = "x-line-signature")
     let hash = hmac_sha256(key = getEnv("LINE_CHANNEL_SECRET"), data = req.body)
     if signature != encode(s = hash):
       await req.respond(Http404, "Not Found")
```

`.env`ファイルに環境変数を１つ追加します。

```.env
LINE_CHANNEL_SECRET="[Channel Secret]"
```

リクエストヘッダから `x-line-signature` を取るとき、`headers` は `table` をトレースしているため、keyから値を取る時は`get`というものはありません。代わりに `getOrDefault`を使って取得しました。
名前長いし、`get` とかはよく使いそうなのであってもいいんじゃないかな…

## まとめ

Nimって何？レベルから試してみましたが、スッキリ書けて、PythonやCを触ったことがある方なら特に扱いやすいのではないかと思います。
記事自体はLINE Botの導入が強いので、ついでに「Nim、なんか良さそうじゃん」と感じていただけたら幸いです。