195 lines
6.0 KiB
Python
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)
|