Nwht0xn1

WikiHubのサービス連携の実装Created on 2016-05-13 by r7kamura

WikiHubで提供している、SlackやWebhookなど外部サービスとの連携機能の実装方法について。

記事が作成されたイベントを通知する例

一番簡単な例が、誰々によって記事が作成されましたという通知メッセージを送るタイプのものなので、これを例に説明する。このイベントは、例えばSlackとの連携では以下のようなメッセージを送ることになる。

2016-05-13 18 16 38

after_create :trigger_article_created_hooks

この処理は、Articleクラスの永続化されていないインスタンスが保存された直後に、callbackを利用して実現することにしている。ActiveRecord::Callbacks などで説明されている通り、ActiveRecordの利用者であればよく知っている機能だと思う。

class Article < ActiveRecord::Base
  after_create :trigger_article_created_hooks
end

この #trigger_article_created_hooks というメソッドの名前は、trigger + article (対象の種類) + created (イベントの動作を表す動詞の過去分詞形) + hooks というパターンに統一していて、他には trigger_article_updated_hooks や trigger_page_destroyed_hooks などの種類がある。イベントの種類は機能の増加に伴って徐々に増えていくので、統一しておくと楽に考えられたり、抽象化できたりして便利になると思う。

community.hooks.on_article_created

Community.has_many :hooks という一対多の関係性があり、更にHookクラスに on_article_created という、このフラグが有効化されているものだけ絞り込むためのscopeが定義されている。そして、取得したそれぞれのレコードのインスタンスに対して Hook#trigger_article_created_hook というメソッドを呼び出している。

class Article < ActiveRecord::Base
  private

  def trigger_article_created_hooks
    community.hooks.on_article_created.each do |hook|
      hook.trigger_article_created_hook(self)
    end
  end
end

flag_shih_tzu

on_article_created などの所謂フラグと呼ばれるようなものの管理は、flag_shih_tzu というGemを利用しながら、Int(11)型のカラムを2進数でビットフラグとして利用して実現している。この手のフラグは高頻度で種類が追加されるので、都度ALTER TABLEするのではなく、この方法を利用することにした。

この辺りの定義は、Hookクラスの以下の表現に現れている:

class Hook < ActiveRecord::Base
  include ::FlagShihTzu

  has_flags(
    1 => :on_page_created,
    2 => :on_page_updated,
    3 => :on_page_destroyed,
    5 => :on_article_created,
    6 => :on_article_updated,
    7 => :on_article_destroyed,
    8 => :on_issue_updated,
    9 => :on_issue_comment_created,
    check_for_column: false,
    flag_query_mode: :bit_operator,
  )
end

hook.trigger_article_created_hook(self)

Hookは抽象クラスで、これを継承した具象クラスが、サービスの種類ごとに存在している。Hookクラスには、以下のようなインターフェースを示すコードが含まれている。

class Hook < ActiveRecord::Base
  # @param article [Article]
  def trigger_article_created_hook(article)
    raise ::NotImplementedError
  end
end

WikiHubでは、以下のような子クラスが存在している。

  • ChatworkHook
  • HipchatHook
  • SlackHook
  • TwitterHook
  • WebHook
  • YoHook

SlackHook#trigger_article_created_hook

今回は利用頻度の高そうなSlackHookを例に挙げて説明しようと思う。SlackHookは、名前の通りHookクラスに対するSlack用の具象クラスである。SlackHookでは、上述した #trigger_article_created_hook の実装は以下のようになっている:

class SlackHook < Hook
  # @note Override
  def trigger_article_created_hook(article)
    post(
      attachments: [
        {
          author_icon: article.user.image_url,
          author_link: generate_user_url(article.user),
          author_name: article.user.name,
          color: COLOR_CODE_GREEN,
          mrkdwn_in: ["text"],
          text: "*created article #{generate_article_link(article)} on #{generate_community_link(article.community)}*\n#{::Slacken.translate(article.rendered_body)}",
        },
      ],
    )
  end
end

サービスによってイベントの表現方法が異なるので、上述したコードでは「Slack連携においてはarticle_createdイベントはこのように処理する」というコードになっている。#post はHTTPクライアントを利用して通信を行うメソッド。Slackにメッセージを送る方法は、Incoming Webhooks | Slack に書かれているので、これを調べるとやり方が分かる。より豪華な表現をしたいのであれば、Message Attachments | Slack というものがあるので、これを調べると色々と修飾できる。

メッセージ送信の非同期化

メッセージの送信は非同期処理にしたいと思うが、サービスの種類によってはそうではない場合もある (かもしれない)。その違いを吸収するために、メッセージ送信の内部実装を非同期に行うかどうかというのは #post の実装次第である、ということにした。実のところ、WikiHubはまだサービス連携を非同期化していない。RedisとWorkerプロセスを動かすためのお金が稼げるようになったら、#post の実装をそう変更しようかなあと考えている。

その他

もし要望があれば、以下の情報も追記するかもしれません。

  • イベントを発火すべきかどうかの条件が存在する場合にどうするか
  • 誰がイベントを発生させたのかがDB上に永続化されていない類のイベントでどう処理するか
  • メッセージに含めるURLのホスト名などを決め打ちにしないようにするにはどうするか
  • Webhookの実装

追記

#post もHookにインターフェースを用意するのだろうか?

というコメントをはてなブックマーク経由で見つけたので、回答しておきます。

「各インターフェース (#trigger_article_created_hook などのメソッドのこと) を実装するのに #post というメソッドを利用するかどうか」は各Hookの子クラス (具象クラス) に委ねられていることなので、そこはインターフェースとして定義しませんでした。例えばイベントの種類ごとに全く別の処理を行うようなサービスが存在するかもしれないので、そこは共通化しない方がいいかなと考えていました。