├── .gitignore ├── .pypirc ├── LICENSE ├── MANIFEST ├── README.md ├── example ├── example │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── requirements.txt ├── setup.py └── snsredis ├── __init__.py ├── admin.py ├── apps.py ├── helpers.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── signals.py ├── tasks.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.pypirc: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers=pypi 3 | 4 | [pypi] 5 | repository = https://upload.pypi.org/legacy/ 6 | username = pauarge -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Pau Argelaguet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | snsredis/__init__.py 4 | snsredis/admin.py 5 | snsredis/apps.py 6 | snsredis/helpers.py 7 | snsredis/models.py 8 | snsredis/signals.py 9 | snsredis/tasks.py 10 | snsredis/tests.py 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django SNS Redis 2 | 3 | ## Welcome! 4 | 5 | Welcome to Django SNS Redis! This package lets you send push notifications right from your [Django](https://www.djangoproject.com) project using [Amazon's](https://aws.amazon.com) [Simple Notification Service](https://aws.amazon.com/sns/). The cool thing about it is that is has a built-in [Redis](http://redis.io) cache for storing devices' ARNs (specially usefull when you have tons of users and don't wanna squeeze your relational database). 6 | 7 | ## How it works 8 | Django SNS Redis allows you to register devices with their registration_id provided by GCM/APNS. Then, it stores it to a database and obtains an ARN from SNS. 9 | 10 | Since that ARN is associated to a user and stored in Redis, when publishing a message, you won't hit the database. That, added to the features SNS provides, will help your app deliver notifications much faster and with less resources. 11 | 12 | ## Requirements 13 | The library might work with versions prior to the ones specified, but they are not tested. Feel free to submit a pull request if you have tested other versions (or if you have patched the library!). 14 | 15 | * [Python](https://www.python.org/) (2.7+) 16 | * [Django](https://www.djangoproject.com/) (1.8+) 17 | * [Django Redis](https://github.com/niwinz/django-redis) (4.3+) 18 | * [Boto](https://github.com/boto/boto) (2.42+) 19 | 20 | ## Setup 21 | Add the following to your Django settings file (usually, settings.py): 22 | 23 | ~~~~ 24 | INSTALLED_APPS = [..., snsredis, ...] 25 | 26 | AWS_ACCESS_KEY_ID = [your key id] 27 | AWS_SECRET_ACCESS_KEY = [your secret] 28 | AWS_REGION_NAME = [region where you have the desired SNS instances] 29 | 30 | AWS_SNS_APNS_ARN = [ARN for your iOS app] 31 | AWS_SNS_GCM_ARN = [ARN for your Android app] 32 | 33 | SNSREDIS_REDIS_CONNECTION = [name of the redis connection] 34 | ~~~~ 35 | 36 | What are we doing there? 37 | 38 | First, we are adding the library to your Django project installed apps. This is necessary for the rest of the project to *talk* to the library. 39 | 40 | Then we are adding some keys that Amazon will provide you. 41 | 42 | * **ACCESS KEY ID** and **SECRET ACCESS KEY** is a pair of keys that let you use your AWS resources from outside. You can set up them [here](https://console.aws.amazon.com/iam/home). Make sure your keys have permissions to operate SNS. 43 | 44 | * **REGION NAME** is the region in which you have configured your SNS instances. *Default: eu-west-1* 45 | 46 | * **SNS APNS/GCM ARN** (ARN stands for Amazon Resource Name). IDs of the SNS applications (you can register them [here](https://eu-west-1.console.aws.amazon.com/sns/v2/home)). This parameters are only required for the platforms you want to use. 47 | 48 | * **SNSREDIS REDIS CONNECTION**. Name for the Redis connection the library is going to use. Use this parameter only if you are using multiple redis connections. *Default: default* 49 | 50 | **Remember to apply migrations!** 51 | 52 | ## Usage 53 | 54 | ### Adding a user token 55 | 56 | ~~~~ 57 | from django.contrib.auth.models import User 58 | from snsredis.tasks import add_token 59 | 60 | user = User.objects.get(id=1) 61 | registration_id = 'xxxxxxxxx' 62 | platform = 'gcm' 63 | 64 | add_token(user, registration_id, platform) 65 | 66 | ~~~~ 67 | 68 | ### Publishing a message 69 | 70 | ~~~~ 71 | from django.contrib.auth.models import User 72 | from snsredis.tasks import publish 73 | 74 | user = User.objects.get(id=1) 75 | message = 'Hello world' 76 | extra = { 77 | 'post': 123, 78 | 'url': 'http://www.example.com' 79 | } 80 | sound = 'default.mp3' 81 | badge = 24 82 | 83 | publish(user, message=message, extra=extra, sound=sound, badge=badge) 84 | 85 | ~~~~ 86 | 87 | ### Removing user token 88 | 89 | ~~~~ 90 | from django.contrib.auth.models import User 91 | from snsredis.tasks import remove_token 92 | 93 | user = User.objects.get(id=1) 94 | registration_id = 'xxxxxxxxx' 95 | 96 | remove_token(user, registration_id) 97 | 98 | ~~~~ -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauarge/django-sns-redis/991c408a128693a88acc0fa0352abcc3a4ec9ce3/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '#d!onjhk@ttg!cvgr_e3fssim$pgz04t@q7e)_35)a8b+25^#b' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'snsredis', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'example.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'example.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | 123 | 124 | # SNSRedis setup 125 | 126 | AWS_ACCESS_KEY_ID = '[your key id]' 127 | AWS_SECRET_ACCESS_KEY = '[your secret]' 128 | AWS_REGION_NAME = 'eu-west-1' 129 | 130 | AWS_SNS_APNS_ARN = '[ARN for your iOS app]' 131 | AWS_SNS_GCM_ARN = '[ARN for your Android app]' 132 | 133 | SNSREDIS_REDIS_CONNECTION = 'default' 134 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | """example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | boto==2.42.0 2 | Django==1.11.28 3 | django-redis==4.4.4 4 | django-sns-redis==0.1.8 5 | redis==2.10.5 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='django-sns-redis', 5 | version='0.2.3', 6 | packages=['snsredis',], 7 | license='MIT License', 8 | description='Simple package for sending push notifications through AWS SNS using Django and Redis', 9 | ) 10 | -------------------------------------------------------------------------------- /snsredis/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'snsredis.apps.SNSRedisConfig' 2 | -------------------------------------------------------------------------------- /snsredis/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import SNSToken 4 | 5 | 6 | class SNSTokenAdmin(admin.ModelAdmin): 7 | list_filter = ['platform'] 8 | list_display = ('user', 'platform', 'created') 9 | search_fields = ['user__username'] 10 | raw_id_fields = ['user'] 11 | 12 | 13 | admin.site.register(SNSToken, SNSTokenAdmin) 14 | -------------------------------------------------------------------------------- /snsredis/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SNSRedisConfig(AppConfig): 5 | name = 'snsredis' 6 | 7 | def ready(self): 8 | from snsredis import signals -------------------------------------------------------------------------------- /snsredis/helpers.py: -------------------------------------------------------------------------------- 1 | from boto import sns 2 | from django.conf import settings 3 | from django_redis import get_redis_connection 4 | import collections 5 | import json 6 | 7 | 8 | def format_message(message, extra=None, sound=None, badge=None, mutable_content=1, apns_category=None): 9 | if not isinstance(extra, collections.Mapping): 10 | extra = {} 11 | 12 | aps = { 13 | 'alert': message, 14 | 'mutable-content': mutable_content 15 | } 16 | if apns_category: 17 | aps['category'] = apns_category 18 | if sound: 19 | aps['sound'] = sound 20 | if badge: 21 | try: 22 | aps['badge'] = int(badge) 23 | except ValueError: 24 | pass 25 | apns = { 26 | "aps": aps, 27 | } 28 | apns.update(extra) 29 | 30 | gcm = { 31 | 'message': message 32 | } 33 | gcm.update(extra) 34 | 35 | data = { 36 | "APNS": json.dumps(apns), 37 | "GCM": json.dumps({ 38 | "data": gcm 39 | }) 40 | } 41 | return json.dumps(data) 42 | 43 | 44 | def get_connection_sns(): 45 | region = sns.connect_to_region(settings.AWS_REGION_NAME, aws_access_key_id=settings.AWS_ACCESS_KEY_ID, 46 | aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY).region 47 | return sns.SNSConnection(aws_access_key_id=settings.AWS_ACCESS_KEY_ID, region=region, 48 | aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY) 49 | 50 | 51 | def get_connection_redis(): 52 | if settings.SNSREDIS_REDIS_CONNECTION: 53 | return get_redis_connection(settings.SNSREDIS_REDIS_CONNECTION) 54 | else: 55 | return get_redis_connection('default') 56 | -------------------------------------------------------------------------------- /snsredis/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='SNSToken', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('arn', models.CharField(max_length=128)), 20 | ('registration_id', models.TextField()), 21 | ('platform', models.CharField(max_length=4, choices=[(b'apns', b'APNS'), (b'gcm', b'GCM')])), 22 | ('created', models.DateTimeField(auto_now_add=True)), 23 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 24 | ], 25 | ), 26 | migrations.AlterUniqueTogether( 27 | name='snstoken', 28 | unique_together=set([('arn', 'user')]), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /snsredis/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauarge/django-sns-redis/991c408a128693a88acc0fa0352abcc3a4ec9ce3/snsredis/migrations/__init__.py -------------------------------------------------------------------------------- /snsredis/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.db import models 4 | from django.utils.encoding import python_2_unicode_compatible 5 | 6 | from .helpers import get_connection_sns, get_connection_redis 7 | 8 | VALID_PLATFORMS = ( 9 | ('apns', 'APNS'), 10 | ('gcm', 'GCM') 11 | ) 12 | 13 | 14 | @python_2_unicode_compatible 15 | class SNSToken(models.Model): 16 | arn = models.CharField(max_length=128) 17 | registration_id = models.TextField() 18 | user = models.ForeignKey(User, db_index=True) 19 | platform = models.CharField(max_length=4, choices=VALID_PLATFORMS) 20 | created = models.DateTimeField(auto_now_add=True) 21 | 22 | def __str__(self): 23 | try: 24 | return '{} - {}'.format(self.user, self.platform) 25 | except UnicodeDecodeError: 26 | pass 27 | 28 | class Meta: 29 | app_label = 'snsredis' 30 | unique_together = ('arn', 'user') 31 | 32 | 33 | class UserManager(object): 34 | def __init__(self, user): 35 | self.redis = get_connection_redis() 36 | self.conn = get_connection_sns() 37 | self.user = user 38 | 39 | def get_endpoints(self): 40 | return self.redis.lrange('sns-endpoints:{}'.format(self.user.id), 0, -1) 41 | 42 | def get_user(self): 43 | return self.user 44 | 45 | def set_user(self, user): 46 | self.user = user 47 | 48 | def add_token(self, token, platform): 49 | if isinstance(token, str) or isinstance(token, unicode): 50 | if platform == 'apns': 51 | app_arn = settings.AWS_SNS_APNS_ARN 52 | elif platform == 'gcm': 53 | app_arn = settings.AWS_SNS_GCM_ARN 54 | else: 55 | return 56 | ret = self.conn.create_platform_endpoint(platform_application_arn=app_arn, token=token) 57 | endpoint_arn = ret.get('CreatePlatformEndpointResponse').get('CreatePlatformEndpointResult').get( 58 | 'EndpointArn') 59 | SNSToken.objects.get_or_create(user=self.user, arn=endpoint_arn, 60 | defaults={'registration_id': token, 'platform': platform}) 61 | 62 | def remove_token(self, token): 63 | if isinstance(token, str) or isinstance(token, unicode): 64 | obj = SNSToken.objects.filter(user=self.user, registration_id=token) 65 | if obj: 66 | for o in obj: 67 | self.conn.delete_endpoint(o.arn) 68 | o.delete() 69 | 70 | 71 | class TopicManager(object): 72 | def __init__(self): 73 | raise NotImplementedError 74 | -------------------------------------------------------------------------------- /snsredis/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save, post_delete 2 | from django.dispatch import receiver 3 | 4 | from .helpers import get_connection_redis 5 | from .models import SNSToken 6 | 7 | 8 | @receiver(post_save, sender=SNSToken) 9 | def add_token_to_redis(sender, instance=None, created=False, **kwargs): 10 | if created: 11 | redis = get_connection_redis() 12 | hash_arn = 'sns-endpoints:{}'.format(instance.user_id) 13 | redis.lpush(hash_arn, instance.arn) 14 | 15 | 16 | @receiver(post_delete, sender=SNSToken) 17 | def remove_token_from_redis(sender, instance=None, **kwargs): 18 | redis = get_connection_redis() 19 | redis.lrem('sns-endpoints:{}'.format(instance.user_id), 0, instance.arn) 20 | -------------------------------------------------------------------------------- /snsredis/tasks.py: -------------------------------------------------------------------------------- 1 | from boto.exception import BotoServerError 2 | from django.conf import settings 3 | from django.contrib.auth.models import User 4 | 5 | from .helpers import format_message, get_connection_sns, get_connection_redis 6 | from .models import SNSToken, UserManager 7 | 8 | 9 | def add_token(user, token, platform): 10 | manager = UserManager(user) 11 | manager.add_token(token, platform) 12 | 13 | 14 | def remove_token(user, token): 15 | manager = UserManager(user) 16 | manager.remove_token(token) 17 | 18 | 19 | def prune_tokens(platform): 20 | if platform == 'apns': 21 | app_arn = settings.AWS_SNS_APNS_ARN 22 | elif platform == 'gcm': 23 | app_arn = settings.AWS_SNS_GCM_ARN 24 | else: 25 | return 26 | conn = get_connection_sns() 27 | res = conn.list_endpoints_by_platform_application(platform_application_arn=app_arn) 28 | while res.get('ListEndpointsByPlatformApplicationResponse').get('ListEndpointsByPlatformApplicationResult').get( 29 | 'NextToken'): 30 | endpoints = res.get('ListEndpointsByPlatformApplicationResponse').get( 31 | 'ListEndpointsByPlatformApplicationResult').get('Endpoints') 32 | for ep in endpoints: 33 | endpoint_arn = ep.get('EndpointArn') 34 | token = ep.get('Attributes').get('Token') 35 | if ep.get('Attributes').get('Enabled') == 'true': 36 | objs = SNSToken.objects.filter(registration_id=token).exclude(arn=endpoint_arn) 37 | for o in objs: 38 | conn.delete_endpoint(o.arn) 39 | o.delete() 40 | else: 41 | conn.delete_endpoint(endpoint_arn) 42 | SNSToken.objects.filter(arn=endpoint_arn).delete() 43 | res = conn.list_endpoints_by_platform_application(platform_application_arn=app_arn, next_token=res.get( 44 | 'ListEndpointsByPlatformApplicationResponse').get('ListEndpointsByPlatformApplicationResult').get( 45 | 'NextToken')) 46 | 47 | 48 | def prune_user_tokens(user): 49 | tokens = SNSToken.objects.filter(user=user) 50 | if len(tokens) > 1: 51 | conn = get_connection_sns() 52 | registred_ids = [] 53 | for t in tokens: 54 | attr = conn.get_endpoint_attributes(t.arn).get('GetEndpointAttributesResponse').get( 55 | 'GetEndpointAttributesResult').get('Attributes') 56 | if attr.get('Enabled') == 'true': 57 | if attr.get('Token') in registred_ids: 58 | conn.delete_endpoint(t.arn) 59 | t.delete() 60 | else: 61 | registred_ids.append(attr.get('Token')) 62 | else: 63 | conn.delete_endpoint(t.arn) 64 | t.delete() 65 | 66 | 67 | def rebuild_redis(): 68 | redis = get_connection_redis() 69 | users = User.objects.all() 70 | for user in users: 71 | _hash = 'sns-endpoints:{}'.format(user.id) 72 | tokens = SNSToken.objects.filter(user=user) 73 | redis.delete(_hash) 74 | for t in tokens: 75 | redis.lpush(_hash, t.arn) 76 | 77 | 78 | def publish(user, message=None, extra=None, sound=None, badge=None, mutable_content=1, apns_category=None): 79 | manager = UserManager(user) 80 | endpoints = manager.get_endpoints() 81 | 82 | if len(endpoints) > 0: 83 | formatted_message = format_message(message, extra, sound, badge, mutable_content, apns_category=apns_category) 84 | conn = get_connection_sns() 85 | 86 | for ep in endpoints: 87 | try: 88 | conn.publish(target_arn=ep, message=formatted_message, message_structure='json') 89 | except BotoServerError as e: 90 | obj = SNSToken.objects.filter(user=user, arn=ep) 91 | if e.error_code == 'EndpointDisabled': 92 | obj.delete() 93 | conn.delete_endpoint(ep) 94 | elif e.error_code == 'InvalidParameter': 95 | obj.delete() 96 | else: 97 | raise e 98 | -------------------------------------------------------------------------------- /snsredis/tests.py: -------------------------------------------------------------------------------- 1 | from builtins import input 2 | from django.test import TestCase 3 | from django.contrib.auth.models import User 4 | 5 | from .models import SNSToken 6 | from .helpers import get_connection_redis, get_connection_sns 7 | from .tasks import add_token, publish, remove_token 8 | 9 | 10 | class SNSRedisTestCase(TestCase): 11 | def setUp(self): 12 | User.objects.create(username="test_user") 13 | 14 | def register_device(self): 15 | redis = get_connection_redis() 16 | self.assertEqual(True, True) 17 | 18 | def send_notification(self): 19 | self.assertEqual(True, True) 20 | 21 | def unregister_device(self): 22 | self.assertEqual(True, True) 23 | --------------------------------------------------------------------------------