Coverage for src/couchers/servicers/api.py: 97%
386 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-05-12 02:28 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-05-12 02:28 +0000
1from datetime import timedelta
2from urllib.parse import urlencode
4import grpc
5from google.protobuf import empty_pb2
6from sqlalchemy.orm import aliased
7from sqlalchemy.sql import and_, delete, func, intersect, or_, union
9from couchers import errors, urls
10from couchers.config import config
11from couchers.crypto import b64encode, generate_hash_signature, random_hex
12from couchers.materialized_views import lite_users, user_response_rates
13from couchers.models import (
14 FriendRelationship,
15 FriendStatus,
16 GroupChatSubscription,
17 HostingStatus,
18 HostRequest,
19 InitiatedUpload,
20 LanguageAbility,
21 LanguageFluency,
22 MeetupStatus,
23 Message,
24 Notification,
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.resources import get_badge_dict, language_is_allowed, region_is_allowed
36from couchers.servicers.account import get_strong_verification_fields
37from couchers.sql import couchers_select as select
38from couchers.sql import is_valid_user_id, is_valid_username
39from couchers.utils import (
40 Duration_from_timedelta,
41 Timestamp_from_datetime,
42 create_coordinate,
43 get_coordinates,
44 is_valid_name,
45 now,
46)
47from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
49MAX_USERS_PER_QUERY = 200
50MAX_PAGINATION_LENGTH = 50
52hostingstatus2sql = {
53 api_pb2.HOSTING_STATUS_UNKNOWN: None,
54 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
55 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
56 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
57}
59hostingstatus2api = {
60 None: api_pb2.HOSTING_STATUS_UNKNOWN,
61 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
62 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
63 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
64}
66meetupstatus2sql = {
67 api_pb2.MEETUP_STATUS_UNKNOWN: None,
68 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
69 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
70 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
71}
73meetupstatus2api = {
74 None: api_pb2.MEETUP_STATUS_UNKNOWN,
75 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
76 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
77 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
78}
80smokinglocation2sql = {
81 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
82 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
83 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
84 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
85 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
86}
88smokinglocation2api = {
89 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
90 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
91 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
92 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
93 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
94}
96sleepingarrangement2sql = {
97 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
98 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
99 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
100 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
101}
103sleepingarrangement2api = {
104 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
105 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
106 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
107 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
108}
110parkingdetails2sql = {
111 api_pb2.PARKING_DETAILS_UNKNOWN: None,
112 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
113 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
114 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
115 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
116}
118parkingdetails2api = {
119 None: api_pb2.PARKING_DETAILS_UNKNOWN,
120 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
121 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
122 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
123 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
124}
126fluency2sql = {
127 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
128 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
129 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
130 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
131}
133fluency2api = {
134 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
135 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
136 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
137 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
138}
141class API(api_pb2_grpc.APIServicer):
142 def Ping(self, request, context, session):
143 # auth ought to make sure the user exists
144 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
146 # gets only the max message by self-joining messages which have a greater id
147 # if it doesn't have a greater id, it's the biggest
148 message_2 = aliased(Message)
149 unseen_sent_host_request_count = session.execute(
150 select(func.count())
151 .select_from(Message)
152 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
153 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
154 .where(HostRequest.surfer_user_id == context.user_id)
155 .where_users_column_visible(context, HostRequest.host_user_id)
156 .where(message_2.id == None)
157 .where(HostRequest.surfer_last_seen_message_id < Message.id)
158 ).scalar_one()
160 unseen_received_host_request_count = session.execute(
161 select(func.count())
162 .select_from(Message)
163 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
164 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
165 .where_users_column_visible(context, HostRequest.surfer_user_id)
166 .where(HostRequest.host_user_id == context.user_id)
167 .where(message_2.id == None)
168 .where(HostRequest.host_last_seen_message_id < Message.id)
169 ).scalar_one()
171 unseen_message_count = session.execute(
172 select(func.count())
173 .select_from(Message)
174 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
175 .where(GroupChatSubscription.user_id == context.user_id)
176 .where(Message.time >= GroupChatSubscription.joined)
177 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
178 .where(Message.id > GroupChatSubscription.last_seen_message_id)
179 ).scalar_one()
181 pending_friend_request_count = session.execute(
182 select(func.count())
183 .select_from(FriendRelationship)
184 .where(FriendRelationship.to_user_id == context.user_id)
185 .where_users_column_visible(context, FriendRelationship.from_user_id)
186 .where(FriendRelationship.status == FriendStatus.pending)
187 ).scalar_one()
189 unseen_notification_count = session.execute(
190 select(func.count(Notification.id))
191 .where(Notification.user_id == context.user_id)
192 .where(Notification.is_seen == False)
193 ).scalar_one()
195 return api_pb2.PingRes(
196 user=user_model_to_pb(user, session, context),
197 unseen_message_count=unseen_message_count,
198 unseen_sent_host_request_count=unseen_sent_host_request_count,
199 unseen_received_host_request_count=unseen_received_host_request_count,
200 pending_friend_request_count=pending_friend_request_count,
201 unseen_notification_count=unseen_notification_count,
202 )
204 def GetUser(self, request, context, session):
205 user = session.execute(
206 select(User).where_users_visible(context).where_username_or_id(request.user)
207 ).scalar_one_or_none()
209 if not user:
210 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
212 return user_model_to_pb(user, session, context)
214 def GetLiteUser(self, request, context, session):
215 lite_user = session.execute(
216 select(lite_users)
217 .where_users_visible(context, table=lite_users.c)
218 .where_username_or_id(request.user, table=lite_users.c)
219 ).one_or_none()
221 if not lite_user:
222 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
224 return lite_user_to_pb(lite_user)
226 def GetLiteUsers(self, request, context, session):
227 if len(request.users) > MAX_USERS_PER_QUERY:
228 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
230 usernames = {u for u in request.users if is_valid_username(u)}
231 ids = {u for u in request.users if is_valid_user_id(u)}
233 users = session.execute(
234 select(lite_users)
235 .where_users_visible(context, table=lite_users.c)
236 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
237 ).all()
239 users_by_id = {str(user.id): user for user in users}
240 users_by_username = {user.username: user for user in users}
242 res = api_pb2.GetLiteUsersRes()
244 for user in request.users:
245 lite_user = None
246 if user in users_by_id:
247 lite_user = users_by_id[user]
248 elif user in users_by_username:
249 lite_user = users_by_username[user]
251 res.responses.append(
252 api_pb2.LiteUserRes(
253 query=user,
254 not_found=lite_user is None,
255 user=lite_user_to_pb(lite_user) if lite_user else None,
256 )
257 )
259 return res
261 def UpdateProfile(self, request, context, session):
262 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
264 if request.HasField("name"):
265 if not is_valid_name(request.name.value):
266 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
267 user.name = request.name.value
269 if request.HasField("city"):
270 user.city = request.city.value
272 if request.HasField("hometown"):
273 if request.hometown.is_null:
274 user.hometown = None
275 else:
276 user.hometown = request.hometown.value
278 if request.HasField("lat") and request.HasField("lng"):
279 if request.lat.value == 0 and request.lng.value == 0:
280 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
281 user.geom = create_coordinate(request.lat.value, request.lng.value)
283 if request.HasField("radius"):
284 user.geom_radius = request.radius.value
286 if request.HasField("avatar_key"):
287 if request.avatar_key.is_null:
288 user.avatar_key = None
289 else:
290 user.avatar_key = request.avatar_key.value
292 # if request.HasField("gender"):
293 # user.gender = request.gender.value
295 if request.HasField("pronouns"):
296 if request.pronouns.is_null:
297 user.pronouns = None
298 else:
299 user.pronouns = request.pronouns.value
301 if request.HasField("occupation"):
302 if request.occupation.is_null:
303 user.occupation = None
304 else:
305 user.occupation = request.occupation.value
307 if request.HasField("education"):
308 if request.education.is_null:
309 user.education = None
310 else:
311 user.education = request.education.value
313 if request.HasField("about_me"):
314 if request.about_me.is_null:
315 user.about_me = None
316 else:
317 user.about_me = request.about_me.value
319 if request.HasField("things_i_like"):
320 if request.things_i_like.is_null:
321 user.things_i_like = None
322 else:
323 user.things_i_like = request.things_i_like.value
325 if request.HasField("about_place"):
326 if request.about_place.is_null:
327 user.about_place = None
328 else:
329 user.about_place = request.about_place.value
331 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
332 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
333 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
334 user.hosting_status = hostingstatus2sql[request.hosting_status]
336 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
337 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
338 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
339 user.meetup_status = meetupstatus2sql[request.meetup_status]
341 if request.HasField("language_abilities"):
342 # delete all existing abilities
343 for ability in user.language_abilities:
344 session.delete(ability)
345 session.flush()
347 # add the new ones
348 for language_ability in request.language_abilities.value:
349 if not language_is_allowed(language_ability.code):
350 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
351 session.add(
352 LanguageAbility(
353 user=user,
354 language_code=language_ability.code,
355 fluency=fluency2sql[language_ability.fluency],
356 )
357 )
359 if request.HasField("regions_visited"):
360 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
362 for region in request.regions_visited.value:
363 if not region_is_allowed(region):
364 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
365 session.add(
366 RegionVisited(
367 user_id=user.id,
368 region_code=region,
369 )
370 )
372 if request.HasField("regions_lived"):
373 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
375 for region in request.regions_lived.value:
376 if not region_is_allowed(region):
377 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
378 session.add(
379 RegionLived(
380 user_id=user.id,
381 region_code=region,
382 )
383 )
385 if request.HasField("additional_information"):
386 if request.additional_information.is_null:
387 user.additional_information = None
388 else:
389 user.additional_information = request.additional_information.value
391 if request.HasField("max_guests"):
392 if request.max_guests.is_null:
393 user.max_guests = None
394 else:
395 user.max_guests = request.max_guests.value
397 if request.HasField("last_minute"):
398 if request.last_minute.is_null:
399 user.last_minute = None
400 else:
401 user.last_minute = request.last_minute.value
403 if request.HasField("has_pets"):
404 if request.has_pets.is_null:
405 user.has_pets = None
406 else:
407 user.has_pets = request.has_pets.value
409 if request.HasField("accepts_pets"):
410 if request.accepts_pets.is_null:
411 user.accepts_pets = None
412 else:
413 user.accepts_pets = request.accepts_pets.value
415 if request.HasField("pet_details"):
416 if request.pet_details.is_null:
417 user.pet_details = None
418 else:
419 user.pet_details = request.pet_details.value
421 if request.HasField("has_kids"):
422 if request.has_kids.is_null:
423 user.has_kids = None
424 else:
425 user.has_kids = request.has_kids.value
427 if request.HasField("accepts_kids"):
428 if request.accepts_kids.is_null:
429 user.accepts_kids = None
430 else:
431 user.accepts_kids = request.accepts_kids.value
433 if request.HasField("kid_details"):
434 if request.kid_details.is_null:
435 user.kid_details = None
436 else:
437 user.kid_details = request.kid_details.value
439 if request.HasField("has_housemates"):
440 if request.has_housemates.is_null:
441 user.has_housemates = None
442 else:
443 user.has_housemates = request.has_housemates.value
445 if request.HasField("housemate_details"):
446 if request.housemate_details.is_null:
447 user.housemate_details = None
448 else:
449 user.housemate_details = request.housemate_details.value
451 if request.HasField("wheelchair_accessible"):
452 if request.wheelchair_accessible.is_null:
453 user.wheelchair_accessible = None
454 else:
455 user.wheelchair_accessible = request.wheelchair_accessible.value
457 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
458 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
460 if request.HasField("smokes_at_home"):
461 if request.smokes_at_home.is_null:
462 user.smokes_at_home = None
463 else:
464 user.smokes_at_home = request.smokes_at_home.value
466 if request.HasField("drinking_allowed"):
467 if request.drinking_allowed.is_null:
468 user.drinking_allowed = None
469 else:
470 user.drinking_allowed = request.drinking_allowed.value
472 if request.HasField("drinks_at_home"):
473 if request.drinks_at_home.is_null:
474 user.drinks_at_home = None
475 else:
476 user.drinks_at_home = request.drinks_at_home.value
478 if request.HasField("other_host_info"):
479 if request.other_host_info.is_null:
480 user.other_host_info = None
481 else:
482 user.other_host_info = request.other_host_info.value
484 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
485 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
487 if request.HasField("sleeping_details"):
488 if request.sleeping_details.is_null:
489 user.sleeping_details = None
490 else:
491 user.sleeping_details = request.sleeping_details.value
493 if request.HasField("area"):
494 if request.area.is_null:
495 user.area = None
496 else:
497 user.area = request.area.value
499 if request.HasField("house_rules"):
500 if request.house_rules.is_null:
501 user.house_rules = None
502 else:
503 user.house_rules = request.house_rules.value
505 if request.HasField("parking"):
506 if request.parking.is_null:
507 user.parking = None
508 else:
509 user.parking = request.parking.value
511 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
512 user.parking_details = parkingdetails2sql[request.parking_details]
514 if request.HasField("camping_ok"):
515 if request.camping_ok.is_null:
516 user.camping_ok = None
517 else:
518 user.camping_ok = request.camping_ok.value
520 # save updates
521 session.commit()
523 return empty_pb2.Empty()
525 def ListFriends(self, request, context, session):
526 rels = (
527 session.execute(
528 select(FriendRelationship)
529 .where_users_column_visible(context, FriendRelationship.from_user_id)
530 .where_users_column_visible(context, FriendRelationship.to_user_id)
531 .where(
532 or_(
533 FriendRelationship.from_user_id == context.user_id,
534 FriendRelationship.to_user_id == context.user_id,
535 )
536 )
537 .where(FriendRelationship.status == FriendStatus.accepted)
538 )
539 .scalars()
540 .all()
541 )
542 return api_pb2.ListFriendsRes(
543 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
544 )
546 def ListMutualFriends(self, request, context, session):
547 if context.user_id == request.user_id:
548 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
550 user = session.execute(
551 select(User).where_users_visible(context).where(User.id == request.user_id)
552 ).scalar_one_or_none()
554 if not user:
555 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
557 q1 = (
558 select(FriendRelationship.from_user_id.label("user_id"))
559 .where(FriendRelationship.to_user_id == context.user_id)
560 .where(FriendRelationship.from_user_id != request.user_id)
561 .where(FriendRelationship.status == FriendStatus.accepted)
562 )
564 q2 = (
565 select(FriendRelationship.to_user_id.label("user_id"))
566 .where(FriendRelationship.from_user_id == context.user_id)
567 .where(FriendRelationship.to_user_id != request.user_id)
568 .where(FriendRelationship.status == FriendStatus.accepted)
569 )
571 q3 = (
572 select(FriendRelationship.from_user_id.label("user_id"))
573 .where(FriendRelationship.to_user_id == request.user_id)
574 .where(FriendRelationship.from_user_id != context.user_id)
575 .where(FriendRelationship.status == FriendStatus.accepted)
576 )
578 q4 = (
579 select(FriendRelationship.to_user_id.label("user_id"))
580 .where(FriendRelationship.from_user_id == request.user_id)
581 .where(FriendRelationship.to_user_id != context.user_id)
582 .where(FriendRelationship.status == FriendStatus.accepted)
583 )
585 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
587 mutual_friends = (
588 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
589 )
591 return api_pb2.ListMutualFriendsRes(
592 mutual_friends=[
593 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
594 for mutual_friend in mutual_friends
595 ]
596 )
598 def SendFriendRequest(self, request, context, session):
599 if context.user_id == request.user_id:
600 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
602 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
603 to_user = session.execute(
604 select(User).where_users_visible(context).where(User.id == request.user_id)
605 ).scalar_one_or_none()
607 if not to_user:
608 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
610 if (
611 session.execute(
612 select(FriendRelationship)
613 .where(
614 or_(
615 and_(
616 FriendRelationship.from_user_id == context.user_id,
617 FriendRelationship.to_user_id == request.user_id,
618 ),
619 and_(
620 FriendRelationship.from_user_id == request.user_id,
621 FriendRelationship.to_user_id == context.user_id,
622 ),
623 )
624 )
625 .where(
626 or_(
627 FriendRelationship.status == FriendStatus.accepted,
628 FriendRelationship.status == FriendStatus.pending,
629 )
630 )
631 ).scalar_one_or_none()
632 is not None
633 ):
634 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
636 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
638 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
639 session.add(friend_relationship)
640 session.flush()
642 notify(
643 session,
644 user_id=friend_relationship.to_user_id,
645 topic_action="friend_request:create",
646 key=friend_relationship.from_user_id,
647 data=notification_data_pb2.FriendRequestCreate(
648 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
649 ),
650 )
652 return empty_pb2.Empty()
654 def ListFriendRequests(self, request, context, session):
655 # both sent and received
656 sent_requests = (
657 session.execute(
658 select(FriendRelationship)
659 .where_users_column_visible(context, FriendRelationship.to_user_id)
660 .where(FriendRelationship.from_user_id == context.user_id)
661 .where(FriendRelationship.status == FriendStatus.pending)
662 )
663 .scalars()
664 .all()
665 )
667 received_requests = (
668 session.execute(
669 select(FriendRelationship)
670 .where_users_column_visible(context, FriendRelationship.from_user_id)
671 .where(FriendRelationship.to_user_id == context.user_id)
672 .where(FriendRelationship.status == FriendStatus.pending)
673 )
674 .scalars()
675 .all()
676 )
678 return api_pb2.ListFriendRequestsRes(
679 sent=[
680 api_pb2.FriendRequest(
681 friend_request_id=friend_request.id,
682 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
683 user_id=friend_request.to_user.id,
684 sent=True,
685 )
686 for friend_request in sent_requests
687 ],
688 received=[
689 api_pb2.FriendRequest(
690 friend_request_id=friend_request.id,
691 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
692 user_id=friend_request.from_user.id,
693 sent=False,
694 )
695 for friend_request in received_requests
696 ],
697 )
699 def RespondFriendRequest(self, request, context, session):
700 friend_request = session.execute(
701 select(FriendRelationship)
702 .where_users_column_visible(context, FriendRelationship.from_user_id)
703 .where(FriendRelationship.to_user_id == context.user_id)
704 .where(FriendRelationship.status == FriendStatus.pending)
705 .where(FriendRelationship.id == request.friend_request_id)
706 ).scalar_one_or_none()
708 if not friend_request:
709 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
711 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
712 friend_request.time_responded = func.now()
714 session.flush()
716 if friend_request.status == FriendStatus.accepted:
717 notify(
718 session,
719 user_id=friend_request.from_user_id,
720 topic_action="friend_request:accept",
721 key=friend_request.to_user_id,
722 data=notification_data_pb2.FriendRequestAccept(
723 other_user=user_model_to_pb(friend_request.to_user, session, context),
724 ),
725 )
727 return empty_pb2.Empty()
729 def CancelFriendRequest(self, request, context, session):
730 friend_request = session.execute(
731 select(FriendRelationship)
732 .where_users_column_visible(context, FriendRelationship.to_user_id)
733 .where(FriendRelationship.from_user_id == context.user_id)
734 .where(FriendRelationship.status == FriendStatus.pending)
735 .where(FriendRelationship.id == request.friend_request_id)
736 ).scalar_one_or_none()
738 if not friend_request:
739 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
741 friend_request.status = FriendStatus.cancelled
742 friend_request.time_responded = func.now()
744 # note no notifications
746 session.commit()
748 return empty_pb2.Empty()
750 def InitiateMediaUpload(self, request, context, session):
751 key = random_hex()
753 created = now()
754 expiry = created + timedelta(minutes=20)
756 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
757 session.add(upload)
758 session.commit()
760 req = media_pb2.UploadRequest(
761 key=upload.key,
762 type=media_pb2.UploadRequest.UploadType.IMAGE,
763 created=Timestamp_from_datetime(upload.created),
764 expiry=Timestamp_from_datetime(upload.expiry),
765 max_width=2000,
766 max_height=1600,
767 ).SerializeToString()
769 data = b64encode(req)
770 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
772 path = "upload?" + urlencode({"data": data, "sig": sig})
774 return api_pb2.InitiateMediaUploadRes(
775 upload_url=urls.media_upload_url(path=path),
776 expiry=Timestamp_from_datetime(expiry),
777 )
779 def ListBadgeUsers(self, request, context, session):
780 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
781 next_user_id = int(request.page_token) if request.page_token else 0
782 badge = get_badge_dict().get(request.badge_id)
783 if not badge:
784 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND)
786 badge_user_ids = (
787 session.execute(
788 select(UserBadge.user_id)
789 .where(UserBadge.badge_id == badge["id"])
790 .where(UserBadge.user_id >= next_user_id)
791 .order_by(UserBadge.user_id)
792 .limit(page_size + 1)
793 )
794 .scalars()
795 .all()
796 )
798 return api_pb2.ListBadgeUsersRes(
799 user_ids=badge_user_ids[:page_size],
800 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
801 )
804def response_rate_to_pb(response_rates):
805 if not response_rates:
806 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
808 _, n, response_rate, _, response_time_p33, response_time_p66 = response_rates
810 # if n is None, the user is new or they have no requests
811 if not n or n < 3:
812 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
814 if response_rate <= 0.33:
815 return {"low": requests_pb2.ResponseRateLow()}
817 response_time_p33_coarsened = Duration_from_timedelta(
818 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60)
819 )
821 if response_rate <= 0.66:
822 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
824 response_time_p66_coarsened = Duration_from_timedelta(
825 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60)
826 )
828 if response_rate <= 0.90:
829 return {
830 "most": requests_pb2.ResponseRateMost(
831 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
832 )
833 }
834 else:
835 return {
836 "almost_all": requests_pb2.ResponseRateAlmostAll(
837 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
838 )
839 }
842def user_model_to_pb(db_user, session, context):
843 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
844 num_references = session.execute(
845 select(func.count())
846 .select_from(Reference)
847 .join(User, User.id == Reference.from_user_id)
848 .where(User.is_visible)
849 .where(Reference.to_user_id == db_user.id)
850 .where(Reference.is_deleted == False)
851 ).scalar_one()
853 # returns (lat, lng)
854 # we put people without coords on null island
855 # https://en.wikipedia.org/wiki/Null_Island
856 lat, lng = db_user.coordinates or (0, 0)
858 pending_friend_request = None
859 if db_user.id == context.user_id:
860 friends_status = api_pb2.User.FriendshipStatus.NA
861 else:
862 friend_relationship = session.execute(
863 select(FriendRelationship)
864 .where(
865 or_(
866 and_(
867 FriendRelationship.from_user_id == context.user_id,
868 FriendRelationship.to_user_id == db_user.id,
869 ),
870 and_(
871 FriendRelationship.from_user_id == db_user.id,
872 FriendRelationship.to_user_id == context.user_id,
873 ),
874 )
875 )
876 .where(
877 or_(
878 FriendRelationship.status == FriendStatus.accepted,
879 FriendRelationship.status == FriendStatus.pending,
880 )
881 )
882 ).scalar_one_or_none()
884 if friend_relationship:
885 if friend_relationship.status == FriendStatus.accepted:
886 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
887 else:
888 friends_status = api_pb2.User.FriendshipStatus.PENDING
889 if friend_relationship.from_user_id == context.user_id:
890 # we sent it
891 pending_friend_request = api_pb2.FriendRequest(
892 friend_request_id=friend_relationship.id,
893 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
894 user_id=friend_relationship.to_user.id,
895 sent=True,
896 )
897 else:
898 # we received it
899 pending_friend_request = api_pb2.FriendRequest(
900 friend_request_id=friend_relationship.id,
901 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
902 user_id=friend_relationship.from_user.id,
903 sent=False,
904 )
905 else:
906 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
908 response_rates = session.execute(
909 select(user_response_rates).where(user_response_rates.c.user_id == db_user.id)
910 ).one_or_none()
912 verification_score = 0.0
913 if db_user.phone_verification_verified:
914 verification_score += 1.0 * db_user.phone_is_verified
916 user = api_pb2.User(
917 user_id=db_user.id,
918 username=db_user.username,
919 name=db_user.name,
920 city=db_user.city,
921 hometown=db_user.hometown,
922 timezone=db_user.timezone,
923 lat=lat,
924 lng=lng,
925 radius=db_user.geom_radius,
926 verification=verification_score,
927 community_standing=db_user.community_standing,
928 num_references=num_references,
929 gender=db_user.gender,
930 pronouns=db_user.pronouns,
931 age=int(db_user.age),
932 joined=Timestamp_from_datetime(db_user.display_joined),
933 last_active=Timestamp_from_datetime(db_user.display_last_active),
934 hosting_status=hostingstatus2api[db_user.hosting_status],
935 meetup_status=meetupstatus2api[db_user.meetup_status],
936 occupation=db_user.occupation,
937 education=db_user.education,
938 about_me=db_user.about_me,
939 things_i_like=db_user.things_i_like,
940 about_place=db_user.about_place,
941 language_abilities=[
942 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
943 for ability in db_user.language_abilities
944 ],
945 regions_visited=[region.code for region in db_user.regions_visited],
946 regions_lived=[region.code for region in db_user.regions_lived],
947 additional_information=db_user.additional_information,
948 friends=friends_status,
949 pending_friend_request=pending_friend_request,
950 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
951 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
952 parking_details=parkingdetails2api[db_user.parking_details],
953 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
954 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
955 badges=[badge.badge_id for badge in db_user.badges],
956 **get_strong_verification_fields(session, db_user),
957 **response_rate_to_pb(response_rates),
958 )
960 if db_user.max_guests is not None:
961 user.max_guests.value = db_user.max_guests
963 if db_user.last_minute is not None:
964 user.last_minute.value = db_user.last_minute
966 if db_user.has_pets is not None:
967 user.has_pets.value = db_user.has_pets
969 if db_user.accepts_pets is not None:
970 user.accepts_pets.value = db_user.accepts_pets
972 if db_user.pet_details is not None:
973 user.pet_details.value = db_user.pet_details
975 if db_user.has_kids is not None:
976 user.has_kids.value = db_user.has_kids
978 if db_user.accepts_kids is not None:
979 user.accepts_kids.value = db_user.accepts_kids
981 if db_user.kid_details is not None:
982 user.kid_details.value = db_user.kid_details
984 if db_user.has_housemates is not None:
985 user.has_housemates.value = db_user.has_housemates
987 if db_user.housemate_details is not None:
988 user.housemate_details.value = db_user.housemate_details
990 if db_user.wheelchair_accessible is not None:
991 user.wheelchair_accessible.value = db_user.wheelchair_accessible
993 if db_user.smokes_at_home is not None:
994 user.smokes_at_home.value = db_user.smokes_at_home
996 if db_user.drinking_allowed is not None:
997 user.drinking_allowed.value = db_user.drinking_allowed
999 if db_user.drinks_at_home is not None:
1000 user.drinks_at_home.value = db_user.drinks_at_home
1002 if db_user.other_host_info is not None:
1003 user.other_host_info.value = db_user.other_host_info
1005 if db_user.sleeping_details is not None:
1006 user.sleeping_details.value = db_user.sleeping_details
1008 if db_user.area is not None:
1009 user.area.value = db_user.area
1011 if db_user.house_rules is not None:
1012 user.house_rules.value = db_user.house_rules
1014 if db_user.parking is not None:
1015 user.parking.value = db_user.parking
1017 if db_user.camping_ok is not None:
1018 user.camping_ok.value = db_user.camping_ok
1020 return user
1023def lite_user_to_pb(lite_user):
1024 lat, lng = get_coordinates(lite_user.geom) or (0, 0)
1026 return api_pb2.LiteUser(
1027 user_id=lite_user.id,
1028 username=lite_user.username,
1029 name=lite_user.name,
1030 city=lite_user.city,
1031 age=int(lite_user.age),
1032 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1033 if lite_user.avatar_filename
1034 else None,
1035 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1036 if lite_user.avatar_filename
1037 else None,
1038 lat=lat,
1039 lng=lng,
1040 radius=lite_user.radius,
1041 has_strong_verification=lite_user.has_strong_verification,
1042 )