Apple Push Notifications POC

clubs
Laurent 1 year ago
parent 7224fb8eeb
commit 9b2bd5ec50
  1. 6
      AuthKey_DHJRAU6BCZ.p8
  2. 8
      api/serializers.py
  3. 1
      api/urls.py
  4. 32
      api/views.py
  5. 2
      requirements.txt
  6. 5
      tournaments/admin.py
  7. 28
      tournaments/migrations/0075_alter_club_creator_devicetoken.py
  8. 18
      tournaments/migrations/0076_alter_devicetoken_value.py
  9. 18
      tournaments/migrations/0077_alter_devicetoken_value.py
  10. 1
      tournaments/models/__init__.py
  11. 10
      tournaments/models/device_token.py
  12. 4
      tournaments/urls.py
  13. 74
      tournaments/utils/apns.py
  14. 2
      tournaments/utils/cryptography.py
  15. 8
      tournaments/views.py

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgtQe/9xXcsNYYOHhs
4EeJIo2qpoCZ/lLdkMnttM3jMF6gCgYIKoZIzj0DAQehRANCAAQU5IruCl0xw3xX
4WJVMZGyFINAA6nTj13nvD5P3fNzYFepgYVBy+ZBFvWrGHi75VnojiRR6v3e+z2K
0DinoPJF
-----END PRIVATE KEY-----

@ -1,6 +1,6 @@
from rest_framework import serializers
from tournaments.models.court import Court
from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log
from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken
from django.contrib.auth import password_validation
from django.utils.translation import gettext_lazy as _
# email
@ -207,3 +207,9 @@ class LogSerializer(serializers.ModelSerializer):
class Meta:
model = Log
fields = '__all__'
class DeviceTokenSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceToken
fields = '__all__'
read_only_fields = ['user']

@ -20,6 +20,7 @@ router.register(r'courts', views.CourtViewSet)
router.register(r'date-intervals', views.DateIntervalViewSet)
router.register(r'failed-api-calls', views.FailedApiCallViewSet)
router.register(r'logs', views.LogViewSet)
router.register(r'device-token', views.DeviceTokenViewSet)
urlpatterns = [
path('', include(router.urls)),

@ -1,5 +1,5 @@
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, UserUpdateSerializer, FailedApiCallSerializer, LogSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log
from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, TournamentSerializer, UserSerializer, ChangePasswordSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, LiveMatchSerializer, PurchaseSerializer, UserUpdateSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer
from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken
from rest_framework import viewsets, permissions
from rest_framework.authtoken.models import Token
@ -242,3 +242,31 @@ class LogViewSet(viewsets.ModelViewSet):
serializer.save(user=self.request.user)
else:
serializer.save()
class DeviceTokenViewSet(viewsets.ModelViewSet):
queryset = DeviceToken.objects.all()
serializer_class = DeviceTokenSerializer
def get_queryset(self):
if self.request.user:
return self.queryset.filter(user=self.request.user)
return []
# def create(self, request, *args, **kwargs):
# value = request.data.get('value')
# if DeviceToken.objects.filter(value=value).exists():
# return Response({"detail": "This device token is already registered."}, status=208)
# print('a')
# serializer = self.get_serializer(data=request.data)
# print('b')
# # serializer.is_valid(raise_exception=True)
# print('c')
# self.perform_create(serializer)
# print('d')
# headers = self.get_success_headers(serializer.data)
# return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
# serializer.is_valid(raise_exception=True)
serializer.save(user=self.request.user)

@ -5,3 +5,5 @@ dj-rest-auth==5.1.0
django-qr-code==4.0.1
pycryptodome==3.20.0
requests==2.31.0
PyJWT==2.8.0
hyper==0.7.0

@ -1,6 +1,7 @@
from django.contrib import admin
from tournaments.models import team_registration
from tournaments.models.device_token import DeviceToken
from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log
from django.contrib.auth.admin import UserAdmin
@ -89,6 +90,9 @@ class LogAdmin(admin.ModelAdmin):
list_display = ['date', 'user', 'message']
list_filter = ['user']
class DeviceTokenAdmin(admin.ModelAdmin):
list_display = ['user', 'value']
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Club, ClubAdmin)
admin.site.register(Event, EventAdmin)
@ -104,3 +108,4 @@ admin.site.register(Court, CourtAdmin)
admin.site.register(DateInterval, DateIntervalAdmin)
admin.site.register(FailedApiCall, FailedApiCallAdmin)
admin.site.register(Log, LogAdmin)
admin.site.register(DeviceToken, DeviceTokenAdmin)

@ -0,0 +1,28 @@
# Generated by Django 4.2.11 on 2024-07-12 10:01
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0074_customuser_device_id'),
]
operations = [
migrations.AlterField(
model_name='club',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='DeviceToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.BinaryField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2024-07-12 16:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0075_alter_club_creator_devicetoken'),
]
operations = [
migrations.AlterField(
model_name='devicetoken',
name='value',
field=models.CharField(max_length=240),
),
]

@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2024-07-14 12:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tournaments', '0076_alter_devicetoken_value'),
]
operations = [
migrations.AlterField(
model_name='devicetoken',
name='value',
field=models.TextField(),
),
]

@ -15,3 +15,4 @@ from .team_score import TeamScore
from .purchase import Purchase
from .failed_api_call import FailedApiCall
from .log import Log
from .device_token import DeviceToken

@ -0,0 +1,10 @@
from django.db import models
from . import CustomUser
import uuid
class DeviceToken(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
value = models.TextField()
def __str__(self):
return f"{self.value}"

@ -30,5 +30,7 @@ urlpatterns = [
),
path('players/', views.players, name='players'),
path('activate/<uidb64>/<token>/', views.activate, name='activate'),
path('download/', views.download, name='download')
path('download/', views.download, name='download'),
path('apns/', views.test_apns, name='test-apns')
]

@ -0,0 +1,74 @@
import http.client
import json
import jwt
import time
from hyper import HTTP20Connection
# APPLE WARNING: Reuse a connection as long as possible.
# In most cases, you can reuse a connection for many hours to days.
# If your connection is mostly idle, you may send a HTTP2 PING frame after an hour of inactivity.
# Reusing a connection often results in less bandwidth and CPU consumption.
# https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
def generate_jwt(key_path, key_id, team_id):
with open(key_path, 'r') as key_file:
key = key_file.read()
headers = {
"alg": "ES256",
"kid": key_id
}
payload = {
"iss": team_id,
"iat": time.time()
}
token = jwt.encode(payload, key, algorithm="ES256", headers=headers)
return token
key_path = 'AuthKey_DHJRAU6BCZ.p8'
key_id = 'DHJRAU6BCZ'
team_id = 'BQ3Y44M3Q6'
bundle_id = 'app.padelclub'
# device_token = 'user_device_token'
message = 'Hello, World!'
def send_push_notification(device_token, message):
jwt_token = generate_jwt(key_path, key_id, team_id)
# APNs endpoint (use 'api.push.apple.com' for production)
host = 'api.sandbox.push.apple.com'
payload = {
"aps": {
"alert": message,
"sound": "default"
}
}
payload_json = json.dumps(payload)
# Create the HTTP connection
connection = HTTP20Connection(host, port=443)
# Set the headers
headers = {
"apns-topic": bundle_id,
"authorization": f"bearer {jwt_token}",
"content-type": "application/json"
}
# Send the notification
connection.request(
"POST",
f"/3/device/{device_token}",
body=payload_json,
headers=headers
)
response = connection.get_response()
response_data = response.read()
print(response.status, response.reason, response_data)

@ -1,8 +1,6 @@
from Crypto.Cipher import AES
import base64
def decrypt_aes_gcm(encrypted_base64, key_base64):
# Decode the base64 encoded data and key
encrypted_data = base64.b64decode(encrypted_base64)

@ -4,6 +4,8 @@ from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.urls import reverse
from tournaments.models.device_token import DeviceToken
# from tournaments.models import group_stage
from .models import Court, DateInterval, Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall
from .models import TeamSummon
@ -18,6 +20,7 @@ import json
from api.tokens import account_activation_token
from qr_code.qrcode.utils import QRCodeOptions
from .utils.apns import send_push_notification
def index(request):
@ -281,3 +284,8 @@ def club_broadcast(request, broadcast_code):
def download(request):
return render(request, 'tournaments/download.html')
def test_apns(request):
token = DeviceToken.objects.first()
send_push_notification(token.value, 'hello!')
return HttpResponse('OK!')

Loading…
Cancel
Save