RubyでSlackのボットを書く方法(なるべく自力で)

RubyでSlackのボットを書くには、slack-ruby-client gemruboty gemなどを使うのが一般的だと思います。 しかし個人的には、Slackボット程度でgemを使うのは好みでないので、なるべく素のRubyだけで書くようにしています。 その方法をまとめておきます。

Slack appを登録する

まず、https://api.slack.com/appsで"Create New App"して、適当に設定をします。

次のYAMLを"App Manifest"に貼ってSave Changesすると一気に設定できます。

display_information:
  name: Sample Slack App
features:
  bot_user:
    display_name: Sample Slack App
    always_online: true
oauth_config:
  scopes:
    bot:
      - app_mentions:read
      - chat:write
settings:
  event_subscriptions:
    request_url: https://example.com/
    bot_events:
      - app_mention
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false

かんたんに説明すると、メンションされたときに通知を受け取る(app_mentions:read Scopeとapp_mention Event Subscription)、チャンネルで発言する(chat:write Scope)、という設定です。

なお、メンションではない発言も全部受け取りたかったらchannels:history Scopeとmessage.channels Event Subscriptionを足すとよいです。

ボットからSlackにメッセージを送る

chat.postMessage APIを叩くだけです。 標準ライブラリだけで簡単にできます。

require "net/http"
require "json"

TOKEN = "xoxb-..."    # Bot User OAuth Token を埋める
CHANNEL = "CXXXXXXXX" # Channel ID を埋める

resp = Net::HTTP.post_form(
  URI.parse("https://slack.com/api/chat.postMessage"),
  {
    token: TOKEN,
    channel: CHANNEL,
    text: "Hello",
  }
)

json = JSON.parse(resp.body, symbolize_names: true)
pp json[:ok]  #=> true on success, false on failure

必要な設定は2つです。

  • Slack appの設定画面の"OAuth & Permissions"からBot User OAuth Tokenをコピーして、TOKENに入れる
  • 発言したいSlackチャンネルの"View channel details"の最下部にあるChannel IDをコピーして、CHANNELにいれる

そうしてコードを実行すれば、発言できるはずです。

ボットから発言した様子

ここでは単純に"Hello"というテキストを送っていますが、text: "Hello"の代わりに次のようなものを書けば、Slackのmarkdown風のマークアップができます。

{
  blocks: [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "*Hello* `world`"
      }
    }
  ]
}

このあたりについて詳しくはReference: blocksをご参照ください。 Block Kit Builderインタラクティブに構築することもできるようです。

Slackからイベント通知を受け取る

Slackから「メンションされた」や「誰かが発言した」などのイベントの通知を受け取るには、Events APIを使います。

2024年現在、Events APIには2種類の通信方法があります。

  • publicなHTTPサーバを立てて、HTTPリクエストとして通知を受け取る
  • WebSocketでSlackに接続し、プッシュ通知を受け取る(Socket Mode

両方かんたんに説明します。

HTTPサーバを立てる方法

sinatra gemでHTTPサーバを書きます。gemですが、sinatraは自分の心の許容リストに入っているのでOKとしています。

require "sinatra"
require "json"
require "openssl"

SIGNING_SECRET = "..." # Signing Secret で埋める

def verify_signature(timestamp, body, sig_actual)
  msg = ["v0", timestamp, body].join(":")
  sig_expected = "v0=" + OpenSSL::HMAC::hexdigest(OpenSSL::Digest::SHA256.new, SIGNING_SECRET, msg)
  OpenSSL.secure_compare(sig_actual, sig_expected)
end

post "/" do
  body = request.body.read

  # Slack からの POST であることを検証する
  halt 401, "{}" unless verify_signature(
    request.env["HTTP_X_SLACK_REQUEST_TIMESTAMP"],
    body,
    request.env["HTTP_X_SLACK_SIGNATURE"]
  )

  json = JSON.parse(body, symbolize_names: true)

  case json[:type]
  when "url_verification"
    # Slack に URL を登録するときのイベント、challenge をそのまま返せば良い
    json[:challenge]

  when "event_callback"
    event = json[:event]

    case event[:type]
    when "app_mention"
      # メンションされた
      p event[:text]
    end
    ""

  else
    ""
  end
end

必要な設定は1つだけです。

  • Slack appの設定画面の"Basic Information"の"App Credentials"からSigning Secret(hexで32桁)をコピーして、SIGNING_SECRETにいれる

このサーバをインターネットからアクセスできるところで実行します。 実験ではngrokなど使うとよいかもしれません。

$ ruby ~/bot-server.rb
...
== Sinatra (v4.0.0) has taken the stage on 4567 for development with backup from WEBrick
[20XX-XX-XX XX:XX:XX] INFO  WEBrick::HTTPServer#start: pid=XXXXXX port=4567

そして、"Event Subscriptions"のRequest URLで、立てたサーバのURLを入力します。 うまく行けば"Verified"となります。

URLの設定に成功した様子(Verified)

Slackでボットに対して@Sample Slack Bot Helloなどとメンションしてみましょう。 うまく行っていれば、HTTPサーバの方に発言内容が出ているはずです。

"<@XXXXXXXXXXX> Hello"
XXX.XXX.XXX.XXX - - [XX/XXX/20XX:XX:XX:XX +0900] "POST / HTTP/1.1" 200 - 0.0064

Socket Modeを使う方法

Socket Modeは、RubyからWebSocketでSlackに接続してプッシュ通知を受け取る方法です。 publicなHTTPサーバを用意しなくてよいので、運用は手軽かもしれません。

残念ながら、RubyでgemなしでWebSocketクライアントを使うのは大変です。いろいろ調べましたが、満足できる方法はみつけられませんでした。

調べたこと(クリックしたら詳細表示)

WebSocketプロトコルを扱うwebsocket gemはよくできていて、依存もゼロなので好みです。

ただ、実際に通信するgemとなると、eventmachineだったりfaradayだったり、巨大なgemに依存しがちです。

その点websocket-client-simple gemは、その手のものに依存しないクライアントというコンセプトは好きなのですが、細かいところで気になることが多かったです *1 。これを好みに合わせて直すくらいなら、Slackボットに特化したかんたんなものを自作するかって気分になりました。

net/httpがWebSocketをサポートしてくれたらいいのになあ。

ということで、あきらめてwebsocket gemのみに依存するslack_socket_mode_botというgemを作りました。それを使う例だけ示しておきます。

require "slack_socket_mode_bot"

SLACK_BOT_TOKEN = "xoxb-..."
SLACK_APP_TOKEN = "xapp-..."

bot = SlackSocketModeBot.new(token: SLACK_BOT_TOKEN, app_token: SLACK_APP_TOKEN) do |data|
  if data[:type] == "events_api" && data[:payload][:event][:type] == "app_mention"
    event = data[:payload][:event]

    p event[:text]
  end
end

bot.run

必要な設定は3つです。

  • Slack appの設定画面の"Socket Mode"でEnable Socket Modeを有効にする
  • Slack appの設定画面の"OAuth & Permissions"からBot User OAuth Tokenをコピーして、SLACK_BOT_TOKENに入れる(`xoxb-で始まるもの)
  • Slack appの設定画面の"Basic Information"の"App-Level Tokens"を作って、SLACK_APP_TOKENにいれる(xapp-で始まるもの)

あとは、このコードを実行するとSlackと通信し始めます。

$ ruby bot.rb

Slackでボットに対して@Sample Slack Bot Helloなどとメンションしてみましょう。 うまく行っていれば、発言内容が通知され、pで出力されるはずです。

"<@XXXXXXXXXXX> Hello"

ちなみに、Socket ModeのWebSocketはあくまでイベント通知を受け取るためだけのものであり、これを経由してchat.postMessageなどのAPIを呼ぶことはできません。 slack_socket_mode_botでは、SlackSocketModeBot#call(method, data)というAPIを呼ぶ方法もおまけで付けておきました。

まとめ

なるべくgemに頼らずにRubyでSlackのボットを書く方法を説明しました。 ここで述べた方法は、Ruby開発者のSlack workspaceで動くボットたちで長期間運用しています。

techlife.cookpad.com

もちろんslack-ruby-client gemなどを使うのがかんたんだと思うし、抵抗がないならそれが賢いと思います。 ただ、slack-ruby-client gemはSlack社謹製ではないので、たとえばまだSocket Modeに対応していないようです。

なぜgemに頼らないかというと、依存を減らしたいという個人的な好み(いわゆるNot Invented Here症候群かも)が最大の理由ですが、Slackはわりと頻繁にAPIを仕様変更するので、多少面倒でもSlack公式のAPIを直接叩いておくほうが長期的にはメンテナンス性が高いのでは? と思ったり思わなかったり。

変更履歴

  • 06/24 22:00 OpenSSL.secure_comparehalt 401, "{}" を使うようにした(thanks @sora_h)

*1:websocket gem以外にも依存があるところ、内部的にThreadを作っているところ、ソケットを1文字ずつ読み込んでいるところ、pingフレームを処理してくれないところ、などなど。