Redmine のチケット更新をデスクトップ通知する Electron アプリの作り方

おはようございます。こんにちは。こんばんは。お疲れ様です。お世話になっております。
さて今回は、最近流行りの Electron で作ってみたアプリについてご紹介いたします。

## 作ったもの

メニューバーやタスクトレイに常駐して Redmine のチケットの更新を監視し、更新があればデスクトップに通知してくれるアプリです。

https://github.com/emsk/redmine-notifier

Mac OSX と Windows でご利用いただけます。
また、各種 OSS を利用したインストーラ作成のための環境も同梱しています。

## 使い方

1. REST API 機能を有効にする
Redmine の管理画面にて REST API の機能を有効にします。
※システム管理者権限が必要です

2. 設定画面を開く
Redmine Notifier を起動すると、メニューバーまたはタスクトレイにアイコンが表示されます。
アイコンのコンテキストメニューにて「Preferences」をクリックし、設定画面を開きます。

3. 設定する
設定画面にて Redmine サーバの URL と API アクセスキーを設定します。
「Fetch Interval Time」には Redmine サーバにアクセスして更新をチェックする間隔を設定しますが、サーバへの負荷を考慮して設定してください。
設定し終わったら「Save」ボタンをクリックし、保存してください。
以後、設定画面を閉じてもメニューバーまたはタスクトレイに常駐し、チケットの更新をチェックしてくれます。

## なぜ作ったか

チームで Redmine を使っていて、誰かがチケットを更新したことに素早く気付けずに対応が遅れてしまった、ということはありませんか。

Redmine にはチケット等の更新をメールで通知してくれる便利な機能があります。
この機能を使えばチケットの更新に気付くことができますので、利用されている方も多いかと思います。

しかし、

* 他の重要なメールがたくさん来るから、これ以上受け取るメールを増やしたくない
* 一日中画面見てるから、もっと雑な通知で良い

というようなことを少し感じていました。
また、Electron を使ってみたいというエンジニアとしての欲求があり、丁度良い機会だと思ったので今回のアプリを作ることにしました。

## 作り方

Electron を使った開発については、最近多くの記事を見かけますし、興味を持たれる方も多いかと思いますので、少し触れておきます。

開発するにあたり、主に下記のようなツールや技術を利用しました。
※下記で引用しているソースコードについては 2016/1/3 現在のものであり、変更される可能性があります

### Electron

https://github.com/atom/electron

設定画面を閉じた際に、更新チェック間隔が設定した時間よりも長くなってしまう問題があったため、ブラウザプロセス側にて下記のようなコードを書くことで対応しています。

```app/main.js
app.commandLine.appendSwitch('disable-renderer-backgrounding');
```

対応方法については[こちらの記事](http://www.pracucci.com/electron-slow-background-performances.html)を参考にしました。

### Redmine Issues API

http://www.redmine.org/projects/redmine/wiki/Rest_Issues

Redmine の全チケットのうち、前回更新をチェックした時刻以降に更新されたチケットを取得するようにしています。

```app/index.js
RedmineNotifier.prototype.fetch = function() {
  var _this = this;
  var xhr = new XMLHttpRequest();
  var requestParams = '?updated_on=%3E%3D' + this._lastExecutionTime + STATIC_REQUEST_PARAMS;

  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      _this.notify(JSON.parse(xhr.responseText).issues);
    }
  };

  xhr.open('GET', this._settings.url + '/issues.json' + requestParams);
  xhr.setRequestHeader('X-Redmine-API-Key', this._settings.apiKey);
  xhr.send();

  this.updateLastExecutionTime();
};
```

### electron-packager

https://github.com/maxogden/electron-packager

Redmine Notifier は app ディレクトリ以下がアプリ本体で、それ以外は開発用のツール群という構成になっています。
electron-packager は、app ディレクトリ以下を Electron アプリとして実行可能な状態にパッケージングするために使用しています。

```bin/build-osx.sh
electron-packager ../app 'Redmine Notifier' \
  --platform=darwin \
  --arch=x64 \
  --version=0.36.2 \
  --out=../dist/osx \
  --icon=../assets/osx/redmine_notifier.icns \
  --asar \
  --asar-unpack='**/app/{node_modules/node-notifier/vendor,images}/**' \
  --ignore='\.DS_Store|npm-debug\.log|^/etc'
```

`--asar-unpack` オプションには、パッケージング後に dist ディレクトリ以下に現れるアプリの中にある app.asar ファイルの外（app.asar.unpacked）に出力したいファイルを glob 記法で指定します。

### electron-builder

https://github.com/loopline-systems/electron-builder

Mac OSX と Windows 用のインストーラを作成するために使用しています。

```bin/pack-osx.sh
electron-builder '../dist/osx/Redmine Notifier-darwin-x64/Redmine Notifier.app' \
  --platform=osx \
  --out=../dist/osx \
  --config=./config.json
```

```bin/config.json
{
  "osx": {
    "title": "Redmine Notifier",
    "background": "../assets/osx/redmine_notifier_installer.png",
    "icon": "../assets/osx/redmine_notifier.icns",
    "icon-size": 120,
    "contents": [
      { "x": 370, "y": 300, "type": "link", "path": "/Applications" },
      { "x": 130, "y": 300, "type": "file" }
    ]
  },
  "win": {
    "title": "Redmine Notifier",
    "version": "0.2.0",
    "icon": "../assets/win/redmine_notifier.ico"
  }
}
```

Mac OSX 用のアイコンについては [Yosemite icon generator](http://ls8h.com/yosemite-icon/) を利用させていただき、雑に作りました。

### node-notifier

https://github.com/mikaelbr/node-notifier

クロスプラットフォームなデスクトップ通知のためのライブラリです。
内部で [terminal-notifier](https://github.com/alloy/terminal-notifier) や [notifu](http://www.paralint.com/projects/notifu/) 等が使用されているようですが、それらの差異を吸収し、同じインターフェイスを提供してくれます。
デスクトップ通知の機能をシンプルに実装したかったため、採用しました。

ただ、asar にアーカイブした場合、通知に使用する画像について app.asar 内のパスを設定できない問題がありました。
こちらについては、パッケージング時に画像を app.asar.unpacked に出力しておき、その画像ファイルのパスを指定することで無理矢理対応しています。

```app/index.js
var appDir = __dirname + '.unpacked'; // Production
try {
  fs.statSync(appDir);
} catch(e) {
  appDir = __dirname; // Development
}

notifier.notify({
  title: '(' + issueCount + ') Redmine Notifier',
  message: issues[0].subject,
  icon: appDir + '/images/' + COLOR_ICON_FILENAME_64,
  wait: true
});
```

パッケージングされたアプリを起動した場合（Production）は app.asar.unpacked/images 以下のファイルが使用され、`npm start` にてアプリを起動した場合（Development）は app/images 以下のファイルが使用されます。

### notie.js

https://github.com/jaredreich/notie.js

設定画面にてメッセージを表示するために使用しています。
それほど凝ったメッセージは表示しない想定でしたので、シンプルな notie.js で十分でした。

### localStorage

設定を保存するために使用しています。
レンダラプロセス側で保存処理を簡単に実装することができました。

## 参考

各 OSS の Github Issue の他、特に下記の記事を参考にさせていただきました。
ありがとうございます。

* http://qiita.com/mizchi/items/2bf64eb51d2018fa8a6a
* http://akabeko.me/blog/2015/09/electron/

## 使ってみてください

自分で使うためにとりあえず作ってみたレベルですので、まだまだ雑な作りになっています。
（最近は雑なレベルでも[早く公開してみることを重視したい](http://qiita.com/emsk/items/5f43037d9049cce6f900#2-3)）

ご意見、ご要望等ありましたら、お気軽に。
よろしくお願いいたします。