2024-02-16 23:56:15 +00:00
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
from typing import Dict, List, Tuple, Union
|
|
|
|
|
|
|
|
import strawberry
|
|
|
|
from sqlalchemy import and_, select
|
|
|
|
from sqlalchemy.orm import aliased
|
2024-01-23 13:40:53 +00:00
|
|
|
from sqlalchemy.sql import not_
|
2024-02-16 23:56:15 +00:00
|
|
|
|
2024-01-26 00:40:49 +00:00
|
|
|
from orm.notification import (
|
|
|
|
Notification,
|
|
|
|
NotificationAction,
|
|
|
|
NotificationEntity,
|
|
|
|
NotificationSeen,
|
|
|
|
)
|
2023-12-22 09:09:03 +00:00
|
|
|
from resolvers.model import (
|
2024-02-16 23:56:15 +00:00
|
|
|
NotificationAuthor,
|
2023-12-22 09:09:03 +00:00
|
|
|
NotificationGroup,
|
2024-02-16 23:56:15 +00:00
|
|
|
NotificationReaction,
|
2023-12-22 09:09:03 +00:00
|
|
|
NotificationShout,
|
|
|
|
NotificationsResult,
|
|
|
|
)
|
2024-02-16 23:56:15 +00:00
|
|
|
from services.db import local_session
|
|
|
|
|
2024-01-22 19:05:19 +00:00
|
|
|
|
2024-02-18 07:47:07 +00:00
|
|
|
logger = logging.getLogger("[resolvers.schema]")
|
2024-01-22 19:05:19 +00:00
|
|
|
logger.setLevel(logging.DEBUG)
|
2023-12-22 09:09:03 +00:00
|
|
|
|
|
|
|
|
2024-02-18 07:47:07 +00:00
|
|
|
def query_notifications(
|
|
|
|
author_id: int, after: int = 0
|
|
|
|
) -> Tuple[int, int, List[Tuple[Notification, bool]]]:
|
2024-02-16 23:56:15 +00:00
|
|
|
notification_seen_alias = aliased(NotificationSeen)
|
2024-02-18 07:47:07 +00:00
|
|
|
query = select(
|
|
|
|
Notification, notification_seen_alias.viewer.label("seen")
|
|
|
|
).outerjoin(
|
2023-12-22 09:09:03 +00:00
|
|
|
NotificationSeen,
|
2024-01-26 00:40:49 +00:00
|
|
|
and_(
|
|
|
|
NotificationSeen.viewer == author_id,
|
|
|
|
NotificationSeen.notification == Notification.id,
|
|
|
|
),
|
2023-12-22 09:09:03 +00:00
|
|
|
)
|
|
|
|
if after:
|
2023-12-22 14:52:55 +00:00
|
|
|
query = query.filter(Notification.created_at > after)
|
2024-01-23 18:39:20 +00:00
|
|
|
query = query.group_by(NotificationSeen.notification, Notification.created_at)
|
2023-12-22 09:09:03 +00:00
|
|
|
|
|
|
|
with local_session() as session:
|
2024-02-04 04:58:44 +00:00
|
|
|
total = (
|
|
|
|
session.query(Notification)
|
2024-01-26 00:40:49 +00:00
|
|
|
.filter(
|
|
|
|
and_(
|
|
|
|
Notification.action == NotificationAction.CREATE.value,
|
|
|
|
Notification.created_at > after,
|
|
|
|
)
|
2024-01-23 09:15:10 +00:00
|
|
|
)
|
2024-02-04 04:58:44 +00:00
|
|
|
.count()
|
|
|
|
)
|
2024-02-16 23:56:15 +00:00
|
|
|
|
2024-02-04 04:58:44 +00:00
|
|
|
unread = (
|
|
|
|
session.query(Notification)
|
|
|
|
.filter(
|
|
|
|
and_(
|
|
|
|
Notification.action == NotificationAction.CREATE.value,
|
|
|
|
Notification.created_at > after,
|
|
|
|
not_(Notification.seen),
|
|
|
|
)
|
2024-01-23 09:15:10 +00:00
|
|
|
)
|
2024-02-04 04:58:44 +00:00
|
|
|
.count()
|
|
|
|
)
|
2024-02-16 23:56:15 +00:00
|
|
|
|
2023-12-22 13:42:37 +00:00
|
|
|
notifications_result = session.execute(query)
|
2024-02-16 23:56:15 +00:00
|
|
|
notifications = []
|
2023-12-22 13:42:37 +00:00
|
|
|
for n, seen in notifications_result:
|
2024-02-16 23:56:15 +00:00
|
|
|
notifications.append((n, seen))
|
|
|
|
|
|
|
|
return total, unread, notifications
|
|
|
|
|
|
|
|
|
|
|
|
def process_shout_notification(
|
|
|
|
notification: Notification, seen: bool
|
|
|
|
) -> Union[Tuple[str, NotificationGroup], None] | None:
|
2024-02-18 07:47:07 +00:00
|
|
|
if not isinstance(notification.payload, str) or not isinstance(
|
|
|
|
notification.entity, str
|
|
|
|
):
|
2024-02-16 23:56:15 +00:00
|
|
|
return
|
|
|
|
payload = json.loads(notification.payload)
|
|
|
|
shout: NotificationShout = payload
|
|
|
|
thread_id = str(shout.id)
|
|
|
|
group = NotificationGroup(
|
|
|
|
id=thread_id,
|
|
|
|
entity=notification.entity,
|
|
|
|
shout=shout,
|
|
|
|
authors=shout.authors,
|
|
|
|
updated_at=shout.created_at,
|
|
|
|
reactions=[],
|
2024-02-18 07:47:07 +00:00
|
|
|
action="create",
|
2024-02-16 23:56:15 +00:00
|
|
|
seen=seen,
|
|
|
|
)
|
|
|
|
return thread_id, group
|
|
|
|
|
|
|
|
|
|
|
|
def process_reaction_notification(
|
|
|
|
notification: Notification, seen: bool
|
|
|
|
) -> Union[Tuple[str, NotificationGroup], None] | None:
|
|
|
|
if (
|
|
|
|
not isinstance(notification, Notification)
|
|
|
|
or not isinstance(notification.payload, str)
|
|
|
|
or not isinstance(notification.entity, str)
|
|
|
|
):
|
|
|
|
return
|
|
|
|
payload = json.loads(notification.payload)
|
|
|
|
reaction: NotificationReaction = payload
|
|
|
|
shout: NotificationShout = reaction.shout
|
|
|
|
thread_id = str(reaction.shout)
|
2024-02-18 07:47:07 +00:00
|
|
|
if reaction.kind == "COMMENT" and reaction.reply_to:
|
|
|
|
thread_id += f"::{reaction.reply_to}"
|
2024-02-16 23:56:15 +00:00
|
|
|
group = NotificationGroup(
|
|
|
|
id=thread_id,
|
|
|
|
action=str(notification.action),
|
|
|
|
entity=notification.entity,
|
|
|
|
updated_at=reaction.created_at,
|
|
|
|
reactions=[reaction.id],
|
|
|
|
shout=shout,
|
|
|
|
authors=[reaction.created_by],
|
|
|
|
seen=seen,
|
|
|
|
)
|
|
|
|
return thread_id, group
|
|
|
|
|
|
|
|
|
|
|
|
def process_follower_notification(
|
|
|
|
notification: Notification, seen: bool
|
|
|
|
) -> Union[Tuple[str, NotificationGroup], None] | None:
|
|
|
|
if not isinstance(notification.payload, str):
|
|
|
|
return
|
|
|
|
payload = json.loads(notification.payload)
|
|
|
|
follower: NotificationAuthor = payload
|
2024-02-18 07:47:07 +00:00
|
|
|
thread_id = "followers"
|
2024-02-16 23:56:15 +00:00
|
|
|
group = NotificationGroup(
|
|
|
|
id=thread_id,
|
|
|
|
authors=[follower],
|
|
|
|
updated_at=int(time.time()),
|
|
|
|
shout=None,
|
|
|
|
reactions=[],
|
2024-02-18 07:47:07 +00:00
|
|
|
entity="follower",
|
|
|
|
action="follow",
|
2024-02-16 23:56:15 +00:00
|
|
|
seen=seen,
|
|
|
|
)
|
|
|
|
return thread_id, group
|
|
|
|
|
|
|
|
|
|
|
|
async def get_notifications_grouped(
|
|
|
|
author_id: int, after: int = 0, limit: int = 10
|
|
|
|
) -> Tuple[Dict[str, NotificationGroup], int, int]:
|
2024-02-18 07:47:07 +00:00
|
|
|
"""
|
|
|
|
Retrieves notifications for a given author.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
author_id (int): The ID of the author for whom notifications are retrieved.
|
|
|
|
after (int, optional): If provided, selects only notifications created after this timestamp will be considered.
|
|
|
|
limit (int, optional): The maximum number of groupa to retrieve.
|
|
|
|
offset (int, optional): Offset for pagination
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Dict[str, NotificationGroup], int, int: A dictionary where keys are thread IDs and values are NotificationGroup objects, unread and total amounts.
|
|
|
|
|
|
|
|
This function queries the database to retrieve notifications for the specified author, considering optional filters.
|
|
|
|
The result is a dictionary where each key is a thread ID, and the corresponding value is a NotificationGroup
|
|
|
|
containing information about the notifications within that thread.
|
|
|
|
|
|
|
|
NotificationGroup structure:
|
|
|
|
{
|
|
|
|
entity: str, # Type of entity (e.g., 'reaction', 'shout', 'follower').
|
|
|
|
updated_at: int, # Timestamp of the latest update in the thread.
|
|
|
|
shout: Optional[NotificationShout]
|
|
|
|
reactions: List[int], # List of reaction ids within the thread.
|
|
|
|
authors: List[NotificationAuthor], # List of authors involved in the thread.
|
|
|
|
}
|
|
|
|
"""
|
2024-02-16 23:56:15 +00:00
|
|
|
total, unread, notifications = query_notifications(author_id, after)
|
|
|
|
groups_by_thread: Dict[str, NotificationGroup] = {}
|
|
|
|
groups_amount = 0
|
|
|
|
|
|
|
|
for notification, seen in notifications:
|
|
|
|
if groups_amount >= limit:
|
|
|
|
break
|
|
|
|
|
2024-02-18 07:47:07 +00:00
|
|
|
if str(notification.entity) == "shout" and str(notification.action) == "create":
|
2024-02-16 23:56:15 +00:00
|
|
|
result = process_shout_notification(notification, seen)
|
|
|
|
if result:
|
|
|
|
thread_id, group = result
|
2023-12-22 14:52:55 +00:00
|
|
|
groups_by_thread[thread_id] = group
|
|
|
|
groups_amount += 1
|
2024-02-16 23:56:15 +00:00
|
|
|
|
|
|
|
elif (
|
|
|
|
str(notification.entity) == NotificationEntity.REACTION.value
|
|
|
|
and str(notification.action) == NotificationAction.CREATE.value
|
|
|
|
):
|
|
|
|
result = process_reaction_notification(notification, seen)
|
|
|
|
if result:
|
|
|
|
thread_id, group = result
|
|
|
|
existing_group = groups_by_thread.get(thread_id)
|
|
|
|
if existing_group:
|
|
|
|
existing_group.seen = False
|
|
|
|
existing_group.shout = group.shout
|
|
|
|
existing_group.authors.append(group.authors[0])
|
|
|
|
if not existing_group.reactions:
|
|
|
|
existing_group.reactions = []
|
|
|
|
existing_group.reactions.extend(group.reactions or [])
|
|
|
|
groups_by_thread[thread_id] = existing_group
|
|
|
|
else:
|
|
|
|
groups_by_thread[thread_id] = group
|
|
|
|
groups_amount += 1
|
|
|
|
|
2024-02-18 07:47:07 +00:00
|
|
|
elif str(notification.entity) == "follower":
|
2024-02-16 23:56:15 +00:00
|
|
|
result = process_follower_notification(notification, seen)
|
|
|
|
if result:
|
|
|
|
thread_id, group = result
|
2023-12-22 14:52:55 +00:00
|
|
|
groups_by_thread[thread_id] = group
|
|
|
|
groups_amount += 1
|
2023-12-22 09:09:03 +00:00
|
|
|
|
2024-02-16 23:56:15 +00:00
|
|
|
return groups_by_thread, unread, total
|
2023-12-22 09:09:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
@strawberry.type
|
|
|
|
class Query:
|
|
|
|
@strawberry.field
|
2024-02-18 07:47:07 +00:00
|
|
|
async def load_notifications(
|
|
|
|
self, info, after: int, limit: int = 50, offset: int = 0
|
|
|
|
) -> NotificationsResult:
|
|
|
|
author_id = info.context.get("author_id")
|
2023-12-22 09:09:03 +00:00
|
|
|
if author_id:
|
2024-02-18 07:47:07 +00:00
|
|
|
groups, unread, total = await get_notifications_grouped(
|
|
|
|
author_id, after, limit
|
|
|
|
)
|
|
|
|
notifications = sorted(
|
|
|
|
groups.values(), key=lambda group: group.updated_at, reverse=True
|
|
|
|
)
|
|
|
|
return NotificationsResult(
|
|
|
|
notifications=notifications, total=total, unread=unread, error=None
|
|
|
|
)
|
2024-02-16 23:56:15 +00:00
|
|
|
return NotificationsResult(notifications=[], total=0, unread=0, error=None)
|