追記(2016.06.02)
Qiita:Teamにグループ機能が追加されて、グループごとにSlackの通知先を指定できるようになりました。
\ワーイ/
ただし、複数のチャンネルに通知を飛ばすことはできないようなので(これは需要があまりない気もしますが…)、もう少し自前の通知で頑張ろうと思います。
ユーザー毎に通知先のチャンネルを指定したい
Qiita:TeamはSlackとの連携を提供していますが、投稿が増えてきたり使う人数が増えてきたりしてくると、単一のSlackチャンネルにしか通知できないことで様々な問題が発生してきます。(大げさ)
たとえば開発チーム以外のメンバーもQiita:Teamを使っているような場合だと、通知先に#devや#marketingようなチームごとのチャンネルを指定するわけにいかず、#generalやQiita:Team用のチャンネルを用意して通知したりしているところもあると思いますが、だいたい誰も見なくなって死チャンネルと化していきます。
せっかくSlack使っているんだしなんとかうまいこと通知したいのですが、公式の連携ではどうやってもみんなが幸せになる道がなかったので、Qiita:TeamのWebhookを使ってQiita:TeamからにSlackへの通知をユーザー毎にチャンネルを指定して行えるように簡単なライブラリを作成しました。
やること
Qiita:Team -- (webhook) --> Heroku -- (SlackAPI) --> Slack
上記のように、
- Qiita:Teamのwebhookで適当に用意したHerokuのサーバーに通知
- Herokuでいい感じに情報を処理(もちろんHerokuじゃなくてもいいです)
- 投稿者などの情報に応じていい感じのチャンネルを選んでAPIでSlackに通知
をしてやるだけです。
準備
Qiita:TeamにWebhookの設定をする
Qiita:Teamの設定から通知先のURLを指定します。
自分はHerokuにQiita:Teamからの通知受取用のサーバーをたてましたが、ご自身の環境等々に合わせて適当なものを用意していただき、そちらを指定してください。
SlackのWebhook URLを取得する
Slackの「Custom Integrations」から「Incoming WebHooks」を追加して、Slackへの通知用にURLを取得します。
チャンネルも指定が必要ですが、適当なところで大丈夫です。
通知処理を書く
※全てのコードを乗せるとかなり長くなってしまったので、下記にライブラリとしてアップしました。
詳しくはこちらをご覧ください。
Slack通知用のクラスを作成
obiyuta/qiita-team-to-slack/lib/Slack.php
特に変わったことはしていないので特に説明するようなところもありませんのでコード自体は乗せませんが、上で取得したWebhook URLに対して下記のようにSlackAPIでしてできるパラメータを一つ一つ指定していけるようになっています。
$slack = new Slack('https://hooks.slack.com/...')
slack->username('Qiita:team')
->attachments($attachments)
->iconUrl('http://xxxxxxxxx/image.jpg');
->send()
Qiita:Teamからの情報をいい感じにするクラスを作成(モデルっぽく扱えるように)
Qiita:Teamからの情報をSlackに通知できるように、SlackのAPIフォーマットへの変換etcを行ってくれるモデル風のクラスを作成します。
最終的にQiita:Teamからの情報をSlack APIのattachments
として送信するためにgetItem()
, getComment()
で投稿情報、コメント情報をSlack用のフォーマットに変換してくれるようになっています。
初期化する際に謎の$branches
がでてきていますが、下記のようにSlackのチャンネルとそのチャンネルに通知するユーザーを連想配列で指定したものをセットすることで、複数チャンネルにユーザーを指定して通知することができます。
具体的にはgetChannelByCreater()
の部分でどのチャンネルに通知すべきか取得できるようになっており、デフォルトでは#generalが指定されます。現在はコメントがついた場合の通知については、記事の作成者のチャンネルに投稿されるようになっています。
$branches = array(
'#dev' => array(
'obi_yuta'
),
'#design' => array(
'hogehoge',
'obi_yuta'
),
);
class QiitaTeam {
private $baseUrl;
private $data;
private $item;
private $comment;
private $project;
private $branches;
private $model;
private $action;
public function __construct($branches, $baseUrl) {
if (empty($branches) || empty($baseUrl)) {
throw new Exception("Failed to initialize class QiitaTeam");
} else {
$this->branches = $branches;
$this->baseUrl = $baseUrl;
}
return $this;
}
public function set(stdClass $data) {
$this->data = $data;
$this->model = $data->model;
$this->action = $data->action;
return $this;
}
public function getItem() {
$this->item = new stdClass;
$this->item->attachments = $this->getItemAttachments();
return $this->item;
}
public function getComment() {
$this->comment = new stdClass;
$this->comment->attachments = $this->getCommentAttachments();
return $this->comment;
}
public function getModel() {
return $this->data->model;
}
public function getAction() {
return $this->data->action;
}
public function getChannelByCreater() {
$username = $this->data->item->user->url_name;
$branches = $this->branches;
$belongs = array();
foreach ($branches as $channel => $members) {
if (in_array($username, $members)) {
$belongs[] = $channel;
}
}
if (empty($belongs)) {
$belongs[] = '#general';
}
return $belongs;
}
private function getItemAttachments() {
$attachments = (object)array(
'attachments' => array(
'color' => '#1887d0',
'pretext' => '<'.$this->baseUrl.$this->data->user->url_name.'|'.$this->data->user->url_name.'> created a new post',
'title' => $this->data->item->title,
'title_link' => $this->data->item->url,
'text' => $this->data->item->raw_body,
"mrkdwn_in"=> array("text", "pretext")
)
);
return $attachments;
}
private function getCommentAttachments() {
$pretext = 'New comment on <';
$pretext.= $this->baseUrl;
$pretext.= $this->data->item->user->url_name.'|';
$pretext.= $this->data->item->user->url_name.'>\'s <';
$pretext.= $this->data->item->url.'|';
$pretext.= $this->data->item->title.'>';
$title = 'Commented by '.$this->data->comment->user->url_name;
$attachments = (object)array(
'attachments' => array(
'color' => '#b1ddfb',
'pretext' => $pretext,
'title' => $title,
'text' => $this->data->comment->raw_body,
"mrkdwn_in"=> array("text", "pretext"),
)
);
return $attachments;
}
}
Qiita:Teamからの情報をいい感じにSlackに通知するクラスを作成
上記2クラスをいい感じに操作してQiita:Team -> Slackに通知をしてくれるQiitaTeamSlackクラスを作成します。(冒頭で読み込んでいるsettings.php
にはメンバーの設定などがしてあります:後述)
投稿が新規作成された時と、コメントが新規作成されたときぐらいしか通知せんでいいだろうということで、その他のイベントについては無視されるようになっています。(不自由はしていないです)
require 'config/setting.php';
require 'lib/Slack.php';
require 'lib/QiitaTeam.php';
class QiitaTeamSlack {
private $payload;
private $slack;
private $qiitaTeam;
public function __construct($payload) {
if (empty($payload)) {
throw new Exception("Failed to initialize class QiitaTeamSlack");
} else {
$this->payload = $payload;
}
$this->setUp();
return $this;
}
public function send() {
$model = $this->qiitaTeam->getModel();
$action = $this->qiitaTeam->getAction();
$this->slack->username('Qiita:team')
->iconUrl('http://s18.postimg.org/569m8zoc5/qiita_team.jpg');
switch ($model) {
case 'item':
if ($action == 'created') {
$item = $this->qiitaTeam->getItem();
$attachments = $item->attachments;
}
break;
case 'comment':
if ($action == 'created') {
$comment = $qiitaTeam->getComment();
$attachments = $comment->attachments;
}
break;
default:
break;
}
if (!empty($attachments)) {
$this->slack->attachments($attachments);
foreach($this->qiitaTeam->getChannelByCreater() as $channel) {
$this->slack->channel($channel)
->send();
}
}
}
private function setUp() {
try {
$this->slack = new Slack(SLACK_WEBHOOK_URL);
$this->qiitaTeam = new QiitaTeam(json_decode(QIITA_MEMBERS, true), QIITA_BASE_URL);
} catch (Exception $e) {
throw new Exception($e->getMessage() . " in class QiitaTeamSlack");
}
$this->qiitaTeam->set($this->payload);
}
}
最後にsetting.php
に以下3つの情報を定義して完成です。
- チャンネルとメンバーの設定
- Qiita:TeamのチームURL
- SlackのWebhook URL
こんな感じ。
define('QIITA_BASE_URL', 'http://YOUR_QIITA_TEAM_URL');
define('SLACK_WEBHOOK_URL', 'http://YOUR_SLACK_WEBHOOK_URL');
define('SLACK_DEV_CHANNEL', '#develop');
define('SLACK_DESIGN_CHANNEL', '#design');
$members = array(
SLACK_DEV_CHANNEL => array(
'hogehoge'
),
SLACK_DESIGN_CHANNEL => array(
'fugafuga'
),
);
つかいかた
実際に使う際は、通知されてきたpayloadをデコードして、QiitaTeamSlackを初期化し、通知処理を行うだけです。
require 'QiitaTeamSlack.php';
$payload = json_decode(file_get_contents('php://input'));
try {
$qiitaTeamSlack = new QiitaTeamSlack($data);
} catch (Exception $e) {
echo $e->getMessage();
}
$qiitaTeamSlack->send();
実際の設定ではQiita:Team以外の通知も同じサーバーで受けられるようにしたいなーと思い、/qiita_teamn
というURLをQiita:Teamの通知先として指定していますので、
$method = strtoupper($_SERVER['REQUEST_METHOD']);
$requestUri = explode('/', $_SERVER['REQUEST_URI']);
$service = $requestUri[1];
if ($service == 'qiita_team' && $method == 'POST') {
// ↑のような通知処理
}
こんな感じでパスに応じて雑に処理を分けるようにしています。
まとめ
コード自体はパッとまとめたのでエラーの処理始末ちゃんとしてなかったり、全体的に雑な感じになってしまっていて恥ずかしい限りなのですが(お気づきのことあればコメントやプルリク等で突っ込んで頂けますと…。)、メンバーによって通知先をわけたことで、関係ないところに通知されちゃうな〜といった遠慮がなくなりコメントしやすくなったり、適度に必要な情報がながれてくるのでDEATHチャンネル現象が解消されたりして、結構よかったかなーと思います。
Qiita:Teamからの通知、死んじゃってるよという方はぜひ試してみてください。