Coverage for app/backend/src/couchers/servicers/api.py: 97%
490 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-22 16:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-22 16:01 +0000
1from collections.abc import Iterable
2from datetime import timedelta
3from typing import cast
4from urllib.parse import urlencode
6import google.protobuf.message
7import grpc
8from google.protobuf import empty_pb2
9from sqlalchemy import ColumnElement, select
10from sqlalchemy.orm import Session, selectinload
11from sqlalchemy.sql import and_, case, delete, exists, func, intersect, or_, union
13from couchers import urls
14from couchers.abuse import maybe_log_nonvisible_user_access
15from couchers.config import config
16from couchers.constants import GHOST_USERNAME
17from couchers.context import CouchersContext, make_notification_user_context
18from couchers.crypto import b64encode, generate_hash_signature, random_hex
19from couchers.event_log import log_event
20from couchers.helpers.completed_profile import has_completed_profile
21from couchers.helpers.strong_verification import get_strong_verification_fields
22from couchers.materialized_views import LiteUser, UserResponseRate
23from couchers.models import (
24 FriendRelationship,
25 FriendStatus,
26 GroupChat,
27 GroupChatSubscription,
28 HostingStatus,
29 HostRequest,
30 InitiatedUpload,
31 LanguageAbility,
32 LanguageFluency,
33 MeetupStatus,
34 Message,
35 ModerationObjectType,
36 NonvisibleUserAccessType,
37 Notification,
38 NotificationDeliveryType,
39 ParkingDetails,
40 RateLimitAction,
41 Reference,
42 RegionLived,
43 RegionVisited,
44 SleepingArrangement,
45 SmokingLocation,
46 User,
47 UserBadge,
48)
49from couchers.models.notifications import NotificationTopicAction
50from couchers.models.uploads import get_avatar_upload
51from couchers.moderation.utils import create_moderation
52from couchers.notifications.notify import notify
53from couchers.notifications.settings import get_topic_actions_by_delivery_type
54from couchers.proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
55from couchers.rate_limits.check import process_rate_limits_and_check_abort
56from couchers.rate_limits.definitions import RATE_LIMIT_HOURS
57from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed
58from couchers.servicers.blocking import is_not_visible
59from couchers.sql import (
60 moderation_state_column_visible,
61 username_or_id,
62 users_visible,
63 where_moderated_content_visible,
64 where_users_column_visible,
65)
66from couchers.utils import (
67 Duration_from_timedelta,
68 Timestamp_from_datetime,
69 create_coordinate,
70 get_coordinates,
71 is_valid_name,
72 is_valid_user_id,
73 is_valid_username,
74 not_none,
75 now,
76)
79class GhostUserSerializationError(Exception):
80 """
81 Raised when attempting to serialize a ghost user (deleted/banned/blocked)
82 """
84 pass
87MAX_USERS_PER_QUERY = 200
88MAX_PAGINATION_LENGTH = 50
90hostingstatus2sql = {
91 api_pb2.HOSTING_STATUS_UNKNOWN: None,
92 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
93 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
94 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
95}
97hostingstatus2api = {
98 None: api_pb2.HOSTING_STATUS_UNKNOWN,
99 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
100 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
101 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
102}
104meetupstatus2sql = {
105 api_pb2.MEETUP_STATUS_UNKNOWN: None,
106 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
107 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
108 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
109}
111meetupstatus2api = {
112 None: api_pb2.MEETUP_STATUS_UNKNOWN,
113 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
114 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
115 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
116}
118smokinglocation2sql = {
119 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
120 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
121 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
122 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
123 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
124}
126smokinglocation2api = {
127 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
128 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
129 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
130 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
131 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
132}
134sleepingarrangement2sql = {
135 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
136 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
137 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
138 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
139}
141sleepingarrangement2api = {
142 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
143 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
144 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
145 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
146}
148parkingdetails2sql = {
149 api_pb2.PARKING_DETAILS_UNKNOWN: None,
150 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
151 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
152 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
153 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
154}
156parkingdetails2api = {
157 None: api_pb2.PARKING_DETAILS_UNKNOWN,
158 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
159 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
160 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
161 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
162}
164fluency2sql = {
165 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
166 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
167 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
168 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
169}
171fluency2api = {
172 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
173 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
174 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
175 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
176}
179class API(api_pb2_grpc.APIServicer):
180 def Ping(self, request: api_pb2.PingReq, context: CouchersContext, session: Session) -> api_pb2.PingRes:
181 # auth ought to make sure the user exists
182 user = session.execute(
183 select(User)
184 .where(User.id == context.user_id)
185 .options(
186 selectinload(User.regions_visited),
187 selectinload(User.regions_lived),
188 selectinload(User.language_abilities),
189 )
190 ).scalar_one()
192 sent_reqs_query = select(HostRequest.conversation_id, HostRequest.initiator_last_seen_message_id).where(
193 HostRequest.initiator_user_id == context.user_id
194 )
195 sent_reqs_query = where_users_column_visible(sent_reqs_query, context, HostRequest.recipient_user_id)
196 sent_reqs_query = where_moderated_content_visible(sent_reqs_query, context, HostRequest, is_list_operation=True)
197 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery()
199 unseen_sent_host_request_count = session.execute(
200 select(func.count())
201 .select_from(sent_reqs_last_seen_message_ids)
202 .where(
203 exists(
204 select(1)
205 .where(Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id)
206 .where(Message.id > sent_reqs_last_seen_message_ids.c.initiator_last_seen_message_id)
207 )
208 )
209 ).scalar_one()
211 received_reqs_query = select(HostRequest.conversation_id, HostRequest.recipient_last_seen_message_id).where(
212 HostRequest.recipient_user_id == context.user_id
213 )
214 received_reqs_query = where_users_column_visible(received_reqs_query, context, HostRequest.initiator_user_id)
215 received_reqs_query = where_moderated_content_visible(
216 received_reqs_query, context, HostRequest, is_list_operation=True
217 )
218 received_reqs_last_seen_message_ids = received_reqs_query.subquery()
220 unseen_received_host_request_count = session.execute(
221 select(func.count())
222 .select_from(received_reqs_last_seen_message_ids)
223 .where(
224 exists(
225 select(1)
226 .where(Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id)
227 .where(Message.id > received_reqs_last_seen_message_ids.c.recipient_last_seen_message_id)
228 )
229 )
230 ).scalar_one()
232 # Role-based unread host-request counts. Unlike sent/received above, these
233 # classify by stay-role, so public-trip offers (role reversal) bucket
234 # correctly: the offering host counts under hosting, the traveller under surfing.
235 def role_based_unseen_host_request_count(party_predicate: ColumnElement[bool]) -> int:
236 viewer_last_seen = case(
237 (HostRequest.initiator_user_id == context.user_id, HostRequest.initiator_last_seen_message_id),
238 else_=HostRequest.recipient_last_seen_message_id,
239 )
240 reqs_query = select(HostRequest.conversation_id, viewer_last_seen.label("last_seen")).where(party_predicate)
241 reqs_query = where_users_column_visible(reqs_query, context, HostRequest.initiator_user_id)
242 reqs_query = where_users_column_visible(reqs_query, context, HostRequest.recipient_user_id)
243 reqs_query = where_moderated_content_visible(reqs_query, context, HostRequest, is_list_operation=True)
244 reqs = reqs_query.subquery()
245 return session.execute(
246 select(func.count())
247 .select_from(reqs)
248 .where(
249 exists(
250 select(1)
251 .where(Message.conversation_id == reqs.c.conversation_id)
252 .where(Message.id > reqs.c.last_seen)
253 )
254 )
255 ).scalar_one()
257 me = context.user_id
258 unseen_hosting_host_request_count = role_based_unseen_host_request_count(
259 or_(
260 and_(HostRequest.public_trip_id.is_(None), HostRequest.recipient_user_id == me),
261 and_(HostRequest.public_trip_id.isnot(None), HostRequest.initiator_user_id == me),
262 )
263 )
264 unseen_surfing_host_request_count = role_based_unseen_host_request_count(
265 or_(
266 and_(HostRequest.public_trip_id.is_(None), HostRequest.initiator_user_id == me),
267 and_(HostRequest.public_trip_id.isnot(None), HostRequest.recipient_user_id == me),
268 )
269 )
270 if context.get_boolean_value("public_trips_enabled", False):
271 unseen_public_trip_offer_count = role_based_unseen_host_request_count(
272 and_(HostRequest.public_trip_id.isnot(None), HostRequest.recipient_user_id == me)
273 )
274 else:
275 unseen_public_trip_offer_count = 0
277 unseen_message_query = (
278 select(func.count(Message.id))
279 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
280 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id)
281 )
282 unseen_message_query = where_moderated_content_visible(
283 unseen_message_query, context, GroupChat, is_list_operation=True
284 )
285 unseen_message_query = (
286 unseen_message_query.where(GroupChatSubscription.user_id == context.user_id)
287 .where(Message.time >= GroupChatSubscription.joined)
288 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
289 .where(Message.id > GroupChatSubscription.last_seen_message_id)
290 )
291 unseen_message_count = session.execute(unseen_message_query).scalar_one()
293 pending_friend_request_query = select(func.count(FriendRelationship.id)).where(
294 FriendRelationship.to_user_id == context.user_id
295 )
296 pending_friend_request_query = where_users_column_visible(
297 pending_friend_request_query, context, FriendRelationship.from_user_id
298 )
299 pending_friend_request_query = pending_friend_request_query.where(
300 FriendRelationship.status == FriendStatus.pending
301 )
302 pending_friend_request_query = where_moderated_content_visible(
303 pending_friend_request_query, context, FriendRelationship, is_list_operation=True
304 )
305 pending_friend_request_count = session.execute(pending_friend_request_query).scalar_one()
307 unseen_notification_count = session.execute(
308 select(func.count())
309 .select_from(Notification)
310 .where(Notification.user_id == context.user_id)
311 .where(Notification.is_seen == False)
312 .where(
313 Notification.topic_action.in_(
314 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push)
315 )
316 )
317 .where(moderation_state_column_visible(context, Notification.moderation_state_id))
318 ).scalar_one()
320 return api_pb2.PingRes(
321 user=user_model_to_pb(user, session, context),
322 unseen_message_count=unseen_message_count,
323 unseen_sent_host_request_count=unseen_sent_host_request_count,
324 unseen_received_host_request_count=unseen_received_host_request_count,
325 unseen_hosting_host_request_count=unseen_hosting_host_request_count,
326 unseen_surfing_host_request_count=unseen_surfing_host_request_count,
327 unseen_public_trip_offer_count=unseen_public_trip_offer_count,
328 pending_friend_request_count=pending_friend_request_count,
329 unseen_notification_count=unseen_notification_count,
330 )
332 def GetUser(self, request: api_pb2.GetUserReq, context: CouchersContext, session: Session) -> api_pb2.User:
333 user = session.execute(
334 select(User)
335 .where(username_or_id(request.user))
336 .options(
337 selectinload(User.regions_visited),
338 selectinload(User.regions_lived),
339 selectinload(User.language_abilities),
340 )
341 ).scalar_one_or_none()
343 if not user: 343 ↛ 344line 343 didn't jump to line 344 because the condition on line 343 was never true
344 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
346 return user_model_to_pb(user, session, context, is_get_user_return_ghosts=True)
348 def GetLiteUser(
349 self, request: api_pb2.GetLiteUserReq, context: CouchersContext, session: Session
350 ) -> api_pb2.LiteUser:
351 lite_user = session.execute(
352 select(LiteUser).where(username_or_id(request.user, table=LiteUser))
353 ).scalar_one_or_none()
355 if not lite_user: 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true
356 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
358 return lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True)
360 def GetLiteUsers(
361 self, request: api_pb2.GetLiteUsersReq, context: CouchersContext, session: Session
362 ) -> api_pb2.GetLiteUsersRes:
363 if len(request.users) > MAX_USERS_PER_QUERY:
364 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "requested_too_many_users")
366 usernames = {u for u in request.users if is_valid_username(u)}
367 ids = {int(u) for u in request.users if is_valid_user_id(u)}
369 # decomposed where_username_or_id...
370 users = (
371 session.execute(select(LiteUser).where(or_(LiteUser.username.in_(usernames), LiteUser.id.in_(ids))))
372 .scalars()
373 .all()
374 )
376 users_by_id = {str(user.id): user for user in users}
377 users_by_username = {user.username: user for user in users}
379 res = api_pb2.GetLiteUsersRes()
381 for user in request.users:
382 lite_user = None
383 if user in users_by_id:
384 lite_user = users_by_id[user]
385 elif user in users_by_username:
386 lite_user = users_by_username[user]
388 res.responses.append(
389 api_pb2.LiteUserRes(
390 query=user,
391 not_found=lite_user is None,
392 user=lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True)
393 if lite_user
394 else None,
395 )
396 )
398 return res
400 def UpdateProfile(
401 self, request: api_pb2.UpdateProfileReq, context: CouchersContext, session: Session
402 ) -> empty_pb2.Empty:
403 user: User = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
405 if request.HasField("name"):
406 if not is_valid_name(request.name.value):
407 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_name")
408 user.name = request.name.value
410 if request.HasField("city"):
411 user.city = request.city.value
413 if request.HasField("hometown"):
414 if request.hometown.is_null:
415 user.hometown = None
416 else:
417 user.hometown = request.hometown.value
419 if request.HasField("lat") and request.HasField("lng"):
420 if request.lat.value == 0 and request.lng.value == 0:
421 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate")
422 user.geom = create_coordinate(request.lat.value, request.lng.value)
423 user.randomized_geom = None
425 if request.HasField("radius"):
426 user.geom_radius = request.radius.value
428 # if request.HasField("gender"):
429 # user.gender = request.gender.value
431 if request.HasField("pronouns"):
432 if request.pronouns.is_null:
433 user.pronouns = None
434 else:
435 user.pronouns = request.pronouns.value
437 if request.HasField("occupation"):
438 if request.occupation.is_null:
439 user.occupation = None
440 else:
441 user.occupation = request.occupation.value
443 if request.HasField("education"):
444 if request.education.is_null:
445 user.education = None
446 else:
447 user.education = request.education.value
449 if request.HasField("about_me"):
450 if request.about_me.is_null:
451 user.about_me = None
452 else:
453 user.about_me = request.about_me.value
455 if request.HasField("things_i_like"):
456 if request.things_i_like.is_null:
457 user.things_i_like = None
458 else:
459 user.things_i_like = request.things_i_like.value
461 if request.HasField("about_place"):
462 if request.about_place.is_null:
463 user.about_place = None
464 else:
465 user.about_place = request.about_place.value
467 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
468 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
469 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_host")
470 user.hosting_status = hostingstatus2sql[request.hosting_status] # type: ignore[assignment]
472 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
473 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
474 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_meet")
475 user.meetup_status = meetupstatus2sql[request.meetup_status] # type: ignore[assignment]
477 if request.HasField("language_abilities"):
478 # delete all existing abilities
479 for ability in user.language_abilities:
480 session.delete(ability)
481 session.flush()
483 # add the new ones
484 for language_ability in request.language_abilities.value:
485 if not language_is_allowed(language_ability.code):
486 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_language")
487 session.add(
488 LanguageAbility(
489 user_id=user.id,
490 language_code=language_ability.code,
491 fluency=not_none(fluency2sql[language_ability.fluency]),
492 )
493 )
495 if request.HasField("regions_visited"):
496 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
498 for region in request.regions_visited.value:
499 if not region_is_allowed(region):
500 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region")
501 session.add(
502 RegionVisited(
503 user_id=user.id,
504 region_code=region,
505 )
506 )
508 if request.HasField("regions_lived"):
509 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
511 for region in request.regions_lived.value:
512 if not region_is_allowed(region):
513 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region")
514 session.add(
515 RegionLived(
516 user_id=user.id,
517 region_code=region,
518 )
519 )
521 if request.HasField("additional_information"):
522 if request.additional_information.is_null:
523 user.additional_information = None
524 else:
525 user.additional_information = request.additional_information.value
527 if request.HasField("max_guests"):
528 if request.max_guests.is_null:
529 user.max_guests = None
530 else:
531 user.max_guests = request.max_guests.value
533 if request.HasField("last_minute"):
534 if request.last_minute.is_null:
535 user.last_minute = None
536 else:
537 user.last_minute = request.last_minute.value
539 if request.HasField("has_pets"):
540 if request.has_pets.is_null:
541 user.has_pets = None
542 else:
543 user.has_pets = request.has_pets.value
545 if request.HasField("accepts_pets"):
546 if request.accepts_pets.is_null:
547 user.accepts_pets = None
548 else:
549 user.accepts_pets = request.accepts_pets.value
551 if request.HasField("pet_details"):
552 if request.pet_details.is_null:
553 user.pet_details = None
554 else:
555 user.pet_details = request.pet_details.value
557 if request.HasField("has_kids"):
558 if request.has_kids.is_null:
559 user.has_kids = None
560 else:
561 user.has_kids = request.has_kids.value
563 if request.HasField("accepts_kids"):
564 if request.accepts_kids.is_null:
565 user.accepts_kids = None
566 else:
567 user.accepts_kids = request.accepts_kids.value
569 if request.HasField("kid_details"):
570 if request.kid_details.is_null:
571 user.kid_details = None
572 else:
573 user.kid_details = request.kid_details.value
575 if request.HasField("has_housemates"):
576 if request.has_housemates.is_null:
577 user.has_housemates = None
578 else:
579 user.has_housemates = request.has_housemates.value
581 if request.HasField("housemate_details"):
582 if request.housemate_details.is_null:
583 user.housemate_details = None
584 else:
585 user.housemate_details = request.housemate_details.value
587 if request.HasField("wheelchair_accessible"):
588 if request.wheelchair_accessible.is_null:
589 user.wheelchair_accessible = None
590 else:
591 user.wheelchair_accessible = request.wheelchair_accessible.value
593 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
594 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
596 if request.HasField("smokes_at_home"):
597 if request.smokes_at_home.is_null:
598 user.smokes_at_home = None
599 else:
600 user.smokes_at_home = request.smokes_at_home.value
602 if request.HasField("drinking_allowed"):
603 if request.drinking_allowed.is_null:
604 user.drinking_allowed = None
605 else:
606 user.drinking_allowed = request.drinking_allowed.value
608 if request.HasField("drinks_at_home"):
609 if request.drinks_at_home.is_null:
610 user.drinks_at_home = None
611 else:
612 user.drinks_at_home = request.drinks_at_home.value
614 if request.HasField("other_host_info"):
615 if request.other_host_info.is_null:
616 user.other_host_info = None
617 else:
618 user.other_host_info = request.other_host_info.value
620 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
621 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
623 if request.HasField("sleeping_details"):
624 if request.sleeping_details.is_null:
625 user.sleeping_details = None
626 else:
627 user.sleeping_details = request.sleeping_details.value
629 if request.HasField("area"):
630 if request.area.is_null:
631 user.area = None
632 else:
633 user.area = request.area.value
635 if request.HasField("house_rules"):
636 if request.house_rules.is_null:
637 user.house_rules = None
638 else:
639 user.house_rules = request.house_rules.value
641 if request.HasField("parking"):
642 if request.parking.is_null:
643 user.parking = None
644 else:
645 user.parking = request.parking.value
647 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
648 user.parking_details = parkingdetails2sql[request.parking_details]
650 if request.HasField("camping_ok"):
651 if request.camping_ok.is_null:
652 user.camping_ok = None
653 else:
654 user.camping_ok = request.camping_ok.value
656 user.profile_last_updated = now()
658 return empty_pb2.Empty()
660 def ListFriends(
661 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
662 ) -> api_pb2.ListFriendsRes:
663 rels_query = select(FriendRelationship)
664 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.from_user_id)
665 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.to_user_id)
666 rels_query = rels_query.where(
667 or_(
668 FriendRelationship.from_user_id == context.user_id,
669 FriendRelationship.to_user_id == context.user_id,
670 )
671 ).where(FriendRelationship.status == FriendStatus.accepted)
672 rels_query = where_moderated_content_visible(rels_query, context, FriendRelationship, is_list_operation=True)
673 rels = session.execute(rels_query).scalars().all()
674 return api_pb2.ListFriendsRes(
675 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
676 )
678 def RemoveFriend(
679 self, request: api_pb2.RemoveFriendReq, context: CouchersContext, session: Session
680 ) -> empty_pb2.Empty:
681 rel_query = select(FriendRelationship)
682 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.from_user_id)
683 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.to_user_id)
684 rel_query = rel_query.where(
685 or_(
686 and_(
687 FriendRelationship.from_user_id == request.user_id,
688 FriendRelationship.to_user_id == context.user_id,
689 ),
690 and_(
691 FriendRelationship.from_user_id == context.user_id,
692 FriendRelationship.to_user_id == request.user_id,
693 ),
694 )
695 ).where(FriendRelationship.status == FriendStatus.accepted)
696 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship)
697 rel = session.execute(rel_query).scalar_one_or_none()
699 if not rel:
700 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "not_friends")
702 session.delete(rel)
703 log_event(context, session, "friendship.removed", {"other_user_id": request.user_id})
705 return empty_pb2.Empty()
707 def ListMutualFriends(
708 self, request: api_pb2.ListMutualFriendsReq, context: CouchersContext, session: Session
709 ) -> api_pb2.ListMutualFriendsRes:
710 if context.user_id == request.user_id:
711 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
713 user = session.execute(
714 select(User).where(users_visible(context)).where(User.id == request.user_id)
715 ).scalar_one_or_none()
717 if not user: 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true
718 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
720 q1 = where_moderated_content_visible(
721 select(FriendRelationship.from_user_id.label("user_id"))
722 .where(FriendRelationship.to_user_id == context.user_id)
723 .where(FriendRelationship.from_user_id != request.user_id)
724 .where(FriendRelationship.status == FriendStatus.accepted),
725 context,
726 FriendRelationship,
727 )
729 q2 = where_moderated_content_visible(
730 select(FriendRelationship.to_user_id.label("user_id"))
731 .where(FriendRelationship.from_user_id == context.user_id)
732 .where(FriendRelationship.to_user_id != request.user_id)
733 .where(FriendRelationship.status == FriendStatus.accepted),
734 context,
735 FriendRelationship,
736 )
738 q3 = where_moderated_content_visible(
739 select(FriendRelationship.from_user_id.label("user_id"))
740 .where(FriendRelationship.to_user_id == request.user_id)
741 .where(FriendRelationship.from_user_id != context.user_id)
742 .where(FriendRelationship.status == FriendStatus.accepted),
743 context,
744 FriendRelationship,
745 )
747 q4 = where_moderated_content_visible(
748 select(FriendRelationship.to_user_id.label("user_id"))
749 .where(FriendRelationship.from_user_id == request.user_id)
750 .where(FriendRelationship.to_user_id != context.user_id)
751 .where(FriendRelationship.status == FriendStatus.accepted),
752 context,
753 FriendRelationship,
754 )
756 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
758 mutual_friends = (
759 session.execute(select(User).where(users_visible(context)).where(User.id.in_(mutual))).scalars().all()
760 )
762 return api_pb2.ListMutualFriendsRes(
763 mutual_friends=[
764 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
765 for mutual_friend in mutual_friends
766 ]
767 )
769 def SendFriendRequest(
770 self, request: api_pb2.SendFriendRequestReq, context: CouchersContext, session: Session
771 ) -> empty_pb2.Empty:
772 if context.user_id == request.user_id: 772 ↛ 773line 772 didn't jump to line 773 because the condition on line 772 was never true
773 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_friend_self")
775 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
776 if not has_completed_profile(session, user):
777 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_send_friend_request")
779 to_user = session.execute(
780 select(User).where(users_visible(context)).where(User.id == request.user_id)
781 ).scalar_one_or_none()
783 if not to_user: 783 ↛ 784line 783 didn't jump to line 784 because the condition on line 783 was never true
784 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
786 if (
787 session.execute(
788 select(FriendRelationship)
789 .where(
790 or_(
791 and_(
792 FriendRelationship.from_user_id == context.user_id,
793 FriendRelationship.to_user_id == request.user_id,
794 ),
795 and_(
796 FriendRelationship.from_user_id == request.user_id,
797 FriendRelationship.to_user_id == context.user_id,
798 ),
799 )
800 )
801 .where(
802 or_(
803 FriendRelationship.status == FriendStatus.accepted,
804 FriendRelationship.status == FriendStatus.pending,
805 )
806 )
807 ).scalar_one_or_none()
808 is not None
809 ):
810 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "friends_already_or_pending")
812 # Check if user has been sending friend requests excessively
813 if process_rate_limits_and_check_abort(
814 session=session, user_id=context.user_id, action=RateLimitAction.friend_request
815 ):
816 context.abort_with_error_code(
817 grpc.StatusCode.RESOURCE_EXHAUSTED,
818 "friend_request_rate_limit2",
819 substitutions={"count": RATE_LIMIT_HOURS},
820 )
822 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
824 # Use callback pattern to handle circular dependency between FriendRelationship and ModerationState
825 friend_relationship = None
827 def create_friend_relationship(moderation_state_id: int) -> int:
828 nonlocal friend_relationship
829 friend_relationship = FriendRelationship(
830 from_user_id=user.id,
831 to_user_id=to_user.id,
832 status=FriendStatus.pending,
833 moderation_state_id=moderation_state_id,
834 )
835 session.add(friend_relationship)
836 session.flush()
837 return friend_relationship.id
839 moderation_state = create_moderation(
840 session, ModerationObjectType.friend_request, create_friend_relationship, context.user_id
841 )
843 assert friend_relationship is not None # set by create_friend_relationship callback
845 notify(
846 session,
847 user_id=friend_relationship.to_user_id,
848 topic_action=NotificationTopicAction.friend_request__create,
849 key=str(friend_relationship.from_user_id),
850 data=notification_data_pb2.FriendRequestCreate(
851 other_user=user_model_to_pb(
852 friend_relationship.from_user,
853 session,
854 make_notification_user_context(user_id=friend_relationship.to_user_id),
855 ),
856 ),
857 moderation_state_id=moderation_state.id,
858 )
859 log_event(context, session, "friendship.request_sent", {"to_user_id": to_user.id})
861 return empty_pb2.Empty()
863 def ListFriendRequests(
864 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
865 ) -> api_pb2.ListFriendRequestsRes:
866 # both sent and received
867 sent_requests_query = select(FriendRelationship)
868 sent_requests_query = where_users_column_visible(sent_requests_query, context, FriendRelationship.to_user_id)
869 sent_requests_query = sent_requests_query.where(FriendRelationship.from_user_id == context.user_id).where(
870 FriendRelationship.status == FriendStatus.pending
871 )
872 sent_requests_query = where_moderated_content_visible(
873 sent_requests_query, context, FriendRelationship, is_list_operation=True
874 )
875 sent_requests = session.execute(sent_requests_query).scalars().all()
877 received_requests_query = select(FriendRelationship)
878 received_requests_query = where_users_column_visible(
879 received_requests_query, context, FriendRelationship.from_user_id
880 )
881 received_requests_query = received_requests_query.where(FriendRelationship.to_user_id == context.user_id).where(
882 FriendRelationship.status == FriendStatus.pending
883 )
884 received_requests_query = where_moderated_content_visible(
885 received_requests_query, context, FriendRelationship, is_list_operation=True
886 )
887 received_requests = session.execute(received_requests_query).scalars().all()
889 return api_pb2.ListFriendRequestsRes(
890 sent=[
891 api_pb2.FriendRequest(
892 friend_request_id=friend_request.id,
893 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
894 user_id=friend_request.to_user.id,
895 sent=True,
896 )
897 for friend_request in sent_requests
898 ],
899 received=[
900 api_pb2.FriendRequest(
901 friend_request_id=friend_request.id,
902 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
903 user_id=friend_request.from_user.id,
904 sent=False,
905 )
906 for friend_request in received_requests
907 ],
908 )
910 def RespondFriendRequest(
911 self, request: api_pb2.RespondFriendRequestReq, context: CouchersContext, session: Session
912 ) -> empty_pb2.Empty:
913 friend_request_query = select(FriendRelationship)
914 friend_request_query = where_users_column_visible(
915 friend_request_query, context, FriendRelationship.from_user_id
916 )
917 friend_request_query = (
918 friend_request_query.where(FriendRelationship.to_user_id == context.user_id)
919 .where(FriendRelationship.status == FriendStatus.pending)
920 .where(FriendRelationship.id == request.friend_request_id)
921 )
922 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship)
923 friend_request = session.execute(friend_request_query).scalar_one_or_none()
925 if not friend_request: 925 ↛ 926line 925 didn't jump to line 926 because the condition on line 925 was never true
926 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found")
928 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
929 friend_request.time_responded = func.now()
931 session.flush()
933 if friend_request.status == FriendStatus.accepted:
934 notify(
935 session,
936 user_id=friend_request.from_user_id,
937 topic_action=NotificationTopicAction.friend_request__accept,
938 key=str(friend_request.to_user_id),
939 data=notification_data_pb2.FriendRequestAccept(
940 other_user=user_model_to_pb(
941 friend_request.to_user,
942 session,
943 make_notification_user_context(user_id=friend_request.from_user_id),
944 ),
945 ),
946 )
948 log_event(
949 context,
950 session,
951 "friendship.request_responded",
952 {"from_user_id": friend_request.from_user_id, "accepted": request.accept},
953 )
955 return empty_pb2.Empty()
957 def CancelFriendRequest(
958 self, request: api_pb2.CancelFriendRequestReq, context: CouchersContext, session: Session
959 ) -> empty_pb2.Empty:
960 friend_request_query = select(FriendRelationship)
961 friend_request_query = where_users_column_visible(friend_request_query, context, FriendRelationship.to_user_id)
962 friend_request_query = (
963 friend_request_query.where(FriendRelationship.from_user_id == context.user_id)
964 .where(FriendRelationship.status == FriendStatus.pending)
965 .where(FriendRelationship.id == request.friend_request_id)
966 )
967 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship)
968 friend_request = session.execute(friend_request_query).scalar_one_or_none()
970 if not friend_request: 970 ↛ 971line 970 didn't jump to line 971 because the condition on line 970 was never true
971 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found")
973 friend_request.status = FriendStatus.cancelled
974 friend_request.time_responded = func.now()
976 # note no notifications
977 log_event(context, session, "friendship.request_cancelled", {"to_user_id": friend_request.to_user_id})
979 session.commit()
981 return empty_pb2.Empty()
983 def InitiateMediaUpload(
984 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
985 ) -> api_pb2.InitiateMediaUploadRes:
986 key = random_hex()
988 created = now()
989 expiry = created + timedelta(minutes=20)
991 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
992 session.add(upload)
993 session.commit()
995 req = media_pb2.UploadRequest(
996 key=upload.key,
997 type=media_pb2.UploadRequest.UploadType.IMAGE,
998 created=Timestamp_from_datetime(upload.created),
999 expiry=Timestamp_from_datetime(upload.expiry),
1000 max_width=2000,
1001 max_height=1600,
1002 ).SerializeToString()
1004 data = b64encode(req)
1005 sig = b64encode(generate_hash_signature(req, config.MEDIA_SERVER_SECRET_KEY))
1007 path = "upload?" + urlencode({"data": data, "sig": sig})
1009 return api_pb2.InitiateMediaUploadRes(
1010 upload_url=urls.media_upload_url(path=path),
1011 expiry=Timestamp_from_datetime(expiry),
1012 )
1014 def ListBadgeUsers(
1015 self, request: api_pb2.ListBadgeUsersReq, context: CouchersContext, session: Session
1016 ) -> api_pb2.ListBadgeUsersRes:
1017 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
1018 next_user_id = int(request.page_token) if request.page_token else 0
1019 badge = get_badge_dict().get(request.badge_id)
1020 if not badge: 1020 ↛ 1021line 1020 didn't jump to line 1021 because the condition on line 1020 was never true
1021 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "badge_not_found")
1023 badge_user_ids_query = (
1024 select(UserBadge.user_id).where(UserBadge.badge_id == badge.id).where(UserBadge.user_id >= next_user_id)
1025 )
1026 badge_user_ids_query = where_users_column_visible(badge_user_ids_query, context, UserBadge.user_id)
1027 badge_user_ids_query = badge_user_ids_query.order_by(UserBadge.user_id).limit(page_size + 1)
1028 badge_user_ids = session.execute(badge_user_ids_query).scalars().all()
1030 return api_pb2.ListBadgeUsersRes(
1031 user_ids=badge_user_ids[:page_size],
1032 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
1033 )
1036def response_rate_to_pb(response_rate: UserResponseRate | None) -> dict[str, google.protobuf.message.Message]:
1037 if not response_rate:
1038 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
1040 # if n is None, the user is new, or they have no requests
1041 if response_rate.requests < 3:
1042 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
1044 if response_rate.response_rate <= 0.33:
1045 return {"low": requests_pb2.ResponseRateLow()}
1047 response_time_p33_coarsened = Duration_from_timedelta(
1048 timedelta(seconds=round(response_rate.response_time_33p.total_seconds() / 60) * 60)
1049 )
1051 if response_rate.response_rate <= 0.66:
1052 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
1054 response_time_p66_coarsened = Duration_from_timedelta(
1055 timedelta(seconds=round(response_rate.response_time_66p.total_seconds() / 60) * 60)
1056 )
1058 if response_rate.response_rate <= 0.90:
1059 return {
1060 "most": requests_pb2.ResponseRateMost(
1061 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
1062 )
1063 }
1064 else:
1065 return {
1066 "almost_all": requests_pb2.ResponseRateAlmostAll(
1067 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
1068 )
1069 }
1072def get_num_references(session: Session, context: CouchersContext, user_ids: Iterable[int]) -> dict[int, int]:
1073 query = where_moderated_content_visible(
1074 select(Reference.to_user_id, func.count(Reference.id)), context, Reference, is_list_operation=True
1075 )
1076 query = (
1077 query.where(Reference.to_user_id.in_(user_ids))
1078 .join(User, User.id == Reference.from_user_id)
1079 .where(User.is_visible)
1080 .group_by(Reference.to_user_id)
1081 )
1082 return cast(dict[int, int], dict(session.execute(query).all())) # type: ignore[arg-type]
1085def user_model_to_pb(
1086 db_user: User,
1087 session: Session,
1088 context: CouchersContext,
1089 *,
1090 is_admin_see_ghosts: bool = False,
1091 is_get_user_return_ghosts: bool = False,
1092) -> api_pb2.User:
1093 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
1094 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context
1096 viewer_user_id = context.user_id if context.is_logged_in() else None
1097 if not is_admin_see_ghosts and is_not_visible(
1098 session, viewer_user_id, db_user.id, ignore_shadow=context.serialize_shadowed
1099 ):
1100 # User is not visible (deleted, banned, or blocked)
1101 if is_get_user_return_ghosts: 1101 ↛ 1116line 1101 didn't jump to line 1116 because the condition on line 1101 was always true
1102 maybe_log_nonvisible_user_access(
1103 context,
1104 db_user,
1105 access_type=NonvisibleUserAccessType.ghost_served,
1106 actor_user_id=viewer_user_id,
1107 )
1108 # Return an anonymized "ghost" user profile
1109 return api_pb2.User(
1110 user_id=db_user.id,
1111 is_ghost=True,
1112 username=GHOST_USERNAME,
1113 name=context.localization.localize_string("ghost_users.display_name"),
1114 about_me=context.localization.localize_string("ghost_users.about_me"),
1115 )
1116 raise GhostUserSerializationError(
1117 f"Tried to serialize ghost profile in user_model_to_pb without appropriate flags. "
1118 f"Context user_id: {context.user_id}, db_user id: {db_user.id} (username: {db_user.username})"
1119 )
1121 num_references = get_num_references(session, context, [db_user.id]).get(db_user.id, 0)
1122 lat, lng = db_user.coordinates
1124 pending_friend_request = None
1125 if context.is_logged_out() or db_user.id == context.user_id:
1126 friends_status = api_pb2.User.FriendshipStatus.NA
1127 else:
1128 friend_relationship = session.execute(
1129 where_moderated_content_visible(
1130 select(FriendRelationship)
1131 .where(
1132 or_(
1133 and_(
1134 FriendRelationship.from_user_id == context.user_id,
1135 FriendRelationship.to_user_id == db_user.id,
1136 ),
1137 and_(
1138 FriendRelationship.from_user_id == db_user.id,
1139 FriendRelationship.to_user_id == context.user_id,
1140 ),
1141 )
1142 )
1143 .where(
1144 or_(
1145 FriendRelationship.status == FriendStatus.accepted,
1146 FriendRelationship.status == FriendStatus.pending,
1147 )
1148 ),
1149 context,
1150 FriendRelationship,
1151 )
1152 ).scalar_one_or_none()
1154 if friend_relationship:
1155 if friend_relationship.status == FriendStatus.accepted:
1156 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
1157 else:
1158 friends_status = api_pb2.User.FriendshipStatus.PENDING
1159 if friend_relationship.from_user_id == context.user_id: 1159 ↛ 1161line 1159 didn't jump to line 1161 because the condition on line 1159 was never true
1160 # we sent it
1161 pending_friend_request = api_pb2.FriendRequest(
1162 friend_request_id=friend_relationship.id,
1163 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
1164 user_id=friend_relationship.to_user.id,
1165 sent=True,
1166 )
1167 else:
1168 # we received it
1169 pending_friend_request = api_pb2.FriendRequest(
1170 friend_request_id=friend_relationship.id,
1171 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
1172 user_id=friend_relationship.from_user.id,
1173 sent=False,
1174 )
1175 else:
1176 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
1178 response_rate = session.execute(
1179 select(UserResponseRate).where(UserResponseRate.user_id == db_user.id)
1180 ).scalar_one_or_none()
1182 avatar_upload = get_avatar_upload(session, db_user)
1184 verification_score = 0.0
1185 if db_user.phone_verification_verified:
1186 verification_score += 1.0 * db_user.phone_is_verified
1188 user = api_pb2.User(
1189 user_id=db_user.id,
1190 username=db_user.username,
1191 name=db_user.name,
1192 city=db_user.city,
1193 hometown=db_user.hometown,
1194 timezone=db_user.timezone,
1195 lat=lat,
1196 lng=lng,
1197 radius=db_user.geom_radius,
1198 verification=verification_score,
1199 community_standing=db_user.community_standing,
1200 num_references=num_references,
1201 gender=db_user.gender,
1202 pronouns=db_user.pronouns,
1203 age=int(db_user.age),
1204 joined=Timestamp_from_datetime(db_user.display_joined),
1205 last_active=Timestamp_from_datetime(db_user.display_last_active),
1206 hosting_status=hostingstatus2api[db_user.hosting_status],
1207 meetup_status=meetupstatus2api[db_user.meetup_status],
1208 occupation=db_user.occupation,
1209 education=db_user.education,
1210 about_me=db_user.about_me,
1211 things_i_like=db_user.things_i_like,
1212 about_place=db_user.about_place,
1213 language_abilities=[
1214 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
1215 for ability in db_user.language_abilities
1216 ],
1217 regions_visited=[region.code for region in db_user.regions_visited],
1218 regions_lived=[region.code for region in db_user.regions_lived],
1219 additional_information=db_user.additional_information,
1220 friends=friends_status,
1221 pending_friend_request=pending_friend_request,
1222 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
1223 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
1224 parking_details=parkingdetails2api[db_user.parking_details],
1225 avatar_url=avatar_upload.full_url if avatar_upload else None,
1226 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None,
1227 profile_gallery_id=db_user.profile_gallery_id,
1228 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id))
1229 .scalars()
1230 .all(),
1231 **get_strong_verification_fields(session, db_user),
1232 **response_rate_to_pb(response_rate), # type: ignore[arg-type]
1233 )
1235 if db_user.max_guests is not None:
1236 user.max_guests.value = db_user.max_guests
1238 if db_user.last_minute is not None:
1239 user.last_minute.value = db_user.last_minute
1241 if db_user.has_pets is not None:
1242 user.has_pets.value = db_user.has_pets
1244 if db_user.accepts_pets is not None:
1245 user.accepts_pets.value = db_user.accepts_pets
1247 if db_user.pet_details is not None:
1248 user.pet_details.value = db_user.pet_details
1250 if db_user.has_kids is not None:
1251 user.has_kids.value = db_user.has_kids
1253 if db_user.accepts_kids is not None:
1254 user.accepts_kids.value = db_user.accepts_kids
1256 if db_user.kid_details is not None:
1257 user.kid_details.value = db_user.kid_details
1259 if db_user.has_housemates is not None:
1260 user.has_housemates.value = db_user.has_housemates
1262 if db_user.housemate_details is not None:
1263 user.housemate_details.value = db_user.housemate_details
1265 if db_user.wheelchair_accessible is not None:
1266 user.wheelchair_accessible.value = db_user.wheelchair_accessible
1268 if db_user.smokes_at_home is not None:
1269 user.smokes_at_home.value = db_user.smokes_at_home
1271 if db_user.drinking_allowed is not None:
1272 user.drinking_allowed.value = db_user.drinking_allowed
1274 if db_user.drinks_at_home is not None:
1275 user.drinks_at_home.value = db_user.drinks_at_home
1277 if db_user.other_host_info is not None:
1278 user.other_host_info.value = db_user.other_host_info
1280 if db_user.sleeping_details is not None:
1281 user.sleeping_details.value = db_user.sleeping_details
1283 if db_user.area is not None:
1284 user.area.value = db_user.area
1286 if db_user.house_rules is not None:
1287 user.house_rules.value = db_user.house_rules
1289 if db_user.parking is not None:
1290 user.parking.value = db_user.parking
1292 if db_user.camping_ok is not None:
1293 user.camping_ok.value = db_user.camping_ok
1295 return user
1298def lite_user_to_pb(
1299 session: Session,
1300 lite_user: LiteUser,
1301 context: CouchersContext,
1302 *,
1303 is_admin_see_ghosts: bool = False,
1304 is_get_user_return_ghosts: bool = False,
1305) -> api_pb2.LiteUser:
1306 if not is_admin_see_ghosts and is_not_visible(
1307 session, context.user_id, lite_user.id, ignore_shadow=context.serialize_shadowed
1308 ):
1309 # User is not visible (deleted, banned, or blocked)
1310 if is_get_user_return_ghosts: 1310 ↛ 1318line 1310 didn't jump to line 1318 because the condition on line 1310 was always true
1311 # Return an anonymized "ghost" user profile
1312 return api_pb2.LiteUser(
1313 user_id=lite_user.id,
1314 is_ghost=True,
1315 username=GHOST_USERNAME,
1316 name=context.localization.localize_string("ghost_users.display_name"),
1317 )
1318 raise GhostUserSerializationError(
1319 f"Tried to serialize ghost profile in lite_user_to_pb without appropriate flags. "
1320 f"Context user_id: {context.user_id}, lite_user id: {lite_user.id} (username: {lite_user.username})"
1321 )
1323 lat, lng = get_coordinates(lite_user.geom)
1325 return api_pb2.LiteUser(
1326 user_id=lite_user.id,
1327 username=lite_user.username,
1328 name=lite_user.name,
1329 city=lite_user.city,
1330 age=int(lite_user.age),
1331 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1332 if lite_user.avatar_filename
1333 else None,
1334 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1335 if lite_user.avatar_filename
1336 else None,
1337 lat=lat,
1338 lng=lng,
1339 radius=lite_user.radius,
1340 has_strong_verification=lite_user.has_strong_verification,
1341 )