GASでissueのコメントをslackに通知する

# 成果物

![スクリーンショット 2021-04-14 7.24.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/91d7074b-afc7-6a07-b4bc-43cbbb583841.png)

こんな感じでissueのコメントを通知できる

[GitHub slack-notifier](https://github.com/teach310/slack-notifier)

# 環境
mac
作成時期は 2021/03

# 構成
![スクリーンショット 2021-04-14 7.35.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/c18dfa15-6624-281c-0246-5cff7e3c7819.png)

slack への webhook は Slack App を使った方式で行う。

# 前提
GAS開発経験があること。
GitHub Organization 作っておくこと
- [新しい Organization をゼロから作成](https://docs.github.com/ja/organizations/collaborating-with-groups-in-organizations/creating-a-new-organization-from-scratch)

# 背景
GitHub 公式の Scheduled Reminders はなぜか issueのコメント を通知してくれないのでそこの部分を自分で実装する。

# Slackへの通知の準備

#### 手順

Slack App を作る

https://api.slack.com/apps
![スクリーンショット_2021-04-16_8_11_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/15c47e04-410e-81fa-99aa-cedfae9c30fe.png)

アプリ名等はよしなに入力。

アイコン等の設定とかもこの段階でやってもよい。
![スクリーンショット_2021-04-16_8_15_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/d719ddec-5d56-61f2-566f-4600a0bf8636.png)

できたら、アプリを選択し、webhookのurlを発行する。
![スクリーンショット_2021-04-16_8_17_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/cfdccc6b-5d98-af91-ab76-a40963909026.png)


![スクリーンショット_2021-04-16_8_19_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/7cad287b-0d0e-4bbe-8dd2-195b1354e4c5.png)


![スクリーンショット_2021-04-16_8_19_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/cdb24cc1-f5ac-60eb-450f-50c7cfbc3a06.png)

curlのサンプルをコピーしてそのままterminalにペーストして実行すると以下のように通知が送れる

```
$ curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/それぞれのリンク
ok%
```

![スクリーンショット 2021-04-16 8.27.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/d4246420-7158-cb87-41b9-efff4d2c5d23.png)


# GAS -> Slack
GASのスクリプトは SpreadSheet に紐づけて作る。

■ 送信用コード

slack.js に記載。

```slack.js
const postToSlack = (url, data) => {
  const options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(data)
  }
  const res = UrlFetchApp.fetch(url, options)
  return res.getContentText()
}
```

[UrlFetchApp.fetch](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetch(String,Object))を使う。

ただのテキストを送るだけなら以下のようにdataを作れば良い。
Block Kit を用いたメッセージは後述する。

```slack.js
const postMessage = (url, message) => {
  const data = {
    text: message
  }
  postToSlack(url, data)
}
```

■ 通知対象チャンネルの設定

```main.js
const defaultChannel = 'notifications';

const doPost = (event) => {

  const spreadsheet = SpreadsheetApp.getActive();
  const sheetReader = new SheetReader(spreadsheet);
  
  channels = sheetReader.findAll('channels');
  webhookUrl = channels.find(x => x.name === defaultChannel).webhookUrl;

...

```

![スクリーンショット_2021-05-09_23_03_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/9fb2d2f7-29fe-5b2d-b3e6-060cff764b6a.png)

こんな感じで webhook_url をシート側に書くようにした。
こうしておくことで、デバッグ中は通知用チャンネルではなく、自分にだけ通知がいくようにするって設定するのが楽。

[SheetReader](https://github.com/teach310/slack-notifier/blob/main/sheet.js) は スプレッドシートの中身読み込むためのクラス。
これについての説明は省略する。


# GitHub -> GAS

■ webhookの設定

GitHub からの webhook
今回はプロジェクトではなく、OrganizationのSettings > Webhookから設定 (おそらくプロジェクトでも問題ない)

![スクリーンショット_2021-05-09_23_19_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/741730c9-8e00-ab9b-1f61-480d419d490f.png)

![スクリーンショット_2021-05-09_23_22_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/eb94b97d-a3dd-2edf-06a0-8da8803321a7.png)

Payload URLは最初は適当にいれておき、あとでGASのデプロイで生成されたURLをつっこむ。
**デプロイするたびにURL変わるのでその度にここ差し替えるのがとても面倒**。
(同じURLでデプロイ更新できないか調べたけどわからなかった。)

Content type: json
Seqret: クリックしたらGoogleが自動生成してくれるっていうのでそれでやった。
もしなければuuidgenとかで生成する

![スクリーンショット 2021-05-09 23.23.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/de7e67ad-b8d3-e6ef-0806-4a1ffd52a234.png)

issueのコメントだけで良いので、Issue commentsにチェックをつける。
なお、 **プルリクエストに書いたコメントも Issue comments として扱われる**。意味不明。

また、設定の下の方の `Recent Deliveries` は Payload 見れたり、再送したりできるのでとても便利。

■ 流れ

```main.js
const doPost = (event) => {

...

  webhookUrl = channels.find(x => x.name === defaultChannel).webhookUrl;

  let eventData;
  try {
    eventData = JSON.parse(event.postData.getDataAsString());
  } catch (e) {
    postMessage(webhookUrl, e.toString());
    return;
  }
  
  const githubEvent = getGitHubEvent(eventData);

  try {
    handleGitHubEvent(sheetReader, githubEvent, eventData)
  } catch (e) {
    postMessage(webhookUrl, e.toString());
  }
  return; // リダイレクトしないように何も返さない
}
```

1. 通知用 Slack の WebhookURL を取得
2. GitHubから送られてきたデータをパース
3. 中身のデータの構造から何のイベントなのかを把握
4. イベントに応じてよしなに処理(通知)

#### 中身のデータの構造から何のイベントなのかを把握
GitHub のイベントの種類は ヘッダーから取得可能だが本ケースでこれはできない。理由は2つ。

- Issue comments は issue のコメントとプルリクのコメントの2種類あるのでこれを分けたい。
- **そもそもGASではヘッダーを読み取れない**

Bodyだけでごり押せるので問題はない。

```main.js
// GASではheader取得方法がないのでbodyからeventを推測する
// イベントとPayloadの一覧　https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#issue_comment
const getGitHubEvent = (eventData) => {
  if ('issue' in eventData && 'comment' in eventData) {
    if ('pull_request' in eventData.issue) {
      return 'pull_request_comment';
    }
    return 'issue_comment';
  }
...
  
  return 'unknown_event';
}
```

Issue comments の部分のみ抜粋。
issue のコメントならば 'issue_comment'
プルリクのコメントなら 'pull_request_comment'
を返す。


#### イベントに応じてよしなに処理(通知)

```main.js
const handleGitHubEvent = (sheetReader, githubEvent, eventData) => {
  switch (githubEvent) {
    case 'issue_comment':
      handleIssueComment(sheetReader, eventData);
      return;
    case 'pull_request_comment':
      return; // scheduled_reminderとの通知と被る。GitHub側で通知のOn・Off制御ができないため、GAS側で弾く。
...
  }
  throw 'unknown event'
}

```

あとはうまいこと情報ぶっこぬいて通知

```main.js

const handleIssueComment = (sheetReader, eventData) => {
  const title = `${eventData.issue.number} ${eventData.issue.title}`;
  handleComment(sheetReader, title, eventData.comment);
}

...

const handleComment = (sheetReader, title, comment, titlePrefix = 'Comment on') => {
  const commentBody = comment.body;
  const commentHtmlUrl = comment.html_url;
  const commentUserName = comment.user.login;
  const commentUserIconUrl = comment.user.avatar_url;

  let message = `${titlePrefix} <${commentHtmlUrl}|#${title}>\n` + commentBody;

  const users = sheetReader.findAll('users');

  message = replaceGitHubMentionToSlack(users, message)

  const blocks = createBlocksForComment(commentUserIconUrl, commentUserName, message);
  const url = channels.find(x => x.name === defaultChannel).webhookUrl;
  return postMessageByBlocks(url, blocks);
}
```

どうやってSlackに送るメッセージにURLつけたりするのかは以下のドキュメントを参照
https://api.slack.com/reference/surfaces/formatting

# Block Kit
https://api.slack.com/block-kit
メッセージ装飾できるようにするやつ。

Block Kit Builder 使って試したりできる。
[Block Kit Builder を使ってインタラクティブな Slack アプリをプロトタイピングしよう](https://qiita.com/seratch/items/628751be65de9eb23a80)
ただ、自由度はあんまり高くないので今後に期待。

今回は以下のようにブロックを構築。
いろいろ試してみたものの、シンプルにアイコンとユーザー名つけるだけなのが一番見やすかった。

```slack.js
const postMessageByBlocks = (url, blocks) => {
  const data = {
    blocks: blocks
  }
  postToSlack(url, data)
}

const createBlocksForComment = (user_icon_url, user_name, message) => {
  return [
    {
      type: "context",
      elements: [
        {
          type: "image",
          image_url: user_icon_url,
          alt_text: "images"
        },
        {
          type: "mrkdwn",
          text: `*${user_name}*`
        }
      ]
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `${message}`
      }
    }
  ]
}
```


# GitHubのメンションをSlackのメンションに変換

メンションのためにSlackの各ユーザーのIDが必要
`Hey <@U024BE7LH>, thanks for submitting your report.`
https://api.slack.com/reference/surfaces/formatting#mentioning-users

■ ユーザーIDの把握

メンションのためにSlackの各ユーザーのIDが必要
ユーザーのIDの取得の仕方は色々あるが、出入りが少ないのなら以下が手っ取り早い

![スクリーンショット_2021-04-03_17_54_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/b47b4a01-8c2f-c4fb-5e5b-f0466e765de5.png)

![スクリーンショット_2021-05-10_0_02_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/b119dfd6-c42d-d372-fbcc-8396495c0cf4.png)


Slackにユーザーの情報が入ったcsvのダウンロードリンクが飛んでくる。

■ ユーザーIDの読み込み

![スクリーンショット_2021-05-09_23_03_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/663b9f27-21d7-1c35-a9bd-1b4a9a253e38.png)

設定はシートに記入。

あとはメッセージをreplace

```main.js

const replaceGitHubMentionToSlack = (users, message) => {
  const githubMentions = message.match(/@[a-zA-Z0-9-]+/g);
  if (!githubMentions) {
    return message;
  }
  let temp = message;
  for (const githubMention of githubMentions) {
    const githubName = githubMention.replace('@', '');
    const user = users.find(x => x.githubName === githubName);
    temp = temp.replace(githubMention, `<@${user.slackUserId}>`);
  }
  return temp;
}
```


# まとめ
GASからSlackへの通知はChatAPIを使ったらより便利になるかも。
https://api.slack.com/methods/chat.postMessage

# 参考
[GitHub メンションを Slack DM する機能を Serverless Framework で作った話](https://tech.medpeer.co.jp/entry/2019/02/28/123000)