pax_global_header00006660000000000000000000000064132344210260014507gustar00rootroot0000000000000052 comment=cee41601ce43d93bb3404bc8a4dbf0fc79269f21 django-push-notifications-1.6.0/000077500000000000000000000000001323442102600166015ustar00rootroot00000000000000django-push-notifications-1.6.0/.editorconfig000066400000000000000000000004401323442102600212540ustar00rootroot00000000000000# 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 django-push-notifications-1.6.0/.gitignore000066400000000000000000000001621323442102600205700ustar00rootroot00000000000000# python compiled __pycache__ *.pyc # distutils MANIFEST build # IDE .idea *.iml # virtualenv .env # tox .tox django-push-notifications-1.6.0/.travis.yml000066400000000000000000000007051323442102600207140ustar00rootroot00000000000000# https://travis-ci.org/jleclanche/django-push-notifications sudo: false language: python 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 notifications: email: on_failure: always on_success: change django-push-notifications-1.6.0/AUTHORS000066400000000000000000000007051323442102600176530ustar00rootroot00000000000000This library was created by Jerome Leclanche , 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/django-push-notifications/django-push-notifications/graphs/contributors django-push-notifications-1.6.0/CHANGELOG.rst000066400000000000000000000117501323442102600206260ustar00rootroot00000000000000v1.6.0 (2018-01-31) =================== * BACKWARDS-INCOMPATIBLE: Drop support for Django < 1.11 * DJANGO: Support Django 2.0 * NEW FEATURE: Add support for WebPush v1.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 v1.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) =================== * BACKWARDS-INCOMPATIBLE: Drop support for Python<3.4 * DJANGO: Support Django 1.9 * GCM: Handle canonical IDs * GCM: Allow full range of GCMDevice.device_id values * GCM: Do not allow duplicate registration_ids * DRF: Work around empty boolean defaults issue (django-rest-framework#1101) * BUGFIX: Do not throw GCMError in bulk messages from the admin * BUGFIX: Avoid generating an extra migration on Python 3 * BUGFIX: Only send in bulk to active devices * 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) =================== * 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. * APNS: Add support for setting the ca_certs file with new APNS_CA_CERTIFICATES setting * GCM: Deactivate GCMDevices when their notifications cause NotRegistered or InvalidRegistration * GCM: Indiscriminately handle all keyword arguments in gcm_send_message and gcm_send_bulk_message * GCM: Never fall back to json in gcm_send_message * BUGFIX: Fixed migration issues from 1.2.0 upgrade. * BUGFIX: Better detection of SQLite/GIS MySQL in various checks * BUGFIX: Assorted Python 3 bugfixes * BUGFIX: Fix display of device_id in admin v1.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 * APNS: Restore error checking on apns_send_bulk_message * GCM: Expose the time_to_live argument in gcm_send_bulk_message * GCM: Fix return value when gcm bulk is split in batches * GCM: Improved error checking reliability * GCM: Properly pass kwargs in GCMDeviceQuerySet.send_message() * BUGFIX: Fix HexIntegerField for Django 1.3 v1.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 * APNS: Implemented support for APNS Feedback Service * APNS: Support for optional "category" dict * GCM: Improved error handling in bulk mode * GCM: Added support for time_to_live parameter * BUGFIX: Fixed various issues relating HexIntegerField * BUGFIX: Fixed issues in the admin with custom user models v1.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`. * Updated APNS protocol support * Allow sending empty sounds on APNS * Several APNS bugfixes * Fixed BigIntegerField support on PostGIS * Assorted migrations bugfixes * Added a test suite v1.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) ================= * 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 * Drop uniqueness on gcm registration_id due to compatibility issues with MySQL * Fix some issues with migrations * Add some basic tests * Integrate with travis-ci * Add an AUTHORS file v0.9 (2013-12-17) ================= * Enable installation with pip * Add wheel support * Add full documentation * Various bug fixes v0.8 (2013-03-15) ================= * Initial release django-push-notifications-1.6.0/CONTRIBUTING.md000066400000000000000000000025411323442102600210340ustar00rootroot00000000000000### 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. 3. When in doubt, [PEP8](https://www.python.org/dev/peps/pep-0008/). Follow its naming conventions. 4. Know when to make exceptions. Also see: [How to name things in programming](http://www.slideshare.net/pirhilton/how-to-name-things-the-hardest-problem-in-programming) ### 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. 2. Every commit should pass all tests on its own. 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. Also see: [Github Help: Using Pull Requests](https://help.github.com/articles/using-pull-requests/) django-push-notifications-1.6.0/LICENSE000066400000000000000000000020621323442102600176060ustar00rootroot00000000000000Copyright (c) Jerome Leclanche Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-push-notifications-1.6.0/MANIFEST.in000066400000000000000000000000671323442102600203420ustar00rootroot00000000000000include MANIFEST.in include README.rst include LICENSE django-push-notifications-1.6.0/README.rst000066400000000000000000000470151323442102600202770ustar00rootroot00000000000000django-push-notifications ========================= .. image:: https://api.travis-ci.org/django-push-notifications/django-push-notifications.png :target: https://travis-ci.org/django-push-notifications/django-push-notifications 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/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 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. Dependencies ------------ - Python 2.7 or 3.4+ - Django 1.11+ - For the API module, Django REST Framework 3.5+ 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 ----- You can install the library directly from pypi using pip: .. code-block:: shell $ pip install django-push-notifications Edit your settings.py file: .. code-block:: python INSTALLED_APPS = ( ... "push_notifications" ) PUSH_NOTIFICATIONS_SETTINGS = { "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 ``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 `_. Native Django migrations are in use. ``manage.py migrate`` will install and migrate all models. .. _settings: Settings list ------------- All settings are contained in a ``PUSH_NOTIFICATIONS_SETTINGS`` dict. 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``. **APNS settings** - ``APNS_CERTIFICATE``: Absolute path to your APNS certificate file. Certificates with passphrases are not supported. - ``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 - ``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. **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 ---------------- 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 from push_notifications.models import APNSDevice, GCMDevice device = GCMDevice.objects.get(registration_id=gcm_reg_id) # The first argument will be sent as "message" to the intent extras Bundle # Retrieve it with intent.getExtras().getString("message") device.send_message("You've got mail") # If you want to customize, send an extra dict and a None message. # the extras dict will be mapped into the intent extras Bundle. # For dicts where all values are keys this will be sent as url parameters, # but for more complex nested collections the extras dict will be sent via # the bulk message api. device.send_message(None, extra={"foo": "bar"}) 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, 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 `_ Sending messages in bulk ------------------------ .. code-block:: python from push_notifications.models import APNSDevice, GCMDevice devices = GCMDevice.objects.filter(user__first_name="James") devices.send_message("Happy name day!") 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. 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: .. code-block:: python devices.send_message( "Happy name day!", badge=lambda token: APNSDevice.objects.get(registration_id=token).user.get_badge() ) Firebase vs Google Cloud Messaging ---------------------------------- ``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 `_. When using FCM, ``django-push-notifications`` will automatically use the `notification and data messages format `_ 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 `_. .. 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 `_ Exceptions ---------- - ``NotificationError(Exception)``: Base exception for all notification-related errors. - ``gcm.GCMError(NotificationError)``: An error was returned by GCM. This is never raised when using bulk notifications. - ``apns.APNSError(NotificationError)``: Something went wrong upon sending APNS notifications. - ``apns.APNSDataOverflow(APNSError)``: The APNS payload exceeds its maximum size and cannot be sent. Django REST Framework (DRF) support ----------------------------------- ViewSets are available for both APNS and GCM devices in two permission flavors: - ``APNSDeviceViewSet`` and ``GCMDeviceViewSet`` - Permissions as specified in settings (``AllowAny`` by default, which is not recommended) - A device may be registered without associating it with a user - ``APNSDeviceAuthorizedViewSet`` and ``GCMDeviceAuthorizedViewSet`` - Permissions are ``IsAuthenticated`` and custom permission ``IsOwner``, which will only allow the ``request.user`` to get and update devices that belong to that user - Requires a user to be authenticated, so all devices will be associated with a user When creating an ``APNSDevice``, the ``registration_id`` is validated to be a 64-character or 200-character hexadecimal string. Since 2016, device tokens are to be increased from 32 bytes to 100 bytes. Routes can be added one of two ways: - Routers_ (include all views) .. _Routers: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#using-routers :: from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet, GCMDeviceAuthorizedViewSet from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'device/apns', APNSDeviceAuthorizedViewSet) router.register(r'device/gcm', GCMDeviceAuthorizedViewSet) urlpatterns = patterns('', # URLs will show up at /device/apns url(r'^', include(router.urls)), # ... ) - Using as_view_ (specify which views to include) .. _as_view: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#binding-viewsets-to-urls-explicitly :: from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet urlpatterns = patterns('', # Only allow creation of devices by authenticated users url(r'^device/apns/?$', APNSDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_apns_device'), # ... ) Update of device with duplicate registration ID ----------------------------------------------- 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. .. [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. django-push-notifications-1.6.0/push_notifications/000077500000000000000000000000001323442102600225115ustar00rootroot00000000000000django-push-notifications-1.6.0/push_notifications/__init__.py000066400000000000000000000002171323442102600246220ustar00rootroot00000000000000import pkg_resources __version__ = pkg_resources.require("django-push-notifications")[0].version class NotificationError(Exception): pass django-push-notifications-1.6.0/push_notifications/admin.py000066400000000000000000000066121323442102600241600ustar00rootroot00000000000000from django.apps import apps from django.contrib import admin, messages from django.utils.translation import ugettext_lazy as _ from .apns import APNSServerError from .gcm import GCMError from .webpush import WebPushError from .models import APNSDevice, GCMDevice, WNSDevice, WebPushDevice from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS User = apps.get_model(*SETTINGS["USER_MODEL"].split(".")) class DeviceAdmin(admin.ModelAdmin): list_display = ("__str__", "device_id", "user", "active", "date_created") 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): """ Provides error handling for DeviceAdmin send_message and send_bulk_message methods. """ ret = [] errors = [] r = "" for device in queryset: try: if bulk: r = queryset.send_message("Test bulk notification") else: r = device.send_message("Test single notification") if r: ret.append(r) except GCMError as e: errors.append(str(e)) except APNSServerError as e: errors.append(e.status) 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 ) if 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: msg = _("All messages were sent: %s" % (ret)) self.message_user(request, msg) 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") 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, GCMDeviceAdmin) admin.site.register(WNSDevice, DeviceAdmin) admin.site.register(WebPushDevice, DeviceAdmin) django-push-notifications-1.6.0/push_notifications/api/000077500000000000000000000000001323442102600232625ustar00rootroot00000000000000django-push-notifications-1.6.0/push_notifications/api/__init__.py000066400000000000000000000000001323442102600253610ustar00rootroot00000000000000django-push-notifications-1.6.0/push_notifications/api/rest_framework.py000066400000000000000000000145401323442102600266720ustar00rootroot00000000000000from __future__ import absolute_import 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 hex_re, UNSIGNED_64BIT_INT_MAX_VALUE from ..models import APNSDevice, GCMDevice, WNSDevice, WebPushDevice from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS # Fields class HexIntegerField(IntegerField): """ Store an integer represented as a hex string of form "0x01". """ def to_internal_value(self, data): # validate hex string and convert it to the unsigned # integer representation for internal use try: 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) def to_representation(self, value): return value # Serializers class DeviceSerializerMixin(ModelSerializer): class Meta: 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 Meta(DeviceSerializerMixin.Meta): model = APNSDevice 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). if hex_re.match(value) is None or len(value) not in (64, 200): raise ValidationError("Registration ID (device token) is invalid") return value class UniqueRegistrationSerializerMixin(Serializer): def validate(self, attrs): devices = None primary_key = None request_method = None 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" 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 devices: raise ValidationError({"registration_id": "This field must be unique."}) return attrs class GCMDeviceSerializer(UniqueRegistrationSerializerMixin, ModelSerializer): device_id = HexIntegerField( help_text="ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)", style={"input_type": "text"}, required=False, allow_null=True ) class Meta(DeviceSerializerMixin.Meta): model = GCMDevice 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 if value > UNSIGNED_64BIT_INT_MAX_VALUE: raise ValidationError("Device ID is out of range") 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): # must be the owner to view the object return obj.user == request.user # Mixins 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: 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) def get_queryset(self): # filter all devices to only those belonging to the current user return self.queryset.filter(user=self.request.user) # 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 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 django-push-notifications-1.6.0/push_notifications/apns.py000066400000000000000000000107551323442102600240340ustar00rootroot00000000000000""" Apple Push Notification Service Documentation is available on the iOS Developer Library: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html """ import time from apns2 import client as apns2_client from apns2 import errors as apns2_errors from apns2 import payload as apns2_payload from . import models from . import NotificationError from .apns_errors import reason_for_exception_class from .conf import get_manager class APNSError(NotificationError): pass class APNSUnsupportedPriority(APNSError): pass class APNSServerError(APNSError): def __init__(self, status): super(APNSServerError, self).__init__(status) self.status = status def _apns_create_socket(certfile=None, application_id=None): certfile = certfile or get_manager().get_apns_certificate(application_id) client = apns2_client.APNsClient( certfile, use_sandbox=get_manager().get_apns_use_sandbox(application_id), use_alternative_port=get_manager().get_apns_use_alternative_port(application_id) ) client.connect() return client def _apns_prepare( token, alert, application_id=None, badge=None, sound=None, category=None, content_available=False, action_loc_key=None, loc_key=None, loc_args=[], extra={}, mutable_content=False, thread_id=None, url_args=None): if action_loc_key or loc_key or loc_args: apns2_alert = apns2_payload.PayloadAlert( body=alert if alert else {}, body_localized_key=loc_key, body_localized_args=loc_args, action_localized_key=action_loc_key) else: apns2_alert = alert if callable(badge): badge = badge(token) return apns2_payload.Payload( apns2_alert, badge, sound, content_available, mutable_content, category, url_args, custom=extra, thread_id=thread_id) def _apns_send( registration_id, alert, batch=False, application_id=None, certfile=None, **kwargs ): client = _apns_create_socket(certfile=certfile, application_id=application_id) notification_kwargs = {} # if expiration isn"t specified use 1 month from now notification_kwargs["expiration"] = kwargs.pop("expiration", None) if not notification_kwargs["expiration"]: notification_kwargs["expiration"] = int(time.time()) + 2592000 priority = kwargs.pop("priority", None) if priority: try: notification_kwargs["priority"] = apns2_client.NotificationPriority(str(priority)) except ValueError: raise APNSUnsupportedPriority("Unsupported priority %d" % (priority)) if batch: data = [apns2_client.Notification( token=rid, payload=_apns_prepare(rid, alert, **kwargs)) for rid in registration_id] return client.send_notification_batch( data, get_manager().get_apns_topic(application_id=application_id), **notification_kwargs ) data = _apns_prepare(registration_id, alert, **kwargs) client.send_notification( registration_id, data, get_manager().get_apns_topic(application_id=application_id), **notification_kwargs ) def apns_send_message(registration_id, alert, application_id=None, certfile=None, **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. """ try: _apns_send( registration_id, alert, application_id=application_id, certfile=certfile, **kwargs ) except apns2_errors.APNsException as apns2_exception: if isinstance(apns2_exception, apns2_errors.Unregistered): device = models.APNSDevice.objects.get(registration_id=registration_id) device.active = False device.save() raise APNSServerError(status=reason_for_exception_class(apns2_exception.__class__)) def apns_send_bulk_message( registration_ids, alert, application_id=None, certfile=None, **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. """ results = _apns_send( registration_ids, alert, batch=True, application_id=application_id, certfile=certfile, **kwargs ) inactive_tokens = [token for token, result in results.items() if result == "Unregistered"] models.APNSDevice.objects.filter(registration_id__in=inactive_tokens).update(active=False) return results django-push-notifications-1.6.0/push_notifications/apns_errors.py000066400000000000000000000033231323442102600254210ustar00rootroot00000000000000from apns2 import errors as apns2_errors def reason_for_exception_class(exception_class): errors = { apns2_errors.PayloadEmpty: "PayloadEmpty", apns2_errors.PayloadTooLarge: "PayloadTooLarge", apns2_errors.BadTopic: "BadTopic", apns2_errors.TopicDisallowed: "TopicDisallowed", apns2_errors.BadMessageId: "BadMessageId", apns2_errors.BadExpirationDate: "BadExpirationDate", apns2_errors.BadPriority: "BadPriority", apns2_errors.MissingDeviceToken: "MissingDeviceToken", apns2_errors.BadDeviceToken: "BadDeviceToken", apns2_errors.DeviceTokenNotForTopic: "DeviceTokenNotForTopic", apns2_errors.Unregistered: "Unregistered", apns2_errors.DuplicateHeaders: "DuplicateHeaders", apns2_errors.BadCertificateEnvironment: "BadCertificateEnvironment", apns2_errors.BadCertificate: "BadCertificate", apns2_errors.Forbidden: "Forbidden", apns2_errors.BadPath: "BadPath", apns2_errors.MethodNotAllowed: "MethodNotAllowed", apns2_errors.TooManyRequests: "TooManyRequests", apns2_errors.IdleTimeout: "IdleTimeout", apns2_errors.Shutdown: "Shutdown", apns2_errors.InternalServerError: "InternalServerError", apns2_errors.ServiceUnavailable: "ServiceUnavailable", apns2_errors.MissingTopic: "MissingTopic", apns2_errors.BadCollapseId: "BadCollapseId", apns2_errors.ConnectionFailed: "ConnectionFailed", apns2_errors.ExpiredProviderToken: "ExpiredProviderToken", apns2_errors.InternalException: "InternalException", apns2_errors.InvalidProviderToken: "InvalidProviderToken", apns2_errors.MissingProviderToken: "MissingProviderToken", apns2_errors.TooManyProviderTokenUpdates: "TooManyProviderTokenUpdates" } if exception_class in errors: return errors[exception_class] return "Unknown APNS error" django-push-notifications-1.6.0/push_notifications/conf/000077500000000000000000000000001323442102600234365ustar00rootroot00000000000000django-push-notifications-1.6.0/push_notifications/conf/__init__.py000066400000000000000000000010001323442102600255360ustar00rootroot00000000000000from 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() django-push-notifications-1.6.0/push_notifications/conf/app.py000066400000000000000000000244631323442102600246010ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured from django.utils.six import string_types from .base import BaseConfig, check_apns_certificate from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS 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 = [ "APNS", "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", ] APNS_REQUIRED_SETTINGS = ["CERTIFICATE"] APNS_OPTIONAL_SETTINGS = [ "USE_SANDBOX", "USE_ALTERNATIVE_PORT", "TOPIC" ] 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_apns_config(self, application_id, application_config): allowed = REQUIRED_SETTINGS + OPTIONAL_SETTINGS + APNS_REQUIRED_SETTINGS + \ APNS_OPTIONAL_SETTINGS self._validate_allowed_settings(application_id, application_config, allowed) self._validate_required_settings( application_id, application_config, APNS_REQUIRED_SETTINGS ) # determine/set optional values application_config.setdefault("USE_SANDBOX", False) application_config.setdefault("USE_ALTERNATIVE_PORT", False) application_config.setdefault("TOPIC", None) self._validate_apns_certificate(application_config["CERTIFICATE"]) def _validate_apns_certificate(self, certfile): """Validate the APNS certificate at startup.""" try: with open(certfile, "r") as f: content = f.read() check_apns_certificate(content) except Exception as e: msg = "The APNS certificate file at %r is not readable: %s" % (certfile, e) raise ImproperlyConfigured(msg) 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 dies trying""" 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_apns_certificate(self, application_id=None): r = self._get_application_settings(application_id, "APNS", "CERTIFICATE") if not isinstance(r, string_types): # probably the (Django) file, and file path should be got if hasattr(r, "path"): return r.path elif (hasattr(r, "has_key") or hasattr(r, "__contains__")) and "path" in r: return r["path"] else: raise ImproperlyConfigured( "The APNS certificate settings value should be a string, or " "should have a 'path' attribute or key" ) return r def get_apns_use_sandbox(self, application_id=None): return self._get_application_settings(application_id, "APNS", "USE_SANDBOX") def get_apns_use_alternative_port(self, application_id=None): return self._get_application_settings(application_id, "APNS", "USE_ALTERNATIVE_PORT") def get_apns_topic(self, application_id=None): return self._get_application_settings(application_id, "APNS", "TOPIC") 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")django-push-notifications-1.6.0/push_notifications/conf/appmodel.py000066400000000000000000000002651323442102600256140ustar00rootroot00000000000000from .base import BaseConfig class AppModelConfig(BaseConfig): """Future home of the Application Model conf adapter Supports multiple applications in the database. """ pass django-push-notifications-1.6.0/push_notifications/conf/base.py000066400000000000000000000030731323442102600247250ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured class BaseConfig(object): def get_apns_certificate(self, application_id=None): raise NotImplementedError def get_apns_use_sandbox(self, application_id=None): raise NotImplementedError def get_apns_use_alternative_port(self, application_id=None): raise NotImplementedError 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 def check_apns_certificate(ss): mode = "start" for s in ss.split("\n"): if mode == "start": if "BEGIN RSA PRIVATE KEY" in s or "BEGIN PRIVATE KEY" in s: mode = "key" elif mode == "key": if "END RSA PRIVATE KEY" in s or "END PRIVATE KEY" in s: mode = "end" break elif s.startswith("Proc-Type") and "ENCRYPTED" in s: raise ImproperlyConfigured("Encrypted APNS private keys are not supported") if mode != "end": raise ImproperlyConfigured("The APNS certificate doesn't contain a private key") django-push-notifications-1.6.0/push_notifications/conf/legacy.py000066400000000000000000000120661323442102600252610ustar00rootroot00000000000000from django.core.exceptions import ImproperlyConfigured from django.utils.six import string_types from .base import BaseConfig from ..settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS __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_apns_certificate(self, application_id=None): r = self._get_application_settings( application_id, "APNS_CERTIFICATE", "You need to setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" ) if not isinstance(r, string_types): # probably the (Django) file, and file path should be got if hasattr(r, "path"): return r.path elif (hasattr(r, "has_key") or hasattr(r, "__contains__")) and "path" in r: return r["path"] else: msg = ( "The APNS certificate settings value should be a string, or " "should have a 'path' attribute or key" ) raise ImproperlyConfigured(msg) return r def get_apns_use_sandbox(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_USE_SANDBOX", msg) def get_apns_use_alternative_port(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_USE_ALTERNATIVE_PORT", msg) def get_apns_topic(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_TOPIC", msg) def get_apns_host(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_HOST", msg) def get_apns_port(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_PORT", msg) def get_apns_feedback_host(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_FEEDBACK_HOST", msg) def get_apns_feedback_port(self, application_id=None): msg = "Setup PUSH_NOTIFICATIONS_SETTINGS properly to send messages" return self._get_application_settings(application_id, "APNS_FEEDBACK_PORT", 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)django-push-notifications-1.6.0/push_notifications/fields.py000066400000000000000000000073651323442102600243440ustar00rootroot00000000000000import re import struct from django import forms 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 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" ] def _using_signed_storage(): return connection.settings_dict["ENGINE"] in signed_integer_engines def _signed_to_unsigned_integer(value): return struct.unpack("Q", struct.pack("q", value))[0] def _unsigned_to_signed_integer(value): return struct.unpack("q", struct.pack("Q", value))[0] def _hex_string_to_unsigned_integer(value): return int(value, 16) def _unsigned_integer_to_hex_string(value): return hex(value).rstrip("L") 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") ] super(HexadecimalField, self).__init__(*args, **kwargs) def prepare_value(self, value): # converts bigint from db to hex before it is displayed in admin if value and not isinstance(value, six.string_types) \ and connection.vendor in ("mysql", "sqlite"): value = _unsigned_integer_to_hex_string(value) return super(forms.CharField, self).prepare_value(value) class HexIntegerField(models.BigIntegerField): """ This field stores a hexadecimal *string* of up to 64 bits as an unsigned integer on *all* backends including postgres. Reasoning: Postgres only supports signed bigints. Since we don't care about signedness, we store it as signed, and cast it to unsigned when we deal with the actual value (with struct) On sqlite and mysql, native unsigned bigint types are used. In all cases, the value we deal with in python is always in hex. """ validators = [ MinValueValidator(UNSIGNED_64BIT_INT_MIN_VALUE), MaxValueValidator(UNSIGNED_64BIT_INT_MAX_VALUE) ] def db_type(self, connection): engine = connection.settings_dict["ENGINE"] if "mysql" in engine: return "bigint unsigned" elif "sqlite" in engine: return "UNSIGNED BIG INT" else: return super(HexIntegerField, self).db_type(connection=connection) def get_prep_value(self, value): """ Return the integer value to be stored from the hex string """ if value is None or value == "": return None if isinstance(value, six.string_types): value = _hex_string_to_unsigned_integer(value) if _using_signed_storage(): value = _unsigned_to_signed_integer(value) return value def from_db_value(self, value, expression, connection, context): """ Return an unsigned int representation from all db backends """ if value is None: return value if _using_signed_storage(): value = _signed_to_unsigned_integer(value) return value def to_python(self, value): """ Return a str representation of the hexadecimal """ if isinstance(value, six.string_types): return value if value is None: return value return _unsigned_integer_to_hex_string(value) def formfield(self, **kwargs): defaults = {"form_class": HexadecimalField} defaults.update(kwargs) # yes, that super call is right return super(models.IntegerField, self).formfield(**defaults) def run_validators(self, value): # make sure validation is performed on integer value not string value value = _hex_string_to_unsigned_integer(value) return super(models.BigIntegerField, self).run_validators(value) django-push-notifications-1.6.0/push_notifications/gcm.py000066400000000000000000000164651323442102600236450ustar00rootroot00000000000000""" 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 try: from urllib.request import Request, urlopen except ImportError: # Python 2 support from urllib2 import Request, urlopen from django.core.exceptions import ImproperlyConfigured from . import NotificationError from .conf import get_manager from .models import GCMDevice # 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): pass def _chunks(l, n): """ Yield successive chunks from list \a l with a minimum size \a n """ for i in range(0, len(l), n): yield l[i:i + n] def _gcm_send(data, content_type, 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(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 _fcm_send(data, content_type, application_id): key = get_manager().get_fcm_api_key(application_id) 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 _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: # 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 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, cloud_message_type=cloud_type ) removed.update(active=0) for old_id, new_id in old_new_ids: _cm_handle_canonical_id(new_id, old_id, cloud_type) if throw_error: raise GCMError(response) return response def _cm_send_request( registration_ids, data, cloud_type="GCM", application_id=None, use_fcm_notifications=True, **kwargs ): """ Sends a FCM or GCM notification to one or more registration_ids as json data. The registration_ids needs to be a list. """ 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: 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 _cm_handle_canonical_id(canonical_id, current_id, cloud_type): """ 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) 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://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)) # 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 # 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(_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) send_bulk_message = send_message django-push-notifications-1.6.0/push_notifications/migrations/000077500000000000000000000000001323442102600246655ustar00rootroot00000000000000django-push-notifications-1.6.0/push_notifications/migrations/0001_initial.py000066400000000000000000000050461323442102600273350ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import push_notifications.fields from django.conf import settings class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] 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, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'APNS device', }, bases=(models.Model,), ), migrations.CreateModel( name='GCMDevice', 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', 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, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'GCM device', }, bases=(models.Model,), ), ] django-push-notifications-1.6.0/push_notifications/migrations/0002_auto_20160106_0850.py000066400000000000000000000007751323442102600303140ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.1 on 2016-01-06 08:50 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('push_notifications', '0001_initial'), ] operations = [ migrations.AlterField( model_name='apnsdevice', name='registration_id', field=models.CharField(max_length=200, unique=True, verbose_name='Registration ID'), ), ] django-push-notifications-1.6.0/push_notifications/migrations/0003_wnsdevice.py000066400000000000000000000027111323442102600276710ustar00rootroot00000000000000# -*- 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 import django.db.models.deletion 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', }, ), ] django-push-notifications-1.6.0/push_notifications/migrations/0004_fcm.py000066400000000000000000000012361323442102600264510ustar00rootroot00000000000000# -*- 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') ), ] django-push-notifications-1.6.0/push_notifications/migrations/0005_applicationid.py000066400000000000000000000024551323442102600305310ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings class Migration(migrations.Migration): dependencies = [ ('push_notifications', '0004_fcm'), ] operations = [ migrations.AddField( model_name='apnsdevice', 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='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, ), ] django-push-notifications-1.6.0/push_notifications/migrations/0006_webpushdevice.py000066400000000000000000000036011323442102600305410ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models from django.conf import settings 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', }, ), ] django-push-notifications-1.6.0/push_notifications/migrations/__init__.py000066400000000000000000000000001323442102600267640ustar00rootroot00000000000000django-push-notifications-1.6.0/push_notifications/models.py000066400000000000000000000161761323442102600243610ustar00rootroot00000000000000from __future__ import unicode_literals 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["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 "%s for %s" % (self.__class__.__name__, self.user or "unknown user") ) class GCMDeviceManager(models.Manager): def get_queryset(self): return GCMDeviceQuerySet(self.model) class GCMDeviceQuerySet(models.query.QuerySet): def send_message(self, message, **kwargs): if self: from .gcm import send_message as gcm_send_message data = kwargs.pop("extra", {}) if message is not None: data["message"] = message 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)") ) 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 send_message as gcm_send_message data = kwargs.pop("extra", {}) if message is not None: data["message"] = message return gcm_send_message( self.registration_id, data, self.cloud_message_type, application_id=self.application_id, **kwargs ) class APNSDeviceManager(models.Manager): def get_queryset(self): return APNSDeviceQuerySet(self.model) class APNSDeviceQuerySet(models.query.QuerySet): def send_message(self, message, certfile=None, **kwargs): if self: from .apns import apns_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 = list(self.filter(active=True, application_id=app_id).values_list( "registration_id", flat=True) ) r = apns_send_bulk_message( registration_ids=reg_ids, alert=message, application_id=app_id, certfile=certfile, **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 ) objects = APNSDeviceManager() class Meta: verbose_name = _("APNS device") def send_message(self, message, certfile=None, **kwargs): from .apns import apns_send_message return apns_send_message( registration_id=self.registration_id, alert=message, application_id=self.application_id, certfile=certfile, **kwargs ) class WNSDeviceManager(models.Manager): def get_queryset(self): return WNSDeviceQuerySet(self.model) class WNSDeviceQuerySet(models.query.QuerySet): def send_message(self, message, **kwargs): if self: 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 = list(self.filter(active=True, application_id=app_id).values_list( "registration_id", flat=True )) r = wns_send_bulk_message(uri_list=reg_ids, message=message, **kwargs) if hasattr(r, "keys"): res += [r] elif hasattr(r, "__getitem__"): res += r return res 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 = WNSDeviceManager() class Meta: verbose_name = _("WNS device") def send_message(self, message, **kwargs): from .wns import wns_send_message return wns_send_message( uri=self.registration_id, message=message, application_id=self.application_id, **kwargs ) 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") 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) @property def device_id(self): return None django-push-notifications-1.6.0/push_notifications/settings.py000066400000000000000000000035111323442102600247230ustar00rootroot00000000000000from 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_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 if settings.DEBUG: PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_USE_SANDBOX", True) else: PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_USE_SANDBOX", False) PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_USE_ALTERNATIVE_PORT", False) PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_TOPIC", None) # 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) django-push-notifications-1.6.0/push_notifications/webpush.py000066400000000000000000000020711323442102600245400ustar00rootroot00000000000000from . import NotificationError from .conf import get_manager from pywebpush import webpush from pywebpush import WebPushException 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): try: response = webpush( subscription_info=get_subscription_info(application_id, uri, browser, auth, p256dh), 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) django-push-notifications-1.6.0/push_notifications/wns.py000066400000000000000000000263431323442102600237020ustar00rootroot00000000000000""" 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 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 from django.core.exceptions import ImproperlyConfigured from . import NotificationError from .conf import get_manager 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': , 'expires_in': , '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: 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 django-push-notifications-1.6.0/setup.cfg000066400000000000000000000021511323442102600204210ustar00rootroot00000000000000[metadata] name = django-push-notifications version = 1.6.0 description = Send push notifications to mobile devices through GCM, APNS or WNS and to browsers through WebPush (supported browsers: Chrome, Firefox and Opera) in Django author = Jerome Leclanche author_email = jerome@leclan.ch url = https://github.com/django-push-notifications/django-push-notifications/ download_url = https://github.com/django-push-notifications/django-push-notifications/tarball/master classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Intended Audience :: Developers License :: OSI Approved :: MIT License Topic :: System :: Networking 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 Framework :: Django Framework :: Django :: 1.11 Framework :: Django :: 2.0 [options] packages = find: install_requires = apns2>=0.3.0 pywebpush>=1.3.0 Django>=1.11 [options.packages.find] exclude = tests [bdist_wheel] universal = 1 django-push-notifications-1.6.0/setup.py000077500000000000000000000000761323442102600203210ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup() django-push-notifications-1.6.0/tests/000077500000000000000000000000001323442102600177435ustar00rootroot00000000000000django-push-notifications-1.6.0/tests/__init__.py000066400000000000000000000000001323442102600220420ustar00rootroot00000000000000django-push-notifications-1.6.0/tests/_mock.py000066400000000000000000000001331323442102600214020ustar00rootroot00000000000000try: from unittest import mock # noqa except ImportError: from mock import mock # noqa django-push-notifications-1.6.0/tests/responses.py000066400000000000000000000042551323442102600223440ustar00rootroot00000000000000# 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"}]}' ) django-push-notifications-1.6.0/tests/settings.py000066400000000000000000000006701323442102600221600ustar00rootroot00000000000000# assert warnings are enabled import warnings warnings.simplefilter("ignore", Warning) DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", } } INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "push_notifications", ] SITE_ID = 1 ROOT_URLCONF = "core.urls" SECRET_KEY = "foobar" PUSH_NOTIFICATIONS_SETTINGS = {} django-push-notifications-1.6.0/tests/test_apns_models.py000066400000000000000000000106051323442102600236620ustar00rootroot00000000000000from apns2.client import NotificationPriority from apns2.errors import BadTopic, PayloadTooLarge, Unregistered from django.conf import settings from django.test import override_settings, TestCase from push_notifications.apns import APNSError from push_notifications.models import APNSDevice from ._mock import mock class APNSModelTestCase(TestCase): def _create_devices(self, devices): for device in devices: APNSDevice.objects.create(registration_id=device) @override_settings() def test_apns_send_bulk_message(self): self._create_devices(["abc", "def"]) # legacy conf manager requires a value settings.PUSH_NOTIFICATIONS_SETTINGS.update({ "APNS_CERTIFICATE": "/path/to/apns/certificate.pem" }) with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification_batch") as s: APNSDevice.objects.all().send_message("Hello world", expiration=1) args, kargs = s.call_args self.assertEqual(args[0][0].token, "abc") self.assertEqual(args[0][1].token, "def") self.assertEqual(args[0][0].payload.alert, "Hello world") self.assertEqual(args[0][1].payload.alert, "Hello world") self.assertEqual(kargs["expiration"], 1) def test_apns_send_message_extra(self): self._create_devices(["abc"]) with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: APNSDevice.objects.get().send_message( "Hello world", expiration=2, priority=5, extra={"foo": "bar"}) args, kargs = s.call_args self.assertEqual(args[0], "abc") self.assertEqual(args[1].alert, "Hello world") self.assertEqual(args[1].custom, {"foo": "bar"}) self.assertEqual(kargs["priority"], NotificationPriority.Delayed) self.assertEqual(kargs["expiration"], 2) def test_apns_send_message(self): self._create_devices(["abc"]) with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: APNSDevice.objects.get().send_message("Hello world", expiration=1) args, kargs = s.call_args self.assertEqual(args[0], "abc") self.assertEqual(args[1].alert, "Hello world") self.assertEqual(kargs["expiration"], 1) def test_apns_send_message_to_single_device_with_error(self): # these errors are device specific, device.active will be set false devices = ["abc"] self._create_devices(devices) with mock.patch("push_notifications.apns._apns_send") as s: s.side_effect = Unregistered device = APNSDevice.objects.get(registration_id="abc") with self.assertRaises(APNSError) as ae: device.send_message("Hello World!") self.assertEqual(ae.exception.status, "Unregistered") self.assertFalse(APNSDevice.objects.get(registration_id="abc").active) def test_apns_send_message_to_several_devices_with_error(self): # these errors are device specific, device.active will be set false devices = ["abc", "def", "ghi"] expected_exceptions_statuses = ["PayloadTooLarge", "BadTopic", "Unregistered"] self._create_devices(devices) with mock.patch("push_notifications.apns._apns_send") as s: s.side_effect = [PayloadTooLarge, BadTopic, Unregistered] for idx, token in enumerate(devices): device = APNSDevice.objects.get(registration_id=token) with self.assertRaises(APNSError) as ae: device.send_message("Hello World!") self.assertEqual(ae.exception.status, expected_exceptions_statuses[idx]) if idx == 2: self.assertFalse(APNSDevice.objects.get(registration_id=token).active) else: self.assertTrue(APNSDevice.objects.get(registration_id=token).active) def test_apns_send_message_to_bulk_devices_with_error(self): # these errors are device specific, device.active will be set false devices = ["abc", "def", "ghi"] results = {"abc": "PayloadTooLarge", "def": "BadTopic", "ghi": "Unregistered"} self._create_devices(devices) with mock.patch("push_notifications.apns._apns_send") as s: s.return_value = results results = APNSDevice.objects.all().send_message("Hello World!") for idx, token in enumerate(devices): if idx == 2: self.assertFalse(APNSDevice.objects.get(registration_id=token).active) else: self.assertTrue(APNSDevice.objects.get(registration_id=token).active) django-push-notifications-1.6.0/tests/test_apns_push_payload.py000066400000000000000000000071331323442102600250710ustar00rootroot00000000000000from apns2.client import NotificationPriority from django.test import TestCase from push_notifications.apns import _apns_send, APNSUnsupportedPriority from ._mock import mock class APNSPushPayloadTest(TestCase): def test_push_payload(self): with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: _apns_send( "123", "Hello world", badge=1, sound="chime", extra={"custom_data": 12345}, expiration=3 ) self.assertTrue(s.called) args, kargs = s.call_args self.assertEqual(args[0], "123") self.assertEqual(args[1].alert, "Hello world") self.assertEqual(args[1].badge, 1) self.assertEqual(args[1].sound, "chime") self.assertEqual(args[1].custom, {"custom_data": 12345}) self.assertEqual(kargs["expiration"], 3) def test_push_payload_with_thread_id(self): with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: _apns_send( "123", "Hello world", thread_id="565", sound="chime", extra={"custom_data": 12345}, expiration=3 ) args, kargs = s.call_args self.assertEqual(args[0], "123") self.assertEqual(args[1].alert, "Hello world") self.assertEqual(args[1].thread_id, "565") self.assertEqual(args[1].sound, "chime") self.assertEqual(args[1].custom, {"custom_data": 12345}) self.assertEqual(kargs["expiration"], 3) def test_push_payload_with_alert_dict(self): with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: _apns_send( "123", alert={"title": "t1", "body": "b1"}, sound="chime", extra={"custom_data": 12345}, expiration=3 ) args, kargs = s.call_args self.assertEqual(args[0], "123") self.assertEqual(args[1].alert["body"], "b1") self.assertEqual(args[1].alert["title"], "t1") self.assertEqual(args[1].sound, "chime") self.assertEqual(args[1].custom, {"custom_data": 12345}) self.assertEqual(kargs["expiration"], 3) def test_localised_push_with_empty_body(self): with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: _apns_send("123", None, loc_key="TEST_LOC_KEY", expiration=3) args, kargs = s.call_args self.assertEqual(args[0], "123") self.assertEqual(args[1].alert.body_localized_key, "TEST_LOC_KEY") self.assertEqual(kargs["expiration"], 3) def test_using_extra(self): with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: _apns_send( "123", "sample", extra={"foo": "bar"}, expiration=30, priority=10 ) args, kargs = s.call_args self.assertEqual(args[0], "123") self.assertEqual(args[1].alert, "sample") self.assertEqual(args[1].custom, {"foo": "bar"}) self.assertEqual(kargs["priority"], NotificationPriority.Immediate) self.assertEqual(kargs["expiration"], 30) def test_bad_priority(self): with mock.patch("apns2.credentials.init_context"): with mock.patch("apns2.client.APNsClient.connect"): with mock.patch("apns2.client.APNsClient.send_notification") as s: self.assertRaises(APNSUnsupportedPriority, _apns_send, "123", "_" * 2049, priority=24) s.assert_has_calls([]) django-push-notifications-1.6.0/tests/test_app_config.py000066400000000000000000000173751323442102600234760ustar00rootroot00000000000000import 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) as ic: manager._get_application_settings(None, None, None) self.assertEqual( str(ic.exception), "push_notifications.conf.AppConfig requires the application_id be " "specified at all times." ) 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) as ic: manager._get_application_settings(application_id, "FCM", "API_KEY") self.assertEqual( str(ic.exception), "No application configured with application_id: {}.".format(application_id) ) 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) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['{}']['PLATFORM'] is required." " Must be one of: APNS, FCM, GCM, WNS.".format(application_id) ) 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) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['{}']['PLATFORM'] is invalid." " Must be one of: APNS, FCM, GCM, WNS.".format(application_id) ) 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) with self.assertRaises(ImproperlyConfigured) as ic: manager._get_application_settings(application_id, "APNS", "CERTIFICATE") self.assertEqual( str(ic.exception), "Application 'my_fcm_app' (FCM) does not support the setting 'CERTIFICATE'." ) 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) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['my_fcm_app']['API_KEY'] is missing." ) def test_validate_apns_config(self): """Verify the settings for APNS platform.""" path = os.path.join(os.path.dirname(__file__), "test_data", "good_revoked.pem") # # all settings specified, required and optional, does not raise an error. # PUSH_SETTINGS = { "APPLICATIONS": { "my_apns_app": { "PLATFORM": "APNS", "CERTIFICATE": path, "USE_ALTERNATIVE_PORT": True, "USE_SANDBOX": True } } } AppConfig(PUSH_SETTINGS) # # missing required settings # PUSH_SETTINGS = { "APPLICATIONS": { "my_apns_app": { "PLATFORM": "APNS", } } } with self.assertRaises(ImproperlyConfigured) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['my_apns_app']['CERTIFICATE'] is missing." ) # # all optional settings have default values # PUSH_SETTINGS = { "APPLICATIONS": { "my_apns_app": { "PLATFORM": "APNS", "CERTIFICATE": path, } } } manager = AppConfig(PUSH_SETTINGS) app_config = manager._settings["APPLICATIONS"]["my_apns_app"] assert app_config["USE_SANDBOX"] is False assert app_config["USE_ALTERNATIVE_PORT"] is False 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) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['my_fcm_app']['API_KEY'] is missing." ) # # 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) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['my_gcm_app']['API_KEY'] is missing." ) # # 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) as ic: AppConfig(PUSH_SETTINGS) self.assertEqual( str(ic.exception), "PUSH_NOTIFICATIONS_SETTINGS.APPLICATIONS['my_wns_app']" "['PACKAGE_SECURITY_ID'] is missing." ) # # 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" django-push-notifications-1.6.0/tests/test_data/000077500000000000000000000000001323442102600217135ustar00rootroot00000000000000django-push-notifications-1.6.0/tests/test_data/good_revoked.pem000066400000000000000000000102321323442102600250630ustar00rootroot00000000000000Bag 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: -----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----- django-push-notifications-1.6.0/tests/test_data/good_with_passwd.pem000066400000000000000000000074751323442102600257770ustar00rootroot00000000000000-----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: -----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----- django-push-notifications-1.6.0/tests/test_data/without_private.pem000066400000000000000000000037101323442102600256540ustar00rootroot00000000000000-----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----- django-push-notifications-1.6.0/tests/test_gcm_push_payload.py000066400000000000000000000062571323442102600247040ustar00rootroot00000000000000from __future__ import absolute_import from django.test import TestCase 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_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'{"notification":{"body":"Hello world"},"registration_ids":["abc"]}', "application/json", application_id=None) 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_id=None) django-push-notifications-1.6.0/tests/test_legacy_config.py000066400000000000000000000021321323442102600241430ustar00rootroot00000000000000from 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." ) django-push-notifications-1.6.0/tests/test_models.py000066400000000000000000000434301323442102600226430ustar00rootroot00000000000000from __future__ import absolute_import import json from django.test import TestCase from django.utils import timezone from push_notifications.gcm import GCMError, send_bulk_message from push_notifications.models import APNSDevice, GCMDevice from . import responses from ._mock import mock class GCMModelTestCase(TestCase): def _create_devices(self, devices): for device in devices: GCMDevice.objects.create(registration_id=device, cloud_message_type="GCM") def _create_fcm_devices(self, devices): for device in devices: GCMDevice.objects.create(registration_id=device, cloud_message_type="FCM") 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_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 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_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_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 ) def test_gcm_send_message_to_multiple_devices(self): self._create_devices(["abc", "abc1"]) 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, cloud_message_type="GCM") GCMDevice.objects.create(registration_id="xyz", active=False, cloud_message_type="GCM") 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 ) def test_gcm_send_message_collapse_to_multiple_devices(self): self._create_devices(["abc", "abc1"]) 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 ) 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) 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_to_single_device_with_error_mismatch(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_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_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_multiple_devices_with_error_b(self): self._create_devices(["abc", "abc1", "abc2"]) 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_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_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=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_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" ) with mock.patch( "push_notifications.gcm._gcm_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_gcm_send_message_with_no_reg_ids(self): self._create_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"}, "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()) django-push-notifications-1.6.0/tests/test_rest_framework.py000066400000000000000000000102461323442102600244110ustar00rootroot00000000000000from django.test import TestCase from push_notifications.api.rest_framework import ( APNSDeviceSerializer, GCMDeviceSerializer, ValidationError ) 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 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", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) self.assertTrue(serializer.is_valid()) # valid data - 32 bytes lower case serializer = APNSDeviceSerializer(data={ "registration_id": "aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae", "name": "Apple iPhone 6+", "device_id": "ffffffffffffffffffffffffffffffff", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) self.assertTrue(serializer.is_valid()) # valid data - 100 bytes upper case serializer = APNSDeviceSerializer(data={ "registration_id": "AE" * 100, "name": "Apple iPhone 6+", "device_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", }) self.assertTrue(serializer.is_valid()) # valid data - 100 bytes lower case serializer = APNSDeviceSerializer(data={ "registration_id": "ae" * 100, "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", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) self.assertFalse(serializer.is_valid()) class GCMDeviceSerializerTestCase(TestCase): def test_device_id_validation_pass(self): serializer = GCMDeviceSerializer(data={ "registration_id": "foobar", "name": "Galaxy Note 3", "device_id": "0x1031af3b", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) self.assertTrue(serializer.is_valid()) def test_registration_id_unique(self): """Validate that a duplicate registration id raises a validation error.""" # add a device serializer = GCMDeviceSerializer(data={ "registration_id": "foobar", "name": "Galaxy Note 3", "device_id": "0x1031af3b", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) serializer.is_valid(raise_exception=True) obj = serializer.save() # ensure updating the same object works serializer = GCMDeviceSerializer(obj, data={ "registration_id": "foobar", "name": "Galaxy Note 5", "device_id": "0x1031af3b", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) serializer.is_valid(raise_exception=True) obj = serializer.save() # try to add a new device with the same token serializer = GCMDeviceSerializer(data={ "registration_id": "foobar", "name": "Galaxy Note 3", "device_id": "0xdeadbeaf", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) with self.assertRaises(ValidationError): serializer.is_valid(raise_exception=True) 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) def test_device_id_validation_fail_out_of_range(self): serializer = GCMDeviceSerializer(data={ "registration_id": "foobar", "name": "Galaxy Note 3", "device_id": "10000000000000000", # 2**64 "application_id": "XXXXXXXXXXXXXXXXXXXX", }) self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, GCM_DRF_OUT_OF_RANGE_ERROR) def test_device_id_validation_value_between_signed_unsigned_64b_int_maximums(self): """ 2**63 < 0xe87a4e72d634997c < 2**64 """ serializer = GCMDeviceSerializer(data={ "registration_id": "foobar", "name": "Nexus 5", "device_id": "e87a4e72d634997c", "application_id": "XXXXXXXXXXXXXXXXXXXX", }) self.assertTrue(serializer.is_valid()) django-push-notifications-1.6.0/tests/test_wns.py000066400000000000000000000125021323442102600221630ustar00rootroot00000000000000import 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"", 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) django-push-notifications-1.6.0/tox.ini000066400000000000000000000012201323442102600201070ustar00rootroot00000000000000[tox] envlist = py27-django111 py34-django{111} py36-django{111,20,master} flake8 [testenv] 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 apns2>=0.3.0 [testenv:flake8] commands = flake8 deps = flake8 flake8-import-order flake8-quotes [flake8] ignore = W191,I201 max-line-length = 92 exclude = .tox, push_notifications/migrations import-order-style = smarkets inline-quotes = double