雑談Slackの #troom チャンネルにあるBotの話


雑談Slack Advent Calendar 2017 二日目です。

昨日は雑談スラック創設者 samezi部長の [新しくなったSlackinでSlackの招待サイトを作る](https://qiita.com/samezi/items/47639874a095f9a5d16f) のお話でした。

今日はそのSlackでBotを日々作って適当に動かしている話をします。

# そもそもどんなBotなの？

zdnjでは様々なbotが日々動いており、毎日飽きないような作りになっています。
その中でも最近 #troom というチャンネルに追加されたbotがとても皆様に使ってもらい、ありがたいことにほぼ毎日使っていただいてます。

ボットの内容は、 犬の鳴き声(`ワン|わん`)に反応して柴犬の画像をランダムで貼り付けるという単純なものです。

![wan.png](https://qiita-image-store.s3.amazonaws.com/0/72478/4e90f9ad-52fe-962a-a085-273e291d2a8f.png)

botとしてはすごい単調なものですが、人によって様々な使い方がされており、一例ですが次のような使い方がされています。

- 出退勤の打刻的な感じでその日のログインボーナス的な使い方
- タスクの合間の、休憩中に癒やしを求める使い方
- なにか勝ったときのご褒美として
- ただ無駄にわんわん吠える

# ボットの仕組みとソースコード

Qiitaなので技術系のことも話します。

## 仕組み
画像に関しては、`shibe.online` というサイトのAPIを使用しています。
SlackBotが、ユーザーの発言に対して (わん|ワン|wan) が存在したら、APIからURLを貰ってそれをSlackに投稿しています。

## 開発環境
IDE `Visual Studio 2017`
使用言語 `C#` `Mono`
サーバー `Conoha` で `Centos7`
使っているライブラリ
- [SlackAPI](https://github.com/Inumedia/SlackAPI)
- [DynamicJson](https://dynamicjson.codeplex.com/)
- [WebSocket4Net](https://websocket4net.codeplex.com/)

## まずはソースコード

```csharp:main.cs
using Codeplex.Data;
using SlackAPI;
using SlackBotPlugin;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using WebSocket4Net;


internal class Program
{
    private static readonly List<ISlackMessagePlugin> SlackMessagePlugins = new List<ISlackMessagePlugin>();

    private static void Main(string[] args)
    {
        ExecMainProc(args).Wait();
    }

    internal static async Task ExecMainProc(string[] args)
    {
        if (args.Length <= 0) return;

        var token = args[0];
        Console.WriteLine("Token :" + token);

        Console.WriteLine("Connectling.");
        var client = new SlackTaskClient(token);
        var responce = await client.ConnectAsync();
        string webSoketUrl = responce.url;
        if (string.IsNullOrEmpty(webSoketUrl))
        {
            Console.WriteLine("Error.");
            return;
        }
        Console.WriteLine("Ok.");
        Console.WriteLine("Oepn Team [{0}]", client.MyTeam.name);
        Console.WriteLine("SocketURL [{0}]", responce.url);
        Console.WriteLine("Connecting Server...");
        using (var ws = new WebSocket(webSoketUrl))
        {
            bool isExit = false;

            // 文字列受信
            ws.MessageReceived += async (s, e) =>
            {
                var data = DynamicJson.Parse(e.Message);
                if (!data.IsDefined("type")) return;
                string type = data["type"];
                if (!string.IsNullOrEmpty(type))
                {
                    if (type == "goodbye")
                    {
                        Console.WriteLine("Slack goodbye.");
                        isExit = true;
                    }

                    // メッセージ以外は弾く
                    if (type != "message") return;
                    if (data.IsDefined("subtype")) return;
                    // 編集されたテキストは弾く
                    if (data.IsDefined("edited")) return;
                    var channel = data["channel"].ToString();
                    var c = client.FindChannel(channel);
                    if (c == null) return;

                    // troomに投稿されたメッセージか確認
                    if (c.name != "troom" && channel != "troom") return;

                    // 文字列に犬の鳴き声がある
                    string text = data["text"];
                    var mc = Regex.Matches(text, @"(わん|ワン|wan|ﾜﾝ)", RegexOptions.Multiline);
                    if (mc.Count <= 0) return;

                    // shibe.online から画像URLを取得
                    var mes = WWW.GET("http://shibe.online/api/shibes?count=1&urls=true&httpsUrls=false");
                    var res = DynamicJson.Parse(mes);

                    // 同じ画像でも展開されるように、現在時刻をUnixTimeで入れる
                    var imageUrl = string.Format("{0}?{1}", res[0].ToString(), TimeUtil.GetUnixTime(DateTime.Now));

                    // チャンネルに投稿
                    await client.PostMessageAsync(channel, imageUrl, "shibainu", icon_emoji: ":shibainu:");
                }
            };

            // サーバーから切断された
            ws.Closed += (s, e) =>
            {
                // 閉じられた
                Console.WriteLine("Websocket close!");
                isExit = true;
            };

            // サーバ接続開始
            ws.Open();

            // 送受信ループ
            while (true)
            {
                Thread.Sleep(100);

                if (isExit)
                {
                    break;
                }
            }
        }
    }
}
```

## ソースコードの解説

SlackAPIではRTMへの接続まではサポートしていなかったので、自前でWebSocketにつなぎます。
Slackの接続とWebSocketのURLを取得します。

```csharp
var client = new SlackTaskClient(token);
var responce = await client.ConnectAsync();
string webSoketUrl = responce.url;
using (var ws = new WebSocket(webSoketUrl))
{
...
```

WebSocketからのメッセージをDynamicJsonでパースします。
そのあと `IsDefined` でそのJSONにtypeがあるか確認してから利用します。

```csharp
var data = DynamicJson.Parse(e.Message);
if (!data.IsDefined("type")) return;
string type = data["type"];
```

`ws.open()` でWebSocketに接続し、その後無限ループで終了されるまで待機します。

```csharp
// サーバ接続開始
ws.Open();

// 送受信ループ
while (true)
{
    Thread.Sleep(100);
    if (isExit)
    {
        break;
    }
}
```

# さいごに

自分がSlackAPIを入れた時期がこの記事よりかなり前で、幾つか使えない機能があるかもしれないですが、
だいたいこんな感じで柴犬が量産できるかなって思います。

また、柴犬だけでなく、[thecatapi](http://thecatapi.com/) を使えば、ネコ画像botにもなりますし、
お天気APIを使えば、天気APIにもなれます。
これを気にSlackBot作りが流行ればなって思います。

また、この柴犬画像botを使ってみたい方は、是非とも [雑談Slack](http://samezi-but.com/zdnj.html) へどうぞ！

~~次の記事は未定ですが、~~ ぜひ書きたいという方は是非ともどうぞ！
[雑談Slack Advent Calendar 2017](https://adventar.org/calendars/2674)

-- 追記 --
次の記事は Neighbor Kind さんの [最近好きな音楽など](http://corner-of-tokyo.hatenablog.com/entry/2017/12/03/%E5%A5%BD%E3%81%8D%E3%81%AA%E9%9F%B3%E6%A5%BD%E3%81%AA%E3%81%A9) です。