Coverage for app/backend/src/couchers/servicers/jail.py: 96%

71 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +0000

1import logging 

2 

3import grpc 

4from google.protobuf import empty_pb2 

5from sqlalchemy import select 

6from sqlalchemy.orm import Session 

7 

8from couchers.constants import GUIDELINES_VERSION, TOS_VERSION 

9from couchers.context import CouchersContext 

10from couchers.helpers.hosting_meetup_status import record_hosting_meetup_status 

11from couchers.models import ( 

12 ActivenessProbe, 

13 ActivenessProbeStatus, 

14 HostingMeetupStatusSource, 

15 HostingStatus, 

16 ModNote, 

17 User, 

18) 

19from couchers.proto import jail_pb2, jail_pb2_grpc 

20from couchers.servicers.account import mod_note_to_pb 

21from couchers.utils import create_coordinate, now 

22 

23logger = logging.getLogger(__name__) 

24 

25 

26def _get_jail_info(user: User) -> jail_pb2.JailInfoRes: 

27 res = jail_pb2.JailInfoRes( 

28 has_not_accepted_tos=user.jailed_missing_tos, 

29 needs_to_update_location=user.is_missing_location, 

30 has_not_accepted_community_guidelines=user.jailed_missing_community_guidelines, 

31 has_pending_mod_notes=user.jailed_pending_mod_notes, 

32 pending_mod_notes=[mod_note_to_pb(note) for note in user.mod_notes.where(ModNote.is_pending)], 

33 has_pending_activeness_probe=user.jailed_pending_activeness_probe, 

34 ) 

35 

36 # if any of the bools in res are true, we're jailed 

37 jailed = False 

38 for field in res.DESCRIPTOR.fields: 

39 if getattr(res, field.name): 

40 jailed = True 

41 res.jailed = jailed 

42 

43 # double check 

44 assert user.is_jailed == jailed 

45 

46 return res 

47 

48 

49class Jail(jail_pb2_grpc.JailServicer): 

50 """ 

51 The Jail servicer. 

52 

53 API calls allowed for users who need to complete some tasks before being 

54 fully active 

55 """ 

56 

57 def JailInfo(self, request: empty_pb2.Empty, context: CouchersContext, session: Session) -> jail_pb2.JailInfoRes: 

58 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

59 return _get_jail_info(user) 

60 

61 def AcceptTOS( 

62 self, request: jail_pb2.AcceptTOSReq, context: CouchersContext, session: Session 

63 ) -> jail_pb2.JailInfoRes: 

64 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

65 

66 if not request.accept: 

67 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_unaccept_tos") 

68 

69 user.accepted_tos = TOS_VERSION 

70 

71 return _get_jail_info(user) 

72 

73 def SetLocation( 

74 self, request: jail_pb2.SetLocationReq, context: CouchersContext, session: Session 

75 ) -> jail_pb2.JailInfoRes: 

76 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

77 

78 if request.lat == 0 and request.lng == 0: 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true

79 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate") 

80 

81 user.city = request.city 

82 user.geom = create_coordinate(request.lat, request.lng) 

83 user.randomized_geom = None 

84 user.geom_radius = request.radius 

85 user.needs_to_update_location = False 

86 

87 return _get_jail_info(user) 

88 

89 def AcceptCommunityGuidelines( 

90 self, request: jail_pb2.AcceptCommunityGuidelinesReq, context: CouchersContext, session: Session 

91 ) -> jail_pb2.JailInfoRes: 

92 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

93 

94 if not request.accept: 

95 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_unaccept_community_guidelines") 

96 

97 user.accepted_community_guidelines = GUIDELINES_VERSION 

98 

99 return _get_jail_info(user) 

100 

101 def AcknowledgePendingModNote( 

102 self, request: jail_pb2.AcknowledgePendingModNoteReq, context: CouchersContext, session: Session 

103 ) -> jail_pb2.JailInfoRes: 

104 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

105 

106 note = session.execute( 

107 select(ModNote) 

108 .where(ModNote.user_id == user.id) 

109 .where(ModNote.is_pending) 

110 .where(ModNote.id == request.note_id) 

111 ).scalar_one_or_none() 

112 

113 if not note: 

114 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "moderator_note_not_found") 

115 

116 if not request.acknowledge: 

117 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "moderator_note_need_to_acknowledge") 

118 

119 note.acknowledged = now() 

120 

121 return _get_jail_info(user) 

122 

123 def RespondToActivenessProbe( 

124 self, request: jail_pb2.RespondToActivenessProbeReq, context: CouchersContext, session: Session 

125 ) -> jail_pb2.JailInfoRes: 

126 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

127 

128 probe = session.execute( 

129 select(ActivenessProbe).where(ActivenessProbe.user_id == user.id).where(ActivenessProbe.is_pending) 

130 ).scalar_one_or_none() 

131 

132 if not probe: 

133 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "probe_not_found") 

134 

135 if request.response == jail_pb2.ACTIVENESS_PROBE_RESPONSE_STILL_ACTIVE: 

136 probe.response = ActivenessProbeStatus.still_active 

137 elif request.response == jail_pb2.ACTIVENESS_PROBE_RESPONSE_NO_LONGER_ACTIVE: 137 ↛ 142line 137 didn't jump to line 142 because the condition on line 137 was always true

138 probe.response = ActivenessProbeStatus.no_longer_active 

139 # disable hosting 

140 user.hosting_status = HostingStatus.cant_host 

141 else: 

142 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "probe_response_invalid") 

143 

144 probe.responded = now() 

145 

146 # after `responded` is set, otherwise the autoflush inside would trip the probe's check constraint 

147 record_hosting_meetup_status(session, user, HostingMeetupStatusSource.activeness_probe_response) 

148 

149 return _get_jail_info(user)