APNsをRubyで実装するための、houstonより便利なGem作った


## 作ったもの
Apns::Persistent
[Gem](https://rubygems.org/gems/apns-persistent)
[GitHub](https://github.com/malt03/apns-persistent)

## 既存の問題点
GemのAPNs用モジュールのデファクトは

- [apns](https://rubygems.org/gems/apns)
- [houston](https://rubygems.org/gems/houston)

の二択だと思います。
この2つのGemには、それぞれ別の問題点があります。

### apns
ソケット保持が出来ない。これに尽きます。
送信ごとにソケットのopen/closeを繰り返すと、AppleからDoS攻撃と見なされる可能性があります。
これでは安心して大量にPush通知を送ることが出来ません。

### houston
エラー処理のところがバグっていて、エラーを取得できません。
プルリクが飛んでいてそれをマージすれば直るはずなのですが、開発者が離れてしまったらしくマージされません。
APNsでは、間違ったデバイストークンに送った後、そのままのソケットで送ると届かないという問題があるために、結構致命的です。

## 解決
ということで、houstonの良い所を丸パクリし、houstonに対するその他不満点も解消したGemを作りました。
解消した不満点は

- ソケット保持をしようとすると、実装がやや猥雑
- エラー処理がバグっていなかったとしても分かりづらい
- バグっていなかったとしても、エラーが起きた時の再接続が面倒
- FeedbackAPIの名前が分かりづらい

辺りでしょうか。真似したところは

- コマンドラインから送れるの嬉しい
- そもそもソケット通信辺りは実装丸パクリ
- URL意識しなくて良い

辺りです。[この記事](http://qiita.com/u1_fukui/items/1534a7a043882805aa88)を参考にしました。

## 実装方法
### Push通知の送信
俺の怪しい英語で書かれたREADMEを見てもらえれば分かりますが、

```ruby:apns.rb
#初期化
c = Apns::Persistent::PushClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true)

#ソケットオープン
c.open

#エラー処理を登録
thread = c.register_error_handle do |command, status, id|
  puts "Send Error! command:#{command} status:#{status} id:#{id}"
end

#送信
c.push(token: '88189fcf 62a1b2eb b7cb1435 597e734e a90da4ce 6196a9b3 309a5421 4c6259e',
       alert: 'foobar',
       sound: 'default',
       id: 1)

begin
  #待機
  thread.join
rescue Interrupt
  #Ctrl-cで終了
  c.close
end
```
こんな感じです。

エラーハンドルを登録するとスレッドを返すので、保持しておけばエラーが起きた時に渡されたブロックを呼んでくれます。
後は送るだけです。
実用例としては、[コマンドラインツール](https://github.com/malt03/apns-persistent/tree/master/exe)の、apns_daemonとapnsの実装をみてもらうのが一番わかりやすいかと思います。

一度だけ送りたい場合、以下の様な使い方も出来ますが、これやるなら素直に[apns](https://rubygems.org/gems/apns)使ったほうが良いと思いますｗ

```ruby:send_once.rb
#保持せずに送信
Apns::Persistent::PushClient.push_once(certificate: '/path/to/apple_push_notification.pem',
                                       token: '88189fcf 62a1b2eb b7cb1435 597e734e a90da4ce 6196a9b3 309a5421 4c6259e9',
                                       alert: 'foobar',
                                       sound: 'default',
                                       id: 1) do |command, status, id|
  puts "Send Error! command:#{command} status:#{status} id:#{id}"
end
```

スレッドとかめんどくさい場合は、以下のようにとりあえずでソケット保持したままの送信も出来ます。

```ruby:without_thread.rb
#初期化
c = Apns::Persistent::PushClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true)

#ソケットオープン
c.open

#送信
c.push(token: '88189fcf 62a1b2eb b7cb1435 597e734e a90da4ce 6196a9b3 309a5421 4c6259e',
       alert: 'foobar',
       sound: 'default',
       id: 1) do |command, status, id|
  puts "Send Error! command:#{command} status:#{status} id:#{id}"
end

#ソケット閉じる
c.close
```

勿論カスタムペイロードとかサイレントプッシュとか、まぁAPNsで出来ることは全部出来るようになっているので、その他の引数は実装見てください。

### Feedback API

Feedback APIでは、何度も送信に失敗したデバイスの情報を取得できます。
デバイスのトークンと、Appleに「このデバイスもうダメ」と判断された日時のユニックスタイムスタンプが返ってきます。
まぁ、正直トークンしか使わないです。トークンだけ取得する実装がこんな感じです。

```ruby:get_device_tokens.rb
#初期化
c = Apns::Persistent::FeedbackClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true)
#ソケットオープン
c.open
#送れないデバイスのトークンのみ取得
device_tokens = c.unregistered_device_tokens
#閉じる
c.close
```

もしタイムスタンプも欲しければこんな感じ。

```ruby:get_devices.rb
#初期化
c = Apns::Persistent::FeedbackClient.new(certificate: '/path/to/apple_push_notification.pem', sandbox: true)
#ソケットオープン
c.open
#送れないデバイス一覧取得
devices = c.unregistered_devices
#閉じる
c.close
```

こちらも1回だけ取得もあります。

```ruby:get_device_tokens_once.rb
device_tokens = Apns::Persistent::FeedbackClient.unregistered_device_tokens_once(certificate: '/path/to/apple_push_notification.pem', sandbox: true)
```

## コマンドラインツール
まず送信処理用デーモンを立ち上げます。

```
$ push_daemon --pemfile <path> [--sandbox]
```

そんで、デーモンに送信命令

```
$ push --token <token> --alert "Hello" --badge 2 --sound "default"
```

こんな感じですね。1度だけ送りたい場合はこちら。

```
$ push_once --pemfile <path> [--sandbox] --token <token> --alert "Hello" --sound "default" --badge 2
```

これも、引数に色々あるので、--help叩いてもらえばなんとなーく分かるかなと。

## 感想
初めてGem作りましたが、なかなか楽しかったです。
もし疑問点、改善点、感想等々、あれば是非コメント頂ければ嬉しく思います。

## 参考
[Local および Push Notification プログラミングガイド](https://developer.apple.com/jp/documentation/RemoteNotificationsPG.pdf)「接続管理に関するベストプラクティス」
[houston GitHub](https://github.com/nomad/houston)