読者です 読者をやめる 読者になる 読者になる

チャットで不在時に発言されたメッセージの扱い方

HipChatやSlackで使われているXMPPにおける不在時に発言されたメッセージの扱われ方、 というきわめてニッチでゴキゲンな話題をお届けします。

Delayed Delivery

XEP-0203という拡張仕様があり、 リアルタイムではなくあとから遅れてメッセージが送られる場合の仕様が定義されている。 あるユーザにメッセージを送ったが宛先のユーザがオフラインだった場合にサーバ側でメッセージを保存しておき、 ユーザがログインしたときに再送するという用途がある。また、チャットルームでの発言履歴機能などにも関係している。 例を見たほうが早そう。

<message from='romeo@montague.net/orchard' to='juliet@capulet.com' type='chat'>
  <body>
    Goodbye, cruel world
  </body>
</message>

宛先のユーザがログイン状態であれば通常こういう形式で届くメッセージが、ログアウト状態だった場合には次のような形式になる。 message要素の中にdelay要素が追加され、この要素がDelayed Deliveryのためのものであること(xmlns属性)や、 このメッセージ遅延させて送ってきた送り元(from属性)、 そしてこのメッセージが発言された元々の時刻(stamp属性)が含まれている。

<message from='romeo@montague.net/orchard' to='juliet@capulet.com' type='chat'>
  <body>
    Goodbye, cruel world
  </body>
  <delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z'>
    Offline Storage
  </delay>
</message>

XMPPの拡張仕様の仕組み

XMPPXMLベースでメッセージをやりとりする仕組みで、RFC3920とRFC3921に中核部分の仕様が定義されている。 更にXEP (XMPP Extension Protocol)という仕様が幾つかあって、ここで拡張仕様が定義されている。 受け取ったXMLに知らない要素が含まれていても基本的に無視しておくという性質を持たせておき、 XEPにおいて「この要素が含まれている場合はこういう挙動をする」という拡張仕様を定義することで、 拡張仕様に対応している場合にのみ何かを行うという挙動を実現している。

Delayed Deliveryによって問題になること

よく問題になるのはXMPP上でBOTを運用する場合。 通常のユースケースではあまり問題になることはなく、 寧ろチャットルームにログインしたときに自分が不在だった間に発言された過去の履歴が取得できて嬉しいことの方が多いと思う。 BOTはある命令に反応してある処理を行うというものが多いので、 例えばBOT用のプログラムに問題があって再起動した場合、 再起動中に発言された過去の発言全てに対して起動時に反応していちいち全ての処理を行ってしまう場合がある。 (例えば、BOTがたまたま停止していて「アレ反応しない...」とか言って何度もデプロイの命令を送ってしまうと、 このあとめちゃくちゃデプロイされるなど)

ライブラリ側での対応

BOTは過去の発言を無視する」というポリシーを実装してこの問題に対処することにした。 ウチのBOTRubotyというBOTフレームワークで動いていて、 チャットサービスとしてXMPPベースのSlackを使っている。 RubotyとSlackを繋ぐためのruboty-slackというアダプタがあるので、 このアダプタ内でメッセージを受け取ったときの処理に条件分岐を入れて過去の発言を無視するようにした。

class Ruboty::Adapters::Slack
  def on_message(message)
    unless message.delayed?
      robot.receive(
        body: message.body,
        from: message.from,
        from_name: username_of(message),
        to: message.to,
        type: message.type,
      )
    end
  end
end

message.delayed? の実体は Xrc::Message#delayed? というRuby用のXMPPライブラリ Xrc の中で実装されている。 このメソッドも昨日までは無くて、今回の対応のために雑に追加した。 delay要素があるかどうかしか判断できないけど、他の情報はまだ使うこともないのであるかどうかわかればまあ良いという判断。 これでBOTがSlackに接続したときに送られてくる過去のメッセージは無視されるようになった。

HipChatでの対応

もしHipChatを利用している場合は、HipChatのXMPPサーバの便利機能を使える。 HipChatのサポートサイトのXMPP/Jabber support detailsってところに1行「もし接続してきたアカウントのJabber IDのResourceの名前が"bot"だった場合は過去の履歴を送信しない」 とある (ResourceについてはXMPP界 - ✘╹◡╹✘を参照のこと)。 よってBOT用に"foo bar/bot"のようなJabber IDを設定するだけで特にライブラリ側で対応する必要がなくなる。 Ruboty用のHipChatアダプタ ruboty-hipchatでは、BOTのJabber IDを自動的に"bot"にする対応をしている。

対応の背景

この発言を見てアー対応しないとなーと思って対応した。積極的なユーザがいると良くなっていく。 https://twitter.com/negipo/status/475245731591122945

あと自分もたまにおかしな挙動してBOTが勝手に自分自身をデプロイしたりして日本人形みたいで恐いなあとは思ってた。 ドッグフーディング便利。 https://twitter.com/r7kamura/status/474890626027110401