Coverage for app/backend/src/couchers/helpers/group_chats.py: 100%
15 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 15:29 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 15:29 +0000
1"""
2Rules for scoping a user's group chat messages, shared by the conversations API and the Ping badge.
3"""
5from datetime import datetime
7from sqlalchemy import ColumnElement, SQLColumnExpression, and_, func, or_, select
9from couchers.models import GroupChatSubscription, Message
10from couchers.proto import conversations_pb2
11from couchers.utils import Timestamp_from_datetime
14def was_subscribed_at(
15 subscription: type[GroupChatSubscription], instant: SQLColumnExpression[datetime] | datetime
16) -> ColumnElement[bool]:
17 """
18 In the chat at the given instant: joined before it and hadn't left yet, or left after it.
19 """
20 return and_(
21 subscription.joined <= instant,
22 or_(subscription.left == None, subscription.left >= instant),
23 )
26def is_unseen(message: type[Message], subscription: type[GroupChatSubscription]) -> ColumnElement[bool]:
27 """
28 Unseen by the subscriber, over the window this subscription covers. Nothing outside it is unread:
29 either the subscriber wasn't in the chat, or it belongs to a subscription they abandoned on
30 rejoining, which marking seen can never advance.
31 """
32 return and_(
33 was_subscribed_at(subscription, message.time),
34 message.id > subscription.last_seen_message_id,
35 )
38def is_newest_subscription(user_id: SQLColumnExpression[int] | int) -> ColumnElement[bool]:
39 """
40 Only the user's newest subscription to each chat they've been in. Rejoining a chat leaves the
41 earlier subscription behind, and it's the newest one that carries the archived and last-seen state.
42 """
43 newest_per_chat = (
44 select(func.max(GroupChatSubscription.id))
45 .where(GroupChatSubscription.user_id == user_id)
46 .group_by(GroupChatSubscription.group_chat_id)
47 # not aliased(): rebuilding the ORM proxy index per call is a hotspot, and Ping runs this on
48 # every poll. correlate_except keeps this table local to the subquery so it doesn't bind to
49 # the enclosing query's GroupChatSubscription
50 .correlate_except(GroupChatSubscription)
51 )
52 return GroupChatSubscription.id.in_(newest_per_chat)
55def mute_info(subscription: GroupChatSubscription) -> conversations_pb2.MuteInfo:
56 (muted, muted_until) = subscription.muted_display()
57 return conversations_pb2.MuteInfo(
58 muted=muted,
59 muted_until=Timestamp_from_datetime(muted_until) if muted_until else None,
60 )