Stackoverflowでコメントがあればメールで通知したい

# 概要
Stackoverflowでコメントを頂いたとき、メールでの通知ができればもう少し早く対応することができ、質問者や回答者にとって便利になるだろうと考えていました。既存の機能を使ってメール通知を行うと3時間毎にメールが届くようですが、これも届いたり届かなかったりといった問題があるようです。あとはブラウザでログインして、そのままの状態で待機するなど。これをもう少しカスタマイズしつつ、新着コメントのチェック間隔を短くしたいと思い、Stack Exchange APIと時間トリガーでスクリプトの起動が可能なGoogle Apps Scriptを使ってこれを実現するスクリプトを作成しました。スクリプトのリポジトリは[こちら](https://github.com/tanaikech/Notifying-Comments-at-Stackoverflow-by-Email)です。

# スクリプトの特徴
1. [Stack Exchange API](https://api.stackexchange.com/) v2.2を使ってInboxを取得します。InboxにはStackoverflowをはじめ、PhysicsやWeb Applicationsなど、全てのコミュニティからのコメントが含まれます。この[Inboxから未読アイテムを取得](https://api.stackexchange.com/docs/user-unread-inbox)し、新着コメントがあればメールで通知します。
1. 新着コメントとは、スクリプトが前回動作した時刻から今動作している時刻までの間に新たに追加されたコメントを意味します。このため、コメントが未読ままであっても重複して取得することはありません。
1. スクリプトを時間トリガーとして設定することで、定期的に新着コメントをチェックすることができます。

# 前準備1
スクリプトを実行する前準備として、Stack Exchange APIを使用するためのアクセストークンを取得します。取得方法は次の通りです。

1. stackapps.comでアプリケーションを登録する。[ここ](https://stackapps.com/apps/oauth/register)をクリックするとログイン画面が現れます。ログインしてからアプリケーションを登録してください。
    - "Application Name", "Description", "OAuth Domain", "Application Website"へ入力してください。
    - 私はサンプルとして、それぞれを"sampleApp", "This is a sample app.", "localhost", "sample"と入力して登録しました。"OAuth Domain"は今の場合"localhost"として登録してください。これはredirect uriで使用します。
    - 登録した先に表示されている"Client Id", "Client Secret", "Key"をコピーします。

1. スコープを承認する。
    - 下記のテンプレートURLに対して``client_id=#####``の``#####``へ先ほどコピーした値を置換します。
    - ``https://stackexchange.com/oauth?client_id=#####&scope=no_expiry%20read_inbox&redirect_uri=http://localhost``
    - ブラウザを起動して作成したURLへアクセスします。すると、下記のような内容が現れますので、"Approve"ボタンを押して承認します。もしもログアウトしている場合は、再度ログイン画面が現れますのでログインしてください。
    - ![fig1.png](https://qiita-image-store.s3.amazonaws.com/0/133383/ac053eee-3b32-4542-5ba3-a0855346cea6.png)
    - 承認後、ページが切り替わったところで、ブラウザのURLにある``http://localhost/?code=### code ###``の``### code ###``をコピーします。このコードを使ってアクセストークンを取得します。
    - アクセストークンの取得は、下記のようなcurlコマンドを使って行います。ターミナルあるいはdosプロンプトを起動して下記コマンドを入力してください。今回の設定では"no_expiry"をスコープに含めているためアクセストークンの使用期限はありません。取得したアクセストークンを使用し続けることが可能です。

~~~bash
curl \
    -d "client_id=#####" \
    -d "client_secret=#####" \
    -d "code=#####" \
    -d "redirect_uri=http://localhost" \
    "https://stackexchange.com/oauth/access_token"
~~~

# 前準備2
次に準備2としてStack Exchange APIを使ってInboxをチェックし、新着コメントをメールで通知するための下記のGoolge Apps Scriptをスクリプトエディタへコピーペーストし、一定時間間隔で起動するように設定します。

- Googleへログイン後、[script.google.com](https://script.google.com/)へ移動し、左側にある「新規スクリプト」をクリックすると新規スタンドアロンプロジェクトとしてスクリプトエディタが起動しますので、タイトル（ファイル名）を入力して下記スクリプトをコピーペーストし、保存してください。
- スクリプトを実行する前に``run()``の``key, accessToken, mail``を自身のものと置き換えてください。mailはメール通知するための宛先を意味します。
- 上記の準備後、一度手動で``run()``を実行して動作を確認します。このとき、スクリプトで使用するスコープの承認を行う必要がありますので、許可してください。
- スクリプトが実行することを確認した後、[時間トリガー設定](https://developers.google.com/apps-script/guides/triggers/installable#managing_triggers_manually)で``run()``をトリガー起動することで定期的にInboxをチェックすることができるようになります。トリガー設定の方法は次の通りです。
    1. スクリプトエディタ上で「編集」 -> 「現在のプロジェクトトリガー」を開きます。
    1. 「今すぐ追加するにはここをクリックしてください。」をクリックします。
    1. 実行は「run」を選択します。
    1. イベントは、「時間主導型」、「時タイマーあるいは分タイマー」を選択して、チェックする間隔の時間を入力し、保存ボタンを押します。

これで準備は完了です。あとはログアウトしても問題ありません。設定した時間間隔でrun()が実行され、Inboxをチェックし、新着コメントがあればメールで通知してくれます。

# スクリプト
~~~javascript
function main(account) {
  var dateStr = function(d) {return Utilities.formatDate(new Date(d * 1000), Session.getScriptTimeZone(), 'yyyy/MM/dd HH:mm:ss')};
  var thisTime = Math.floor(new Date().getTime() / 1000);
  var url = "https://api.stackexchange.com/2.2/inbox/unread?filter=withbody&pagesize=100&access_token=" + account.accessToken + "&key=" + account.key;
  var comments = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
  if (comments.getResponseCode() == 200) {
    comments = JSON.parse(comments.getContentText());
    var itemsLen = comments.items.length;
    if (itemsLen > 0) {
      var prop = PropertiesService.getScriptProperties();
      var lastTime = prop.getProperty("time");
      lastTime = parseInt((lastTime == null ? 0 : lastTime), 10);
      var newItems = comments.items.filter(function(e) {return e.creation_date > lastTime});
      var len = newItems.length;
      if (len > 0) {
        var header = "Comments got from " + dateStr(lastTime) + " to " + dateStr(thisTime) + ".\r\n\r\n";
        var body = newItems.reduce(function(s, e, i) {
          return s +
            "- Comment " + (i + 1) + "/" + len + "\r\n" +
            "date: " + dateStr(e.creation_date) + "\r\n" +
            "title: " + e.title + "\r\n" +
            "comment: " + e.body + "\r\n" +
            "link: " + e.link + "\r\n" +
            "Site: " + e.site.api_site_parameter + "\r\n\r\n";
        }, "");
        var footer = "quota_remaining: " + comments.quota_remaining + "/" + comments.quota_max + "\r\n";
        MailApp.sendEmail({to: account.mail, subject: "SoReport: Got " + len + (len == 1 ? " comment." : " comments."), body: header + body + footer});
      }
      prop.setProperty("time", thisTime.toString());
    }
  }
}

function run() {
  // Please modify this object to yours.
  var account = {
    key: "#####",
    accessToken: "#####",
    mail: "#####",
  };
  main(account);
}
~~~

# ポイント
- 新着コメントのチェックは、時間トリガーにより起動した際に、前回起動した時間から今回起動した時間までに追加されたコメントを取得してメールで通知します。
    - 時間トリガーで起動することを前提にしていますので、一度のコールで取得するコメント数は100に設定しています。もしも設定した時間間隔の間に100以上のコメントがある場合は、時間間隔を小さくするか、[エンドポイント](https://api.stackexchange.com/docs/user-unread-inbox)にあるクエリパラメータのpagesizeを変更してください。
- メールボディはテキストにしています。もしもHTMLとして必要な場合は、スクリプトを変更してください。
- メール通知の際、APIの残量も通知されますので、その際に使用頻度とトリガーの時間間隔設定を調整することもできます。
- クエリパラメータへfilter=withbodyを使うことでコメントの中身も取得しています。コメントが長いと途中で切られてしまうのは仕様のようです。