SwiftPMとSlackKitを使ってSlackBotを作った話

この記事は Willgate Advent Calendar 201９ の15日目の記事です。
 
昨日は @ryota_hnk さんの `OKE(Oracle Container Engine for Kubernetes) を Rancher で監視する` でした。
本日は、ウィルゲートでPHPやJavaScript、CSSで開発している@N06ARです！
よろしくお願いします！

# はじめに
私は普段の業務でSwiftを書くことがありませんが、最近で昔作ったiOSアプリを`Swift1`から`Swift5`にジャンプアップデートする機会がありました。
久しぶりのSwiftが楽しかったので、[SlackKit](https://github.com/pvzig/SlackKit)を使いSlackBotを作ってみました。
そこでこの記事では、SlackKitを使ったSlackBotの導入をご紹介します！

## ここで書かれていること

この記事では次の3点について書いています。

- SlackKitの導入方法
- SlackKitを使ったSlackでのメッセージの送受信
- SlackKitで作ったSlackBotを使ってみての感想x

この３点で、私が失敗や上手く行かなかったところは（:bomb:）を書いています。
もし、この記事を読んで作ろう！と思った方は、参考にしてみてください！


# 開発環境

開発環境は、次のバージョンで開発しました。

|          | バージョン     |
| -------- | -------------- |
| OS       | macOS Catalina |
| Swift    | 5.1            |
| SlackKit | 4.5.0          |
| Xcode    | 11             |

# 0.事前準備
コードを書き始める前に、事前に必要なものを準備します。

## 0-1.SlackでAPI Tokenの発行

はじめにSlackからAPI Tokenを取得してきます。
[このURLから](https://my.slack.com/services/new/bot)API Tokenを発行します。

![Botの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/f39d4ab0-bd1f-8115-ba19-bcaeffc44de7.png)

Botの名前を入力し、botを追加するボタンを押すと次の画像が一部として表示される画面が表示されます。
画像のところにAPI Tokenがあるのでメモしておいてください。
（画像のTokenはダミーです）

![API Tokenの表示箇所](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/cab5b398-2a16-840b-819d-ea73b2d002c4.png)

## 0-2.（オプション）LibreSSLのインストール

SlackKitの依存パッケージの中にSwiftNIOがあり、LibreSSLが必要になります。
インストールしていなければ[1-5.ビルド](#1-5.ビルド)でビルドに失敗するのでインストールします。（:bomb:）


```bash
$ brew install libressl
```

# 1.SwiftPMの設定

SwiftPMを使いSwiftパッケージディレクトリ（プロジェクトフォルダ）を作成し、`SlackKit`を使えるようにします。

## 1-1.SlackKitの実行パッケージディレクトリの作成

次のコマンドを実行して、SlackBotを作るディレクトリを作成し、実行パッケージディレクトリを作成します。
`swift package init --type executable`を実行した時に特に入力を求められませんが、問題ありません。

```bash
$ mkdir 【プロジェクト名】
$ cd 【プロジェクト名】
$ swift package init --type executable
```

## 1-2.（オプション）実行確認

次のコマンドを実行した後 `Hello, world!`と表示されたら準備完了です！

```bash
$ swift build
$ ./.build/debug/【プロジェクト名】
```

## 1-3.SwiftPMで使用するパッケージにSlackKitを追加

次に`Package.swift`を編集します。
`Package.swift`をエディタで開き`dependencies`の箇所に今回使用するパッケージの`SlackKit`を追加します。

```Package.swift
import PackageDescription

let package = Package(
    name: "【プロジェクト名】",
    dependencies: [
        .package(url: "https://github.com/pvzig/SlackKit.git", .upToNextMinor(from: "4.5.0"))
    ]
)
```

##1-4. （オプション）テストもしたい場合

テストもしたい場合は、`targets`にもSlackKitのパッケージを追加する必要があります。
SlackKitの追加を忘れると[1-5.ビルド](#1-5.ビルド)でビルドに失敗します。（:bomb:）

```Package.swift
import PackageDescription

let package = Package(
    name: "【プロジェクト名】",
    dependencies: [
        .package(url: "https://github.com/pvzig/SlackKit.git", .upToNextMinor(from: "4.5.0"))
    ],
    targets: [
        .target(
            name: "【プロジェクト名】",
            dependencies: ["SlackKit"]),
        .testTarget(
            name: "【プロジェクト名】Tests",
            dependencies: ["【プロジェクト名】","SlackKit"]),
    ]
)
```


## 1-5.ビルド

Package.swiftを編集した後に、 `swift build`をするとSlackKitを取得し使えるようになります。

```bash

$ swift build
```

## 1-6.（オプション）XcodeやAppCodeを使う場合

XcodeやAppCodeなどのエディタを使いたくなると思います。その時は次のコマンドを実行して`.xcodeproj`を生成してください。

```bash

$ swift package generate-xcodeproj
```

ただし、Package.swiftにパッケージを追加変更する度に、`.xcodeproj`を再生成する必要があります。（:bomb:）
AppCodeで発生しましたが、プロジェクト内のファイル名を変更したときビルドエラーになりました。
その時も再生成してあげると良さそうです。（:bomb::bomb:）

# 2.SlackBotのコードを書く

ここから実際にSlackKitを使ってSlackbotのコードを書いていきます。

## 2-1.main.swiftの編集

まずはじめに、今回SwiftPMで作成した　のエントリーポイントの`main.swift`を編集します。
今回は、SlackBotの機能を`main.swift`に書かず`SlackBot`クラスを作成し分離します。
`SlackBot`のインスタンス生成時に、[0-1.SlackでAPI Tokenの発行](#0-1.SlackでAPI Tokenの発行) で準備したAPI Tokenを引数に入れます。

```Sources/【プロジェクト名】/main.swift
import Foundation
SlackBot(apiToken: "xoxb-XXXXXXXXXXXXXXXXXXXX")
RunLoop.main.run()
```

## 2-3.SlackBotのクラスを作成

次に、`SlackBot`クラス用のファイルを作成し、コードを作成していきます。
コンストラクタ（`init`）で、API Tokenを受け取れるようにして、
SlackKitで`Slack Web API`と`Slack Real Time Messaging API`使えるようにするために`API Token`を渡します。
`API Token`を渡すには、`addWebAPIAccessWithToken`と`addRTMBotWithAPIToken`を使用します。

``` Sources/【プロジェクト名】/SlackBot.swift
import Foundation
import SlackKit

class SlackBot {
    /// SlackBotのKit
    let slackkit = SlackKit()

    init(apiToken: String) {
        slackkit.addWebAPIAccessWithToken(apiToken)
        slackkit.addRTMBotWithAPIToken(apiToken)
    }
}
```

## 2-4.メッセージを受け取る

次にSlackからメッセージを受け取ります。
メッセージは、SlackKitの`notificationForEvent`を使い受け取ります。
受け取ったメッセージの内容は、クロージャの`event.message`に入っており、
`event.message`には発言したユーザIDやチャンネルID、スレッドID、メッセージの文章等が入っています。

``` Sources/【プロジェクト名】/SlackBot.swift
import Foundation
import SlackKit

class Slackbot {
    /// SlackBotのKit
    let slackkit = SlackKit()

    init(apiToken: String) {
        slackkit.addWebAPIAccessWithToken(apiToken)
        slackkit.addRTMBotWithAPIToken(apiToken)
        slackkit.notificationForEvent(.message) { [weak self] (event, client) in
            // ここに聞いたメッセージを受信したときのメッセージを書く！
        }
    }
}
```

### 【余談】[SlackKitのサンプルコード](https://github.com/pvzig/SlackKit/blob/master/Examples/Leaderboard/)のように書くときの注意
[サンプルコードのこの箇所]
(https://github.com/pvzig/SlackKit/blob/master/Examples/Leaderboard/Leaderboard/Sources/Leaderbot.swift#L70)のように、実装した場合でAPI Tokenを渡すと
`listen`メソッド実行時に、`client`クラスが生成されないためか`nil`のため`guard`構文で`return`されるので何もせずに終わります。（:bomb:）

``` Sources/【プロジェクト名】/SlackBot.swift
import Foundation
import SlackKit

class SlackBot {
    /// SlackBotのKit
    let slackkit = SlackKit()

    init(apiToken: String) {
        slackkit.addWebAPIAccessWithToken(apiToken)
        slackkit.addRTMBotWithAPIToken(apiToken)
        slackkit.notificationForEvent(.message) { [weak self] (event, client) in
            self?.listen(client?.client, message: event.message)
        }
    }

    private func listen(_ client: Client?, message: Message?) {
        guard let client = client,
              let message = message,
              let text = message.text,
              let channel = message.channel
                else {
            return
        }
    } 
}
```

## 2-5.メッセージを送信する

メッセージを受け取れるようになったので、今度はメッセージを送れるようにします。
今回は、全てのメッセージに大して`こんにちは`と返事をするメッセージを送信する処理を書きます。
メッセージの送信には、`SlackKit`の`WebAPI`クラスの`sendMessage`メソッドを使用します。
`sendMessage`メソッドの引数には、`送信するチャンネル`、`表示するユーザネーム`、`アイコン画像のURL`、`メッセージ`を入れてください。

``` Sources/【プロジェクト名】/SlackBot.swift
import Foundation
import SlackKit

class SlackBot {
    /// SlackBotのKit
    let slackkit = SlackKit()

    init(apiToken: String) {
        slackkit.addWebAPIAccessWithToken(apiToken)
        slackkit.addRTMBotWithAPIToken(apiToken)
        slackkit.notificationForEvent(.message) { [weak self] (event, client) in
            // ここに聞いたメッセージを受信したときのメッセージを書く！
            slackkit.webAPI?.sendMessage(
                channel: message.channel,
                text: "こんにちは",
                username: "【botの名前】",
                iconURL:  "【アイコン画像のURL】",
                success: nil,
                failure: { (error) in
                    print("メッセージ送信失敗:\(error)")
                }
            )
        }
    }
}
```

これでbotが入ったチャンネルか、DMでメッセージを送った時に`こんにちは`と返事するようになりました！
メッセージ以外にも[Attachmentクラス](https://github.com/pvzig/SlackKit/blob/master/SKCore/Sources/Attachment.swift)を使い[Attachment](https://api.slack.com/docs/message-attachments)をつけることもできます。

### 【余談】アイコン画像とBotの名前がない場合

実のところ`表示するユーザネーム`と`アイコン画像のURL`は、必須ではないので引数に入れず送信することができます。
仮に引数なしで送信をした場合は、次の画像の結果になります。
<img width="173" alt="表示するユーザネームとアイコン画像のURLがない場合の画像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/f546e7f6-c1f3-8751-1fd1-4e5f16953594.png">
アイコン画像が`botの画像`になり、名前が`bot`になります。（:bomb:）
そのため、名前と画像のURLを渡たしてあげる必要があります。

## 2-6.起動！！

最後ビルドして起動します。
起動には、デバックモードの起動とリリースモードの起動があります。

### 2-6-1.デバックモードで起動！

ビルドは今まで通り次のコマンドを入力してください。

```Bash

$ swift build
```

最後に次のコマンドで起動します！
コマンド実行するとシェルの入力はできなくなるので、`nohup`を使いバックグラウンド実行すると良いです。

```bash

.build/debug/【プロジェクト名】
```
### 2-6-2.リリースモードで起動！

ビルドコマンドは今までとは少し異なります。

```Bash

$ swift build -c release
```

最後に次のコマンドで起動します！

```bash

.build/release/【プロジェクト名】
```

# 3.macOS以外で起動したい！
SlackBotを動かすのに`Linux`やサービスを使って動かしたいことがあると思います。
そこで、`Heroku`と`Docker`で動作させる方法をご紹介します。

## 3-1.Herokuにデプロイしたい場合

Herokuにデプロイする場合は、`HerokuCLI`を使用します。
そのため、次のコマンドでHerokuCLIをインストールしてください。

```bash
brew tap heroku/brew && brew install heroku
```

### 3-1-1.Heroku用設定ファイルの追加

はじめに、Heroku用の設定ファイルを追加します。
次のコマンドを実行して、`.swift-version`と`Procfile`を作成してください。

```bash
$ echo 5.1 > .swift-version
$ echo slackbot: .swift-bin/【プロジェクト名】 > Procfile
```
Herokuでデプロイするために、[heroku-buildpack-swift](https://github.com/kylef/heroku-buildpack-swift)を使用します。
[heroku-buildpack-swift](https://github.com/kylef/heroku-buildpack-swift)では、ビルドしたときのディレクトリ名は`.build`ではなく`.swift-bin`なので気をつけてください。（:bomb:）

### 3-1-2.Herokuの設定

次にHerokuCLIでアカウントにログインし、Herokuにアプリケーションを作成します。
アプリケーション作成時に、[heroku-buildpack-swift](https://github.com/kylef/heroku-buildpack-swift)をbuildpackとして追加します。

```bash
$ heroku login
$ heroku create --buildpack kyle/swift 【プロジェクト名】
```

### 3-1-3.デプロイ

Herokuに、コードを`push`すればheroku側でデプロイが開始されます。

```bash
$ git push heroku master
```
起動するには、Herokuの画面から`slackbot`の`Dynos`をオンにします。

## 3-2.Dockerのコンテナにデプロイしたい場合

次は、`Docker`でSlackBotを起動する方法をご紹介します。

### 3-2-1.Dockerfileを書く

まず、プロジェクトのディレクトリに次の`Dockerfile`を追加してください。
（他のディレクトリに`Dockerfile`を置く場合はCOPYの第１引数を修正してください。）
`libssl-dev`をインストールしていますが、これはSwiftNIOで必要となるためです。
`libssl-dev`をインストールしないとbuildできません。（:bomb::bomb::bomb:）

```Dockerfile
FROM swift:5.1
RUN chmod -R o+r /usr/lib/swift/CoreFoundation && \
    apt-get update && \
    apt-get install -y libssl-dev
COPY . /【プロジェクト名】
WORKDIR /【プロジェクト名】
RUN swift build -c release
CMD [ "/【プロジェクト名】/.build/release/【プロジェクト名】"]
```

### 3-2-2.コンテナのビルド

次のコマンドを実行し、Dockerfileを元にビルドします。

```bash
docker build . -t slackbot
```

### 3-2-3.起動！

最後に、次のコマンドを実行してコンテナを立ち上げてください。

```bash
docker run --detach -t slackbot
```

これで、DockerでSlackBotの起動できます。

# 4.SwiftとSwiftPM、SlackKitのよかったところ、困ったところ

今回SwiftでSlackBot作りました、作っていて次のよかったところと困ったところがありました。

## 良かったところ

- macOSなら開発環境がすぐにできる
- sendMessegeメソッド

### macOSならSwiftの開発環境が簡単にできる
さすが開発元がAppleなだけあり、さえXcodeをインストールすればSwiftの開発環境はおおよそ揃えられました。
SlackKitでは追加でライブラリを一つインストールする必要がありましたが
また、Xcode11からはSwiftPMのパッケージを管理できるようになりました。

### sendMessegeメソッド
余談でも書きましたが、SlackKitのWebAPIを使うとSlackBotの名前とアイコン画像を変更することができます。
細かい仕様を調ベていないので詳細は分かりませんが、昔`Node.js`の`BotKit`でSlackBotを作った時はなかった仕様でした。
![名前と画像が切り替えられるサンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/d3803ec2-7533-96a4-c0e5-eab52067d4af.png)
これを逆手にとると、画像のようにメッセージごとに名前とアイコン画像を切り替えることができます。
これが不具合なのか、いつ修正されるか分かりませんが、アイコンを切り替えて感情豊かなBotを作れそうです。

## 困ったところ
- Swiftのパッケージの大半がiOSのパッケージ
- Herokuやbitbucket-pipelineなどがSwiftに公式で対応していない
- 記事などの知見が少なく古い

### Swiftのパッケージの大半がiOSのパッケージ
Googleカレンダーを使った機能を実装したいときに、使えそうなパッケージがないか調べるとiOSでしか使えないパッケージしかない等のことが多かったです。
使ったことがないので正確なことは言えませんが、Vaporのパッケージは色々揃っているので使うと楽になりそうです。

### Herokuやbitbucket-pipelineなどがSwiftに公式で対応していない
AttrasianやHerokuなどのWebサイトを見てみるとわかりますが、自動デプロイに公式で対応してません。
そのため、`Docker`で対処するか、すでにあるライブラリやパッケージ等を頼りに対応する必要がありました。

### 記事などの知見が少なく古い
良くありがちなことですが、今では使用できないものが書かれている記事が多かったです。
主にSlackKitを導入するにあたり、資料が古くSwiftやSwiftPMのバージョンアップにより上手く動作しないことがありました。
特に`libressl`関連と`Heroku`、`Docker`は、問題が解決するより、SlackBotの作成時間の方が短かったです。

# 今回作ったSlackBot

![ななちゃんBot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/a28e6344-ee11-b5de-2c4f-e06756a52f5d.png)

今回作ったSlackBot私の家にいる なな（猫）を模したBotをつくりました。
ななちゃんBotは、私の分報チャンネルに生息しています。

## Botができること
ななちゃんbotには、次の機能を実装しました。

- メッセージにあるキーワード（ななちゃん）があるとリアクションする
- 勤怠の打刻をリマインドとリンクの表示
- GoogleMapでランチまたは、居酒屋を検索したURLの表示
- ユーザを祝ったり応援する機能

残念ながら二つ目の勤怠の打刻の通知以外はあまり使われていませんが、要望や個人的に欲しかった機能を実装しました。

## SlackBotを分報に入れて気がついたこと

### よかったこと

#### Botにキャラクターを持たせること
今回たまたま思いつきでしたが、家のなな(猫)をBotにしたことは正解でした。
ななちゃんBotの最初期は、個人のSlackチームで作っていたことや、SlackKitが手探りだったこと、環境が出来上がっていなかったこともありバグが多かったです。
それに、確認できる環境がSlack上のため、本番で動作確認をしている状態でした。

![ななちゃんの想定外の動き](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/525584e6-9d14-29b2-06ed-2a651afd7237.jpeg)

この時はバグを発見したら極力すぐに直すようにしていました。
ですが、このバグに関してSlackでは次のような反応があり、バグがあっても良いのではという意見がありました

![この自由沙がむしろ猫っぽい](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/0caacd21-51cb-9422-5d0c-653182ad78ed.png)

この件について社内の同僚から、ご紹介いただいたこの[記事](https://mint.hateblo.jp/entry/2014/12/11/233922)に次のことが書かれていました。

>バグは極力直さない
>バカな子ほど可愛いという諺がある
>怒られにくくするには愛着がわきやすい Bot を作るのが大切ということです

![Botに何かしらのキャラクターを入れると愛着がわく](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13618/6d7e911f-02ab-1ea5-5f27-20de734a4baf.png)

読んでみて確かにと思いました。
記事の通り愛着が沸きやすいマスコットとして作った方が周囲から可愛がられ、今後社内で広く使うには良い方法だと感じました。
また、Botで遊んでいるのを見ていると開発のモチベーションも上がるため、偶然でしたが猫のBotとして作ったのは正解だったと思いました。

### 失敗したこと

#### デプロイ方法を見つけるまでローカルで起動していた
今回HerokuとDockerでの起動方法をご紹介しましたが、先月ではまだできないでいて、
最初は私のローカル（MacBook Pro)で動作させていていました。
ただ、ローカルで動作していると、`Sleepするとき`や`会議の移動でネットワークが一時的に切断されたとき`に、オフラインになり使えなくなる状態ができ不便に思うことが増えました。
また、その度に再起動していたのですが、何度もやっていると流石に面倒でした。
そのためMac以外で常時起動できる方法を探しHerokuとDockerに行き着きました。

# 最後に
今回SlackBotをSwiftで作ることで、Swiftのverison1から久しぶりにがっつりコードをかきました。
Version1のころと比較して、結構文法が変わっていましたが変わらないところは変わっていなかったのですんなり使えるようになりました。
また今回初めて技術記事を書きましたが、こんなに文章を書くのが難しいとは思いませんでした。
普段から情報を発信している方々の凄さに驚きました。
最後になりますが、もしこの記事の内容で誤り等ありましたら、お教えいただけますと幸いです。

# 参考・参照文献
- [【SwiftPM】SwiftでもSlack Botを作りたい](https://qiita.com/sotozaki/items/e39a7bdd8de6be9cf725)
- [SlackKit](https://github.com/pvzig/SlackKit)
- [Building Slack Bots In Swift](https://medium.com/@pvzig/building-slack-bots-in-swift-b99e243e444c)
- [heroku-buildpack-swift](https://github.com/kylef/heroku-buildpack-swift)
- [Update to Swift 5.0.1 breaks builds on Ubuntu 18/04](https://github.com/apple/swift-docker/issues/138)
- [Slack で役に立たない Bot を運用するときの知見とか](https://mint.hateblo.jp/entry/2014/12/11/233922)


明日は@8845musignさんです！
よろしくお願いします！