Rubotyでbot宛てのメッセージじゃなくても反応して欲しくてコードを読んだ話


概要

社内で「rubotyってメッセージの文頭にbot名ないと反応してくれないんですか?」
と聞かれたので、これはOSSコミットチャンスか!?
とテンション上がって色々しらべたときのお話。

調査

ドキュメントを読む

ドキュメントを一通り眺めてみる。
それっぽい言葉は無い。
きっと対応していないのだろう。
あんまり用途無いしな。
よ〜し、ちゃんと読んで、きれいに実装するぞ〜!!
そういう気持ちでコードリーディングを開始する。

コードリーディング開始

adapter

何度かコードを読んで、大体仕組みは把握している。
仕組みに興味がある人はtbpgrさんの、このあたりの投稿を読まれると良いだろう。

メッセージ周りは、adapterを読めば良いのだろう。
adapterは、chat toolとrubotyをつなぐためのプラグインである。
今回はshell.rbを読んでいく。

lib/ruboty/adapters/shell.rb
  def listen
    step until stopped?
  rescue Interrupt
    stop
  end

  def step
    case body = read
    when "exit", "quit", nil
      stop
    else
      robot.receive(body: body, source: SOURCE)
    end
  end

ふむ。全てのメッセージをrobot.receiveで受け取っているらしいな。
ということは、メッセージの文頭にbot名無くても大丈夫そう。
次は、robot.rbを読もう。

robot.rb

robot.rbのreceiveを読む。
どうやら、handlersを総なめして、条件にヒットしたものをcallしてるらしい。
ということで、handlerを読もう。
(ちなみに missing: trueのときのみに、handlerの中身は処理される)

lib/ruboty/robot.rb
def receive(attributes)
  message = Message.new(attributes.merge(robot: self))
  unless handlers.inject(false) { |matched, handler| matched | handler.call(message) }
    handlers.each do |handler|
      handler.call(message, missing: true)
    end
  end
end

handlers/base.rb

handlerはrubotyで実行する各種タスクの条件を記載しているclassである。

lib/ruboty/handlers/ping.rb
# pingタスクの例
module Ruboty
  module Handlers
    class Ping < Base
      on(/ping\z/i, name: "ping", description: "Return PONG to PING")

      def ping(message)
        Ruboty::Actions::Ping.new(message).call
      end
    end
  end
end

さて、handlersのcallを読んでいく。
handlers/base.rbにあるようだ。

lib/ruboty/handlers/base.rb
  def call(message, options = {})
    self.class.actions.inject(false) do |matched, action|
      matched | action.call(self, message, options)
    end
  end

ふむ。ここでも別classをcallしているようだ。
どこかで配列actionsに値を代入しているメソッドがあるのだろう。
ということで、actionsに対して値を代入しているメソッドを探す。
どうやら、onメソッドのようだ。Action インスタンスを代入しているらしい。
ということで、次はaction.rbを読む。

lib/ruboty/handlers/base.rb
def on(pattern, options = {})
  actions << Action.new(pattern, options)
end

lib/ruboty/action.rb

もう何度目のcallだろうか。
そういう気持ちでcallメソッドを読んでいく。
ふむふむ。pattern_withで引っかかったやつは実行されるようだ。

lib/ruboty/action.rb
def call(handler, message, options = {})
  if !!options[:missing] == missing? && message.match(pattern_with(handler.robot.name))
    !!handler.send(name, message)
  else
    false
  end
end

そしてなんと....
あれ? all?
え、その正規表現は.....

lib/ruboty/action.rb
def all?
  !!options[:all]
end
# 省略

private

def pattern_with(robot_name)
  if all?
    /\A#{pattern}/
  else
    /#{self.class.prefix_pattern(robot_name)}#{pattern}/
  end
end

結論

handlerのonメソッドに対して、optionでall: trueを入れれば解決。

on(/hear\z/i, name: "hear", description: "", all: true)

うむ....既に機能としてありました。さすがです。
ありがたく使わせていただこうと思います。