SlackのreactionをリアルタイムなON/OFFスイッチとして使う

Slackはいわゆるチャットツールなのですが、他のサービスと比べると外部連携に力を入れているようで（個人の感想です）、APIが充実しているので触っていて面白いです。

この記事ではAPIの中でもreaction関連の部分にフォーカスしてみます。

## この記事でやること

+ reactionの特徴を考える
+ reactionをスイッチとして使ってみる

## その前にreactionとは何か？

reactionというのは、いわゆるFacebookの「いいね！」ボタンみたいなものを、各メッセージに後付でつけたり消したりすることができる機能です。

画像で説明すると、メッセージの下の「親指上げる絵文字」のところがreactionです。

![2015-12-02_055330.png](https://qiita-image-store.s3.amazonaws.com/0/32778/0facccbc-cab8-7744-fa0f-9f3f6dbf84eb.png)


詳しい説明はこちら。[Slackで、投稿に対して絵文字で「いいね！」などのリアクションが返せるようになった](http://qiita.com/mono0926/items/7ccd4599a75a97711442)

## reactionはどう使うと効果的か

reactionは、他のメンバーから各記事に「いいね」をもらったり投票したりするのに使うのが一般的です。それ以外の用途に使おうと思った場合、まずreactionの特徴から考えるのがよさそうです。

+ ボタンを押すだけなので、意思表示しやすい（メッセージを打たなくて良い）
  + 一度押すと＋１で、もう一度押すと－１（マイナス１）になる
+ Slackの[Real Time Messaging API](https://api.slack.com/rtm)だと、リアルタイムにreaction ON/OFFのイベントがあがってくるので、BOTがすばやく反応できる
+ そのチャンネルの議論とは異なるメッセージのやりとりを、reaction側に寄せることができる
  + BOTもメッセージ投稿せずにreactionをつけたり外したりできる
+ 最初にメッセージにreactionを付与するのが結構面倒
  + 一度つけられたreactionへの操作は超楽

上記を元に考えると、

+ 何かのON/OFFスイッチとしてreactionを利用する
+ reactionはなんらかのメッセージをキーに、BOTに付与してもらう

というアイデアを思いつきました。

## SlackでLEDチカチカ
何をON/OFFさせるのか考えたのですが、リアルタイムON/OFFといえば自作電子回路のLEDをチカチカさせるのが便利さが伝わりやすそうです。

便利さを強調するために、iPhone上のSlackから、BOT経由で、小型Wifiモジュール ESP-WROOM-02を利用した電子回路のLEDをチカチカさせてみます。両方場所選ばないので、どこでもLチカが簡単に実現できます（今回は電源部分だけ有線です）

## 準備（Slack BOT側）
SlackのBOTのプログラムは、[slappy](https://github.com/wakaba260/slappy)のv0.6.0を利用しました。
参考記事：[Slappy - 簡単にslackのbotを作れるgemを作った](http://qiita.com/wakaba260/items/0c6c69467046c21847cf)

BOT用のスクリプトを本体から独立したDSLで記述できるところと、Slack RTMイベントのフックがサポートされているところが超便利です。

今回は、SlackのRTMイベントの[reaction_added event](https://api.slack.com/events/reaction_added), [reaction_removed event](https://api.slack.com/events/reaction_removed)をフックして、+1されたらLED点灯、-1されたらLED消灯としています。

一部そのままでは動かなかった部分(lib/slappy/listeners/concerns/validatable.rb)と手抜きしたかった部分(lib/slappy/event.rb)を改造しています。
(2015/12/4追記：v0.6.1で以下の2項目について採用されましたので、v0.6.1を利用する場合は下記パッチは不要です。コメント欄参照）

```ruby:lib/slappy/listeners/concerns/validatable.rb
module Slappy
  module Listener
    module Validatable
      include Slappy::Debuggable

      attr_accessor :pattern

      def valid?(event)
        unless time_valid?(event)
          Debug.log 'Event happend in before start time'
          Debug.log event.ts
          Debug.log Slappy.client.start_time
          return false
        end

        target = event.send(target_element)
        unless target
          Debug.log 'Target is nil'
          return false
        end

        event.matches = target.match pattern
        unless event.matches
          Debug.log "Target is not match pattern(#{pattern})"
          return false
        end

        true
      end

      private

      def time_valid?(event)
        # eventによってはtsがない場合があるのでその場合は無視
        return true unless event.has_key?(:ts)
        event.ts > Slappy.client.start_time
      end
    end
  end
end
```

```ruby:lib/slappy/event.rb
module Slappy
  class Event
    extend Forwardable
    include Debuggable

    attr_accessor :matches

    def_delegators :@data, :method_missing, :respond_to_missing?

    def initialize(data)
      @data = Hashie::Mash.new data
    end

    def text
      @data['text'].to_s
    end

    def channel
      SlackAPI::Channel.find(id: @data['channel']) ||
        SlackAPI::Group.find(id: @data['channel']) ||
        SlackAPI::Direct.find(id: @data['channel'])
    end

    def user
      SlackAPI::User.find(id: @data['user'])
    end

    def ts
      Time.at(@data['ts'].to_f)
    end

    def reply(text, options = {})
      options[:text] = text
      options[:channel] = channel
      Messenger.new(options).message
    end

    def reaction(emoji)
      result = ::Slack.reactions_add name: emoji, channel: @data['channel'], timestamp: @data['ts']
      Debug.log "Reaction response: #{result}"
    end

    def bot_message?
      @data['subtype'] && @data['subtype'] == 'bot_message'
    end

    # @dataの内容を外で直接扱いたい（手抜きしたい）
    def data
      @data
    end
  end
end
```

BOTスクリプトは、`slappy new`した時に作成される、slappy-scripts/example.rbを直接書き換えました。

ESPSERVERのところは、次に立てるWifiモジュールのIPアドレスを入れてください。

ここではローカルにあるPCからSlackにアクセスして、同じくローカルネットワーク内にあるWifiモジュールとプライベートIPで接続する想定にしていますが、グローバルIPがあれば世界中どこでもつながります。

BOT_USERIDのところは、BOTがreactionを付与する際にもイベントがあがるので、BOTだったらLチカさせないようにする必要があるのですが、BOTかどうかの判定がイベントであがってこないので、BOTのID（xoxx-～のIDではなくて、UxxxxxxxというBOTのユーザIDとして付与されるID）を調べて入れてください。
（イベントあがってきたときのeventオブジェクトの内容を`p event`等でコンソールにdumpすると出てくるuserパラメータの値になります。）

```ruby:slappy-scripts/example.rb
require 'net/http'
require 'uri'

ESPSERVER = ##YOUR WIFI WEBSERVER IP##
BOT_USERID = ##YOUR BOT USERID##
BUTTON1 = 'red_circle'
BUTTON2 = 'large_blue_circle'

hello do
  puts 'successfly connected'
end

hear 'Lチカ' do |event|
  event.reaction BUTTON1
  event.reaction BUTTON2
end

monitor 'reaction_added' do |event|
  next if event.data["user"] == BOT_USERID 
  button =
    case event.data["reaction"]
    when BUTTON1; 1
    when BUTTON2; 2
    else 0
    end
  Net::HTTP.get(URI.parse(ESPSERVER + button.to_s + '/on')) if button>0
end

monitor 'reaction_removed' do |event|
  next if event.data["user"] == BOT_USERID 
  button =
    case event.data["reaction"]
    when BUTTON1; 1
    when BUTTON2; 2
    else 0
    end
  Net::HTTP.get(URI.parse(ESPSERVER + button.to_s + '/off')) if button>0
end
```

## 準備（Wifi WEB Server側）
Arduino IDEにESP8266用のデータをダウンロードすると、スケッチの例として、ESP8266WebServer/HelloServerというプログラムを読み込むことが出来ます。

参考記事：[技適済み格安高性能Wi-FiモジュールESP8266をArduinoIDEを使ってIoT開発する為の環境準備を10分でやる方法](http://qiita.com/azusa9/items/264165005aefaa3e8d7d)

今回は、HelloServerのプログラムを改造しました。`http://##ipaddr##/1/on`でLED1が点灯、`http://##ipaddr##/2/on`でLED2が点灯、onをoffに変えると消灯するプログラムになります。

```helloserver.ino
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

const char* ssid = ##YOUR SSID##;
const char* password = ##YOUR Wifi PASSWORD##;

ESP8266WebServer server(80);

const int led1 = 13;
const int led2 = 12;

void setup(void){
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  digitalWrite(led1, 0);
  digitalWrite(led2, 0);
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(WiFi.localIP());

  server.on("/1/on", [](){
    digitalWrite(led1, 1);
    server.send(200, "text/plain", "led1 on");
  });
  server.on("/2/on", [](){
    digitalWrite(led2, 1);
    server.send(200, "text/plain", "led2 on");
  });
  server.on("/1/off", [](){
    digitalWrite(led1, 0);
    server.send(200, "text/plain", "led1 off");
  });
  server.on("/2/off", [](){
    digitalWrite(led2, 0);
    server.send(200, "text/plain", "led2 off");
  });
  
  server.begin();
  Serial.println("HTTP server started");
}

void loop(void){
  server.handleClient();
}
```

Arduino IDEから、ESP-WROOM-02にプログラムを転送します。シリアルモニタを立ち上げて、WEBServerが立ち上がったことと、IPアドレスを確認してください。(IPアドレスは、準備（Slack BOT側）のBOTスクリプトに埋め込む必要があります）

## 動かそう

iPhoneとWifiモジュールに回路を組み込んだものを重ねてみました。

![P1030266_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/32778/e0ee8ef6-a4ed-ec13-0841-0bcf6fccfa62.jpeg)

画像だと伝わりにくいですが、「Lチカ」とメッセージを書くとbotがreactionを２つつけてくれます。そのreactionをそれぞれ押すと、下の回路の対応する赤LEDと青LEDが光る／消えるとなります。

![P1030267_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/32778/c40e4281-5352-7cdd-936e-406a66e173c5.jpeg)

動画だとこういう感じです。

![output_7.gif](https://qiita-image-store.s3.amazonaws.com/0/32778/f14a4cdc-d0fb-c05b-f729-e4cc81950d88.gif)

「Lチカ」とメッセージを送った後、reactionが出てくるのが遅いのは、RTM APIではなくて、[Slack WEB API](https://api.slack.com/web)を使わないといけないため、1APIにつき1秒のリミットがあるからです。

![output_12.gif](https://qiita-image-store.s3.amazonaws.com/0/32778/65fa0bc9-9a0a-adc4-3d2d-251f6987a030.gif)

reactionを押すと、押した直後にLEDが点灯していますね。これぐらいだとわりと実用的に使えるのではないでしょうか。

## まとめ
SlackとIoTは親和性高いんじゃないかと思いました。スマホから簡単に使えるので、中途半端なインターフェース作るくらいなら、Slackでまずはプロトタイプというのも選択肢に入りそうです。