Coverage for app/backend/src/couchers/servicers/api.py: 97%
478 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 04:10 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 04:10 +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
11from sqlalchemy.sql import and_, 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.references import where_references_not_hidden_by_reciprocity
22from couchers.helpers.strong_verification import get_strong_verification_fields
23from couchers.materialized_views import LiteUser, UserResponseRate
24from couchers.models import (
25 FriendRelationship,
26 FriendStatus,
27 GroupChat,
28 GroupChatSubscription,
29 HostingStatus,
30 HostRequest,
31 InitiatedUpload,
32 LanguageAbility,
33 LanguageFluency,
34 MeetupStatus,
35 Message,
36 ModerationObjectType,
37 NonvisibleUserAccessType,
38 Notification,
39 NotificationDeliveryType,
40 ParkingDetails,
41 RateLimitAction,
42 Reference,
43 RegionLived,
44 RegionVisited,
45 SleepingArrangement,
46 SmokingLocation,
47 User,
48 UserBadge,
49)
50from couchers.models.notifications import NotificationTopicAction
51from couchers.models.uploads import get_avatar_upload
52from couchers.moderation.utils import create_moderation
53from couchers.notifications.notify import notify
54from couchers.notifications.settings import get_topic_actions_by_delivery_type
55from couchers.proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
56from couchers.rate_limits.check import process_rate_limits_and_check_abort
57from couchers.rate_limits.definitions import RATE_LIMIT_HOURS
58from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed
59from couchers.servicers.blocking import is_not_visible
60from couchers.sql import (
61 moderation_state_column_visible,
62 username_or_id,
63 users_visible,
64 where_moderated_content_visible,
65 where_users_column_visible,
66)
67from couchers.utils import (
68 Duration_from_timedelta,
69 Timestamp_from_datetime,
70 create_coordinate,
71 get_coordinates,
72 is_valid_name,
73 is_valid_user_id,
74 is_valid_username,
75 not_none,
76 now,
77)
80class GhostUserSerializationError(Exception):
81 """
82 Raised when attempting to serialize a ghost user (deleted/banned/blocked)
83 """
85 pass
88MAX_USERS_PER_QUERY = 200
89MAX_PAGINATION_LENGTH = 50
91hostingstatus2sql = {
92 api_pb2.HOSTING_STATUS_UNKNOWN: None,
93 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
94 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
95 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
96}
98hostingstatus2api = {
99 None: api_pb2.HOSTING_STATUS_UNKNOWN,
100 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
101 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
102 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
103}
105meetupstatus2sql = {
106 api_pb2.MEETUP_STATUS_UNKNOWN: None,
107 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
108 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
109 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
110}
112meetupstatus2api = {
113 None: api_pb2.MEETUP_STATUS_UNKNOWN,
114 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
115 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
116 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
117}
119smokinglocation2sql = {
120 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
121 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
122 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
123 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
124 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
125}
127smokinglocation2api = {
128 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
129 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
130 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
131 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
132 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
133}
135sleepingarrangement2sql = {
136 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
137 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
138 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
139 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
140}
142sleepingarrangement2api = {
143 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
144 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
145 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
146 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
147}
149parkingdetails2sql = {
150 api_pb2.PARKING_DETAILS_UNKNOWN: None,
151 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
152 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
153 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
154 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
155}
157parkingdetails2api = {
158 None: api_pb2.PARKING_DETAILS_UNKNOWN,
159 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
160 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
161 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
162 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
163}
165fluency2sql = {
166 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
167 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
168 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
169 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
170}
172fluency2api = {
173 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
174 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
175 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
176 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
177}
180class API(api_pb2_grpc.APIServicer):
181 def Ping(self, request: api_pb2.PingReq, context: CouchersContext, session: Session) -> api_pb2.PingRes:
182 # auth ought to make sure the user exists
183 user = session.execute(
184 select(User)
185 .where(User.id == context.user_id)
186 .options(
187 selectinload(User.regions_visited),
188 selectinload(User.regions_lived),
189 selectinload(User.language_abilities),
190 )
191 ).scalar_one()
193 sent_reqs_query = select(HostRequest.conversation_id, HostRequest.initiator_last_seen_message_id).where(
194 HostRequest.initiator_user_id == context.user_id
195 )
196 sent_reqs_query = where_users_column_visible(sent_reqs_query, context, HostRequest.recipient_user_id)
197 sent_reqs_query = where_moderated_content_visible(sent_reqs_query, context, HostRequest, is_list_operation=True)
198 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery()
200 unseen_sent_host_request_count = session.execute(
201 select(func.count())
202 .select_from(sent_reqs_last_seen_message_ids)
203 .where(
204 exists(
205 select(1)
206 .where(Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id)
207 .where(Message.id > sent_reqs_last_seen_message_ids.c.initiator_last_seen_message_id)
208 )
209 )
210 ).scalar_one()
212 received_reqs_query = select(HostRequest.conversation_id, HostRequest.recipient_last_seen_message_id).where(
213 HostRequest.recipient_user_id == context.user_id
214 )
215 received_reqs_query = where_users_column_visible(received_reqs_query, context, HostRequest.initiator_user_id)
216 received_reqs_query = where_moderated_content_visible(
217 received_reqs_query, context, HostRequest, is_list_operation=True
218 )
219 received_reqs_last_seen_message_ids = received_reqs_query.subquery()
221 unseen_received_host_request_count = session.execute(
222 select(func.count())
223 .select_from(received_reqs_last_seen_message_ids)
224 .where(
225 exists(
226 select(1)
227 .where(Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id)
228 .where(Message.id > received_reqs_last_seen_message_ids.c.recipient_last_seen_message_id)
229 )
230 )
231 ).scalar_one()
233 unseen_message_query = (
234 select(func.count(Message.id))
235 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
236 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id)
237 )
238 unseen_message_query = where_moderated_content_visible(
239 unseen_message_query, context, GroupChat, is_list_operation=True
240 )
241 unseen_message_query = (
242 unseen_message_query.where(GroupChatSubscription.user_id == context.user_id)
243 .where(Message.time >= GroupChatSubscription.joined)
244 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
245 .where(Message.id > GroupChatSubscription.last_seen_message_id)
246 )
247 unseen_message_count = session.execute(unseen_message_query).scalar_one()
249 pending_friend_request_query = select(func.count(FriendRelationship.id)).where(
250 FriendRelationship.to_user_id == context.user_id
251 )
252 pending_friend_request_query = where_users_column_visible(
253 pending_friend_request_query, context, FriendRelationship.from_user_id
254 )
255 pending_friend_request_query = pending_friend_request_query.where(
256 FriendRelationship.status == FriendStatus.pending
257 )
258 pending_friend_request_query = where_moderated_content_visible(
259 pending_friend_request_query, context, FriendRelationship, is_list_operation=True
260 )
261 pending_friend_request_count = session.execute(pending_friend_request_query).scalar_one()
263 unseen_notification_count = session.execute(
264 select(func.count())
265 .select_from(Notification)
266 .where(Notification.user_id == context.user_id)
267 .where(Notification.is_seen == False)
268 .where(
269 Notification.topic_action.in_(
270 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push)
271 )
272 )
273 .where(moderation_state_column_visible(context, Notification.moderation_state_id))
274 ).scalar_one()
276 return api_pb2.PingRes(
277 user=user_model_to_pb(user, session, context),
278 unseen_message_count=unseen_message_count,
279 unseen_sent_host_request_count=unseen_sent_host_request_count,
280 unseen_received_host_request_count=unseen_received_host_request_count,
281 pending_friend_request_count=pending_friend_request_count,
282 unseen_notification_count=unseen_notification_count,
283 )
285 def GetUser(self, request: api_pb2.GetUserReq, context: CouchersContext, session: Session) -> api_pb2.User:
286 user = session.execute(
287 select(User)
288 .where(username_or_id(request.user))
289 .options(
290 selectinload(User.regions_visited),
291 selectinload(User.regions_lived),
292 selectinload(User.language_abilities),
293 )
294 ).scalar_one_or_none()
296 if not user: 296 ↛ 297line 296 didn't jump to line 297 because the condition on line 296 was never true
297 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
299 return user_model_to_pb(user, session, context, is_get_user_return_ghosts=True)
301 def GetLiteUser(
302 self, request: api_pb2.GetLiteUserReq, context: CouchersContext, session: Session
303 ) -> api_pb2.LiteUser:
304 lite_user = session.execute(
305 select(LiteUser).where(username_or_id(request.user, table=LiteUser))
306 ).scalar_one_or_none()
308 if not lite_user: 308 ↛ 309line 308 didn't jump to line 309 because the condition on line 308 was never true
309 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
311 return lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True)
313 def GetLiteUsers(
314 self, request: api_pb2.GetLiteUsersReq, context: CouchersContext, session: Session
315 ) -> api_pb2.GetLiteUsersRes:
316 if len(request.users) > MAX_USERS_PER_QUERY:
317 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "requested_too_many_users")
319 usernames = {u for u in request.users if is_valid_username(u)}
320 ids = {int(u) for u in request.users if is_valid_user_id(u)}
322 # decomposed where_username_or_id...
323 users = (
324 session.execute(select(LiteUser).where(or_(LiteUser.username.in_(usernames), LiteUser.id.in_(ids))))
325 .scalars()
326 .all()
327 )
329 users_by_id = {str(user.id): user for user in users}
330 users_by_username = {user.username: user for user in users}
332 res = api_pb2.GetLiteUsersRes()
334 for user in request.users:
335 lite_user = None
336 if user in users_by_id:
337 lite_user = users_by_id[user]
338 elif user in users_by_username:
339 lite_user = users_by_username[user]
341 res.responses.append(
342 api_pb2.LiteUserRes(
343 query=user,
344 not_found=lite_user is None,
345 user=lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True)
346 if lite_user
347 else None,
348 )
349 )
351 return res
353 def UpdateProfile(
354 self, request: api_pb2.UpdateProfileReq, context: CouchersContext, session: Session
355 ) -> empty_pb2.Empty:
356 user: User = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
358 if request.HasField("name"):
359 if not is_valid_name(request.name.value):
360 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_name")
361 user.name = request.name.value
363 if request.HasField("city"):
364 user.city = request.city.value
366 if request.HasField("hometown"):
367 if request.hometown.is_null:
368 user.hometown = None
369 else:
370 user.hometown = request.hometown.value
372 if request.HasField("lat") and request.HasField("lng"):
373 if request.lat.value == 0 and request.lng.value == 0:
374 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate")
375 user.geom = create_coordinate(request.lat.value, request.lng.value)
376 user.randomized_geom = None
378 if request.HasField("radius"):
379 user.geom_radius = request.radius.value
381 # if request.HasField("gender"):
382 # user.gender = request.gender.value
384 if request.HasField("pronouns"):
385 if request.pronouns.is_null:
386 user.pronouns = None
387 else:
388 user.pronouns = request.pronouns.value
390 if request.HasField("occupation"):
391 if request.occupation.is_null:
392 user.occupation = None
393 else:
394 user.occupation = request.occupation.value
396 if request.HasField("education"):
397 if request.education.is_null:
398 user.education = None
399 else:
400 user.education = request.education.value
402 if request.HasField("about_me"):
403 if request.about_me.is_null:
404 user.about_me = None
405 else:
406 user.about_me = request.about_me.value
408 if request.HasField("things_i_like"):
409 if request.things_i_like.is_null:
410 user.things_i_like = None
411 else:
412 user.things_i_like = request.things_i_like.value
414 if request.HasField("about_place"):
415 if request.about_place.is_null:
416 user.about_place = None
417 else:
418 user.about_place = request.about_place.value
420 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
421 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
422 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_host")
423 user.hosting_status = hostingstatus2sql[request.hosting_status] # type: ignore[assignment]
425 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
426 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
427 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_meet")
428 user.meetup_status = meetupstatus2sql[request.meetup_status] # type: ignore[assignment]
430 if request.HasField("language_abilities"):
431 # delete all existing abilities
432 for ability in user.language_abilities:
433 session.delete(ability)
434 session.flush()
436 # add the new ones
437 for language_ability in request.language_abilities.value:
438 if not language_is_allowed(language_ability.code):
439 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_language")
440 session.add(
441 LanguageAbility(
442 user_id=user.id,
443 language_code=language_ability.code,
444 fluency=not_none(fluency2sql[language_ability.fluency]),
445 )
446 )
448 if request.HasField("regions_visited"):
449 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
451 for region in request.regions_visited.value:
452 if not region_is_allowed(region):
453 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region")
454 session.add(
455 RegionVisited(
456 user_id=user.id,
457 region_code=region,
458 )
459 )
461 if request.HasField("regions_lived"):
462 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
464 for region in request.regions_lived.value:
465 if not region_is_allowed(region):
466 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region")
467 session.add(
468 RegionLived(
469 user_id=user.id,
470 region_code=region,
471 )
472 )
474 if request.HasField("additional_information"):
475 if request.additional_information.is_null:
476 user.additional_information = None
477 else:
478 user.additional_information = request.additional_information.value
480 if request.HasField("max_guests"):
481 if request.max_guests.is_null:
482 user.max_guests = None
483 else:
484 user.max_guests = request.max_guests.value
486 if request.HasField("last_minute"):
487 if request.last_minute.is_null:
488 user.last_minute = None
489 else:
490 user.last_minute = request.last_minute.value
492 if request.HasField("has_pets"):
493 if request.has_pets.is_null:
494 user.has_pets = None
495 else:
496 user.has_pets = request.has_pets.value
498 if request.HasField("accepts_pets"):
499 if request.accepts_pets.is_null:
500 user.accepts_pets = None
501 else:
502 user.accepts_pets = request.accepts_pets.value
504 if request.HasField("pet_details"):
505 if request.pet_details.is_null:
506 user.pet_details = None
507 else:
508 user.pet_details = request.pet_details.value
510 if request.HasField("has_kids"):
511 if request.has_kids.is_null:
512 user.has_kids = None
513 else:
514 user.has_kids = request.has_kids.value
516 if request.HasField("accepts_kids"):
517 if request.accepts_kids.is_null:
518 user.accepts_kids = None
519 else:
520 user.accepts_kids = request.accepts_kids.value
522 if request.HasField("kid_details"):
523 if request.kid_details.is_null:
524 user.kid_details = None
525 else:
526 user.kid_details = request.kid_details.value
528 if request.HasField("has_housemates"):
529 if request.has_housemates.is_null:
530 user.has_housemates = None
531 else:
532 user.has_housemates = request.has_housemates.value
534 if request.HasField("housemate_details"):
535 if request.housemate_details.is_null:
536 user.housemate_details = None
537 else:
538 user.housemate_details = request.housemate_details.value
540 if request.HasField("wheelchair_accessible"):
541 if request.wheelchair_accessible.is_null:
542 user.wheelchair_accessible = None
543 else:
544 user.wheelchair_accessible = request.wheelchair_accessible.value
546 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
547 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
549 if request.HasField("smokes_at_home"):
550 if request.smokes_at_home.is_null:
551 user.smokes_at_home = None
552 else:
553 user.smokes_at_home = request.smokes_at_home.value
555 if request.HasField("drinking_allowed"):
556 if request.drinking_allowed.is_null:
557 user.drinking_allowed = None
558 else:
559 user.drinking_allowed = request.drinking_allowed.value
561 if request.HasField("drinks_at_home"):
562 if request.drinks_at_home.is_null:
563 user.drinks_at_home = None
564 else:
565 user.drinks_at_home = request.drinks_at_home.value
567 if request.HasField("other_host_info"):
568 if request.other_host_info.is_null:
569 user.other_host_info = None
570 else:
571 user.other_host_info = request.other_host_info.value
573 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
574 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
576 if request.HasField("sleeping_details"):
577 if request.sleeping_details.is_null:
578 user.sleeping_details = None
579 else:
580 user.sleeping_details = request.sleeping_details.value
582 if request.HasField("area"):
583 if request.area.is_null:
584 user.area = None
585 else:
586 user.area = request.area.value
588 if request.HasField("house_rules"):
589 if request.house_rules.is_null:
590 user.house_rules = None
591 else:
592 user.house_rules = request.house_rules.value
594 if request.HasField("parking"):
595 if request.parking.is_null:
596 user.parking = None
597 else:
598 user.parking = request.parking.value
600 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
601 user.parking_details = parkingdetails2sql[request.parking_details]
603 if request.HasField("camping_ok"):
604 if request.camping_ok.is_null:
605 user.camping_ok = None
606 else:
607 user.camping_ok = request.camping_ok.value
609 user.profile_last_updated = now()
611 return empty_pb2.Empty()
613 def ListFriends(
614 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
615 ) -> api_pb2.ListFriendsRes:
616 rels_query = select(FriendRelationship)
617 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.from_user_id)
618 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.to_user_id)
619 rels_query = rels_query.where(
620 or_(
621 FriendRelationship.from_user_id == context.user_id,
622 FriendRelationship.to_user_id == context.user_id,
623 )
624 ).where(FriendRelationship.status == FriendStatus.accepted)
625 rels_query = where_moderated_content_visible(rels_query, context, FriendRelationship, is_list_operation=True)
626 rels = session.execute(rels_query).scalars().all()
627 return api_pb2.ListFriendsRes(
628 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
629 )
631 def RemoveFriend(
632 self, request: api_pb2.RemoveFriendReq, context: CouchersContext, session: Session
633 ) -> empty_pb2.Empty:
634 rel_query = select(FriendRelationship)
635 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.from_user_id)
636 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.to_user_id)
637 rel_query = rel_query.where(
638 or_(
639 and_(
640 FriendRelationship.from_user_id == request.user_id,
641 FriendRelationship.to_user_id == context.user_id,
642 ),
643 and_(
644 FriendRelationship.from_user_id == context.user_id,
645 FriendRelationship.to_user_id == request.user_id,
646 ),
647 )
648 ).where(FriendRelationship.status == FriendStatus.accepted)
649 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship)
650 rel = session.execute(rel_query).scalar_one_or_none()
652 if not rel:
653 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "not_friends")
655 session.delete(rel)
656 log_event(context, session, "friendship.removed", {"other_user_id": request.user_id})
658 return empty_pb2.Empty()
660 def ListMutualFriends(
661 self, request: api_pb2.ListMutualFriendsReq, context: CouchersContext, session: Session
662 ) -> api_pb2.ListMutualFriendsRes:
663 if context.user_id == request.user_id:
664 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
666 user = session.execute(
667 select(User).where(users_visible(context)).where(User.id == request.user_id)
668 ).scalar_one_or_none()
670 if not user: 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true
671 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
673 q1 = where_moderated_content_visible(
674 select(FriendRelationship.from_user_id.label("user_id"))
675 .where(FriendRelationship.to_user_id == context.user_id)
676 .where(FriendRelationship.from_user_id != request.user_id)
677 .where(FriendRelationship.status == FriendStatus.accepted),
678 context,
679 FriendRelationship,
680 )
682 q2 = where_moderated_content_visible(
683 select(FriendRelationship.to_user_id.label("user_id"))
684 .where(FriendRelationship.from_user_id == context.user_id)
685 .where(FriendRelationship.to_user_id != request.user_id)
686 .where(FriendRelationship.status == FriendStatus.accepted),
687 context,
688 FriendRelationship,
689 )
691 q3 = where_moderated_content_visible(
692 select(FriendRelationship.from_user_id.label("user_id"))
693 .where(FriendRelationship.to_user_id == request.user_id)
694 .where(FriendRelationship.from_user_id != context.user_id)
695 .where(FriendRelationship.status == FriendStatus.accepted),
696 context,
697 FriendRelationship,
698 )
700 q4 = where_moderated_content_visible(
701 select(FriendRelationship.to_user_id.label("user_id"))
702 .where(FriendRelationship.from_user_id == request.user_id)
703 .where(FriendRelationship.to_user_id != context.user_id)
704 .where(FriendRelationship.status == FriendStatus.accepted),
705 context,
706 FriendRelationship,
707 )
709 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
711 mutual_friends = (
712 session.execute(select(User).where(users_visible(context)).where(User.id.in_(mutual))).scalars().all()
713 )
715 return api_pb2.ListMutualFriendsRes(
716 mutual_friends=[
717 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
718 for mutual_friend in mutual_friends
719 ]
720 )
722 def SendFriendRequest(
723 self, request: api_pb2.SendFriendRequestReq, context: CouchersContext, session: Session
724 ) -> empty_pb2.Empty:
725 if context.user_id == request.user_id: 725 ↛ 726line 725 didn't jump to line 726 because the condition on line 725 was never true
726 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_friend_self")
728 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
729 if not has_completed_profile(session, user):
730 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_send_friend_request")
732 to_user = session.execute(
733 select(User).where(users_visible(context)).where(User.id == request.user_id)
734 ).scalar_one_or_none()
736 if not to_user: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true
737 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
739 if (
740 session.execute(
741 select(FriendRelationship)
742 .where(
743 or_(
744 and_(
745 FriendRelationship.from_user_id == context.user_id,
746 FriendRelationship.to_user_id == request.user_id,
747 ),
748 and_(
749 FriendRelationship.from_user_id == request.user_id,
750 FriendRelationship.to_user_id == context.user_id,
751 ),
752 )
753 )
754 .where(
755 or_(
756 FriendRelationship.status == FriendStatus.accepted,
757 FriendRelationship.status == FriendStatus.pending,
758 )
759 )
760 ).scalar_one_or_none()
761 is not None
762 ):
763 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "friends_already_or_pending")
765 # Check if user has been sending friend requests excessively
766 if process_rate_limits_and_check_abort(
767 session=session, user_id=context.user_id, action=RateLimitAction.friend_request
768 ):
769 context.abort_with_error_code(
770 grpc.StatusCode.RESOURCE_EXHAUSTED,
771 "friend_request_rate_limit2",
772 substitutions={"count": RATE_LIMIT_HOURS},
773 )
775 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
777 # Use callback pattern to handle circular dependency between FriendRelationship and ModerationState
778 friend_relationship = None
780 def create_friend_relationship(moderation_state_id: int) -> int:
781 nonlocal friend_relationship
782 friend_relationship = FriendRelationship(
783 from_user_id=user.id,
784 to_user_id=to_user.id,
785 status=FriendStatus.pending,
786 moderation_state_id=moderation_state_id,
787 )
788 session.add(friend_relationship)
789 session.flush()
790 return friend_relationship.id
792 moderation_state = create_moderation(
793 session, ModerationObjectType.friend_request, create_friend_relationship, context.user_id
794 )
796 assert friend_relationship is not None # set by create_friend_relationship callback
798 notify(
799 session,
800 user_id=friend_relationship.to_user_id,
801 topic_action=NotificationTopicAction.friend_request__create,
802 key=str(friend_relationship.from_user_id),
803 data=notification_data_pb2.FriendRequestCreate(
804 other_user=user_model_to_pb(
805 friend_relationship.from_user,
806 session,
807 make_notification_user_context(user_id=friend_relationship.to_user_id),
808 ),
809 ),
810 moderation_state_id=moderation_state.id,
811 )
812 log_event(context, session, "friendship.request_sent", {"to_user_id": to_user.id})
814 return empty_pb2.Empty()
816 def ListFriendRequests(
817 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
818 ) -> api_pb2.ListFriendRequestsRes:
819 # both sent and received
820 sent_requests_query = select(FriendRelationship)
821 sent_requests_query = where_users_column_visible(sent_requests_query, context, FriendRelationship.to_user_id)
822 sent_requests_query = sent_requests_query.where(FriendRelationship.from_user_id == context.user_id).where(
823 FriendRelationship.status == FriendStatus.pending
824 )
825 sent_requests_query = where_moderated_content_visible(
826 sent_requests_query, context, FriendRelationship, is_list_operation=True
827 )
828 sent_requests = session.execute(sent_requests_query).scalars().all()
830 received_requests_query = select(FriendRelationship)
831 received_requests_query = where_users_column_visible(
832 received_requests_query, context, FriendRelationship.from_user_id
833 )
834 received_requests_query = received_requests_query.where(FriendRelationship.to_user_id == context.user_id).where(
835 FriendRelationship.status == FriendStatus.pending
836 )
837 received_requests_query = where_moderated_content_visible(
838 received_requests_query, context, FriendRelationship, is_list_operation=True
839 )
840 received_requests = session.execute(received_requests_query).scalars().all()
842 return api_pb2.ListFriendRequestsRes(
843 sent=[
844 api_pb2.FriendRequest(
845 friend_request_id=friend_request.id,
846 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
847 user_id=friend_request.to_user.id,
848 sent=True,
849 )
850 for friend_request in sent_requests
851 ],
852 received=[
853 api_pb2.FriendRequest(
854 friend_request_id=friend_request.id,
855 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
856 user_id=friend_request.from_user.id,
857 sent=False,
858 )
859 for friend_request in received_requests
860 ],
861 )
863 def RespondFriendRequest(
864 self, request: api_pb2.RespondFriendRequestReq, context: CouchersContext, session: Session
865 ) -> empty_pb2.Empty:
866 friend_request_query = select(FriendRelationship)
867 friend_request_query = where_users_column_visible(
868 friend_request_query, context, FriendRelationship.from_user_id
869 )
870 friend_request_query = (
871 friend_request_query.where(FriendRelationship.to_user_id == context.user_id)
872 .where(FriendRelationship.status == FriendStatus.pending)
873 .where(FriendRelationship.id == request.friend_request_id)
874 )
875 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship)
876 friend_request = session.execute(friend_request_query).scalar_one_or_none()
878 if not friend_request: 878 ↛ 879line 878 didn't jump to line 879 because the condition on line 878 was never true
879 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found")
881 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
882 friend_request.time_responded = func.now()
884 session.flush()
886 if friend_request.status == FriendStatus.accepted:
887 notify(
888 session,
889 user_id=friend_request.from_user_id,
890 topic_action=NotificationTopicAction.friend_request__accept,
891 key=str(friend_request.to_user_id),
892 data=notification_data_pb2.FriendRequestAccept(
893 other_user=user_model_to_pb(
894 friend_request.to_user,
895 session,
896 make_notification_user_context(user_id=friend_request.from_user_id),
897 ),
898 ),
899 )
901 log_event(
902 context,
903 session,
904 "friendship.request_responded",
905 {"from_user_id": friend_request.from_user_id, "accepted": request.accept},
906 )
908 return empty_pb2.Empty()
910 def CancelFriendRequest(
911 self, request: api_pb2.CancelFriendRequestReq, context: CouchersContext, session: Session
912 ) -> empty_pb2.Empty:
913 friend_request_query = select(FriendRelationship)
914 friend_request_query = where_users_column_visible(friend_request_query, context, FriendRelationship.to_user_id)
915 friend_request_query = (
916 friend_request_query.where(FriendRelationship.from_user_id == context.user_id)
917 .where(FriendRelationship.status == FriendStatus.pending)
918 .where(FriendRelationship.id == request.friend_request_id)
919 )
920 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship)
921 friend_request = session.execute(friend_request_query).scalar_one_or_none()
923 if not friend_request: 923 ↛ 924line 923 didn't jump to line 924 because the condition on line 923 was never true
924 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found")
926 friend_request.status = FriendStatus.cancelled
927 friend_request.time_responded = func.now()
929 # note no notifications
930 log_event(context, session, "friendship.request_cancelled", {"to_user_id": friend_request.to_user_id})
932 session.commit()
934 return empty_pb2.Empty()
936 def InitiateMediaUpload(
937 self, request: empty_pb2.Empty, context: CouchersContext, session: Session
938 ) -> api_pb2.InitiateMediaUploadRes:
939 key = random_hex()
941 created = now()
942 expiry = created + timedelta(minutes=20)
944 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
945 session.add(upload)
946 session.commit()
948 req = media_pb2.UploadRequest(
949 key=upload.key,
950 type=media_pb2.UploadRequest.UploadType.IMAGE,
951 created=Timestamp_from_datetime(upload.created),
952 expiry=Timestamp_from_datetime(upload.expiry),
953 max_width=2000,
954 max_height=1600,
955 ).SerializeToString()
957 data = b64encode(req)
958 sig = b64encode(generate_hash_signature(req, config.MEDIA_SERVER_SECRET_KEY))
960 path = "upload?" + urlencode({"data": data, "sig": sig})
962 return api_pb2.InitiateMediaUploadRes(
963 upload_url=urls.media_upload_url(path=path),
964 expiry=Timestamp_from_datetime(expiry),
965 )
967 def ListBadgeUsers(
968 self, request: api_pb2.ListBadgeUsersReq, context: CouchersContext, session: Session
969 ) -> api_pb2.ListBadgeUsersRes:
970 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
971 next_user_id = int(request.page_token) if request.page_token else 0
972 badge = get_badge_dict().get(request.badge_id)
973 if not badge: 973 ↛ 974line 973 didn't jump to line 974 because the condition on line 973 was never true
974 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "badge_not_found")
976 badge_user_ids_query = (
977 select(UserBadge.user_id).where(UserBadge.badge_id == badge.id).where(UserBadge.user_id >= next_user_id)
978 )
979 badge_user_ids_query = where_users_column_visible(badge_user_ids_query, context, UserBadge.user_id)
980 badge_user_ids_query = badge_user_ids_query.order_by(UserBadge.user_id).limit(page_size + 1)
981 badge_user_ids = session.execute(badge_user_ids_query).scalars().all()
983 return api_pb2.ListBadgeUsersRes(
984 user_ids=badge_user_ids[:page_size],
985 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
986 )
989def response_rate_to_pb(response_rate: UserResponseRate | None) -> dict[str, google.protobuf.message.Message]:
990 if not response_rate:
991 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
993 # if n is None, the user is new, or they have no requests
994 if response_rate.requests < 3:
995 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
997 if response_rate.response_rate <= 0.33:
998 return {"low": requests_pb2.ResponseRateLow()}
1000 response_time_p33_coarsened = Duration_from_timedelta(
1001 timedelta(seconds=round(response_rate.response_time_33p.total_seconds() / 60) * 60)
1002 )
1004 if response_rate.response_rate <= 0.66:
1005 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
1007 response_time_p66_coarsened = Duration_from_timedelta(
1008 timedelta(seconds=round(response_rate.response_time_66p.total_seconds() / 60) * 60)
1009 )
1011 if response_rate.response_rate <= 0.90:
1012 return {
1013 "most": requests_pb2.ResponseRateMost(
1014 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
1015 )
1016 }
1017 else:
1018 return {
1019 "almost_all": requests_pb2.ResponseRateAlmostAll(
1020 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
1021 )
1022 }
1025def get_num_references(session: Session, context: CouchersContext, user_ids: Iterable[int]) -> dict[int, int]:
1026 query = where_moderated_content_visible(
1027 select(Reference.to_user_id, func.count(Reference.id)), context, Reference, is_list_operation=True
1028 )
1029 # exclude references still hidden by the reciprocal-reference rule, matching ListReferences
1030 query = where_references_not_hidden_by_reciprocity(query)
1031 query = (
1032 query.where(Reference.to_user_id.in_(user_ids))
1033 .join(User, User.id == Reference.from_user_id)
1034 .where(User.is_visible)
1035 .group_by(Reference.to_user_id)
1036 )
1037 return cast(dict[int, int], dict(session.execute(query).all())) # type: ignore[arg-type]
1040def user_model_to_pb(
1041 db_user: User,
1042 session: Session,
1043 context: CouchersContext,
1044 *,
1045 is_admin_see_ghosts: bool = False,
1046 is_get_user_return_ghosts: bool = False,
1047) -> api_pb2.User:
1048 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
1049 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context
1051 viewer_user_id = context.user_id if context.is_logged_in() else None
1052 if not is_admin_see_ghosts and is_not_visible(
1053 session, viewer_user_id, db_user.id, ignore_shadow=context.serialize_shadowed
1054 ):
1055 # User is not visible (deleted, banned, or blocked)
1056 if is_get_user_return_ghosts: 1056 ↛ 1071line 1056 didn't jump to line 1071 because the condition on line 1056 was always true
1057 maybe_log_nonvisible_user_access(
1058 context,
1059 db_user,
1060 access_type=NonvisibleUserAccessType.ghost_served,
1061 actor_user_id=viewer_user_id,
1062 )
1063 # Return an anonymized "ghost" user profile
1064 return api_pb2.User(
1065 user_id=db_user.id,
1066 is_ghost=True,
1067 username=GHOST_USERNAME,
1068 name=context.localization.localize_string("ghost_users.display_name"),
1069 about_me=context.localization.localize_string("ghost_users.about_me"),
1070 )
1071 raise GhostUserSerializationError(
1072 f"Tried to serialize ghost profile in user_model_to_pb without appropriate flags. "
1073 f"Context user_id: {context.user_id}, db_user id: {db_user.id} (username: {db_user.username})"
1074 )
1076 num_references = get_num_references(session, context, [db_user.id]).get(db_user.id, 0)
1077 lat, lng = db_user.coordinates
1079 pending_friend_request = None
1080 if context.is_logged_out() or db_user.id == context.user_id:
1081 friends_status = api_pb2.User.FriendshipStatus.NA
1082 else:
1083 friend_relationship = session.execute(
1084 where_moderated_content_visible(
1085 select(FriendRelationship)
1086 .where(
1087 or_(
1088 and_(
1089 FriendRelationship.from_user_id == context.user_id,
1090 FriendRelationship.to_user_id == db_user.id,
1091 ),
1092 and_(
1093 FriendRelationship.from_user_id == db_user.id,
1094 FriendRelationship.to_user_id == context.user_id,
1095 ),
1096 )
1097 )
1098 .where(
1099 or_(
1100 FriendRelationship.status == FriendStatus.accepted,
1101 FriendRelationship.status == FriendStatus.pending,
1102 )
1103 ),
1104 context,
1105 FriendRelationship,
1106 )
1107 ).scalar_one_or_none()
1109 if friend_relationship:
1110 if friend_relationship.status == FriendStatus.accepted:
1111 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
1112 else:
1113 friends_status = api_pb2.User.FriendshipStatus.PENDING
1114 if friend_relationship.from_user_id == context.user_id: 1114 ↛ 1116line 1114 didn't jump to line 1116 because the condition on line 1114 was never true
1115 # we sent it
1116 pending_friend_request = api_pb2.FriendRequest(
1117 friend_request_id=friend_relationship.id,
1118 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
1119 user_id=friend_relationship.to_user.id,
1120 sent=True,
1121 )
1122 else:
1123 # we received it
1124 pending_friend_request = api_pb2.FriendRequest(
1125 friend_request_id=friend_relationship.id,
1126 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
1127 user_id=friend_relationship.from_user.id,
1128 sent=False,
1129 )
1130 else:
1131 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
1133 response_rate = session.execute(
1134 select(UserResponseRate).where(UserResponseRate.user_id == db_user.id)
1135 ).scalar_one_or_none()
1137 avatar_upload = get_avatar_upload(session, db_user)
1139 verification_score = 0.0
1140 if db_user.phone_verification_verified:
1141 verification_score += 1.0 * db_user.phone_is_verified
1143 user = api_pb2.User(
1144 user_id=db_user.id,
1145 username=db_user.username,
1146 name=db_user.name,
1147 city=db_user.city,
1148 hometown=db_user.hometown,
1149 timezone=db_user.timezone,
1150 lat=lat,
1151 lng=lng,
1152 radius=db_user.geom_radius,
1153 verification=verification_score,
1154 community_standing=db_user.community_standing,
1155 num_references=num_references,
1156 gender=db_user.gender,
1157 pronouns=db_user.pronouns,
1158 age=int(db_user.age),
1159 joined=Timestamp_from_datetime(db_user.display_joined),
1160 last_active=Timestamp_from_datetime(db_user.display_last_active),
1161 hosting_status=hostingstatus2api[db_user.hosting_status],
1162 meetup_status=meetupstatus2api[db_user.meetup_status],
1163 occupation=db_user.occupation,
1164 education=db_user.education,
1165 about_me=db_user.about_me,
1166 things_i_like=db_user.things_i_like,
1167 about_place=db_user.about_place,
1168 language_abilities=[
1169 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
1170 for ability in db_user.language_abilities
1171 ],
1172 regions_visited=[region.code for region in db_user.regions_visited],
1173 regions_lived=[region.code for region in db_user.regions_lived],
1174 additional_information=db_user.additional_information,
1175 friends=friends_status,
1176 pending_friend_request=pending_friend_request,
1177 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
1178 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
1179 parking_details=parkingdetails2api[db_user.parking_details],
1180 avatar_url=avatar_upload.full_url if avatar_upload else None,
1181 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None,
1182 profile_gallery_id=db_user.profile_gallery_id,
1183 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id))
1184 .scalars()
1185 .all(),
1186 **get_strong_verification_fields(session, db_user),
1187 **response_rate_to_pb(response_rate), # type: ignore[arg-type]
1188 )
1190 if db_user.max_guests is not None:
1191 user.max_guests.value = db_user.max_guests
1193 if db_user.last_minute is not None:
1194 user.last_minute.value = db_user.last_minute
1196 if db_user.has_pets is not None:
1197 user.has_pets.value = db_user.has_pets
1199 if db_user.accepts_pets is not None:
1200 user.accepts_pets.value = db_user.accepts_pets
1202 if db_user.pet_details is not None:
1203 user.pet_details.value = db_user.pet_details
1205 if db_user.has_kids is not None:
1206 user.has_kids.value = db_user.has_kids
1208 if db_user.accepts_kids is not None:
1209 user.accepts_kids.value = db_user.accepts_kids
1211 if db_user.kid_details is not None:
1212 user.kid_details.value = db_user.kid_details
1214 if db_user.has_housemates is not None:
1215 user.has_housemates.value = db_user.has_housemates
1217 if db_user.housemate_details is not None:
1218 user.housemate_details.value = db_user.housemate_details
1220 if db_user.wheelchair_accessible is not None:
1221 user.wheelchair_accessible.value = db_user.wheelchair_accessible
1223 if db_user.smokes_at_home is not None:
1224 user.smokes_at_home.value = db_user.smokes_at_home
1226 if db_user.drinking_allowed is not None:
1227 user.drinking_allowed.value = db_user.drinking_allowed
1229 if db_user.drinks_at_home is not None:
1230 user.drinks_at_home.value = db_user.drinks_at_home
1232 if db_user.other_host_info is not None:
1233 user.other_host_info.value = db_user.other_host_info
1235 if db_user.sleeping_details is not None:
1236 user.sleeping_details.value = db_user.sleeping_details
1238 if db_user.area is not None:
1239 user.area.value = db_user.area
1241 if db_user.house_rules is not None:
1242 user.house_rules.value = db_user.house_rules
1244 if db_user.parking is not None:
1245 user.parking.value = db_user.parking
1247 if db_user.camping_ok is not None:
1248 user.camping_ok.value = db_user.camping_ok
1250 return user
1253def lite_user_to_pb(
1254 session: Session,
1255 lite_user: LiteUser,
1256 context: CouchersContext,
1257 *,
1258 is_admin_see_ghosts: bool = False,
1259 is_get_user_return_ghosts: bool = False,
1260) -> api_pb2.LiteUser:
1261 if not is_admin_see_ghosts and is_not_visible(
1262 session, context.user_id, lite_user.id, ignore_shadow=context.serialize_shadowed
1263 ):
1264 # User is not visible (deleted, banned, or blocked)
1265 if is_get_user_return_ghosts: 1265 ↛ 1273line 1265 didn't jump to line 1273 because the condition on line 1265 was always true
1266 # Return an anonymized "ghost" user profile
1267 return api_pb2.LiteUser(
1268 user_id=lite_user.id,
1269 is_ghost=True,
1270 username=GHOST_USERNAME,
1271 name=context.localization.localize_string("ghost_users.display_name"),
1272 )
1273 raise GhostUserSerializationError(
1274 f"Tried to serialize ghost profile in lite_user_to_pb without appropriate flags. "
1275 f"Context user_id: {context.user_id}, lite_user id: {lite_user.id} (username: {lite_user.username})"
1276 )
1278 lat, lng = get_coordinates(lite_user.geom)
1280 return api_pb2.LiteUser(
1281 user_id=lite_user.id,
1282 username=lite_user.username,
1283 name=lite_user.name,
1284 city=lite_user.city,
1285 age=int(lite_user.age),
1286 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1287 if lite_user.avatar_filename
1288 else None,
1289 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1290 if lite_user.avatar_filename
1291 else None,
1292 lat=lat,
1293 lng=lng,
1294 radius=lite_user.radius,
1295 has_strong_verification=lite_user.has_strong_verification,
1296 )