debian-python-django-push-n.../push_notifications/gcm.py

195 lines
6.0 KiB
Python

"""
Google Cloud Messaging
Previously known as C2DM
Documentation is available on the Android Developer website:
https://developer.android.com/google/gcm/index.html
"""
import json
from .models import GCMDevice
try:
from urllib.request import Request, urlopen
from urllib.parse import urlencode
except ImportError:
# Python 2 support
from urllib2 import Request, urlopen
from urllib import urlencode
from django.core.exceptions import ImproperlyConfigured
from . import NotificationError
from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
class GCMError(NotificationError):
pass
def _chunks(l, n):
"""
Yield successive chunks from list \a l with a minimum size \a n
"""
for i in range(0, len(l), n):
yield l[i:i + n]
def _gcm_send(data, content_type):
key = SETTINGS.get("GCM_API_KEY")
if not key:
raise ImproperlyConfigured('You need to set PUSH_NOTIFICATIONS_SETTINGS["GCM_API_KEY"] to send messages through GCM.')
headers = {
"Content-Type": content_type,
"Authorization": "key=%s" % (key),
"Content-Length": str(len(data)),
}
request = Request(SETTINGS["GCM_POST_URL"], data, headers)
return urlopen(request).read().decode("utf-8")
def _gcm_send_plain(registration_id, data, **kwargs):
"""
Sends a GCM notification to a single registration_id.
This will send the notification as form data.
If sending multiple notifications, it is more efficient to use
gcm_send_bulk_message() with a list of registration_ids
"""
values = {"registration_id": registration_id}
for k, v in data.items():
values["data.%s" % (k)] = v.encode("utf-8")
for k, v in kwargs.items():
if v:
if isinstance(v, bool):
# Encode bools into ints
v = 1
values[k] = v
data = urlencode(sorted(values.items())).encode("utf-8") # sorted items for tests
result = _gcm_send(data, "application/x-www-form-urlencoded;charset=UTF-8")
# Information about handling response from Google docs (https://developers.google.com/cloud-messaging/http):
# If first line starts with id, check second line:
# If second line starts with registration_id, gets its value and replace the registration tokens in your
# server database. Otherwise, get the value of Error
if result.startswith("id"):
lines = result.split("\n")
if len(lines) > 1 and lines[1].startswith("registration_id"):
new_id = lines[1].split("=")[-1]
_gcm_handle_canonical_id(new_id, registration_id)
elif result.startswith("Error="):
if result in ("Error=NotRegistered", "Error=InvalidRegistration"):
# Deactivate the problematic device
device = GCMDevice.objects.filter(registration_id=values["registration_id"])
device.update(active=0)
return result
raise GCMError(result)
return result
def _gcm_send_json(registration_ids, data, **kwargs):
"""
Sends a GCM notification to one or more registration_ids. The registration_ids
needs to be a list.
This will send the notification as json data.
"""
values = {"registration_ids": registration_ids}
if data is not None:
values["data"] = data
for k, v in kwargs.items():
if v:
values[k] = v
data = json.dumps(values, separators=(",", ":"), sort_keys=True).encode("utf-8") # keys sorted for tests
response = json.loads(_gcm_send(data, "application/json"))
if response["failure"] or response["canonical_ids"]:
ids_to_remove, old_new_ids = [], []
throw_error = False
for index, result in enumerate(response["results"]):
error = result.get("error")
if error:
# Information from Google docs (https://developers.google.com/cloud-messaging/http)
# If error is NotRegistered or InvalidRegistration, then we will deactivate devices because this
# registration ID is no more valid and can't be used to send messages, otherwise raise error
if error in ("NotRegistered", "InvalidRegistration"):
ids_to_remove.append(registration_ids[index])
else:
throw_error = True
# If registration_id is set, replace the original ID with the new value (canonical ID) in your
# server database. Note that the original ID is not part of the result, so you need to obtain it
# from the list of registration_ids passed in the request (using the same index).
new_id = result.get("registration_id")
if new_id:
old_new_ids.append((registration_ids[index], new_id))
if ids_to_remove:
removed = GCMDevice.objects.filter(registration_id__in=ids_to_remove)
removed.update(active=0)
for old_id, new_id in old_new_ids:
_gcm_handle_canonical_id(new_id, old_id)
if throw_error:
raise GCMError(response)
return response
def _gcm_handle_canonical_id(canonical_id, current_id):
"""
Handle situation when GCM server response contains canonical ID
"""
if GCMDevice.objects.filter(registration_id=canonical_id, active=True).exists():
GCMDevice.objects.filter(registration_id=current_id).update(active=False)
else:
GCMDevice.objects.filter(registration_id=current_id).update(registration_id=canonical_id)
def gcm_send_message(registration_id, data, **kwargs):
"""
Sends a GCM notification to a single registration_id.
If sending multiple notifications, it is more efficient to use
gcm_send_bulk_message() with a list of registration_ids
A reference of extra keyword arguments sent to the server is available here:
https://developers.google.com/cloud-messaging/server-ref#downstream
"""
return _gcm_send_plain(registration_id, data, **kwargs)
def gcm_send_bulk_message(registration_ids, data, **kwargs):
"""
Sends a GCM notification to one or more registration_ids. The registration_ids
needs to be a list.
This will send the notification as json data.
A reference of extra keyword arguments sent to the server is available here:
https://developers.google.com/cloud-messaging/server-ref#downstream
"""
# GCM only allows up to 1000 reg ids per bulk message
# https://developer.android.com/google/gcm/gcm.html#request
max_recipients = SETTINGS.get("GCM_MAX_RECIPIENTS")
if len(registration_ids) > max_recipients:
ret = []
for chunk in _chunks(registration_ids, max_recipients):
ret.append(_gcm_send_json(chunk, data, **kwargs))
return ret
return _gcm_send_json(registration_ids, data, **kwargs)