Webhook GitHub→*GAS*→Discord GASでGitHubの任意のファイルの監視がしたい

# 動機
DiscordのGitHub向けデフォWebhookが使い物にならなかったのでどうにかしたかった
具体的には特定のファイルを含んだコミットを検出したかった

# Webhook is 何
初めて触るのでざっと調べた

正体はただのPOSTリクエスト
JSON等のファイルをPOSTで送るだけ
普段ｵﾗｯﾃﾞｰﾀﾖｺｾｯ!っていうところで急にチョコを送り付けるような雰囲気
チョコのことは一般的にpayloadと呼ぶらしい

POSTリクエストを受け付けられて送りつけることができる鯖が欲しい

……それGASでよくね？

# つくる
## Discord
チャンネルの設定画面からWebhookを受けるURLを作る
ここにJSONでPOSTリクエストを投げるとチャンネルにメッセージが流れる
## GitHub
リポジトリ→Setting→Webhook
から新しくWebhookを作る

Discordと繋げる場合通常ならコピーしたURL末尾に`/github`を付け加えて送信形式をJSONにする
一度試してみるのもあり
今回はGASを挟むので送信先のURLはあとでGASのものに書き換える

送信イベントはcustomにしておいた方が良い
GAS側でフィルターをかけてもよいが非常に面倒だったうえに
無料プランでの呼び出し制限(doPostに影響するかは分からないが)
をむやみに消費しそうで嫌だったので私はやめた

## GAS
### GitHub→GAS
GASのdoPostを使う
どうやらJSONを受け取ったときは`e.parameter`ではダメな模様

https://qiita.com/shirakiya/items/db22de49f00710478cfc

これでJSONをオブジェクトで扱えるようになった
ここから欲しい情報を取り出して整形していく

```js
function doPost(e){
  e=JSON.parse(e.postData.getDataAsString());
  main(e);
}
```
本来ならreturnをするところなのだがreturnするとGitHub側のWebhookがリダイレクトに対応していない影響でGAS側の処理は成功しているにもかかわらず302エラーを吐く
(GASはセキュリティ上の理由でレスポンスする際にリダイレクトを挟むらしい)
別にGitHubがエラーを吐こうと全く問題はないのだがログにひたすら赤文字が並ぶのも嫌なのであえてreturnしない

### GAS→Discord
DiscordにPOSTでJSONを投げる
`UrlFetchApp.fetch()`を使う
使い方は普通のfetchとほぼ同じ
[doc](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app.html#advanced-parameters)
```js
function send(x){
  UrlFetchApp.fetch('Webhook送信先URL',{
    contentType:'application/json',
    method:'post',
    payload:JSON.stringify(x)
  });
}
```

### 整形

GitHubが投げるJSON↓

https://docs.github.com/ja/developers/webhooks-and-events/webhooks/webhook-events-and-payloads


GitHubのWebhook設定画面からログが見れるのでそれも使うと良い

Discordが受けるJSON↓

https://discord.com/developers/docs/resources/webhook#execute-webhook

上のリファレンスをよく読んでオプションであるかどうかをよく判断してから実装しないと`{"embed":["0"]}`エラーが返ってくるので注意
特に`fields[][value]`は空文字もnull判定されるので注意

```js
function main(x){
  const keys=Object.keys(x),//キーからイベントを判断する
    type=//今回はissues, issue_comment, push のみを扱う
      keys.includes('head_commit')?
        'push':
        keys.includes('issue')?
          keys.includes('comment')?
            'issue_comment':
            'issues':
          '';
  send({
    username:'McbeEringi',
    avatar_url:'https://mcbeeringi.github.io/img/icon.png',
    embeds:[{
      color:parseInt('221144',16),//10進数にする必要がある
      title:'NULL',
      author:{name:x.sender?.login,url:x.sender?.url,icon_url:x.sender?.avatar_url},
      footer:{text:x.repository?.full_name,icon_url:x.repository?.owner?.avatar_url},
      ...(({
        push:()=>{
          const diff=x.commits.reduce(
              (a,y)=>{['added','removed','modified'].forEach((z,i)=>a[i].value=[...a[i].value,...y[z]]);return a;},
              ['追加','削除','変更'].map(y=>({name:y,value:[]}))
            );
          if(diff[2].value.includes('skysw.js'))//特定のファイルの更新のキャッチもできる
            send({
              username:'McbeEringi',
              avatar_url:'https://mcbeeringi.github.io/img/icon.png',
              content:'変更内容にアップデートが検出されました\n次々回起動時に自動で更新が入ります'
            });
          return{
            color:parseInt('4488ff',16),
            title:'ぷっしゅ!',
            description:'新しい変更が加えられました',
            url:x.compare,
            fields:diff.filter(y=>y.value.length).map(y=>Object.assign(y,{value:'>>> '+y.value.join('\n'),inline:true}))
          };
        },
        issues:()=>({
          color:parseInt('ff4444',16),
          title:`#${x.issue.number} "${x.issue.title}"`,
          description:`**バグ報告 ${x.action}**`,
          url:x.issue.url,
          fields:[
            {name:'内容',value:`>>> ** ${x.issue.title} **\n${x.issue.body}`},
            ...(x.changes?[{name:'変更前',value:`>>> ** ${x.changes.title.from} **\n${x.changes.body.from}`}]:[]),
            {name:'ラベル',value:'>>> '+(x.issue.labels.map(y=>y.name).join('\n')||'*なし*')},
          ]
        }),
        issue_comment:()=>({
          color:parseInt('ff4444',16),
          title:`#${x.issue.number} "${x.issue.title}"`,
          description:`**バグ報告 コメント ${x.action}**`,
          url:x.comment.url,
          fields:[
            {name:'内容',value:`>>> ${x.comment.body}`},
            ...(x.changes?[{name:'変更前',value:`>>> ${x.changes.body.from}`}]:[])
          ]
        })
      })[type]||Object)()
    }]
  });
}
```

### デバッグし辛い!
そんなあなたに
```js
const sample={//GitHubのWebhookのログで手に入れたpayloadデータ
  action:'edited',
  repository:…
  …
};
function debug(){doPost({postData:Utilities.newBlob(JSON.stringify(sample))});}
```
Blob作って投げればデバック可能

### 公開してGitHubにつなげる
右上の青い`デプロイ`→`新しいデプロイ`→`種類の選択 ウェブアプリ`→`自分として実行 全員がアクセス可能`→`デプロイ`→`ウェブアプリのurl`でdoPostにつながるURLを作れる
これをGitHubのWebhook設定に貼り付けに行く

以後更新する際は`デプロイ`→`デプロイを管理`→`✏`→`新しいバージョン`→`デプロイ`の手順を踏まないと毎回URLが変わって大変なことになるので注意

# つくった
一応これで動く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479627/28792ca5-34f9-852e-735e-3802ff6bbf75.png)

特定のファイルやそれ以外のイベントの監視などそこそこ使えるので重宝することになりそう
少なくとも素でGitHubに繋げるよりかははるかに便利

よきDiscordライフを