diff --git a/apps/notifications/admin.py b/apps/notifications/admin.py deleted file mode 100644 index 488f9e4..0000000 --- a/apps/notifications/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin -from .models import PushUser - - -admin.site.register(PushUser) diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py deleted file mode 100644 index a914bc0..0000000 --- a/apps/notifications/migrations/0001_initial.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.2.3 on 2025-07-01 08:37 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='PushUser', - fields=[ - ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), - ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), - ('client_token', models.CharField(blank=True, max_length=32, null=True)), - ('application_token', models.CharField(blank=True, max_length=32, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/apps/notifications/serializers.py b/apps/notifications/serializers.py deleted file mode 100644 index 374e4d2..0000000 --- a/apps/notifications/serializers.py +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework import serializers - -from .models import PushUser - - -class PushSerializer(serializers.ModelSerializer): - class Meta: - model = PushUser - fields = ( - "user", - "client_token", - "application_token" - ) diff --git a/apps/notifications/views.py b/apps/notifications/views.py deleted file mode 100644 index 4896414..0000000 --- a/apps/notifications/views.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.shortcuts import render - -from django.http import Http404 -from django.shortcuts import render -from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements -from rest_framework import viewsets, mixins -from rest_framework.generics import get_object_or_404 -from rest_framework.viewsets import GenericViewSet - -from .models import PushUser -from .serializers import PushSerializer -from apps.gooyal_oauth2.models import Application -from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements -from apps.gooyal_oauth2.utils import get_application - - -class PushUserViewSet(mixins.RetrieveModelMixin, - # mixins.UpdateModelMixin, - # mixins.DestroyModelMixin, - GenericViewSet): - queryset = PushUser.objects.all() - serializer_class = PushSerializer - - permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] - required_alternate_scopes = { - "GET": [["notifications.push:get_client_token"]], - } - - def get_object(self): - user = self.request.user - auth = self.request.auth - if user: - # plan = Plan.objects.filter(application_id=auth.application_id).first() - push_user = PushUser.objects.submit(user) - return push_user - - raise Http404 - diff --git a/apps/notifications/__init__.py b/apps/push_notifications/__init__.py similarity index 100% rename from apps/notifications/__init__.py rename to apps/push_notifications/__init__.py diff --git a/apps/push_notifications/admin.py b/apps/push_notifications/admin.py new file mode 100644 index 0000000..39e55c2 --- /dev/null +++ b/apps/push_notifications/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import PushUser, PushMessage + + +admin.site.register(PushUser) +admin.site.register(PushMessage) diff --git a/apps/push_notifications/application_urls.py b/apps/push_notifications/application_urls.py new file mode 100644 index 0000000..8293d59 --- /dev/null +++ b/apps/push_notifications/application_urls.py @@ -0,0 +1,59 @@ +from rest_framework import routers +from rest_framework.routers import DefaultRouter, DynamicRoute, Route +from . import views +from django.urls import NoReverseMatch, path, re_path + + +app_name = 'push_notifications' + + +class ApplicationRouter(DefaultRouter): + + routes = [ + # List route. + Route( + url=r'^{prefix}/{trailing_slash}$', + mapping={ + 'get': 'list', + 'post': 'create' + }, + name='{basename}-list', + detail=False, + initkwargs={'suffix': 'List'} + ), + # Dynamically generated list routes. Generated using + # @action(detail=False) decorator on methods of the viewset. + DynamicRoute( + url=r'^{prefix}//{url_path}{trailing_slash}$', + name='{basename}-{url_name}', + detail=False, + initkwargs={} + ), + # # Detail route. + Route( + url=r'^{prefix}//{lookup}{trailing_slash}$', + mapping={ + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }, + name='{basename}-detail', + detail=True, + initkwargs={'suffix': 'Instance'} + ), + # # Dynamically generated detail routes. Generated using + # # @action(detail=True) decorator on methods of the viewset. + DynamicRoute( + url=r'^{prefix}//{lookup}/{url_path}{trailing_slash}$', + name='{basename}-{url_name}', + detail=True, + initkwargs={} + ), + ] + + +application_router = DefaultRouter() +application_router.register('application', views.ApplicationPushUserViewSet, basename='application') + +urlpatterns = application_router.urls diff --git a/apps/notifications/apps.py b/apps/push_notifications/apps.py similarity index 77% rename from apps/notifications/apps.py rename to apps/push_notifications/apps.py index 03a81ff..b211a87 100644 --- a/apps/notifications/apps.py +++ b/apps/push_notifications/apps.py @@ -3,4 +3,4 @@ from django.apps import AppConfig class NotificationsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.notifications' + name = 'apps.push_notifications' diff --git a/apps/notifications/migrations/__init__.py b/apps/push_notifications/migrations/__init__.py similarity index 100% rename from apps/notifications/migrations/__init__.py rename to apps/push_notifications/migrations/__init__.py diff --git a/apps/notifications/models.py b/apps/push_notifications/models.py similarity index 61% rename from apps/notifications/models.py rename to apps/push_notifications/models.py index 8ac5930..75ca4be 100644 --- a/apps/notifications/models.py +++ b/apps/push_notifications/models.py @@ -1,4 +1,5 @@ from django.db import models +from oauth2_provider.settings import oauth2_settings from apps.users.models import User from utils.models import BaseModel @@ -15,9 +16,11 @@ class PushUserQuerySet(models.QuerySet): return push_user class PushUser(BaseModel): - user = models.ForeignKey(User, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.PROTECT) client_token = models.CharField(max_length=32, null=True, blank=True) application_token = models.CharField(max_length=32, null=True, blank=True) + app_id = None + objects = PushUserQuerySet.as_manager() @@ -39,3 +42,17 @@ class PushUser(BaseModel): def send_push(self): return + +class PushMessage(BaseModel): + push_user = models.ForeignKey(PushUser, on_delete=models.PROTECT) + application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, + related_name='+', null=True, blank=True) + title = models.CharField(max_length=128) + message = models.TextField() + priority = models.IntegerField(default=5) + extras = models.JSONField(default=dict) + + def send_push(self): + gotify.send_notif(token=self.push_user.application_token ,title=self.title, message=self.message, priority=self.priority, extras=self.extras) + + diff --git a/apps/push_notifications/serializers.py b/apps/push_notifications/serializers.py new file mode 100644 index 0000000..f266f55 --- /dev/null +++ b/apps/push_notifications/serializers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from .models import PushUser, PushMessage + + +class PushUserSerializer(serializers.ModelSerializer): + class Meta: + model = PushUser + fields = ( + "user", + "client_token", + "application_token" + ) + +class PushMessageSerializer(serializers.ModelSerializer): + class Meta: + model = PushMessage + fields = ( + "push_user", + "application", + "title", + "message", + "priority", + "extras" + ) + read_only_fields = ("push_user","application") diff --git a/apps/notifications/tests.py b/apps/push_notifications/tests.py similarity index 100% rename from apps/notifications/tests.py rename to apps/push_notifications/tests.py diff --git a/apps/notifications/urls.py b/apps/push_notifications/user_urls.py similarity index 84% rename from apps/notifications/urls.py rename to apps/push_notifications/user_urls.py index 8cbac75..44aa6f1 100644 --- a/apps/notifications/urls.py +++ b/apps/push_notifications/user_urls.py @@ -1,9 +1,12 @@ from rest_framework import routers from rest_framework.routers import DefaultRouter, DynamicRoute, Route from . import views +from django.urls import NoReverseMatch, path, re_path -class CRUDRouter(DefaultRouter): +app_name = 'push_notifications' + +class PushUserRouter(DefaultRouter): routes = [ # List route. Route( @@ -47,11 +50,8 @@ class CRUDRouter(DefaultRouter): ), ] +user_router = PushUserRouter() +user_router.register('push_user', views.PushUserViewSet, basename='user-data') -router = CRUDRouter() -app_name = 'notifications' - -router.register('push_user', views.PushUserViewSet, basename='user-data') - -urlpatterns = router.urls +urlpatterns = user_router.urls diff --git a/apps/push_notifications/views.py b/apps/push_notifications/views.py new file mode 100644 index 0000000..c29063a --- /dev/null +++ b/apps/push_notifications/views.py @@ -0,0 +1,69 @@ +from django.shortcuts import render + +from django.http import Http404 +from django.shortcuts import render +from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements +from rest_framework import viewsets, mixins +from rest_framework.generics import get_object_or_404 +from rest_framework.viewsets import GenericViewSet + +from .models import PushUser, PushMessage +from .serializers import PushUserSerializer, PushMessageSerializer +from apps.gooyal_oauth2.models import Application +from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +from apps.gooyal_oauth2.utils import get_application +from ..users.models import User + + +class PushUserViewSet(mixins.RetrieveModelMixin, + # mixins.UpdateModelMixin, + # mixins.DestroyModelMixin, + GenericViewSet): + queryset = PushUser.objects.all() + serializer_class = PushUserSerializer + + permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] + required_alternate_scopes = { + "GET": [["notifications.push:get_client_token"]], + } + + def get_object(self): + user = self.request.user + auth = self.request.auth + if user: + # plan = Plan.objects.filter(application_id=auth.application_id).first() + push_user = PushUser.objects.submit(user) + return push_user + + raise Http404 + + +class ApplicationPushUserViewSet( + # mixins.RetrieveModelMixin, + mixins.ListModelMixin, + # mixins.UpdateModelMixin, + # mixins.DestroyModelMixin, + mixins.CreateModelMixin, + GenericViewSet +): + queryset = PushMessage.objects.all() + serializer_class = PushMessageSerializer + permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] + required_alternate_scopes = { + "GET": [["notifications.application.push:submit_message"]], + "POST": [["notifications.application.push:submit_message"]], + } + + def get_object(self): + queryset = self.get_queryset() + user_uuid = self.kwargs.get('user_uuid') + push_message = get_object_or_404(queryset, user=user_uuid) + return push_message + + def perform_create(self, serializer): + user_uuid = self.kwargs.get('user_uuid') + user, _ = User.objects.get_or_create(uuid=user_uuid) + push_user = PushUser.objects.submit(user) + application = get_application(self.request) + instance = serializer.save(push_user=push_user, application=application) + instance.send_push() diff --git a/main/settings.py b/main/settings.py index 473e16c..d3beecc 100644 --- a/main/settings.py +++ b/main/settings.py @@ -54,7 +54,7 @@ INSTALLED_APPS = [ 'apps.core', 'apps.users', 'apps.gooyal_oauth2', - 'apps.notifications', + 'apps.push_notifications', ] MIDDLEWARE = [ @@ -241,37 +241,37 @@ CACHES = { } } -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} AT: {asctime} LINE: {lineno} LOGGER: {name} FILE: {pathname} MESSAGE: {message}', - 'style': '{', - }, - 'simple': { - 'format': '{levelname} {message}', - 'style': '{', - }, - }, - 'handlers': { - 'file': { - 'level': 'DEBUG', - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/app.log', - 'formatter': 'verbose', - "when": 'D', - "backupCount": 5, - }, - }, - 'loggers': { - 'root': { - 'handlers': ['file'], - 'level': 'DEBUG', - 'propagate': True, - }, - }, -} +# LOGGING = { +# 'version': 1, +# 'disable_existing_loggers': False, +# 'formatters': { +# 'verbose': { +# 'format': '{levelname} AT: {asctime} LINE: {lineno} LOGGER: {name} FILE: {pathname} MESSAGE: {message}', +# 'style': '{', +# }, +# 'simple': { +# 'format': '{levelname} {message}', +# 'style': '{', +# }, +# }, +# 'handlers': { +# 'file': { +# 'level': 'DEBUG', +# 'class': 'logging.handlers.TimedRotatingFileHandler', +# 'filename': 'logs/app.log', +# 'formatter': 'verbose', +# "when": 'D', +# "backupCount": 5, +# }, +# }, +# 'loggers': { +# 'root': { +# 'handlers': ['file'], +# 'level': 'DEBUG', +# 'propagate': True, +# }, +# }, +# } # from datetime import timedelta # from typing import List, Tuple diff --git a/main/urls.py b/main/urls.py index 56f5f0e..1605331 100644 --- a/main/urls.py +++ b/main/urls.py @@ -35,7 +35,8 @@ urlpatterns = [ path('admin/', admin.site.urls), path('', include('apps.core.urls')), - path('', include('apps.notifications.urls')), + path('push/user/', include('apps.push_notifications.user_urls')), + path('push/application//', include('apps.push_notifications.application_urls')), # path('users/', include('apps.users.urls')), # path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), diff --git a/utils/clients/gotify.py b/utils/clients/gotify.py index 52ae454..0eb9e08 100644 --- a/utils/clients/gotify.py +++ b/utils/clients/gotify.py @@ -1,4 +1,5 @@ import time +from django.utils import timezone from typing import Any import httpx @@ -14,6 +15,7 @@ from gotify_rest_api_client.api.message import create_message from gotify_rest_api_client.api.user import create_user from gotify_rest_api_client.api.client import create_client from gotify_rest_api_client.api.application import create_app +from gotify_rest_api_client.types import Unset ADMIN_CLIENT_TOKEN = 'CbWdsJrmRTDzbuD' @@ -62,30 +64,19 @@ def get_basic_client(username, password): return client -def send_notif(data): - class Tmp(): - def to_dict(self): - return data - - final_data = Tmp() - client = get_client() +def send_notif(token, title=None, message=None, priority=None, extras=None): + extras = extras or Unset() + final_data = MessageExternalModel(appid=1, date=timezone.now(), id=1, title=title, message=message, priority=priority, extras=extras) + client = get_client(access_token=token) response: Any = create_message.sync(client=client, body=final_data) return response def submit_user(user_uuid): - data_dict = { - "admin": False, - "name": user_uuid, - "pass": user_uuid - } - - class Tmp(): - def to_dict(self): - return data_dict - - final_data = Tmp() + final_data = CreateUserExternalModel(admin=False, + name=user_uuid, + pass_=user_uuid) client = get_client(ADMIN_CLIENT_TOKEN) response: UserExternalModel = create_user.sync(client=client, body=final_data)