[Slimaneチュートリアル] Swift + Slimane + Docker + HerokuでLINE Botを作成する


このドキュメントは、Swift製のウェブアプリケーションフレームワークであるSlimaneを利用した開発の流れを体験することを目的としたチュートリアルです。

このチュートリアルでは、受け取ったメッセージをそのまま返信するだけの簡単なLINE Botを作成し、Dockerを利用してHerokuへデプロイします。

## 事前準備

下記のアカウントが必要です。

* Heroku
* LINE developers （あらかじめチャンネルを作成しておく）

下記のものをあらかじめインストールしておきます。

* Heroku toolbelt （あらかじめログインしておく）
* Docker
* swiftenv
* npm

また、Swift 3.0の開発版を利用するため、下記のようにしてあらかじめインストールしておいてください。

```
swiftenv install swift-DEVELOPMENT-SNAPSHOT-2016-05-31-a
```

## Slimane CLIのインストール

下記のようにしてSlimane CLIをインストールします。

```
npm install -g slimane-cli
```

##プロジェクトの作成

下記を実行することで、空のSlimaneプロジェクトを生成できます。ここでは`slimane-tutorial-linebot`というプロジェクト名で生成します。

```
slimane new slimane-tutorial-linebot
```

`slimane-tutorial-linebot`というディレクトリが生成され、最低限必要なファイルが同時に生成されています。以降の作業は全てこのディレクトリの中で行います。

## プロジェクトのビルドと実行

まだ何もコードを書いていませんが、一旦この空のプロジェクトをビルドしてみましょう。下記のようにすることで、Slimaneプロジェクトをビルドできます。

```
slimane build
```

しばらく待つとビルドが完了するので、下記のようにして実行します。

```
slimane run
```

無事に起動すると3000番ポートを待ち受けるようになっているため、試しにHTTPリクエストを投げてみましょう。

```
curl 'http://localhost:3000/'
```

`Welcome to Slimane!`というメッセージが返ってくれば成功です。

## Dockerイメージのビルド

せっかくなので、このSlimaneアプリケーションをDockerで動かせるようにしてみましょう。下記の内容で`Dockerfile`と`.dockerignore`を作成します。

```:Dockerfile
FROM takebayashi/slimane
```

```:.dockerignore
.build
.git
Packages
```

下記のようにしてビルドと実行をしてみます。

```
docker build -t slimane-tutorial-linebot .
docker run -i -p 3000:3000 -t slimane-tutorial-linebot
```

さきほどと同じように3000番ポートにリクエストを投げてみて、正常にレスポンスが返ってくれば成功です。

（[ここまでの手順を反映したものをGitHubで見る](https://github.com/takebayashi/slimane-tutorial-linebot/tree/0bb5c35409edd872fe2aabd8edbacc3df718de3b)）

## 依存パッケージの追加

このチュートリアルでは下記のパッケージを利用することにします。

* [SwiftLineBot](https://github.com/takebayashi/SwiftLineBot)
* [HTTPSClient](https://github.com/VeniceX/HTTPSClient)

`SwiftLineBot`はLINE Bot APIで利用されるJSONデータのシリアライズおよびデシリアライズを行うライブラリです。Botがコールバックリクエストを受ける際にはJSONデータを一緒に受け取りますが、そのデータのパースなどにこのライブラリを利用します。

`HTTPSClient`は、HTTPSリクエストを発行するためのライブラリです。受け取ったメッセージに対して返信を行う際にはHTTP APIをコールする必要があるため、その際にこのライブラリを利用します。

この2つのライブラリを利用できるようにするため、`Package.swift`を下記のように変更します。

```diff
--- a/Package.swift
+++ b/Package.swift
@@ -3,6 +3,8 @@ import PackageDescription
 let package = Package(
     name: "slimane-tutorial-linebot",
     dependencies: [
-        .Package(url: "https://github.com/noppoMan/Slimane.git", majorVersion: 0, minor: 6)
+        .Package(url: "https://github.com/noppoMan/Slimane.git", majorVersion: 0, minor: 6),
+        .Package(url: "https://github.com/takebayashi/SwiftLineBot.git", majorVersion: 0, minor: 0),
+        .Package(url: "https://github.com/VeniceX/HTTPSClient.git", majorVersion: 0, minor: 8)
     ]
 )
```

（[ここまでの手順を反映したものをGitHubで見る](https://github.com/takebayashi/slimane-tutorial-linebot/tree/02c5c11f1f5657ff30a2eec6f0a8388398f02d7b)）

## 設定項目の追加

LINE Botを動かすには、Channel ID、Channel Secret、MIDの3項目が必要となるため、これらを設定値として環境変数から取得できるようにします。

`main.swift`にはもともと環境変数から値を取得するコードが含まれているため、その下に上記の設定値を取得するコードを追加します。

```diff
--- a/Sources/main.swift
+++ b/Sources/main.swift
@@ -12,6 +12,9 @@ var PORT: Int {
 }
 let HOST = Process.env["HOST"] ?? "0.0.0.0"
 let SLIMANE_ENV = Process.env["SLIMANE_ENV"] ?? "development"
+let LINE_CHANNEL_ID = Process.env["LINE_CHANNEL_ID"]!
+let LINE_CHANNEL_SECRET = Process.env["LINE_CHANNEL_SECRET"]!
+let LINE_CHANNEL_MID = Process.env["LINE_CHANNEL_MID"]!

 do {
     try launchApp()
```

## Botの実装

実際にLINEメッセージを受け取り返信する部分を実装します。

```diff
--- a/Sources/app.swift
+++ b/Sources/app.swift
@@ -1,4 +1,6 @@
 import Slimane
+import SwiftLineBot
+import HTTPSClient
 
 func launchApp() throws {
     let app = Slimane()
@@ -17,6 +19,58 @@ func launchApp() throws {
         }
     }
     
+    app.post("/") { req, responder in
+        switch req.body {
+        case let .buffer(d):
+            for message in ReceivedMessage.parseAll(json: d) {
+                switch message.content {
+                case let textMessage as ReceivedTextMessageContent:
+                    // create a message for replying
+                    let sendingMessage = SendingMessage(
+                        toUsers: [textMessage.fromUser],
+                        toChannel: 1383378250, // fixed value
+                        eventType: .sendingMessage,
+                        content: SendingTextMessageContent(
+                            contentType: .text,
+                            toType: 1, // fixed value
+                            text: "Re: " + textMessage.text
+                        )
+                    )
+                    let serialized = sendingMessage.toJSONString()
+                    
+                    // send the message
+                    let uri = URI(scheme: "https", host: "trialbot-api.line.me", port: 443)
+                    do {
+                        let client = try HTTPSClient.Client(uri: uri)
+                        let headers: Headers = [
+                            "X-Line-ChannelID": LINE_CHANNEL_ID,
+                            "X-Line-ChannelSecret": LINE_CHANNEL_SECRET,
+                            "X-Line-Trusted-User-With-ACL": LINE_CHANNEL_MID,
+                            "Content-Type": "application/json; charset=UTF-8"
+                        ]
+                        let response = try client.send(
+                            method: .post,
+                            uri: "/v1/events",
+                            headers: headers,
+                            body: Data(serialized)
+                        )
+                    }
+                    catch {
+                        print("some errors occured")
+                    }
+                    break
+                default:
+                    print("received unsupported message")
+                    break
+                }
+            }
+        default:
+            print("received unsupported request")
+        }
+        responder {
+            Response(body: "OK")
+        }
+    }
 
     print("The server is listening at \(HOST):\(PORT)")
     try app.listen(host: HOST, port: PORT)
```

このコードでは主に以下の処理を行っています。

1. `/`へのPOSTリクエストを受け付ける
2. 受け取ったJSONデータをパースし、受信メッセージ`ReceivedMessage`の配列として受け取る
3. メッセージコンテントが`ReceivedTextMessageContent`であった場合、返信用メッセージ`SendingMessage`を作成する
  * メッセージ本文は"Re: {受信メッセージ本文}"とする
4. 3.で作成したメッセージをJSONデータとしてシリアライズする
5. `https://trialbot-api.line.me/v1/events`に対して返信のためのリクエストを送信する

（[ここまでの手順を反映したものをGitHubで見る](https://github.com/takebayashi/slimane-tutorial-linebot/tree/6b6eaf0e15b8a334bb6ddc87612fa39a1dcc2d10)）

## Herokuへのデプロイ

下記のようにしてHeroku上のアプリケーションを作成します。

```
heroku create
```

次に、環境変数を設定します。`${LINE_CHANNEL_ID}`、`${LINE_CHANNEL_SECRET}`、`${LINE_CHANNEL_MID}
`には実際にはLINE developersで確認した値を入れてください。

```
heroku config:set LINE_CHANNEL_ID=${LINE_CHANNEL_ID}
heroku config:set LINE_CHANNEL_SECRET=${LINE_CHANNEL_SECRET}
heroku config:set LINE_CHANNEL_MID=${LINE_CHANNEL_MID}
```

最後に下記のコマンドを実行するとDockerイメージのビルドとHerokuへのpushが行われます（数分かかります）。

```
heroku container:push web
```

これでデプロイは完了です。`heroku open`を実行するとデプロイしたアプリケーションがウェブブラウザで開きます。`Welcome to Slimane!`と表示されていればデプロイ成功です。

## Botの設定と動作確認

LINE developersのチャンネル設定画面を開き、Callback URLに`heroku open`で開いたURIを入力します。この時、`https://xxxxxxxx.herokuapp.com:443/`のようにポート番号をつけた形にします。設定後、Verifyボタンを押して`Success.`と表示されれば設定完了です。

実際にLINEでチャンネルに対してメッセージを送ってみて、返信メッセージが返ってくれば成功です。

![スクリーンショット](https://qiita-image-store.s3.amazonaws.com/0/1866/bffa9702-037e-aee2-7194-640699b40b7c.png)

このチュートリアルでは、Slimaneプロジェクトの作成、ビルド、Dockerイメージのビルド、Herokuへのデプロイを学びました。

（[完成品をGitHubで見る](https://github.com/takebayashi/slimane-tutorial-linebot)）