fixed-fmt-linted
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
from orm.notification import Notification
|
||||
from resolvers.model import NotificationReaction, NotificationAuthor, NotificationShout
|
||||
from services.db import local_session
|
||||
from services.rediscache import redis
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("[listener.listen_task] ")
|
||||
from orm.notification import Notification
|
||||
from resolvers.model import NotificationAuthor, NotificationReaction, NotificationShout
|
||||
from services.db import local_session
|
||||
from services.rediscache import redis
|
||||
|
||||
|
||||
logger = logging.getLogger('[listener.listen_task] ')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -19,8 +21,8 @@ async def handle_notification(n: ServiceMessage, channel: str):
|
||||
"""создаеёт новое хранимое уведомление"""
|
||||
with local_session() as session:
|
||||
try:
|
||||
if channel.startswith("follower:"):
|
||||
author_id = int(channel.split(":")[1])
|
||||
if channel.startswith('follower:'):
|
||||
author_id = int(channel.split(':')[1])
|
||||
if isinstance(n.payload, NotificationAuthor):
|
||||
n.payload.following_id = author_id
|
||||
n = Notification(action=n.action, entity=n.entity, payload=n.payload)
|
||||
@@ -28,7 +30,7 @@ async def handle_notification(n: ServiceMessage, channel: str):
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"[listener.handle_reaction] error: {str(e)}")
|
||||
logger.error(f'[listener.handle_reaction] error: {str(e)}')
|
||||
|
||||
|
||||
async def listen_task(pattern):
|
||||
@@ -38,9 +40,9 @@ async def listen_task(pattern):
|
||||
notification_message = ServiceMessage(**message_data)
|
||||
await handle_notification(notification_message, str(channel))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing notification: {str(e)}")
|
||||
logger.error(f'Error processing notification: {str(e)}')
|
||||
|
||||
|
||||
async def notifications_worker():
|
||||
# Use asyncio.gather to run tasks concurrently
|
||||
await asyncio.gather(listen_task("follower:*"), listen_task("reaction"), listen_task("shout"))
|
||||
await asyncio.gather(listen_task('follower:*'), listen_task('reaction'), listen_task('shout'))
|
||||
|
||||
@@ -1,53 +1,31 @@
|
||||
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
|
||||
from sqlalchemy.sql import not_
|
||||
from services.db import local_session
|
||||
|
||||
from orm.notification import Notification, NotificationAction, NotificationEntity, NotificationSeen
|
||||
from resolvers.model import (
|
||||
NotificationReaction,
|
||||
NotificationGroup,
|
||||
NotificationShout,
|
||||
NotificationAuthor,
|
||||
NotificationGroup,
|
||||
NotificationReaction,
|
||||
NotificationShout,
|
||||
NotificationsResult,
|
||||
)
|
||||
from orm.notification import NotificationAction, NotificationEntity, NotificationSeen, Notification
|
||||
from typing import Dict, List
|
||||
import time
|
||||
import json
|
||||
import strawberry
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy import select, and_
|
||||
import logging
|
||||
from services.db import local_session
|
||||
|
||||
logger = logging.getLogger("[resolvers.schema] ")
|
||||
|
||||
logger = logging.getLogger('[resolvers.schema]')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def get_notifications_grouped(author_id: int, after: int = 0, limit: int = 10, offset: int = 0):
|
||||
"""
|
||||
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.
|
||||
}
|
||||
"""
|
||||
NotificationSeenAlias = aliased(NotificationSeen)
|
||||
query = select(Notification, NotificationSeenAlias.viewer.label("seen")).outerjoin(
|
||||
def query_notifications(author_id: int, after: int = 0) -> Tuple[int, int, List[Tuple[Notification, bool]]]:
|
||||
notification_seen_alias = aliased(NotificationSeen)
|
||||
query = select(Notification, notification_seen_alias.viewer.label('seen')).outerjoin(
|
||||
NotificationSeen,
|
||||
and_(NotificationSeen.viewer == author_id, NotificationSeen.notification == Notification.id),
|
||||
)
|
||||
@@ -55,17 +33,13 @@ async def get_notifications_grouped(author_id: int, after: int = 0, limit: int =
|
||||
query = query.filter(Notification.created_at > after)
|
||||
query = query.group_by(NotificationSeen.notification, Notification.created_at)
|
||||
|
||||
groups_amount = 0
|
||||
unread = 0
|
||||
total = 0
|
||||
notifications_by_thread: Dict[str, List[Notification]] = {}
|
||||
groups_by_thread: Dict[str, NotificationGroup] = {}
|
||||
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(
|
||||
@@ -77,123 +51,140 @@ async def get_notifications_grouped(author_id: int, after: int = 0, limit: int =
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
notifications_result = session.execute(query)
|
||||
notifications = []
|
||||
for n, seen in notifications_result:
|
||||
thread_id = ""
|
||||
payload = json.loads(n.payload)
|
||||
logger.debug(f"[resolvers.schema] {n.action} {n.entity}: {payload}")
|
||||
if n.entity == "shout" and n.action == "create":
|
||||
shout: NotificationShout = payload
|
||||
thread_id += f"{shout.id}"
|
||||
logger.debug(f"create shout: {shout}")
|
||||
group = groups_by_thread.get(thread_id) or NotificationGroup(
|
||||
id=thread_id,
|
||||
entity=n.entity,
|
||||
shout=shout,
|
||||
authors=shout.authors,
|
||||
updated_at=shout.created_at,
|
||||
reactions=[],
|
||||
action="create",
|
||||
seen=author_id in n.seen,
|
||||
)
|
||||
# store group in result
|
||||
groups_by_thread[thread_id] = group
|
||||
notifications = notifications_by_thread.get(thread_id, [])
|
||||
if n not in notifications:
|
||||
notifications.append(n)
|
||||
notifications_by_thread[thread_id] = notifications
|
||||
groups_amount += 1
|
||||
elif n.entity == NotificationEntity.REACTION.value and n.action == NotificationAction.CREATE.value:
|
||||
reaction: NotificationReaction = payload
|
||||
shout: NotificationShout = reaction.shout
|
||||
thread_id += f"{reaction.shout}"
|
||||
if reaction.kind == "LIKE" or reaction.kind == "DISLIKE":
|
||||
# TODO: making published reaction vote announce
|
||||
pass
|
||||
elif reaction.kind == "COMMENT":
|
||||
if reaction.reply_to:
|
||||
thread_id += f"{'::' + str(reaction.reply_to)}"
|
||||
group: NotificationGroup | None = groups_by_thread.get(thread_id)
|
||||
notifications: List[Notification] = notifications_by_thread.get(thread_id, [])
|
||||
if group and notifications:
|
||||
group.seen = False # any not seen notification make it false
|
||||
group.shout = shout
|
||||
group.authors.append(reaction.created_by)
|
||||
if not group.reactions:
|
||||
group.reactions = []
|
||||
group.reactions.append(reaction.id)
|
||||
# store group in result
|
||||
groups_by_thread[thread_id] = group
|
||||
notifications = notifications_by_thread.get(thread_id, [])
|
||||
if n not in notifications:
|
||||
notifications.append(n)
|
||||
notifications_by_thread[thread_id] = notifications
|
||||
groups_amount += 1
|
||||
else:
|
||||
groups_amount += 1
|
||||
if groups_amount > limit:
|
||||
break
|
||||
else:
|
||||
# init notification group
|
||||
reactions = []
|
||||
reactions.append(reaction.id)
|
||||
group = NotificationGroup(
|
||||
id=thread_id,
|
||||
action=n.action,
|
||||
entity=n.entity,
|
||||
updated_at=reaction.created_at,
|
||||
reactions=reactions,
|
||||
shout=shout,
|
||||
authors=[
|
||||
reaction.created_by,
|
||||
],
|
||||
seen=author_id in n.seen,
|
||||
)
|
||||
# store group in result
|
||||
groups_by_thread[thread_id] = group
|
||||
notifications = notifications_by_thread.get(thread_id, [])
|
||||
if n not in notifications:
|
||||
notifications.append(n)
|
||||
notifications_by_thread[thread_id] = notifications
|
||||
notifications.append((n, seen))
|
||||
|
||||
elif n.entity == "follower":
|
||||
thread_id = "followers"
|
||||
follower: NotificationAuthor = payload
|
||||
group = groups_by_thread.get(thread_id) or NotificationGroup(
|
||||
id=thread_id,
|
||||
authors=[follower],
|
||||
updated_at=int(time.time()),
|
||||
shout=None,
|
||||
reactions=[],
|
||||
entity="follower",
|
||||
action="follow",
|
||||
seen=author_id in n.seen,
|
||||
)
|
||||
group.authors = [
|
||||
follower,
|
||||
]
|
||||
group.updated_at = int(time.time())
|
||||
# store group in result
|
||||
return total, unread, notifications
|
||||
|
||||
|
||||
def process_shout_notification(
|
||||
notification: Notification, seen: bool
|
||||
) -> Union[Tuple[str, NotificationGroup], None] | None:
|
||||
if not isinstance(notification.payload, str) or not isinstance(notification.entity, str):
|
||||
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=[],
|
||||
action='create',
|
||||
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)
|
||||
if reaction.kind == 'COMMENT' and reaction.reply_to:
|
||||
thread_id += f'::{reaction.reply_to}'
|
||||
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
|
||||
thread_id = 'followers'
|
||||
group = NotificationGroup(
|
||||
id=thread_id,
|
||||
authors=[follower],
|
||||
updated_at=int(time.time()),
|
||||
shout=None,
|
||||
reactions=[],
|
||||
entity='follower',
|
||||
action='follow',
|
||||
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]:
|
||||
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
|
||||
|
||||
if str(notification.entity) == 'shout' and str(notification.action) == 'create':
|
||||
result = process_shout_notification(notification, seen)
|
||||
if result:
|
||||
thread_id, group = result
|
||||
groups_by_thread[thread_id] = group
|
||||
notifications = notifications_by_thread.get(thread_id, [])
|
||||
if n not in notifications:
|
||||
notifications.append(n)
|
||||
notifications_by_thread[thread_id] = notifications
|
||||
groups_amount += 1
|
||||
|
||||
if groups_amount > limit:
|
||||
break
|
||||
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
|
||||
|
||||
return groups_by_thread, notifications_by_thread, unread, total
|
||||
elif str(notification.entity) == 'follower':
|
||||
result = process_follower_notification(notification, seen)
|
||||
if result:
|
||||
thread_id, group = result
|
||||
groups_by_thread[thread_id] = group
|
||||
groups_amount += 1
|
||||
|
||||
return groups_by_thread, unread, total
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Query:
|
||||
@strawberry.field
|
||||
async def load_notifications(self, info, after: int, limit: int = 50, offset: int = 0) -> NotificationsResult:
|
||||
author_id = info.context.get("author_id")
|
||||
groups: Dict[str, NotificationGroup] = {}
|
||||
author_id = info.context.get('author_id')
|
||||
if author_id:
|
||||
groups, notifications, total, unread = await get_notifications_grouped(author_id, after, limit, offset)
|
||||
notifications = sorted(groups.values(), key=lambda group: group.updated_at, reverse=True)
|
||||
return NotificationsResult(notifications=notifications, total=0, unread=0, error=None)
|
||||
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)
|
||||
return NotificationsResult(notifications=[], total=0, unread=0, error=None)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import strawberry
|
||||
from typing import List, Optional
|
||||
|
||||
import strawberry
|
||||
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
|
||||
|
||||
from orm.notification import Notification as NotificationMessage
|
||||
|
||||
|
||||
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import strawberry
|
||||
from strawberry.schema.config import StrawberryConfig
|
||||
|
||||
from services.auth import LoginRequiredMiddleware
|
||||
from resolvers.load import Query
|
||||
from resolvers.seen import Mutation
|
||||
from services.auth import LoginRequiredMiddleware
|
||||
from services.db import Base, engine
|
||||
|
||||
|
||||
schema = strawberry.Schema(
|
||||
query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False), extensions=[LoginRequiredMiddleware]
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from sqlalchemy import and_
|
||||
from orm.notification import NotificationSeen
|
||||
from services.db import local_session
|
||||
from resolvers.model import Notification, NotificationSeenResult, NotificationReaction
|
||||
import json
|
||||
import logging
|
||||
|
||||
import strawberry
|
||||
import logging
|
||||
import json
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from orm.notification import NotificationSeen
|
||||
from resolvers.model import Notification, NotificationReaction, NotificationSeenResult
|
||||
from services.db import local_session
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
||||
class Mutation:
|
||||
@strawberry.mutation
|
||||
async def mark_seen(self, info, notification_id: int) -> NotificationSeenResult:
|
||||
author_id = info.context.get("author_id")
|
||||
author_id = info.context.get('author_id')
|
||||
if author_id:
|
||||
with local_session() as session:
|
||||
try:
|
||||
@@ -27,9 +27,9 @@ class Mutation:
|
||||
except SQLAlchemyError as e:
|
||||
session.rollback()
|
||||
logger.error(
|
||||
f"[mark_notification_as_read] Ошибка при обновлении статуса прочтения уведомления: {str(e)}"
|
||||
f'[mark_notification_as_read] Ошибка при обновлении статуса прочтения уведомления: {str(e)}'
|
||||
)
|
||||
return NotificationSeenResult(error="cant mark as read")
|
||||
return NotificationSeenResult(error='cant mark as read')
|
||||
return NotificationSeenResult(error=None)
|
||||
|
||||
@strawberry.mutation
|
||||
@@ -37,7 +37,7 @@ class Mutation:
|
||||
# TODO: use latest loaded notification_id as input offset parameter
|
||||
error = None
|
||||
try:
|
||||
author_id = info.context.get("author_id")
|
||||
author_id = info.context.get('author_id')
|
||||
if author_id:
|
||||
with local_session() as session:
|
||||
nnn = session.query(Notification).filter(and_(Notification.created_at > after)).all()
|
||||
@@ -50,22 +50,22 @@ class Mutation:
|
||||
session.rollback()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
error = "cant mark as read"
|
||||
error = 'cant mark as read'
|
||||
return NotificationSeenResult(error=error)
|
||||
|
||||
@strawberry.mutation
|
||||
async def mark_seen_thread(self, info, thread: str, after: int) -> NotificationSeenResult:
|
||||
error = None
|
||||
author_id = info.context.get("author_id")
|
||||
author_id = info.context.get('author_id')
|
||||
if author_id:
|
||||
[shout_id, reply_to_id] = thread.split("::")
|
||||
[shout_id, reply_to_id] = thread.split('::')
|
||||
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.action == 'create',
|
||||
Notification.entity == 'reaction',
|
||||
Notification.created_at > after,
|
||||
)
|
||||
.all()
|
||||
@@ -73,13 +73,13 @@ class Mutation:
|
||||
removed_reaction_notifications = (
|
||||
session.query(Notification)
|
||||
.filter(
|
||||
Notification.action == "delete",
|
||||
Notification.entity == "reaction",
|
||||
Notification.action == 'delete',
|
||||
Notification.entity == 'reaction',
|
||||
Notification.created_at > after,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
exclude = set([])
|
||||
exclude = set()
|
||||
for nr in removed_reaction_notifications:
|
||||
reaction: NotificationReaction = json.loads(nr.payload)
|
||||
exclude.add(reaction.id)
|
||||
@@ -97,5 +97,5 @@ class Mutation:
|
||||
except Exception:
|
||||
session.rollback()
|
||||
else:
|
||||
error = "You are not logged in"
|
||||
error = 'You are not logged in'
|
||||
return NotificationSeenResult(error=error)
|
||||
|
||||
Reference in New Issue
Block a user