Coverage for src/couchers/servicers/api.py: 97%
385 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-04-16 15:13 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-04-16 15:13 +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 ParkingDetails,
25 Reference,
26 RegionLived,
27 RegionVisited,
28 SleepingArrangement,
29 SmokingLocation,
30 User,
31 UserBadge,
32)
33from couchers.notifications.notify import notify
34from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed
35from couchers.servicers.account import get_strong_verification_fields
36from couchers.sql import couchers_select as select
37from couchers.sql import is_valid_user_id, is_valid_username
38from couchers.utils import (
39 Duration_from_timedelta,
40 Timestamp_from_datetime,
41 create_coordinate,
42 get_coordinates,
43 is_valid_name,
44 now,
45)
46from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2
48MAX_USERS_PER_QUERY = 200
49MAX_PAGINATION_LENGTH = 50
51hostingstatus2sql = {
52 api_pb2.HOSTING_STATUS_UNKNOWN: None,
53 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
54 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
55 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
56}
58hostingstatus2api = {
59 None: api_pb2.HOSTING_STATUS_UNKNOWN,
60 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
61 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
62 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
63}
65meetupstatus2sql = {
66 api_pb2.MEETUP_STATUS_UNKNOWN: None,
67 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
68 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
69 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
70}
72meetupstatus2api = {
73 None: api_pb2.MEETUP_STATUS_UNKNOWN,
74 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
75 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
76 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
77}
79smokinglocation2sql = {
80 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
81 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
82 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
83 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
84 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
85}
87smokinglocation2api = {
88 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
89 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
90 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
91 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
92 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
93}
95sleepingarrangement2sql = {
96 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
97 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
98 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
99 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
100}
102sleepingarrangement2api = {
103 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
104 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
105 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
106 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
107}
109parkingdetails2sql = {
110 api_pb2.PARKING_DETAILS_UNKNOWN: None,
111 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
112 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
113 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
114 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
115}
117parkingdetails2api = {
118 None: api_pb2.PARKING_DETAILS_UNKNOWN,
119 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
120 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
121 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
122 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
123}
125fluency2sql = {
126 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
127 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
128 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
129 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
130}
132fluency2api = {
133 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
134 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
135 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
136 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
137}
140class API(api_pb2_grpc.APIServicer):
141 def Ping(self, request, context, session):
142 # auth ought to make sure the user exists
143 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
145 # gets only the max message by self-joining messages which have a greater id
146 # if it doesn't have a greater id, it's the biggest
147 message_2 = aliased(Message)
148 unseen_sent_host_request_count = session.execute(
149 select(func.count())
150 .select_from(Message)
151 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
152 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
153 .where(HostRequest.surfer_user_id == context.user_id)
154 .where_users_column_visible(context, HostRequest.host_user_id)
155 .where(message_2.id == None)
156 .where(HostRequest.surfer_last_seen_message_id < Message.id)
157 ).scalar_one()
159 unseen_received_host_request_count = session.execute(
160 select(func.count())
161 .select_from(Message)
162 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
163 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
164 .where_users_column_visible(context, HostRequest.surfer_user_id)
165 .where(HostRequest.host_user_id == context.user_id)
166 .where(message_2.id == None)
167 .where(HostRequest.host_last_seen_message_id < Message.id)
168 ).scalar_one()
170 unseen_message_count = session.execute(
171 select(func.count())
172 .select_from(Message)
173 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
174 .where(GroupChatSubscription.user_id == context.user_id)
175 .where(Message.time >= GroupChatSubscription.joined)
176 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
177 .where(Message.id > GroupChatSubscription.last_seen_message_id)
178 ).scalar_one()
180 pending_friend_request_count = session.execute(
181 select(func.count())
182 .select_from(FriendRelationship)
183 .where(FriendRelationship.to_user_id == context.user_id)
184 .where_users_column_visible(context, FriendRelationship.from_user_id)
185 .where(FriendRelationship.status == FriendStatus.pending)
186 ).scalar_one()
188 return api_pb2.PingRes(
189 user=user_model_to_pb(user, session, context),
190 unseen_message_count=unseen_message_count,
191 unseen_sent_host_request_count=unseen_sent_host_request_count,
192 unseen_received_host_request_count=unseen_received_host_request_count,
193 pending_friend_request_count=pending_friend_request_count,
194 )
196 def GetUser(self, request, context, session):
197 user = session.execute(
198 select(User).where_users_visible(context).where_username_or_id(request.user)
199 ).scalar_one_or_none()
201 if not user:
202 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
204 return user_model_to_pb(user, session, context)
206 def GetLiteUser(self, request, context, session):
207 lite_user = session.execute(
208 select(lite_users)
209 .where_users_visible(context, table=lite_users.c)
210 .where_username_or_id(request.user, table=lite_users.c)
211 ).one_or_none()
213 if not lite_user:
214 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
216 return lite_user_to_pb(lite_user)
218 def GetLiteUsers(self, request, context, session):
219 if len(request.users) > MAX_USERS_PER_QUERY:
220 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
222 usernames = {u for u in request.users if is_valid_username(u)}
223 ids = {u for u in request.users if is_valid_user_id(u)}
225 users = session.execute(
226 select(lite_users)
227 .where_users_visible(context, table=lite_users.c)
228 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
229 ).all()
231 users_by_id = {str(user.id): user for user in users}
232 users_by_username = {user.username: user for user in users}
234 res = api_pb2.GetLiteUsersRes()
236 for user in request.users:
237 lite_user = None
238 if user in users_by_id:
239 lite_user = users_by_id[user]
240 elif user in users_by_username:
241 lite_user = users_by_username[user]
243 res.responses.append(
244 api_pb2.LiteUserRes(
245 query=user,
246 not_found=lite_user is None,
247 user=lite_user_to_pb(lite_user) if lite_user else None,
248 )
249 )
251 return res
253 def UpdateProfile(self, request, context, session):
254 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
256 if request.HasField("name"):
257 if not is_valid_name(request.name.value):
258 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
259 user.name = request.name.value
261 if request.HasField("city"):
262 user.city = request.city.value
264 if request.HasField("hometown"):
265 if request.hometown.is_null:
266 user.hometown = None
267 else:
268 user.hometown = request.hometown.value
270 if request.HasField("lat") and request.HasField("lng"):
271 if request.lat.value == 0 and request.lng.value == 0:
272 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
273 user.geom = create_coordinate(request.lat.value, request.lng.value)
275 if request.HasField("radius"):
276 user.geom_radius = request.radius.value
278 if request.HasField("avatar_key"):
279 if request.avatar_key.is_null:
280 user.avatar_key = None
281 else:
282 user.avatar_key = request.avatar_key.value
284 # if request.HasField("gender"):
285 # user.gender = request.gender.value
287 if request.HasField("pronouns"):
288 if request.pronouns.is_null:
289 user.pronouns = None
290 else:
291 user.pronouns = request.pronouns.value
293 if request.HasField("occupation"):
294 if request.occupation.is_null:
295 user.occupation = None
296 else:
297 user.occupation = request.occupation.value
299 if request.HasField("education"):
300 if request.education.is_null:
301 user.education = None
302 else:
303 user.education = request.education.value
305 if request.HasField("about_me"):
306 if request.about_me.is_null:
307 user.about_me = None
308 else:
309 user.about_me = request.about_me.value
311 if request.HasField("things_i_like"):
312 if request.things_i_like.is_null:
313 user.things_i_like = None
314 else:
315 user.things_i_like = request.things_i_like.value
317 if request.HasField("about_place"):
318 if request.about_place.is_null:
319 user.about_place = None
320 else:
321 user.about_place = request.about_place.value
323 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
324 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
325 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
326 user.hosting_status = hostingstatus2sql[request.hosting_status]
328 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
329 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
330 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
331 user.meetup_status = meetupstatus2sql[request.meetup_status]
333 if request.HasField("language_abilities"):
334 # delete all existing abilities
335 for ability in user.language_abilities:
336 session.delete(ability)
337 session.flush()
339 # add the new ones
340 for language_ability in request.language_abilities.value:
341 if not language_is_allowed(language_ability.code):
342 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
343 session.add(
344 LanguageAbility(
345 user=user,
346 language_code=language_ability.code,
347 fluency=fluency2sql[language_ability.fluency],
348 )
349 )
351 if request.HasField("regions_visited"):
352 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
354 for region in request.regions_visited.value:
355 if not region_is_allowed(region):
356 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
357 session.add(
358 RegionVisited(
359 user_id=user.id,
360 region_code=region,
361 )
362 )
364 if request.HasField("regions_lived"):
365 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
367 for region in request.regions_lived.value:
368 if not region_is_allowed(region):
369 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
370 session.add(
371 RegionLived(
372 user_id=user.id,
373 region_code=region,
374 )
375 )
377 if request.HasField("additional_information"):
378 if request.additional_information.is_null:
379 user.additional_information = None
380 else:
381 user.additional_information = request.additional_information.value
383 if request.HasField("max_guests"):
384 if request.max_guests.is_null:
385 user.max_guests = None
386 else:
387 user.max_guests = request.max_guests.value
389 if request.HasField("last_minute"):
390 if request.last_minute.is_null:
391 user.last_minute = None
392 else:
393 user.last_minute = request.last_minute.value
395 if request.HasField("has_pets"):
396 if request.has_pets.is_null:
397 user.has_pets = None
398 else:
399 user.has_pets = request.has_pets.value
401 if request.HasField("accepts_pets"):
402 if request.accepts_pets.is_null:
403 user.accepts_pets = None
404 else:
405 user.accepts_pets = request.accepts_pets.value
407 if request.HasField("pet_details"):
408 if request.pet_details.is_null:
409 user.pet_details = None
410 else:
411 user.pet_details = request.pet_details.value
413 if request.HasField("has_kids"):
414 if request.has_kids.is_null:
415 user.has_kids = None
416 else:
417 user.has_kids = request.has_kids.value
419 if request.HasField("accepts_kids"):
420 if request.accepts_kids.is_null:
421 user.accepts_kids = None
422 else:
423 user.accepts_kids = request.accepts_kids.value
425 if request.HasField("kid_details"):
426 if request.kid_details.is_null:
427 user.kid_details = None
428 else:
429 user.kid_details = request.kid_details.value
431 if request.HasField("has_housemates"):
432 if request.has_housemates.is_null:
433 user.has_housemates = None
434 else:
435 user.has_housemates = request.has_housemates.value
437 if request.HasField("housemate_details"):
438 if request.housemate_details.is_null:
439 user.housemate_details = None
440 else:
441 user.housemate_details = request.housemate_details.value
443 if request.HasField("wheelchair_accessible"):
444 if request.wheelchair_accessible.is_null:
445 user.wheelchair_accessible = None
446 else:
447 user.wheelchair_accessible = request.wheelchair_accessible.value
449 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
450 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
452 if request.HasField("smokes_at_home"):
453 if request.smokes_at_home.is_null:
454 user.smokes_at_home = None
455 else:
456 user.smokes_at_home = request.smokes_at_home.value
458 if request.HasField("drinking_allowed"):
459 if request.drinking_allowed.is_null:
460 user.drinking_allowed = None
461 else:
462 user.drinking_allowed = request.drinking_allowed.value
464 if request.HasField("drinks_at_home"):
465 if request.drinks_at_home.is_null:
466 user.drinks_at_home = None
467 else:
468 user.drinks_at_home = request.drinks_at_home.value
470 if request.HasField("other_host_info"):
471 if request.other_host_info.is_null:
472 user.other_host_info = None
473 else:
474 user.other_host_info = request.other_host_info.value
476 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
477 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
479 if request.HasField("sleeping_details"):
480 if request.sleeping_details.is_null:
481 user.sleeping_details = None
482 else:
483 user.sleeping_details = request.sleeping_details.value
485 if request.HasField("area"):
486 if request.area.is_null:
487 user.area = None
488 else:
489 user.area = request.area.value
491 if request.HasField("house_rules"):
492 if request.house_rules.is_null:
493 user.house_rules = None
494 else:
495 user.house_rules = request.house_rules.value
497 if request.HasField("parking"):
498 if request.parking.is_null:
499 user.parking = None
500 else:
501 user.parking = request.parking.value
503 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
504 user.parking_details = parkingdetails2sql[request.parking_details]
506 if request.HasField("camping_ok"):
507 if request.camping_ok.is_null:
508 user.camping_ok = None
509 else:
510 user.camping_ok = request.camping_ok.value
512 # save updates
513 session.commit()
515 return empty_pb2.Empty()
517 def ListFriends(self, request, context, session):
518 rels = (
519 session.execute(
520 select(FriendRelationship)
521 .where_users_column_visible(context, FriendRelationship.from_user_id)
522 .where_users_column_visible(context, FriendRelationship.to_user_id)
523 .where(
524 or_(
525 FriendRelationship.from_user_id == context.user_id,
526 FriendRelationship.to_user_id == context.user_id,
527 )
528 )
529 .where(FriendRelationship.status == FriendStatus.accepted)
530 )
531 .scalars()
532 .all()
533 )
534 return api_pb2.ListFriendsRes(
535 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
536 )
538 def ListMutualFriends(self, request, context, session):
539 if context.user_id == request.user_id:
540 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
542 user = session.execute(
543 select(User).where_users_visible(context).where(User.id == request.user_id)
544 ).scalar_one_or_none()
546 if not user:
547 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
549 q1 = (
550 select(FriendRelationship.from_user_id.label("user_id"))
551 .where(FriendRelationship.to_user_id == context.user_id)
552 .where(FriendRelationship.from_user_id != request.user_id)
553 .where(FriendRelationship.status == FriendStatus.accepted)
554 )
556 q2 = (
557 select(FriendRelationship.to_user_id.label("user_id"))
558 .where(FriendRelationship.from_user_id == context.user_id)
559 .where(FriendRelationship.to_user_id != request.user_id)
560 .where(FriendRelationship.status == FriendStatus.accepted)
561 )
563 q3 = (
564 select(FriendRelationship.from_user_id.label("user_id"))
565 .where(FriendRelationship.to_user_id == request.user_id)
566 .where(FriendRelationship.from_user_id != context.user_id)
567 .where(FriendRelationship.status == FriendStatus.accepted)
568 )
570 q4 = (
571 select(FriendRelationship.to_user_id.label("user_id"))
572 .where(FriendRelationship.from_user_id == request.user_id)
573 .where(FriendRelationship.to_user_id != context.user_id)
574 .where(FriendRelationship.status == FriendStatus.accepted)
575 )
577 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
579 mutual_friends = (
580 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
581 )
583 return api_pb2.ListMutualFriendsRes(
584 mutual_friends=[
585 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
586 for mutual_friend in mutual_friends
587 ]
588 )
590 def SendFriendRequest(self, request, context, session):
591 if context.user_id == request.user_id:
592 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
594 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
595 to_user = session.execute(
596 select(User).where_users_visible(context).where(User.id == request.user_id)
597 ).scalar_one_or_none()
599 if not to_user:
600 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
602 if (
603 session.execute(
604 select(FriendRelationship)
605 .where(
606 or_(
607 and_(
608 FriendRelationship.from_user_id == context.user_id,
609 FriendRelationship.to_user_id == request.user_id,
610 ),
611 and_(
612 FriendRelationship.from_user_id == request.user_id,
613 FriendRelationship.to_user_id == context.user_id,
614 ),
615 )
616 )
617 .where(
618 or_(
619 FriendRelationship.status == FriendStatus.accepted,
620 FriendRelationship.status == FriendStatus.pending,
621 )
622 )
623 ).scalar_one_or_none()
624 is not None
625 ):
626 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
628 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
630 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
631 session.add(friend_relationship)
632 session.flush()
634 notify(
635 session,
636 user_id=friend_relationship.to_user_id,
637 topic_action="friend_request:create",
638 key=friend_relationship.from_user_id,
639 data=notification_data_pb2.FriendRequestCreate(
640 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
641 ),
642 )
644 return empty_pb2.Empty()
646 def ListFriendRequests(self, request, context, session):
647 # both sent and received
648 sent_requests = (
649 session.execute(
650 select(FriendRelationship)
651 .where_users_column_visible(context, FriendRelationship.to_user_id)
652 .where(FriendRelationship.from_user_id == context.user_id)
653 .where(FriendRelationship.status == FriendStatus.pending)
654 )
655 .scalars()
656 .all()
657 )
659 received_requests = (
660 session.execute(
661 select(FriendRelationship)
662 .where_users_column_visible(context, FriendRelationship.from_user_id)
663 .where(FriendRelationship.to_user_id == context.user_id)
664 .where(FriendRelationship.status == FriendStatus.pending)
665 )
666 .scalars()
667 .all()
668 )
670 return api_pb2.ListFriendRequestsRes(
671 sent=[
672 api_pb2.FriendRequest(
673 friend_request_id=friend_request.id,
674 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
675 user_id=friend_request.to_user.id,
676 sent=True,
677 )
678 for friend_request in sent_requests
679 ],
680 received=[
681 api_pb2.FriendRequest(
682 friend_request_id=friend_request.id,
683 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
684 user_id=friend_request.from_user.id,
685 sent=False,
686 )
687 for friend_request in received_requests
688 ],
689 )
691 def RespondFriendRequest(self, request, context, session):
692 friend_request = session.execute(
693 select(FriendRelationship)
694 .where_users_column_visible(context, FriendRelationship.from_user_id)
695 .where(FriendRelationship.to_user_id == context.user_id)
696 .where(FriendRelationship.status == FriendStatus.pending)
697 .where(FriendRelationship.id == request.friend_request_id)
698 ).scalar_one_or_none()
700 if not friend_request:
701 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
703 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
704 friend_request.time_responded = func.now()
706 session.flush()
708 if friend_request.status == FriendStatus.accepted:
709 notify(
710 session,
711 user_id=friend_request.from_user_id,
712 topic_action="friend_request:accept",
713 key=friend_request.to_user_id,
714 data=notification_data_pb2.FriendRequestAccept(
715 other_user=user_model_to_pb(friend_request.to_user, session, context),
716 ),
717 )
719 return empty_pb2.Empty()
721 def CancelFriendRequest(self, request, context, session):
722 friend_request = session.execute(
723 select(FriendRelationship)
724 .where_users_column_visible(context, FriendRelationship.to_user_id)
725 .where(FriendRelationship.from_user_id == context.user_id)
726 .where(FriendRelationship.status == FriendStatus.pending)
727 .where(FriendRelationship.id == request.friend_request_id)
728 ).scalar_one_or_none()
730 if not friend_request:
731 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
733 friend_request.status = FriendStatus.cancelled
734 friend_request.time_responded = func.now()
736 # note no notifications
738 session.commit()
740 return empty_pb2.Empty()
742 def InitiateMediaUpload(self, request, context, session):
743 key = random_hex()
745 created = now()
746 expiry = created + timedelta(minutes=20)
748 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
749 session.add(upload)
750 session.commit()
752 req = media_pb2.UploadRequest(
753 key=upload.key,
754 type=media_pb2.UploadRequest.UploadType.IMAGE,
755 created=Timestamp_from_datetime(upload.created),
756 expiry=Timestamp_from_datetime(upload.expiry),
757 max_width=2000,
758 max_height=1600,
759 ).SerializeToString()
761 data = b64encode(req)
762 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
764 path = "upload?" + urlencode({"data": data, "sig": sig})
766 return api_pb2.InitiateMediaUploadRes(
767 upload_url=urls.media_upload_url(path=path),
768 expiry=Timestamp_from_datetime(expiry),
769 )
771 def ListBadgeUsers(self, request, context, session):
772 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
773 next_user_id = int(request.page_token) if request.page_token else 0
774 badge = get_badge_dict().get(request.badge_id)
775 if not badge:
776 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND)
778 badge_user_ids = (
779 session.execute(
780 select(UserBadge.user_id)
781 .where(UserBadge.badge_id == badge["id"])
782 .where(UserBadge.user_id >= next_user_id)
783 .order_by(UserBadge.user_id)
784 .limit(page_size + 1)
785 )
786 .scalars()
787 .all()
788 )
790 return api_pb2.ListBadgeUsersRes(
791 user_ids=badge_user_ids[:page_size],
792 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
793 )
796def response_rate_to_pb(response_rates):
797 if not response_rates:
798 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
800 _, n, response_rate, _, response_time_p33, response_time_p66 = response_rates
802 # if n is None, the user is new or they have no requests
803 if not n or n < 3:
804 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()}
806 if response_rate <= 0.33:
807 return {"low": requests_pb2.ResponseRateLow()}
809 response_time_p33_coarsened = Duration_from_timedelta(
810 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60)
811 )
813 if response_rate <= 0.66:
814 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)}
816 response_time_p66_coarsened = Duration_from_timedelta(
817 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60)
818 )
820 if response_rate <= 0.90:
821 return {
822 "most": requests_pb2.ResponseRateMost(
823 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
824 )
825 }
826 else:
827 return {
828 "almost_all": requests_pb2.ResponseRateAlmostAll(
829 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened
830 )
831 }
834def user_model_to_pb(db_user, session, context):
835 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser
836 num_references = session.execute(
837 select(func.count())
838 .select_from(Reference)
839 .join(User, User.id == Reference.from_user_id)
840 .where(User.is_visible)
841 .where(Reference.to_user_id == db_user.id)
842 .where(Reference.is_deleted == False)
843 ).scalar_one()
845 # returns (lat, lng)
846 # we put people without coords on null island
847 # https://en.wikipedia.org/wiki/Null_Island
848 lat, lng = db_user.coordinates or (0, 0)
850 pending_friend_request = None
851 if db_user.id == context.user_id:
852 friends_status = api_pb2.User.FriendshipStatus.NA
853 else:
854 friend_relationship = session.execute(
855 select(FriendRelationship)
856 .where(
857 or_(
858 and_(
859 FriendRelationship.from_user_id == context.user_id,
860 FriendRelationship.to_user_id == db_user.id,
861 ),
862 and_(
863 FriendRelationship.from_user_id == db_user.id,
864 FriendRelationship.to_user_id == context.user_id,
865 ),
866 )
867 )
868 .where(
869 or_(
870 FriendRelationship.status == FriendStatus.accepted,
871 FriendRelationship.status == FriendStatus.pending,
872 )
873 )
874 ).scalar_one_or_none()
876 if friend_relationship:
877 if friend_relationship.status == FriendStatus.accepted:
878 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
879 else:
880 friends_status = api_pb2.User.FriendshipStatus.PENDING
881 if friend_relationship.from_user_id == context.user_id:
882 # we sent it
883 pending_friend_request = api_pb2.FriendRequest(
884 friend_request_id=friend_relationship.id,
885 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
886 user_id=friend_relationship.to_user.id,
887 sent=True,
888 )
889 else:
890 # we received 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.from_user.id,
895 sent=False,
896 )
897 else:
898 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
900 response_rates = session.execute(
901 select(user_response_rates).where(user_response_rates.c.user_id == db_user.id)
902 ).one_or_none()
904 verification_score = 0.0
905 if db_user.phone_verification_verified:
906 verification_score += 1.0 * db_user.phone_is_verified
908 user = api_pb2.User(
909 user_id=db_user.id,
910 username=db_user.username,
911 name=db_user.name,
912 city=db_user.city,
913 hometown=db_user.hometown,
914 timezone=db_user.timezone,
915 lat=lat,
916 lng=lng,
917 radius=db_user.geom_radius,
918 verification=verification_score,
919 community_standing=db_user.community_standing,
920 num_references=num_references,
921 gender=db_user.gender,
922 pronouns=db_user.pronouns,
923 age=int(db_user.age),
924 joined=Timestamp_from_datetime(db_user.display_joined),
925 last_active=Timestamp_from_datetime(db_user.display_last_active),
926 hosting_status=hostingstatus2api[db_user.hosting_status],
927 meetup_status=meetupstatus2api[db_user.meetup_status],
928 occupation=db_user.occupation,
929 education=db_user.education,
930 about_me=db_user.about_me,
931 things_i_like=db_user.things_i_like,
932 about_place=db_user.about_place,
933 language_abilities=[
934 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
935 for ability in db_user.language_abilities
936 ],
937 regions_visited=[region.code for region in db_user.regions_visited],
938 regions_lived=[region.code for region in db_user.regions_lived],
939 additional_information=db_user.additional_information,
940 friends=friends_status,
941 pending_friend_request=pending_friend_request,
942 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
943 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
944 parking_details=parkingdetails2api[db_user.parking_details],
945 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
946 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
947 badges=[badge.badge_id for badge in db_user.badges],
948 **get_strong_verification_fields(session, db_user),
949 **response_rate_to_pb(response_rates),
950 )
952 if db_user.max_guests is not None:
953 user.max_guests.value = db_user.max_guests
955 if db_user.last_minute is not None:
956 user.last_minute.value = db_user.last_minute
958 if db_user.has_pets is not None:
959 user.has_pets.value = db_user.has_pets
961 if db_user.accepts_pets is not None:
962 user.accepts_pets.value = db_user.accepts_pets
964 if db_user.pet_details is not None:
965 user.pet_details.value = db_user.pet_details
967 if db_user.has_kids is not None:
968 user.has_kids.value = db_user.has_kids
970 if db_user.accepts_kids is not None:
971 user.accepts_kids.value = db_user.accepts_kids
973 if db_user.kid_details is not None:
974 user.kid_details.value = db_user.kid_details
976 if db_user.has_housemates is not None:
977 user.has_housemates.value = db_user.has_housemates
979 if db_user.housemate_details is not None:
980 user.housemate_details.value = db_user.housemate_details
982 if db_user.wheelchair_accessible is not None:
983 user.wheelchair_accessible.value = db_user.wheelchair_accessible
985 if db_user.smokes_at_home is not None:
986 user.smokes_at_home.value = db_user.smokes_at_home
988 if db_user.drinking_allowed is not None:
989 user.drinking_allowed.value = db_user.drinking_allowed
991 if db_user.drinks_at_home is not None:
992 user.drinks_at_home.value = db_user.drinks_at_home
994 if db_user.other_host_info is not None:
995 user.other_host_info.value = db_user.other_host_info
997 if db_user.sleeping_details is not None:
998 user.sleeping_details.value = db_user.sleeping_details
1000 if db_user.area is not None:
1001 user.area.value = db_user.area
1003 if db_user.house_rules is not None:
1004 user.house_rules.value = db_user.house_rules
1006 if db_user.parking is not None:
1007 user.parking.value = db_user.parking
1009 if db_user.camping_ok is not None:
1010 user.camping_ok.value = db_user.camping_ok
1012 return user
1015def lite_user_to_pb(lite_user):
1016 lat, lng = get_coordinates(lite_user.geom) or (0, 0)
1018 return api_pb2.LiteUser(
1019 user_id=lite_user.id,
1020 username=lite_user.username,
1021 name=lite_user.name,
1022 city=lite_user.city,
1023 age=int(lite_user.age),
1024 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
1025 if lite_user.avatar_filename
1026 else None,
1027 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
1028 if lite_user.avatar_filename
1029 else None,
1030 lat=lat,
1031 lng=lng,
1032 radius=lite_user.radius,
1033 has_strong_verification=lite_user.has_strong_verification,
1034 )