Compare commits
No commits in common. "master" and "debian" have entirely different histories.
|
@ -0,0 +1,16 @@
|
|||
# EditorConfig: http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
quote_type = double
|
||||
insert_final_newline = true
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.py]
|
||||
spaces_around_brackets = none
|
||||
spaces_around_operators = true
|
30
.travis.yml
30
.travis.yml
|
@ -1,12 +1,28 @@
|
|||
# https://travis-ci.org/jazzband/django-push-notifications
|
||||
sudo: false
|
||||
language: python
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- deadsnakes
|
||||
packages:
|
||||
- python3.5
|
||||
python: "3.6"
|
||||
|
||||
env:
|
||||
- TOXENV=py27-django111
|
||||
- TOXENV=py34-django111
|
||||
- TOXENV=py36-django111
|
||||
- TOXENV=py36-django20
|
||||
- TOXENV=py36-djangomaster
|
||||
- TOXENV=flake8
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- $TRAVIS_BUILD_DIR/.tox
|
||||
|
||||
install:
|
||||
- pip install tox
|
||||
|
||||
script:
|
||||
- tox
|
||||
sudo: false
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_failure: always
|
||||
on_success: change
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
This library was created by Jerome Leclanche <jerome@leclan.ch>, for use on the
|
||||
Anthill application (https://www.anthill.com).
|
||||
|
||||
Special thanks to the following core and frequent contributors:
|
||||
|
||||
Adam "Cezar" Jenkins
|
||||
Arthur Silva
|
||||
Camille Fabreguettes
|
||||
Jamaal Scarlett
|
||||
Matthew Hershberger
|
||||
Pablo Martín
|
||||
|
||||
|
||||
The full contributor list is available at the following URL:
|
||||
|
||||
https://github.com/jazzband/django-push-notifications/graphs/contributors
|
44
AUTHORS
44
AUTHORS
|
@ -1,44 +0,0 @@
|
|||
This library was created by Jerome Leclanche <jerome@leclan.ch>, for use on the
|
||||
Anthill application (https://www.anthill.com).
|
||||
|
||||
Special thanks to everyone who contributed:
|
||||
|
||||
Adam "Cezar" Jenkins
|
||||
Alan Descoins
|
||||
Ales Dokhanin
|
||||
Alistair Broomhead
|
||||
Andrey Zevakin
|
||||
Antonin Lenfant
|
||||
Arthur Silva
|
||||
Avichal Pandey
|
||||
Brad Pitcher
|
||||
Daniel Kronovet
|
||||
David Pretty
|
||||
Dilvane Zanardine
|
||||
Florian Finke
|
||||
Florian Purchess
|
||||
Francois Lebel
|
||||
halak
|
||||
Innocenty Enikeew
|
||||
Jack Feng
|
||||
Jamaal Scarlett
|
||||
Jay Camp
|
||||
Jeremy Morgan
|
||||
Jerome Leclanche
|
||||
Julien Dubiel
|
||||
Lital Natan
|
||||
Luke Burden
|
||||
Marconi Moreto
|
||||
Matthew Hershberger
|
||||
Maxim Kamenkov
|
||||
Mohamad Nour Chawich
|
||||
Nicolas Delaby
|
||||
Remigiusz Dymecki
|
||||
Ruslan Kovtun
|
||||
Sander Heling
|
||||
Sergei Evdokimov
|
||||
Sujit Nair
|
||||
Thomas Iovine
|
||||
Valentin Hăloiu
|
||||
Wyan Jow
|
||||
@hoongun
|
|
@ -1,10 +1,37 @@
|
|||
v1.4.1 (2016-01-11)
|
||||
===================
|
||||
## 1.7.0 (unreleased)
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Django Rest Framework < 3.7
|
||||
|
||||
|
||||
## 1.6.0 (2018-01-31)
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Django < 1.11
|
||||
* DJANGO: Support Django 2.0
|
||||
* NEW FEATURE: Add support for WebPush
|
||||
|
||||
|
||||
## 1.5.0 (2017-04-16)
|
||||
* BACKWARDS-INCOMPATIBLE: Remove `push_notifications.api.tastypie` module. Only DRF is supported now.
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Django < 1.10
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Django Rest Framework < 3.5
|
||||
* DJANGO: Support Django 1.10, 1.11
|
||||
* APNS: APNS is now supported using PyAPNS2 instead of an internal implementation.
|
||||
* APNS: Stricter certificate validity checks
|
||||
* APNS: Allow overriding the certfile from send_message()
|
||||
* APNS: Add human-readable error messages
|
||||
* APNS: Support thread-id in payload
|
||||
* FCM: Add support for FCM (Firebase Cloud Messaging)
|
||||
* FCM: Introduce `use_fcm_notification` option to enforce legacy GCM payload
|
||||
* GCM: Add GCM_ERROR_TIMEOUT setting
|
||||
* GCM: Fix support for sending GCM messages to topic subscribers
|
||||
* WNS: Add support for WNS (Windows Notification Service)
|
||||
* MISC: Make get_expired_tokens available in push_notifications.utils
|
||||
|
||||
|
||||
## 1.4.1 (2016-01-11)
|
||||
* APNS: Increased max device token size to 100 bytes (WWDC 2015, iOS 9)
|
||||
* BUGFIX: Fix an index error in the admin
|
||||
|
||||
v1.4.0 (2015-12-13)
|
||||
===================
|
||||
|
||||
## 1.4.0 (2015-12-13)
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Python<3.4
|
||||
* DJANGO: Support Django 1.9
|
||||
* GCM: Handle canonical IDs
|
||||
|
@ -17,12 +44,7 @@ v1.4.0 (2015-12-13)
|
|||
* BUGFIX: Display models correctly in the admin on both Python 2 and 3
|
||||
|
||||
|
||||
v1.3.1 (2015-06-30)
|
||||
===================
|
||||
This is an errata release.
|
||||
|
||||
v1.3.0 (2015-06-30)
|
||||
===================
|
||||
## 1.3.0 (2015-06-30)
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Python<2.7
|
||||
* BACKWARDS-INCOMPATIBLE: Drop support for Django<1.8
|
||||
* NEW FEATURE: Added a Django Rest Framework API. Requires DRF>=3.0.
|
||||
|
@ -35,8 +57,8 @@ v1.3.0 (2015-06-30)
|
|||
* BUGFIX: Assorted Python 3 bugfixes
|
||||
* BUGFIX: Fix display of device_id in admin
|
||||
|
||||
v1.2.1 (2015-04-11)
|
||||
===================
|
||||
|
||||
## 1.2.1 (2015-04-11)
|
||||
* APNS, GCM: Add a db_index to the device_id field
|
||||
* APNS: Use the native UUIDField on Django 1.8
|
||||
* APNS: Fix timeout handling on Python 3
|
||||
|
@ -47,8 +69,8 @@ v1.2.1 (2015-04-11)
|
|||
* GCM: Properly pass kwargs in GCMDeviceQuerySet.send_message()
|
||||
* BUGFIX: Fix HexIntegerField for Django 1.3
|
||||
|
||||
v1.2.0 (2014-10-07)
|
||||
===================
|
||||
|
||||
## 1.2.0 (2014-10-07)
|
||||
* BACKWARDS-INCOMPATIBLE: Added support for Django 1.7 migrations. South users will have to upgrade to South 1.0 or Django 1.7.
|
||||
* APNS: APNS MAX_NOTIFICATION_SIZE is now a setting and its default has been increased to 2048
|
||||
* APNS: Always connect with TLSv1 instead of SSLv3
|
||||
|
@ -59,8 +81,8 @@ v1.2.0 (2014-10-07)
|
|||
* BUGFIX: Fixed various issues relating HexIntegerField
|
||||
* BUGFIX: Fixed issues in the admin with custom user models
|
||||
|
||||
v1.1.0 (2014-06-29)
|
||||
===================
|
||||
|
||||
## 1.1.0 (2014-06-29)
|
||||
* BACKWARDS-INCOMPATIBLE: The arguments for device.send_message() have changed. See README.rst for details.
|
||||
* Added a date_created field to GCMDevice and APNSDevice. This field keeps track of when the Device was created.
|
||||
This requires a `manage.py migrate`.
|
||||
|
@ -71,13 +93,13 @@ v1.1.0 (2014-06-29)
|
|||
* Assorted migrations bugfixes
|
||||
* Added a test suite
|
||||
|
||||
v1.0.1 (2013-01-16)
|
||||
===================
|
||||
|
||||
## 1.0.1 (2013-01-16)
|
||||
* Migrations have been reset. If you were using migrations pre-1.0 you should upgrade to 1.0 instead and only
|
||||
upgrade to 1.0.1 when you are ready to reset your migrations.
|
||||
|
||||
v1.0 (2013-01-15)
|
||||
=================
|
||||
|
||||
## 1.0 (2013-01-15)
|
||||
* Full Python 3 support
|
||||
* GCM device_id is now a custom field based on BigIntegerField and always unsigned (it should be input as hex)
|
||||
* Django versions older than 1.5 now require 'six' to be installed
|
||||
|
@ -87,15 +109,13 @@ v1.0 (2013-01-15)
|
|||
* Integrate with travis-ci
|
||||
* Add an AUTHORS file
|
||||
|
||||
v0.9 (2013-12-17)
|
||||
=================
|
||||
|
||||
## 0.9 (2013-12-17)
|
||||
* Enable installation with pip
|
||||
* Add wheel support
|
||||
* Add full documentation
|
||||
* Various bug fixes
|
||||
|
||||
v0.8 (2013-03-15)
|
||||
=================
|
||||
|
||||
## 0.8 (2013-03-15)
|
||||
* Initial release
|
|
@ -1,6 +1,14 @@
|
|||
### Coding style
|
||||
## Jazzband
|
||||
|
||||
Please adhere to the coding style throughout the project.
|
||||
[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/)
|
||||
|
||||
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines).
|
||||
|
||||
|
||||
## Coding style
|
||||
This project follows the [HearthSim Styleguide](https://hearthsim.info/styleguide/).
|
||||
|
||||
In short:
|
||||
|
||||
1. Always use tabs. [Here](https://leclan.ch/tabs) is a short explanation why tabs are preferred.
|
||||
2. Always use double quotes for strings, unless single quotes avoid unnecessary escapes.
|
||||
|
@ -9,9 +17,10 @@ Please adhere to the coding style throughout the project.
|
|||
|
||||
Also see: [How to name things in programming](http://www.slideshare.net/pirhilton/how-to-name-things-the-hardest-problem-in-programming)
|
||||
|
||||
Flake8 tests are available with `tox -e flake8`. Run them before you commit!
|
||||
|
||||
### Commits and Pull Requests
|
||||
|
||||
## Commits and Pull Requests
|
||||
Keep the commit log as healthy as the code. It is one of the first places new contributors will look at the project.
|
||||
|
||||
1. No more than one change per commit. There should be no changes in a commit which are unrelated to its message.
|
||||
|
@ -19,6 +28,7 @@ Keep the commit log as healthy as the code. It is one of the first places new co
|
|||
3. Follow [these conventions](http://chris.beams.io/posts/git-commit/) when writing the commit message
|
||||
|
||||
When filing a Pull Request, make sure it is rebased on top of most recent master.
|
||||
If you need to modify it or amend it in some way, you should always appropriately [fixup](https://help.github.com/articles/about-git-rebase/) the issues in git and force-push your changes to your fork.
|
||||
If you need to modify it or amend it in some way, you should always appropriately
|
||||
[fixup](https://help.github.com/articles/about-git-rebase/) the issues in git and force-push your changes to your fork.
|
||||
|
||||
Also see: [Github Help: Using Pull Requests](https://help.github.com/articles/using-pull-requests/)
|
||||
|
|
377
README.rst
377
README.rst
|
@ -1,30 +1,35 @@
|
|||
django-push-notifications
|
||||
=========================
|
||||
.. image:: https://api.travis-ci.org/jleclanche/django-push-notifications.png
|
||||
:target: https://travis-ci.org/jleclanche/django-push-notifications
|
||||
.. image:: https://api.travis-ci.org/jazzband/django-push-notifications.png
|
||||
:target: https://travis-ci.org/jazzband/django-push-notifications
|
||||
|
||||
A minimal Django app that implements Device models that can send messages through APNS and GCM.
|
||||
.. image:: https://jazzband.co/static/img/badge.svg
|
||||
:target: https://jazzband.co/
|
||||
:alt: Jazzband
|
||||
|
||||
The app implements two models: ``GCMDevice`` and ``APNSDevice``. Those models share the same attributes:
|
||||
A minimal Django app that implements Device models that can send messages through APNS, FCM/GCM and WNS.
|
||||
|
||||
The app implements three models: ``GCMDevice``, ``APNSDevice`` and ``WNSDevice``. Those models share the same attributes:
|
||||
- ``name`` (optional): A name for the device.
|
||||
- ``active`` (default True): A boolean that determines whether the device will be sent notifications.
|
||||
- ``user`` (optional): A foreign key to auth.User, if you wish to link the device to a specific user.
|
||||
- ``device_id`` (optional): A UUID for the device obtained from Android/iOS APIs, if you wish to uniquely identify it.
|
||||
- ``registration_id`` (required): The GCM registration id or the APNS token for the device.
|
||||
- ``device_id`` (optional): A UUID for the device obtained from Android/iOS/Windows APIs, if you wish to uniquely identify it.
|
||||
- ``registration_id`` (required): The FCM/GCM registration id or the APNS token for the device.
|
||||
|
||||
|
||||
The app also implements an admin panel, through which you can test single and bulk notifications. Select one or more
|
||||
GCM or APNS devices and in the action dropdown, select "Send test message" or "Send test message in bulk", accordingly.
|
||||
FCM/GCM, APNS or WNS devices and in the action dropdown, select "Send test message" or "Send test message in bulk", accordingly.
|
||||
Note that sending a non-bulk test message to more than one device will just iterate over the devices and send multiple
|
||||
single messages.
|
||||
UPDATE_ON_DUPLICATE_REG_ID: Transform create of an existing Device (based on registration id) into a update. See below Update of device with duplicate registration ID for more details.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
Django 1.8 is required. Support for older versions is available in the release 1.2.1.
|
||||
|
||||
Tastypie support should work on Tastypie 0.11.0 and newer.
|
||||
|
||||
Django REST Framework support should work on DRF version 3.0 and newer.
|
||||
- Python 2.7 or 3.4+
|
||||
- Django 1.11+
|
||||
- For the API module, Django REST Framework 3.7+ is required.
|
||||
- For WebPush (WP), pywebpush 1.3.0+ is required. py-vapid 1.3.0+ is required for generating the WebPush private key; however this
|
||||
step does not need to occur on the application server.
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
@ -45,15 +50,21 @@ Edit your settings.py file:
|
|||
)
|
||||
|
||||
PUSH_NOTIFICATIONS_SETTINGS = {
|
||||
"GCM_API_KEY": "<your api key>",
|
||||
"FCM_API_KEY": "[your api key]",
|
||||
"GCM_API_KEY": "[your api key]",
|
||||
"APNS_CERTIFICATE": "/path/to/your/certificate.pem",
|
||||
"APNS_TOPIC": "com.example.push_test",
|
||||
"WNS_PACKAGE_SECURITY_ID": "[your package security id, e.g: 'ms-app://e-3-4-6234...']",
|
||||
"WNS_SECRET_KEY": "[your app secret key, e.g.: 'KDiejnLKDUWodsjmewuSZkk']",
|
||||
"WP_PRIVATE_KEY": "/path/to/your/private.pem",
|
||||
"WP_CLAIMS": {'sub': "mailto: development@example.com"}
|
||||
}
|
||||
|
||||
.. note::
|
||||
If you are planning on running your project with ``DEBUG=True``, then make sure you have set the
|
||||
If you are planning on running your project with ``APNS_USE_SANDBOX=True``, then make sure you have set the
|
||||
*development* certificate as your ``APNS_CERTIFICATE``. Otherwise the app will not be able to connect to the correct host. See settings_ for details.
|
||||
|
||||
You can learn more about APNS certificates `here <https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html>`_.
|
||||
You can learn more about APNS certificates `here <https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html>`_.
|
||||
|
||||
Native Django migrations are in use. ``manage.py migrate`` will install and migrate all models.
|
||||
|
||||
|
@ -63,22 +74,232 @@ Settings list
|
|||
-------------
|
||||
All settings are contained in a ``PUSH_NOTIFICATIONS_SETTINGS`` dict.
|
||||
|
||||
In order to use GCM, you are required to include ``GCM_API_KEY``.
|
||||
In order to use FCM/GCM, you are required to include ``FCM_API_KEY`` or ``GCM_API_KEY``.
|
||||
For APNS, you are required to include ``APNS_CERTIFICATE``.
|
||||
For WNS, you need both the ``WNS_PACKAGE_SECURITY_KEY`` and the ``WNS_SECRET_KEY``.
|
||||
|
||||
**General settings**
|
||||
|
||||
- ``USER_MODEL``: Your user model of choice. Eg. ``myapp.User``. Defaults to ``settings.AUTH_USER_MODEL``.
|
||||
- ``UPDATE_ON_DUPLICATE_REG_ID``: Transform create of an existing Device (based on registration id) into a update. See below `Update of device with duplicate registration ID`_ for more details.
|
||||
|
||||
**APNS settings**
|
||||
|
||||
- ``APNS_CERTIFICATE``: Absolute path to your APNS certificate file. Certificates with passphrases are not supported.
|
||||
- ``APNS_CA_CERTIFICATES``: Absolute path to a CA certificates file for APNS. Optional - do not set if not needed. Defaults to None.
|
||||
- ``GCM_API_KEY``: Your API key for GCM.
|
||||
- ``APNS_HOST``: The hostname used for the APNS sockets.
|
||||
- When ``DEBUG=True``, this defaults to ``gateway.sandbox.push.apple.com``.
|
||||
- When ``DEBUG=False``, this defaults to ``gateway.push.apple.com``.
|
||||
- ``APNS_PORT``: The port used along with APNS_HOST. Defaults to 2195.
|
||||
- ``GCM_POST_URL``: The full url that GCM notifications will be POSTed to. Defaults to https://android.googleapis.com/gcm/send.
|
||||
- ``GCM_MAX_RECIPIENTS``: The maximum amount of recipients that can be contained per bulk message. If the ``registration_ids`` list is larger than that number, multiple bulk messages will be sent. Defaults to 1000 (the maximum amount supported by GCM).
|
||||
- ``APNS_TOPIC``: The topic of the remote notification, which is typically the bundle ID for your app. If you omit this header and your APNs certificate does not specify multiple topics, the APNs server uses the certificate’s Subject as the default topic.
|
||||
- ``APNS_USE_ALTERNATIVE_PORT``: Use port 2197 for APNS, instead of default port 443.
|
||||
- ``APNS_USE_SANDBOX``: Use 'api.development.push.apple.com', instead of default host 'api.push.apple.com'.
|
||||
|
||||
**FCM/GCM settings**
|
||||
|
||||
- ``FCM_API_KEY``: Your API key for Firebase Cloud Messaging.
|
||||
- ``FCM_POST_URL``: The full url that FCM notifications will be POSTed to. Defaults to https://fcm.googleapis.com/fcm/send.
|
||||
- ``FCM_MAX_RECIPIENTS``: The maximum amount of recipients that can be contained per bulk message. If the ``registration_ids`` list is larger than that number, multiple bulk messages will be sent. Defaults to 1000 (the maximum amount supported by FCM).
|
||||
- ``FCM_ERROR_TIMEOUT``: The timeout on FCM POSTs.
|
||||
- ``GCM_API_KEY``, ``GCM_POST_URL``, ``GCM_MAX_RECIPIENTS``, ``GCM_ERROR_TIMEOUT``: Same parameters for GCM
|
||||
|
||||
**WNS settings**
|
||||
|
||||
- ``WNS_PACKAGE_SECURITY_KEY``: TODO
|
||||
- ``WNS_SECRET_KEY``: TODO
|
||||
|
||||
**WP settings**
|
||||
|
||||
- Install:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pip install pywebpush
|
||||
pip install py-vapid (Only for generating key)
|
||||
|
||||
- Getting keys:
|
||||
|
||||
- Create file (claim.json) like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
{
|
||||
"sub": "mailto: development@example.com",
|
||||
"aud": "https://android.googleapis.com"
|
||||
}
|
||||
|
||||
- Generate public and private keys:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vapid --sign claim.json
|
||||
|
||||
No private_key.pem file found.
|
||||
Do you want me to create one for you? (Y/n)Y
|
||||
Do you want me to create one for you? (Y/n)Y
|
||||
Generating private_key.pem
|
||||
Generating public_key.pem
|
||||
Include the following headers in your request:
|
||||
|
||||
Crypto-Key: p256ecdsa=BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70
|
||||
|
||||
Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2FuZHJvaWQuZ29vZ2xlYXBpcy5jb20iLCJleHAiOiIxNTA4NDkwODM2Iiwic3ViIjoibWFpbHRvOiBkZXZlbG9wbWVudEBleGFtcGxlLmNvbSJ9.r5CYMs86X3JZ4AEs76pXY5PxsnEhIFJ-0ckbibmFHZuyzfIpf1ZGIJbSI7knA4ufu7Hm8RFfEg5wWN1Yf-dR2A
|
||||
|
||||
- Generate client public key (applicationServerKey)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vapid --applicationServerKey
|
||||
|
||||
Application Server Key = BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70
|
||||
|
||||
|
||||
- Configure settings:
|
||||
|
||||
- ``WP_PRIVATE_KEY``: Absolute path to your private certificate file: os.path.join(BASE_DIR, "private_key.pem")
|
||||
- ``WP_CLAIMS``: Dictionary with the same sub info like claims file: {'sub': "mailto: development@example.com"}
|
||||
- ``WP_ERROR_TIMEOUT``: The timeout on WebPush POSTs. (Optional)
|
||||
- ``WP_POST_URL``: A dictionary (key per browser supported) with the full url that webpush notifications will be POSTed to. (Optional)
|
||||
|
||||
|
||||
- Configure client (javascript):
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// Utils functions:
|
||||
|
||||
function urlBase64ToUint8Array (base64String) {
|
||||
var padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||
var base64 = (base64String + padding)
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
|
||||
var rawData = window.atob(base64)
|
||||
var outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
for (var i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
function loadVersionBrowser (userAgent) {
|
||||
var ua = userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
||||
if (/trident/i.test(M[1])) {
|
||||
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
||||
return {name: 'IE', version: (tem[1] || '')};
|
||||
}
|
||||
if (M[1] === 'Chrome') {
|
||||
tem = ua.match(/\bOPR\/(\d+)/);
|
||||
if (tem != null) {
|
||||
return {name: 'Opera', version: tem[1]};
|
||||
}
|
||||
}
|
||||
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
|
||||
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
|
||||
M.splice(1, 1, tem[1]);
|
||||
}
|
||||
return {
|
||||
name: M[0],
|
||||
version: M[1]
|
||||
};
|
||||
};
|
||||
var applicationServerKey = "BEFuGfKKEFp-kEBMxAIw7ng8HeH_QwnH5_h55ijKD4FRvgdJU1GVlDo8K5U5ak4cMZdQTUJlkA34llWF0xHya70";
|
||||
....
|
||||
|
||||
// In your ready listener
|
||||
if ('serviceWorker' in navigator) {
|
||||
// The service worker has to store in the root of the app
|
||||
// http://stackoverflow.com/questions/29874068/navigator-serviceworker-is-never-ready
|
||||
var browser = loadVersionBrowser();
|
||||
navigator.serviceWorker.register('navigatorPush.service.js?version=1.0.0').then(function (reg) {
|
||||
reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(applicationServerKey)
|
||||
}).then(function (sub) {
|
||||
var endpointParts = sub.endpoint.split('/');
|
||||
var registration_id = endpointParts[endpointParts.length - 1];
|
||||
var data = {
|
||||
'browser': browser.name.toUpperCase(),
|
||||
'p256dh': btoa(String.fromCharCode.apply(null, new Uint8Array(sub.getKey('p256dh')))),
|
||||
'auth': btoa(String.fromCharCode.apply(null, new Uint8Array(sub.getKey('auth')))),
|
||||
'name': 'XXXXX',
|
||||
'registration_id': registration_id
|
||||
};
|
||||
requestPOSTToServer(data);
|
||||
})
|
||||
}).catch(function (err) {
|
||||
console.log(':^(', err);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// Example navigatorPush.service.js file
|
||||
|
||||
var getTitle = function (title) {
|
||||
if (title === "") {
|
||||
title = "TITLE DEFAULT";
|
||||
}
|
||||
return title;
|
||||
};
|
||||
var getNotificationOptions = function (message, message_tag) {
|
||||
var options = {
|
||||
body: message,
|
||||
icon: '/img/icon_120.png',
|
||||
tag: message_tag,
|
||||
vibrate: [200, 100, 200, 100, 200, 100, 200]
|
||||
};
|
||||
return options;
|
||||
};
|
||||
|
||||
self.addEventListener('install', function (event) {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('push', function(event) {
|
||||
try {
|
||||
// Push is a JSON
|
||||
var response_json = event.data.json();
|
||||
var title = response_json.title;
|
||||
var message = response_json.message;
|
||||
var message_tag = response_json.tag;
|
||||
} catch (err) {
|
||||
// Push is a simple text
|
||||
var title = "";
|
||||
var message = event.data.text();
|
||||
var message_tag = "";
|
||||
}
|
||||
self.registration.showNotification(getTitle(title), getNotificationOptions(message, message_tag));
|
||||
// Optional: Comunicating with our js application. Send a signal
|
||||
self.clients.matchAll({includeUncontrolled: true, type: 'window'}).then(function (clients) {
|
||||
clients.forEach(function (client) {
|
||||
client.postMessage({
|
||||
"data": message_tag,
|
||||
"data_title": title,
|
||||
"data_body": message});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Optional: Added to that the browser opens when you click on the notification push web.
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
// Android doesn't close the notification when you click it
|
||||
// See http://crbug.com/463146
|
||||
event.notification.close();
|
||||
// Check if there's already a tab open with this URL.
|
||||
// If yes: focus on the tab.
|
||||
// If no: open a tab with the URL.
|
||||
event.waitUntil(clients.matchAll({type: 'window', includeUncontrolled: true}).then(function(windowClients) {
|
||||
for (var i = 0; i < windowClients.length; i++) {
|
||||
var client = windowClients[i];
|
||||
if ('focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Sending messages
|
||||
----------------
|
||||
GCM and APNS services have slightly different semantics. The app tries to offer a common interface for both when using the models.
|
||||
FCM/GCM and APNS services have slightly different semantics. The app tries to offer a common interface for both when using the models.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -98,12 +319,16 @@ GCM and APNS services have slightly different semantics. The app tries to offer
|
|||
device = APNSDevice.objects.get(registration_id=apns_token)
|
||||
device.send_message("You've got mail") # Alert message may only be sent as text.
|
||||
device.send_message(None, badge=5) # No alerts but with badge.
|
||||
device.send_message(None, badge=1, extra={"foo": "bar"}) # Silent message with badge and added custom data.
|
||||
device.send_message(None, content_available=1, extra={"foo": "bar"}) # Silent message with custom data.
|
||||
# alert with title and body.
|
||||
device.send_message(message={"title" : "Game Request", "body" : "Bob wants to play poker"}, extra={"foo": "bar"})
|
||||
device.send_message("Hello again", thread_id="123", extra={"foo": "bar"}) # set thread-id to allow iOS to merge notifications
|
||||
|
||||
.. note::
|
||||
APNS does not support sending payloads that exceed 2048 bytes (increased from 256 in 2014).
|
||||
The message is only one part of the payload, if
|
||||
once constructed the payload exceeds the maximum size, an ``APNSDataOverflow`` exception will be raised before anything is sent.
|
||||
Reference: `Apple Payload Documentation <https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW1>`_
|
||||
|
||||
Sending messages in bulk
|
||||
------------------------
|
||||
|
@ -117,22 +342,71 @@ Sending messages in bulk
|
|||
Sending messages in bulk makes use of the bulk mechanics offered by GCM and APNS. It is almost always preferable to send
|
||||
bulk notifications instead of single ones.
|
||||
|
||||
Administration
|
||||
--------------
|
||||
It's also possible to pass badge parameter as a function which accepts token parameter in order to set different badge
|
||||
value per user. Assuming User model has a method get_badge returning badge count for a user:
|
||||
|
||||
APNS devices which are not receiving push notifications can be set to inactive by two methods. The web admin interface for
|
||||
APNS devices has a "prune devices" option. Any selected devices which are not receiving notifications will be set to inactive [1]_.
|
||||
There is also a management command to prune all devices failing to receive notifications:
|
||||
.. code-block:: python
|
||||
|
||||
.. code-block:: shell
|
||||
devices.send_message(
|
||||
"Happy name day!",
|
||||
badge=lambda token: APNSDevice.objects.get(registration_id=token).user.get_badge()
|
||||
)
|
||||
|
||||
$ python manage.py prune_devices
|
||||
Firebase vs Google Cloud Messaging
|
||||
----------------------------------
|
||||
|
||||
This removes all devices which are not receiving notifications.
|
||||
``django-push-notifications`` supports both Google Cloud Messaging and Firebase Cloud Messaging (which is now the officially supported messaging platform from Google). When registering a device, you must pass the ``cloud_message_type`` parameter to set the cloud type that matches the device needs.
|
||||
This is currently defaulting to ``'GCM'``, but may change to ``'FCM'`` at some point. You are encouraged to use the `officially supported library <https://developers.google.com/cloud-messaging/faq>`_.
|
||||
|
||||
For more information, please refer to the APNS feedback service_.
|
||||
When using FCM, ``django-push-notifications`` will automatically use the `notification and data messages format <https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages>`_ to be conveniently handled by Firebase devices. You may want to check the payload to see if it matches your needs, and review your notification statuses in `FCM Diagnostic console <https://support.google.com/googleplay/android-developer/answer/2663268?hl=en>`_.
|
||||
|
||||
.. _service: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create a FCM device
|
||||
fcm_device = GCMDevice.objects.create(registration_id="token", cloud_message_type="FCM", user=the_user)
|
||||
|
||||
# Send a notification message
|
||||
fcm_device.send_message("This is a message")
|
||||
|
||||
# Send a notification message with additionnal payload
|
||||
fcm_device.send_message("This is a enriched message", extra={"title": "Notification title", "icon": "icon_ressource"})
|
||||
|
||||
# Send a notification message with additionnal payload (alternative syntax)
|
||||
fcm_device.send_message("This is a enriched message", title="Notification title", badge=6)
|
||||
|
||||
# Send a notification message with extra data
|
||||
fcm_device.send_message("This is a message with data", extra={"other": "content", "misc": "data"})
|
||||
|
||||
# Send a notification message with options
|
||||
fcm_device.send_message("This is a message", time_to_live=3600)
|
||||
|
||||
# Send a data message only
|
||||
fcm_device.send_message(None, extra={"other": "content", "misc": "data"})
|
||||
|
||||
You can disable this default behaviour by setting ``use_fcm_notifications`` to ``False``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fcm_device = GCMDevice.objects.create(registration_id="token", cloud_message_type="FCM", user=the_user)
|
||||
|
||||
# Send a data message with classic format
|
||||
fcm_device.send_message("This is a message", use_fcm_notifications=False)
|
||||
|
||||
|
||||
Sending FCM/GCM messages to topic members
|
||||
-----------------------------------------
|
||||
FCM/GCM topic messaging allows your app server to send a message to multiple devices that have opted in to a particular topic. Based on the publish/subscribe model, topic messaging supports unlimited subscriptions per app. Developers can choose any topic name that matches the regular expression, "/topics/[a-zA-Z0-9-_.~%]+".
|
||||
Note: gcm_send_bulk_message must be used when sending messages to topic subscribers, and setting the first param to any value other than None will result in a 400 Http error.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from push_notifications.gcm import send_message
|
||||
|
||||
# First param is "None" because no Registration_id is needed, the message will be sent to all devices subscribed to the topic.
|
||||
send_message(None, {"body": "Hello members of my_topic!"}, to="/topics/my_topic")
|
||||
|
||||
Reference: `FCM Documentation <https://firebase.google.com/docs/cloud-messaging/android/topic-messaging>`_
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
@ -142,24 +416,6 @@ Exceptions
|
|||
- ``apns.APNSError(NotificationError)``: Something went wrong upon sending APNS notifications.
|
||||
- ``apns.APNSDataOverflow(APNSError)``: The APNS payload exceeds its maximum size and cannot be sent.
|
||||
|
||||
Tastypie support
|
||||
----------------
|
||||
|
||||
The app includes tastypie-compatible resources in push_notifications.api.tastypie. These can be used as-is, or as base classes
|
||||
for more involved APIs.
|
||||
The following resources are available:
|
||||
|
||||
- ``APNSDeviceResource``
|
||||
- ``GCMDeviceResource``
|
||||
- ``APNSDeviceAuthenticatedResource``
|
||||
- ``GCMDeviceAuthenticatedResource``
|
||||
|
||||
The base device resources will not ask for authentication, while the authenticated ones will link the logged in user to
|
||||
the device they register.
|
||||
Subclassing the authenticated resources in order to add a ``SameUserAuthentication`` and a user ``ForeignKey`` is recommended.
|
||||
|
||||
When registered, the APIs will show up at ``<api_root>/device/apns`` and ``<api_root>/device/gcm``, respectively.
|
||||
|
||||
Django REST Framework (DRF) support
|
||||
-----------------------------------
|
||||
|
||||
|
@ -210,10 +466,17 @@ Routes can be added one of two ways:
|
|||
# ...
|
||||
)
|
||||
|
||||
Update of device with duplicate registration ID
|
||||
-----------------------------------------------
|
||||
|
||||
Python 3 support
|
||||
----------------
|
||||
The DRF viewset enforce the uniqueness of the registration ID. In same use case it
|
||||
may cause issue: If an already registered mobile change its user and it will
|
||||
fail to register because the registration ID already exist.
|
||||
|
||||
When option ``UPDATE_ON_DUPLICATE_REG_ID`` is set to True, then any creation of
|
||||
device with an already existing registration ID will be transformed into an update.
|
||||
|
||||
The ``UPDATE_ON_DUPLICATE_REG_ID`` only works with DRF.
|
||||
|
||||
``django-push-notifications`` is fully compatible with Python 3.4 & 3.5
|
||||
|
||||
.. [1] Any devices which are not selected, but are not receiving notifications will not be deactivated on a subsequent call to "prune devices" unless another attempt to send a message to the device fails after the call to the feedback service.
|
||||
|
|
|
@ -9,11 +9,13 @@ Build-Depends: debhelper (>= 9),
|
|||
python3-django,
|
||||
python3-djangorestframework,
|
||||
python3-mock,
|
||||
python3-pywebpush,
|
||||
python3-setuptools,
|
||||
python-all,
|
||||
python-django,
|
||||
python-djangorestframework,
|
||||
python-mock,
|
||||
python-pywebpush,
|
||||
python-setuptools
|
||||
Standards-Version: 3.9.8
|
||||
X-Python3-Version: >= 3.4
|
||||
|
|
|
@ -9,4 +9,3 @@ export PYBUILD_NAME=django-push-notifications
|
|||
dh $@ --with python2,python3 --buildsystem=pybuild
|
||||
|
||||
override_dh_auto_test:
|
||||
dh_auto_test -- --system=custom --test-args="{interpreter} tests/runtests.py"
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
|
||||
__author__ = "Jerome Leclanche"
|
||||
__email__ = "jerome@leclan.ch"
|
||||
__version__ = "1.4.1"
|
||||
import pkg_resources
|
||||
|
||||
|
||||
class NotificationError(Exception):
|
||||
pass
|
||||
__version__ = pkg_resources.require("django-push-notifications")[0].version
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
from django.apps import apps
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .gcm import GCMError
|
||||
from .models import APNSDevice, GCMDevice, get_expired_tokens
|
||||
from .models import GCMDevice, WebPushDevice, WNSDevice
|
||||
from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
from .webpush import WebPushError
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
User = apps.get_model(*SETTINGS["USER_MODEL"].split("."))
|
||||
|
||||
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
list_display = ("__str__", "device_id", "user", "active", "date_created")
|
||||
search_fields = ("name", "device_id", "user__%s" % (User.USERNAME_FIELD))
|
||||
list_filter = ("active", )
|
||||
actions = ("send_message", "send_bulk_message", "prune_devices", "enable", "disable")
|
||||
list_filter = ("active",)
|
||||
actions = ("send_message", "send_bulk_message", "enable", "disable")
|
||||
raw_id_fields = ("user",)
|
||||
|
||||
if hasattr(User, "USERNAME_FIELD"):
|
||||
search_fields = ("name", "device_id", "user__%s" % (User.USERNAME_FIELD))
|
||||
else:
|
||||
search_fields = ("name", "device_id")
|
||||
|
||||
def send_messages(self, request, queryset, bulk=False):
|
||||
"""
|
||||
|
@ -32,15 +40,43 @@ class DeviceAdmin(admin.ModelAdmin):
|
|||
ret.append(r)
|
||||
except GCMError as e:
|
||||
errors.append(str(e))
|
||||
except WebPushError as e:
|
||||
errors.append(e.message)
|
||||
|
||||
if bulk:
|
||||
break
|
||||
|
||||
# Because NotRegistered and InvalidRegistration do not throw GCMError
|
||||
# catch them here to display error msg.
|
||||
if not bulk:
|
||||
for r in ret:
|
||||
if "error" in r["results"][0]:
|
||||
errors.append(r["results"][0]["error"])
|
||||
else:
|
||||
try:
|
||||
errors = [r["error"] for r in ret[0][0]["results"] if "error" in r]
|
||||
except TypeError:
|
||||
for entry in ret[0][0]:
|
||||
errors = errors + [r["error"] for r in entry["results"] if "error" in r]
|
||||
if errors:
|
||||
self.message_user(request, _("Some messages could not be processed: %r" % (", ".join(errors))), level=messages.ERROR)
|
||||
self.message_user(
|
||||
request, _("Some messages could not be processed: %r" % (", ".join(errors))),
|
||||
level=messages.ERROR
|
||||
)
|
||||
if ret:
|
||||
if not bulk:
|
||||
ret = ", ".join(ret)
|
||||
if bulk:
|
||||
# When the queryset exceeds the max_recipients value, the
|
||||
# send_message method returns a list of dicts, one per chunk
|
||||
try:
|
||||
success = ret[0][0]["success"]
|
||||
except TypeError:
|
||||
success = 0
|
||||
for entry in ret[0][0]:
|
||||
success = success + entry["success"]
|
||||
if success == 0:
|
||||
return
|
||||
elif len(errors) == len(ret):
|
||||
return
|
||||
if errors:
|
||||
msg = _("Some messages were sent: %s" % (ret))
|
||||
else:
|
||||
|
@ -49,33 +85,32 @@ class DeviceAdmin(admin.ModelAdmin):
|
|||
|
||||
def send_message(self, request, queryset):
|
||||
self.send_messages(request, queryset)
|
||||
|
||||
send_message.short_description = _("Send test message")
|
||||
|
||||
def send_bulk_message(self, request, queryset):
|
||||
self.send_messages(request, queryset, True)
|
||||
|
||||
send_bulk_message.short_description = _("Send test message in bulk")
|
||||
|
||||
def enable(self, request, queryset):
|
||||
queryset.update(active=True)
|
||||
|
||||
enable.short_description = _("Enable selected devices")
|
||||
|
||||
def disable(self, request, queryset):
|
||||
queryset.update(active=False)
|
||||
|
||||
disable.short_description = _("Disable selected devices")
|
||||
|
||||
def prune_devices(self, request, queryset):
|
||||
# Note that when get_expired_tokens() is called, Apple's
|
||||
# feedback service resets, so, calling it again won't return
|
||||
# the device again (unless a message is sent to it again). So,
|
||||
# if the user doesn't select all the devices for pruning, we
|
||||
# could very easily leave an expired device as active. Maybe
|
||||
# this is just a bad API.
|
||||
expired = get_expired_tokens()
|
||||
devices = queryset.filter(registration_id__in=expired)
|
||||
for d in devices:
|
||||
d.active = False
|
||||
d.save()
|
||||
|
||||
class GCMDeviceAdmin(DeviceAdmin):
|
||||
list_display = (
|
||||
"__str__", "device_id", "user", "active", "date_created", "cloud_message_type"
|
||||
)
|
||||
list_filter = ("active", "cloud_message_type")
|
||||
|
||||
|
||||
admin.site.register(APNSDevice, DeviceAdmin)
|
||||
admin.site.register(GCMDevice, DeviceAdmin)
|
||||
admin.site.register(GCMDevice, GCMDeviceAdmin)
|
||||
admin.site.register(WNSDevice, DeviceAdmin)
|
||||
admin.site.register(WebPushDevice, DeviceAdmin)
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
if "tastypie" in settings.INSTALLED_APPS:
|
||||
# Tastypie resources are importable from the api package level (backwards compatibility)
|
||||
from .tastypie import APNSDeviceResource, GCMDeviceResource, APNSDeviceAuthenticatedResource, GCMDeviceAuthenticatedResource
|
||||
|
||||
__all__ = [
|
||||
"APNSDeviceResource",
|
||||
"GCMDeviceResource",
|
||||
"APNSDeviceAuthenticatedResource",
|
||||
"GCMDeviceAuthenticatedResource"
|
||||
]
|
|
@ -1,18 +1,17 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from rest_framework import permissions
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.fields import IntegerField
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, Serializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from ..fields import UNSIGNED_64BIT_INT_MAX_VALUE, hex_re
|
||||
from ..models import GCMDevice, WebPushDevice, WNSDevice
|
||||
from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
|
||||
from push_notifications.models import APNSDevice, GCMDevice
|
||||
from push_notifications.fields import hex_re
|
||||
from push_notifications.fields import UNSIGNED_64BIT_INT_MAX_VALUE
|
||||
|
||||
# Fields
|
||||
|
||||
|
||||
class HexIntegerField(IntegerField):
|
||||
"""
|
||||
Store an integer represented as a hex string of form "0x01".
|
||||
|
@ -22,7 +21,7 @@ class HexIntegerField(IntegerField):
|
|||
# validate hex string and convert it to the unsigned
|
||||
# integer representation for internal use
|
||||
try:
|
||||
data = int(data, 16)
|
||||
data = int(data, 16) if type(data) != int else data
|
||||
except ValueError:
|
||||
raise ValidationError("Device ID is not a valid hex number")
|
||||
return super(HexIntegerField, self).to_internal_value(data)
|
||||
|
@ -34,47 +33,63 @@ class HexIntegerField(IntegerField):
|
|||
# Serializers
|
||||
class DeviceSerializerMixin(ModelSerializer):
|
||||
class Meta:
|
||||
fields = ("name", "registration_id", "device_id", "active", "date_created")
|
||||
read_only_fields = ("date_created", )
|
||||
fields = (
|
||||
"id", "name", "application_id", "registration_id", "device_id",
|
||||
"active", "date_created"
|
||||
)
|
||||
read_only_fields = ("date_created",)
|
||||
|
||||
# See https://github.com/tomchristie/django-rest-framework/issues/1101
|
||||
extra_kwargs = {"active": {"default": True}}
|
||||
|
||||
|
||||
class APNSDeviceSerializer(ModelSerializer):
|
||||
class UniqueRegistrationSerializerMixin(Serializer):
|
||||
def validate(self, attrs):
|
||||
devices = None
|
||||
primary_key = None
|
||||
request_method = None
|
||||
|
||||
class Meta(DeviceSerializerMixin.Meta):
|
||||
model = APNSDevice
|
||||
if self.initial_data.get("registration_id", None):
|
||||
if self.instance:
|
||||
request_method = "update"
|
||||
primary_key = self.instance.id
|
||||
else:
|
||||
request_method = "create"
|
||||
else:
|
||||
if self.context["request"].method in ["PUT", "PATCH"]:
|
||||
request_method = "update"
|
||||
primary_key = self.instance.id
|
||||
elif self.context["request"].method == "POST":
|
||||
request_method = "create"
|
||||
|
||||
def validate_registration_id(self, value):
|
||||
# iOS device tokens are 256-bit hexadecimal (64 characters). In 2016 Apple is increasing
|
||||
# iOS device tokens to 100 bytes hexadecimal (200 characters).
|
||||
Device = self.Meta.model
|
||||
if request_method == "update":
|
||||
reg_id = attrs.get("registration_id", self.instance.registration_id)
|
||||
devices = Device.objects.filter(registration_id=reg_id) \
|
||||
.exclude(id=primary_key)
|
||||
elif request_method == "create":
|
||||
devices = Device.objects.filter(registration_id=attrs["registration_id"])
|
||||
|
||||
if hex_re.match(value) is None or len(value) not in (64, 200):
|
||||
raise ValidationError("Registration ID (device token) is invalid")
|
||||
|
||||
return value
|
||||
if devices:
|
||||
raise ValidationError({"registration_id": "This field must be unique."})
|
||||
return attrs
|
||||
|
||||
|
||||
class GCMDeviceSerializer(ModelSerializer):
|
||||
class GCMDeviceSerializer(UniqueRegistrationSerializerMixin, ModelSerializer):
|
||||
device_id = HexIntegerField(
|
||||
help_text="ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)",
|
||||
style={'input_type': 'text'},
|
||||
required=False
|
||||
style={"input_type": "text"},
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta(DeviceSerializerMixin.Meta):
|
||||
model = GCMDevice
|
||||
|
||||
extra_kwargs = {
|
||||
# Work around an issue with validating the uniqueness of
|
||||
# registration ids of up to 4k
|
||||
'registration_id': {
|
||||
'validators': [
|
||||
UniqueValidator(queryset=GCMDevice.objects.all())
|
||||
]
|
||||
}
|
||||
}
|
||||
fields = (
|
||||
"id", "name", "registration_id", "device_id", "active", "date_created",
|
||||
"cloud_message_type", "application_id",
|
||||
)
|
||||
extra_kwargs = {"id": {"read_only": False, "required": False}}
|
||||
|
||||
def validate_device_id(self, value):
|
||||
# device ids are 64 bit unsigned values
|
||||
|
@ -83,6 +98,20 @@ class GCMDeviceSerializer(ModelSerializer):
|
|||
return value
|
||||
|
||||
|
||||
class WNSDeviceSerializer(UniqueRegistrationSerializerMixin, ModelSerializer):
|
||||
class Meta(DeviceSerializerMixin.Meta):
|
||||
model = WNSDevice
|
||||
|
||||
|
||||
class WebPushDeviceSerializer(UniqueRegistrationSerializerMixin, ModelSerializer):
|
||||
class Meta(DeviceSerializerMixin.Meta):
|
||||
model = WebPushDevice
|
||||
fields = (
|
||||
"id", "name", "registration_id", "active", "date_created",
|
||||
"p256dh", "auth", "browser", "application_id",
|
||||
)
|
||||
|
||||
|
||||
# Permissions
|
||||
class IsOwner(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
|
@ -94,11 +123,38 @@ class IsOwner(permissions.BasePermission):
|
|||
class DeviceViewSetMixin(object):
|
||||
lookup_field = "registration_id"
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = None
|
||||
is_update = False
|
||||
if SETTINGS.get("UPDATE_ON_DUPLICATE_REG_ID") and "registration_id" in request.data:
|
||||
instance = self.queryset.model.objects.filter(
|
||||
registration_id=request.data["registration_id"]
|
||||
).first()
|
||||
if instance:
|
||||
serializer = self.get_serializer(instance, data=request.data)
|
||||
is_update = True
|
||||
if not serializer:
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
if is_update:
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if self.request.user.is_authenticated():
|
||||
if self.request.user.is_authenticated:
|
||||
serializer.save(user=self.request.user)
|
||||
return super(DeviceViewSetMixin, self).perform_create(serializer)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
if self.request.user.is_authenticated:
|
||||
serializer.save(user=self.request.user)
|
||||
return super(DeviceViewSetMixin, self).perform_update(serializer)
|
||||
|
||||
|
||||
class AuthorizedMixin(object):
|
||||
permission_classes = (permissions.IsAuthenticated, IsOwner)
|
||||
|
@ -109,15 +165,6 @@ class AuthorizedMixin(object):
|
|||
|
||||
|
||||
# ViewSets
|
||||
class APNSDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
|
||||
queryset = APNSDevice.objects.all()
|
||||
serializer_class = APNSDeviceSerializer
|
||||
|
||||
|
||||
class APNSDeviceAuthorizedViewSet(AuthorizedMixin, APNSDeviceViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class GCMDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
|
||||
queryset = GCMDevice.objects.all()
|
||||
serializer_class = GCMDeviceSerializer
|
||||
|
@ -125,3 +172,21 @@ class GCMDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
|
|||
|
||||
class GCMDeviceAuthorizedViewSet(AuthorizedMixin, GCMDeviceViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class WNSDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
|
||||
queryset = WNSDevice.objects.all()
|
||||
serializer_class = WNSDeviceSerializer
|
||||
|
||||
|
||||
class WNSDeviceAuthorizedViewSet(AuthorizedMixin, WNSDeviceViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class WebPushDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
|
||||
queryset = WebPushDevice.objects.all()
|
||||
serializer_class = WebPushDeviceSerializer
|
||||
|
||||
|
||||
class WebPushDeviceAuthorizedViewSet(AuthorizedMixin, WebPushDeviceViewSet):
|
||||
pass
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
from tastypie.authorization import Authorization
|
||||
from tastypie.authentication import BasicAuthentication
|
||||
from tastypie.resources import ModelResource
|
||||
from push_notifications.models import APNSDevice, GCMDevice
|
||||
|
||||
|
||||
class APNSDeviceResource(ModelResource):
|
||||
class Meta:
|
||||
authorization = Authorization()
|
||||
queryset = APNSDevice.objects.all()
|
||||
resource_name = "device/apns"
|
||||
|
||||
|
||||
class GCMDeviceResource(ModelResource):
|
||||
class Meta:
|
||||
authorization = Authorization()
|
||||
queryset = GCMDevice.objects.all()
|
||||
resource_name = "device/gcm"
|
||||
|
||||
|
||||
class APNSDeviceAuthenticatedResource(APNSDeviceResource):
|
||||
# user = ForeignKey(UserResource, "user")
|
||||
|
||||
class Meta(APNSDeviceResource.Meta):
|
||||
authentication = BasicAuthentication()
|
||||
# authorization = SameUserAuthorization()
|
||||
|
||||
def obj_create(self, bundle, **kwargs):
|
||||
# See https://github.com/toastdriven/django-tastypie/issues/854
|
||||
return super(APNSDeviceAuthenticatedResource, self).obj_create(bundle, user=bundle.request.user, **kwargs)
|
||||
|
||||
|
||||
class GCMDeviceAuthenticatedResource(GCMDeviceResource):
|
||||
# user = ForeignKey(UserResource, "user")
|
||||
|
||||
class Meta(GCMDeviceResource.Meta):
|
||||
authentication = BasicAuthentication()
|
||||
# authorization = SameUserAuthorization()
|
||||
|
||||
def obj_create(self, bundle, **kwargs):
|
||||
# See https://github.com/toastdriven/django-tastypie/issues/854
|
||||
return super(GCMDeviceAuthenticatedResource, self).obj_create(bundle, user=bundle.request.user, **kwargs)
|
|
@ -1,238 +0,0 @@
|
|||
"""
|
||||
Apple Push Notification Service
|
||||
Documentation is available on the iOS Developer Library:
|
||||
https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import json
|
||||
import ssl
|
||||
import struct
|
||||
import socket
|
||||
import time
|
||||
from contextlib import closing
|
||||
from binascii import unhexlify
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from . import NotificationError
|
||||
from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
|
||||
|
||||
class APNSError(NotificationError):
|
||||
pass
|
||||
|
||||
|
||||
class APNSServerError(APNSError):
|
||||
def __init__(self, status, identifier):
|
||||
super(APNSServerError, self).__init__(status, identifier)
|
||||
self.status = status
|
||||
self.identifier = identifier
|
||||
|
||||
|
||||
class APNSDataOverflow(APNSError):
|
||||
pass
|
||||
|
||||
|
||||
def _apns_create_socket(address_tuple):
|
||||
certfile = SETTINGS.get("APNS_CERTIFICATE")
|
||||
if not certfile:
|
||||
raise ImproperlyConfigured(
|
||||
'You need to set PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] to send messages through APNS.'
|
||||
)
|
||||
|
||||
try:
|
||||
with open(certfile, "r") as f:
|
||||
f.read()
|
||||
except Exception as e:
|
||||
raise ImproperlyConfigured("The APNS certificate file at %r is not readable: %s" % (certfile, e))
|
||||
|
||||
ca_certs = SETTINGS.get("APNS_CA_CERTIFICATES")
|
||||
|
||||
sock = socket.socket()
|
||||
sock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1, certfile=certfile, ca_certs=ca_certs)
|
||||
sock.connect(address_tuple)
|
||||
|
||||
return sock
|
||||
|
||||
|
||||
def _apns_create_socket_to_push():
|
||||
return _apns_create_socket((SETTINGS["APNS_HOST"], SETTINGS["APNS_PORT"]))
|
||||
|
||||
|
||||
def _apns_create_socket_to_feedback():
|
||||
return _apns_create_socket((SETTINGS["APNS_FEEDBACK_HOST"], SETTINGS["APNS_FEEDBACK_PORT"]))
|
||||
|
||||
|
||||
def _apns_pack_frame(token_hex, payload, identifier, expiration, priority):
|
||||
token = unhexlify(token_hex)
|
||||
# |COMMAND|FRAME-LEN|{token}|{payload}|{id:4}|{expiration:4}|{priority:1}
|
||||
frame_len = 3 * 5 + len(token) + len(payload) + 4 + 4 + 1 # 5 items, each 3 bytes prefix, then each item length
|
||||
frame_fmt = "!BIBH%ssBH%ssBHIBHIBHB" % (len(token), len(payload))
|
||||
frame = struct.pack(
|
||||
frame_fmt,
|
||||
2, frame_len,
|
||||
1, len(token), token,
|
||||
2, len(payload), payload,
|
||||
3, 4, identifier,
|
||||
4, 4, expiration,
|
||||
5, 1, priority)
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
def _apns_check_errors(sock):
|
||||
timeout = SETTINGS["APNS_ERROR_TIMEOUT"]
|
||||
if timeout is None:
|
||||
return # assume everything went fine!
|
||||
saved_timeout = sock.gettimeout()
|
||||
try:
|
||||
sock.settimeout(timeout)
|
||||
data = sock.recv(6)
|
||||
if data:
|
||||
command, status, identifier = struct.unpack("!BBI", data)
|
||||
# apple protocol says command is always 8. See http://goo.gl/ENUjXg
|
||||
assert command == 8, "Command must be 8!"
|
||||
if status != 0:
|
||||
raise APNSServerError(status, identifier)
|
||||
except socket.timeout: # py3, see http://bugs.python.org/issue10272
|
||||
pass
|
||||
except ssl.SSLError as e: # py2
|
||||
if "timed out" not in e.message:
|
||||
raise
|
||||
finally:
|
||||
sock.settimeout(saved_timeout)
|
||||
|
||||
|
||||
def _apns_send(token, alert, badge=None, sound=None, category=None, content_available=False,
|
||||
action_loc_key=None, loc_key=None, loc_args=[], extra={}, identifier=0,
|
||||
expiration=None, priority=10, socket=None):
|
||||
data = {}
|
||||
aps_data = {}
|
||||
|
||||
if action_loc_key or loc_key or loc_args:
|
||||
alert = {"body": alert} if alert else {}
|
||||
if action_loc_key:
|
||||
alert["action-loc-key"] = action_loc_key
|
||||
if loc_key:
|
||||
alert["loc-key"] = loc_key
|
||||
if loc_args:
|
||||
alert["loc-args"] = loc_args
|
||||
|
||||
if alert is not None:
|
||||
aps_data["alert"] = alert
|
||||
|
||||
if badge is not None:
|
||||
aps_data["badge"] = badge
|
||||
|
||||
if sound is not None:
|
||||
aps_data["sound"] = sound
|
||||
|
||||
if category is not None:
|
||||
aps_data["category"] = category
|
||||
|
||||
if content_available:
|
||||
aps_data["content-available"] = 1
|
||||
|
||||
data["aps"] = aps_data
|
||||
data.update(extra)
|
||||
|
||||
# convert to json, avoiding unnecessary whitespace with separators (keys sorted for tests)
|
||||
json_data = json.dumps(data, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||
|
||||
max_size = SETTINGS["APNS_MAX_NOTIFICATION_SIZE"]
|
||||
if len(json_data) > max_size:
|
||||
raise APNSDataOverflow("Notification body cannot exceed %i bytes" % (max_size))
|
||||
|
||||
# if expiration isn't specified use 1 month from now
|
||||
expiration_time = expiration if expiration is not None else int(time.time()) + 2592000
|
||||
|
||||
frame = _apns_pack_frame(token, json_data, identifier, expiration_time, priority)
|
||||
|
||||
if socket:
|
||||
socket.write(frame)
|
||||
else:
|
||||
with closing(_apns_create_socket_to_push()) as socket:
|
||||
socket.write(frame)
|
||||
_apns_check_errors(socket)
|
||||
|
||||
|
||||
def _apns_read_and_unpack(socket, data_format):
|
||||
length = struct.calcsize(data_format)
|
||||
data = socket.recv(length)
|
||||
if data:
|
||||
return struct.unpack_from(data_format, data, 0)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _apns_receive_feedback(socket):
|
||||
expired_token_list = []
|
||||
|
||||
# read a timestamp (4 bytes) and device token length (2 bytes)
|
||||
header_format = '!LH'
|
||||
has_data = True
|
||||
while has_data:
|
||||
try:
|
||||
# read the header tuple
|
||||
header_data = _apns_read_and_unpack(socket, header_format)
|
||||
if header_data is not None:
|
||||
timestamp, token_length = header_data
|
||||
# Unpack format for a single value of length bytes
|
||||
token_format = '%ss' % token_length
|
||||
device_token = _apns_read_and_unpack(socket, token_format)
|
||||
if device_token is not None:
|
||||
# _apns_read_and_unpack returns a tuple, but
|
||||
# it's just one item, so get the first.
|
||||
expired_token_list.append((timestamp, device_token[0]))
|
||||
else:
|
||||
has_data = False
|
||||
except socket.timeout: # py3, see http://bugs.python.org/issue10272
|
||||
pass
|
||||
except ssl.SSLError as e: # py2
|
||||
if "timed out" not in e.message:
|
||||
raise
|
||||
|
||||
return expired_token_list
|
||||
|
||||
|
||||
def apns_send_message(registration_id, alert, **kwargs):
|
||||
"""
|
||||
Sends an APNS notification to a single registration_id.
|
||||
This will send the notification as form data.
|
||||
If sending multiple notifications, it is more efficient to use
|
||||
apns_send_bulk_message()
|
||||
|
||||
Note that if set alert should always be a string. If it is not set,
|
||||
it won't be included in the notification. You will need to pass None
|
||||
to this for silent notifications.
|
||||
"""
|
||||
|
||||
_apns_send(registration_id, alert, **kwargs)
|
||||
|
||||
|
||||
def apns_send_bulk_message(registration_ids, alert, **kwargs):
|
||||
"""
|
||||
Sends an APNS notification to one or more registration_ids.
|
||||
The registration_ids argument needs to be a list.
|
||||
|
||||
Note that if set alert should always be a string. If it is not set,
|
||||
it won't be included in the notification. You will need to pass None
|
||||
to this for silent notifications.
|
||||
"""
|
||||
with closing(_apns_create_socket_to_push()) as socket:
|
||||
for identifier, registration_id in enumerate(registration_ids):
|
||||
_apns_send(registration_id, alert, identifier=identifier, socket=socket, **kwargs)
|
||||
_apns_check_errors(socket)
|
||||
|
||||
|
||||
def apns_fetch_inactive_ids():
|
||||
"""
|
||||
Queries the APNS server for id's that are no longer active since
|
||||
the last fetch
|
||||
"""
|
||||
with closing(_apns_create_socket_to_feedback()) as socket:
|
||||
inactive_ids = []
|
||||
# Maybe we should have a flag to return the timestamp?
|
||||
# It doesn't seem that useful right now, though.
|
||||
for tStamp, registration_id in _apns_receive_feedback(socket):
|
||||
inactive_ids.append(codecs.encode(registration_id, 'hex_codec'))
|
||||
return inactive_ids
|
|
@ -0,0 +1,10 @@
|
|||
# flake8:noqa
|
||||
|
||||
try:
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
except ImportError:
|
||||
# Python 2 support
|
||||
from urllib2 import HTTPError, Request, urlopen
|
||||
from urllib import urlencode
|
|
@ -0,0 +1,21 @@
|
|||
from django.utils.module_loading import import_string
|
||||
from .app import AppConfig # noqa: F401
|
||||
from .appmodel import AppModelConfig # noqa: F401
|
||||
from .legacy import LegacyConfig # noqa: F401
|
||||
from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
|
||||
|
||||
manager = None
|
||||
|
||||
|
||||
def get_manager(reload=False):
|
||||
global manager
|
||||
|
||||
if not manager or reload is True:
|
||||
manager = import_string(SETTINGS["CONFIG"])()
|
||||
|
||||
return manager
|
||||
|
||||
|
||||
# implementing get_manager as a function allows tests to reload settings
|
||||
get_manager()
|
|
@ -0,0 +1,260 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.six import string_types
|
||||
|
||||
from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
from .base import BaseConfig
|
||||
|
||||
|
||||
SETTING_MISMATCH = (
|
||||
"Application '{application_id}' ({platform}) does not support the setting '{setting}'."
|
||||
)
|
||||
|
||||
# code can be "missing" or "invalid"
|
||||
BAD_PLATFORM = (
|
||||
'PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS["{application_id}"]["PLATFORM"] is {code}. '
|
||||
"Must be one of: {platforms}."
|
||||
)
|
||||
|
||||
UNKNOWN_PLATFORM = (
|
||||
"Unknown Platform: {platform}. Must be one of: {platforms}."
|
||||
)
|
||||
|
||||
MISSING_SETTING = (
|
||||
'PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS["{application_id}"]["{setting}"] is missing.'
|
||||
)
|
||||
|
||||
PLATFORMS = [
|
||||
"FCM",
|
||||
"GCM",
|
||||
"WNS",
|
||||
"WP",
|
||||
]
|
||||
|
||||
# Settings that all applications must have
|
||||
REQUIRED_SETTINGS = [
|
||||
"PLATFORM",
|
||||
]
|
||||
|
||||
# Settings that an application may have to enable optional features
|
||||
# these settings are stubs for registry support and have no effect on the operation
|
||||
# of the application at this time.
|
||||
OPTIONAL_SETTINGS = [
|
||||
"APPLICATION_GROUP", "APPLICATION_SECRET"
|
||||
]
|
||||
|
||||
FCM_REQUIRED_SETTINGS = GCM_REQUIRED_SETTINGS = ["API_KEY"]
|
||||
FCM_OPTIONAL_SETTINGS = GCM_OPTIONAL_SETTINGS = [
|
||||
"POST_URL", "MAX_RECIPIENTS", "ERROR_TIMEOUT"
|
||||
]
|
||||
|
||||
WNS_REQUIRED_SETTINGS = ["PACKAGE_SECURITY_ID", "SECRET_KEY"]
|
||||
WNS_OPTIONAL_SETTINGS = ["WNS_ACCESS_URL"]
|
||||
|
||||
WP_REQUIRED_SETTINGS = ["PRIVATE_KEY", "CLAIMS"]
|
||||
WP_OPTIONAL_SETTINGS = ["ERROR_TIMEOUT", "POST_URL"]
|
||||
|
||||
|
||||
class AppConfig(BaseConfig):
|
||||
"""
|
||||
Supports any number of push notification enabled applications.
|
||||
"""
|
||||
|
||||
def __init__(self, settings=None):
|
||||
# supports overriding the settings to be loaded. Will load from ..settings by default.
|
||||
self._settings = settings or SETTINGS
|
||||
|
||||
# initialize APPLICATIONS to an empty collection
|
||||
self._settings.setdefault("APPLICATIONS", {})
|
||||
|
||||
# validate application configurations
|
||||
self._validate_applications(self._settings["APPLICATIONS"])
|
||||
|
||||
def _validate_applications(self, apps):
|
||||
"""Validate the application collection"""
|
||||
for application_id, application_config in apps.items():
|
||||
self._validate_config(application_id, application_config)
|
||||
|
||||
application_config["APPLICATION_ID"] = application_id
|
||||
|
||||
def _validate_config(self, application_id, application_config):
|
||||
platform = application_config.get("PLATFORM", None)
|
||||
|
||||
# platform is not present
|
||||
if platform is None:
|
||||
raise ImproperlyConfigured(
|
||||
BAD_PLATFORM.format(
|
||||
application_id=application_id,
|
||||
code="required",
|
||||
platforms=", ".join(PLATFORMS)
|
||||
)
|
||||
)
|
||||
|
||||
# platform is not a valid choice from PLATFORMS
|
||||
if platform not in PLATFORMS:
|
||||
raise ImproperlyConfigured(
|
||||
BAD_PLATFORM.format(
|
||||
application_id=application_id,
|
||||
code="invalid",
|
||||
platforms=", ".join(PLATFORMS)
|
||||
)
|
||||
)
|
||||
|
||||
validate_fn = "_validate_{platform}_config".format(platform=platform).lower()
|
||||
|
||||
if hasattr(self, validate_fn):
|
||||
getattr(self, validate_fn)(application_id, application_config)
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
UNKNOWN_PLATFORM.format(
|
||||
platform=platform,
|
||||
platforms=", ".join(PLATFORMS)
|
||||
)
|
||||
)
|
||||
|
||||
def _validate_fcm_config(self, application_id, application_config):
|
||||
allowed = (
|
||||
REQUIRED_SETTINGS + OPTIONAL_SETTINGS + FCM_REQUIRED_SETTINGS + FCM_OPTIONAL_SETTINGS
|
||||
)
|
||||
|
||||
self._validate_allowed_settings(application_id, application_config, allowed)
|
||||
self._validate_required_settings(
|
||||
application_id, application_config, FCM_REQUIRED_SETTINGS
|
||||
)
|
||||
|
||||
application_config.setdefault("POST_URL", "https://fcm.googleapis.com/fcm/send")
|
||||
application_config.setdefault("MAX_RECIPIENTS", 1000)
|
||||
application_config.setdefault("ERROR_TIMEOUT", None)
|
||||
|
||||
def _validate_gcm_config(self, application_id, application_config):
|
||||
allowed = (
|
||||
REQUIRED_SETTINGS + OPTIONAL_SETTINGS + GCM_REQUIRED_SETTINGS + GCM_OPTIONAL_SETTINGS
|
||||
)
|
||||
|
||||
self._validate_allowed_settings(application_id, application_config, allowed)
|
||||
self._validate_required_settings(
|
||||
application_id, application_config, GCM_REQUIRED_SETTINGS
|
||||
)
|
||||
|
||||
application_config.setdefault("POST_URL", "https://android.googleapis.com/gcm/send")
|
||||
application_config.setdefault("MAX_RECIPIENTS", 1000)
|
||||
application_config.setdefault("ERROR_TIMEOUT", None)
|
||||
|
||||
def _validate_wns_config(self, application_id, application_config):
|
||||
allowed = (
|
||||
REQUIRED_SETTINGS + OPTIONAL_SETTINGS + WNS_REQUIRED_SETTINGS + WNS_OPTIONAL_SETTINGS
|
||||
)
|
||||
|
||||
self._validate_allowed_settings(application_id, application_config, allowed)
|
||||
self._validate_required_settings(
|
||||
application_id, application_config, WNS_REQUIRED_SETTINGS
|
||||
)
|
||||
|
||||
application_config.setdefault("WNS_ACCESS_URL", "https://login.live.com/accesstoken.srf")
|
||||
|
||||
def _validate_wp_config(self, application_id, application_config):
|
||||
allowed = (
|
||||
REQUIRED_SETTINGS + OPTIONAL_SETTINGS + WP_REQUIRED_SETTINGS + WP_OPTIONAL_SETTINGS
|
||||
)
|
||||
|
||||
self._validate_allowed_settings(application_id, application_config, allowed)
|
||||
self._validate_required_settings(
|
||||
application_id, application_config, WP_REQUIRED_SETTINGS
|
||||
)
|
||||
application_config.setdefault("POST_URL", {
|
||||
"CHROME": "https://fcm.googleapis.com/fcm/send",
|
||||
"OPERA": "https://fcm.googleapis.com/fcm/send",
|
||||
"FIREFOX": "https://updates.push.services.mozilla.com/wpush/v2",
|
||||
})
|
||||
|
||||
def _validate_allowed_settings(self, application_id, application_config, allowed_settings):
|
||||
"""Confirm only allowed settings are present."""
|
||||
|
||||
for setting_key in application_config.keys():
|
||||
if setting_key not in allowed_settings:
|
||||
raise ImproperlyConfigured(
|
||||
"Platform {}, app {} does not support the setting: {}.".format(
|
||||
application_config["PLATFORM"], application_id, setting_key
|
||||
)
|
||||
)
|
||||
|
||||
def _validate_required_settings(
|
||||
self, application_id, application_config, required_settings
|
||||
):
|
||||
"""All required keys must be present"""
|
||||
|
||||
for setting_key in required_settings:
|
||||
if setting_key not in application_config.keys():
|
||||
raise ImproperlyConfigured(
|
||||
MISSING_SETTING.format(
|
||||
application_id=application_id, setting=setting_key
|
||||
)
|
||||
)
|
||||
|
||||
def _get_application_settings(self, application_id, platform, settings_key):
|
||||
"""
|
||||
Walks through PUSH_NOTIFICATIONS_SETTINGS to find the correct setting value
|
||||
or raises ImproperlyConfigured.
|
||||
"""
|
||||
|
||||
if not application_id:
|
||||
conf_cls = "push_notifications.conf.AppConfig"
|
||||
raise ImproperlyConfigured(
|
||||
"{} requires the application_id be specified at all times.".format(conf_cls)
|
||||
)
|
||||
|
||||
# verify that the application config exists
|
||||
app_config = self._settings.get("APPLICATIONS").get(application_id, None)
|
||||
if app_config is None:
|
||||
raise ImproperlyConfigured(
|
||||
"No application configured with application_id: {}.".format(application_id)
|
||||
)
|
||||
|
||||
# fetch a setting for the incorrect type of platform
|
||||
if app_config.get("PLATFORM") != platform:
|
||||
raise ImproperlyConfigured(
|
||||
SETTING_MISMATCH.format(
|
||||
application_id=application_id,
|
||||
platform=app_config.get("PLATFORM"),
|
||||
setting=settings_key
|
||||
)
|
||||
)
|
||||
|
||||
# finally, try to fetch the setting
|
||||
if settings_key not in app_config:
|
||||
raise ImproperlyConfigured(
|
||||
MISSING_SETTING.format(
|
||||
application_id=application_id, setting=settings_key
|
||||
)
|
||||
)
|
||||
|
||||
return app_config.get(settings_key)
|
||||
|
||||
def get_gcm_api_key(self, application_id=None):
|
||||
return self._get_application_settings(application_id, "GCM", "API_KEY")
|
||||
|
||||
def get_fcm_api_key(self, application_id=None):
|
||||
return self._get_application_settings(application_id, "FCM", "API_KEY")
|
||||
|
||||
def get_post_url(self, cloud_type, application_id=None):
|
||||
return self._get_application_settings(application_id, cloud_type, "POST_URL")
|
||||
|
||||
def get_error_timeout(self, cloud_type, application_id=None):
|
||||
return self._get_application_settings(application_id, cloud_type, "ERROR_TIMEOUT")
|
||||
|
||||
def get_max_recipients(self, cloud_type, application_id=None):
|
||||
return self._get_application_settings(application_id, cloud_type, "MAX_RECIPIENTS")
|
||||
|
||||
def get_wns_package_security_id(self, application_id=None):
|
||||
return self._get_application_settings(application_id, "WNS", "PACKAGE_SECURITY_ID")
|
||||
|
||||
def get_wns_secret_key(self, application_id=None):
|
||||
return self._get_application_settings(application_id, "WNS", "SECRET_KEY")
|
||||
|
||||
def get_wp_post_url(self, application_id, browser):
|
||||
return self._get_application_settings(application_id, "WP", "POST_URL")[browser]
|
||||
|
||||
def get_wp_private_key(self, application_id=None):
|
||||
return self._get_application_settings(application_id, "WP", "PRIVATE_KEY")
|
||||
|
||||
def get_wp_claims(self, application_id=None):
|
||||
return self._get_application_settings(application_id, "WP", "CLAIMS")
|
|
@ -0,0 +1,10 @@
|
|||
from .base import BaseConfig
|
||||
|
||||
|
||||
class AppModelConfig(BaseConfig):
|
||||
"""Future home of the Application Model conf adapter
|
||||
|
||||
Supports multiple applications in the database.
|
||||
"""
|
||||
|
||||
pass
|
|
@ -0,0 +1,29 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class BaseConfig(object):
|
||||
def get_fcm_api_key(self, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_gcm_api_key(self, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_wns_package_security_id(self, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_wns_secret_key(self, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_post_url(self, cloud_type, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_error_timeout(self, cloud_type, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_max_recipients(self, cloud_type, application_id=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_applications(self):
|
||||
"""Returns a collection containing the configured applications."""
|
||||
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,90 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.six import string_types
|
||||
|
||||
from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
from .base import BaseConfig
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LegacyConfig"
|
||||
]
|
||||
|
||||
|
||||
class empty(object):
|
||||
pass
|
||||
|
||||
|
||||
class LegacyConfig(BaseConfig):
|
||||
def _get_application_settings(self, application_id, settings_key, error_message):
|
||||
"""Legacy behaviour"""
|
||||
|
||||
if not application_id:
|
||||
value = SETTINGS.get(settings_key, empty)
|
||||
if value is empty:
|
||||
raise ImproperlyConfigured(error_message)
|
||||
return value
|
||||
else:
|
||||
msg = (
|
||||
"LegacySettings does not support application_id. To enable "
|
||||
"multiple application support, use push_notifications.conf.AppSettings."
|
||||
)
|
||||
raise ImproperlyConfigured(msg)
|
||||
|
||||
def get_gcm_api_key(self, application_id=None):
|
||||
msg = (
|
||||
"Set PUSH_NOTIFICATIONS_SETTINGS[\"GCM_API_KEY\"] to send messages through GCM."
|
||||
)
|
||||
return self._get_application_settings(application_id, "GCM_API_KEY", msg)
|
||||
|
||||
def get_fcm_api_key(self, application_id=None):
|
||||
msg = (
|
||||
"Set PUSH_NOTIFICATIONS_SETTINGS[\"FCM_API_KEY\"] to send messages through FCM."
|
||||
)
|
||||
return self._get_application_settings(application_id, "FCM_API_KEY", msg)
|
||||
|
||||
def get_post_url(self, cloud_type, application_id=None):
|
||||
key = "{}_POST_URL".format(cloud_type)
|
||||
msg = (
|
||||
"Set PUSH_NOTIFICATIONS_SETTINGS[\"{}\"] to send messages through {}.".format(
|
||||
key, cloud_type
|
||||
)
|
||||
)
|
||||
return self._get_application_settings(application_id, key, msg)
|
||||
|
||||
def get_error_timeout(self, cloud_type, application_id=None):
|
||||
key = "{}_ERROR_TIMEOUT".format(cloud_type)
|
||||
msg = (
|
||||
"Set PUSH_NOTIFICATIONS_SETTINGS[\"{}\"] to send messages through {}.".format(
|
||||
key, cloud_type
|
||||
)
|
||||
)
|
||||
return self._get_application_settings(application_id, key, msg)
|
||||
|
||||
def get_max_recipients(self, cloud_type, application_id=None):
|
||||
key = "{}_MAX_RECIPIENTS".format(cloud_type)
|
||||
msg = (
|
||||
"Set PUSH_NOTIFICATIONS_SETTINGS[\"{}\"] to send messages through {}.".format(
|
||||
key, cloud_type
|
||||
)
|
||||
)
|
||||
return self._get_application_settings(application_id, key, msg)
|
||||
|
||||
def get_wns_package_security_id(self, application_id=None):
|
||||
msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages"
|
||||
return self._get_application_settings(application_id, "WNS_PACKAGE_SECURITY_ID", msg)
|
||||
|
||||
def get_wns_secret_key(self, application_id=None):
|
||||
msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages"
|
||||
return self._get_application_settings(application_id, "WNS_SECRET_KEY", msg)
|
||||
|
||||
def get_wp_post_url(self, application_id, browser):
|
||||
msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages"
|
||||
return self._get_application_settings(application_id, "WP_POST_URL", msg)[browser]
|
||||
|
||||
def get_wp_private_key(self, application_id=None):
|
||||
msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages"
|
||||
return self._get_application_settings(application_id, "WP_PRIVATE_KEY", msg)
|
||||
|
||||
def get_wp_claims(self, application_id=None):
|
||||
msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages"
|
||||
return self._get_application_settings(application_id, "WP_CLAIMS", msg)
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
class NotificationError(Exception):
|
||||
pass
|
|
@ -1,21 +1,22 @@
|
|||
import re
|
||||
import struct
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models, connection
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||
from django.db import connection, models
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
__all__ = ["HexadecimalField", "HexIntegerField"]
|
||||
|
||||
UNSIGNED_64BIT_INT_MIN_VALUE = 0
|
||||
UNSIGNED_64BIT_INT_MAX_VALUE = 2 ** 64 - 1
|
||||
|
||||
__all__ = ["HexadecimalField", "HexIntegerField"]
|
||||
|
||||
|
||||
hex_re = re.compile(r"^(([0-9A-f])|(0x[0-9A-f]))+$")
|
||||
signed_integer_engines = [
|
||||
"django.db.backends.postgresql",
|
||||
"django.db.backends.postgresql_psycopg2",
|
||||
"django.contrib.gis.db.backends.postgis",
|
||||
"django.db.backends.sqlite3"
|
||||
|
@ -47,7 +48,9 @@ class HexadecimalField(forms.CharField):
|
|||
A form field that accepts only hexadecimal numbers
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.default_validators = [RegexValidator(hex_re, _("Enter a valid hexadecimal number"), "invalid")]
|
||||
self.default_validators = [
|
||||
RegexValidator(hex_re, _("Enter a valid hexadecimal number"), "invalid")
|
||||
]
|
||||
super(HexadecimalField, self).__init__(*args, **kwargs)
|
||||
|
||||
def prepare_value(self, value):
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
"""
|
||||
Google Cloud Messaging
|
||||
Previously known as C2DM
|
||||
Documentation is available on the Android Developer website:
|
||||
https://developer.android.com/google/gcm/index.html
|
||||
Firebase Cloud Messaging
|
||||
Previously known as GCM / C2DM
|
||||
Documentation is available on the Firebase Developer website:
|
||||
https://firebase.google.com/docs/cloud-messaging/
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .compat import Request, urlopen
|
||||
from .conf import get_manager
|
||||
from .exceptions import NotificationError
|
||||
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
|
||||
# Valid keys for FCM messages. Reference:
|
||||
# https://firebase.google.com/docs/cloud-messaging/http-server-ref
|
||||
FCM_TARGETS_KEYS = [
|
||||
"to", "condition", "notification_key"
|
||||
]
|
||||
FCM_OPTIONS_KEYS = [
|
||||
"collapse_key", "priority", "content_available", "delay_while_idle", "time_to_live",
|
||||
"restricted_package_name", "dry_run"
|
||||
]
|
||||
FCM_NOTIFICATIONS_PAYLOAD_KEYS = [
|
||||
"title", "body", "icon", "sound", "badge", "color", "tag", "click_action",
|
||||
"body_loc_key", "body_loc_args", "title_loc_key", "title_loc_args"
|
||||
]
|
||||
|
||||
|
||||
class GCMError(NotificationError):
|
||||
|
@ -34,161 +42,171 @@ def _chunks(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.')
|
||||
def _gcm_send(data, content_type, application_id):
|
||||
key = get_manager().get_gcm_api_key(application_id)
|
||||
|
||||
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")
|
||||
request = Request(get_manager().get_post_url("GCM", application_id), data, headers)
|
||||
return urlopen(
|
||||
request, timeout=get_manager().get_error_timeout("GCM", application_id)
|
||||
).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
|
||||
"""
|
||||
def _fcm_send(data, content_type, application_id):
|
||||
key = get_manager().get_fcm_api_key(application_id)
|
||||
|
||||
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
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"Authorization": "key=%s" % (key),
|
||||
"Content-Length": str(len(data)),
|
||||
}
|
||||
request = Request(get_manager().get_post_url("FCM", application_id), data, headers)
|
||||
return urlopen(
|
||||
request, timeout=get_manager().get_error_timeout("FCM", application_id)
|
||||
).read().decode("utf-8")
|
||||
|
||||
|
||||
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"]:
|
||||
def _cm_handle_response(registration_ids, response_data, cloud_type, application_id=None):
|
||||
response = response_data
|
||||
if response.get("failure") or response.get("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
|
||||
# https://firebase.google.com/docs/cloud-messaging/http-server-ref#error-codes
|
||||
# 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).
|
||||
result["original_registration_id"] = registration_ids[index]
|
||||
# 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, you need
|
||||
# to obtain it from the list of registration_ids 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 = GCMDevice.objects.filter(
|
||||
registration_id__in=ids_to_remove, cloud_message_type=cloud_type
|
||||
)
|
||||
removed.update(active=0)
|
||||
|
||||
for old_id, new_id in old_new_ids:
|
||||
_gcm_handle_canonical_id(new_id, old_id)
|
||||
_cm_handle_canonical_id(new_id, old_id, cloud_type)
|
||||
|
||||
if throw_error:
|
||||
raise GCMError(response)
|
||||
return response
|
||||
|
||||
|
||||
def _gcm_handle_canonical_id(canonical_id, current_id):
|
||||
def _cm_send_request(
|
||||
registration_ids, data, cloud_type="GCM", application_id=None,
|
||||
use_fcm_notifications=True, **kwargs
|
||||
):
|
||||
"""
|
||||
Handle situation when GCM server response contains canonical ID
|
||||
Sends a FCM or GCM notification to one or more registration_ids as json data.
|
||||
The registration_ids needs to be a list.
|
||||
"""
|
||||
if GCMDevice.objects.filter(registration_id=canonical_id, active=True).exists():
|
||||
GCMDevice.objects.filter(registration_id=current_id).update(active=False)
|
||||
|
||||
payload = {"registration_ids": registration_ids} if registration_ids else {}
|
||||
|
||||
# If using FCM, optionnally autodiscovers notification related keys
|
||||
# https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages
|
||||
if cloud_type == "FCM" and use_fcm_notifications:
|
||||
notification_payload = {}
|
||||
if "message" in data:
|
||||
notification_payload["body"] = data.pop("message", None)
|
||||
|
||||
for key in FCM_NOTIFICATIONS_PAYLOAD_KEYS:
|
||||
value_from_extra = data.pop(key, None)
|
||||
if value_from_extra:
|
||||
notification_payload[key] = value_from_extra
|
||||
value_from_kwargs = kwargs.pop(key, None)
|
||||
if value_from_kwargs:
|
||||
notification_payload[key] = value_from_kwargs
|
||||
if notification_payload:
|
||||
payload["notification"] = notification_payload
|
||||
|
||||
if data:
|
||||
payload["data"] = data
|
||||
|
||||
# Attach any additional non falsy keyword args (targets, options)
|
||||
# See ref : https://firebase.google.com/docs/cloud-messaging/http-server-ref#table1
|
||||
payload.update({
|
||||
k: v for k, v in kwargs.items() if v and (k in FCM_TARGETS_KEYS or k in FCM_OPTIONS_KEYS)
|
||||
})
|
||||
|
||||
# Sort the keys for deterministic output (useful for tests)
|
||||
json_payload = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||
|
||||
# Sends requests and handles the response
|
||||
if cloud_type == "GCM":
|
||||
response = json.loads(_gcm_send(
|
||||
json_payload, "application/json", application_id=application_id
|
||||
))
|
||||
elif cloud_type == "FCM":
|
||||
response = json.loads(_fcm_send(
|
||||
json_payload, "application/json", application_id=application_id
|
||||
))
|
||||
else:
|
||||
GCMDevice.objects.filter(registration_id=current_id).update(registration_id=canonical_id)
|
||||
raise ImproperlyConfigured("cloud_type must be FCM or GCM not %s" % str(cloud_type))
|
||||
return _cm_handle_response(registration_ids, response, cloud_type, application_id)
|
||||
|
||||
|
||||
def gcm_send_message(registration_id, data, **kwargs):
|
||||
def _cm_handle_canonical_id(canonical_id, current_id, cloud_type):
|
||||
"""
|
||||
Sends a GCM notification to a single registration_id.
|
||||
Handle situation when FCM server response contains canonical ID
|
||||
"""
|
||||
devices = GCMDevice.objects.filter(cloud_message_type=cloud_type)
|
||||
if devices.filter(registration_id=canonical_id, active=True).exists():
|
||||
devices.filter(registration_id=current_id).update(active=False)
|
||||
else:
|
||||
devices.filter(registration_id=current_id).update(registration_id=canonical_id)
|
||||
|
||||
If sending multiple notifications, it is more efficient to use
|
||||
gcm_send_bulk_message() with a list of registration_ids
|
||||
|
||||
def send_message(registration_ids, data, cloud_type, application_id=None, **kwargs):
|
||||
"""
|
||||
Sends a FCM (or GCM) notification to one or more registration_ids. The registration_ids
|
||||
can be a list or a single string. 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
|
||||
https://firebase.google.com/docs/cloud-messaging/http-server-ref#table1
|
||||
"""
|
||||
if cloud_type == "FCM":
|
||||
max_recipients = get_manager().get_max_recipients(cloud_type, application_id)
|
||||
elif cloud_type == "GCM":
|
||||
max_recipients = get_manager().get_max_recipients(cloud_type, application_id)
|
||||
else:
|
||||
raise ImproperlyConfigured("cloud_type must be FCM or GCM not %s" % str(cloud_type))
|
||||
|
||||
return _gcm_send_plain(registration_id, data, **kwargs)
|
||||
# Checks for valid recipient
|
||||
if registration_ids is None and "/topics/" not in kwargs.get("to", ""):
|
||||
return
|
||||
|
||||
# Bundles the registration_ids in an list if only one is sent
|
||||
if not isinstance(registration_ids, list):
|
||||
registration_ids = [registration_ids] if registration_ids else None
|
||||
|
||||
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:
|
||||
# FCM only allows up to 1000 reg ids per bulk message
|
||||
# https://firebase.google.com/docs/cloud-messaging/server#http-request
|
||||
if registration_ids:
|
||||
ret = []
|
||||
for chunk in _chunks(registration_ids, max_recipients):
|
||||
ret.append(_gcm_send_json(chunk, data, **kwargs))
|
||||
return ret
|
||||
ret.append(_cm_send_request(
|
||||
chunk, data, cloud_type=cloud_type, application_id=application_id, **kwargs
|
||||
))
|
||||
return ret[0] if len(ret) == 1 else ret
|
||||
else:
|
||||
return _cm_send_request(None, data, cloud_type=cloud_type, **kwargs)
|
||||
|
||||
return _gcm_send_json(registration_ids, data, **kwargs)
|
||||
|
||||
send_bulk_message = send_message
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
can_import_settings = True
|
||||
help = 'Deactivate APNS devices that are not receiving notifications'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from push_notifications.models import APNSDevice, get_expired_tokens
|
||||
expired = get_expired_tokens()
|
||||
devices = APNSDevice.objects.filter(registration_id__in=expired)
|
||||
for d in devices:
|
||||
self.stdout.write('deactivating [%s]' % d.registration_id)
|
||||
d.active = False
|
||||
d.save()
|
||||
self.stdout.write('deactivated %d devices' % len(devices))
|
|
@ -1,9 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import push_notifications.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import push_notifications.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -13,22 +14,6 @@ class Migration(migrations.Migration):
|
|||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='APNSDevice',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=255, null=True, verbose_name='Name', blank=True)),
|
||||
('active', models.BooleanField(default=True, help_text='Inactive devices will not be sent notifications', verbose_name='Is active')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date', null=True)),
|
||||
('device_id', models.UUIDField(help_text='UDID / UIDevice.identifierForVendor()', max_length=32, null=True, verbose_name='Device ID', blank=True, db_index=True)),
|
||||
('registration_id', models.CharField(unique=True, max_length=64, verbose_name='Registration ID')),
|
||||
('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'APNS device',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GCMDevice',
|
||||
fields=[
|
||||
|
@ -38,7 +23,7 @@ class Migration(migrations.Migration):
|
|||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date', null=True)),
|
||||
('device_id', push_notifications.fields.HexIntegerField(help_text='ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)', null=True, verbose_name='Device ID', blank=True, db_index=True)),
|
||||
('registration_id', models.TextField(verbose_name='Registration ID')),
|
||||
('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'GCM device',
|
||||
|
|
|
@ -12,9 +12,4 @@ class Migration(migrations.Migration):
|
|||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='apnsdevice',
|
||||
name='registration_id',
|
||||
field=models.CharField(max_length=200, unique=True, verbose_name='Registration ID'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.6 on 2016-06-13 20:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('push_notifications', '0002_auto_20160106_0850'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WNSDevice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Name')),
|
||||
('active', models.BooleanField(default=True, help_text='Inactive devices will not be sent notifications', verbose_name='Is active')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Creation date')),
|
||||
('device_id', models.UUIDField(blank=True, db_index=True, help_text='GUID()', null=True, verbose_name='Device ID')),
|
||||
('registration_id', models.TextField(verbose_name='Notification URI')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'WNS device',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.6 on 2016-06-13 20:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('push_notifications', '0003_wnsdevice'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gcmdevice',
|
||||
name='cloud_message_type',
|
||||
field=models.CharField(choices=[('FCM', 'Firebase Cloud Message'), ('GCM', 'Google Cloud Message')], default='GCM', help_text='You should choose FCM or GCM', max_length=3, verbose_name='Cloud Message Type')
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('push_notifications', '0004_fcm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gcmdevice',
|
||||
name='application_id',
|
||||
field=models.CharField(help_text='Opaque application identity, should be filled in for multiple key/certificate access', max_length=64, null=True, verbose_name='Application ID', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='wnsdevice',
|
||||
name='application_id',
|
||||
field=models.CharField(help_text='Opaque application identity, should be filled in for multiple key/certificate access', max_length=64, null=True, verbose_name='Application ID', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('push_notifications', '0005_applicationid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WebPushDevice',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=255, null=True, verbose_name='Name', blank=True)),
|
||||
('active', models.BooleanField(default=True, help_text='Inactive devices will not be sent notifications', verbose_name='Is active')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date', null=True)),
|
||||
('application_id', models.CharField(help_text='Opaque application identity, should be filled in for multiple key/certificate access', max_length=64, null=True, verbose_name='Application ID', blank=True)),
|
||||
('registration_id', models.TextField(verbose_name='Registration ID')),
|
||||
('p256dh', models.CharField(max_length=88, verbose_name='User public encryption key')),
|
||||
('auth', models.CharField(max_length=24, verbose_name='User auth secret')),
|
||||
('browser', models.CharField(default='CHROME', help_text='Currently only support to Chrome, Firefox and Opera browsers', max_length=10, verbose_name='Browser', choices=[('CHROME', 'Chrome'), ('FIREFOX', 'Firefox'), ('OPERA', 'Opera')])),
|
||||
('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'WebPush device',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,28 +1,55 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .fields import HexIntegerField
|
||||
from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
|
||||
|
||||
CLOUD_MESSAGE_TYPES = (
|
||||
("FCM", "Firebase Cloud Message"),
|
||||
("GCM", "Google Cloud Message"),
|
||||
)
|
||||
|
||||
BROWSER_TYPES = (
|
||||
("CHROME", "Chrome"),
|
||||
("FIREFOX", "Firefox"),
|
||||
("OPERA", "Opera"),
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Device(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"), blank=True, null=True)
|
||||
active = models.BooleanField(verbose_name=_("Is active"), default=True,
|
||||
help_text=_("Inactive devices will not be sent notifications"))
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True)
|
||||
date_created = models.DateTimeField(verbose_name=_("Creation date"), auto_now_add=True, null=True)
|
||||
active = models.BooleanField(
|
||||
verbose_name=_("Is active"), default=True,
|
||||
help_text=_("Inactive devices will not be sent notifications")
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
SETTINGS["USER_MODEL"], blank=True, null=True, on_delete=models.CASCADE
|
||||
)
|
||||
date_created = models.DateTimeField(
|
||||
verbose_name=_("Creation date"), auto_now_add=True, null=True
|
||||
)
|
||||
application_id = models.CharField(
|
||||
max_length=64, verbose_name=_("Application ID"),
|
||||
help_text=_(
|
||||
"Opaque application identity, should be filled in for multiple"
|
||||
" key/certificate access"
|
||||
),
|
||||
blank=True, null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return self.name or \
|
||||
str(self.device_id or "") or \
|
||||
return (
|
||||
self.name or str(self.device_id or "") or
|
||||
"%s for %s" % (self.__class__.__name__, self.user or "unknown user")
|
||||
)
|
||||
|
||||
|
||||
class GCMDeviceManager(models.Manager):
|
||||
|
@ -33,68 +60,150 @@ class GCMDeviceManager(models.Manager):
|
|||
class GCMDeviceQuerySet(models.query.QuerySet):
|
||||
def send_message(self, message, **kwargs):
|
||||
if self:
|
||||
from .gcm import gcm_send_bulk_message
|
||||
from .gcm import send_message as gcm_send_message
|
||||
|
||||
data = kwargs.pop("extra", {})
|
||||
if message is not None:
|
||||
data["message"] = message
|
||||
|
||||
reg_ids = list(self.filter(active=True).values_list('registration_id', flat=True))
|
||||
return gcm_send_bulk_message(registration_ids=reg_ids, data=data, **kwargs)
|
||||
app_ids = self.filter(active=True).order_by(
|
||||
"application_id"
|
||||
).values_list("application_id", flat=True).distinct()
|
||||
response = []
|
||||
for cloud_type in ("FCM", "GCM"):
|
||||
for app_id in app_ids:
|
||||
reg_ids = list(
|
||||
self.filter(
|
||||
active=True, cloud_message_type=cloud_type, application_id=app_id).values_list(
|
||||
"registration_id", flat=True
|
||||
)
|
||||
)
|
||||
if reg_ids:
|
||||
r = gcm_send_message(reg_ids, data, cloud_type, application_id=app_id, **kwargs)
|
||||
response.append(r)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class GCMDevice(Device):
|
||||
# device_id cannot be a reliable primary key as fragmentation between different devices
|
||||
# can make it turn out to be null and such:
|
||||
# http://android-developers.blogspot.co.uk/2011/03/identifying-app-installations.html
|
||||
device_id = HexIntegerField(verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
|
||||
help_text=_("ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)"))
|
||||
device_id = HexIntegerField(
|
||||
verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
|
||||
help_text=_("ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)")
|
||||
)
|
||||
registration_id = models.TextField(verbose_name=_("Registration ID"))
|
||||
|
||||
cloud_message_type = models.CharField(
|
||||
verbose_name=_("Cloud Message Type"), max_length=3,
|
||||
choices=CLOUD_MESSAGE_TYPES, default="GCM",
|
||||
help_text=_("You should choose FCM or GCM")
|
||||
)
|
||||
objects = GCMDeviceManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("GCM device")
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
from .gcm import gcm_send_message
|
||||
from .gcm import send_message as gcm_send_message
|
||||
|
||||
data = kwargs.pop("extra", {})
|
||||
if message is not None:
|
||||
data["message"] = message
|
||||
return gcm_send_message(registration_id=self.registration_id, data=data, **kwargs)
|
||||
|
||||
return gcm_send_message(
|
||||
self.registration_id, data, self.cloud_message_type,
|
||||
application_id=self.application_id, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class APNSDeviceManager(models.Manager):
|
||||
class WNSDeviceManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return APNSDeviceQuerySet(self.model)
|
||||
return WNSDeviceQuerySet(self.model)
|
||||
|
||||
|
||||
class APNSDeviceQuerySet(models.query.QuerySet):
|
||||
class WNSDeviceQuerySet(models.query.QuerySet):
|
||||
def send_message(self, message, **kwargs):
|
||||
if self:
|
||||
from .apns import apns_send_bulk_message
|
||||
reg_ids = list(self.filter(active=True).values_list('registration_id', flat=True))
|
||||
return apns_send_bulk_message(registration_ids=reg_ids, alert=message, **kwargs)
|
||||
from .wns import wns_send_bulk_message
|
||||
|
||||
app_ids = self.filter(active=True).order_by("application_id").values_list(
|
||||
"application_id", flat=True
|
||||
).distinct()
|
||||
res = []
|
||||
for app_id in app_ids:
|
||||
reg_ids = self.filter(active=True, application_id=app_id).values_list(
|
||||
"registration_id", flat=True
|
||||
)
|
||||
r = wns_send_bulk_message(uri_list=list(reg_ids), message=message, **kwargs)
|
||||
if hasattr(r, "keys"):
|
||||
res += [r]
|
||||
elif hasattr(r, "__getitem__"):
|
||||
res += r
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class APNSDevice(Device):
|
||||
device_id = models.UUIDField(verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
|
||||
help_text="UDID / UIDevice.identifierForVendor()")
|
||||
registration_id = models.CharField(verbose_name=_("Registration ID"), max_length=200, unique=True)
|
||||
class WNSDevice(Device):
|
||||
device_id = models.UUIDField(
|
||||
verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
|
||||
help_text=_("GUID()")
|
||||
)
|
||||
registration_id = models.TextField(verbose_name=_("Notification URI"))
|
||||
|
||||
objects = APNSDeviceManager()
|
||||
objects = WNSDeviceManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("APNS device")
|
||||
verbose_name = _("WNS device")
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
from .apns import apns_send_message
|
||||
from .wns import wns_send_message
|
||||
|
||||
return apns_send_message(registration_id=self.registration_id, alert=message, **kwargs)
|
||||
return wns_send_message(
|
||||
uri=self.registration_id, message=message, application_id=self.application_id,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
# This is an APNS-only function right now, but maybe GCM will implement it
|
||||
# in the future. But the definition of 'expired' may not be the same. Whatevs
|
||||
def get_expired_tokens():
|
||||
from .apns import apns_fetch_inactive_ids
|
||||
return apns_fetch_inactive_ids()
|
||||
class WebPushDeviceManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return WebPushDeviceQuerySet(self.model)
|
||||
|
||||
|
||||
class WebPushDeviceQuerySet(models.query.QuerySet):
|
||||
def send_message(self, message, **kwargs):
|
||||
devices = self.filter(active=True).order_by("application_id").distinct()
|
||||
res = []
|
||||
for device in devices:
|
||||
res.append(device.send_message(message))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class WebPushDevice(Device):
|
||||
registration_id = models.TextField(verbose_name=_("Registration ID"))
|
||||
p256dh = models.CharField(
|
||||
verbose_name=_("User public encryption key"),
|
||||
max_length=88)
|
||||
auth = models.CharField(
|
||||
verbose_name=_("User auth secret"),
|
||||
max_length=24)
|
||||
browser = models.CharField(
|
||||
verbose_name=_("Browser"), max_length=10,
|
||||
choices=BROWSER_TYPES, default=BROWSER_TYPES[0][0],
|
||||
help_text=_("Currently only support to Chrome, Firefox and Opera browsers")
|
||||
)
|
||||
objects = WebPushDeviceManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("WebPush device")
|
||||
|
||||
@property
|
||||
def device_id(self):
|
||||
return None
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
from .webpush import webpush_send_message
|
||||
|
||||
return webpush_send_message(
|
||||
uri=self.registration_id, message=message, browser=self.browser,
|
||||
auth=self.auth, p256dh=self.p256dh, application_id=self.application_id, **kwargs)
|
||||
|
|
|
@ -1,21 +1,45 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
PUSH_NOTIFICATIONS_SETTINGS = getattr(settings, "PUSH_NOTIFICATIONS_SETTINGS", {})
|
||||
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault(
|
||||
"CONFIG", "push_notifications.conf.LegacyConfig"
|
||||
)
|
||||
|
||||
# GCM
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("GCM_POST_URL", "https://android.googleapis.com/gcm/send")
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault(
|
||||
"GCM_POST_URL", "https://android.googleapis.com/gcm/send"
|
||||
)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("GCM_MAX_RECIPIENTS", 1000)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("GCM_ERROR_TIMEOUT", None)
|
||||
|
||||
# FCM
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault(
|
||||
"FCM_POST_URL", "https://fcm.googleapis.com/fcm/send"
|
||||
)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("FCM_MAX_RECIPIENTS", 1000)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("FCM_ERROR_TIMEOUT", None)
|
||||
|
||||
# APNS
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_PORT", 2195)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_FEEDBACK_PORT", 2196)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_ERROR_TIMEOUT", None)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_MAX_NOTIFICATION_SIZE", 2048)
|
||||
if settings.DEBUG:
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_HOST", "gateway.sandbox.push.apple.com")
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_FEEDBACK_HOST", "feedback.sandbox.push.apple.com")
|
||||
else:
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_HOST", "gateway.push.apple.com")
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_FEEDBACK_HOST", "feedback.push.apple.com")
|
||||
# WNS
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("WNS_PACKAGE_SECURITY_ID", None)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("WNS_SECRET_KEY", None)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault(
|
||||
"WNS_ACCESS_URL", "https://login.live.com/accesstoken.srf"
|
||||
)
|
||||
|
||||
# WP (WebPush)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("WP_POST_URL", {
|
||||
"CHROME": PUSH_NOTIFICATIONS_SETTINGS["FCM_POST_URL"],
|
||||
"OPERA": PUSH_NOTIFICATIONS_SETTINGS["FCM_POST_URL"],
|
||||
"FIREFOX": "https://updates.push.services.mozilla.com/wpush/v2",
|
||||
})
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("WP_PRIVATE_KEY", None)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("WP_CLAIMS", None)
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("WP_ERROR_TIMEOUT", None)
|
||||
|
||||
# User model
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("USER_MODEL", settings.AUTH_USER_MODEL)
|
||||
|
||||
# API endpoint settings
|
||||
PUSH_NOTIFICATIONS_SETTINGS.setdefault("UPDATE_ON_DUPLICATE_REG_ID", False)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
from pywebpush import WebPushException, webpush
|
||||
|
||||
from .conf import get_manager
|
||||
from .exceptions import NotificationError
|
||||
|
||||
|
||||
class WebPushError(NotificationError):
|
||||
pass
|
||||
|
||||
|
||||
def get_subscription_info(application_id, uri, browser, auth, p256dh):
|
||||
url = get_manager().get_wp_post_url(application_id, browser)
|
||||
return {
|
||||
"endpoint": "%s/%s" % (url, uri),
|
||||
"keys": {
|
||||
"auth": auth,
|
||||
"p256dh": p256dh,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def webpush_send_message(
|
||||
uri, message, browser, auth, p256dh, application_id=None, **kwargs
|
||||
):
|
||||
subscription_info = get_subscription_info(application_id, uri, browser, auth, p256dh)
|
||||
|
||||
try:
|
||||
response = webpush(
|
||||
subscription_info=subscription_info,
|
||||
data=message,
|
||||
vapid_private_key=get_manager().get_wp_private_key(application_id),
|
||||
vapid_claims=get_manager().get_wp_claims(application_id),
|
||||
**kwargs
|
||||
)
|
||||
results = {"results": [{}]}
|
||||
if not response.ok:
|
||||
results["results"][0]["error"] = response.content
|
||||
results["results"][0]["original_registration_id"] = response.content
|
||||
else:
|
||||
results["success"] = 1
|
||||
return results
|
||||
except WebPushException as e:
|
||||
raise WebPushError(e.message)
|
|
@ -0,0 +1,371 @@
|
|||
"""
|
||||
Windows Notification Service
|
||||
|
||||
Documentation is available on the Windows Dev Center:
|
||||
https://msdn.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-windows-push-notification-services--wns--overview
|
||||
"""
|
||||
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .compat import HTTPError, Request, urlencode, urlopen
|
||||
from .conf import get_manager
|
||||
from .exceptions import NotificationError
|
||||
from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
|
||||
|
||||
|
||||
class WNSError(NotificationError):
|
||||
pass
|
||||
|
||||
|
||||
class WNSAuthenticationError(WNSError):
|
||||
pass
|
||||
|
||||
|
||||
class WNSNotificationResponseError(WNSError):
|
||||
pass
|
||||
|
||||
|
||||
def _wns_authenticate(scope="notify.windows.com", application_id=None):
|
||||
"""
|
||||
Requests an Access token for WNS communication.
|
||||
|
||||
:return: dict: {'access_token': <str>, 'expires_in': <int>, 'token_type': 'bearer'}
|
||||
"""
|
||||
client_id = get_manager().get_wns_package_security_id(application_id)
|
||||
client_secret = get_manager().get_wns_secret_key(application_id)
|
||||
if not client_id:
|
||||
raise ImproperlyConfigured(
|
||||
'You need to set PUSH_NOTIFICATIONS_SETTINGS["WNS_PACKAGE_SECURITY_ID"] to use WNS.'
|
||||
)
|
||||
|
||||
if not client_secret:
|
||||
raise ImproperlyConfigured(
|
||||
'You need to set PUSH_NOTIFICATIONS_SETTINGS["WNS_SECRET_KEY"] to use WNS.'
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
params = {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"scope": scope,
|
||||
}
|
||||
data = urlencode(params).encode("utf-8")
|
||||
|
||||
request = Request(SETTINGS["WNS_ACCESS_URL"], data=data, headers=headers)
|
||||
try:
|
||||
response = urlopen(request)
|
||||
except HTTPError as err:
|
||||
if err.code == 400:
|
||||
# One of your settings is probably jacked up.
|
||||
# https://msdn.microsoft.com/en-us/library/windows/apps/xaml/hh868245
|
||||
raise WNSAuthenticationError("Authentication failed, check your WNS settings.")
|
||||
raise err
|
||||
|
||||
oauth_data = response.read().decode("utf-8")
|
||||
try:
|
||||
oauth_data = json.loads(oauth_data)
|
||||
except Exception:
|
||||
# Upstream WNS issue
|
||||
raise WNSAuthenticationError("Received invalid JSON data from WNS.")
|
||||
|
||||
access_token = oauth_data.get("access_token")
|
||||
if not access_token:
|
||||
# Upstream WNS issue
|
||||
raise WNSAuthenticationError("Access token missing from WNS response.")
|
||||
|
||||
return access_token
|
||||
|
||||
|
||||
def _wns_send(uri, data, wns_type="wns/toast", application_id=None):
|
||||
"""
|
||||
Sends a notification data and authentication to WNS.
|
||||
|
||||
:param uri: str: The device's unique notification URI
|
||||
:param data: dict: The notification data to be sent.
|
||||
:return:
|
||||
"""
|
||||
access_token = _wns_authenticate(application_id=application_id)
|
||||
|
||||
content_type = "text/xml"
|
||||
if wns_type == "wns/raw":
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
headers = {
|
||||
# content_type is "text/xml" (toast/badge/tile) | "application/octet-stream" (raw)
|
||||
"Content-Type": content_type,
|
||||
"Authorization": "Bearer %s" % (access_token),
|
||||
"X-WNS-Type": wns_type, # wns/toast | wns/badge | wns/tile | wns/raw
|
||||
}
|
||||
|
||||
if type(data) is str:
|
||||
data = data.encode("utf-8")
|
||||
|
||||
request = Request(uri, data, headers)
|
||||
|
||||
# A lot of things can happen, let them know which one.
|
||||
try:
|
||||
response = urlopen(request)
|
||||
except HTTPError as err:
|
||||
if err.code == 400:
|
||||
msg = "One or more headers were specified incorrectly or conflict with another header."
|
||||
elif err.code == 401:
|
||||
msg = "The cloud service did not present a valid authentication ticket."
|
||||
elif err.code == 403:
|
||||
msg = "The cloud service is not authorized to send a notification to this URI."
|
||||
elif err.code == 404:
|
||||
msg = "The channel URI is not valid or is not recognized by WNS."
|
||||
elif err.code == 405:
|
||||
msg = "Invalid method. Only POST or DELETE is allowed."
|
||||
elif err.code == 406:
|
||||
msg = "The cloud service exceeded its throttle limit"
|
||||
elif err.code == 410:
|
||||
msg = "The channel expired."
|
||||
elif err.code == 413:
|
||||
msg = "The notification payload exceeds the 500 byte limit."
|
||||
elif err.code == 500:
|
||||
msg = "An internal failure caused notification delivery to fail."
|
||||
elif err.code == 503:
|
||||
msg = "The server is currently unavailable."
|
||||
else:
|
||||
raise err
|
||||
raise WNSNotificationResponseError("HTTP %i: %s" % (err.code, msg))
|
||||
|
||||
return response.read().decode("utf-8")
|
||||
|
||||
|
||||
def _wns_prepare_toast(data, **kwargs):
|
||||
"""
|
||||
Creates the xml tree for a `toast` notification
|
||||
|
||||
:param data: dict: The notification data to be converted to an xml tree.
|
||||
|
||||
{
|
||||
"text": ["Title text", "Message Text", "Another message!"],
|
||||
"image": ["src1", "src2"],
|
||||
}
|
||||
|
||||
:return: str
|
||||
"""
|
||||
root = ET.Element("toast")
|
||||
visual = ET.SubElement(root, "visual")
|
||||
binding = ET.SubElement(visual, "binding")
|
||||
binding.attrib["template"] = kwargs.pop("template", "ToastText01")
|
||||
if "text" in data:
|
||||
for count, item in enumerate(data["text"], start=1):
|
||||
elem = ET.SubElement(binding, "text")
|
||||
elem.text = item
|
||||
elem.attrib["id"] = str(count)
|
||||
if "image" in data:
|
||||
for count, item in enumerate(data["image"], start=1):
|
||||
elem = ET.SubElement(binding, "img")
|
||||
elem.attrib["src"] = item
|
||||
elem.attrib["id"] = str(count)
|
||||
return ET.tostring(root)
|
||||
|
||||
|
||||
def wns_send_message(
|
||||
uri, message=None, xml_data=None, raw_data=None, application_id=None, **kwargs
|
||||
):
|
||||
"""
|
||||
Sends a notification request to WNS.
|
||||
There are four notification types that WNS can send: toast, tile, badge and raw.
|
||||
Toast, tile, and badge can all be customized to use different
|
||||
templates/icons/sounds/launch params/etc.
|
||||
See docs for more information:
|
||||
https://msdn.microsoft.com/en-us/library/windows/apps/br212853.aspx
|
||||
|
||||
There are multiple ways to input notification data:
|
||||
|
||||
1. The simplest and least custom notification to send is to just pass a string
|
||||
to `message`. This will create a toast notification with one text element. e.g.:
|
||||
"This is my notification title"
|
||||
|
||||
2. You can also pass a dictionary to `message`: it can only contain one or both
|
||||
keys: ["text", "image"]. The value of each key must be a list with the text and
|
||||
src respectively. e.g.:
|
||||
{
|
||||
"text": ["text1", "text2"],
|
||||
"image": ["src1", "src2"],
|
||||
}
|
||||
|
||||
3. Passing a dictionary to `xml_data` will create one of three types of
|
||||
notifications depending on the dictionary data (toast, tile, badge).
|
||||
See `dict_to_xml_schema` docs for more information on dictionary formatting.
|
||||
|
||||
4. Passing a value to `raw_data` will create a `raw` notification and send the
|
||||
input data as is.
|
||||
|
||||
:param uri: str: The device's unique notification uri.
|
||||
:param message: str|dict: The notification data to be sent.
|
||||
:param xml_data: dict: A dictionary containing data to be converted to an xml tree.
|
||||
:param raw_data: str: Data to be sent via a `raw` notification.
|
||||
"""
|
||||
# Create a simple toast notification
|
||||
if message:
|
||||
wns_type = "wns/toast"
|
||||
if isinstance(message, str):
|
||||
message = {
|
||||
"text": [message, ],
|
||||
}
|
||||
prepared_data = _wns_prepare_toast(data=message, **kwargs)
|
||||
# Create a toast/tile/badge notification from a dictionary
|
||||
elif xml_data:
|
||||
xml = dict_to_xml_schema(xml_data)
|
||||
wns_type = "wns/%s" % xml.tag
|
||||
prepared_data = ET.tostring(xml)
|
||||
# Create a raw notification
|
||||
elif raw_data:
|
||||
wns_type = "wns/raw"
|
||||
prepared_data = raw_data
|
||||
else:
|
||||
raise TypeError(
|
||||
"At least one of the following parameters must be set:"
|
||||
"`message`, `xml_data`, `raw_data`"
|
||||
)
|
||||
|
||||
return _wns_send(
|
||||
uri=uri, data=prepared_data, wns_type=wns_type, application_id=application_id
|
||||
)
|
||||
|
||||
|
||||
def wns_send_bulk_message(
|
||||
uri_list, message=None, xml_data=None, raw_data=None, application_id=None, **kwargs
|
||||
):
|
||||
"""
|
||||
WNS doesn't support bulk notification, so we loop through each uri.
|
||||
|
||||
:param uri_list: list: A list of uris the notification will be sent to.
|
||||
:param message: str: The notification data to be sent.
|
||||
:param xml_data: dict: A dictionary containing data to be converted to an xml tree.
|
||||
:param raw_data: str: Data to be sent via a `raw` notification.
|
||||
"""
|
||||
res = []
|
||||
if uri_list:
|
||||
for uri in uri_list:
|
||||
r = wns_send_message(
|
||||
uri=uri, message=message, xml_data=xml_data,
|
||||
raw_data=raw_data, application_id=application_id, **kwargs
|
||||
)
|
||||
res.append(r)
|
||||
return res
|
||||
|
||||
|
||||
def dict_to_xml_schema(data):
|
||||
"""
|
||||
Input a dictionary to be converted to xml. There should be only one key at
|
||||
the top level. The value must be a dict with (required) `children` key and
|
||||
(optional) `attrs` key. This will be called the `sub-element dictionary`.
|
||||
|
||||
The `attrs` value must be a dictionary; each value will be added to the
|
||||
element's xml tag as attributes. e.g.:
|
||||
{"example": {
|
||||
"attrs": {
|
||||
"key1": "value1",
|
||||
...
|
||||
},
|
||||
...
|
||||
}}
|
||||
|
||||
would result in:
|
||||
<example key1="value1" key2="value2"></example>
|
||||
|
||||
If the value is a dict it must contain one or more keys which will be used
|
||||
as the sub-element names. Each sub-element must have a value of a sub-element
|
||||
dictionary(see above) or a list of sub-element dictionaries.
|
||||
If the value is not a dict, it will be the value of the element.
|
||||
If the value is a list, multiple elements of the same tag will be created
|
||||
from each sub-element dict in the list.
|
||||
|
||||
:param data: dict: Used to create an XML tree. e.g.:
|
||||
example_data = {
|
||||
"toast": {
|
||||
"attrs": {
|
||||
"launch": "param",
|
||||
"duration": "short",
|
||||
},
|
||||
"children": {
|
||||
"visual": {
|
||||
"children": {
|
||||
"binding": {
|
||||
"attrs": {"template": "ToastText01"},
|
||||
"children": {
|
||||
"text": [
|
||||
{
|
||||
"attrs": {"id": "1"},
|
||||
"children": "text1",
|
||||
},
|
||||
{
|
||||
"attrs": {"id": "2"},
|
||||
"children": "text2",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
:return: ElementTree.Element
|
||||
"""
|
||||
for key, value in data.items():
|
||||
root = _add_element_attrs(ET.Element(key), value.get("attrs", {}))
|
||||
children = value.get("children", None)
|
||||
if isinstance(children, dict):
|
||||
_add_sub_elements_from_dict(root, children)
|
||||
return root
|
||||
|
||||
|
||||
def _add_sub_elements_from_dict(parent, sub_dict):
|
||||
"""
|
||||
Add SubElements to the parent element.
|
||||
|
||||
:param parent: ElementTree.Element: The parent element for the newly created SubElement.
|
||||
:param sub_dict: dict: Used to create a new SubElement. See `dict_to_xml_schema`
|
||||
method docstring for more information. e.g.:
|
||||
{"example": {
|
||||
"attrs": {
|
||||
"key1": "value1",
|
||||
...
|
||||
},
|
||||
...
|
||||
}}
|
||||
"""
|
||||
for key, value in sub_dict.items():
|
||||
if isinstance(value, list):
|
||||
for repeated_element in value:
|
||||
sub_element = ET.SubElement(parent, key)
|
||||
_add_element_attrs(sub_element, repeated_element.get("attrs", {}))
|
||||
children = repeated_element.get("children", None)
|
||||
if isinstance(children, dict):
|
||||
_add_sub_elements_from_dict(sub_element, children)
|
||||
elif isinstance(children, str):
|
||||
sub_element.text = children
|
||||
else:
|
||||
sub_element = ET.SubElement(parent, key)
|
||||
_add_element_attrs(sub_element, value.get("attrs", {}))
|
||||
children = value.get("children", None)
|
||||
if isinstance(children, dict):
|
||||
_add_sub_elements_from_dict(sub_element, children)
|
||||
elif isinstance(children, str):
|
||||
sub_element.text = children
|
||||
|
||||
|
||||
def _add_element_attrs(elem, attrs):
|
||||
"""
|
||||
Add attributes to the given element.
|
||||
|
||||
:param elem: ElementTree.Element: The element the attributes are being added to.
|
||||
:param attrs: dict: A dictionary of attributes. e.g.:
|
||||
{"attribute1": "value", "attribute2": "another"}
|
||||
:return: ElementTree.Element
|
||||
"""
|
||||
for attr, value in attrs.items():
|
||||
elem.attrib[attr] = value
|
||||
return elem
|
|
@ -1 +0,0 @@
|
|||
Django
|
36
setup.cfg
36
setup.cfg
|
@ -1,2 +1,36 @@
|
|||
[wheel]
|
||||
[metadata]
|
||||
name = django-push-notifications
|
||||
version = 1.6.0
|
||||
description = Send push notifications to mobile devices through GCM, APNS or WNS and to WebPush (Chrome, Firefox and Opera) in Django
|
||||
author = Jerome Leclanche
|
||||
author_email = jerome@leclan.ch
|
||||
url = https://github.com/jazzband/django-push-notifications
|
||||
download_url = https://github.com/jazzband/django-push-notifications/tarball/master
|
||||
classifiers =
|
||||
Development Status :: 5 - Production/Stable
|
||||
Environment :: Web Environment
|
||||
Framework :: Django
|
||||
Framework :: Django :: 1.11
|
||||
Framework :: Django :: 2.0
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Topic :: Internet :: WWW/HTTP
|
||||
Topic :: System :: Networking
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
install_requires =
|
||||
pywebpush>=1.3.0
|
||||
Django>=1.11
|
||||
|
||||
[options.packages.find]
|
||||
exclude = tests
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
|
38
setup.py
38
setup.py
|
@ -1,40 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
import os.path
|
||||
import push_notifications
|
||||
from distutils.core import setup
|
||||
|
||||
README = open(os.path.join(os.path.dirname(__file__), "README.rst")).read()
|
||||
from setuptools import setup
|
||||
|
||||
CLASSIFIERS = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: System :: Networking",
|
||||
]
|
||||
|
||||
setup(
|
||||
name="django-push-notifications",
|
||||
packages=[
|
||||
"push_notifications",
|
||||
"push_notifications/api",
|
||||
"push_notifications/migrations",
|
||||
"push_notifications/management",
|
||||
"push_notifications/management/commands",
|
||||
],
|
||||
author=push_notifications.__author__,
|
||||
author_email=push_notifications.__email__,
|
||||
classifiers=CLASSIFIERS,
|
||||
description="Send push notifications to mobile devices through GCM or APNS in Django.",
|
||||
download_url="https://github.com/jleclanche/django-push-notifications/tarball/master",
|
||||
long_description=README,
|
||||
url="https://github.com/jleclanche/django-push-notifications",
|
||||
version=push_notifications.__version__,
|
||||
)
|
||||
setup()
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
from test_models import *
|
||||
from test_gcm_push_payload import *
|
||||
from test_apns_push_payload import *
|
||||
from test_management_commands import *
|
||||
|
||||
# conditionally test rest_framework api if the DRF package is installed
|
||||
try:
|
||||
import rest_framework
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
from test_rest_framework import *
|
|
@ -0,0 +1,4 @@
|
|||
try:
|
||||
from unittest import mock # noqa
|
||||
except ImportError:
|
||||
from mock import mock # noqa
|
|
@ -1,17 +0,0 @@
|
|||
GCM_PLAIN_RESPONSE = 'id=1:08'
|
||||
GCM_JSON_RESPONSE = '{"multicast_id":108,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"1:08"}]}'
|
||||
GCM_MULTIPLE_JSON_RESPONSE = ('{"multicast_id":108,"success":2,"failure":0,"canonical_ids":0,"results":'
|
||||
'[{"message_id":"1:08"}, {"message_id": "1:09"}]}')
|
||||
GCM_PLAIN_RESPONSE_ERROR = ['Error=NotRegistered', 'Error=InvalidRegistration']
|
||||
GCM_PLAIN_RESPONSE_ERROR_B = 'Error=MismatchSenderId'
|
||||
GCM_PLAIN_CANONICAL_ID_RESPONSE = "id=1:2342\nregistration_id=NEW_REGISTRATION_ID"
|
||||
GCM_JSON_RESPONSE_ERROR = ('{"success":1, "failure": 2, "canonical_ids": 0, "cast_id": 6358665107659088804, "results":'
|
||||
' [{"error": "NotRegistered"}, {"message_id": "0:1433830664381654%3449593ff9fd7ecd"}, '
|
||||
'{"error": "InvalidRegistration"}]}')
|
||||
GCM_JSON_RESPONSE_ERROR_B = ('{"success":1, "failure": 2, "canonical_ids": 0, "cast_id": 6358665107659088804, '
|
||||
'"results": [{"error": "MismatchSenderId"}, {"message_id": '
|
||||
'"0:1433830664381654%3449593ff9fd7ecd"}, {"error": "InvalidRegistration"}]}')
|
||||
GCM_DRF_INVALID_HEX_ERROR = {'device_id': [u"Device ID is not a valid hex number"]}
|
||||
GCM_DRF_OUT_OF_RANGE_ERROR = {'device_id': [u"Device ID is out of range"]}
|
||||
GCM_JSON_CANONICAL_ID_RESPONSE = '{"failure":0,"canonical_ids":1,"success":2,"multicast_id":7173139966327257000,"results":[{"registration_id":"NEW_REGISTRATION_ID","message_id":"0:1440068396670935%6868637df9fd7ecd"},{"message_id":"0:1440068396670937%6868637df9fd7ecd"}]}'
|
||||
GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE = '{"failure":0,"canonical_ids":1,"success":2,"multicast_id":7173139966327257000,"results":[{"registration_id":"bar","message_id":"0:1440068396670935%6868637df9fd7ecd"},{"message_id":"0:1440068396670937%6868637df9fd7ecd"}]}'
|
|
@ -0,0 +1,49 @@
|
|||
# flake8: noqa
|
||||
|
||||
|
||||
GCM_JSON = '{"cast_id":108,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"1:08"}]}'
|
||||
GCM_JSON_ERROR_NOTREGISTERED = (
|
||||
'{"failure": 1, "canonical_ids": 0, "cast_id": 6358665107659088804, "results":'
|
||||
' [{"error": "NotRegistered"}]}'
|
||||
)
|
||||
GCM_JSON_ERROR_INVALIDREGISTRATION = (
|
||||
'{"failure": 1, "canonical_ids": 0, "cast_id": 6358665107659088804, "results":'
|
||||
' [{"error": "InvalidRegistration"}]}'
|
||||
)
|
||||
GCM_JSON_ERROR_MISMATCHSENDERID = (
|
||||
'{"success":0, "failure": 1, "canonical_ids": 0, "results":'
|
||||
' [{"error": "MismatchSenderId"}]}'
|
||||
)
|
||||
GCM_JSON_CANONICAL_ID = (
|
||||
'{"failure":0,"canonical_ids":1,"success":1,"cast_id":7173139966327257000,"results":'
|
||||
'[{"registration_id":"NEW_REGISTRATION_ID","message_id":"0:1440068396670935%6868637df9fd7ecd"}]}'
|
||||
)
|
||||
GCM_JSON_CANONICAL_ID_SAME_DEVICE = (
|
||||
'{"failure":0,"canonical_ids":1,"success":1,"cast_id":7173139966327257000,"results":'
|
||||
'[{"registration_id":"bar","message_id":"0:1440068396670935%6868637df9fd7ecd"}]}'
|
||||
)
|
||||
|
||||
GCM_JSON_MULTIPLE = (
|
||||
'{"multicast_id":108,"success":2,"failure":0,"canonical_ids":0,"results":'
|
||||
'[{"message_id":"1:08"}, {"message_id": "1:09"}]}'
|
||||
)
|
||||
GCM_JSON_MULTIPLE_ERROR = (
|
||||
'{"success":1, "failure": 2, "canonical_ids": 0, "cast_id": 6358665107659088804, "results":'
|
||||
' [{"error": "NotRegistered"}, {"message_id": "0:1433830664381654%3449593ff9fd7ecd"}, '
|
||||
'{"error": "InvalidRegistration"}]}'
|
||||
)
|
||||
GCM_JSON_MULTIPLE_ERROR_B = (
|
||||
'{"success":1, "failure": 2, "canonical_ids": 0, "cast_id": 6358665107659088804, '
|
||||
'"results": [{"error": "MismatchSenderId"}, {"message_id": '
|
||||
'"0:1433830664381654%3449593ff9fd7ecd"}, {"error": "InvalidRegistration"}]}'
|
||||
)
|
||||
GCM_JSON_MULTIPLE_CANONICAL_ID = (
|
||||
'{"failure":0,"canonical_ids":1,"success":2,"multicast_id":7173139966327257000,"results":'
|
||||
'[{"registration_id":"NEW_REGISTRATION_ID","message_id":"0:1440068396670935%6868637df9fd7ecd"},'
|
||||
'{"message_id":"0:1440068396670937%6868637df9fd7ecd"}]}'
|
||||
)
|
||||
GCM_JSON_MULTIPLE_CANONICAL_ID_SAME_DEVICE = (
|
||||
'{"failure":0,"canonical_ids":1,"success":2,"multicast_id":7173139966327257000,'
|
||||
'"results":[{"registration_id":"bar","message_id":"0:1440068396670935%6868637df9fd7ecd"}'
|
||||
',{"message_id":"0:1440068396670937%6868637df9fd7ecd"}]}'
|
||||
)
|
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
def setup():
|
||||
"""
|
||||
set up test environment
|
||||
"""
|
||||
|
||||
# add test/src folders to sys path
|
||||
test_folder = os.path.abspath(os.path.dirname(__file__))
|
||||
src_folder = os.path.abspath(os.path.join(test_folder, os.pardir))
|
||||
sys.path.insert(0, test_folder)
|
||||
sys.path.insert(0, src_folder)
|
||||
|
||||
# define settings
|
||||
import django.conf
|
||||
os.environ[django.conf.ENVIRONMENT_VARIABLE] = "settings"
|
||||
|
||||
# set up environment
|
||||
from django.test.utils import setup_test_environment
|
||||
setup_test_environment()
|
||||
|
||||
# See https://docs.djangoproject.com/en/dev/releases/1.7/#app-loading-changes
|
||||
import django
|
||||
if django.VERSION >= (1, 7, 0):
|
||||
django.setup()
|
||||
|
||||
# set up database
|
||||
from django.db import connection
|
||||
connection.creation.create_test_db()
|
||||
|
||||
|
||||
def tear_down():
|
||||
"""
|
||||
tear down test environment
|
||||
"""
|
||||
|
||||
# destroy test database
|
||||
from django.db import connection
|
||||
connection.creation.destroy_test_db("not_needed")
|
||||
|
||||
# teardown environment
|
||||
from django.test.utils import teardown_test_environment
|
||||
teardown_test_environment()
|
||||
|
||||
|
||||
# fire in the hole!
|
||||
if __name__ == "__main__":
|
||||
setup()
|
||||
|
||||
import tests
|
||||
unittest.main(module=tests)
|
||||
|
||||
tear_down()
|
|
@ -1,7 +1,10 @@
|
|||
# assert warnings are enabled
|
||||
import warnings
|
||||
|
||||
|
||||
warnings.simplefilter("ignore", Warning)
|
||||
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
|
@ -21,3 +24,5 @@ SITE_ID = 1
|
|||
ROOT_URLCONF = "core.urls"
|
||||
|
||||
SECRET_KEY = "foobar"
|
||||
|
||||
PUSH_NOTIFICATIONS_SETTINGS = {}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import mock
|
||||
from django.test import TestCase
|
||||
from push_notifications.apns import _apns_send, APNSDataOverflow
|
||||
|
||||
|
||||
class APNSPushPayloadTest(TestCase):
|
||||
def test_push_payload(self):
|
||||
socket = mock.MagicMock()
|
||||
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
|
||||
_apns_send("123", "Hello world",
|
||||
badge=1, sound="chime", extra={"custom_data": 12345}, expiration=3, socket=socket)
|
||||
p.assert_called_once_with("123",
|
||||
b'{"aps":{"alert":"Hello world","badge":1,"sound":"chime"},"custom_data":12345}', 0, 3, 10)
|
||||
|
||||
def test_localised_push_with_empty_body(self):
|
||||
socket = mock.MagicMock()
|
||||
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
|
||||
_apns_send("123", None, loc_key="TEST_LOC_KEY", expiration=3, socket=socket)
|
||||
p.assert_called_once_with("123", b'{"aps":{"alert":{"loc-key":"TEST_LOC_KEY"}}}', 0, 3, 10)
|
||||
|
||||
def test_using_extra(self):
|
||||
socket = mock.MagicMock()
|
||||
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
|
||||
_apns_send("123", "sample", extra={"foo": "bar"}, identifier=10, expiration=30, priority=10, socket=socket)
|
||||
p.assert_called_once_with("123", b'{"aps":{"alert":"sample"},"foo":"bar"}', 10, 30, 10)
|
||||
|
||||
def test_oversized_payload(self):
|
||||
socket = mock.MagicMock()
|
||||
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
|
||||
self.assertRaises(APNSDataOverflow, _apns_send, "123", "_" * 2049, socket=socket)
|
||||
p.assert_has_calls([])
|
|
@ -0,0 +1,239 @@
|
|||
import os
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
|
||||
from push_notifications.conf import AppConfig
|
||||
|
||||
|
||||
class AppConfigTestCase(TestCase):
|
||||
def test_application_id_required(self):
|
||||
"""Using AppConfig without an application_id raises ImproperlyConfigured."""
|
||||
|
||||
manager = AppConfig()
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
manager._get_application_settings(None, None, None)
|
||||
|
||||
def test_application_not_found(self):
|
||||
"""
|
||||
Using AppConfig with an application_id that does not exist raises
|
||||
ImproperlyConfigured.
|
||||
"""
|
||||
|
||||
application_id = "my_fcm_app"
|
||||
|
||||
manager = AppConfig()
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
manager._get_application_settings(application_id, "FCM", "API_KEY")
|
||||
|
||||
def test_platform_configured(self):
|
||||
"""
|
||||
Using AppConfig with an application config that does not define PLATFORM
|
||||
raises ImproperlyConfigured.
|
||||
"""
|
||||
|
||||
application_id = "my_fcm_app"
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
application_id: {}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
def test_platform_invalid(self):
|
||||
"""
|
||||
Using AppConfig with an invalid platform raises ImproperlyConfigured.
|
||||
"""
|
||||
|
||||
application_id = "my_fcm_app"
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
application_id: {
|
||||
"PLATFORM": "XXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
def test_platform_invalid_setting(self):
|
||||
"""
|
||||
Fetching application settings for the wrong platform raises ImproperlyConfigured.
|
||||
"""
|
||||
|
||||
application_id = "my_fcm_app"
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
application_id: {
|
||||
"PLATFORM": "FCM",
|
||||
"API_KEY": "[my_api_key]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager = AppConfig(PUSH_SETTINGS)
|
||||
|
||||
def test_missing_setting(self):
|
||||
"""
|
||||
Missing application settings raises ImproperlyConfigured.
|
||||
"""
|
||||
|
||||
application_id = "my_fcm_app"
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
application_id: {
|
||||
"PLATFORM": "FCM"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
def test_get_allowed_settings_fcm(self):
|
||||
"""Verify the settings allowed for FCM platform."""
|
||||
|
||||
#
|
||||
# all settings specified, required and optional, does not raise an error.
|
||||
#
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_fcm_app": {
|
||||
"PLATFORM": "FCM",
|
||||
"API_KEY": "...",
|
||||
"POST_URL": "...",
|
||||
"MAX_RECIPIENTS": "...",
|
||||
"ERROR_TIMEOUT": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
# missing required settings
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_fcm_app": {
|
||||
"PLATFORM": "FCM",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
# all optional settings have default values
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_fcm_app": {
|
||||
"PLATFORM": "FCM",
|
||||
"API_KEY": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager = AppConfig(PUSH_SETTINGS)
|
||||
app_config = manager._settings["APPLICATIONS"]["my_fcm_app"]
|
||||
|
||||
assert app_config["POST_URL"] == "https://fcm.googleapis.com/fcm/send"
|
||||
assert app_config["MAX_RECIPIENTS"] == 1000
|
||||
assert app_config["ERROR_TIMEOUT"] is None
|
||||
|
||||
def test_get_allowed_settings_gcm(self):
|
||||
"""Verify the settings allowed for GCM platform."""
|
||||
|
||||
#
|
||||
# all settings specified, required and optional, does not raise an error.
|
||||
#
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_gcm_app": {
|
||||
"PLATFORM": "GCM",
|
||||
"API_KEY": "...",
|
||||
"POST_URL": "...",
|
||||
"MAX_RECIPIENTS": "...",
|
||||
"ERROR_TIMEOUT": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
#
|
||||
# missing required settings
|
||||
#
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_gcm_app": {
|
||||
"PLATFORM": "GCM",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
# all optional settings have default values
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_gcm_app": {
|
||||
"PLATFORM": "GCM",
|
||||
"API_KEY": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager = AppConfig(PUSH_SETTINGS)
|
||||
app_config = manager._settings["APPLICATIONS"]["my_gcm_app"]
|
||||
|
||||
assert app_config["POST_URL"] == "https://android.googleapis.com/gcm/send"
|
||||
assert app_config["MAX_RECIPIENTS"] == 1000
|
||||
assert app_config["ERROR_TIMEOUT"] is None
|
||||
|
||||
def test_get_allowed_settings_wns(self):
|
||||
"""
|
||||
Verify the settings allowed for WNS platform.
|
||||
"""
|
||||
|
||||
# all settings specified, required and optional, does not raise an error.
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_wns_app": {
|
||||
"PLATFORM": "WNS",
|
||||
"PACKAGE_SECURITY_ID": "...",
|
||||
"SECRET_KEY": "...",
|
||||
"WNS_ACCESS_URL": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
# missing required settings
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_wns_app": {
|
||||
"PLATFORM": "WNS",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
AppConfig(PUSH_SETTINGS)
|
||||
|
||||
# all optional settings have default values
|
||||
PUSH_SETTINGS = {
|
||||
"APPLICATIONS": {
|
||||
"my_wns_app": {
|
||||
"PLATFORM": "WNS",
|
||||
"PACKAGE_SECURITY_ID": "...",
|
||||
"SECRET_KEY": "...",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager = AppConfig(PUSH_SETTINGS)
|
||||
app_config = manager._settings["APPLICATIONS"]["my_wns_app"]
|
||||
|
||||
assert app_config["WNS_ACCESS_URL"] == "https://login.live.com/accesstoken.srf"
|
|
@ -0,0 +1,68 @@
|
|||
Bag Attributes
|
||||
friendlyName: Apple Development IOS Push Services: com.baseride.Magnitapp
|
||||
localKeyID: 38 EE 6A 28 9F 92 3A 56 3F 30 A4 06 13 94 4F 76 E7 BB 58 6C
|
||||
subject=/UID=com.baseride.Magnitapp/CN=Apple Development IOS Push Services: com.baseride.Magnitapp/OU=QAMD48Y2CA/C=US
|
||||
issuer=/C=US/O=Apple Inc./OU=Apple Worldwide Developer Relations/CN=Apple Worldwide Developer Relations Certification Authority
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFkTCCBHmgAwIBAgIIJjlosjhqgjswDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
|
||||
ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
|
||||
aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
|
||||
HhcNMTUxMTI0MTI0MTU3WhcNMTYxMTIzMTI0MTU3WjCBkDEmMCQGCgmSJomT8ixk
|
||||
AQEMFmNvbS5iYXNlcmlkZS5NYWduaXRhcHAxRDBCBgNVBAMMO0FwcGxlIERldmVs
|
||||
b3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uYmFzZXJpZGUuTWFnbml0YXBw
|
||||
MRMwEQYDVQQLDApRQU1ENDhZMkNBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBANFtOjCZMuhVhoENiR1d88xsBeKB2DxThcAhCntp
|
||||
yYSGeQyqszMgpkM4DWGrF/BmAzywAviS9Y4uyZ6WaawS0ViqWdRG4lL1riOKgCTV
|
||||
72H48lqKLwyRuQQTXCOSBsQgXhxxWuDc8nMaG5LgpJOqjHM+W7TzsM72KzDagdNg
|
||||
/hUqf16uXbqdfCXcRyL99wbPsAQ1TrI3wExTAmpEBpeo742IrhvM6/ApQeKlHoif
|
||||
u49mguTcyH4sFLPN+T2zmu2aS2rsgZryj1Mzq/yKE8q4l06O2z/ElVEcsCFPaYV0
|
||||
EF7nNEG4sw1kiUDEe7HTBFFpoRi1a9sJLoyThFXpJMwbIJ8CAwEAAaOCAeUwggHh
|
||||
MB0GA1UdDgQWBBQ47moon5I6Vj8wpAYTlE9257tYbDAJBgNVHRMEAjAAMB8GA1Ud
|
||||
IwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MIIBDwYDVR0gBIIBBjCCAQIwgf8G
|
||||
CSqGSIb3Y2QFATCB8TCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlz
|
||||
IGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2Yg
|
||||
dGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9u
|
||||
cyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBw
|
||||
cmFjdGljZSBzdGF0ZW1lbnRzLjApBggrBgEFBQcCARYdaHR0cDovL3d3dy5hcHBs
|
||||
ZS5jb20vYXBwbGVjYS8wTQYDVR0fBEYwRDBCoECgPoY8aHR0cDovL2RldmVsb3Bl
|
||||
ci5hcHBsZS5jb20vY2VydGlmaWNhdGlvbmF1dGhvcml0eS93d2RyY2EuY3JsMAsG
|
||||
A1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAQBgoqhkiG92NkBgMBBAIF
|
||||
ADANBgkqhkiG9w0BAQUFAAOCAQEAALUmZX306s/eEz7xeDA1WOP8iHDx7W3K4Pd7
|
||||
Aisw5ZYfoqZF8ZuX057j0myCz8BFdZeEeUjF/+DzrZ9MdZN/DmOgeekvSI9UlanE
|
||||
6v9APYL0I9AoQE2uoRYXvFJqrZoFtcg/htxyILVah+p9JxQ9M6OkdU/zTGyIX+QG
|
||||
hlCAJBuqEvGPrme/SYkqLv2p1WKw4IJ8IKaza6QI7MQYI1UMIabbBVbq2GxaJuEO
|
||||
0/lNjPFaNVfPcFNx74yHqCQVfz1hNUigjebqzxQ2A2qHzqIyU9FT1A1zE7HCYShe
|
||||
7UqEW+7kihGAW1T0KOhcX5xtLVVFzCrJhsAsVQehTKmB0nR9+A==
|
||||
-----END CERTIFICATE-----
|
||||
Bag Attributes
|
||||
friendlyName: PushNotificationCloudBus
|
||||
localKeyID: 38 EE 6A 28 9F 92 3A 56 3F 30 A4 06 13 94 4F 76 E7 BB 58 6C
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA0W06MJky6FWGgQ2JHV3zzGwF4oHYPFOFwCEKe2nJhIZ5DKqz
|
||||
MyCmQzgNYasX8GYDPLAC+JL1ji7JnpZprBLRWKpZ1EbiUvWuI4qAJNXvYfjyWoov
|
||||
DJG5BBNcI5IGxCBeHHFa4NzycxobkuCkk6qMcz5btPOwzvYrMNqB02D+FSp/Xq5d
|
||||
up18JdxHIv33Bs+wBDVOsjfATFMCakQGl6jvjYiuG8zr8ClB4qUeiJ+7j2aC5NzI
|
||||
fiwUs835PbOa7ZpLauyBmvKPUzOr/IoTyriXTo7bP8SVURywIU9phXQQXuc0Qbiz
|
||||
DWSJQMR7sdMEUWmhGLVr2wkujJOEVekkzBsgnwIDAQABAoIBACOs06jLsBxb1VnO
|
||||
kHjsNEeybx4yuD8uiy47cqmrT6S/s4cw3O3steXleoIUvzM4bXy9DwSBJEtgNQBK
|
||||
5x1k5zyPaFX87TjsmQl84m9j8i9iVQaPW4xslnPXSG7WxUhLqzx1IuIDQVnSLLhM
|
||||
hDyTZPGMwdqFWK0oyhq8Xjk/4IiCMcYG2M14jGVvEIsjMF246v+inAIpSUwZr1FD
|
||||
qzylj1FRnm0hTjXKIWrvumDiIodybFK5ruGbaKWlciokmyBaFXlt5JCzG1hrGetf
|
||||
wgg6gomjqSf7WuWILjWhHr6ZeNVKm8KdyOCs0csY1DSQj+CsLjUCF8fvE+59dN2k
|
||||
/u+qASECgYEA9Me6OcT6EhrbD1aqmDfg+DgFp6IOkP0We8Y2Z3Yg9fSzwRz0bZmE
|
||||
T9juuelhUxDph74fHvLmE0iMrueMIbWvzF8xGef0JIpvMVQmxvslzqRLFfPRclbA
|
||||
WoSWm8pzaI/X+tZetlQySoVVeS21HbzIEKnPdFBdkyC397xyV+iCQLsCgYEA2wao
|
||||
llTQ9TvQYDKad6T8RsgdCnk/BwLaq6EkLI+sNOBDRlzeSYbKkfqkwIPOhORe/ibg
|
||||
2OO76P8QrmqLg76hQlYK4i6k/Fwz3pRajdfQ6KxS7sOLm0x5aqrFXHVhKVnCD5C9
|
||||
PldJ2mOAowAEe7HMPcNeYbX9bW6T1hcslTKkI20CgYAJxkP4dJYrzOi8dxB+3ZRd
|
||||
NRd8tyrvvTt9m8+mWAA+8hOPfZGBIuU2rwnxYJFjWMSKiBwEB10KnhYIEfT1j6TC
|
||||
e3ahezKzlteT17FotrSuyL661K6jazVpJ+w/sljjbwMH4DGOBFSxxxs/qISX+Gbg
|
||||
y3ceROtHqcHO4baLLhytawKBgC9wosVk+5mSahDcBQ8TIj1mjLu/BULMgHaaQY6R
|
||||
U/hj9s5fwRnl4yx5QIQeSHYKTPT5kMwJj6Lo1EEi/LL9cEpA/rx84+lxQx7bvT1p
|
||||
2Gr9ID1tB2kMyGOtN3BOUEw3j8v1SrgdCfcOhEdJ8q6kFRvvnBrH42t3fvfpLxPl
|
||||
0x2FAoGAbSkII3zPpc8mRcD3rtyOl2TlahBtXMuxfWSxsg/Zwf47crBfEbLD+5wz
|
||||
7A9qnfwiDO98GJyE5/s+ynnL2PhIomCm0P13nkZuC4d9twYMtEvcD20mdQ+gsEhz
|
||||
Eg8ssRvYkO8DQwAFJKJVwVtVqMcnm/fkWu8GIfgqH6/fWNev6vs=
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,66 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFkTCCBHmgAwIBAgIIRc+fhlv8zowwDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
|
||||
ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
|
||||
aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
|
||||
HhcNMTUxMjE1MTIzMDE3WhcNMTYxMjE0MTIzMDE3WjCBkDEmMCQGCgmSJomT8ixk
|
||||
AQEMFmNvbS5iYXNlcmlkZS5NYWduaXRhcHAxRDBCBgNVBAMMO0FwcGxlIERldmVs
|
||||
b3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uYmFzZXJpZGUuTWFnbml0YXBw
|
||||
MRMwEQYDVQQLDApRQU1ENDhZMkNBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBAJq6041XOdS4wTOT6UeWVKr6DqZFsYSTA8TFVyqT
|
||||
cZYc19KWi9gQ2NK+WwsoRxHMmtAdZxYMTecMlqD/B4r3aiNpMjZWV8x25ymjwlGa
|
||||
2zLZJ6y05/j2YDAk5mNSCensQmKOB4aJ0MtCnCbONDY1GDlB1PXMqs9VsWkI+glC
|
||||
T4DF0PdF6cWqeR1SRm0vm32WHBX4RkMJp4QxE2jYDS0ENWTnkqOQ0JLLk2eb/2Lq
|
||||
Tk0/F7wemyOsmYpscSnuwtYM0zkl2un5eWQR0pzpBStvVQP7TWyQPmEnasIGccWK
|
||||
LBftpJTCvG9eJkJhyH9UtoKMFq7r58WfggdLb/mL9ZAf+7cCAwEAAaOCAeUwggHh
|
||||
MB0GA1UdDgQWBBTGL6K5Ta3vxjOLdQTBY/wDTMYpbTAJBgNVHRMEAjAAMB8GA1Ud
|
||||
IwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MIIBDwYDVR0gBIIBBjCCAQIwgf8G
|
||||
CSqGSIb3Y2QFATCB8TCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlz
|
||||
IGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2Yg
|
||||
dGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9u
|
||||
cyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBw
|
||||
cmFjdGljZSBzdGF0ZW1lbnRzLjApBggrBgEFBQcCARYdaHR0cDovL3d3dy5hcHBs
|
||||
ZS5jb20vYXBwbGVjYS8wTQYDVR0fBEYwRDBCoECgPoY8aHR0cDovL2RldmVsb3Bl
|
||||
ci5hcHBsZS5jb20vY2VydGlmaWNhdGlvbmF1dGhvcml0eS93d2RyY2EuY3JsMAsG
|
||||
A1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAQBgoqhkiG92NkBgMBBAIF
|
||||
ADANBgkqhkiG9w0BAQUFAAOCAQEAayrzBuIGSZnIMbF+DhAlWeJNNimNSFOWX7X8
|
||||
f3YKytp8F5LpvX1kvDuJwntyXgdBsziQYtMG/RweQ9DZSYnCEzWDCQFckn9F67Eb
|
||||
fj1ohidh+sPgfsuV8sx5Rm9wvCdg1jSSrqxnHMDxuReX+d8DjU9e2Z1fqd7wcEbk
|
||||
LJUWxBR7+0KGYOqqUa5OrGS7tYcZj0v7f3xJyqsYVFSfYk1h7WoTr11bCSdCV+0y
|
||||
zzeVLQB4RQLQt+LLb1OZlj5NdM73fSicTwy3ihFxvWPTDozxfCBvIgLT3PYJBc3k
|
||||
NonhJADFD84YUTCnZC/xreAbjY7ss90fKLzxJfzMB+Wskf/EKQ==
|
||||
-----END CERTIFICATE-----
|
||||
Bag Attributes
|
||||
friendlyName: CloudTrack Push
|
||||
localKeyID: C6 2F A2 B9 4D AD EF C6 33 8B 75 04 C1 63 FC 03 4C C6 29 6D
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,61A87CB1762663CC
|
||||
|
||||
z8z9Q9eVhRazYHnvx1LOJtWp9v7UaX/YluJ8qFuH8QG1cRbn5wxYqz61ZECDNQIl
|
||||
bUYRW95QQ1GO8PpNzJ+0z0tyJ63TuzZvg1GlhGtDSCpQaRfS1SGypIYsejnEwsUN
|
||||
IppR63g4TJALeP80KFGetIGhvpUURiAhRV+HV44naMkUfExa12YJs6b7ZLRN4uDz
|
||||
JWl41t+h/nvXKgNtIyVQIMj6rTkrLNE0YQ8fgerc8L7XSYOg0mdpp3CyLn9rkrWI
|
||||
rCbxjHyudT+LzJZBr0KWJZ2FvJp3KGVGAtGhUJ3biuRqKw6syUlCSDEaRmYSiI4C
|
||||
GvINoBMag7nXS6lbsEgpS8+N43tT13uxmzNDax/5ASMXuslFaD/s2GbcUXxWv2YL
|
||||
+GybNO83C8TzUDavWEzUBcbWdboim+Rh2HELPFNt1fEUzyj7ekqboT5YTQ4ceLJY
|
||||
dgjM9kNCKYum8Gfy5gfXPSwIGOKPo6hssHMEOVjDLM3169POfRc11KWIU4NEGZP8
|
||||
4CML5mrYdP/y3KziPDyXRUvlwGNJh9mr7ucqyjfLd5fBrYZit2jE2zlD8H8UQf1F
|
||||
0VMmw6Szc6pimxhLOqXu1jHfCvP9s9w5dY8s2MFKS5trevsXNI5RzDuF2cBlGk0l
|
||||
3x3akNkq10jiqsN/v+BWpmhEMhf46/BqLDGIXBsDGkqVpjunJ9Hn+lc4Fkwtq73d
|
||||
LSLcPit3WRifgd9NX5BJKoSEalyCHnsteWFteS+W3J1lEnH1E8Vri6V5aOefwcFI
|
||||
lDn34XTB/huzi31p6301vhGftI4+qcYVm322TcSvyMR4jwZ28UCMrgFa+RH4Xn4n
|
||||
W0OmbaDjDzwvXkh9RlgTyuLQNR64ZVb27kYsUGumoNmg7DbpmM6PCaTTKyMw6Tgo
|
||||
CvGZ/cdpGOcgwFVM40HaIFsH12QIUeAkepHWuzkvhUlAv9mAOZtgV8q5er0kPPBs
|
||||
AgTIqCWJtlkU54HGdToR59TlENKAVxO2+v98T8I28NvduMYiwP94Ihfd/pto1tAg
|
||||
Ovwb3HudRNuGO/IrQokTY9B7yyldZ9YIC5suJwQ+1M06HK3D4E81GsfifQvUoZjD
|
||||
4foEf+gEdHt3ayUk98oHw9k/LNKkZhRviBHvFR7NnCTY77EX4LfHR0E3h2DzWIU+
|
||||
oGC8InbtN9eV8o05SiRusM3zGK9qn3nHmw/KjjO6K+FxwnoaKHYYOyL1Xdu/CJGR
|
||||
0+vLKqIUTOoVK0Ox3yj2zaJWpm5rgKdTxhCoopS4LoHP+J7h24LwxcCk/rVTd6o5
|
||||
YIX5MyyW7e17uB96KYPwFioCSpFQKECd929r71Mm6uitf34/FIUBoglJozuOKXDf
|
||||
dnzMVRqLNA8qdX1+sN5XQeyBjFp2eokiampycIyo07buU6khEemZxvGOfQsbpHD6
|
||||
AuBk4Dj/3oYqlXWmg2aGiUHsERbHnwcHGNP1QBMFnZtZTGYiK4dc5JS90drDCIXI
|
||||
c1XpnJ6f5yqQZS8eUMO6x+cUxYRqCvsPyZPTP07J3zem4i/os20S5tJXH4PctzuX
|
||||
YQ5JiUOVkPFG77gw1Cq/WKLppS3k7+VRcNbX9wWZb6fs/Ruo1STtPG4llFNC8DcG
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,32 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFkTCCBHmgAwIBAgIIJjlosjhqgjswDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
|
||||
ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
|
||||
aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
|
||||
HhcNMTUxMTI0MTI0MTU3WhcNMTYxMTIzMTI0MTU3WjCBkDEmMCQGCgmSJomT8ixk
|
||||
AQEMFmNvbS5iYXNlcmlkZS5NYWduaXRhcHAxRDBCBgNVBAMMO0FwcGxlIERldmVs
|
||||
b3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uYmFzZXJpZGUuTWFnbml0YXBw
|
||||
MRMwEQYDVQQLDApRQU1ENDhZMkNBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBANFtOjCZMuhVhoENiR1d88xsBeKB2DxThcAhCntp
|
||||
yYSGeQyqszMgpkM4DWGrF/BmAzywAviS9Y4uyZ6WaawS0ViqWdRG4lL1riOKgCTV
|
||||
72H48lqKLwyRuQQTXCOSBsQgXhxxWuDc8nMaG5LgpJOqjHM+W7TzsM72KzDagdNg
|
||||
/hUqf16uXbqdfCXcRyL99wbPsAQ1TrI3wExTAmpEBpeo742IrhvM6/ApQeKlHoif
|
||||
u49mguTcyH4sFLPN+T2zmu2aS2rsgZryj1Mzq/yKE8q4l06O2z/ElVEcsCFPaYV0
|
||||
EF7nNEG4sw1kiUDEe7HTBFFpoRi1a9sJLoyThFXpJMwbIJ8CAwEAAaOCAeUwggHh
|
||||
MB0GA1UdDgQWBBQ47moon5I6Vj8wpAYTlE9257tYbDAJBgNVHRMEAjAAMB8GA1Ud
|
||||
IwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MIIBDwYDVR0gBIIBBjCCAQIwgf8G
|
||||
CSqGSIb3Y2QFATCB8TCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlz
|
||||
IGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2Yg
|
||||
dGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9u
|
||||
cyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBw
|
||||
cmFjdGljZSBzdGF0ZW1lbnRzLjApBggrBgEFBQcCARYdaHR0cDovL3d3dy5hcHBs
|
||||
ZS5jb20vYXBwbGVjYS8wTQYDVR0fBEYwRDBCoECgPoY8aHR0cDovL2RldmVsb3Bl
|
||||
ci5hcHBsZS5jb20vY2VydGlmaWNhdGlvbmF1dGhvcml0eS93d2RyY2EuY3JsMAsG
|
||||
A1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAQBgoqhkiG92NkBgMBBAIF
|
||||
ADANBgkqhkiG9w0BAQUFAAOCAQEAALUmZX306s/eEz7xeDA1WOP8iHDx7W3K4Pd7
|
||||
Aisw5ZYfoqZF8ZuX057j0myCz8BFdZeEeUjF/+DzrZ9MdZN/DmOgeekvSI9UlanE
|
||||
6v9APYL0I9AoQE2uoRYXvFJqrZoFtcg/htxyILVah+p9JxQ9M6OkdU/zTGyIX+QG
|
||||
hlCAJBuqEvGPrme/SYkqLv2p1WKw4IJ8IKaza6QI7MQYI1UMIabbBVbq2GxaJuEO
|
||||
0/lNjPFaNVfPcFNx74yHqCQVfz1hNUigjebqzxQ2A2qHzqIyU9FT1A1zE7HCYShe
|
||||
7UqEW+7kihGAW1T0KOhcX5xtLVVFzCrJhsAsVQehTKmB0nR9+A==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,28 +1,77 @@
|
|||
import mock
|
||||
import json
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.test import TestCase
|
||||
from push_notifications.gcm import gcm_send_message, gcm_send_bulk_message
|
||||
from tests.mock_responses import GCM_PLAIN_RESPONSE, GCM_JSON_RESPONSE
|
||||
|
||||
from push_notifications.gcm import send_bulk_message, send_message
|
||||
|
||||
from ._mock import mock
|
||||
from .responses import GCM_JSON, GCM_JSON_MULTIPLE
|
||||
|
||||
|
||||
class GCMPushPayloadTest(TestCase):
|
||||
def test_push_payload(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
|
||||
gcm_send_message("abc", {"message": "Hello world"})
|
||||
p.assert_called_once_with(
|
||||
b"data.message=Hello+world®istration_id=abc",
|
||||
"application/x-www-form-urlencoded;charset=UTF-8")
|
||||
|
||||
def test_push_payload_params(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
|
||||
gcm_send_message("abc", {"message": "Hello world"}, delay_while_idle=True, time_to_live=3600)
|
||||
def test_fcm_push_payload(self):
|
||||
with mock.patch("push_notifications.gcm._fcm_send", return_value=GCM_JSON) as p:
|
||||
send_message("abc", {"message": "Hello world"}, "FCM")
|
||||
p.assert_called_once_with(
|
||||
b"data.message=Hello+world&delay_while_idle=1®istration_id=abc&time_to_live=3600",
|
||||
"application/x-www-form-urlencoded;charset=UTF-8")
|
||||
b'{"notification":{"body":"Hello world"},"registration_ids":["abc"]}',
|
||||
"application/json", application_id=None)
|
||||
|
||||
def test_bulk_push_payload(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_JSON_RESPONSE) as p:
|
||||
gcm_send_bulk_message(["abc", "123"], {"message": "Hello world"})
|
||||
def test_push_payload_with_app_id(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_JSON) as p:
|
||||
send_message("abc", {"message": "Hello world"}, "GCM")
|
||||
p.assert_called_once_with(
|
||||
b'{"data":{"message":"Hello world"},"registration_ids":["abc"]}',
|
||||
"application/json", application_id=None)
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_JSON) as p:
|
||||
send_message("abc", {"message": "Hello world"}, "GCM")
|
||||
p.assert_called_once_with(
|
||||
b'{"data":{"message":"Hello world"},"registration_ids":["abc"]}',
|
||||
"application/json", application_id=None)
|
||||
|
||||
def test_fcm_push_payload_params(self):
|
||||
with mock.patch("push_notifications.gcm._fcm_send", return_value=GCM_JSON) as p:
|
||||
send_message(
|
||||
"abc",
|
||||
{"message": "Hello world", "title": "Push notification", "other": "misc"},
|
||||
"FCM",
|
||||
delay_while_idle=True, time_to_live=3600, foo="bar",
|
||||
)
|
||||
p.assert_called_once_with(
|
||||
b'{"data":{"other":"misc"},"delay_while_idle":true,'
|
||||
b'"notification":{"body":"Hello world","title":"Push notification"},'
|
||||
b'"registration_ids":["abc"],"time_to_live":3600}',
|
||||
"application/json", application_id=None)
|
||||
|
||||
def test_fcm_push_payload_many(self):
|
||||
with mock.patch("push_notifications.gcm._fcm_send", return_value=GCM_JSON_MULTIPLE) as p:
|
||||
send_bulk_message(["abc", "123"], {"message": "Hello world"}, "FCM")
|
||||
p.assert_called_once_with(
|
||||
b'{"notification":{"body":"Hello world"},"registration_ids":["abc","123"]}',
|
||||
"application/json", application_id=None)
|
||||
|
||||
def test_gcm_push_payload(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_JSON) as p:
|
||||
send_message("abc", {"message": "Hello world"}, "GCM")
|
||||
p.assert_called_once_with(
|
||||
b'{"data":{"message":"Hello world"},"registration_ids":["abc"]}',
|
||||
"application/json", application_id=None)
|
||||
|
||||
def test_gcm_push_payload_params(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_JSON) as p:
|
||||
send_message(
|
||||
"abc", {"message": "Hello world"}, "GCM",
|
||||
delay_while_idle=True, time_to_live=3600, foo="bar",
|
||||
)
|
||||
p.assert_called_once_with(
|
||||
b'{"data":{"message":"Hello world"},"delay_while_idle":true,'
|
||||
b'"registration_ids":["abc"],"time_to_live":3600}',
|
||||
"application/json", application_id=None)
|
||||
|
||||
def test_gcm_push_payload_many(self):
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_JSON_MULTIPLE) as p:
|
||||
send_bulk_message(["abc", "123"], {"message": "Hello world"}, "GCM")
|
||||
p.assert_called_once_with(
|
||||
b'{"data":{"message":"Hello world"},"registration_ids":["abc","123"]}',
|
||||
"application/json")
|
||||
"application/json",
|
||||
application_id=None)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
|
||||
from push_notifications.conf import LegacyConfig
|
||||
|
||||
|
||||
class LegacyConfigTestCase(TestCase):
|
||||
def test_get_error_timeout(self):
|
||||
|
||||
config = LegacyConfig()
|
||||
|
||||
# confirm default value is None
|
||||
assert config.get_error_timeout("GCM") is None
|
||||
|
||||
# confirm default value is None
|
||||
assert config.get_error_timeout("FCM") is None
|
||||
|
||||
# confirm legacy does not support GCM with an application_id
|
||||
with self.assertRaises(ImproperlyConfigured) as ic:
|
||||
config.get_error_timeout("GCM", "my_app_id")
|
||||
|
||||
self.assertEqual(
|
||||
str(ic.exception),
|
||||
"LegacySettings does not support application_id. To enable multiple"
|
||||
" application support, use push_notifications.conf.AppSettings."
|
||||
)
|
||||
|
||||
# confirm legacy does not support FCM with an application_id
|
||||
with self.assertRaises(ImproperlyConfigured) as ic:
|
||||
config.get_error_timeout("FCM", "my_app_id")
|
||||
|
||||
self.assertEqual(
|
||||
str(ic.exception),
|
||||
"LegacySettings does not support application_id. To enable multiple"
|
||||
" application support, use push_notifications.conf.AppSettings."
|
||||
)
|
|
@ -1,25 +0,0 @@
|
|||
import mock
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
from django.test import TestCase
|
||||
from push_notifications.apns import _apns_send, APNSDataOverflow
|
||||
|
||||
|
||||
class CommandsTestCase(TestCase):
|
||||
|
||||
def test_prune_devices(self):
|
||||
from push_notifications.models import APNSDevice
|
||||
|
||||
device = APNSDevice.objects.create(
|
||||
registration_id="616263", # hex encoding of b'abc'
|
||||
)
|
||||
with mock.patch(
|
||||
'push_notifications.apns._apns_create_socket_to_feedback',
|
||||
mock.MagicMock()):
|
||||
with mock.patch('push_notifications.apns._apns_receive_feedback',
|
||||
mock.MagicMock()) as receiver:
|
||||
receiver.side_effect = lambda s: [(b'', b'abc')]
|
||||
call_command('prune_devices')
|
||||
device = APNSDevice.objects.get(pk=device.pk)
|
||||
self.assertFalse(device.active)
|
|
@ -1,232 +1,462 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from push_notifications.models import GCMDevice, APNSDevice
|
||||
from tests.mock_responses import ( GCM_PLAIN_RESPONSE,GCM_MULTIPLE_JSON_RESPONSE, GCM_PLAIN_RESPONSE_ERROR,
|
||||
GCM_JSON_RESPONSE_ERROR, GCM_PLAIN_RESPONSE_ERROR_B, GCM_JSON_RESPONSE_ERROR_B,
|
||||
GCM_PLAIN_CANONICAL_ID_RESPONSE, GCM_JSON_CANONICAL_ID_RESPONSE,
|
||||
GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE)
|
||||
from push_notifications.gcm import GCMError
|
||||
|
||||
from push_notifications.gcm import GCMError, send_bulk_message
|
||||
from push_notifications.models import GCMDevice
|
||||
|
||||
from . import responses
|
||||
from ._mock import mock
|
||||
|
||||
|
||||
class ModelTestCase(TestCase):
|
||||
def test_can_save_gcm_device(self):
|
||||
device = GCMDevice.objects.create(
|
||||
registration_id="a valid registration id"
|
||||
)
|
||||
assert device.id is not None
|
||||
assert device.date_created is not None
|
||||
assert device.date_created.date() == timezone.now().date()
|
||||
class GCMModelTestCase(TestCase):
|
||||
def _create_devices(self, devices):
|
||||
for device in devices:
|
||||
GCMDevice.objects.create(registration_id=device, cloud_message_type="GCM")
|
||||
|
||||
def test_can_create_save_device(self):
|
||||
device = APNSDevice.objects.create(
|
||||
registration_id="a valid registration id"
|
||||
)
|
||||
assert device.id is not None
|
||||
assert device.date_created is not None
|
||||
assert device.date_created.date() == timezone.now().date()
|
||||
def _create_fcm_devices(self, devices):
|
||||
for device in devices:
|
||||
GCMDevice.objects.create(registration_id=device, cloud_message_type="FCM")
|
||||
|
||||
def test_gcm_send_message(self):
|
||||
device = GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
|
||||
device.send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
b"data.message=Hello+world®istration_id=abc",
|
||||
"application/x-www-form-urlencoded;charset=UTF-8")
|
||||
def test_can_save_gcm_device(self):
|
||||
device = GCMDevice.objects.create(
|
||||
registration_id="a valid registration id", cloud_message_type="GCM"
|
||||
)
|
||||
assert device.id is not None
|
||||
assert device.date_created is not None
|
||||
assert device.date_created.date() == timezone.now().date()
|
||||
|
||||
def test_gcm_send_message_extra(self):
|
||||
device = GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
|
||||
device.send_message("Hello world", extra={"foo": "bar"})
|
||||
p.assert_called_once_with(
|
||||
b"data.foo=bar&data.message=Hello+world®istration_id=abc",
|
||||
"application/x-www-form-urlencoded;charset=UTF-8")
|
||||
def test_gcm_send_message(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="GCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": {"message": "Hello world"},
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_gcm_send_message_collapse_key(self):
|
||||
device = GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
|
||||
device.send_message("Hello world", collapse_key="test_key")
|
||||
p.assert_called_once_with(
|
||||
b"collapse_key=test_key&data.message=Hello+world®istration_id=abc",
|
||||
"application/x-www-form-urlencoded;charset=UTF-8")
|
||||
def test_gcm_send_message_extra(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="GCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world", extra={"foo": "bar"}, collapse_key="test_key")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"collapse_key": "test_key",
|
||||
"data": {"message": "Hello world", "foo": "bar"},
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_gcm_send_message_to_multiple_devices(self):
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
def test_gcm_send_message_collapse_key(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="GCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world", collapse_key="test_key")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": {"message": "Hello world"},
|
||||
"registration_ids": ["abc"],
|
||||
"collapse_key": "test_key"
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc1",
|
||||
)
|
||||
def test_gcm_send_message_to_multiple_devices(self):
|
||||
self._create_devices(["abc", "abc1"])
|
||||
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": { "message": "Hello world" },
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_MULTIPLE
|
||||
) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": {"message": "Hello world"},
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_gcm_send_message_active_devices(self):
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
active=True
|
||||
)
|
||||
def test_gcm_send_message_active_devices(self):
|
||||
GCMDevice.objects.create(registration_id="abc", active=True, cloud_message_type="GCM")
|
||||
GCMDevice.objects.create(registration_id="xyz", active=False, cloud_message_type="GCM")
|
||||
|
||||
GCMDevice.objects.create(
|
||||
registration_id="xyz",
|
||||
active=False
|
||||
)
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_MULTIPLE
|
||||
) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": {"message": "Hello world"},
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": { "message": "Hello world" },
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
|
||||
def test_gcm_send_message_collapse_to_multiple_devices(self):
|
||||
self._create_devices(["abc", "abc1"])
|
||||
|
||||
def test_gcm_send_message_extra_to_multiple_devices(self):
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_MULTIPLE
|
||||
) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world", collapse_key="test_key")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"collapse_key": "test_key",
|
||||
"data": {"message": "Hello world"},
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc1",
|
||||
)
|
||||
def test_gcm_send_message_to_single_device_with_error(self):
|
||||
# these errors are device specific, device.active will be set false
|
||||
devices = ["abc", "abc1"]
|
||||
self._create_devices(devices)
|
||||
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world", extra={"foo": "bar"})
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": { "foo": "bar", "message": "Hello world" },
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
|
||||
errors = [
|
||||
responses.GCM_JSON_ERROR_NOTREGISTERED,
|
||||
responses.GCM_JSON_ERROR_INVALIDREGISTRATION
|
||||
]
|
||||
for index, error in enumerate(errors):
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=error):
|
||||
device = GCMDevice.objects.get(registration_id=devices[index])
|
||||
device.send_message("Hello World!")
|
||||
assert GCMDevice.objects.get(registration_id=devices[index]).active is False
|
||||
|
||||
def test_gcm_send_message_collapse_to_multiple_devices(self):
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
def test_gcm_send_message_to_single_device_with_error_mismatch(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="GCM")
|
||||
|
||||
GCMDevice.objects.create(
|
||||
registration_id="abc1",
|
||||
)
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send",
|
||||
return_value=responses.GCM_JSON_ERROR_MISMATCHSENDERID
|
||||
):
|
||||
# these errors are not device specific, GCMError should be thrown
|
||||
with self.assertRaises(GCMError):
|
||||
device.send_message("Hello World!")
|
||||
assert GCMDevice.objects.get(registration_id="abc").active is True
|
||||
|
||||
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world", collapse_key="test_key")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"collapse_key": "test_key",
|
||||
"data": { "message": "Hello world" },
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
|
||||
def test_gcm_send_message_to_multiple_devices_with_error(self):
|
||||
self._create_devices(["abc", "abc1", "abc2"])
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_MULTIPLE_ERROR
|
||||
):
|
||||
devices = GCMDevice.objects.all()
|
||||
devices.send_message("Hello World")
|
||||
assert not GCMDevice.objects.get(registration_id="abc").active
|
||||
assert GCMDevice.objects.get(registration_id="abc1").active
|
||||
assert not GCMDevice.objects.get(registration_id="abc2").active
|
||||
|
||||
def test_gcm_send_message_to_single_device_with_error(self):
|
||||
# these errors are device specific, device.active will be set false
|
||||
device_list = ['abc', 'abc1']
|
||||
self.create_devices(device_list)
|
||||
for index, error in enumerate(GCM_PLAIN_RESPONSE_ERROR):
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=error) as p:
|
||||
device = GCMDevice.objects. \
|
||||
get(registration_id=device_list[index])
|
||||
device.send_message("Hello World!")
|
||||
assert GCMDevice.objects.get(registration_id=device_list[index]).active is False
|
||||
def test_gcm_send_message_to_multiple_devices_with_error_b(self):
|
||||
self._create_devices(["abc", "abc1", "abc2"])
|
||||
|
||||
def test_gcm_send_message_to_single_device_with_error_b(self):
|
||||
# these errors are not device specific, GCMError should be thrown
|
||||
device_list = ['abc']
|
||||
self.create_devices(device_list)
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=GCM_PLAIN_RESPONSE_ERROR_B) as p:
|
||||
device = GCMDevice.objects. \
|
||||
get(registration_id=device_list[0])
|
||||
with self.assertRaises(GCMError):
|
||||
device.send_message("Hello World!")
|
||||
assert GCMDevice.objects.get(registration_id=device_list[0]).active is True
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_MULTIPLE_ERROR_B
|
||||
):
|
||||
devices = GCMDevice.objects.all()
|
||||
with self.assertRaises(GCMError):
|
||||
devices.send_message("Hello World")
|
||||
assert GCMDevice.objects.get(registration_id="abc").active is True
|
||||
assert GCMDevice.objects.get(registration_id="abc1").active is True
|
||||
assert GCMDevice.objects.get(registration_id="abc2").active is False
|
||||
|
||||
def test_gcm_send_message_to_multiple_devices_with_error(self):
|
||||
device_list = ['abc', 'abc1', 'abc2']
|
||||
self.create_devices(device_list)
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=GCM_JSON_RESPONSE_ERROR) as p:
|
||||
devices = GCMDevice.objects.all()
|
||||
devices.send_message("Hello World")
|
||||
assert GCMDevice.objects.get(registration_id=device_list[0]).active is False
|
||||
assert GCMDevice.objects.get(registration_id=device_list[1]).active is True
|
||||
assert GCMDevice.objects.get(registration_id=device_list[2]).active is False
|
||||
def test_gcm_send_message_to_multiple_devices_with_canonical_id(self):
|
||||
self._create_devices(["foo", "bar"])
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_MULTIPLE_CANONICAL_ID
|
||||
):
|
||||
GCMDevice.objects.all().send_message("Hello World")
|
||||
assert not GCMDevice.objects.filter(registration_id="foo").exists()
|
||||
assert GCMDevice.objects.filter(registration_id="bar").exists()
|
||||
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
|
||||
|
||||
def test_gcm_send_message_to_multiple_devices_with_error_b(self):
|
||||
device_list = ['abc', 'abc1', 'abc2']
|
||||
self.create_devices(device_list)
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=GCM_JSON_RESPONSE_ERROR_B) as p:
|
||||
devices = GCMDevice.objects.all()
|
||||
with self.assertRaises(GCMError):
|
||||
devices.send_message("Hello World")
|
||||
assert GCMDevice.objects.get(registration_id=device_list[0]).active is True
|
||||
assert GCMDevice.objects.get(registration_id=device_list[1]).active is True
|
||||
assert GCMDevice.objects.get(registration_id=device_list[2]).active is False
|
||||
def test_gcm_send_message_to_single_user_with_canonical_id(self):
|
||||
old_registration_id = "foo"
|
||||
self._create_devices([old_registration_id])
|
||||
|
||||
def test_gcm_send_message_to_multiple_devices_with_canonical_id(self):
|
||||
device_list = ['foo', 'bar']
|
||||
self.create_devices(device_list)
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=GCM_JSON_CANONICAL_ID_RESPONSE):
|
||||
GCMDevice.objects.all().send_message("Hello World")
|
||||
assert GCMDevice.objects.filter(registration_id=device_list[0]).exists() is False
|
||||
assert GCMDevice.objects.filter(registration_id=device_list[1]).exists() is True
|
||||
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send", return_value=responses.GCM_JSON_CANONICAL_ID
|
||||
):
|
||||
GCMDevice.objects.get(registration_id=old_registration_id).send_message("Hello World")
|
||||
assert not GCMDevice.objects.filter(registration_id=old_registration_id).exists()
|
||||
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists()
|
||||
|
||||
def test_gcm_send_message_to_single_user_with_canonical_id(self):
|
||||
old_registration_id = 'foo'
|
||||
self.create_devices([old_registration_id])
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=GCM_PLAIN_CANONICAL_ID_RESPONSE):
|
||||
GCMDevice.objects.get(registration_id=old_registration_id).send_message("Hello World")
|
||||
assert GCMDevice.objects.filter(registration_id=old_registration_id).exists() is False
|
||||
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
|
||||
def test_gcm_send_message_to_same_devices_with_canonical_id(self):
|
||||
first_device = GCMDevice.objects.create(
|
||||
registration_id="foo", active=True, cloud_message_type="GCM"
|
||||
)
|
||||
second_device = GCMDevice.objects.create(
|
||||
registration_id="bar", active=False, cloud_message_type="GCM"
|
||||
)
|
||||
|
||||
def test_gcm_send_message_to_same_devices_with_canonical_id(self):
|
||||
device_list = ['foo', 'bar']
|
||||
self.create_devices(device_list)
|
||||
first_device_pk = GCMDevice.objects.get(registration_id='foo').pk
|
||||
second_device_pk = GCMDevice.objects.get(registration_id='bar').pk
|
||||
with mock.patch("push_notifications.gcm._gcm_send",
|
||||
return_value=GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE):
|
||||
GCMDevice.objects.all().send_message("Hello World")
|
||||
first_device = GCMDevice.objects.get(pk=first_device_pk)
|
||||
second_device = GCMDevice.objects.get(pk=second_device_pk)
|
||||
assert first_device.active is False
|
||||
assert second_device.active is True
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._gcm_send",
|
||||
return_value=responses.GCM_JSON_CANONICAL_ID_SAME_DEVICE
|
||||
):
|
||||
GCMDevice.objects.all().send_message("Hello World")
|
||||
|
||||
def test_apns_send_message(self):
|
||||
device = APNSDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
socket = mock.MagicMock()
|
||||
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
|
||||
device.send_message("Hello world", socket=socket, expiration=1)
|
||||
p.assert_called_once_with("abc", b'{"aps":{"alert":"Hello world"}}', 0, 1, 10)
|
||||
assert first_device.active is True
|
||||
assert second_device.active is False
|
||||
|
||||
def test_apns_send_message_extra(self):
|
||||
device = APNSDevice.objects.create(
|
||||
registration_id="abc",
|
||||
)
|
||||
socket = mock.MagicMock()
|
||||
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
|
||||
device.send_message("Hello world", extra={"foo": "bar"}, socket=socket, identifier=1, expiration=2, priority=5)
|
||||
p.assert_called_once_with("abc", b'{"aps":{"alert":"Hello world"},"foo":"bar"}', 1, 2, 5)
|
||||
def test_gcm_send_message_with_no_reg_ids(self):
|
||||
self._create_devices(["abc", "abc1"])
|
||||
|
||||
def create_devices(self, devices):
|
||||
for device in devices:
|
||||
GCMDevice.objects.create(
|
||||
registration_id=device,
|
||||
)
|
||||
with mock.patch("push_notifications.gcm._cm_send_request", return_value="") as p:
|
||||
GCMDevice.objects.filter(registration_id="xyz").send_message("Hello World")
|
||||
p.assert_not_called()
|
||||
|
||||
with mock.patch("push_notifications.gcm._cm_send_request", return_value="") as p:
|
||||
reg_ids = [obj.registration_id for obj in GCMDevice.objects.all()]
|
||||
send_bulk_message(reg_ids, {"message": "Hello World"}, "GCM")
|
||||
p.assert_called_once_with(
|
||||
[u"abc", u"abc1"], {"message": "Hello World"}, cloud_type="GCM", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="FCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"notification": {"body": "Hello world"},
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_extra_data(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="FCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world", extra={"foo": "bar"})
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"data": {"foo": "bar"},
|
||||
"notification": {"body": "Hello world"},
|
||||
"registration_ids": ["abc"],
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json",
|
||||
application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_extra_options(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="FCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world", collapse_key="test_key", foo="bar")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"collapse_key": "test_key",
|
||||
"notification": {"body": "Hello world"},
|
||||
"registration_ids": ["abc"],
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json",
|
||||
application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_extra_notification(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="FCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message("Hello world", extra={"icon": "test_icon"}, title="test")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"notification": {"body": "Hello world", "title": "test", "icon": "test_icon"},
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_extra_options_and_notification_and_data(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="FCM")
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON
|
||||
) as p:
|
||||
device.send_message(
|
||||
"Hello world",
|
||||
extra={"foo": "bar", "icon": "test_icon"},
|
||||
title="test",
|
||||
collapse_key="test_key"
|
||||
)
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"notification": {"body": "Hello world", "title": "test", "icon": "test_icon"},
|
||||
"data": {"foo": "bar"},
|
||||
"registration_ids": ["abc"],
|
||||
"collapse_key": "test_key"
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_to_multiple_devices(self):
|
||||
self._create_fcm_devices(["abc", "abc1"])
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_MULTIPLE
|
||||
) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"notification": {"body": "Hello world"},
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_active_devices(self):
|
||||
GCMDevice.objects.create(registration_id="abc", active=True, cloud_message_type="FCM")
|
||||
GCMDevice.objects.create(registration_id="xyz", active=False, cloud_message_type="FCM")
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_MULTIPLE
|
||||
) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"notification": {"body": "Hello world"},
|
||||
"registration_ids": ["abc"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_collapse_to_multiple_devices(self):
|
||||
self._create_fcm_devices(["abc", "abc1"])
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_MULTIPLE
|
||||
) as p:
|
||||
GCMDevice.objects.all().send_message("Hello world", collapse_key="test_key")
|
||||
p.assert_called_once_with(
|
||||
json.dumps({
|
||||
"collapse_key": "test_key",
|
||||
"notification": {"body": "Hello world"},
|
||||
"registration_ids": ["abc", "abc1"]
|
||||
}, separators=(",", ":"), sort_keys=True).encode("utf-8"),
|
||||
"application/json", application_id=None
|
||||
)
|
||||
|
||||
def test_fcm_send_message_to_single_device_with_error(self):
|
||||
# these errors are device specific, device.active will be set false
|
||||
devices = ["abc", "abc1"]
|
||||
self._create_fcm_devices(devices)
|
||||
|
||||
errors = [
|
||||
responses.GCM_JSON_ERROR_NOTREGISTERED,
|
||||
responses.GCM_JSON_ERROR_INVALIDREGISTRATION
|
||||
]
|
||||
for index, error in enumerate(errors):
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=error):
|
||||
device = GCMDevice.objects.get(registration_id=devices[index])
|
||||
device.send_message("Hello World!")
|
||||
assert GCMDevice.objects.get(registration_id=devices[index]).active is False
|
||||
|
||||
def test_fcm_send_message_to_single_device_with_error_mismatch(self):
|
||||
device = GCMDevice.objects.create(registration_id="abc", cloud_message_type="FCM")
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send",
|
||||
return_value=responses.GCM_JSON_ERROR_MISMATCHSENDERID
|
||||
):
|
||||
# these errors are not device specific, GCMError should be thrown
|
||||
with self.assertRaises(GCMError):
|
||||
device.send_message("Hello World!")
|
||||
assert GCMDevice.objects.get(registration_id="abc").active is True
|
||||
|
||||
def test_fcm_send_message_to_multiple_devices_with_error(self):
|
||||
self._create_fcm_devices(["abc", "abc1", "abc2"])
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_MULTIPLE_ERROR
|
||||
):
|
||||
devices = GCMDevice.objects.all()
|
||||
devices.send_message("Hello World")
|
||||
assert not GCMDevice.objects.get(registration_id="abc").active
|
||||
assert GCMDevice.objects.get(registration_id="abc1").active
|
||||
assert not GCMDevice.objects.get(registration_id="abc2").active
|
||||
|
||||
def test_fcm_send_message_to_multiple_devices_with_error_b(self):
|
||||
self._create_fcm_devices(["abc", "abc1", "abc2"])
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_MULTIPLE_ERROR_B
|
||||
):
|
||||
devices = GCMDevice.objects.all()
|
||||
with self.assertRaises(GCMError):
|
||||
devices.send_message("Hello World")
|
||||
assert GCMDevice.objects.get(registration_id="abc").active is True
|
||||
assert GCMDevice.objects.get(registration_id="abc1").active is True
|
||||
assert GCMDevice.objects.get(registration_id="abc2").active is False
|
||||
|
||||
def test_fcm_send_message_to_multiple_devices_with_canonical_id(self):
|
||||
self._create_fcm_devices(["foo", "bar"])
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_MULTIPLE_CANONICAL_ID
|
||||
):
|
||||
GCMDevice.objects.all().send_message("Hello World")
|
||||
assert not GCMDevice.objects.filter(registration_id="foo").exists()
|
||||
assert GCMDevice.objects.filter(registration_id="bar").exists()
|
||||
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
|
||||
|
||||
def test_fcm_send_message_to_single_user_with_canonical_id(self):
|
||||
old_registration_id = "foo"
|
||||
self._create_fcm_devices([old_registration_id])
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send", return_value=responses.GCM_JSON_CANONICAL_ID
|
||||
):
|
||||
GCMDevice.objects.get(registration_id=old_registration_id).send_message("Hello World")
|
||||
assert not GCMDevice.objects.filter(registration_id=old_registration_id).exists()
|
||||
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists()
|
||||
|
||||
def test_fcm_send_message_to_same_devices_with_canonical_id(self):
|
||||
first_device = GCMDevice.objects.create(
|
||||
registration_id="foo", active=True, cloud_message_type="FCM"
|
||||
)
|
||||
second_device = GCMDevice.objects.create(
|
||||
registration_id="bar", active=False, cloud_message_type="FCM"
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"push_notifications.gcm._fcm_send",
|
||||
return_value=responses.GCM_JSON_CANONICAL_ID_SAME_DEVICE
|
||||
):
|
||||
GCMDevice.objects.all().send_message("Hello World")
|
||||
|
||||
assert first_device.active is True
|
||||
assert second_device.active is False
|
||||
|
||||
def test_fcm_send_message_with_no_reg_ids(self):
|
||||
self._create_fcm_devices(["abc", "abc1"])
|
||||
|
||||
with mock.patch("push_notifications.gcm._cm_send_request", return_value="") as p:
|
||||
GCMDevice.objects.filter(registration_id="xyz").send_message("Hello World")
|
||||
p.assert_not_called()
|
||||
|
||||
with mock.patch("push_notifications.gcm._cm_send_request", return_value="") as p:
|
||||
reg_ids = [obj.registration_id for obj in GCMDevice.objects.all()]
|
||||
send_bulk_message(reg_ids, {"message": "Hello World"}, "FCM")
|
||||
p.assert_called_once_with(
|
||||
[u"abc", u"abc1"], {"message": "Hello World"}, cloud_type="FCM",
|
||||
application_id=None
|
||||
)
|
||||
|
||||
def test_can_save_wsn_device(self):
|
||||
device = GCMDevice.objects.create(registration_id="a valid registration id")
|
||||
self.assertIsNotNone(device.pk)
|
||||
self.assertIsNotNone(device.date_created)
|
||||
self.assertEqual(device.date_created.date(), timezone.now().date())
|
||||
|
|
|
@ -1,52 +1,12 @@
|
|||
from django.test import TestCase
|
||||
from push_notifications.api.rest_framework import APNSDeviceSerializer, GCMDeviceSerializer
|
||||
from rest_framework.serializers import ValidationError
|
||||
from tests.mock_responses import GCM_DRF_INVALID_HEX_ERROR, GCM_DRF_OUT_OF_RANGE_ERROR
|
||||
|
||||
from push_notifications.api.rest_framework import (
|
||||
GCMDeviceSerializer, ValidationError
|
||||
)
|
||||
|
||||
|
||||
class APNSDeviceSerializerTestCase(TestCase):
|
||||
def test_validation(self):
|
||||
# valid data - 32 bytes upper case
|
||||
serializer = APNSDeviceSerializer(data={
|
||||
"registration_id": "AEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAE",
|
||||
"name": "Apple iPhone 6+",
|
||||
"device_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||
})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
# valid data - 32 bytes lower case
|
||||
serializer = APNSDeviceSerializer(data={
|
||||
"registration_id": "aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae",
|
||||
"name": "Apple iPhone 6+",
|
||||
"device_id": "ffffffffffffffffffffffffffffffff",
|
||||
})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
# valid data - 100 bytes upper case
|
||||
serializer = APNSDeviceSerializer(data={
|
||||
"registration_id": "AEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAE",
|
||||
"name": "Apple iPhone 6+",
|
||||
"device_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||
})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
# valid data - 100 bytes lower case
|
||||
serializer = APNSDeviceSerializer(data={
|
||||
"registration_id": "aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae",
|
||||
"name": "Apple iPhone 6+",
|
||||
"device_id": "ffffffffffffffffffffffffffffffff",
|
||||
})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
# invalid data - device_id, registration_id
|
||||
serializer = APNSDeviceSerializer(data={
|
||||
"registration_id": "invalid device token contains no hex",
|
||||
"name": "Apple iPhone 6+",
|
||||
"device_id": "ffffffffffffffffffffffffffffake",
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors["device_id"][0], '"ffffffffffffffffffffffffffffake" is not a valid UUID.')
|
||||
self.assertEqual(serializer.errors["registration_id"][0], "Registration ID (device token) is invalid")
|
||||
GCM_DRF_INVALID_HEX_ERROR = {"device_id": [u"Device ID is not a valid hex number"]}
|
||||
GCM_DRF_OUT_OF_RANGE_ERROR = {"device_id": [u"Device ID is out of range"]}
|
||||
|
||||
|
||||
class GCMDeviceSerializerTestCase(TestCase):
|
||||
|
@ -55,6 +15,7 @@ class GCMDeviceSerializerTestCase(TestCase):
|
|||
"registration_id": "foobar",
|
||||
"name": "Galaxy Note 3",
|
||||
"device_id": "0x1031af3b",
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
|
@ -66,6 +27,7 @@ class GCMDeviceSerializerTestCase(TestCase):
|
|||
"registration_id": "foobar",
|
||||
"name": "Galaxy Note 3",
|
||||
"device_id": "0x1031af3b",
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
|
@ -75,6 +37,7 @@ class GCMDeviceSerializerTestCase(TestCase):
|
|||
"registration_id": "foobar",
|
||||
"name": "Galaxy Note 5",
|
||||
"device_id": "0x1031af3b",
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
|
@ -84,17 +47,18 @@ class GCMDeviceSerializerTestCase(TestCase):
|
|||
"registration_id": "foobar",
|
||||
"name": "Galaxy Note 3",
|
||||
"device_id": "0xdeadbeaf",
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError) as ex:
|
||||
with self.assertRaises(ValidationError):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.assertEqual({'registration_id': [u'This field must be unique.']}, ex.exception.detail)
|
||||
|
||||
def test_device_id_validation_fail_bad_hex(self):
|
||||
serializer = GCMDeviceSerializer(data={
|
||||
"registration_id": "foobar",
|
||||
"name": "Galaxy Note 3",
|
||||
"device_id": "0x10r",
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, GCM_DRF_INVALID_HEX_ERROR)
|
||||
|
@ -103,7 +67,8 @@ class GCMDeviceSerializerTestCase(TestCase):
|
|||
serializer = GCMDeviceSerializer(data={
|
||||
"registration_id": "foobar",
|
||||
"name": "Galaxy Note 3",
|
||||
"device_id": "10000000000000000", # 2**64
|
||||
"device_id": "10000000000000000", # 2**64
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertEqual(serializer.errors, GCM_DRF_OUT_OF_RANGE_ERROR)
|
||||
|
@ -116,5 +81,6 @@ class GCMDeviceSerializerTestCase(TestCase):
|
|||
"registration_id": "foobar",
|
||||
"name": "Nexus 5",
|
||||
"device_id": "e87a4e72d634997c",
|
||||
"application_id": "XXXXXXXXXXXXXXXXXXXX",
|
||||
})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
import xml.etree.ElementTree as ET
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from push_notifications.wns import (
|
||||
dict_to_xml_schema, wns_send_bulk_message, wns_send_message
|
||||
)
|
||||
|
||||
from ._mock import mock
|
||||
|
||||
|
||||
class WNSSendMessageTestCase(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@mock.patch("push_notifications.wns._wns_prepare_toast", return_value="this is expected")
|
||||
@mock.patch("push_notifications.wns._wns_send")
|
||||
def test_send_message_calls_wns_send_with_toast(self, mock_method, _):
|
||||
wns_send_message(uri="one", message="test message")
|
||||
mock_method.assert_called_with(
|
||||
application_id=None, uri="one", data="this is expected", wns_type="wns/toast"
|
||||
)
|
||||
|
||||
@mock.patch("push_notifications.wns._wns_prepare_toast", return_value="this is expected")
|
||||
@mock.patch("push_notifications.wns._wns_send")
|
||||
def test_send_message_calls_wns_send_with_application_id(self, mock_method, _):
|
||||
wns_send_message(uri="one", message="test message", application_id="123456")
|
||||
mock_method.assert_called_with(
|
||||
application_id="123456", uri="one", data="this is expected", wns_type="wns/toast"
|
||||
)
|
||||
|
||||
@mock.patch("push_notifications.wns.dict_to_xml_schema", return_value=ET.Element("toast"))
|
||||
@mock.patch("push_notifications.wns._wns_send")
|
||||
def test_send_message_calls_wns_send_with_xml(self, mock_method, _):
|
||||
wns_send_message(uri="one", xml_data={"key": "value"})
|
||||
mock_method.assert_called_with(
|
||||
application_id=None, uri="one", data=b"<toast />", wns_type="wns/toast"
|
||||
)
|
||||
|
||||
def test_send_message_raises_TypeError_if_one_of_the_data_params_arent_filled(self):
|
||||
with self.assertRaises(TypeError):
|
||||
wns_send_message(uri="one")
|
||||
|
||||
|
||||
class WNSSendBulkMessageTestCase(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@mock.patch("push_notifications.wns.wns_send_message")
|
||||
def test_send_bulk_message_doesnt_call_send_message_with_empty_list(self, mock_method):
|
||||
wns_send_bulk_message(uri_list=[], message="test message")
|
||||
mock_method.assert_not_called()
|
||||
|
||||
@mock.patch("push_notifications.wns.wns_send_message")
|
||||
def test_send_bulk_message_calls_send_message(self, mock_method):
|
||||
wns_send_bulk_message(uri_list=["one", ], message="test message")
|
||||
mock_method.assert_called_with(
|
||||
application_id=None, message="test message", raw_data=None, uri="one", xml_data=None
|
||||
)
|
||||
|
||||
|
||||
class WNSDictToXmlSchemaTestCase(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_create_simple_xml_from_dict(self):
|
||||
xml_data = {
|
||||
"toast": {
|
||||
"attrs": {"key": "value"},
|
||||
"children": {
|
||||
"visual": {
|
||||
"children": {
|
||||
"binding": {
|
||||
"attrs": {"template": "ToastText01"},
|
||||
"children": {
|
||||
"text": {
|
||||
"attrs": {"id": "1"},
|
||||
"children": "toast notification"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Converting xml to str via tostring is inconsistent, so we have to check each element.
|
||||
xml_tree = dict_to_xml_schema(xml_data)
|
||||
self.assertEqual(xml_tree.tag, "toast")
|
||||
self.assertEqual(xml_tree.attrib, {"key": "value"})
|
||||
visual = xml_tree.getchildren()[0]
|
||||
self.assertEqual(visual.tag, "visual")
|
||||
binding = visual.getchildren()[0]
|
||||
self.assertEqual(binding.tag, "binding")
|
||||
self.assertEqual(binding.attrib, {"template": "ToastText01"})
|
||||
text = binding.getchildren()[0]
|
||||
self.assertEqual(text.tag, "text")
|
||||
self.assertEqual(text.attrib, {"id": "1"})
|
||||
self.assertEqual(text.text, "toast notification")
|
||||
|
||||
def test_create_multi_sub_element_xml_from_dict(self):
|
||||
xml_data = {
|
||||
"toast": {
|
||||
"attrs": {
|
||||
"key": "value"
|
||||
},
|
||||
"children": {
|
||||
"visual": {
|
||||
"children": {
|
||||
"binding": {
|
||||
"attrs": {"template": "ToastText02"},
|
||||
"children": {
|
||||
"text": [
|
||||
{"attrs": {"id": "1"}, "children": "first text"},
|
||||
{"attrs": {"id": "2"}, "children": "second text"},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Converting xml to str via tostring is inconsistent, so we have to check each element.
|
||||
xml_tree = dict_to_xml_schema(xml_data)
|
||||
self.assertEqual(xml_tree.tag, "toast")
|
||||
self.assertEqual(xml_tree.attrib, {"key": "value"})
|
||||
visual = xml_tree.getchildren()[0]
|
||||
self.assertEqual(visual.tag, "visual")
|
||||
binding = visual.getchildren()[0]
|
||||
self.assertEqual(binding.tag, "binding")
|
||||
self.assertEqual(binding.attrib, {"template": "ToastText02"})
|
||||
children = binding.getchildren()
|
||||
self.assertEqual(len(children), 2)
|
||||
|
||||
def test_create_two_multi_sub_element_xml_from_dict(self):
|
||||
xml_data = {
|
||||
"toast": {
|
||||
"attrs": {
|
||||
"key": "value"
|
||||
},
|
||||
"children": {
|
||||
"visual": {
|
||||
"children": {
|
||||
"binding": {
|
||||
"attrs": {
|
||||
"template": "ToastText02"
|
||||
},
|
||||
"children": {
|
||||
"text": [
|
||||
{"attrs": {"id": "1"}, "children": "first text"},
|
||||
{"attrs": {"id": "2"}, "children": "second text"},
|
||||
],
|
||||
"image": [
|
||||
{"attrs": {"src": "src1"}},
|
||||
{"attrs": {"src": "src2"}},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Converting xml to str via tostring is inconsistent, so we have to check each element.
|
||||
xml_tree = dict_to_xml_schema(xml_data)
|
||||
self.assertEqual(xml_tree.tag, "toast")
|
||||
self.assertEqual(xml_tree.attrib, {"key": "value"})
|
||||
visual = xml_tree.getchildren()[0]
|
||||
self.assertEqual(visual.tag, "visual")
|
||||
binding = visual.getchildren()[0]
|
||||
self.assertEqual(binding.tag, "binding")
|
||||
self.assertEqual(binding.attrib, {"template": "ToastText02"})
|
||||
children = binding.getchildren()
|
||||
self.assertEqual(len(children), 4)
|
50
tox.ini
50
tox.ini
|
@ -1,20 +1,44 @@
|
|||
[tox]
|
||||
envlist = {py27,py34,py35}--django{18,19}--drf{32,33},flake8
|
||||
envlist =
|
||||
py27-django111
|
||||
py34-django{111}
|
||||
py36-django{111,20,master}
|
||||
flake8
|
||||
|
||||
[testenv]
|
||||
commands = python ./tests/runtests.py
|
||||
deps=
|
||||
django18: Django>=1.8,<1.9
|
||||
django19: Django>=1.9,<2.0
|
||||
mock==1.0.1
|
||||
drf32: djangorestframework>=3.2,<3.3
|
||||
drf33: djangorestframework>=3.3,<3.4
|
||||
setenv =
|
||||
PYTHONWARNINGS = all
|
||||
DJANGO_SETTINGS_MODULE = tests.settings
|
||||
PYTHONPATH = {toxinidir}
|
||||
commands = pytest
|
||||
deps =
|
||||
pytest
|
||||
pytest-django
|
||||
mock
|
||||
djangorestframework>=3.5
|
||||
django111: Django>=1.11,<2.0
|
||||
django20: Django>=2.0,<2.1
|
||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||
|
||||
[testenv:flake8]
|
||||
commands = flake8 push_notifications
|
||||
deps = flake8
|
||||
commands = flake8
|
||||
deps =
|
||||
flake8
|
||||
flake8-isort
|
||||
flake8-quotes
|
||||
|
||||
[flake8]
|
||||
ignore = F403,W191,E126,E128
|
||||
max-line-length = 160
|
||||
exclude = push_notifications/migrations/*
|
||||
ignore = W191
|
||||
max-line-length = 92
|
||||
exclude = .tox, push_notifications/migrations
|
||||
inline-quotes = double
|
||||
|
||||
[isort]
|
||||
indent = tab
|
||||
line_length = 92
|
||||
lines_after_imports = 2
|
||||
balanced_wrapping = True
|
||||
default_section = THIRDPARTY
|
||||
known_first_party = push_notifications
|
||||
multi_line_output = 5
|
||||
skip = .tox/
|
||||
|
|
Loading…
Reference in New Issue