Coverage for app/backend/src/couchers/notifications/notify.py: 100%

19 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +0000

1import logging 

2 

3from google.protobuf import empty_pb2 

4from google.protobuf.message import Message 

5from sqlalchemy import update 

6from sqlalchemy.orm import Session 

7 

8from couchers.jobs.enqueue import queue_job 

9from couchers.models import Notification 

10from couchers.models.notifications import NotificationTopicAction 

11from couchers.proto.internal import jobs_pb2 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16def notify( 

17 session: Session, 

18 *, 

19 user_id: int, 

20 topic_action: NotificationTopicAction, 

21 key: str, 

22 data: Message | None = None, 

23 moderation_state_id: int | None = None, 

24) -> None: 

25 """ 

26 Queues a notification given the notification and a target, i.e. a tuple (user_id, topic, key), and an action. 

27 

28 Notifications are sent to user identified by user_id, and are collapsed/grouped based on the combination of 

29 (topic, key). 

30 

31 For example, topic may be "chat" for a group chat/direct message, and the key might be the chat id; so that messages 

32 in the same group chat show up in one group. 

33 

34 The action is a simple identifier describing the action that caused the notification. For the above example, the 

35 action might be "add_admin" if the notification was caused by another user adding an admin into the gorup chat. 

36 

37 Each different notification type should have its own action. 

38 

39 If moderation_state_id is provided, the notification delivery is deferred until the linked content 

40 becomes VISIBLE or UNLISTED. This is used for notifications related to moderated content. 

41 

42 The key parameter is required. Pass key="" for notifications that intentionally don't have a key 

43 (e.g., security notifications like password changes, or aggregated notifications like chat:missed_messages). 

44 """ 

45 logger.info(f"Generating notification of type {topic_action.display} for user {user_id}") 

46 # Import here to avoid circular dependency 

47 from couchers.notifications.background import handle_notification # noqa: PLC0415 

48 

49 notification = Notification( 

50 user_id=user_id, 

51 topic_action=topic_action, 

52 key=key, 

53 data=(data or empty_pb2.Empty()).SerializeToString(), 

54 moderation_state_id=moderation_state_id, 

55 ) 

56 session.add(notification) 

57 session.flush() 

58 

59 queue_job( 

60 session, 

61 job=handle_notification, 

62 payload=jobs_pb2.HandleNotificationPayload( 

63 notification_id=notification.id, 

64 ), 

65 ) 

66 

67 

68def mark_notifications_seen( 

69 session: Session, 

70 *, 

71 user_id: int, 

72 key: str, 

73 topic_actions: list[NotificationTopicAction], 

74) -> None: 

75 """ 

76 Marks all unseen notifications for the given user, key, and topic actions as seen. 

77 """ 

78 session.execute( 

79 update(Notification) 

80 .values(is_seen=True) 

81 .where(Notification.user_id == user_id) 

82 .where(Notification.key == key) 

83 .where(Notification.topic_action.in_(topic_actions)) 

84 )