2024-03-04 07:35:33 +00:00
|
|
|
import json
|
|
|
|
import time
|
|
|
|
from typing import List, Tuple
|
|
|
|
|
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
|
2024-03-04 10:43:02 +00:00
|
|
|
from orm.author import Author
|
|
|
|
from orm.shout import Shout
|
2024-03-04 07:35:33 +00:00
|
|
|
from services.auth import login_required
|
|
|
|
from services.schema import mutation, query
|
|
|
|
from sqlalchemy import and_, select
|
|
|
|
from sqlalchemy.orm import aliased
|
|
|
|
from sqlalchemy.sql import not_
|
|
|
|
|
|
|
|
from orm.notification import (
|
|
|
|
Notification,
|
|
|
|
NotificationAction,
|
|
|
|
NotificationEntity,
|
|
|
|
NotificationSeen,
|
|
|
|
)
|
|
|
|
from services.db import local_session
|
|
|
|
from services.logger import root_logger as logger
|
|
|
|
|
|
|
|
|
2024-03-06 09:25:55 +00:00
|
|
|
def query_notifications(
|
|
|
|
author_id: int, after: int = 0
|
|
|
|
) -> Tuple[int, int, List[Tuple[Notification, bool]]]:
|
2024-03-04 07:35:33 +00:00
|
|
|
notification_seen_alias = aliased(NotificationSeen)
|
2024-03-06 09:25:55 +00:00
|
|
|
q = select(Notification, notification_seen_alias.viewer.label('seen')).outerjoin(
|
|
|
|
NotificationSeen,
|
|
|
|
and_(
|
|
|
|
NotificationSeen.viewer == author_id,
|
|
|
|
NotificationSeen.notification == Notification.id,
|
|
|
|
),
|
2024-03-04 07:35:33 +00:00
|
|
|
)
|
|
|
|
if after:
|
|
|
|
q = q.filter(Notification.created_at > after)
|
|
|
|
q = q.group_by(NotificationSeen.notification, Notification.created_at)
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
total = (
|
|
|
|
session.query(Notification)
|
|
|
|
.filter(
|
|
|
|
and_(
|
|
|
|
Notification.action == NotificationAction.CREATE.value,
|
|
|
|
Notification.created_at > after,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.count()
|
|
|
|
)
|
|
|
|
|
|
|
|
unread = (
|
|
|
|
session.query(Notification)
|
|
|
|
.filter(
|
|
|
|
and_(
|
|
|
|
Notification.action == NotificationAction.CREATE.value,
|
|
|
|
Notification.created_at > after,
|
|
|
|
not_(Notification.seen),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.count()
|
|
|
|
)
|
|
|
|
|
|
|
|
notifications_result = session.execute(q)
|
|
|
|
notifications = []
|
|
|
|
for n, seen in notifications_result:
|
|
|
|
notifications.append((n, seen))
|
|
|
|
|
|
|
|
return total, unread, notifications
|
|
|
|
|
|
|
|
|
2024-03-06 09:25:55 +00:00
|
|
|
def group_notification(
|
|
|
|
thread, authors=None, shout=None, reactions=None, entity='follower', action='follow'
|
|
|
|
):
|
2024-03-04 10:43:02 +00:00
|
|
|
reactions = reactions or []
|
|
|
|
authors = authors or []
|
2024-03-04 07:35:33 +00:00
|
|
|
return {
|
2024-03-06 09:25:55 +00:00
|
|
|
'thread': thread,
|
|
|
|
'authors': authors,
|
|
|
|
'updated_at': int(time.time()),
|
|
|
|
'shout': shout,
|
|
|
|
'reactions': reactions,
|
|
|
|
'entity': entity,
|
|
|
|
'action': action,
|
2024-03-04 07:35:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-03-06 09:25:55 +00:00
|
|
|
def get_notifications_grouped(
|
|
|
|
author_id: int, after: int = 0, limit: int = 10, offset: int = 0
|
|
|
|
):
|
2024-03-04 07:35:33 +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.
|
2024-03-04 12:47:17 +00:00
|
|
|
offset (int, optional): offset
|
2024-03-04 07:35:33 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
total, unread, notifications = query_notifications(author_id, after)
|
|
|
|
groups_by_thread = {}
|
|
|
|
groups_amount = 0
|
|
|
|
|
|
|
|
for notification, seen in notifications:
|
2024-03-04 12:47:17 +00:00
|
|
|
if (groups_amount + offset) >= limit:
|
2024-03-04 07:35:33 +00:00
|
|
|
break
|
|
|
|
|
2024-03-04 10:43:02 +00:00
|
|
|
payload = json.loads(notification.payload)
|
2024-03-04 07:35:33 +00:00
|
|
|
|
|
|
|
if notification.entity == NotificationEntity.SHOUT.value:
|
2024-03-04 10:43:02 +00:00
|
|
|
shout = payload
|
|
|
|
shout_id = shout.get('id')
|
|
|
|
author_id = shout.get('created_by')
|
2024-03-04 07:35:33 +00:00
|
|
|
thread_id = f'shout-{shout_id}'
|
2024-03-04 10:43:02 +00:00
|
|
|
with local_session() as session:
|
|
|
|
author = session.query(Author).filter(Author.id == author_id).first()
|
|
|
|
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
|
|
|
if author and shout:
|
|
|
|
author = author.dict()
|
|
|
|
shout = shout.dict()
|
2024-03-06 09:25:55 +00:00
|
|
|
group = group_notification(
|
|
|
|
thread_id,
|
|
|
|
shout=shout,
|
|
|
|
authors=[author],
|
|
|
|
action=notification.action,
|
|
|
|
entity=notification.entity,
|
|
|
|
)
|
2024-03-04 07:35:33 +00:00
|
|
|
groups_by_thread[thread_id] = group
|
|
|
|
groups_amount += 1
|
|
|
|
|
2024-03-04 10:43:02 +00:00
|
|
|
elif notification.entity == NotificationEntity.REACTION.value:
|
|
|
|
reaction = payload
|
|
|
|
shout_id = shout.get('shout')
|
|
|
|
author_id = shout.get('created_by')
|
|
|
|
with local_session() as session:
|
|
|
|
author = session.query(Author).filter(Author.id == author_id).first()
|
|
|
|
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
|
|
|
if shout and author:
|
|
|
|
author = author.dict()
|
|
|
|
shout = shout.dict()
|
|
|
|
reply_id = reaction.get('reply_to')
|
|
|
|
thread_id = f'shout-{shout_id}'
|
|
|
|
if reply_id and reaction.get('kind', '').lower() == 'comment':
|
|
|
|
thread_id += f'{reply_id}'
|
|
|
|
existing_group = groups_by_thread.get(thread_id)
|
|
|
|
if existing_group:
|
|
|
|
existing_group['seen'] = False
|
|
|
|
existing_group['authors'].append(author_id)
|
|
|
|
existing_group['reactions'] = existing_group['reactions'] or []
|
|
|
|
existing_group['reactions'].append(reaction)
|
|
|
|
groups_by_thread[thread_id] = existing_group
|
|
|
|
else:
|
2024-03-06 09:25:55 +00:00
|
|
|
group = group_notification(
|
|
|
|
thread_id,
|
|
|
|
authors=[author],
|
|
|
|
shout=shout,
|
|
|
|
reactions=[reaction],
|
|
|
|
entity=notification.entity,
|
|
|
|
action=notification.action,
|
|
|
|
)
|
2024-03-04 10:43:02 +00:00
|
|
|
if group:
|
|
|
|
groups_by_thread[thread_id] = group
|
|
|
|
groups_amount += 1
|
|
|
|
|
2024-03-06 09:25:55 +00:00
|
|
|
elif notification.entity == 'follower':
|
2024-03-04 10:43:02 +00:00
|
|
|
thread_id = 'followers'
|
|
|
|
follower = json.loads(payload)
|
2024-03-04 07:35:33 +00:00
|
|
|
group = groups_by_thread.get(thread_id)
|
|
|
|
if group:
|
2024-03-04 10:43:02 +00:00
|
|
|
if notification.action == 'follow':
|
|
|
|
group['authors'].append(follower)
|
|
|
|
elif notification.action == 'unfollow':
|
|
|
|
follower_id = follower.get('id')
|
|
|
|
for author in group['authors']:
|
|
|
|
if author.get('id') == follower_id:
|
|
|
|
group['authors'].remove(author)
|
|
|
|
break
|
2024-03-04 07:35:33 +00:00
|
|
|
else:
|
2024-03-06 09:25:55 +00:00
|
|
|
group = group_notification(
|
|
|
|
thread_id,
|
|
|
|
authors=[follower],
|
|
|
|
entity=notification.entity,
|
|
|
|
action=notification.action,
|
|
|
|
)
|
2024-03-04 07:35:33 +00:00
|
|
|
groups_amount += 1
|
|
|
|
groups_by_thread[thread_id] = group
|
|
|
|
return groups_by_thread, unread, total
|
|
|
|
|
|
|
|
|
|
|
|
@query.field('load_notifications')
|
|
|
|
@login_required
|
2024-03-04 12:47:17 +00:00
|
|
|
async def load_notifications(_, info, after: int, limit: int = 50, offset=0):
|
2024-03-06 09:25:55 +00:00
|
|
|
author_id = info.context.get('author_id')
|
2024-03-04 10:43:02 +00:00
|
|
|
error = None
|
|
|
|
total = 0
|
|
|
|
unread = 0
|
|
|
|
notifications = []
|
|
|
|
try:
|
|
|
|
if author_id:
|
|
|
|
groups, unread, total = get_notifications_grouped(author_id, after, limit)
|
2024-03-06 09:25:55 +00:00
|
|
|
notifications = sorted(
|
|
|
|
groups.values(), key=lambda group: group.updated_at, reverse=True
|
|
|
|
)
|
2024-03-04 10:43:02 +00:00
|
|
|
except Exception as e:
|
|
|
|
error = e
|
|
|
|
logger.error(e)
|
2024-03-06 09:25:55 +00:00
|
|
|
return {
|
|
|
|
'notifications': notifications,
|
|
|
|
'total': total,
|
|
|
|
'unread': unread,
|
|
|
|
'error': error,
|
|
|
|
}
|
2024-03-04 07:35:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@mutation.field('notification_mark_seen')
|
|
|
|
@login_required
|
|
|
|
async def notification_mark_seen(_, info, notification_id: int):
|
|
|
|
author_id = info.context.get('author_id')
|
|
|
|
if author_id:
|
|
|
|
with local_session() as session:
|
|
|
|
try:
|
|
|
|
ns = NotificationSeen(notification=notification_id, viewer=author_id)
|
|
|
|
session.add(ns)
|
|
|
|
session.commit()
|
|
|
|
except SQLAlchemyError as e:
|
|
|
|
session.rollback()
|
|
|
|
logger.error(f'seen mutation failed: {e}')
|
2024-03-06 09:25:55 +00:00
|
|
|
return {'error': 'cant mark as read'}
|
|
|
|
return {'error': None}
|
2024-03-04 07:35:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@mutation.field('notifications_seen_after')
|
|
|
|
@login_required
|
|
|
|
async def notifications_seen_after(_, info, after: int):
|
|
|
|
# TODO: use latest loaded notification_id as input offset parameter
|
|
|
|
error = None
|
|
|
|
try:
|
|
|
|
author_id = info.context.get('author_id')
|
|
|
|
if author_id:
|
|
|
|
with local_session() as session:
|
2024-03-06 09:25:55 +00:00
|
|
|
nnn = (
|
|
|
|
session.query(Notification)
|
|
|
|
.filter(and_(Notification.created_at > after))
|
|
|
|
.all()
|
|
|
|
)
|
2024-03-04 07:35:33 +00:00
|
|
|
for n in nnn:
|
|
|
|
try:
|
|
|
|
ns = NotificationSeen(notification=n.id, viewer=author_id)
|
|
|
|
session.add(ns)
|
|
|
|
session.commit()
|
|
|
|
except SQLAlchemyError:
|
|
|
|
session.rollback()
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
error = 'cant mark as read'
|
2024-03-06 09:25:55 +00:00
|
|
|
return {'error': error}
|
2024-03-04 07:35:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@mutation.field('notifications_seen_thread')
|
|
|
|
@login_required
|
|
|
|
async def notifications_seen_thread(_, info, thread: str, after: int):
|
|
|
|
error = None
|
|
|
|
author_id = info.context.get('author_id')
|
|
|
|
if author_id:
|
2024-03-04 10:43:02 +00:00
|
|
|
[shout_id, reply_to_id] = thread.split(':')
|
2024-03-04 07:35:33 +00:00
|
|
|
with local_session() as session:
|
|
|
|
# TODO: handle new follower and new shout notifications
|
|
|
|
new_reaction_notifications = (
|
|
|
|
session.query(Notification)
|
|
|
|
.filter(
|
|
|
|
Notification.action == 'create',
|
|
|
|
Notification.entity == 'reaction',
|
|
|
|
Notification.created_at > after,
|
2024-03-06 09:25:55 +00:00
|
|
|
)
|
2024-03-04 07:35:33 +00:00
|
|
|
.all()
|
|
|
|
)
|
|
|
|
removed_reaction_notifications = (
|
|
|
|
session.query(Notification)
|
|
|
|
.filter(
|
|
|
|
Notification.action == 'delete',
|
|
|
|
Notification.entity == 'reaction',
|
|
|
|
Notification.created_at > after,
|
2024-03-06 09:25:55 +00:00
|
|
|
)
|
2024-03-04 07:35:33 +00:00
|
|
|
.all()
|
|
|
|
)
|
|
|
|
exclude = set()
|
|
|
|
for nr in removed_reaction_notifications:
|
|
|
|
reaction = json.loads(nr.payload)
|
|
|
|
reaction_id = reaction.get('id')
|
|
|
|
exclude.add(reaction_id)
|
|
|
|
for n in new_reaction_notifications:
|
|
|
|
reaction = json.loads(n.payload)
|
|
|
|
reaction_id = reaction.get('id')
|
|
|
|
if (
|
2024-03-06 09:25:55 +00:00
|
|
|
reaction_id not in exclude
|
|
|
|
and reaction.get('shout') == shout_id
|
|
|
|
and reaction.get('reply_to') == reply_to_id
|
2024-03-04 07:35:33 +00:00
|
|
|
):
|
|
|
|
try:
|
|
|
|
ns = NotificationSeen(notification=n.id, viewer=author_id)
|
|
|
|
session.add(ns)
|
|
|
|
session.commit()
|
|
|
|
except Exception as e:
|
|
|
|
logger.warn(e)
|
|
|
|
session.rollback()
|
|
|
|
else:
|
|
|
|
error = 'You are not logged in'
|
2024-03-06 09:25:55 +00:00
|
|
|
return {'error': error}
|