詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

GitHub の通知メールで、クローズやマージされたものにラベルをつける

Google Apps Script でメールの自動アーカイブの続編です。

https://github.com/motemen/gas-gmail-scripts/blob/master/dist/label-github-issue-status.js

GitHub のリポジトリにやってくるイシューや Pull Request って、対応する心的コストがばかにならないので放置しがちなんだけど、すでにクローズとかマージされたものに関しては気楽に流し読みしていいはず。そういうシグナルで自分を解き放っていきます。

Gmail 本体のフィルタルールでもできそうなものだけど、"Closed" を含む〜みたいなルールだとわりと誤爆するので、GAS で書く。

こんな感じにラベルがつけられて、便利ですね。

f:id:motemen:20181211200302p:plain

メールの本文を見て処理を行うだけなら簡単なんだけど、"Closed #42." みたいなのを勝手に書いた場合と区別がつかないので、(現実的にはないことだろうとはいえ)ちょっと気持ち悪い。というか、メタデータ的なものがあってしかるべきって感もある。

そこで GitHub から届いたメールを見てみると、どうもメタデータらしきものが text/html パートに埋め込まれている様子……。

<script type="application/json" data-scope="inboxmarkup">{"api_version":"1.0","publisher":{"api_key":"********************************","name":"GitHub"},"entity":{"external_key":"github/motemen/test-repository","title":"motemen/test-repository","subtitle":"GitHub repository","main_image_url":"https://github.githubassets.com/images/email/message_cards/header.png","avatar_image_url":"https://github.githubassets.com/images/email/message_cards/avatar.png","action":{"name":"Open in GitHub","url":"https://github.com/motemen/test-repository"}},"updates":{"snippets":[{"icon":"DESCRIPTION","message":"Closed #34."}],"action":{"name":"View Issue","url":"https://github.com/motemen/test-repository/issues/34#event-2043751833"}}}</script>
{
  "api_version": "1.0",
  "publisher": {
    "api_key": "********************************",
    "name": "GitHub"
  },
  "entity": {
    "external_key": "github/motemen/test-repository",
    "title": "motemen/test-repository",
    "subtitle": "GitHub repository",
    "main_image_url": "https://github.githubassets.com/images/email/message_cards/header.png",
    "avatar_image_url": "https://github.githubassets.com/images/email/message_cards/avatar.png",
    "action": {
      "name": "Open in GitHub",
      "url": "https://github.com/motemen/test-repository"
    }
  },
  "updates": {
    "snippets": [
      {
        "icon": "DESCRIPTION",
        "message": "Closed #34."
      }
    ],
    "action": {
      "name": "View Issue",
      "url": "https://github.com/motemen/test-repository/issues/34#event-2043751833"
    }
  }
}

api_key は公開のものっぽいけど、読んでる側がビビるのでマスクしてます)

ちなみに、イシューのクローズじゃなく勝手なコメントをつけた場合は以下のようになる。

<script type="application/json" data-scope="inboxmarkup">{"api_version":"1.0","publisher":{"api_key":"********************************","name":"GitHub"},"entity":{"external_key":"github/motemen/test-repository","title":"motemen/test-repository","subtitle":"GitHub repository","main_image_url":"https://github.githubassets.com/images/email/message_cards/header.png","avatar_image_url":"https://github.githubassets.com/images/email/message_cards/avatar.png","action":{"name":"Open in GitHub","url":"https://github.com/motemen/test-repository"}},"updates":{"snippets":[{"icon":"PERSON","message":"@motemen in #34: Closed #34."}],"action":{"name":"View Issue","url":"https://github.com/motemen/test-repository/issues/34#issuecomment-449838417"}}}</script>
{
  "api_version": "1.0",
  "publisher": {
    "api_key": "********************************",
    "name": "GitHub"
  },
  "entity": {
    "external_key": "github/motemen/test-repository",
    "title": "motemen/test-repository",
    "subtitle": "GitHub repository",
    "main_image_url": "https://github.githubassets.com/images/email/message_cards/header.png",
    "avatar_image_url": "https://github.githubassets.com/images/email/message_cards/avatar.png",
    "action": {
      "name": "Open in GitHub",
      "url": "https://github.com/motemen/test-repository"
    }
  },
  "updates": {
    "snippets": [
      {
        "icon": "PERSON",
        "message": "@motemen in #34: Closed #34."
      }
    ],
    "action": {
      "name": "View Issue",
      "url": "https://github.com/motemen/test-repository/issues/34#issuecomment-449838417"
    }
  }
}

diff はこんな感じ。

   "updates": {
     "snippets": [
       {
-        "icon": "DESCRIPTION",
-        "message": "Closed #34."
+        "icon": "PERSON",
+        "message": "@motemen in #34: Closed #34."
       }
     ],
     "action": {
       "name": "View Issue",
-      "url": "https://github.com/motemen/test-repository/issues/34#event-2043751833"
+      "url": "https://github.com/motemen/test-repository/issues/34#issuecomment-449838417"
     }
   }
 }

これがまあまあ使えそうですね。名前てきには Inbox by Gmail のためのマークアップなんではないかと予想するけど、特に公式なドキュメントはないので、いつまで使えるのかは神のみぞ知るです。

Google Apps Script でメールの自動アーカイブ

メールってやつはプッシュ型のタスク生成・情報取得ツールとしてすげー便利に使ってるのですが、受信トレイを綺麗に保つのには不断の努力が必要で、放っておくとすぐに管理不能な状態にまで膨れあがってしまう(いちおう、いわゆる Inbox Zero を理想として、とはいえそこからは遠いところにいます)。

ちょっと見ないすきに溜まったメールをザザーッと処理するんだけど、毎回これアーカイブしてるよな……と思うものもある。こういうものを機械に処理させられないとしたら、それは技術の敗北である。というわけでなんとかしたい。たとえば、最新情報であったり現在のステータスを知るために受け取ってるメールってのは、定期的に増えてくる一方で、古くなったものには価値がないので、勝手に消えていってほしいわけです。

具体的には、以下のようなメールを自動で処理したい。

  • 【毎日更新】Kindle日替わりセール
    • 前日のやつは不要
  • 〇〇さんの今日の予定リスト(午前 5 時現在)
    • Google カレンダーが送ってきてくれるやつ。前日のやつは不要
  • ニュース系
    • 一週間以上ためこむと読む気にならないので読まずにアーカイブしてる
  • カレンダーの招待
    • すでに終わったイベントの招待は自動でアーカイブできるはず

つわけで Google Apps Script を書きました。リポジトリはここです。

GitHub - motemen/gas-gmail-scripts

以下のスクリプトは mainという名前の関数を定期的に実行する想定で書かれてます。script.google.com から新規スクリプトを作成して、時間ベースのトリガを設定してください。

指定日数が経ったら自動でアーカイブする

https://github.com/motemen/gas-gmail-scripts/blob/master/dist/expire.js

+expires-1d というラベルがついたメールのうち、1日以上古いものを自動でアーカイブします。1日経つと情報が古くなるようなメール(Kindle日替わりセールとか)に Gmail のフィルタでこのラベルをつけておけば OK。

実は +expires- 以降は自由で、アーカイブ対象とするメールの検索条件older_than: として渡されるだけなので、+expires-7d とか +expires-1m とかいうラベルでも有効です。かしこい。

Google カレンダーの招待で、すでに終わったものを自動でアーカイブする

https://github.com/motemen/gas-gmail-scripts/blob/master/dist/archive-stale-calendar-invitations.js

見出しのとおり。Google カレンダーの招待メールに添付されている invite.ics ファイルをかんたんに解析して、イベントの終了時刻を過ぎていたらアーカイブします。iCal 形式のファイルの解析をしてるけど、タイムゾーンの対応は面倒なので UTC で書かれているものしか対応していない。Google カレンダーは ics ファイルの中身を UTC で書いてるみたいなので、特に問題はないと思います。

書いてて思ったけど、イベントの更新があったら古い招待をアーカイブするってのがあってもいいかもしんないですね。PR 期待してます。


どうぞご利用ください。

Spreadsheet に送信したスピードキュービングの記録を Slack や Pixela に連携する

ご機嫌いかがでしょうか。以前作ったタイマーアプリ fuTimer はスピードキュービングの測定記録をスプレッドシートに送信できるんですが、

記録をスプレッドシートに保存できるスピードキューブ用タイマーアプリを作った - 詩と創作・思索のひろば

スプレッドシートは貧者のサーバレスこと(言い飽きた)Google Apps Script を呼びだせるので、記録データをその他の外部サービスと連携できます。自分が求めているのは Slack と Pixela の2つだったので、これらに投稿する仕組みを作りました。あとは Twitter もあってもいいかもな。

Slack

こんな感じで、セッションをシートに保存するたびに、Slack に通知できます。

f:id:motemen:20181106101801p:plain

インストールと設定

  1. fuTimer の記録に使っているスプレッドシートを開き、メニューから [ツール] → [スクリプト エディタ] を選択して開く。リポジトリにある slack.js を "slack.gs" など適当な名前のスクリプトファイルとして追加します。
  2. [ファイル] → [プロジェクトのプロパティ] から、[スクリプトのプロパティ] を選択し、 SLACK_WEBHOOK_URL という名前で、任意の Slack ウェブフック URL を追加します。
    • f:id:motemen:20181106102022p:plain:w450
  3. [編集] → [現在のプロジェクトのトリガー] で、「スプレッドシートから」「変更時」に postStatsToSlack を呼ぶよう設定します。さっき見たらここの UI が変わっててびっくりした。
    • f:id:motemen:20181106101812p:plain:w450
  4. あとは fuTimer から記録を送信するたびに Slack に通知されるはずてすが、外部アクセスの認可は一度だけ手動で与えてやる必要があるので、[実行] → [関数の実行] から手で postStatsToSlack を実行し、権限を与えてください。たぶん「このアプリは確認されていません」と出るけど、「詳細」からポチポチしてると進められた。

仕組み

シートに仕込んだスクリプトでは、シートの編集時に onEdit という名前の関数が(あれば)呼ばれ、引数のイベントオブジェクトから変更された範囲も取得できるんだけど、fuTimer がやってるような API からの変更の場合はどうもこれが呼ばれないらしい。

ってのと、onEdit トリガからだと外部サービスを呼べないっぽい気がする(未検証)。

仕方がないので上に書いたように onChange トリガを受けるようにして、変更された範囲も取れないのでスクリプトプロパティに、最後に取り扱った行数を記録して、差分を Slack に送信しています。

Pixela

同様に Pixela。Pixela については、commit以外の数値でも草を生やせる、PixelaというAPIサービスを作った! - えいのうにっき を読んでもらうとして、毎日の計測回数でもって、以下のように芝を生やすことができます。自分の場合は fuTimer 以前の記録もシートに転記しているので、練習はじめたころからの記録もある。

意外と計測してない日があって、身が引き締まりますね。

インストールと設定

  1. https://pixe.la のドキュメントにのっとって、Pixela のユーザとグラフを作成します。
  2. Slack の場合と同様に、 pixela.js を配置します。
  3. [ファイル] → [プロジェクトのプロパティ] から、[スクリプトのプロパティ] を選択し、 PIXELA_API_URL という名前で、任意の Pixela API エンドポイント URL を、PIXELA_TOKEN という名前で Pixela ユーザトークンを設定します。
  4. [編集] → [現在のプロジェクトのトリガー] で、「時間主導型」「日付ベースのタイマー」「午前3〜4時(自分が寝てそうな時間帯)」で syncPixelDelta を呼び出すよう設定する。
  5. 上記のトリガは差分更新(前日1日分)なので、手動で syncPixelaAll を実行して、これまでの記録を Pixela に送信します。

仕組み

こちらは一日一度のバッチとして動かして、前日の日付の記録を洗い出し、その数を前日のカウントとして Pixela に投稿している。簡単ですね。なので更新は一日遅れになります。

Webpack で Google Apps Script のスクリプトを生成する

gas-webpack-plugin を利用した。global のプロパティへの代入があれば、トップレベルの関数として宣言するようになっているらしいんだけど、README 通り mode: "production" とすると Uglify されるからかうまくいかなくて、mode: "development" としたうえで devtool: false としpluginsModuleConcatenationPlugin を与えた らうまくコンパイルできた。