Coverage for app/backend/src/couchers/servicers/api.py: 97%
479 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
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 select
10from sqlalchemy.orm import Session, selectinload, undefer
11from sqlalchemy.sql import and_, case, delete, 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.group_chats import is_newest_subscription, is_unseen
22from couchers.helpers.host_requests import (
23 has_unseen_host_request_messages,
24 is_hosting_party,
25 is_public_trip_offer_recipient,
26 is_surfing_party,
27)
28from couchers.helpers.hosting_meetup_status import record_hosting_meetup_status
29from couchers.helpers.references import where_reference_user_visible, where_references_not_hidden_by_reciprocity
30from couchers.helpers.strong_verification import get_strong_verification_fields
31from couchers.materialized_views import LiteUser, UserResponseRate
32from couchers.models import (
33 FriendRelationship,
34 FriendStatus,
35 GroupChat,
36 GroupChatSubscription,
37 HostingMeetupStatusSource,
38 HostingStatus,
39 HostRequest,
40 InitiatedUpload,
41 LanguageAbility,
42 LanguageFluency,
43 MeetupStatus,
44 Message,
45 ModerationObjectType,
46 NonvisibleUserAccessType,
47 Notification,
48 NotificationDeliveryType,
49 ParkingDetails,
50 RateLimitAction,
51 Reference,
52 RegionLived,
53 RegionVisited,
54 SleepingArrangement,
55 SmokingLocation,
56 User,
57 UserBadge,
58)
59from couchers.models.notifications import NotificationTopicAction
60from couchers.models.uploads import get_avatar_upload
61from couchers.moderation.utils import create_moderation
62from couchers.notifications.notify import notify
63from couchers.notifications.settings import get_topic_actions_by_delivery_type
64from couchers.proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
65from couchers.rate_limits.check import process_rate_limits_and_check_abort
66from couchers.rate_limits.definitions import RATE_LIMIT_HOURS
67from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed
68from couchers.servicers.blocking import is_not_visible
69from couchers.sql import (
70 moderation_state_column_visible,
71 username_or_id,
72 users_visible,
73 where_moderated_content_visible,
74 where_users_column_visible,
75)
76from couchers.utils import (
77 Duration_from_timedelta,
78 Timestamp_from_datetime,
79 create_coordinate,
80 get_coordinates,
81 is_valid_name,
82 is_valid_user_id,
83 is_valid_username,
84 not_none,
85 now,
86)
89class GhostUserSerializationError(Exception):
90 """
91 Raised when attempting to serialize a ghost user (deleted/banned/blocked)
92 """
94 pass
97MAX_USERS_PER_QUERY = 200
98MAX_PAGINATION_LENGTH = 50
100hostingstatus2sql = {
101 api_pb2.HOSTING_STATUS_UNKNOWN: None,
102 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
103 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
104 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
105}
107hostingstatus2api = {
108 None: api_pb2.HOSTING_STATUS_UNKNOWN,
109 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
110 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
111 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
112}
114meetupstatus2sql = {
115 api_pb2.MEETUP_STATUS_UNKNOWN: None,
116 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
117 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
118 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
119}
121meetupstatus2api = {
122 None: api_pb2.MEETUP_STATUS_UNKNOWN,
123 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
124 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
125 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
126}
128smokinglocation2sql = {
129 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
130 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
131 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
132 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
133 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
134}
136smokinglocation2api = {
137 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
138 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
139 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
140 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
141 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
142}
144sleepingarrangement2sql = {
145 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
146 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
147 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
148 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
149}
151sleepingarrangement2api = {
152 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
153 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
154 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
155 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
156}
158parkingdetails2sql = {
159 api_pb2.PARKING_DETAILS_UNKNOWN: None,
160 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
161 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
162 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
163 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
164}
166parkingdetails2api = {
167 None: api_pb2.PARKING_DETAILS_UNKNOWN,
168 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
169 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
170 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
171 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
172}
174fluency2sql = {
175 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
176 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
177 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
178 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
179}
181fluency2api = {
182 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
183 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
184 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
185 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
186}
189class API(api_pb2_grpc.APIServicer):
190 def Ping(self, request: api_pb2.PingReq, context: CouchersContext, session: Session) -> api_pb2.PingRes:
191 # auth ought to make sure the user exists
192 user = session.execute(
193 select(User)
194 .where(User.id == context.user_id)
195 .options(
196 selectinload(User.regions_visited),
197 selectinload(User.regions_lived),
198 selectinload(User.language_abilities),
199 # deferred, and user_model_to_pb below reads it; loading it here saves a round trip on
200 # what is the most frequently polled endpoint we have
201 undefer(User.timezone),
202 )
203 ).scalar_one()
205 # One pass over the requests the viewer is a party to that have unread messages, tallied both by
206 # direction (sent/received) and by stay-role. The role-based tallies bucket public-trip offers
207 # correctly despite the role reversal: the offering host counts under hosting, the traveller
208 # under surfing.
209 other_party_user_id = case(
210 (HostRequest.initiator_user_id == context.user_id, HostRequest.recipient_user_id),
211 else_=HostRequest.initiator_user_id,
212 )
213 unseen_host_request_counts_query = (
214 select(
215 func.count().filter(HostRequest.initiator_user_id == context.user_id).label("sent"),
216 func.count().filter(HostRequest.recipient_user_id == context.user_id).label("received"),
217 func.count().filter(is_hosting_party(context.user_id)).label("hosting"),
218 func.count().filter(is_surfing_party(context.user_id)).label("surfing"),
219 func.count().filter(is_public_trip_offer_recipient(context.user_id)).label("public_trip_offer"),
220 )
221 .select_from(HostRequest)
222 .where(
223 or_(
224 HostRequest.initiator_user_id == context.user_id,
225 HostRequest.recipient_user_id == context.user_id,
226 )
227 )
228 .where(has_unseen_host_request_messages(context.user_id))
229 )
230 unseen_host_request_counts_query = where_users_column_visible(
231 unseen_host_request_counts_query, context, other_party_user_id
232 )
233 unseen_host_request_counts_query = where_moderated_content_visible(
234 unseen_host_request_counts_query, context, HostRequest, is_list_operation=True
235 )
236 unseen_host_request_counts = session.execute(unseen_host_request_counts_query).one()
238 unseen_public_trip_offer_count = (
239 unseen_host_request_counts.public_trip_offer
240 if context.get_boolean_value("public_trips_enabled", False)
241 else 0
242 )
244 unseen_message_query = (
245 select(func.count(Message.id))
246 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
247 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id)
248 )
249 unseen_message_query = where_moderated_content_visible(
250 unseen_message_query, context, GroupChat, is_list_operation=True
251 )
252 unseen_message_query = (
253 unseen_message_query.where(GroupChatSubscription.user_id == context.user_id)
254 .where(is_newest_subscription(context.user_id))
255 .where(is_unseen(Message, GroupChatSubscription))
256 )
257 unseen_message_count = session.execute(unseen_message_query).scalar_one()
259 pending_friend_request_query = select(func.count(FriendRelationship.id)).where(
260 FriendRelationship.to_user_id == context.user_id
261 )
262 pending_friend_request_query = where_users_column_visible(
263 pending_friend_request_query, context, FriendRelationship.from_user_id
264 )
265 pending_friend_request_query = pending_friend_request_query.where(
266 FriendRelationship.status == FriendStatus.pending
267 )
268 pending_friend_request_query = where_moderated_content_visible(
269 pending_friend_request_query, context, FriendRelationship, is_list_operation=True
270 )
271 pending_friend_request_count = session.execute(pending_friend_request_query).scalar_one()
273 unseen_notification_count = session.execute(
274 select(func.count())
275 .select_from(Notification)
276 .where(Notification.user_id == context.user_id)
277 .where(Notification.is_seen == False)
278 .where(
279 Notification.topic_action.in_(
280 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push)
281 )
282 )
283 .where(moderation_state_column_visible(context, Notification.moderation_state_id))
284 ).scalar_one()
286 return api_pb2.PingRes(
287 user=user_model_to_pb(user, session, context),
288 unseen_message_count=unseen_message_count,
289 unseen_sent_host_request_count=unseen_host_request_counts.sent,
290 unseen_received_host_request_count=unseen_host_request_counts.received,
291 unseen_hosting_host_request_count=unseen_host_request_counts.hosting,
292 unseen_surfing_host_request_count=unseen_host_request_counts.surfing,
293 unseen_public_trip_offer_count=unseen_public_trip_offer_count,
294 pending_friend_request_count=pending_friend_request_count,
295 unseen_notification_count=unseen_notification_count,
296 )
298 def GetUser(self, request: api_pb2.GetUserReq, context: CouchersContext, session: Session) -> api_pb2.User:
299 user = session.execute(
300 select(User)
301 .where(username_or_id(request.user))
302 .options(
303 selectinload(User.regions_visited),
304 selectinload(User.regions_lived),
305 selectinload(User.language_abilities),
306 )
307 ).scalar_one_or_none()
309 if not user: 309 ↛ 310line 309 didn't jump to line 310 because the condition on line 309 was never true
310 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
312 return user_model_to_pb(user, session, context, is_get_user_return_ghosts=True)
314 def GetLiteUser(
315 self, request: api_pb2.GetLiteUserReq, context: CouchersContext, session: Session
316 ) -> api_pb2.LiteUser:
317 lite_user = session.execute(
318 select(LiteUser).where(username_or_id(request.user, table=LiteUser))
319 ).scalar_one_or_none()
321 if not lite_user: 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true
322 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
324 return lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True)
326 def GetLiteUsers(
327 self, request: api_pb2.GetLiteUsersReq, context: CouchersContext, session: Session
328 ) -> api_pb2.GetLiteUsersRes:
329 if len(request.users) > MAX_USERS_PER_QUERY:
330 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "requested_too_many_users")
332 usernames = {u for u in request.users if is_valid_username(u)}
333 ids = {int(u) for u in request.users if is_valid_user_id(u)}
335 # decomposed where_username_or_id...
336 users = (
337 session.execute(select(LiteUser).where(or_(LiteUser.username.in_(usernames), LiteUser.id.in_(ids))))
338 .scalars()
339 .all()
340 )
342 users_by_id = {str(user.id): user for user in users}
343 users_by_username = {user.username: user for user in users}
345 res = api_pb2.GetLiteUsersRes()
347 for user in request.users:
348 lite_user = None
349 if user in users_by_id:
350 lite_user = users_by_id[user]
351 elif user in users_by_username:
352 lite_user = users_by_username[user]
354 res.responses.append(
355 api_pb2.LiteUserRes(
356 query=user,
357 not_found=lite_user is None,
358 user=lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True)
359 if lite_user
360 else None,
361 )
362 )
364 return res
366 def UpdateProfile(
367 self, request: api_pb2.UpdateProfileReq, context: CouchersContext, session: Session
368 ) -> empty_pb2.Empty:
369 user: User = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
371 if request.HasField("name"):
372 if not is_valid_name(request.name.value):
373 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_name")
374 user.name = request.name.value
376 if request.HasField("city"):
377 user.city = request.city.value
379 if request.HasField("hometown"):
380 if request.hometown.is_null:
381 user.hometown = None
382 else:
383 user.hometown = request.hometown.value
385 if request.HasField("lat") and request.HasField("lng"):
386 if request.lat.value == 0 and request.lng.value == 0:
387 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate")
388 user.geom = create_coordinate(request.lat.value, request.lng.value)
389 user.randomized_geom = None
391 if request.HasField("radius"):
392 user.geom_radius = request.radius.value
394 # if request.HasField("gender"):
395 # user.gender = request.gender.value
397 if request.HasField("pronouns"):
398 if request.pronouns.is_null:
399 user.pronouns = None
400 else:
401 user.pronouns = request.pronouns.value
403 if request.HasField("occupation"):
404 if request.occupation.is_null:
405 user.occupation = None
406 else:
407 user.occupation = request.occupation.value
409 if request.HasField("education"):
410 if request.education.is_null:
411 user.education = None
412 else:
413 user.education = request.education.value
415 if request.HasField("about_me"):
416 if request.about_me.is_null:
417 user.about_me = None
418 else:
419 user.about_me = request.about_me.value
421 if request.HasField("things_i_like"):
422 if request.things_i_like.is_null:
423 user.things_i_like = None
424 else:
425 user.things_i_like = request.things_i_like.value
427 if request.HasField("about_place"):
428 if request.about_place.is_null:
429 user.about_place = None
430 else:
431 user.about_place = request.about_place.value
433 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
434 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
435 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_host")
436 user.hosting_status = hostingstatus2sql[request.hosting_status] # type: ignore[assignment]
438 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
439 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
440 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_meet")
441 user.meetup_status = meetupstatus2sql[request.meetup_status] # type: ignore[assignment]
443 if (
444 request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED
445 or request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED
446 ):
447 record_hosting_meetup_status(session, user, HostingMeetupStatusSource.profile_edit)
449 if request.HasField("language_abilities"):
450 # delete all existing abilities
451 for ability in user.language_abilities:
452 session.delete(ability)
453 session.flush()
455 # add the new ones
456 for language_ability in request.language_abilities.value:
457 if not language_is_allowed(language_ability.code):
458 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_language")
459 session.add(
460 LanguageAbility(
461 user_id=user.id,
462 language_code=language_ability.code,
463 fluency=not_none(fluency2sql[language_ability.fluency]),
464 )
465 )
467 if request.HasField("regions_visited"):
468 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
470 for region in request.regions_visited.value:
471 if not region_is_allowed(region):
472 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region")
473 session.add(
474 RegionVisited(
475 user_id=user.id,
476 region_code=region,
477 )
478 )
480 if request.HasField("regions_lived"):
481 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
483 for region in request.regions_lived.value:
484 if not region_is_allowed(region):
485 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region")
486 session.add(
487 RegionLived(
488 user_id=user.id,
489 region_code=region,
490 )
491 )
493 if request.HasField("additional_information"):
494 if request.additional_information.is_null:
495 user.additional_information = None
496 else:
497 user.additional_information = request.additional_information.value
499 if request.HasField("max_guests"):
500 if request.max_guests.is_null:
501 user.max_guests = None
502 else:
503 user.max_guests = request.max_guests.value
505 if request.HasField("last_minute"):
506 if request.last_minute.is_null:
507 user.last_minute = None
508 else:
509 user.last_minute = request.last_minute.value
511 if request.HasField("has_pets"):
512 if request.has_pets.is_null:
513 user.has_pets = None
514 else:
515 user.has_pets = request.has_pets.value
517 if request.HasField("accepts_pets"):
518 if request.accepts_pets.is_null:
519 user.accepts_pets = None
520 else:
521 user.accepts_pets = request.accepts_pets.value
523 if request.HasField("pet_details"):
524 if request.pet_details.is_null:
525 user.pet_details = None
526 else:
527 user.pet_details = request.pet_details.value
529 if request.HasField("has_kids"):
530 if request.has_kids.is_null:
531 user.has_kids = None
532 else:
533 user.has_kids = request.has_kids.value
535 if request.HasField("accepts_kids"):
536 if request.accepts_kids.is_null:
537 user.accepts_kids = None
538 else:
539 user.accepts_kids = request.accepts_kids.value
541 if request.HasField("kid_details"):
542 if request.kid_details.is_null:
543 user.kid_details = None
544 else:
545 user.kid_details = request.kid_details.value
547 if request.HasField("has_housemates"):
548 if request.has_housemates.is_null:
549 user.has_housemates = None
550 else:
551 user.has_housemates = request.has_housemates.value
553 if request.HasField("housemate_details"):
554 if request.housemate_details.is_null:
555 user.housemate_details = None
556 else:
557 user.housemate_details = request.housemate_details.value
559 if request.HasField("wheelchair_accessible"):
560 if request.wheelchair_accessible.is_null:
561 user.wheelchair_accessible = None
562 else:
563 user.wheelchair_accessible = request.wheelchair_accessible.value
565 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
566 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
568 if request.HasField("smokes_at_home"):
569 if request.smokes_at_home.is_null:
570 user.smokes_at_home = None
571 else:
572 user.smokes_at_home = request.smokes_at_home.value
574 if request.HasField("drinking_allowed"):
575 if request.drinking_allowed.is_null:
576 user.drinking_allowed = None
577 else:
578 user.drinking_allowed = request.drinking_allowed.value
580 if request.HasField("drinks_at_home"):
581 if request.drinks_at_home.is_null:
582 user.drinks_at_home = None
583 else:
584 user.drinks_at_home = request.drinks_at_home.value
586 if request.HasField("other_host_info"):
587 if request.other_host_info.is_null:
588 user.other_host_info = None
589 else:
590 user.other_host_info = request.other_host_info.value
592 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
593 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
595 if request.HasField("sleeping_details"):
596 if request.sleeping_details.is_null:
597 user.sleeping_details = None
598 else:
599 user.sleeping_details = request.sleeping_details.value
601 if request.HasField("area"):
602 if request.area.is_null:
603 user.area = None
604 else:
605 user.area = request.area.value
607 if request.HasField("house_rules"):
608 if request.house_rules.is_null:
609 user.house_rules = None
610 else:
611 user.house_rules = request.house_rules.value
613 if request.HasField("parking"):
614 if request.parking.is_null:
615 user.parking = None
616 else:
617 user.parking = request.parking.value
619 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
620 user.parking_details = parkingdetails2sql[request.parking_details]
622 if request.HasField("camping_ok"):
623 if request.camping_ok.is_null:
624 user.camping_ok = None
625 else:
626 user.camping_ok = request.camping_ok.value
628 user.profile_last_updated = now()
630 return empty_pb2.Empty()
632 def ListFriends(
633 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
634 ) -> api_pb2.ListFriendsRes:
635 rels_query = select(FriendRelationship)
636 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.from_user_id)
637 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.to_user_id)
638 rels_query = rels_query.where(
639 or_(
640 FriendRelationship.from_user_id == context.user_id,
641 FriendRelationship.to_user_id == context.user_id,
642 )
643 ).where(FriendRelationship.status == FriendStatus.accepted)
644 rels_query = where_moderated_content_visible(rels_query, context, FriendRelationship, is_list_operation=True)
645 rels = session.execute(rels_query).scalars().all()
646 return api_pb2.ListFriendsRes(
647 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
648 )
650 def RemoveFriend(
651 self, request: api_pb2.RemoveFriendReq, context: CouchersContext, session: Session
652 ) -> empty_pb2.Empty:
653 rel_query = select(FriendRelationship)
654 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.from_user_id)
655 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.to_user_id)
656 rel_query = rel_query.where(
657 or_(
658 and_(
659 FriendRelationship.from_user_id == request.user_id,
660 FriendRelationship.to_user_id == context.user_id,
661 ),
662 and_(
663 FriendRelationship.from_user_id == context.user_id,
664 FriendRelationship.to_user_id == request.user_id,
665 ),
666 )
667 ).where(FriendRelationship.status == FriendStatus.accepted)
668 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship)
669 rel = session.execute(rel_query).scalar_one_or_none()
671 if not rel:
672 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "not_friends")
674 session.delete(rel)
675 log_event(context, session, "friendship.removed", {"other_user_id": request.user_id})
677 return empty_pb2.Empty()
679 def ListMutualFriends(
680 self, request: api_pb2.ListMutualFriendsReq, context: CouchersContext, session: Session
681 ) -> api_pb2.ListMutualFriendsRes:
682 if context.user_id == request.user_id:
683 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
685 user = session.execute(
686 select(User).where(users_visible(context)).where(User.id == request.user_id)
687 ).scalar_one_or_none()
689 if not user: 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true
690 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
692 q1 = where_moderated_content_visible(
693 select(FriendRelationship.from_user_id.label("user_id"))
694 .where(FriendRelationship.to_user_id == context.user_id)
695 .where(FriendRelationship.from_user_id != request.user_id)
696 .where(FriendRelationship.status == FriendStatus.accepted),
697 context,
698 FriendRelationship,
699 )
701 q2 = where_moderated_content_visible(
702 select(FriendRelationship.to_user_id.label("user_id"))
703 .where(FriendRelationship.from_user_id == context.user_id)
704 .where(FriendRelationship.to_user_id != request.user_id)
705 .where(FriendRelationship.status == FriendStatus.accepted),
706 context,
707 FriendRelationship,
708 )
710 q3 = where_moderated_content_visible(
711 select(FriendRelationship.from_user_id.label("user_id"))
712 .where(FriendRelationship.to_user_id == request.user_id)
713 .where(FriendRelationship.from_user_id != context.user_id)
714 .where(FriendRelationship.status == FriendStatus.accepted),
715 context,
716 FriendRelationship,
717 )
719 q4 = where_moderated_content_visible(
720 select(FriendRelationship.to_user_id.label("user_id"))
721 .where(FriendRelationship.from_user_id == request.user_id)
722 .where(FriendRelationship.to_user_id != context.user_id)
723 .where(FriendRelationship.status == FriendStatus.accepted),
724 context,
725 FriendRelationship,
726 )
728 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
730 mutual_friends = (
731 session.execute(select(User).where(users_visible(context)).where(User.id.in_(mutual))).scalars().all()
732 )
734 return api_pb2.ListMutualFriendsRes(
735 mutual_friends=[
736 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
737 for mutual_friend in mutual_friends
738 ]
739 )
741 def SendFriendRequest(
742 self, request: api_pb2.SendFriendRequestReq, context: CouchersContext, session: Session
743 ) -> empty_pb2.Empty:
744 if context.user_id == request.user_id: 744 ↛ 745line 744 didn't jump to line 745 because the condition on line 744 was never true
745 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_friend_self")
747 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
748 if not has_completed_profile(session, user):
749 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_send_friend_request")
751 to_user = session.execute(
752 select(User).where(users_visible(context)).where(User.id == request.user_id)
753 ).scalar_one_or_none()
755 if not to_user: 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true
756 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
758 if (
759 session.execute(
760 select(FriendRelationship)
761 .where(
762 or_(
763 and_(
764 FriendRelationship.from_user_id == context.user_id,
765 FriendRelationship.to_user_id == request.user_id,
766 ),
767 and_(
768 FriendRelationship.from_user_id == request.user_id,
769 FriendRelationship.to_user_id == context.user_id,
770 ),
771 )
772 )
773 .where(
774 or_(
775 FriendRelationship.status == FriendStatus.accepted,
776 FriendRelationship.status == FriendStatus.pending,
777 )
778 )
779 ).scalar_one_or_none()
780 is not None
781 ):
782 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "friends_already_or_pending")
784 # Check if user has been sending friend requests excessively
785 if process_rate_limits_and_check_abort(
786 session=session, user_id=context.user_id, action=RateLimitAction.friend_request
787 ):
788 context.abort_with_error_code(
789 grpc.StatusCode.RESOURCE_EXHAUSTED,
790 "friend_request_rate_limit2",
791 substitutions={"count": RATE_LIMIT_HOURS},
792 )
794 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
796 # Use callback pattern to handle circular dependency between FriendRelationship and ModerationState
797 friend_relationship = None
799 def create_friend_relationship(moderation_state_id: int) -> int:
800 nonlocal friend_relationship
801 friend_relationship = FriendRelationship(
802 from_user_id=user.id,
803 to_user_id=to_user.id,
804 status=FriendStatus.pending,
805 moderation_state_id=moderation_state_id,
806 )
807 session.add(friend_relationship)
808 session.flush()
809 return friend_relationship.id
811 moderation_state = create_moderation(
812 session, ModerationObjectType.friend_request, create_friend_relationship, context.user_id
813 )
815 assert friend_relationship is not None # set by create_friend_relationship callback
817 notify(
818 session,
819 user_id=friend_relationship.to_user_id,
820 topic_action=NotificationTopicAction.friend_request__create,
821 key=str(friend_relationship.from_user_id),
822 data=notification_data_pb2.FriendRequestCreate(
823 other_user=user_model_to_pb(
824 friend_relationship.from_user,
825 session,
826 make_notification_user_context(user_id=friend_relationship.to_user_id),
827 ),
828 ),
829 moderation_state_id=moderation_state.id,
830 )
831 log_event(context, session, "friendship.request_sent", {"to_user_id": to_user.id})
833 return empty_pb2.Empty()
835 def ListFriendRequests(
836 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
837 ) -> api_pb2.ListFriendRequestsRes:
838 # both sent and received
839 sent_requests_query = select(FriendRelationship)
840 sent_requests_query = where_users_column_visible(sent_requests_query, context, FriendRelationship.to_user_id)
841 sent_requests_query = sent_requests_query.where(FriendRelationship.from_user_id == context.user_id).where(
842 FriendRelationship.status == FriendStatus.pending
843 )
844 sent_requests_query = where_moderated_content_visible(
845 sent_requests_query, context, FriendRelationship, is_list_operation=True
846 )
847 sent_requests = session.execute(sent_requests_query).scalars().all()
849 received_requests_query = select(FriendRelationship)
850 received_requests_query = where_users_column_visible(
851 received_requests_query, context, FriendRelationship.from_user_id
852 )
853 received_requests_query = received_requests_query.where(FriendRelationship.to_user_id == context.user_id).where(
854 FriendRelationship.status == FriendStatus.pending
855 )
856 received_requests_query = where_moderated_content_visible(
857 received_requests_query, context, FriendRelationship, is_list_operation=True
858 )
859 received_requests = session.execute(received_requests_query).scalars().all()
861 return api_pb2.ListFriendRequestsRes(
862 sent=[
863 api_pb2.FriendRequest(
864 friend_request_id=friend_request.id,
865 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
866 user_id=friend_request.to_user.id,
867 sent=True,
868 )
869 for friend_request in sent_requests
870 ],
871 received=[
872 api_pb2.FriendRequest(
873 friend_request_id=friend_request.id,
874 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
875 user_id=friend_request.from_user.id,
876 sent=False,
877 )
878 for friend_request in received_requests
879 ],
880 )
882 def RespondFriendRequest(
883 self, request: api_pb2.RespondFriendRequestReq, context: CouchersContext, session: Session
884 ) -> empty_pb2.Empty:
885 friend_request_query = select(FriendRelationship)
886 friend_request_query = where_users_column_visible(
887 friend_request_query, context, FriendRelationship.from_user_id
888 )
889 friend_request_query = (
890 friend_request_query.where(FriendRelationship.to_user_id == context.user_id)
891 .where(FriendRelationship.status == FriendStatus.pending)
892 .where(FriendRelationship.id == request.friend_request_id)
893 )
894 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship)
895 friend_request = session.execute(friend_request_query).scalar_one_or_none()
897 if not friend_request: 897 ↛ 898line 897 didn't jump to line 898 because the condition on line 897 was never true
898 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found")
900 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
901 friend_request.time_responded = func.now()
903 session.flush()
905 if friend_request.status == FriendStatus.accepted:
906 notify(
907 session,
908 user_id=friend_request.from_user_id,
909 topic_action=NotificationTopicAction.friend_request__accept,
910 key=str(friend_request.to_user_id),
911 data=notification_data_pb2.FriendRequestAccept(
912 other_user=user_model_to_pb(
913 friend_request.to_user,
914 session,
915 make_notification_user_context(user_id=friend_request.from_user_id),
916 ),
917 ),
918 )
920 log_event(
921 context,
922 session,
923 "friendship.request_responded",
924 {"from_user_id": friend_request.from_user_id, "accepted": request.accept},
925 )
927 return empty_pb2.Empty()
929 def CancelFriendRequest(
930 self, request: api_pb2.CancelFriendRequestReq, context: CouchersContext, session: Session
931 ) -> empty_pb2.Empty:
932 friend_request_query = select(FriendRelationship)
933 friend_request_query = where_users_column_visible(friend_request_query, context, FriendRelationship.to_user_id)
934 friend_request_query = (
935 friend_request_query.where(FriendRelationship.from_user_id == context.user_id)
936 .where(FriendRelationship.status == FriendStatus.pending)
937 .where(FriendRelationship.id == request.friend_request_id)
938 )
939 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship)
940 friend_request = session.execute(friend_request_query).scalar_one_or_none()
942 if not friend_request: 942 ↛ 943line 942 didn't jump to line 943 because the condition on line 942 was never true
943 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found")
945 friend_request.status = FriendStatus.cancelled
946 friend_request.time_responded = func.now()
948 # note no notifications
949 log_event(context, session, "friendship.request_cancelled", {"to_user_id": friend_request.to_user_id})
951 session.commit()
953 return empty_pb2.Empty()
955 def InitiateMediaUpload(
956 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
957 ) -> api_pb2.InitiateMediaUploadRes:
958 key = random_hex()
960 created = now()
961 expiry = created + timedelta(minutes=20)
963 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
964 session.add(upload)
965 session.commit()
967 req = media_pb2.UploadRequest(
968 key=upload.key,
969 type=media_pb2.UploadRequest.UploadType.IMAGE,
970 created=Timestamp_from_datetime(upload.created),
971 expiry=Timestamp_from_datetime(upload.expiry),
972 max_width=2000,
973 max_height=1600,
974 ).SerializeToString()
976 data = b64encode(req)
977 sig = b64encode(generate_hash_signature(req, config.MEDIA_SERVER_SECRET_KEY))
979 path = "upload?" + urlencode({"data": data, "sig": sig})
981 return api_pb2.InitiateMediaUploadRes(
982 upload_url=urls.media_upload_url(path=path),
983 expiry=Timestamp_from_datetime(expiry),
984 )
986 def ListBadgeUsers(
987 self, request: api_pb2.ListBadgeUsersReq, context: CouchersContext, session: Session
988 ) -> api_pb2.ListBadgeUsersRes:
989 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
990 next_user_id = int(request.page_token) if request.page_token else 0
991 badge = get_badge_dict().get(request.badge_id)
992 if not badge: 992 ↛ 993line 992 didn't jump to line 993 because the condition on line 992 was never true
993 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "badge_not_found")
995 badge_user_ids_query = (
996 select(UserBadge.user_id).where(UserBadge.badge_id == badge.id).where(UserBadge.user_id >= next_user_id)
997 )
998 badge_user_ids_query = where_users_column_visible(badge_user_ids_query, context, UserBadge.user_id)
999 badge_user_ids_query = badge_user_ids_query.order_by(UserBadge.user_id).limit(page_size + 1)
1000 badge_user_ids = session.execute(badge_user_ids_query).scalars().all()
1002 return api_pb2.ListBadgeUsersRes(
1003 user_ids=badge_user_ids[:page_size],
1004 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
1005 )
1008def response_rate_to_pb(response_rate: UserResponseRate | None) -> dict[str, google.protobuf.message.Message]:
1009 if not response_rate:
1010 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
1012 # if n is None, the user is new, or they have no requests
1013 if response_rate.requests < 3:
1014 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
1016 if response_rate.response_rate <= 0.33:
1017 return {"low": requests_pb2.ResponseRateLow()}
1019 response_time_p33_coarsened = Duration_from_timedelta(
1020 timedelta(seconds=round(response_rate.response_time_33p.total_seconds() / 60) * 60)
1021 )
1023 if response_rate.response_rate <= 0.66:
1024 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
1026 response_time_p66_coarsened = Duration_from_timedelta(
1027 timedelta(seconds=round(response_rate.response_time_66p.total_seconds() / 60) * 60)
1028 )
1030 if response_rate.response_rate <= 0.90:
1031 return {
1032 "most": requests_pb2.ResponseRateMost(
1033 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
1034 )
1035 }
1036 else:
1037 return {
1038 "almost_all": requests_pb2.ResponseRateAlmostAll(
1039 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
1040 )
1041 }
1044def get_num_references(session: Session, context: CouchersContext, user_ids: Iterable[int]) -> dict[int, int]:
1045 query = where_moderated_content_visible(
1046 select(Reference.to_user_id, func.count(Reference.id)), context, Reference, is_list_operation=True
1047 )
1048 # exclude references still hidden by the reciprocal-reference rule, matching ListReferences
1049 query = where_references_not_hidden_by_reciprocity(query)
1050 query = where_reference_user_visible(
1051 query.where(Reference.to_user_id.in_(user_ids)), context, Reference.from_user_id
1052 ).group_by(Reference.to_user_id)
1053 return cast(dict[int, int], dict(session.execute(query).all())) # type: ignore[arg-type]
1056def user_model_to_pb(
1057 db_user: User,
1058 session: Session,
1059 context: CouchersContext,
1060 *,
1061 is_admin_see_ghosts: bool = False,
1062 is_get_user_return_ghosts: bool = False,
1063) -> api_pb2.User:
1064 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
1065 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context
1067 viewer_user_id = context.user_id if context.is_logged_in() else None
1068 if not is_admin_see_ghosts and is_not_visible(
1069 session, viewer_user_id, db_user.id, ignore_shadow=context.serialize_shadowed
1070 ):
1071 # User is not visible (deleted, banned, or blocked)
1072 if is_get_user_return_ghosts: 1072 ↛ 1087line 1072 didn't jump to line 1087 because the condition on line 1072 was always true
1073 maybe_log_nonvisible_user_access(
1074 context,
1075 db_user,
1076 access_type=NonvisibleUserAccessType.ghost_served,
1077 actor_user_id=viewer_user_id,
1078 )
1079 # Return an anonymized "ghost" user profile
1080 return api_pb2.User(
1081 user_id=db_user.id,
1082 is_ghost=True,
1083 username=GHOST_USERNAME,
1084 name=context.localization.localize_string("ghost_users.display_name"),
1085 about_me=context.localization.localize_string("ghost_users.about_me"),
1086 )
1087 raise GhostUserSerializationError(
1088 f"Tried to serialize ghost profile in user_model_to_pb without appropriate flags. "
1089 f"Context user_id: {viewer_user_id}, db_user id: {db_user.id} (username: {db_user.username})"
1090 )
1092 num_references = get_num_references(session, context, [db_user.id]).get(db_user.id, 0)
1093 lat, lng = db_user.coordinates
1095 pending_friend_request = None
1096 if context.is_logged_out() or db_user.id == context.user_id:
1097 friends_status = api_pb2.User.FriendshipStatus.NA
1098 else:
1099 friend_relationship = session.execute(
1100 where_moderated_content_visible(
1101 select(FriendRelationship)
1102 .where(
1103 or_(
1104 and_(
1105 FriendRelationship.from_user_id == context.user_id,
1106 FriendRelationship.to_user_id == db_user.id,
1107 ),
1108 and_(
1109 FriendRelationship.from_user_id == db_user.id,
1110 FriendRelationship.to_user_id == context.user_id,
1111 ),
1112 )
1113 )
1114 .where(
1115 or_(
1116 FriendRelationship.status == FriendStatus.accepted,
1117 FriendRelationship.status == FriendStatus.pending,
1118 )
1119 ),
1120 context,
1121 FriendRelationship,
1122 )
1123 ).scalar_one_or_none()
1125 if friend_relationship:
1126 if friend_relationship.status == FriendStatus.accepted:
1127 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
1128 else:
1129 friends_status = api_pb2.User.FriendshipStatus.PENDING
1130 if friend_relationship.from_user_id == context.user_id: 1130 ↛ 1132line 1130 didn't jump to line 1132 because the condition on line 1130 was never true
1131 # we sent it
1132 pending_friend_request = api_pb2.FriendRequest(
1133 friend_request_id=friend_relationship.id,
1134 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
1135 user_id=friend_relationship.to_user.id,
1136 sent=True,
1137 )
1138 else:
1139 # we received it
1140 pending_friend_request = api_pb2.FriendRequest(
1141 friend_request_id=friend_relationship.id,
1142 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
1143 user_id=friend_relationship.from_user.id,
1144 sent=False,
1145 )
1146 else:
1147 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
1149 response_rate = session.execute(
1150 select(UserResponseRate).where(UserResponseRate.user_id == db_user.id)
1151 ).scalar_one_or_none()
1153 avatar_upload = get_avatar_upload(session, db_user)
1155 verification_score = 0.0
1156 if db_user.phone_verification_verified:
1157 verification_score += 1.0 * db_user.phone_is_verified
1159 user = api_pb2.User(
1160 user_id=db_user.id,
1161 username=db_user.username,
1162 name=db_user.name,
1163 city=db_user.city,
1164 hometown=db_user.hometown,
1165 timezone=db_user.timezone,
1166 lat=lat,
1167 lng=lng,
1168 radius=db_user.geom_radius,
1169 verification=verification_score,
1170 community_standing=db_user.community_standing,
1171 num_references=num_references,
1172 gender=db_user.gender,
1173 pronouns=db_user.pronouns,
1174 age=int(db_user.age),
1175 joined=Timestamp_from_datetime(db_user.display_joined),
1176 last_active=Timestamp_from_datetime(db_user.display_last_active),
1177 hosting_status=hostingstatus2api[db_user.hosting_status],
1178 meetup_status=meetupstatus2api[db_user.meetup_status],
1179 occupation=db_user.occupation,
1180 education=db_user.education,
1181 about_me=db_user.about_me,
1182 things_i_like=db_user.things_i_like,
1183 about_place=db_user.about_place,
1184 language_abilities=[
1185 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
1186 for ability in db_user.language_abilities
1187 ],
1188 regions_visited=[region.code for region in db_user.regions_visited],
1189 regions_lived=[region.code for region in db_user.regions_lived],
1190 additional_information=db_user.additional_information,
1191 friends=friends_status,
1192 pending_friend_request=pending_friend_request,
1193 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
1194 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
1195 parking_details=parkingdetails2api[db_user.parking_details],
1196 avatar_url=avatar_upload.full_url if avatar_upload else None,
1197 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None,
1198 profile_gallery_id=db_user.profile_gallery_id,
1199 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id))
1200 .scalars()
1201 .all(),
1202 **get_strong_verification_fields(session, db_user),
1203 **response_rate_to_pb(response_rate), # type: ignore[arg-type]
1204 )
1206 if db_user.max_guests is not None:
1207 user.max_guests.value = db_user.max_guests
1209 if db_user.last_minute is not None:
1210 user.last_minute.value = db_user.last_minute
1212 if db_user.has_pets is not None:
1213 user.has_pets.value = db_user.has_pets
1215 if db_user.accepts_pets is not None:
1216 user.accepts_pets.value = db_user.accepts_pets
1218 if db_user.pet_details is not None:
1219 user.pet_details.value = db_user.pet_details
1221 if db_user.has_kids is not None:
1222 user.has_kids.value = db_user.has_kids
1224 if db_user.accepts_kids is not None:
1225 user.accepts_kids.value = db_user.accepts_kids
1227 if db_user.kid_details is not None:
1228 user.kid_details.value = db_user.kid_details
1230 if db_user.has_housemates is not None:
1231 user.has_housemates.value = db_user.has_housemates
1233 if db_user.housemate_details is not None:
1234 user.housemate_details.value = db_user.housemate_details
1236 if db_user.wheelchair_accessible is not None:
1237 user.wheelchair_accessible.value = db_user.wheelchair_accessible
1239 if db_user.smokes_at_home is not None:
1240 user.smokes_at_home.value = db_user.smokes_at_home
1242 if db_user.drinking_allowed is not None:
1243 user.drinking_allowed.value = db_user.drinking_allowed
1245 if db_user.drinks_at_home is not None:
1246 user.drinks_at_home.value = db_user.drinks_at_home
1248 if db_user.other_host_info is not None:
1249 user.other_host_info.value = db_user.other_host_info
1251 if db_user.sleeping_details is not None:
1252 user.sleeping_details.value = db_user.sleeping_details
1254 if db_user.area is not None:
1255 user.area.value = db_user.area
1257 if db_user.house_rules is not None:
1258 user.house_rules.value = db_user.house_rules
1260 if db_user.parking is not None:
1261 user.parking.value = db_user.parking
1263 if db_user.camping_ok is not None:
1264 user.camping_ok.value = db_user.camping_ok
1266 return user
1269def lite_user_to_pb(
1270 session: Session,
1271 lite_user: LiteUser,
1272 context: CouchersContext,
1273 *,
1274 is_admin_see_ghosts: bool = False,
1275 is_get_user_return_ghosts: bool = False,
1276) -> api_pb2.LiteUser:
1277 if not is_admin_see_ghosts and is_not_visible(
1278 session, context.user_id, lite_user.id, ignore_shadow=context.serialize_shadowed
1279 ):
1280 # User is not visible (deleted, banned, or blocked)
1281 if is_get_user_return_ghosts: 1281 ↛ 1289line 1281 didn't jump to line 1289 because the condition on line 1281 was always true
1282 # Return an anonymized "ghost" user profile
1283 return api_pb2.LiteUser(
1284 user_id=lite_user.id,
1285 is_ghost=True,
1286 username=GHOST_USERNAME,
1287 name=context.localization.localize_string("ghost_users.display_name"),
1288 )
1289 raise GhostUserSerializationError(
1290 f"Tried to serialize ghost profile in lite_user_to_pb without appropriate flags. "
1291 f"Context user_id: {context.user_id}, lite_user id: {lite_user.id} (username: {lite_user.username})"
1292 )
1294 lat, lng = get_coordinates(lite_user.geom)
1296 return api_pb2.LiteUser(
1297 user_id=lite_user.id,
1298 username=lite_user.username,
1299 name=lite_user.name,
1300 city=lite_user.city,
1301 age=int(lite_user.age),
1302 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1303 if lite_user.avatar_filename
1304 else None,
1305 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1306 if lite_user.avatar_filename
1307 else None,
1308 lat=lat,
1309 lng=lng,
1310 radius=lite_user.radius,
1311 has_strong_verification=lite_user.has_strong_verification,
1312 )