Coverage for src/couchers/servicers/api.py: 97%
395 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-06-01 15:07 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-06-01 15:07 +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 Reference,
27 RegionLived,
28 RegionVisited,
29 SleepingArrangement,
30 SmokingLocation,
31 User,
32 UserBadge,
33)
34from couchers.notifications.notify import notify
35from couchers.notifications.settings import get_topic_actions_by_delivery_type
36from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed
37from couchers.servicers.account import get_strong_verification_fields
38from couchers.sql import couchers_select as select
39from couchers.sql import is_valid_user_id, is_valid_username
40from couchers.utils import (
41 Duration_from_timedelta,
42 Timestamp_from_datetime,
43 create_coordinate,
44 get_coordinates,
45 is_valid_name,
46 now,
47)
48from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
50MAX_USERS_PER_QUERY = 200
51MAX_PAGINATION_LENGTH = 50
53hostingstatus2sql = {
54 api_pb2.HOSTING_STATUS_UNKNOWN: None,
55 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
56 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
57 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
58}
60hostingstatus2api = {
61 None: api_pb2.HOSTING_STATUS_UNKNOWN,
62 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
63 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
64 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
65}
67meetupstatus2sql = {
68 api_pb2.MEETUP_STATUS_UNKNOWN: None,
69 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
70 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
71 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
72}
74meetupstatus2api = {
75 None: api_pb2.MEETUP_STATUS_UNKNOWN,
76 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
77 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
78 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
79}
81smokinglocation2sql = {
82 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
83 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
84 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
85 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
86 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
87}
89smokinglocation2api = {
90 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
91 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
92 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
93 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
94 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
95}
97sleepingarrangement2sql = {
98 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
99 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
100 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
101 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
102}
104sleepingarrangement2api = {
105 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
106 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
107 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
108 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
109}
111parkingdetails2sql = {
112 api_pb2.PARKING_DETAILS_UNKNOWN: None,
113 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
114 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
115 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
116 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
117}
119parkingdetails2api = {
120 None: api_pb2.PARKING_DETAILS_UNKNOWN,
121 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
122 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
123 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
124 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
125}
127fluency2sql = {
128 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
129 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
130 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
131 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
132}
134fluency2api = {
135 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
136 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
137 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
138 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
139}
142class API(api_pb2_grpc.APIServicer):
143 def Ping(self, request, context, session):
144 # auth ought to make sure the user exists
145 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
147 sent_reqs_last_seen_message_ids = (
148 select(HostRequest.conversation_id, HostRequest.surfer_last_seen_message_id)
149 .where(HostRequest.surfer_user_id == context.user_id)
150 .where_users_column_visible(context, HostRequest.host_user_id)
151 ).subquery()
153 unseen_sent_host_request_count = session.execute(
154 select(func.count(distinct(sent_reqs_last_seen_message_ids.c.conversation_id)))
155 .join(
156 Message,
157 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id,
158 )
159 .where(sent_reqs_last_seen_message_ids.c.surfer_last_seen_message_id < Message.id)
160 .where(Message.id != None)
161 ).scalar_one()
163 received_reqs_last_seen_message_ids = (
164 select(HostRequest.conversation_id, HostRequest.host_last_seen_message_id)
165 .where(HostRequest.host_user_id == context.user_id)
166 .where_users_column_visible(context, HostRequest.surfer_user_id)
167 ).subquery()
169 unseen_received_host_request_count = session.execute(
170 select(func.count(distinct(received_reqs_last_seen_message_ids.c.conversation_id)))
171 .join(
172 Message,
173 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id,
174 )
175 .where(received_reqs_last_seen_message_ids.c.host_last_seen_message_id < Message.id)
176 .where(Message.id != None)
177 ).scalar_one()
179 unseen_message_count = session.execute(
180 select(func.count(Message.id))
181 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
182 .where(GroupChatSubscription.user_id == context.user_id)
183 .where(Message.time >= GroupChatSubscription.joined)
184 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
185 .where(Message.id > GroupChatSubscription.last_seen_message_id)
186 ).scalar_one()
188 pending_friend_request_count = session.execute(
189 select(func.count(FriendRelationship.id))
190 .where(FriendRelationship.to_user_id == context.user_id)
191 .where_users_column_visible(context, FriendRelationship.from_user_id)
192 .where(FriendRelationship.status == FriendStatus.pending)
193 ).scalar_one()
195 unseen_notification_count = session.execute(
196 select(func.count())
197 .select_from(Notification)
198 .where(Notification.user_id == context.user_id)
199 .where(Notification.is_seen == False)
200 .where(
201 Notification.topic_action.in_(
202 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push)
203 )
204 )
205 ).scalar_one()
207 return api_pb2.PingRes(
208 user=user_model_to_pb(user, session, context),
209 unseen_message_count=unseen_message_count,
210 unseen_sent_host_request_count=unseen_sent_host_request_count,
211 unseen_received_host_request_count=unseen_received_host_request_count,
212 pending_friend_request_count=pending_friend_request_count,
213 unseen_notification_count=unseen_notification_count,
214 )
216 def GetUser(self, request, context, session):
217 user = session.execute(
218 select(User).where_users_visible(context).where_username_or_id(request.user)
219 ).scalar_one_or_none()
221 if not user:
222 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
224 return user_model_to_pb(user, session, context)
226 def GetLiteUser(self, request, context, session):
227 lite_user = session.execute(
228 select(lite_users)
229 .where_users_visible(context, table=lite_users.c)
230 .where_username_or_id(request.user, table=lite_users.c)
231 ).one_or_none()
233 if not lite_user:
234 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
236 return lite_user_to_pb(lite_user)
238 def GetLiteUsers(self, request, context, session):
239 if len(request.users) > MAX_USERS_PER_QUERY:
240 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
242 usernames = {u for u in request.users if is_valid_username(u)}
243 ids = {u for u in request.users if is_valid_user_id(u)}
245 users = session.execute(
246 select(lite_users)
247 .where_users_visible(context, table=lite_users.c)
248 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
249 ).all()
251 users_by_id = {str(user.id): user for user in users}
252 users_by_username = {user.username: user for user in users}
254 res = api_pb2.GetLiteUsersRes()
256 for user in request.users:
257 lite_user = None
258 if user in users_by_id:
259 lite_user = users_by_id[user]
260 elif user in users_by_username:
261 lite_user = users_by_username[user]
263 res.responses.append(
264 api_pb2.LiteUserRes(
265 query=user,
266 not_found=lite_user is None,
267 user=lite_user_to_pb(lite_user) if lite_user else None,
268 )
269 )
271 return res
273 def UpdateProfile(self, request, context, session):
274 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
276 if request.HasField("name"):
277 if not is_valid_name(request.name.value):
278 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
279 user.name = request.name.value
281 if request.HasField("city"):
282 user.city = request.city.value
284 if request.HasField("hometown"):
285 if request.hometown.is_null:
286 user.hometown = None
287 else:
288 user.hometown = request.hometown.value
290 if request.HasField("lat") and request.HasField("lng"):
291 if request.lat.value == 0 and request.lng.value == 0:
292 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
293 user.geom = create_coordinate(request.lat.value, request.lng.value)
294 user.randomized_geom = None
296 if request.HasField("radius"):
297 user.geom_radius = request.radius.value
299 if request.HasField("avatar_key"):
300 if request.avatar_key.is_null:
301 user.avatar_key = None
302 else:
303 user.avatar_key = request.avatar_key.value
305 # if request.HasField("gender"):
306 # user.gender = request.gender.value
308 if request.HasField("pronouns"):
309 if request.pronouns.is_null:
310 user.pronouns = None
311 else:
312 user.pronouns = request.pronouns.value
314 if request.HasField("occupation"):
315 if request.occupation.is_null:
316 user.occupation = None
317 else:
318 user.occupation = request.occupation.value
320 if request.HasField("education"):
321 if request.education.is_null:
322 user.education = None
323 else:
324 user.education = request.education.value
326 if request.HasField("about_me"):
327 if request.about_me.is_null:
328 user.about_me = None
329 else:
330 user.about_me = request.about_me.value
332 if request.HasField("things_i_like"):
333 if request.things_i_like.is_null:
334 user.things_i_like = None
335 else:
336 user.things_i_like = request.things_i_like.value
338 if request.HasField("about_place"):
339 if request.about_place.is_null:
340 user.about_place = None
341 else:
342 user.about_place = request.about_place.value
344 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
345 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
346 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
347 user.hosting_status = hostingstatus2sql[request.hosting_status]
349 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
350 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
351 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
352 user.meetup_status = meetupstatus2sql[request.meetup_status]
354 if request.HasField("language_abilities"):
355 # delete all existing abilities
356 for ability in user.language_abilities:
357 session.delete(ability)
358 session.flush()
360 # add the new ones
361 for language_ability in request.language_abilities.value:
362 if not language_is_allowed(language_ability.code):
363 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
364 session.add(
365 LanguageAbility(
366 user=user,
367 language_code=language_ability.code,
368 fluency=fluency2sql[language_ability.fluency],
369 )
370 )
372 if request.HasField("regions_visited"):
373 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
375 for region in request.regions_visited.value:
376 if not region_is_allowed(region):
377 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
378 session.add(
379 RegionVisited(
380 user_id=user.id,
381 region_code=region,
382 )
383 )
385 if request.HasField("regions_lived"):
386 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
388 for region in request.regions_lived.value:
389 if not region_is_allowed(region):
390 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
391 session.add(
392 RegionLived(
393 user_id=user.id,
394 region_code=region,
395 )
396 )
398 if request.HasField("additional_information"):
399 if request.additional_information.is_null:
400 user.additional_information = None
401 else:
402 user.additional_information = request.additional_information.value
404 if request.HasField("max_guests"):
405 if request.max_guests.is_null:
406 user.max_guests = None
407 else:
408 user.max_guests = request.max_guests.value
410 if request.HasField("last_minute"):
411 if request.last_minute.is_null:
412 user.last_minute = None
413 else:
414 user.last_minute = request.last_minute.value
416 if request.HasField("has_pets"):
417 if request.has_pets.is_null:
418 user.has_pets = None
419 else:
420 user.has_pets = request.has_pets.value
422 if request.HasField("accepts_pets"):
423 if request.accepts_pets.is_null:
424 user.accepts_pets = None
425 else:
426 user.accepts_pets = request.accepts_pets.value
428 if request.HasField("pet_details"):
429 if request.pet_details.is_null:
430 user.pet_details = None
431 else:
432 user.pet_details = request.pet_details.value
434 if request.HasField("has_kids"):
435 if request.has_kids.is_null:
436 user.has_kids = None
437 else:
438 user.has_kids = request.has_kids.value
440 if request.HasField("accepts_kids"):
441 if request.accepts_kids.is_null:
442 user.accepts_kids = None
443 else:
444 user.accepts_kids = request.accepts_kids.value
446 if request.HasField("kid_details"):
447 if request.kid_details.is_null:
448 user.kid_details = None
449 else:
450 user.kid_details = request.kid_details.value
452 if request.HasField("has_housemates"):
453 if request.has_housemates.is_null:
454 user.has_housemates = None
455 else:
456 user.has_housemates = request.has_housemates.value
458 if request.HasField("housemate_details"):
459 if request.housemate_details.is_null:
460 user.housemate_details = None
461 else:
462 user.housemate_details = request.housemate_details.value
464 if request.HasField("wheelchair_accessible"):
465 if request.wheelchair_accessible.is_null:
466 user.wheelchair_accessible = None
467 else:
468 user.wheelchair_accessible = request.wheelchair_accessible.value
470 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
471 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
473 if request.HasField("smokes_at_home"):
474 if request.smokes_at_home.is_null:
475 user.smokes_at_home = None
476 else:
477 user.smokes_at_home = request.smokes_at_home.value
479 if request.HasField("drinking_allowed"):
480 if request.drinking_allowed.is_null:
481 user.drinking_allowed = None
482 else:
483 user.drinking_allowed = request.drinking_allowed.value
485 if request.HasField("drinks_at_home"):
486 if request.drinks_at_home.is_null:
487 user.drinks_at_home = None
488 else:
489 user.drinks_at_home = request.drinks_at_home.value
491 if request.HasField("other_host_info"):
492 if request.other_host_info.is_null:
493 user.other_host_info = None
494 else:
495 user.other_host_info = request.other_host_info.value
497 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
498 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
500 if request.HasField("sleeping_details"):
501 if request.sleeping_details.is_null:
502 user.sleeping_details = None
503 else:
504 user.sleeping_details = request.sleeping_details.value
506 if request.HasField("area"):
507 if request.area.is_null:
508 user.area = None
509 else:
510 user.area = request.area.value
512 if request.HasField("house_rules"):
513 if request.house_rules.is_null:
514 user.house_rules = None
515 else:
516 user.house_rules = request.house_rules.value
518 if request.HasField("parking"):
519 if request.parking.is_null:
520 user.parking = None
521 else:
522 user.parking = request.parking.value
524 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
525 user.parking_details = parkingdetails2sql[request.parking_details]
527 if request.HasField("camping_ok"):
528 if request.camping_ok.is_null:
529 user.camping_ok = None
530 else:
531 user.camping_ok = request.camping_ok.value
533 return empty_pb2.Empty()
535 def ListFriends(self, request, context, session):
536 rels = (
537 session.execute(
538 select(FriendRelationship)
539 .where_users_column_visible(context, FriendRelationship.from_user_id)
540 .where_users_column_visible(context, FriendRelationship.to_user_id)
541 .where(
542 or_(
543 FriendRelationship.from_user_id == context.user_id,
544 FriendRelationship.to_user_id == context.user_id,
545 )
546 )
547 .where(FriendRelationship.status == FriendStatus.accepted)
548 )
549 .scalars()
550 .all()
551 )
552 return api_pb2.ListFriendsRes(
553 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
554 )
556 def RemoveFriend(self, request, context, session):
557 rel = session.execute(
558 select(FriendRelationship)
559 .where_users_column_visible(context, FriendRelationship.from_user_id)
560 .where_users_column_visible(context, FriendRelationship.to_user_id)
561 .where(
562 or_(
563 and_(
564 FriendRelationship.from_user_id == request.user_id,
565 FriendRelationship.to_user_id == context.user_id,
566 ),
567 and_(
568 FriendRelationship.from_user_id == context.user_id,
569 FriendRelationship.to_user_id == request.user_id,
570 ),
571 )
572 )
573 .where(FriendRelationship.status == FriendStatus.accepted)
574 ).scalar_one_or_none()
576 if not rel:
577 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.NOT_FRIENDS)
579 session.delete(rel)
581 return empty_pb2.Empty()
583 def ListMutualFriends(self, request, context, session):
584 if context.user_id == request.user_id:
585 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
587 user = session.execute(
588 select(User).where_users_visible(context).where(User.id == request.user_id)
589 ).scalar_one_or_none()
591 if not user:
592 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
594 q1 = (
595 select(FriendRelationship.from_user_id.label("user_id"))
596 .where(FriendRelationship.to_user_id == context.user_id)
597 .where(FriendRelationship.from_user_id != request.user_id)
598 .where(FriendRelationship.status == FriendStatus.accepted)
599 )
601 q2 = (
602 select(FriendRelationship.to_user_id.label("user_id"))
603 .where(FriendRelationship.from_user_id == context.user_id)
604 .where(FriendRelationship.to_user_id != request.user_id)
605 .where(FriendRelationship.status == FriendStatus.accepted)
606 )
608 q3 = (
609 select(FriendRelationship.from_user_id.label("user_id"))
610 .where(FriendRelationship.to_user_id == request.user_id)
611 .where(FriendRelationship.from_user_id != context.user_id)
612 .where(FriendRelationship.status == FriendStatus.accepted)
613 )
615 q4 = (
616 select(FriendRelationship.to_user_id.label("user_id"))
617 .where(FriendRelationship.from_user_id == request.user_id)
618 .where(FriendRelationship.to_user_id != context.user_id)
619 .where(FriendRelationship.status == FriendStatus.accepted)
620 )
622 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
624 mutual_friends = (
625 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
626 )
628 return api_pb2.ListMutualFriendsRes(
629 mutual_friends=[
630 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
631 for mutual_friend in mutual_friends
632 ]
633 )
635 def SendFriendRequest(self, request, context, session):
636 if context.user_id == request.user_id:
637 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
639 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
640 to_user = session.execute(
641 select(User).where_users_visible(context).where(User.id == request.user_id)
642 ).scalar_one_or_none()
644 if not to_user:
645 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
647 if (
648 session.execute(
649 select(FriendRelationship)
650 .where(
651 or_(
652 and_(
653 FriendRelationship.from_user_id == context.user_id,
654 FriendRelationship.to_user_id == request.user_id,
655 ),
656 and_(
657 FriendRelationship.from_user_id == request.user_id,
658 FriendRelationship.to_user_id == context.user_id,
659 ),
660 )
661 )
662 .where(
663 or_(
664 FriendRelationship.status == FriendStatus.accepted,
665 FriendRelationship.status == FriendStatus.pending,
666 )
667 )
668 ).scalar_one_or_none()
669 is not None
670 ):
671 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
673 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
675 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
676 session.add(friend_relationship)
677 session.flush()
679 notify(
680 session,
681 user_id=friend_relationship.to_user_id,
682 topic_action="friend_request:create",
683 key=friend_relationship.from_user_id,
684 data=notification_data_pb2.FriendRequestCreate(
685 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
686 ),
687 )
689 return empty_pb2.Empty()
691 def ListFriendRequests(self, request, context, session):
692 # both sent and received
693 sent_requests = (
694 session.execute(
695 select(FriendRelationship)
696 .where_users_column_visible(context, FriendRelationship.to_user_id)
697 .where(FriendRelationship.from_user_id == context.user_id)
698 .where(FriendRelationship.status == FriendStatus.pending)
699 )
700 .scalars()
701 .all()
702 )
704 received_requests = (
705 session.execute(
706 select(FriendRelationship)
707 .where_users_column_visible(context, FriendRelationship.from_user_id)
708 .where(FriendRelationship.to_user_id == context.user_id)
709 .where(FriendRelationship.status == FriendStatus.pending)
710 )
711 .scalars()
712 .all()
713 )
715 return api_pb2.ListFriendRequestsRes(
716 sent=[
717 api_pb2.FriendRequest(
718 friend_request_id=friend_request.id,
719 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
720 user_id=friend_request.to_user.id,
721 sent=True,
722 )
723 for friend_request in sent_requests
724 ],
725 received=[
726 api_pb2.FriendRequest(
727 friend_request_id=friend_request.id,
728 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
729 user_id=friend_request.from_user.id,
730 sent=False,
731 )
732 for friend_request in received_requests
733 ],
734 )
736 def RespondFriendRequest(self, request, context, session):
737 friend_request = session.execute(
738 select(FriendRelationship)
739 .where_users_column_visible(context, FriendRelationship.from_user_id)
740 .where(FriendRelationship.to_user_id == context.user_id)
741 .where(FriendRelationship.status == FriendStatus.pending)
742 .where(FriendRelationship.id == request.friend_request_id)
743 ).scalar_one_or_none()
745 if not friend_request:
746 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
748 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
749 friend_request.time_responded = func.now()
751 session.flush()
753 if friend_request.status == FriendStatus.accepted:
754 notify(
755 session,
756 user_id=friend_request.from_user_id,
757 topic_action="friend_request:accept",
758 key=friend_request.to_user_id,
759 data=notification_data_pb2.FriendRequestAccept(
760 other_user=user_model_to_pb(friend_request.to_user, session, context),
761 ),
762 )
764 return empty_pb2.Empty()
766 def CancelFriendRequest(self, request, context, session):
767 friend_request = session.execute(
768 select(FriendRelationship)
769 .where_users_column_visible(context, FriendRelationship.to_user_id)
770 .where(FriendRelationship.from_user_id == context.user_id)
771 .where(FriendRelationship.status == FriendStatus.pending)
772 .where(FriendRelationship.id == request.friend_request_id)
773 ).scalar_one_or_none()
775 if not friend_request:
776 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
778 friend_request.status = FriendStatus.cancelled
779 friend_request.time_responded = func.now()
781 # note no notifications
783 session.commit()
785 return empty_pb2.Empty()
787 def InitiateMediaUpload(self, request, context, session):
788 key = random_hex()
790 created = now()
791 expiry = created + timedelta(minutes=20)
793 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
794 session.add(upload)
795 session.commit()
797 req = media_pb2.UploadRequest(
798 key=upload.key,
799 type=media_pb2.UploadRequest.UploadType.IMAGE,
800 created=Timestamp_from_datetime(upload.created),
801 expiry=Timestamp_from_datetime(upload.expiry),
802 max_width=2000,
803 max_height=1600,
804 ).SerializeToString()
806 data = b64encode(req)
807 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
809 path = "upload?" + urlencode({"data": data, "sig": sig})
811 return api_pb2.InitiateMediaUploadRes(
812 upload_url=urls.media_upload_url(path=path),
813 expiry=Timestamp_from_datetime(expiry),
814 )
816 def ListBadgeUsers(self, request, context, session):
817 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
818 next_user_id = int(request.page_token) if request.page_token else 0
819 badge = get_badge_dict().get(request.badge_id)
820 if not badge:
821 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND)
823 badge_user_ids = (
824 session.execute(
825 select(UserBadge.user_id)
826 .where(UserBadge.badge_id == badge["id"])
827 .where(UserBadge.user_id >= next_user_id)
828 .order_by(UserBadge.user_id)
829 .limit(page_size + 1)
830 )
831 .scalars()
832 .all()
833 )
835 return api_pb2.ListBadgeUsersRes(
836 user_ids=badge_user_ids[:page_size],
837 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
838 )
841def response_rate_to_pb(response_rates):
842 if not response_rates:
843 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
845 _, n, response_rate, _, response_time_p33, response_time_p66 = response_rates
847 # if n is None, the user is new or they have no requests
848 if not n or n < 3:
849 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
851 if response_rate <= 0.33:
852 return {"low": requests_pb2.ResponseRateLow()}
854 response_time_p33_coarsened = Duration_from_timedelta(
855 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60)
856 )
858 if response_rate <= 0.66:
859 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
861 response_time_p66_coarsened = Duration_from_timedelta(
862 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60)
863 )
865 if response_rate <= 0.90:
866 return {
867 "most": requests_pb2.ResponseRateMost(
868 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
869 )
870 }
871 else:
872 return {
873 "almost_all": requests_pb2.ResponseRateAlmostAll(
874 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
875 )
876 }
879def get_num_references(session, user_ids):
880 return dict(
881 session.execute(
882 select(Reference.to_user_id, func.count(Reference.id))
883 .where(Reference.to_user_id.in_(user_ids))
884 .where(Reference.is_deleted == False)
885 .join(User, User.id == Reference.from_user_id)
886 .where(User.is_visible)
887 .group_by(Reference.to_user_id)
888 ).all()
889 )
892def user_model_to_pb(db_user, session, context):
893 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
894 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context
895 num_references = get_num_references(session, [db_user.id]).get(db_user.id, 0)
897 # returns (lat, lng)
898 # we put people without coords on null island
899 # https://en.wikipedia.org/wiki/Null_Island
900 lat, lng = db_user.coordinates or (0, 0)
902 pending_friend_request = None
903 if db_user.id == context.user_id:
904 friends_status = api_pb2.User.FriendshipStatus.NA
905 else:
906 friend_relationship = session.execute(
907 select(FriendRelationship)
908 .where(
909 or_(
910 and_(
911 FriendRelationship.from_user_id == context.user_id,
912 FriendRelationship.to_user_id == db_user.id,
913 ),
914 and_(
915 FriendRelationship.from_user_id == db_user.id,
916 FriendRelationship.to_user_id == context.user_id,
917 ),
918 )
919 )
920 .where(
921 or_(
922 FriendRelationship.status == FriendStatus.accepted,
923 FriendRelationship.status == FriendStatus.pending,
924 )
925 )
926 ).scalar_one_or_none()
928 if friend_relationship:
929 if friend_relationship.status == FriendStatus.accepted:
930 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
931 else:
932 friends_status = api_pb2.User.FriendshipStatus.PENDING
933 if friend_relationship.from_user_id == context.user_id:
934 # we sent it
935 pending_friend_request = api_pb2.FriendRequest(
936 friend_request_id=friend_relationship.id,
937 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
938 user_id=friend_relationship.to_user.id,
939 sent=True,
940 )
941 else:
942 # we received 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.from_user.id,
947 sent=False,
948 )
949 else:
950 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
952 response_rates = session.execute(
953 select(user_response_rates).where(user_response_rates.c.user_id == db_user.id)
954 ).one_or_none()
956 verification_score = 0.0
957 if db_user.phone_verification_verified:
958 verification_score += 1.0 * db_user.phone_is_verified
960 user = api_pb2.User(
961 user_id=db_user.id,
962 username=db_user.username,
963 name=db_user.name,
964 city=db_user.city,
965 hometown=db_user.hometown,
966 timezone=db_user.timezone,
967 lat=lat,
968 lng=lng,
969 radius=db_user.geom_radius,
970 verification=verification_score,
971 community_standing=db_user.community_standing,
972 num_references=num_references,
973 gender=db_user.gender,
974 pronouns=db_user.pronouns,
975 age=int(db_user.age),
976 joined=Timestamp_from_datetime(db_user.display_joined),
977 last_active=Timestamp_from_datetime(db_user.display_last_active),
978 hosting_status=hostingstatus2api[db_user.hosting_status],
979 meetup_status=meetupstatus2api[db_user.meetup_status],
980 occupation=db_user.occupation,
981 education=db_user.education,
982 about_me=db_user.about_me,
983 things_i_like=db_user.things_i_like,
984 about_place=db_user.about_place,
985 language_abilities=[
986 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
987 for ability in db_user.language_abilities
988 ],
989 regions_visited=[region.code for region in db_user.regions_visited],
990 regions_lived=[region.code for region in db_user.regions_lived],
991 additional_information=db_user.additional_information,
992 friends=friends_status,
993 pending_friend_request=pending_friend_request,
994 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
995 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
996 parking_details=parkingdetails2api[db_user.parking_details],
997 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
998 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
999 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id))
1000 .scalars()
1001 .all(),
1002 **get_strong_verification_fields(session, db_user),
1003 **response_rate_to_pb(response_rates),
1004 )
1006 if db_user.max_guests is not None:
1007 user.max_guests.value = db_user.max_guests
1009 if db_user.last_minute is not None:
1010 user.last_minute.value = db_user.last_minute
1012 if db_user.has_pets is not None:
1013 user.has_pets.value = db_user.has_pets
1015 if db_user.accepts_pets is not None:
1016 user.accepts_pets.value = db_user.accepts_pets
1018 if db_user.pet_details is not None:
1019 user.pet_details.value = db_user.pet_details
1021 if db_user.has_kids is not None:
1022 user.has_kids.value = db_user.has_kids
1024 if db_user.accepts_kids is not None:
1025 user.accepts_kids.value = db_user.accepts_kids
1027 if db_user.kid_details is not None:
1028 user.kid_details.value = db_user.kid_details
1030 if db_user.has_housemates is not None:
1031 user.has_housemates.value = db_user.has_housemates
1033 if db_user.housemate_details is not None:
1034 user.housemate_details.value = db_user.housemate_details
1036 if db_user.wheelchair_accessible is not None:
1037 user.wheelchair_accessible.value = db_user.wheelchair_accessible
1039 if db_user.smokes_at_home is not None:
1040 user.smokes_at_home.value = db_user.smokes_at_home
1042 if db_user.drinking_allowed is not None:
1043 user.drinking_allowed.value = db_user.drinking_allowed
1045 if db_user.drinks_at_home is not None:
1046 user.drinks_at_home.value = db_user.drinks_at_home
1048 if db_user.other_host_info is not None:
1049 user.other_host_info.value = db_user.other_host_info
1051 if db_user.sleeping_details is not None:
1052 user.sleeping_details.value = db_user.sleeping_details
1054 if db_user.area is not None:
1055 user.area.value = db_user.area
1057 if db_user.house_rules is not None:
1058 user.house_rules.value = db_user.house_rules
1060 if db_user.parking is not None:
1061 user.parking.value = db_user.parking
1063 if db_user.camping_ok is not None:
1064 user.camping_ok.value = db_user.camping_ok
1066 return user
1069def lite_user_to_pb(lite_user):
1070 lat, lng = get_coordinates(lite_user.geom) or (0, 0)
1072 return api_pb2.LiteUser(
1073 user_id=lite_user.id,
1074 username=lite_user.username,
1075 name=lite_user.name,
1076 city=lite_user.city,
1077 age=int(lite_user.age),
1078 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1079 if lite_user.avatar_filename
1080 else None,
1081 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1082 if lite_user.avatar_filename
1083 else None,
1084 lat=lat,
1085 lng=lng,
1086 radius=lite_user.radius,
1087 has_strong_verification=lite_user.has_strong_verification,
1088 )