Seiichi Yonezawa

ExpressからElectronに通知する方法

自宅サーバでGitLabを運用しているのですが、相変わらず便利です。 しかしSMTPサーバは運用していないため、基本的にメール経由の通知は利用できません。 特に困るのはGitLab CIの結果はブラウザのタブを開きっぱなしにしておくか、後から調べなければなりません。

Webhook

GitLabはSlackやMattermostなどに対応しているため、通知を送信することも可能です。 サーバはインターネット上に公開してはいるものの、不用意に外部のサービスとは連携したくありません。 また、チャットでタブを開き続けるならGitLab CIのタブを開き続けるのとなんら変わりません。

幸いGitLabではWebhookをサポートしています。 Webhookを簡単に説明すると、GitLabのCIを通知する時に指定したエンドポイントにHTTPリクエストを発します。 つまり、GitLabからExpressをバイパスすることで間接的にプッシュ通知が実現できるというわけです。

Web Notification

送信する側の仕組みがわかったので、今度は受信する側の仕組みです。 先述のタブを避けるため、今回はElectronを採用します。 Electronを使う理由はデスクトップ通知が驚くほど簡単にできてしまうからです。

Notification.requestPermission().then(result => {
  if (result === 'granted') {
    new Notification("PONG");
  }
});

通常はブラウザで上記のコードを実行すると、デスクトップ通知を出す前にユーザの同意が必要になります。 しかし、これをElectronで実行すると確認なしで表示されます。 あとはこれをExpress側がElectronに対して任意のタイミングで実行できるようにしてあげればよいのです。

Express

続いてサーバ側です。 今回はExpressを利用しますが、SSEが書けるならば言語は問いません。 また、SSEを書くこと自体も簡単です。

const app = require('express')()
const connections = []

app.get('/', (req, res, next) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive"
  });
  connections.push(res);
})

app.post('/', (req, res, next) => {
  connections.forEach(response => {
    response.write(`data: PING\n\n`);
  });

  res.end(200);
})

それぞれGET /ではElectronからの接続を受けて、POST /で一斉通知をします。 ヘッダ側の指定とres.sendres.jsonのような関数を実行しないため、HTTPの接続は保持されます。 ただしブラウザ側の接続が切れた場合に備えてサーバ側で何らかの対策を行うべきでしょう。

EventSource

サーバ側の準備が整ったので再びElectronに戻ります。 今回は一方的な通知なのでWebSocketを使わずにEventSourceを利用します。 この実装も簡単です。

const es = new EventSource('http://localhost:3000/')

es.addEventListener('message', () => {
  Notification.requestPermission().then(result => {
    if (result === 'granted') {
      new Notification("PONG");
    }
  });
})

EventSourceのイベントリスナーに'message'を登録するだけです。 現時点ではPINGに対してPONGを返すだけですが、JSONでやりとりすればCIの結果などが送信できると思います。

Nginxで実行する場合

https://serverfault.com/questions/801628/for-server-sent-events-sse-what-nginx-proxy-configuration-is-appropriate

開発環境で上記のコードは問題ありませんでしたが、いざデプロイしてみるとうまく動作しませんでした。 原因を探ってみるとどうやらNginxがSSEのリクエストを分断してしまうようです。 そこで、Server Faultの回答を参考にサーバ側のコードを修正しました:

 app.get('/', (req, res, next) => {
   res.writeHead(200, {
     "Content-Type": "text/event-stream",
     "Cache-Control": "no-cache",
-    "Connection": "keep-alive"
+    "X-Accel-Buffering": "no"
   });
   connections.push(res);
 })

今回はヘッダの更新のみで、Nginx側の変更は必要ありませんでした。 Nginx側の不具合を特定するのは難しく、エラーもわかりにくいので大変でした。


今回SSEによる方法を調べる前に、Gitifyというプロジェクトを参考にしようと思いました。 ElectronだけでGitHubの通知がわかるのかと不思議でしたが、おそらくこちらは60秒ごとにfetchNotificationsを実行しているのでロングポーリングの手法を採用していると思います。 もともとは60秒ごとに調べるようなコードでもよかったのですが、今回プッシュ通知ができるか試したかったので諦めました。

しかし、ReactとElectronが組み合わせてあり、頑張れば読めそうなコード量なので今後の参考になりそうです。 なによりGitHubを使うならこういったGitifyを利用してみてもよいかもしれません。

投稿一覧