From fc91e716ed86a6f269159799fa07800f227139ed Mon Sep 17 00:00:00 2001 From: mortezaei Date: Mon, 12 May 2025 16:09:20 +0330 Subject: [PATCH] fix: notif --- apps/account/serializers/user.py | 5 ++ apps/account/tasks.py | 101 ++++++++++++++++++++++++++++- apps/account/urls.py | 4 +- apps/account/views/notification.py | 56 +++++++++++++++- apps/account/views/user.py | 19 +++++- requirements.txt | 2 + 6 files changed, 181 insertions(+), 6 deletions(-) diff --git a/apps/account/serializers/user.py b/apps/account/serializers/user.py index 91d2954..e6fe2ef 100644 --- a/apps/account/serializers/user.py +++ b/apps/account/serializers/user.py @@ -141,3 +141,8 @@ class UserGuestSerializer(serializers.ModelSerializer): return data + +class UserFCMSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['fcm'] diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 40217de..1ea57ed 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -1,9 +1,108 @@ import time from config.settings import base as settings - from celery import shared_task import requests import json +import logging +import firebase_admin +from firebase_admin import credentials, messaging +import firebase_admin +from firebase_admin import credentials, messaging +from google.oauth2 import service_account +import google.auth.transport.requests + + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# تنظیمات Firebase +data = { + "type": "service_account", + "project_id": "imamjavad-25c31", + "private_key_id": "1edc90fb80a335809c4b04a713403355ff4e8bd0", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCM57lia6vNNzJL\nYBwFcx49sFPXtrYKkrRhtDN13EOnF2j+y8vlwtqYR6P7HB1l10GyHx3mlN8XpYXN\n24yrTsK6WugqPdWGl/z/BgqHximH+v4NCPQTBr3lemHbTXlEkhNtmaf9zM8IzsXP\nEzD4z5u9hy3AgfZdHKO0isTvxRTuTlpTKU3PQDwiIjfb6Bk8IjfjrDQqWbHLC4am\nidwM5F+L8ecAyhVe/G7IXAflqyi4zVM/hM5FiAknA5FmyfGd9HCxaLkw3Dqtnof8\nqflJmZp0vptT3Lte7ObeUEMoRoT0bZt6DbMBI+w19OIBu0ne0OjLu/1z4CFYoR+r\nAeWGvWCHAgMBAAECgf9HnQx/FY90oO5gtiLdE/pnJxqSMtjEhufRazaDd4vOYKXD\nhLQ5EkFcsij66PnPHiZiC+BfbUpnSIAqrmsliXBSYv4OCELTJU/FovcMfHG7qtU7\nIBjsrw64ISXT+ow1+EEEAWm1eA0WwjmOBTL7CTPJA3l2QXrYu5ki8IDuP1i5UwKu\nSR3kW0+BfsQG0z2q00AjqGnFV9IuDDjcAvu2ojwanM/H+eGB+I/dtpqe87KhbBZ9\nFuKCdYNgRa3Z76mU/2jSyGQ9eyXCX0x0vKpPavkbfir7mJcvCrp+3z0h8ot0u1Mi\nj7IJd9Ot37qUj09obXyInYk8Vnj46lj8+QjdgAECgYEAxj9Fmu9oSgLBLsuYU0kU\nmUcl0HOv3UllKAYX+8Z6L/dR1KKsfSoRWQoyGE1TxXsR/uJ4uQJJZLlHMVSw3mz3\nmOHep3F5TNSM6cfJnh5/NSMoAklOzRZxW/UELcu9vaR+e1QSgBMaNmc3b483pbfs\neVD3CPPWFt2A4lI3Y77jQAECgYEAtfQLcrBYv+SEIrVML6pXrHRC19RwCzmLyC69\n07LyRG2THu26IhOK+aSzLT5FRXTOP1VD+FHfD+AOr2d1oc2HrmgxU0mVio93KSW4\nxDrmBrej1DmVjB7LSqxu7chiD/lBUdFh2Fam8dsiTQtqR02qfcQGLynvEb2yTUbj\n0lTmoIcCgYBWZ7VatgXqXBD+6FXX1v5XYB8nH4UDGb4xF5bUcclHpq/P0acEVpWB\nDWSQGwPsCpvpT6P2XvzGHcrdwV/lUfEIfUmiCV8pEWrpad6CQCCJdG03sePal3GI\n9t1/aFGmmk9WSWpWz/yYwZvzz6QdYnB638ML79rb1GccPWFO5CAAAQKBgA4+Hi9K\nEohi0N0Op/oLMXW0XA8c9/BI/uIalo1dso0crql7HljQgs5r0AK4nx+CtypJ+FoV\nvoo1lbCxPon91qMWUNYeKnCALmmwJDhoC912voI8R7KCLpOXz88ZImPxtOU8qJYQ\nolzINHUncZhHQhM6JunGNIqE+NIHvImYT709AoGALJGUb9jAg/QpSoFKlbp4xrEA\n3G/caXeB+lE19KGZxgADBbWsUsfMI7CxnROZFobCzTdhIE6N+LaAFX/6rn0P6Nf9\nN6w8//442RjkWxtmDgw7lCykXwyLSfrP3Dbzd78gGIBqngPTej9JCc7WJYnnN75M\n5TGjxvmxYqR231/L/p0=\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-fbsvc@imamjavad-25c31.iam.gserviceaccount.com", + "client_id": "103207313184637638669", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40imamjavad-25c31.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} + +PROJECT_ID = 'imamjavad-25c31' +BASE_URL = 'https://fcm.googleapis.com' +SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'] +FCM_ENDPOINT = f'v1/projects/{PROJECT_ID}/messages:send' +FCM_URL = f'{BASE_URL}/{FCM_ENDPOINT}' + +def _get_access_token(): + """Retrieve a valid access token that can be used to authorize requests. + + :return: Access token. + """ + credentials = service_account.Credentials.from_service_account_info( + data, scopes=SCOPES) + request = google.auth.transport.requests.Request() + credentials.refresh(request) + return credentials.token + + +# @shared_task +async def send_notification(ids: list, title: str = None, body: str = None, data=None, + extra_notification_kwargs: dict = None) -> list: + if not ids: + return [] + + chunked_ids = [ids[i:i + 500] for i in range(0, len(ids), 500)] + + responses = [] + for chunk in chunked_ids: + + + access_token = _get_access_token() + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json', + } + payload = { + 'message': { + 'token': chunk[0], + 'notification': { + 'title': title, + 'body': body, + }, + 'data': {k: str(v) for k, v in (data or {}).items()}, + 'android': { + 'priority': 'high', + 'notification': { + 'title': title, + 'body': body, + # 'sound': 'incoming_call_sound', + 'color': '#06EEBD', + # 'channel_id': 'incoming_call_channel', + 'visibility': 'public', + }, + }, + } + } + # Send the POST request to FCM API + print(f'=========(send-notif)===******') + response = requests.post(FCM_URL, headers=headers, json=payload) + if response.status_code == 200: + logger.warning('Successfully sent message:', response.json()) + responses.append(response.json()) + else: + responses.append({'status': 'error', 'message': ""}) + logger.error(f'Failed to send message notif') + + return responses + + @shared_task def send_otp_code(phone_number, code): diff --git a/apps/account/urls.py b/apps/account/urls.py index bcddf1e..adbdbec 100644 --- a/apps/account/urls.py +++ b/apps/account/urls.py @@ -17,8 +17,6 @@ urlpatterns = [ path('location-update/', views.LocationHistoryView.as_view(), name='user-location-history'), - # path('notif/', views.NotificationListView.as_view(), name='user-notif'), - # path('notif/read', views.NotificationReadAllView.as_view(), name='user-notif-read-all'), # # URL to get user details, supports GET for fetching user profile based on the provided token. @@ -29,11 +27,13 @@ urlpatterns = [ path('notif/', views.NotificationListView.as_view(), name='user-notif'), path('notif/read', views.NotificationReadAllView.as_view(), name='user-notif-read-all'), + path('notif/send/', views.SendNotificationView.as_view(), name='user-send-notif'), # # URL to update user details, supports PUT to update user fields like phone or email given a token. path('profile/update/', views.UserUpdateView.as_view(), name='user-update'), # # delete user account path('profile/delete/', views.UserDeleteView.as_view(), name='user-delete'), + path('update-fcm/', views.UpdateFCMView.as_view(), name='update-fcm'), ] \ No newline at end of file diff --git a/apps/account/views/notification.py b/apps/account/views/notification.py index d370773..6308fc0 100644 --- a/apps/account/views/notification.py +++ b/apps/account/views/notification.py @@ -5,8 +5,8 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from rest_framework.permissions import IsAuthenticated from apps.account.serializers import NotificationSerializer, NotificationSendSerializer -from apps.account.models import Notification -# from apps.account.fcm_notification import send_notification +from apps.account.models import Notification, User +from apps.account.tasks import send_notification @@ -103,3 +103,55 @@ class NotificationReadAllView(generics.GenericAPIView): +class SendNotificationView(generics.GenericAPIView): + + @swagger_auto_schema( + operation_description="Send a notification to a user by user_id.", + tags=['Notifications'], + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['user_id', 'title', 'body'], + properties={ + 'user_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID of the user to send notification to'), + 'title': openapi.Schema(type=openapi.TYPE_STRING, description='Notification title'), + 'body': openapi.Schema(type=openapi.TYPE_STRING, description='Notification body'), + 'data': openapi.Schema(type=openapi.TYPE_OBJECT, description='Additional data payload', default={'slam': 'qatreh'}), + }, + ), + responses={ + 200: openapi.Response('Notification sent successfully.'), + 400: openapi.Response('FCM token not available for this user.'), + 404: openapi.Response('User not found.'), + 500: openapi.Response('Internal server error.'), + } + ) + def post(self, request, *args, **kwargs): + + user_id = request.data.get('user_id', 1) + + try: + user = User.objects.get(id=user_id) + except User.DoesNotExist: + return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND) + + notification_title = request.data.get('title', 'test qatreh') + notification_body = request.data.get('body', 'test qatreh body') + data_payload = request.data.get('data', {'slam':'qatreh'}) + + fcm_token = user.fcm # Ensure that 'fcm' is a field in your User model + + if not fcm_token: + return Response({ + 'error': 'FCM token not available for this user.' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + send_notification([fcm_token], notification_title, notification_body, data_payload) + return Response({ + 'message': 'Notification sent successfully.' + }, status=status.HTTP_200_OK) + except Exception as e: + return Response({ + 'error': str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + diff --git a/apps/account/views/user.py b/apps/account/views/user.py index 7609f3a..89af525 100644 --- a/apps/account/views/user.py +++ b/apps/account/views/user.py @@ -23,7 +23,7 @@ from rest_framework.exceptions import ValidationError from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException from apps.account.models import User -from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer +from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer,UserFCMSerializer from utils.redis import RedisManager from utils.exceptions import AppAPIException from utils import send_email, is_valid_email @@ -370,3 +370,20 @@ class UserDeleteView(APIView): return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND) +class UpdateFCMView(GenericAPIView): + permission_classes = [IsAuthenticated] + serializer_class = UserFCMSerializer + + def post(self, request, *args, **kwargs): + user = request.user + + fcm_token = request.data.get('fcm') + + if not fcm_token: + return Response({"detail": "FCM token is required."}, status=status.HTTP_200_OK) + + user.fcm = fcm_token + user.save() + + return Response({"detail": "FCM token updated successfully."}, status=status.HTTP_200_OK) + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5e173f6..bf0e30f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -117,6 +117,8 @@ webencodings==0.5.1 whitenoise==6.9.0 wrapt==1.16.0 wsproto==1.2.0 +firebase-admin==6.2.0 +google-auth==2.6.0 https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/django-limitless-dashboard.git/archive/master.zip https://yaghoubi:e07059e0ac6be3b0032ded5f65f03363fbd3811f@git.habibapp.com/NewHorizon/ajax-datatable.git/archive/master.zip