Slack × GAS × GitHub APIで見逃さないPullRequest

PullRequestの通知をSlack Appで連携している方は多いでしょう
特定のチャンネルに通知していますが、エンジニアが増えて以下のような問題を抱えている人がいました

* 通知が多く、自分に対しての情報を見逃す
＊ 投稿数が多くてちょっとうざったい

結局オリジナルなカスタマイズ通知しないと解決できないけど、「サーバ立てるとかめんどい…」
そんな方向けに**GASとGitHubAPIを組み合わせてうまくSlackへ通知する**カッコいい(?)方法を紹介します

（最近は [Pull Panda](https://pullpanda.com/) などのSlack Appも出てきておりますので必要に応じて使い分けるのが良いと思います）

これでPullRequestに対してのやり取りが効率化されて余計なストレスから解消されることでしょう！

# GitHub APIを叩くための準備

https://github.com/settings/profile から [Developer settings] - [Personal access tokens] へ遷移し
**Generate new token** で新しいTokenを発行します

※ 念の為、Repositoryの閲覧権限とUserの読み込み権限だけつけるようにしましょう

<img width="1006" alt="New_personal_access_token.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66063/234aaf10-3129-bcff-9a5c-783b4081bf1d.png">

発行が終わったらToken文字列を控えておきます


# Slackに通知するための準備
https://slack.com/apps/A0F7XDUAZ--incoming-webhook- から
投稿用のチャンネル指定と投稿用のエンドポイントを取得します

# GASからSlackへ通知してみる
早速、slack通知をGAS（Google App Script）から行ってみましょう

```javascript
function slackWebhookUrl() {
  return PropertiesService.getScriptProperties().getProperty("SLACK_WEBHOOK_URL");
}

function send_message(message, channel, attachments) {
  var url = slackWebhookUrl();
  var payload = {
    channel: channel,
    text: message,
    username: "GitHuby",
    icon_emoji: ":zap:",
    attachments: attachments,
  }
  var option = {
    'method': 'post',
    'payload': JSON.stringify(payload),
    'contentType': 'application/x-www-form-urlencoded; charset=utf-8',
    'muteHttpExceptions': true
  };
  var response = UrlFetchApp.fetch(url, option);
  Logger.log(response);
}

function test() {
  send_message('Testだよん', '{channel idとか}');
}
```

※ 2.で取得したURLはスクリプトプロパティに設定しておきます（コードを公開しないならべた書きでも問題ない）

GAS上から「testメソッド」を叩くと通知が行くと思います

# GASからGitHub APIの結果を受け取る

今回はあえてGraphQLを利用して情報取得を行います
[GraphQL API v4 API](https://developer.github.com/v4/)からどの情報を取得するかはGitHubが提供している[Explorer](https://developer.github.com/v4/explorer/)を利用すると理解しやすいです


```javascript
function githubAccessToken() {
  return PropertiesService.getScriptProperties().getProperty("GITHUB_ACCESS_TOKEN");
}

function fetch_pullreq_data_by_graphql(owner, repository) {
  const graphql_query = 
    '{\
  repository(owner: "' + owner + '", name: "' + repository + '") {\
    name\
    pullRequests(last: 20, states: OPEN) {\
      nodes {\
        title\
        url\
        author {\
          login\
        }\
        reviewRequests(last: 20) {\
          nodes {\
            requestedReviewer {\
              ... on User {\
                login\
              }\
            }\
          }\
        }\
        comments(last: 50) {\
          nodes {\
            author {\
              login\
              avatarUrl(size: 20)\
            }\
            body\
            createdAt\
            updatedAt\
            url\
          }\
        }\
        reviews(last: 50) {\
          nodes {\
            author {\
              login\
            }\
            url\
            comments(last: 50) {\
              nodes {\
                author {\
                  login\
                  avatarUrl(size: 20)\
                }\
                replyTo {\
                  author {\
                    login\
                  }\
                }\
                body\
                createdAt\
                updatedAt\
                url\
              }\
            }\
          }\
        }\
      }\
    }\
  }\
}';

  const option = buildRequestOption(graphql_query);
  return UrlFetchApp.fetch("https://api.github.com/graphql", option);
}

function buildRequestOption(graphql) {
  return {
    method: "post",
    contentType: "application/json",
    headers: {
      Authorization: "bearer " + githubAccessToken(),
    },
    payload: JSON.stringify({ query: graphql }),
  };
}

function test() {
  Logger.log(fetch_pullreq_data_by_graphql('{ユーザ名}', '{対象リポジトリ名}'));
}
```

…カオスですね。。。とりあえずgraphqlなのでしょうがないと割り切りましょう
Loggerにひとまず情報が吐き出されていれば問題ありません

# GitHub APIの情報をSlackにフィルタして通知する

いよいよ本丸です
GraphQL取得した情報をフィルタ・整形してSlackに通知します

```javascript
function notice_relation_comments() {
  if (isHoliday())
    return;

  // ref. https://api.slack.com/methods/channels.list/test
  var target_users = [
    {name: "hogehoge", channel: "{channel_id}"}
  ];

  // 現在日時を取得してメッセージ取得する時間幅を計算する
  var now = new Date();
  var logtime = now.setMinutes(now.getMinutes() - 5);
    
  target_users.forEach(function(target_user) {
    all_repositories().forEach(function(repository_name){
      var json = fetch_repostiroy_json(repository_name);
      var repository = json.data.repository;

      var attachments = [];
      repository.pullRequests.nodes.forEach(function(pullRequest) {
        Array.prototype.push.apply(attachments, build_relation_comments_attachment(repository, pullRequest, logtime, target_user.name));
      });
      if (!attachments.length)
        return;
    
      send_message("", target_user.channel, attachments);
    });
  });
}

function fetch_repostiroy_json(repo) {
  var response = fetch_pullreq_data_by_graphql(repo);
  var json = response.getContentText();
  return JSON.parse(json);
}

function build_relation_comments_attachment(repository, pullRequest, logtime, user_name) {
    result = []
    pullRequest.comments.nodes.forEach(function(comment) {
      if (!is_delivery_target(pullRequest, user_name, comment, logtime))
        return;
      result.push(create_attachment(repository, pullRequest, comment));
    });
  
    pullRequest.reviews.nodes.forEach(function(review) {
      review.comments.nodes.forEach(function(comment) {
        if (!is_delivery_target(pullRequest, user_name, comment, logtime))
          return;
        result.push(create_attachment(repository, pullRequest, comment));
      });
    });
    return result;
}
    
function create_attachment(repository, pullRequest, comment) {
  return {
    "color": "#a8bdff",          
    "author_name": comment.author.login,
    "author_icon": comment.author.avatarUrl,
    "title": "comment link",
    "title_link": comment.url,
    "text": comment.body,
    "footer": "[" + repository.name + "] " + pullRequest.title,
    "footer_icon": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
    "footer_link": pullRequest.url,
    "mrkdwn_in": ["text", "footer"]
  }
}
    
function is_delivery_target(pullRequest, user_name, comment, logtime) {
  // コメントの時間をフィルタして通知する
  if (new Date(comment.createdAt) < logtime)
    return false;

  // コメント記入者なら通知しない
  if (comment.author.login === user_name)
    return false;

  // コメントのリプライ先なら通知する
  if (comment.replyTo && comment.replyTo.author.login === user_name)
    return true;
  
  // プルリクの作成者なら通知
  if (pullRequest.author.login === user_name)
    return true;
    
  // 本文に自分宛てのメンションがあるなら通知
  if (comment.body.indexOf(user_name) !== -1)
    return true;
    
  return false;
}
```

いよいよ手に負えないレベルで煩雑になってきましたね…
ざっくり各メソッドの役割を明記しておきます

`notice_relation_comments`： メイン処理系
`fetch_repostiroy_json`： GraphQLで取得した情報をJSONにパースします
`build_relation_comments_attachment`: フィルタ処理から整形処理をになう処理系
`is_delivery_target`: フィルタ処理、自分に関連あるとされる情報のみを選定する
`create_attachment`： slackにかっこよく出すためにGitHub APIから取得した情報をアタッチメントに整形します

姑息にも**複数ユーザ×複数リポジトリ**に対応しています
`isHoliday()` / `all_repositories()`は未実装ですが、
「休日は通知しないように判定」 と 「通知対象のリポジトリ配列を返す」と読み替えてもらえればいいと思います

※ isHolidayの詳細はリポジトリを見てもらえるといいです https://github.com/kichion/gas-github-notifier/blob/master/holiday.gs

# 定期実行させる

コードはできましたがこれを手動で叩くだけならかっこよくないですよね？
GASはトリガーを設定して特定のスクリプトを定期実行させることが可能です
ref. [【Google Apps Script(GAS)】トリガーを設定してスクリプトを実行する](https://vba-gas.info/gas-trigger)

今回はコードに意味深なマジックナンバーを忍ばせてしまっているので、5分おきに実行させてみましょう

```javascript
  // (中略)
  // 現在日時を取得してメッセージ取得する時間幅を計算する
  var now = new Date();
  var logtime = now.setMinutes(now.getMinutes() - 5);
```

1分おきに実行させたいなら…言うまでもありませんね

# まとめ

これでキレイに通知がされるはずです
![Slack_-_Fukuroulabo-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66063/917cb880-7324-2400-4856-403fbf820680.png)

※ 伏せ字だらけで分かりづらいですがユーザ名やリポジトリ名、プルリクタイトルが出てきます

コード上のフィルタ実装を変えるだけでお好みの通知が実現できるのでぜひやってみてください！！
よいプルリクライフを！！