最近、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のログインが求められます。
新規作成
【STEP1 プロバイダーを選択してください】
プロバイダー名を適当に入れてください。
【STEP2 Messaging APIの情報を入力してください】
大事なアプリ名や業種を入力します。
プランは Developer Trial
を使います。
【STEP3 入力内容をご確認いただき作成ボタンで完了してください】
特にここまでは問題ないと思います。
Channel基本設定
作成し終えるとBotの Channel基本設定
が確認できると思います。
今回使う情報は、以下の2つです。メモしておいてください。
※ 初期状態だとアクセストークンは生成されていないと思うので、その時は再発行をしてください。
- Channel Secret
- アクセストークン(ロングターム)
Webhookを用意
いよいよNimで、ユーザが送信したメッセージを受け取ってみます。
まずは最小限のサーバを立てるために、asynchttpserverを使います。
とりあえず確認するだけのコード。
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
LINE BOT側の設定で、以下の2つを変更します
- Webhook送信を「利用する」に変更
- Webhook URL を
ngrok
で発行されたもの + パスを指定
接続確認
をクリックすると、立ち上げたサーバに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ですね。
オウム返し
先にコードを。
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
いかがでしょう? オウム返しできたと思います!
おまけ
もうここまで出来たら、あとはアイデア次第!ですが、
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
を取るとき、headers
は table
をトレースしているため、keyから値を取る時はget
というものはありません。代わりに getOrDefault
を使って取得しました。
名前長いし、get
とかはよく使いそうなのであってもいいんじゃないかな…
まとめ
Nimって何?レベルから試してみましたが、スッキリ書けて、PythonやCを触ったことがある方なら特に扱いやすいのではないかと思います。
記事自体はLINE Botの導入が強いので、ついでに「Nim、なんか良さそうじゃん」と感じていただけたら幸いです。