From 3f00b350eb538d540d1199904d420d34cac81108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=9A=A8=EC=A7=84?= Date: Sun, 28 Apr 2024 09:49:36 +0900 Subject: [PATCH] notification_infra --- ara/controller/api2.py | 0 .../notification/notification_controller.py | 42 +++++++++++ ara/domain/notification/__init__.py | 0 ara/domain/notification/constants.py | 6 ++ .../notification/notification_domain.py | 12 ++++ ara/domain/notification/type.py | 11 +++ ara/infra/notification/__init__.py | 0 ara/infra/notification/notification_infra.py | 56 +++++++++++++++ .../notification/notification_service.py | 69 +++++++++++++++++++ 9 files changed, 196 insertions(+) create mode 100644 ara/controller/api2.py create mode 100644 ara/controller/notification/notification_controller.py create mode 100644 ara/domain/notification/__init__.py create mode 100644 ara/domain/notification/constants.py create mode 100644 ara/domain/notification/notification_domain.py create mode 100644 ara/domain/notification/type.py create mode 100644 ara/infra/notification/__init__.py create mode 100644 ara/infra/notification/notification_infra.py create mode 100644 ara/service/notification/notification_service.py diff --git a/ara/controller/api2.py b/ara/controller/api2.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/controller/notification/notification_controller.py b/ara/controller/notification/notification_controller.py new file mode 100644 index 00000000..decc6549 --- /dev/null +++ b/ara/controller/notification/notification_controller.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, Depends, HTTPException +from ara.controller.authentication import get_current_user +from ara.service.notification.notification_service import NotificationService +from ara.domain.user import User +from ara.domain.exceptions import EntityDoesNotExist +from pydantic import BaseModel + +router = APIRouter() +notification_service = NotificationService() + +class NotificationRead(BaseModel): + notification_id: int + +@router.get("/notifications") +async def list_notifications(current_user: User = Depends(get_current_user)): + notifications = notification_service.get_notifications_for_user(current_user) + return notifications + +@router.post("/notifications/{notification_id}/read") +async def mark_notification_as_read(notification_id: int, current_user: User = Depends(get_current_user)): + try: + notification_service.mark_notification_as_read(notification_id, current_user) + except EntityDoesNotExist: + raise HTTPException(status_code=404, detail="Notification not found") + except PermissionError: + raise HTTPException(status_code=403, detail="You are not allowed to mark this notification as read") + return {"message": "Notification marked as read successfully"} + +@router.post("/notifications/read-all") +async def mark_all_notifications_as_read(current_user: User = Depends(get_current_user)): + notification_service.mark_all_notifications_as_read(current_user) + return {"message": "All notifications marked as read successfully"} + +@router.post("/notifications/send-push-notification") +async def send_push_notification(notification: NotificationRead, current_user: User = Depends(get_current_user)): + try: + notification_service.send_push_notification(notification.notification_id, current_user) + except EntityDoesNotExist: + raise HTTPException(status_code=404, detail="Notification not found") + except PermissionError: + raise HTTPException(status_code=403, detail="You are not allowed to send push notification for this notification") + return {"message": "Push notification sent successfully"} diff --git a/ara/domain/notification/__init__.py b/ara/domain/notification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/domain/notification/constants.py b/ara/domain/notification/constants.py new file mode 100644 index 00000000..63cd4a4c --- /dev/null +++ b/ara/domain/notification/constants.py @@ -0,0 +1,6 @@ +from enum import IntFlag, auto + +class NameType(IntFlag): + REGULAR = auto() + ANONYMOUS = auto() + REALNAME = auto() \ No newline at end of file diff --git a/ara/domain/notification/notification_domain.py b/ara/domain/notification/notification_domain.py new file mode 100644 index 00000000..f02712e4 --- /dev/null +++ b/ara/domain/notification/notification_domain.py @@ -0,0 +1,12 @@ +from ara.domain.notification.type import NotificationInfo +from ara.infra.notification.notification_infra import NotificationInfra + +class NotificationDomain: + def __init__(self) -> None: + self.notification_infra = NotificationInfra() + + def get_all_notifications(self) -> list[NotificationInfo]: + return self.notification_infra.get_all_notifications() + + class Config: + orm_mode = True \ No newline at end of file diff --git a/ara/domain/notification/type.py b/ara/domain/notification/type.py new file mode 100644 index 00000000..1cf9c1b3 --- /dev/null +++ b/ara/domain/notification/type.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel +from typing import Optional + +class NotificationInfo(BaseModel): + id: int + type: str + title: str + content: str + related_article_id: int | None + related_comment_id: Optional[int] + is_read: bool diff --git a/ara/infra/notification/__init__.py b/ara/infra/notification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/infra/notification/notification_infra.py b/ara/infra/notification/notification_infra.py new file mode 100644 index 00000000..409e8359 --- /dev/null +++ b/ara/infra/notification/notification_infra.py @@ -0,0 +1,56 @@ +from typing import List +import logging +from ara.domain.notification.type import NotificationInfo +from ara.infra.django_infra import AraDjangoInfra +from apps.core.models import Notification, NotificationReadLog + + +class NotificationInfra(AraDjangoInfra[Notification]): + def __init__(self) -> None: + super().__init__(Notification) + + def get_all_notifications(self) -> list[NotificationInfo]: + + queryset = Notification.objects.filter( + notification_read_log_set__read_by=self.request.user, + ).select_related( + "related_article", + "related_comment", + ).prefetch_related( + "related_article__attachments", + NotificationReadLog.prefetch_my_notification_read_log( + self.request.user + ), + ) + + notifications_info = [self._to_notification_info(notification) for notification in queryset] + return notifications_info + + + def _to_notification_info(self, notification: Notification) -> NotificationInfo: + return NotificationInfo( + id=notification.id, + type=notification.type, + title=notification.title, + content=notification.content, + related_article_id=notification.related_article_id, + related_comment_id=notification.related_comment_id, + is_read=False + ) + + def read_all_notifications(self) -> None: + notifications = self.get_all_notifications() + NotificationReadLog.objects.filter(notification__in=notifications, read_by=self.request.user).update(is_read=True) + + def read_notification(self) -> None: + try: + notification_read_log = self.get_object().notification_read_log_set.get( + read_by=self.request.user, + ) + + notification_read_log.is_read = True + + notification_read_log.save() + except (Notification.DoesNotExist, NotificationReadLog.DoesNotExist) as e: + logging.error(f"Failed to read notification: {e}") + \ No newline at end of file diff --git a/ara/service/notification/notification_service.py b/ara/service/notification/notification_service.py new file mode 100644 index 00000000..9c6bcd4d --- /dev/null +++ b/ara/service/notification/notification_service.py @@ -0,0 +1,69 @@ +from fastapi import HTTPException +from ara.infra.notification.notification_infra import NotificationRepository +from ara.domain.notification.notification_domain import Notification +from ara.domain.exceptions import EntityDoesNotExist +from ara.domain.article import Article, NameType +from ara.domain.user_profile import UserProfile +from ara.domain.comment import Comment + +class NotificationService: + def __init__(self, notification_repo: NotificationRepository): + self.notification_repo = notification_repo + + def get_display_name(self, article: Article, profile: UserProfile) -> str: + + if article.name_type == NameType.REALNAME: + return profile.realname + elif article.name_type == NameType.REGULAR: + return profile.nickname + else: + return "익명" + + async def notify_commented(self, comment: Comment) -> None: + + article = comment.parent_article if comment.parent_article else comment.parent_comment.parent_article + + if comment.created_by != article.created_by: + await self._notify_article_commented(article, comment) + + if comment.parent_comment and comment.created_by != comment.parent_comment.created_by: + await self._notify_comment_commented(article, comment) + + async def _notify_article_commented(self, parent_article: Article, comment: Comment) -> None: + + name = self.get_display_name(parent_article, comment.created_by.profile) + title = f"{name} 님이 새로운 댓글을 작성했습니다." + + notification = Notification( + id=None, + type="article_commented", + title=title, + content=comment.content[:32], + related_article=parent_article, + related_comment=None + ) + await self.notification_repo.save(notification) + + # Send push notification + await fcm_notify_comment(parent_article.created_by, title, comment.content[:32], f"post/{parent_article.id}") + + async def _notify_comment_commented(self, parent_article: Article, comment: Comment) -> None: + """ + Notifies the user when a comment is added to their comment. + """ + name = self.get_display_name(parent_article, comment.created_by.profile) + title = f"{name} 님이 새로운 대댓글을 작성했습니다." + + # Save the notification + notification = Notification( + id=None, # Since it's a new notification, let the database generate the ID + type="comment_commented", + title=title, + content=comment.content[:32], # Truncate content if necessary + related_article=parent_article, + related_comment=comment.parent_comment + ) + await self.notification_repo.save(notification) + + # Send push notification + await fcm_notify_comment(comment.parent_comment.created_by, title, comment.content[:32], f"post/{parent_article.id}")