Coverage for src/couchers/servicers/api.py: 97%
398 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-07-14 16:54 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-07-14 16:54 +0000
1from datetime import timedelta
2from urllib.parse import urlencode
4import grpc
5from google.protobuf import empty_pb2
6from sqlalchemy.sql import and_, delete, distinct, func, intersect, or_, union
8from couchers import errors, urls
9from couchers.config import config
10from couchers.crypto import b64encode, generate_hash_signature, random_hex
11from couchers.materialized_views import lite_users, user_response_rates
12from couchers.models import (
13 FriendRelationship,
14 FriendStatus,
15 GroupChatSubscription,
16 HostingStatus,
17 HostRequest,
18 InitiatedUpload,
19 LanguageAbility,
20 LanguageFluency,
21 MeetupStatus,
22 Message,
23 Notification,
24 NotificationDeliveryType,
25 ParkingDetails,
26 RateLimitAction,
27 Reference,
28 RegionLived,
29 RegionVisited,
30 SleepingArrangement,
31 SmokingLocation,
32 User,
33 UserBadge,
34)
35from couchers.notifications.notify import notify
36from couchers.notifications.settings import get_topic_actions_by_delivery_type
37from couchers.rate_limits.check import process_rate_limits_and_check_abort
38from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed
39from couchers.servicers.account import get_strong_verification_fields
40from couchers.sql import couchers_select as select
41from couchers.sql import is_valid_user_id, is_valid_username
42from couchers.utils import (
43 Duration_from_timedelta,
44 Timestamp_from_datetime,
45 create_coordinate,
46 get_coordinates,
47 is_valid_name,
48 now,
49)
50from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
52MAX_USERS_PER_QUERY = 200
53MAX_PAGINATION_LENGTH = 50
55hostingstatus2sql = {
56 api_pb2.HOSTING_STATUS_UNKNOWN: None,
57 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
58 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
59 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
60}
62hostingstatus2api = {
63 None: api_pb2.HOSTING_STATUS_UNKNOWN,
64 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
65 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
66 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
67}
69meetupstatus2sql = {
70 api_pb2.MEETUP_STATUS_UNKNOWN: None,
71 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
72 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
73 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
74}
76meetupstatus2api = {
77 None: api_pb2.MEETUP_STATUS_UNKNOWN,
78 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
79 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
80 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
81}
83smokinglocation2sql = {
84 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
85 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
86 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
87 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
88 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
89}
91smokinglocation2api = {
92 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
93 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
94 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
95 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
96 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
97}
99sleepingarrangement2sql = {
100 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
101 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
102 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
103 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
104}
106sleepingarrangement2api = {
107 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
108 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
109 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
110 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
111}
113parkingdetails2sql = {
114 api_pb2.PARKING_DETAILS_UNKNOWN: None,
115 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
116 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
117 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
118 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
119}
121parkingdetails2api = {
122 None: api_pb2.PARKING_DETAILS_UNKNOWN,
123 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
124 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
125 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
126 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
127}
129fluency2sql = {
130 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
131 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
132 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
133 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
134}
136fluency2api = {
137 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
138 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
139 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
140 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
141}
144class API(api_pb2_grpc.APIServicer):
145 def Ping(self, request, context, session):
146 # auth ought to make sure the user exists
147 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
149 sent_reqs_last_seen_message_ids = (
150 select(HostRequest.conversation_id, HostRequest.surfer_last_seen_message_id)
151 .where(HostRequest.surfer_user_id == context.user_id)
152 .where_users_column_visible(context, HostRequest.host_user_id)
153 ).subquery()
155 unseen_sent_host_request_count = session.execute(
156 select(func.count(distinct(sent_reqs_last_seen_message_ids.c.conversation_id)))
157 .join(
158 Message,
159 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id,
160 )
161 .where(sent_reqs_last_seen_message_ids.c.surfer_last_seen_message_id < Message.id)
162 .where(Message.id != None)
163 ).scalar_one()
165 received_reqs_last_seen_message_ids = (
166 select(HostRequest.conversation_id, HostRequest.host_last_seen_message_id)
167 .where(HostRequest.host_user_id == context.user_id)
168 .where_users_column_visible(context, HostRequest.surfer_user_id)
169 ).subquery()
171 unseen_received_host_request_count = session.execute(
172 select(func.count(distinct(received_reqs_last_seen_message_ids.c.conversation_id)))
173 .join(
174 Message,
175 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id,
176 )
177 .where(received_reqs_last_seen_message_ids.c.host_last_seen_message_id < Message.id)
178 .where(Message.id != None)
179 ).scalar_one()
181 unseen_message_count = session.execute(
182 select(func.count(Message.id))
183 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
184 .where(GroupChatSubscription.user_id == context.user_id)
185 .where(Message.time >= GroupChatSubscription.joined)
186 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
187 .where(Message.id > GroupChatSubscription.last_seen_message_id)
188 ).scalar_one()
190 pending_friend_request_count = session.execute(
191 select(func.count(FriendRelationship.id))
192 .where(FriendRelationship.to_user_id == context.user_id)
193 .where_users_column_visible(context, FriendRelationship.from_user_id)
194 .where(FriendRelationship.status == FriendStatus.pending)
195 ).scalar_one()
197 unseen_notification_count = session.execute(
198 select(func.count())
199 .select_from(Notification)
200 .where(Notification.user_id == context.user_id)
201 .where(Notification.is_seen == False)
202 .where(
203 Notification.topic_action.in_(
204 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push)
205 )
206 )
207 ).scalar_one()
209 return api_pb2.PingRes(
210 user=user_model_to_pb(user, session, context),
211 unseen_message_count=unseen_message_count,
212 unseen_sent_host_request_count=unseen_sent_host_request_count,
213 unseen_received_host_request_count=unseen_received_host_request_count,
214 pending_friend_request_count=pending_friend_request_count,
215 unseen_notification_count=unseen_notification_count,
216 )
218 def GetUser(self, request, context, session):
219 user = session.execute(
220 select(User).where_users_visible(context).where_username_or_id(request.user)
221 ).scalar_one_or_none()
223 if not user:
224 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
226 return user_model_to_pb(user, session, context)
228 def GetLiteUser(self, request, context, session):
229 lite_user = session.execute(
230 select(lite_users)
231 .where_users_visible(context, table=lite_users.c)
232 .where_username_or_id(request.user, table=lite_users.c)
233 ).one_or_none()
235 if not lite_user:
236 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
238 return lite_user_to_pb(lite_user)
240 def GetLiteUsers(self, request, context, session):
241 if len(request.users) > MAX_USERS_PER_QUERY:
242 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
244 usernames = {u for u in request.users if is_valid_username(u)}
245 ids = {u for u in request.users if is_valid_user_id(u)}
247 users = session.execute(
248 select(lite_users)
249 .where_users_visible(context, table=lite_users.c)
250 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
251 ).all()
253 users_by_id = {str(user.id): user for user in users}
254 users_by_username = {user.username: user for user in users}
256 res = api_pb2.GetLiteUsersRes()
258 for user in request.users:
259 lite_user = None
260 if user in users_by_id:
261 lite_user = users_by_id[user]
262 elif user in users_by_username:
263 lite_user = users_by_username[user]
265 res.responses.append(
266 api_pb2.LiteUserRes(
267 query=user,
268 not_found=lite_user is None,
269 user=lite_user_to_pb(lite_user) if lite_user else None,
270 )
271 )
273 return res
275 def UpdateProfile(self, request, context, session):
276 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
278 if request.HasField("name"):
279 if not is_valid_name(request.name.value):
280 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
281 user.name = request.name.value
283 if request.HasField("city"):
284 user.city = request.city.value
286 if request.HasField("hometown"):
287 if request.hometown.is_null:
288 user.hometown = None
289 else:
290 user.hometown = request.hometown.value
292 if request.HasField("lat") and request.HasField("lng"):
293 if request.lat.value == 0 and request.lng.value == 0:
294 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
295 user.geom = create_coordinate(request.lat.value, request.lng.value)
296 user.randomized_geom = None
298 if request.HasField("radius"):
299 user.geom_radius = request.radius.value
301 if request.HasField("avatar_key"):
302 if request.avatar_key.is_null:
303 user.avatar_key = None
304 else:
305 user.avatar_key = request.avatar_key.value
307 # if request.HasField("gender"):
308 # user.gender = request.gender.value
310 if request.HasField("pronouns"):
311 if request.pronouns.is_null:
312 user.pronouns = None
313 else:
314 user.pronouns = request.pronouns.value
316 if request.HasField("occupation"):
317 if request.occupation.is_null:
318 user.occupation = None
319 else:
320 user.occupation = request.occupation.value
322 if request.HasField("education"):
323 if request.education.is_null:
324 user.education = None
325 else:
326 user.education = request.education.value
328 if request.HasField("about_me"):
329 if request.about_me.is_null:
330 user.about_me = None
331 else:
332 user.about_me = request.about_me.value
334 if request.HasField("things_i_like"):
335 if request.things_i_like.is_null:
336 user.things_i_like = None
337 else:
338 user.things_i_like = request.things_i_like.value
340 if request.HasField("about_place"):
341 if request.about_place.is_null:
342 user.about_place = None
343 else:
344 user.about_place = request.about_place.value
346 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
347 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
348 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
349 user.hosting_status = hostingstatus2sql[request.hosting_status]
351 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
352 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
353 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
354 user.meetup_status = meetupstatus2sql[request.meetup_status]
356 if request.HasField("language_abilities"):
357 # delete all existing abilities
358 for ability in user.language_abilities:
359 session.delete(ability)
360 session.flush()
362 # add the new ones
363 for language_ability in request.language_abilities.value:
364 if not language_is_allowed(language_ability.code):
365 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
366 session.add(
367 LanguageAbility(
368 user=user,
369 language_code=language_ability.code,
370 fluency=fluency2sql[language_ability.fluency],
371 )
372 )
374 if request.HasField("regions_visited"):
375 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
377 for region in request.regions_visited.value:
378 if not region_is_allowed(region):
379 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
380 session.add(
381 RegionVisited(
382 user_id=user.id,
383 region_code=region,
384 )
385 )
387 if request.HasField("regions_lived"):
388 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
390 for region in request.regions_lived.value:
391 if not region_is_allowed(region):
392 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
393 session.add(
394 RegionLived(
395 user_id=user.id,
396 region_code=region,
397 )
398 )
400 if request.HasField("additional_information"):
401 if request.additional_information.is_null:
402 user.additional_information = None
403 else:
404 user.additional_information = request.additional_information.value
406 if request.HasField("max_guests"):
407 if request.max_guests.is_null:
408 user.max_guests = None
409 else:
410 user.max_guests = request.max_guests.value
412 if request.HasField("last_minute"):
413 if request.last_minute.is_null:
414 user.last_minute = None
415 else:
416 user.last_minute = request.last_minute.value
418 if request.HasField("has_pets"):
419 if request.has_pets.is_null:
420 user.has_pets = None
421 else:
422 user.has_pets = request.has_pets.value
424 if request.HasField("accepts_pets"):
425 if request.accepts_pets.is_null:
426 user.accepts_pets = None
427 else:
428 user.accepts_pets = request.accepts_pets.value
430 if request.HasField("pet_details"):
431 if request.pet_details.is_null:
432 user.pet_details = None
433 else:
434 user.pet_details = request.pet_details.value
436 if request.HasField("has_kids"):
437 if request.has_kids.is_null:
438 user.has_kids = None
439 else:
440 user.has_kids = request.has_kids.value
442 if request.HasField("accepts_kids"):
443 if request.accepts_kids.is_null:
444 user.accepts_kids = None
445 else:
446 user.accepts_kids = request.accepts_kids.value
448 if request.HasField("kid_details"):
449 if request.kid_details.is_null:
450 user.kid_details = None
451 else:
452 user.kid_details = request.kid_details.value
454 if request.HasField("has_housemates"):
455 if request.has_housemates.is_null:
456 user.has_housemates = None
457 else:
458 user.has_housemates = request.has_housemates.value
460 if request.HasField("housemate_details"):
461 if request.housemate_details.is_null:
462 user.housemate_details = None
463 else:
464 user.housemate_details = request.housemate_details.value
466 if request.HasField("wheelchair_accessible"):
467 if request.wheelchair_accessible.is_null:
468 user.wheelchair_accessible = None
469 else:
470 user.wheelchair_accessible = request.wheelchair_accessible.value
472 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
473 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
475 if request.HasField("smokes_at_home"):
476 if request.smokes_at_home.is_null:
477 user.smokes_at_home = None
478 else:
479 user.smokes_at_home = request.smokes_at_home.value
481 if request.HasField("drinking_allowed"):
482 if request.drinking_allowed.is_null:
483 user.drinking_allowed = None
484 else:
485 user.drinking_allowed = request.drinking_allowed.value
487 if request.HasField("drinks_at_home"):
488 if request.drinks_at_home.is_null:
489 user.drinks_at_home = None
490 else:
491 user.drinks_at_home = request.drinks_at_home.value
493 if request.HasField("other_host_info"):
494 if request.other_host_info.is_null:
495 user.other_host_info = None
496 else:
497 user.other_host_info = request.other_host_info.value
499 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
500 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
502 if request.HasField("sleeping_details"):
503 if request.sleeping_details.is_null:
504 user.sleeping_details = None
505 else:
506 user.sleeping_details = request.sleeping_details.value
508 if request.HasField("area"):
509 if request.area.is_null:
510 user.area = None
511 else:
512 user.area = request.area.value
514 if request.HasField("house_rules"):
515 if request.house_rules.is_null:
516 user.house_rules = None
517 else:
518 user.house_rules = request.house_rules.value
520 if request.HasField("parking"):
521 if request.parking.is_null:
522 user.parking = None
523 else:
524 user.parking = request.parking.value
526 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
527 user.parking_details = parkingdetails2sql[request.parking_details]
529 if request.HasField("camping_ok"):
530 if request.camping_ok.is_null:
531 user.camping_ok = None
532 else:
533 user.camping_ok = request.camping_ok.value
535 return empty_pb2.Empty()
537 def ListFriends(self, request, context, session):
538 rels = (
539 session.execute(
540 select(FriendRelationship)
541 .where_users_column_visible(context, FriendRelationship.from_user_id)
542 .where_users_column_visible(context, FriendRelationship.to_user_id)
543 .where(
544 or_(
545 FriendRelationship.from_user_id == context.user_id,
546 FriendRelationship.to_user_id == context.user_id,
547 )
548 )
549 .where(FriendRelationship.status == FriendStatus.accepted)
550 )
551 .scalars()
552 .all()
553 )
554 return api_pb2.ListFriendsRes(
555 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
556 )
558 def RemoveFriend(self, request, context, session):
559 rel = session.execute(
560 select(FriendRelationship)
561 .where_users_column_visible(context, FriendRelationship.from_user_id)
562 .where_users_column_visible(context, FriendRelationship.to_user_id)
563 .where(
564 or_(
565 and_(
566 FriendRelationship.from_user_id == request.user_id,
567 FriendRelationship.to_user_id == context.user_id,
568 ),
569 and_(
570 FriendRelationship.from_user_id == context.user_id,
571 FriendRelationship.to_user_id == request.user_id,
572 ),
573 )
574 )
575 .where(FriendRelationship.status == FriendStatus.accepted)
576 ).scalar_one_or_none()
578 if not rel:
579 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.NOT_FRIENDS)
581 session.delete(rel)
583 return empty_pb2.Empty()
585 def ListMutualFriends(self, request, context, session):
586 if context.user_id == request.user_id:
587 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
589 user = session.execute(
590 select(User).where_users_visible(context).where(User.id == request.user_id)
591 ).scalar_one_or_none()
593 if not user:
594 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
596 q1 = (
597 select(FriendRelationship.from_user_id.label("user_id"))
598 .where(FriendRelationship.to_user_id == context.user_id)
599 .where(FriendRelationship.from_user_id != request.user_id)
600 .where(FriendRelationship.status == FriendStatus.accepted)
601 )
603 q2 = (
604 select(FriendRelationship.to_user_id.label("user_id"))
605 .where(FriendRelationship.from_user_id == context.user_id)
606 .where(FriendRelationship.to_user_id != request.user_id)
607 .where(FriendRelationship.status == FriendStatus.accepted)
608 )
610 q3 = (
611 select(FriendRelationship.from_user_id.label("user_id"))
612 .where(FriendRelationship.to_user_id == request.user_id)
613 .where(FriendRelationship.from_user_id != context.user_id)
614 .where(FriendRelationship.status == FriendStatus.accepted)
615 )
617 q4 = (
618 select(FriendRelationship.to_user_id.label("user_id"))
619 .where(FriendRelationship.from_user_id == request.user_id)
620 .where(FriendRelationship.to_user_id != context.user_id)
621 .where(FriendRelationship.status == FriendStatus.accepted)
622 )
624 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
626 mutual_friends = (
627 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
628 )
630 return api_pb2.ListMutualFriendsRes(
631 mutual_friends=[
632 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
633 for mutual_friend in mutual_friends
634 ]
635 )
637 def SendFriendRequest(self, request, context, session):
638 if context.user_id == request.user_id:
639 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
641 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
642 to_user = session.execute(
643 select(User).where_users_visible(context).where(User.id == request.user_id)
644 ).scalar_one_or_none()
646 if not to_user:
647 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
649 if (
650 session.execute(
651 select(FriendRelationship)
652 .where(
653 or_(
654 and_(
655 FriendRelationship.from_user_id == context.user_id,
656 FriendRelationship.to_user_id == request.user_id,
657 ),
658 and_(
659 FriendRelationship.from_user_id == request.user_id,
660 FriendRelationship.to_user_id == context.user_id,
661 ),
662 )
663 )
664 .where(
665 or_(
666 FriendRelationship.status == FriendStatus.accepted,
667 FriendRelationship.status == FriendStatus.pending,
668 )
669 )
670 ).scalar_one_or_none()
671 is not None
672 ):
673 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
675 # Check if user has been sending friend requests excessively
676 if process_rate_limits_and_check_abort(
677 session=session, user_id=context.user_id, action=RateLimitAction.friend_request
678 ):
679 context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, errors.FRIEND_REQUEST_RATE_LIMIT)
681 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
683 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
684 session.add(friend_relationship)
685 session.flush()
687 notify(
688 session,
689 user_id=friend_relationship.to_user_id,
690 topic_action="friend_request:create",
691 key=friend_relationship.from_user_id,
692 data=notification_data_pb2.FriendRequestCreate(
693 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
694 ),
695 )
697 return empty_pb2.Empty()
699 def ListFriendRequests(self, request, context, session):
700 # both sent and received
701 sent_requests = (
702 session.execute(
703 select(FriendRelationship)
704 .where_users_column_visible(context, FriendRelationship.to_user_id)
705 .where(FriendRelationship.from_user_id == context.user_id)
706 .where(FriendRelationship.status == FriendStatus.pending)
707 )
708 .scalars()
709 .all()
710 )
712 received_requests = (
713 session.execute(
714 select(FriendRelationship)
715 .where_users_column_visible(context, FriendRelationship.from_user_id)
716 .where(FriendRelationship.to_user_id == context.user_id)
717 .where(FriendRelationship.status == FriendStatus.pending)
718 )
719 .scalars()
720 .all()
721 )
723 return api_pb2.ListFriendRequestsRes(
724 sent=[
725 api_pb2.FriendRequest(
726 friend_request_id=friend_request.id,
727 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
728 user_id=friend_request.to_user.id,
729 sent=True,
730 )
731 for friend_request in sent_requests
732 ],
733 received=[
734 api_pb2.FriendRequest(
735 friend_request_id=friend_request.id,
736 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
737 user_id=friend_request.from_user.id,
738 sent=False,
739 )
740 for friend_request in received_requests
741 ],
742 )
744 def RespondFriendRequest(self, request, context, session):
745 friend_request = session.execute(
746 select(FriendRelationship)
747 .where_users_column_visible(context, FriendRelationship.from_user_id)
748 .where(FriendRelationship.to_user_id == context.user_id)
749 .where(FriendRelationship.status == FriendStatus.pending)
750 .where(FriendRelationship.id == request.friend_request_id)
751 ).scalar_one_or_none()
753 if not friend_request:
754 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
756 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
757 friend_request.time_responded = func.now()
759 session.flush()
761 if friend_request.status == FriendStatus.accepted:
762 notify(
763 session,
764 user_id=friend_request.from_user_id,
765 topic_action="friend_request:accept",
766 key=friend_request.to_user_id,
767 data=notification_data_pb2.FriendRequestAccept(
768 other_user=user_model_to_pb(friend_request.to_user, session, context),
769 ),
770 )
772 return empty_pb2.Empty()
774 def CancelFriendRequest(self, request, context, session):
775 friend_request = session.execute(
776 select(FriendRelationship)
777 .where_users_column_visible(context, FriendRelationship.to_user_id)
778 .where(FriendRelationship.from_user_id == context.user_id)
779 .where(FriendRelationship.status == FriendStatus.pending)
780 .where(FriendRelationship.id == request.friend_request_id)
781 ).scalar_one_or_none()
783 if not friend_request:
784 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
786 friend_request.status = FriendStatus.cancelled
787 friend_request.time_responded = func.now()
789 # note no notifications
791 session.commit()
793 return empty_pb2.Empty()
795 def InitiateMediaUpload(self, request, context, session):
796 key = random_hex()
798 created = now()
799 expiry = created + timedelta(minutes=20)
801 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
802 session.add(upload)
803 session.commit()
805 req = media_pb2.UploadRequest(
806 key=upload.key,
807 type=media_pb2.UploadRequest.UploadType.IMAGE,
808 created=Timestamp_from_datetime(upload.created),
809 expiry=Timestamp_from_datetime(upload.expiry),
810 max_width=2000,
811 max_height=1600,
812 ).SerializeToString()
814 data = b64encode(req)
815 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
817 path = "upload?" + urlencode({"data": data, "sig": sig})
819 return api_pb2.InitiateMediaUploadRes(
820 upload_url=urls.media_upload_url(path=path),
821 expiry=Timestamp_from_datetime(expiry),
822 )
824 def ListBadgeUsers(self, request, context, session):
825 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
826 next_user_id = int(request.page_token) if request.page_token else 0
827 badge = get_badge_dict().get(request.badge_id)
828 if not badge:
829 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND)
831 badge_user_ids = (
832 session.execute(
833 select(UserBadge.user_id)
834 .where(UserBadge.badge_id == badge["id"])
835 .where(UserBadge.user_id >= next_user_id)
836 .order_by(UserBadge.user_id)
837 .limit(page_size + 1)
838 )
839 .scalars()
840 .all()
841 )
843 return api_pb2.ListBadgeUsersRes(
844 user_ids=badge_user_ids[:page_size],
845 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
846 )
849def response_rate_to_pb(response_rates):
850 if not response_rates:
851 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
853 _, n, response_rate, _, response_time_p33, response_time_p66 = response_rates
855 # if n is None, the user is new or they have no requests
856 if not n or n < 3:
857 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
859 if response_rate <= 0.33:
860 return {"low": requests_pb2.ResponseRateLow()}
862 response_time_p33_coarsened = Duration_from_timedelta(
863 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60)
864 )
866 if response_rate <= 0.66:
867 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
869 response_time_p66_coarsened = Duration_from_timedelta(
870 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60)
871 )
873 if response_rate <= 0.90:
874 return {
875 "most": requests_pb2.ResponseRateMost(
876 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
877 )
878 }
879 else:
880 return {
881 "almost_all": requests_pb2.ResponseRateAlmostAll(
882 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
883 )
884 }
887def get_num_references(session, user_ids):
888 return dict(
889 session.execute(
890 select(Reference.to_user_id, func.count(Reference.id))
891 .where(Reference.to_user_id.in_(user_ids))
892 .where(Reference.is_deleted == False)
893 .join(User, User.id == Reference.from_user_id)
894 .where(User.is_visible)
895 .group_by(Reference.to_user_id)
896 ).all()
897 )
900def user_model_to_pb(db_user, session, context):
901 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
902 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context
903 num_references = get_num_references(session, [db_user.id]).get(db_user.id, 0)
905 # returns (lat, lng)
906 # we put people without coords on null island
907 # https://en.wikipedia.org/wiki/Null_Island
908 lat, lng = db_user.coordinates or (0, 0)
910 pending_friend_request = None
911 if db_user.id == context.user_id:
912 friends_status = api_pb2.User.FriendshipStatus.NA
913 else:
914 friend_relationship = session.execute(
915 select(FriendRelationship)
916 .where(
917 or_(
918 and_(
919 FriendRelationship.from_user_id == context.user_id,
920 FriendRelationship.to_user_id == db_user.id,
921 ),
922 and_(
923 FriendRelationship.from_user_id == db_user.id,
924 FriendRelationship.to_user_id == context.user_id,
925 ),
926 )
927 )
928 .where(
929 or_(
930 FriendRelationship.status == FriendStatus.accepted,
931 FriendRelationship.status == FriendStatus.pending,
932 )
933 )
934 ).scalar_one_or_none()
936 if friend_relationship:
937 if friend_relationship.status == FriendStatus.accepted:
938 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
939 else:
940 friends_status = api_pb2.User.FriendshipStatus.PENDING
941 if friend_relationship.from_user_id == context.user_id:
942 # we sent it
943 pending_friend_request = api_pb2.FriendRequest(
944 friend_request_id=friend_relationship.id,
945 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
946 user_id=friend_relationship.to_user.id,
947 sent=True,
948 )
949 else:
950 # we received it
951 pending_friend_request = api_pb2.FriendRequest(
952 friend_request_id=friend_relationship.id,
953 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
954 user_id=friend_relationship.from_user.id,
955 sent=False,
956 )
957 else:
958 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
960 response_rates = session.execute(
961 select(user_response_rates).where(user_response_rates.c.user_id == db_user.id)
962 ).one_or_none()
964 verification_score = 0.0
965 if db_user.phone_verification_verified:
966 verification_score += 1.0 * db_user.phone_is_verified
968 user = api_pb2.User(
969 user_id=db_user.id,
970 username=db_user.username,
971 name=db_user.name,
972 city=db_user.city,
973 hometown=db_user.hometown,
974 timezone=db_user.timezone,
975 lat=lat,
976 lng=lng,
977 radius=db_user.geom_radius,
978 verification=verification_score,
979 community_standing=db_user.community_standing,
980 num_references=num_references,
981 gender=db_user.gender,
982 pronouns=db_user.pronouns,
983 age=int(db_user.age),
984 joined=Timestamp_from_datetime(db_user.display_joined),
985 last_active=Timestamp_from_datetime(db_user.display_last_active),
986 hosting_status=hostingstatus2api[db_user.hosting_status],
987 meetup_status=meetupstatus2api[db_user.meetup_status],
988 occupation=db_user.occupation,
989 education=db_user.education,
990 about_me=db_user.about_me,
991 things_i_like=db_user.things_i_like,
992 about_place=db_user.about_place,
993 language_abilities=[
994 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
995 for ability in db_user.language_abilities
996 ],
997 regions_visited=[region.code for region in db_user.regions_visited],
998 regions_lived=[region.code for region in db_user.regions_lived],
999 additional_information=db_user.additional_information,
1000 friends=friends_status,
1001 pending_friend_request=pending_friend_request,
1002 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
1003 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
1004 parking_details=parkingdetails2api[db_user.parking_details],
1005 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
1006 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
1007 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id))
1008 .scalars()
1009 .all(),
1010 **get_strong_verification_fields(session, db_user),
1011 **response_rate_to_pb(response_rates),
1012 )
1014 if db_user.max_guests is not None:
1015 user.max_guests.value = db_user.max_guests
1017 if db_user.last_minute is not None:
1018 user.last_minute.value = db_user.last_minute
1020 if db_user.has_pets is not None:
1021 user.has_pets.value = db_user.has_pets
1023 if db_user.accepts_pets is not None:
1024 user.accepts_pets.value = db_user.accepts_pets
1026 if db_user.pet_details is not None:
1027 user.pet_details.value = db_user.pet_details
1029 if db_user.has_kids is not None:
1030 user.has_kids.value = db_user.has_kids
1032 if db_user.accepts_kids is not None:
1033 user.accepts_kids.value = db_user.accepts_kids
1035 if db_user.kid_details is not None:
1036 user.kid_details.value = db_user.kid_details
1038 if db_user.has_housemates is not None:
1039 user.has_housemates.value = db_user.has_housemates
1041 if db_user.housemate_details is not None:
1042 user.housemate_details.value = db_user.housemate_details
1044 if db_user.wheelchair_accessible is not None:
1045 user.wheelchair_accessible.value = db_user.wheelchair_accessible
1047 if db_user.smokes_at_home is not None:
1048 user.smokes_at_home.value = db_user.smokes_at_home
1050 if db_user.drinking_allowed is not None:
1051 user.drinking_allowed.value = db_user.drinking_allowed
1053 if db_user.drinks_at_home is not None:
1054 user.drinks_at_home.value = db_user.drinks_at_home
1056 if db_user.other_host_info is not None:
1057 user.other_host_info.value = db_user.other_host_info
1059 if db_user.sleeping_details is not None:
1060 user.sleeping_details.value = db_user.sleeping_details
1062 if db_user.area is not None:
1063 user.area.value = db_user.area
1065 if db_user.house_rules is not None:
1066 user.house_rules.value = db_user.house_rules
1068 if db_user.parking is not None:
1069 user.parking.value = db_user.parking
1071 if db_user.camping_ok is not None:
1072 user.camping_ok.value = db_user.camping_ok
1074 return user
1077def lite_user_to_pb(lite_user):
1078 lat, lng = get_coordinates(lite_user.geom) or (0, 0)
1080 return api_pb2.LiteUser(
1081 user_id=lite_user.id,
1082 username=lite_user.username,
1083 name=lite_user.name,
1084 city=lite_user.city,
1085 age=int(lite_user.age),
1086 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1087 if lite_user.avatar_filename
1088 else None,
1089 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1090 if lite_user.avatar_filename
1091 else None,
1092 lat=lat,
1093 lng=lng,
1094 radius=lite_user.radius,
1095 has_strong_verification=lite_user.has_strong_verification,
1096 )