gitbucketからslackへ通知する

## はじめに

職場では、社内サーバーにgitbucketを立てており、slackによりメンバー間のコミュニケーションを取っています。
以下のような流れでレビューを実施しているのですが、「プルリクエストへのコメントにmentionがつかない」ので、
誰がプルリクエストへコメントしたかどうかが判断しにくいと、チームから意見がでました。

そこでrustを使用して、プルリクエストへコメントをmention付きでslackへ通知するプログラムを作成しました。

1. プルリクエストを作成
2. コードレビューを実施し、gitbucketでコメントを記載
3. slackへ2で記載したコメントを通知（[slack-github連携](https://github.com/takezoe/gitbucket/wiki/Setup-Slack-Integration)）
    - **ここでコメントは指定したチャンネルに通知されるが、mentionがつかない！**

## インストール

Macではhomebrewでインストールします。
```
brew install rust
```

## Slack Token生成
rustプログラムからslackへ通知できるよう、アプリケーションを作成し、tokenを生成します。

> slack app（チーム名の下矢印クリック） → Customize Slack → API（左サイドメニュー）→ Your Apps（右上メニュー）

Legacy Tokenを使用するので、[ここ](https://api.slack.com/custom-integrations/legacy-tokens)から取得。

## docker環境構築
Docker上で動作させる為、Dockerfileを作成します。

```
FROM ubuntu:16.04

RUN apt-get -y update
RUN apt-get install -y vim curl less
RUN apt-get install -y build-essential
RUN apt-get install -y libtool
RUN apt-get install -y zlib1g-dev
RUN apt-get install -y openssl libssl-dev libcrypto++-dev
RUN apt-get install -y sudo
RUN apt-get install -y pkg-config
RUN curl -sSf https://static.rust-lang.org/rustup.sh | sh

RUN mkdir -p /var/slack
WORKDIR /var/slack
ADD ./source/Cargo.toml /var/slack
ADD ./source/Cargo.lock /var/slack
ADD ./source/src/ /var/slack/src
ADD ./source/target/ /var/slack/target
RUN cargo build --release

CMD cargo run --release
```

## ironサーバー構築
gitbucketからの通知を受け付けるために、[ironサーバー](https://github.com/iron/iron)を構築します。
Cargoを使用して、新しいプロジェクトを作成します([参考：cargoの使い方](http://keens.github.io/blog/2015/11/29/cargonotsukaikata/))。

```
cargo new notification
```
Cargo.tomlを修正し、iron等必要なcrateをインストールします。

```
[package]
name = "slack"
version = "0.1.0"
authors = ["developer"]

[dependencies]
iron = "*"
params = "*"
slack_api = "0.17.0"
reqwest = "*"
```

| crate名   | 説明                                                 | 参考                                     |
|:----------|:-----------------------------------------------------|:-----------------------------------------|
| params    | リクエストパラメータをパースするためのironプラグイン | https://github.com/iron/params           |
| slack_api | Slackへ通知を送るcrate                               | https://github.com/slack-rs/slack-rs-api |
| reqwest   | HTTPクライアント(?)                                  | https://github.com/seanmonstar/reqwest   |

## Slack通知処理
rustからSlackへ通知します。
流れ的には、

1. gitbucketのプルリクエストへコメント記載
2. gitbucketからslackへWebohook機能を使用して、ironサーバーへコメント情報をJSONで通知
3. ironサーバーでJSONをパースし、特定チャンネルへmention付きでコメント通知


以下、コードになります。
gitbucketユーザーとslackユーザーが異なるので、紐付けるため「create_table」でテーブルを作成しています。
各種tokenやユーザー名/チャンネル名は、任意のものを指定してください。

```rust:main.rs
extern crate iron;
extern crate params;
extern crate slack_api as slack;
extern crate reqwest;

use iron::prelude::*;
use iron::status;
use std::collections::HashMap;
use std::default::Default;

static TOKEN: &'static str = [slack legacy token];

// git/slackアカウント構造体.
struct Account {
    git_name: String,  // gitbucket上のユーザー名.
    slack_name: String // slackユーザー名.
}

impl Account {
    fn new(git: &str, slack: &str) -> Account {
        Account{ git_name: git.to_string(), slack_name: slack.to_string() }
    }
}

// slack-gitテーブル作成.
fn create_table() -> HashMap<i32, Account> {
    // gitbucketユーザーからslackユーザーを検索できるよう、ここでテーブルを作成しています。
    let mut git_slack_hash = HashMap::<i32, Account>::new();
    git_slack_hash.insert(1, Account::new("gitbucket_test", "slack_test"));
    git_slack_hash
}

// search slack user info.
fn get_slack_userid(_u: &Account) -> Result<String, String> {
    let client = reqwest::Client::new().unwrap();
    let request = slack::rtm::StartRequest::default();
    let response = slack::rtm::start(&client, TOKEN, &request);
    if let Ok(response) = response {
        if let Some(users) = response.users {
            // search user id.
            let account = users.iter()
                               .filter(|u| u.name.clone().unwrap() == _u.slack_name)
                               .next()
                               .unwrap();
            return Ok(account.clone().id.unwrap());
        }
    }
    Err("Failed".to_string())
}

// send message to slack.
fn send_to_slack(id: &str, mention: &str, msg: &str) -> Result<String, String> {
    // send to slack.
    let req_info = slack::chat::PostMessageRequest{
        channel: [チャンネル名],
        text: &format!("<@{}|{}>\n{}", id, mention, msg),
        username: Some([ユーザー名]),
        parse: None,
        link_names: None,
        attachments: None,
        unfurl_links: None,
        as_user: None,
        icon_emoji: None,
        icon_url: None,
        reply_broadcast: None,
        thread_ts: None,
        unfurl_media: None
    };
    let client = reqwest::Client::new().unwrap();
    match slack::chat::post_message(&client, TOKEN, &req_info) {
        Ok(_) => return Ok("Send Success".to_string()),
        Err(_) => return Err("Send Error".to_string())
    };
}

// main function.
fn main() {
    //-------------------------------.
    // slackへ通知を流す.
    //-------------------------------.
    fn notification(req: &mut Request) -> IronResult<Response> {
        // slack hash table from gitbucket account.
        let table = create_table();
        use params::{Params, Value};
        let data = req.get_ref::<Params>().unwrap();

        // gitユーザー名取得.
        let _name = match data.find(&["issue", "user", "login"]) {
            Some(&Value::String(ref name)) => name,
            _ => ""
        };
        // gitユーザ名からslackユーザー名検索.
        let user = table.iter()
                        .filter(|&(_, val)| &val.git_name == _name)
                        .next()
                        .unwrap();

        // コメント取得.
        let msg = match data.find(&["comment", "body"]) {
            Some(&Value::String(ref body)) => body,
            _ => ""
        };

        // send to slack.
        if let Ok(id) = get_slack_userid(&user.1) {
            match send_to_slack(&id, &user.1.slack_name, &msg) {
                Ok(_) => return Ok(Response::with((status::Ok, "Success"))),
                Err(_) => return Ok(Response::with((status::Ok, "Success")))
            };
        }
        else {
            return Ok(Response::with((status::Ok, "Success")))
        }
    }
    Iron::new(notification).http("0.0.0.0:8080").unwrap();
}
```

## ironサーバー起動
`docker build && docker up`にて、ironサーバーを起動します。
また、gitbucketサーバーからironサーバーへ通知がいくように設定を行います。
（gitbucketのwebhook設定は割愛します）

## まとめ
ひとまずはgitbucketから受け取った情報をrustにてパースし、slackへ通知することができました。
ただし、`unwrap`で直接取り出していたりとまだまだレベルが低いので、[エラーハンドリング](https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/error-handling.html)をもう少し勉強したいと思います。

## 課題

このプログラムで、githubからslackへmention付きでコメントが通知されるようになりましたが、プルリクエストを作成した人がコメントすると、その人自身にmentionがついてしまいます。
例えば、Aさんがプルリクエストを作成しコメントを行うと、Aさん自身にmentionが付き、コメントが記載されます。
このあたりを、例えば「プルリクエストを許可する人」対してmentionがつくよう、改良していきたいと思います（できるのかな？）。