Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

至高の言語、Nimを始めるエンジニアへ を書いたエンジニアが、
「Nimいいよ!速いし綺麗だし!とりあえず一回書いてみ?」とアピールが凄いので、とりあえず一回触ってみました。

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

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

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

$ 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 というライブラリマネージャも使えるようになりました。

$ 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 があるのでささっと入れます。

LINE BOT

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

LINE Developers へアクセスして、 Messaging API(ボット)をはじめる を選択してください。
※ LINEのログインが求められます。

LINE_Developers.png

新規作成

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

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

LINE_Developers 4.png

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

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

LINE_Developers 5.png

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

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

Channel基本設定

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

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

  • Channel Secret
  • アクセストークン(ロングターム)

Webhookを用意

いよいよNimで、ユーザが送信したメッセージを受け取ってみます。
まずは最小限のサーバを立てるために、asynchttpserverを使います。

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

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 を付与します。

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

$ nim c -r app.nim

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

$ 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 を使うといいです。

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

$ ngrok http 8888

2__ngrok_http_8888__ngrok_.png

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

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

LINE_Developers 6.png

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

LINE_Developers.png

{
  "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 に環境変数を記載してよしなに読み込んで扱うためのライブラリの初期化です。

let env = initDotEnv()
env.load()
AUTHORIZATION_KEY="[アクセストークン]"

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

$ 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

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

$ 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

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

  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ファイルに環境変数を1つ追加します。

LINE_CHANNEL_SECRET="[Channel Secret]"

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

まとめ

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした