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

1""" 

2Rules for scoping a user's group chat messages, shared by the conversations API and the Ping badge. 

3""" 

4 

5from datetime import datetime 

6 

7from sqlalchemy import ColumnElement, SQLColumnExpression, and_, func, or_, select 

8 

9from couchers.models import GroupChatSubscription, Message 

10from couchers.proto import conversations_pb2 

11from couchers.utils import Timestamp_from_datetime 

12 

13 

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 ) 

24 

25 

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 ) 

36 

37 

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) 

53 

54 

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 )