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
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
1import logging
3import grpc
4from google.protobuf import empty_pb2
5from sqlalchemy import select
6from sqlalchemy.orm import Session
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
23logger = logging.getLogger(__name__)
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 )
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
43 # double check
44 assert user.is_jailed == jailed
46 return res
49class Jail(jail_pb2_grpc.JailServicer):
50 """
51 The Jail servicer.
53 API calls allowed for users who need to complete some tasks before being
54 fully active
55 """
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)
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()
66 if not request.accept:
67 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_unaccept_tos")
69 user.accepted_tos = TOS_VERSION
71 return _get_jail_info(user)
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()
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")
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
87 return _get_jail_info(user)
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()
94 if not request.accept:
95 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_unaccept_community_guidelines")
97 user.accepted_community_guidelines = GUIDELINES_VERSION
99 return _get_jail_info(user)
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()
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()
113 if not note:
114 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "moderator_note_not_found")
116 if not request.acknowledge:
117 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "moderator_note_need_to_acknowledge")
119 note.acknowledged = now()
121 return _get_jail_info(user)
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()
128 probe = session.execute(
129 select(ActivenessProbe).where(ActivenessProbe.user_id == user.id).where(ActivenessProbe.is_pending)
130 ).scalar_one_or_none()
132 if not probe:
133 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "probe_not_found")
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")
144 probe.responded = now()
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)
149 return _get_jail_info(user)