Parse.com REST APIを使ったAPN(remote notification)を試した


Parse.comでは、Push Notificationをサポートしています。
見た目にもわかりやすいUIのDashboardからPush notificationを送信できるのですが、
それ以外にREST APIを使って、アプリ側からPush notificationを送信できます。
DeviceTokenを使って細かく制御する方法が若干わかりにくかったので、それについて書きます。

##Parse APIへの送信

Push notificationは最低限`alert`があればいいので、`curl`を使うと下記のように指定して送信します。
`X-Parse-Application-Id`(Application ID)と`X-Parse-REST-API-Key`(REST API Key)は、DashboardのOverviewから取得出来ます。
後はなんとログインした状態で、Documentを見ている場合、コード例に直接Keyが埋め込まれているためそのまま実行出来ます!!

```
curl -X POST \
  -H "X-Parse-Application-Id: <Application ID>" \
  -H "X-Parse-REST-API-Key: <REST API Key>" \
  -H "Content-Type: application/json" \
  -d '{
        "data": {
          "alert": "The Giants won against the Mets 2-3."
        }
      }' \
  https://api.parse.com/1/push
```

##ParseのNotificationフォーマットについて

簡単に表すと下記のJSONフォーマットになります。
`where`のところに条件を指定するので、ここに`deviceToken`を指定すれば特定の端末にPush Notificaitonを送信することができます。

- `data`のところに、iOSまたはAndroidのnotificationデータを入れる
- 送信日時、有効期限の指定が簡単
- `where`のところに、条件Queryを指定できる
  - `$lt`(less then)、`$in`(containd in)など、一般的な条件演算子がひと通り揃っているので、柔軟なリクエストが可能

```parse_push.py
{'where': {'deviceToken': [],                   # Query APIを使った条件指定
    },
    'push_time': '2013-01-23T12:00:00Z',        # 通知開始日時(iso)
    'expiration_interval': '518400',            # 有効期限
    'expiration_time': '2013-01-23T21:08:39Z',  # 有効期限日時指定(iso)
    'data': {
        'alert': 'message',
        'badge': 'Increment',                   # Number or 'Increment'(iOS only).
        'sound': 'sound bundle',                # iOS only.
        'content-available': '1',               # Newsstand content count(iOS only).
        'action': 'Intent',                     # Android only.
        'title': 'Notification!!',              # Android only.
    }
}
```

条件演算子を指定した場合は、こんな感じです。

```parse_push2.py
{'data': {'alert': 'push notification message!',
          'badge': 'Increment',
          'sound': 'default'},
 'where': {'deviceToken': {'$in': [u'<deviceToken1>', u'<deviceToken2>']}}}
```

###コード例

Pythonで書くとこんな感じになります。

```apn.py
# coding: utf-8

PARSE_API_BASE = 'https://api.parse.com'
PARSE_PUSH_API = '/1/push'
PARSE_PUSH_URL = PARSE_API_BASE + PARSE_PUSH_API
# Push Notificationに必要なKeyをHTTP Headerで指定
HEADERS = {
    "X-Parse-Application-Id": "<Application ID>",
    "X-Parse-REST-API-Key": "<REST API Key>",
    "X-Parse-Master-Key": "<Master Key>",
    "Content-Type": "application/json"
}


class APNMessageQueue(object):
    """ APN(Apple Push notification)用メッセージを貯める
    """
    structure = {
        'user_id': lambda x: (x is not None and len(x) > 0),
        'notify_token': lambda x: (x is not None and len(x) > 0),
        'message': lambda x: (x is not None and len(x) > 0),
    }

    def __init__(self, *args, **kwargs):
        for k in self.structure.keys():
            self.__setattr__(k, kwargs.get(k, None))

    @classmethod
    def create(cls, user_id, notify_token, message):
        src_props = dict(
            user_id=user_id,
            notify_token=notify_token,
            message=message,
        )
        props = {}

        for k, func in cls.structure.iteritems():
            value = src_props.get(k)
            if func(value):
                props[k] = value

        if props:
            return APNMessageQueue(**props)
        return None


def create_notify_message(message, badge='Increment', sound='default'):
    """ Notificationメッセージから、PushNotificationフォーマット作成

    Args:
        message: Alertメッセージ.
        badge: バッチカウント('Increment'はParse.com独自).
        sound: Notification効果音.

    Return:
        PushNotificationフォーマット文字列.

    """
    return {'alert': message, 'badge': badge, 'sound': sound}


def create_request_data(notify_token, apn_data):
    """ Parse Notificationフォーマットテンプレ形式のデータ作成

    Args:
        notify_token: デバイストークンかそれのリスト.
        apn_data: Apple Push Notificationデータ.

    Returns:
        ParseNotificationフォーマットテンプレ形式のデータ.

    """
    request_data = {'data': apn_data}
    if isinstance(notify_token, list):
        request_data['where'] = {'deviceToken': {'$in': notify_token}}
    else:
        request_data['where'] = {'deviceToken': notify_token}
    return request_data


def process_do_send(request_data):
    """ ParseNotificationフォーマットテンプレ形式のデータを渡してPushNotification送信

    Args:
        request_data: ParseNotificationフォーマットテンプレ形式のデータ

    Returns:
        正常終了かエラーかを返す

    """
    import json
    from urllib2 import Request, urlopen, URLError

    payload = json.dumps(request_data)
    request = Request(PARSE_PUSH_URL, payload, HEADERS)
    try:
        response = urlopen(request)
        data = json.loads(response.read())
        if isinstance(data, dict):
            return data.get('result', False)
    except URLError, e:
        if hasattr(e, 'reason'):
            print 'We failed to reach a server.'
            print 'Reason: %s' % e.reason
        elif hasattr(e, 'code'):
            print 'The server couldn\'t fulfill the request.'
            print 'Error code: %d' % e.code
    return False


def send_notification(notify_queue_list):
    """ APNsへPushNotificationを送信

    APNsサーバーへPushNotificationメッセージを一斉送信

    Args:
        notify_queue_list: APNMessageQueueのlist

    """
    from itertools import groupby
    notify_sorted_queue_list = sorted(notify_queue_list, key=lambda x: x.message)
    # 同じメッセージはまとめて送る
    for message, group in groupby(notify_sorted_queue_list, key=lambda x: x.message):
        distict_tokens = list(set([user.notify_token for user in group]))
        aps_data = create_notify_message(message)
        request_data = create_request_data(distict_tokens, aps_data)
        print request_data
        process_do_send(request_data)
```

使う側はこう

```main.py
# coding: utf-8

def send_push_parse_for_apn(message):
    import models
    import apn

    # ユーザーデータから、Notification用deviceToken一覧を取得
    targets = [x for x in models.User.find_by_existing_notify_token() if x is not None]
    notify_token_list = [apn.APNMessageQueue(x.user_id, x.notify_token, message) for x in targets]

    if len(notify_token_list) > 0:
        # 発射ー
        apn.send_notification(notify_token_list)

if __name__ == '__main__':
    send_push_parse_for_apn('あボーーん!!')
```