Coverage for src/couchers/servicers/api.py: 97%
369 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-03-11 15:27 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-03-11 15:27 +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
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 Timestamp_from_datetime,
40 create_coordinate,
41 get_coordinates,
42 is_valid_name,
43 now,
44)
45from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2
47MAX_USERS_PER_QUERY = 200
48MAX_PAGINATION_LENGTH = 50
50hostingstatus2sql = {
51 api_pb2.HOSTING_STATUS_UNKNOWN: None,
52 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
53 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
54 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
55}
57hostingstatus2api = {
58 None: api_pb2.HOSTING_STATUS_UNKNOWN,
59 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
60 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
61 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
62}
64meetupstatus2sql = {
65 api_pb2.MEETUP_STATUS_UNKNOWN: None,
66 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
67 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
68 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
69}
71meetupstatus2api = {
72 None: api_pb2.MEETUP_STATUS_UNKNOWN,
73 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
74 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
75 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
76}
78smokinglocation2sql = {
79 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
80 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
81 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
82 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
83 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
84}
86smokinglocation2api = {
87 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
88 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
89 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
90 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
91 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
92}
94sleepingarrangement2sql = {
95 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
96 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
97 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
98 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
99}
101sleepingarrangement2api = {
102 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
103 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
104 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
105 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
106}
108parkingdetails2sql = {
109 api_pb2.PARKING_DETAILS_UNKNOWN: None,
110 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
111 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
112 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
113 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
114}
116parkingdetails2api = {
117 None: api_pb2.PARKING_DETAILS_UNKNOWN,
118 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
119 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
120 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
121 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
122}
124fluency2sql = {
125 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
126 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
127 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
128 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
129}
131fluency2api = {
132 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
133 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
134 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
135 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
136}
139class API(api_pb2_grpc.APIServicer):
140 def Ping(self, request, context, session):
141 # auth ought to make sure the user exists
142 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
144 # gets only the max message by self-joining messages which have a greater id
145 # if it doesn't have a greater id, it's the biggest
146 message_2 = aliased(Message)
147 unseen_sent_host_request_count = session.execute(
148 select(func.count())
149 .select_from(Message)
150 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
151 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
152 .where(HostRequest.surfer_user_id == context.user_id)
153 .where_users_column_visible(context, HostRequest.host_user_id)
154 .where(message_2.id == None)
155 .where(HostRequest.surfer_last_seen_message_id < Message.id)
156 ).scalar_one()
158 unseen_received_host_request_count = session.execute(
159 select(func.count())
160 .select_from(Message)
161 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
162 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
163 .where_users_column_visible(context, HostRequest.surfer_user_id)
164 .where(HostRequest.host_user_id == context.user_id)
165 .where(message_2.id == None)
166 .where(HostRequest.host_last_seen_message_id < Message.id)
167 ).scalar_one()
169 unseen_message_count = session.execute(
170 select(func.count())
171 .select_from(Message)
172 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
173 .where(GroupChatSubscription.user_id == context.user_id)
174 .where(Message.time >= GroupChatSubscription.joined)
175 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
176 .where(Message.id > GroupChatSubscription.last_seen_message_id)
177 ).scalar_one()
179 pending_friend_request_count = session.execute(
180 select(func.count())
181 .select_from(FriendRelationship)
182 .where(FriendRelationship.to_user_id == context.user_id)
183 .where_users_column_visible(context, FriendRelationship.from_user_id)
184 .where(FriendRelationship.status == FriendStatus.pending)
185 ).scalar_one()
187 return api_pb2.PingRes(
188 user=user_model_to_pb(user, session, context),
189 unseen_message_count=unseen_message_count,
190 unseen_sent_host_request_count=unseen_sent_host_request_count,
191 unseen_received_host_request_count=unseen_received_host_request_count,
192 pending_friend_request_count=pending_friend_request_count,
193 )
195 def GetUser(self, request, context, session):
196 user = session.execute(
197 select(User).where_users_visible(context).where_username_or_id(request.user)
198 ).scalar_one_or_none()
200 if not user:
201 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
203 return user_model_to_pb(user, session, context)
205 def GetLiteUser(self, request, context, session):
206 lite_user = session.execute(
207 select(lite_users)
208 .where_users_visible(context, table=lite_users.c)
209 .where_username_or_id(request.user, table=lite_users.c)
210 ).one_or_none()
212 if not lite_user:
213 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
215 return lite_user_to_pb(lite_user)
217 def GetLiteUsers(self, request, context, session):
218 if len(request.users) > MAX_USERS_PER_QUERY:
219 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
221 usernames = {u for u in request.users if is_valid_username(u)}
222 ids = {u for u in request.users if is_valid_user_id(u)}
224 users = session.execute(
225 select(lite_users)
226 .where_users_visible(context, table=lite_users.c)
227 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
228 ).all()
230 users_by_id = {str(user.id): user for user in users}
231 users_by_username = {user.username: user for user in users}
233 res = api_pb2.GetLiteUsersRes()
235 for user in request.users:
236 lite_user = None
237 if user in users_by_id:
238 lite_user = users_by_id[user]
239 elif user in users_by_username:
240 lite_user = users_by_username[user]
242 res.responses.append(
243 api_pb2.LiteUserRes(
244 query=user,
245 not_found=lite_user is None,
246 user=lite_user_to_pb(lite_user) if lite_user else None,
247 )
248 )
250 return res
252 def UpdateProfile(self, request, context, session):
253 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
255 if request.HasField("name"):
256 if not is_valid_name(request.name.value):
257 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
258 user.name = request.name.value
260 if request.HasField("city"):
261 user.city = request.city.value
263 if request.HasField("hometown"):
264 if request.hometown.is_null:
265 user.hometown = None
266 else:
267 user.hometown = request.hometown.value
269 if request.HasField("lat") and request.HasField("lng"):
270 if request.lat.value == 0 and request.lng.value == 0:
271 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
272 user.geom = create_coordinate(request.lat.value, request.lng.value)
274 if request.HasField("radius"):
275 user.geom_radius = request.radius.value
277 if request.HasField("avatar_key"):
278 if request.avatar_key.is_null:
279 user.avatar_key = None
280 else:
281 user.avatar_key = request.avatar_key.value
283 # if request.HasField("gender"):
284 # user.gender = request.gender.value
286 if request.HasField("pronouns"):
287 if request.pronouns.is_null:
288 user.pronouns = None
289 else:
290 user.pronouns = request.pronouns.value
292 if request.HasField("occupation"):
293 if request.occupation.is_null:
294 user.occupation = None
295 else:
296 user.occupation = request.occupation.value
298 if request.HasField("education"):
299 if request.education.is_null:
300 user.education = None
301 else:
302 user.education = request.education.value
304 if request.HasField("about_me"):
305 if request.about_me.is_null:
306 user.about_me = None
307 else:
308 user.about_me = request.about_me.value
310 if request.HasField("things_i_like"):
311 if request.things_i_like.is_null:
312 user.things_i_like = None
313 else:
314 user.things_i_like = request.things_i_like.value
316 if request.HasField("about_place"):
317 if request.about_place.is_null:
318 user.about_place = None
319 else:
320 user.about_place = request.about_place.value
322 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
323 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
324 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
325 user.hosting_status = hostingstatus2sql[request.hosting_status]
327 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
328 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
329 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
330 user.meetup_status = meetupstatus2sql[request.meetup_status]
332 if request.HasField("language_abilities"):
333 # delete all existing abilities
334 for ability in user.language_abilities:
335 session.delete(ability)
336 session.flush()
338 # add the new ones
339 for language_ability in request.language_abilities.value:
340 if not language_is_allowed(language_ability.code):
341 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
342 session.add(
343 LanguageAbility(
344 user=user,
345 language_code=language_ability.code,
346 fluency=fluency2sql[language_ability.fluency],
347 )
348 )
350 if request.HasField("regions_visited"):
351 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
353 for region in request.regions_visited.value:
354 if not region_is_allowed(region):
355 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
356 session.add(
357 RegionVisited(
358 user_id=user.id,
359 region_code=region,
360 )
361 )
363 if request.HasField("regions_lived"):
364 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
366 for region in request.regions_lived.value:
367 if not region_is_allowed(region):
368 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
369 session.add(
370 RegionLived(
371 user_id=user.id,
372 region_code=region,
373 )
374 )
376 if request.HasField("additional_information"):
377 if request.additional_information.is_null:
378 user.additional_information = None
379 else:
380 user.additional_information = request.additional_information.value
382 if request.HasField("max_guests"):
383 if request.max_guests.is_null:
384 user.max_guests = None
385 else:
386 user.max_guests = request.max_guests.value
388 if request.HasField("last_minute"):
389 if request.last_minute.is_null:
390 user.last_minute = None
391 else:
392 user.last_minute = request.last_minute.value
394 if request.HasField("has_pets"):
395 if request.has_pets.is_null:
396 user.has_pets = None
397 else:
398 user.has_pets = request.has_pets.value
400 if request.HasField("accepts_pets"):
401 if request.accepts_pets.is_null:
402 user.accepts_pets = None
403 else:
404 user.accepts_pets = request.accepts_pets.value
406 if request.HasField("pet_details"):
407 if request.pet_details.is_null:
408 user.pet_details = None
409 else:
410 user.pet_details = request.pet_details.value
412 if request.HasField("has_kids"):
413 if request.has_kids.is_null:
414 user.has_kids = None
415 else:
416 user.has_kids = request.has_kids.value
418 if request.HasField("accepts_kids"):
419 if request.accepts_kids.is_null:
420 user.accepts_kids = None
421 else:
422 user.accepts_kids = request.accepts_kids.value
424 if request.HasField("kid_details"):
425 if request.kid_details.is_null:
426 user.kid_details = None
427 else:
428 user.kid_details = request.kid_details.value
430 if request.HasField("has_housemates"):
431 if request.has_housemates.is_null:
432 user.has_housemates = None
433 else:
434 user.has_housemates = request.has_housemates.value
436 if request.HasField("housemate_details"):
437 if request.housemate_details.is_null:
438 user.housemate_details = None
439 else:
440 user.housemate_details = request.housemate_details.value
442 if request.HasField("wheelchair_accessible"):
443 if request.wheelchair_accessible.is_null:
444 user.wheelchair_accessible = None
445 else:
446 user.wheelchair_accessible = request.wheelchair_accessible.value
448 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
449 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
451 if request.HasField("smokes_at_home"):
452 if request.smokes_at_home.is_null:
453 user.smokes_at_home = None
454 else:
455 user.smokes_at_home = request.smokes_at_home.value
457 if request.HasField("drinking_allowed"):
458 if request.drinking_allowed.is_null:
459 user.drinking_allowed = None
460 else:
461 user.drinking_allowed = request.drinking_allowed.value
463 if request.HasField("drinks_at_home"):
464 if request.drinks_at_home.is_null:
465 user.drinks_at_home = None
466 else:
467 user.drinks_at_home = request.drinks_at_home.value
469 if request.HasField("other_host_info"):
470 if request.other_host_info.is_null:
471 user.other_host_info = None
472 else:
473 user.other_host_info = request.other_host_info.value
475 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
476 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
478 if request.HasField("sleeping_details"):
479 if request.sleeping_details.is_null:
480 user.sleeping_details = None
481 else:
482 user.sleeping_details = request.sleeping_details.value
484 if request.HasField("area"):
485 if request.area.is_null:
486 user.area = None
487 else:
488 user.area = request.area.value
490 if request.HasField("house_rules"):
491 if request.house_rules.is_null:
492 user.house_rules = None
493 else:
494 user.house_rules = request.house_rules.value
496 if request.HasField("parking"):
497 if request.parking.is_null:
498 user.parking = None
499 else:
500 user.parking = request.parking.value
502 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
503 user.parking_details = parkingdetails2sql[request.parking_details]
505 if request.HasField("camping_ok"):
506 if request.camping_ok.is_null:
507 user.camping_ok = None
508 else:
509 user.camping_ok = request.camping_ok.value
511 # save updates
512 session.commit()
514 return empty_pb2.Empty()
516 def ListFriends(self, request, context, session):
517 rels = (
518 session.execute(
519 select(FriendRelationship)
520 .where_users_column_visible(context, FriendRelationship.from_user_id)
521 .where_users_column_visible(context, FriendRelationship.to_user_id)
522 .where(
523 or_(
524 FriendRelationship.from_user_id == context.user_id,
525 FriendRelationship.to_user_id == context.user_id,
526 )
527 )
528 .where(FriendRelationship.status == FriendStatus.accepted)
529 )
530 .scalars()
531 .all()
532 )
533 return api_pb2.ListFriendsRes(
534 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
535 )
537 def ListMutualFriends(self, request, context, session):
538 if context.user_id == request.user_id:
539 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
541 user = session.execute(
542 select(User).where_users_visible(context).where(User.id == request.user_id)
543 ).scalar_one_or_none()
545 if not user:
546 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
548 q1 = (
549 select(FriendRelationship.from_user_id.label("user_id"))
550 .where(FriendRelationship.to_user_id == context.user_id)
551 .where(FriendRelationship.from_user_id != request.user_id)
552 .where(FriendRelationship.status == FriendStatus.accepted)
553 )
555 q2 = (
556 select(FriendRelationship.to_user_id.label("user_id"))
557 .where(FriendRelationship.from_user_id == context.user_id)
558 .where(FriendRelationship.to_user_id != request.user_id)
559 .where(FriendRelationship.status == FriendStatus.accepted)
560 )
562 q3 = (
563 select(FriendRelationship.from_user_id.label("user_id"))
564 .where(FriendRelationship.to_user_id == request.user_id)
565 .where(FriendRelationship.from_user_id != context.user_id)
566 .where(FriendRelationship.status == FriendStatus.accepted)
567 )
569 q4 = (
570 select(FriendRelationship.to_user_id.label("user_id"))
571 .where(FriendRelationship.from_user_id == request.user_id)
572 .where(FriendRelationship.to_user_id != context.user_id)
573 .where(FriendRelationship.status == FriendStatus.accepted)
574 )
576 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
578 mutual_friends = (
579 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
580 )
582 return api_pb2.ListMutualFriendsRes(
583 mutual_friends=[
584 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
585 for mutual_friend in mutual_friends
586 ]
587 )
589 def SendFriendRequest(self, request, context, session):
590 if context.user_id == request.user_id:
591 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
593 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
594 to_user = session.execute(
595 select(User).where_users_visible(context).where(User.id == request.user_id)
596 ).scalar_one_or_none()
598 if not to_user:
599 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
601 if (
602 session.execute(
603 select(FriendRelationship)
604 .where(
605 or_(
606 and_(
607 FriendRelationship.from_user_id == context.user_id,
608 FriendRelationship.to_user_id == request.user_id,
609 ),
610 and_(
611 FriendRelationship.from_user_id == request.user_id,
612 FriendRelationship.to_user_id == context.user_id,
613 ),
614 )
615 )
616 .where(
617 or_(
618 FriendRelationship.status == FriendStatus.accepted,
619 FriendRelationship.status == FriendStatus.pending,
620 )
621 )
622 ).scalar_one_or_none()
623 is not None
624 ):
625 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
627 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
629 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
630 session.add(friend_relationship)
631 session.flush()
633 notify(
634 session,
635 user_id=friend_relationship.to_user_id,
636 topic_action="friend_request:create",
637 key=friend_relationship.from_user_id,
638 data=notification_data_pb2.FriendRequestCreate(
639 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
640 ),
641 )
643 return empty_pb2.Empty()
645 def ListFriendRequests(self, request, context, session):
646 # both sent and received
647 sent_requests = (
648 session.execute(
649 select(FriendRelationship)
650 .where_users_column_visible(context, FriendRelationship.to_user_id)
651 .where(FriendRelationship.from_user_id == context.user_id)
652 .where(FriendRelationship.status == FriendStatus.pending)
653 )
654 .scalars()
655 .all()
656 )
658 received_requests = (
659 session.execute(
660 select(FriendRelationship)
661 .where_users_column_visible(context, FriendRelationship.from_user_id)
662 .where(FriendRelationship.to_user_id == context.user_id)
663 .where(FriendRelationship.status == FriendStatus.pending)
664 )
665 .scalars()
666 .all()
667 )
669 return api_pb2.ListFriendRequestsRes(
670 sent=[
671 api_pb2.FriendRequest(
672 friend_request_id=friend_request.id,
673 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
674 user_id=friend_request.to_user.id,
675 sent=True,
676 )
677 for friend_request in sent_requests
678 ],
679 received=[
680 api_pb2.FriendRequest(
681 friend_request_id=friend_request.id,
682 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
683 user_id=friend_request.from_user.id,
684 sent=False,
685 )
686 for friend_request in received_requests
687 ],
688 )
690 def RespondFriendRequest(self, request, context, session):
691 friend_request = session.execute(
692 select(FriendRelationship)
693 .where_users_column_visible(context, FriendRelationship.from_user_id)
694 .where(FriendRelationship.to_user_id == context.user_id)
695 .where(FriendRelationship.status == FriendStatus.pending)
696 .where(FriendRelationship.id == request.friend_request_id)
697 ).scalar_one_or_none()
699 if not friend_request:
700 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
702 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
703 friend_request.time_responded = func.now()
705 session.flush()
707 if friend_request.status == FriendStatus.accepted:
708 notify(
709 session,
710 user_id=friend_request.from_user_id,
711 topic_action="friend_request:accept",
712 key=friend_request.to_user_id,
713 data=notification_data_pb2.FriendRequestAccept(
714 other_user=user_model_to_pb(friend_request.to_user, session, context),
715 ),
716 )
718 return empty_pb2.Empty()
720 def CancelFriendRequest(self, request, context, session):
721 friend_request = session.execute(
722 select(FriendRelationship)
723 .where_users_column_visible(context, FriendRelationship.to_user_id)
724 .where(FriendRelationship.from_user_id == context.user_id)
725 .where(FriendRelationship.status == FriendStatus.pending)
726 .where(FriendRelationship.id == request.friend_request_id)
727 ).scalar_one_or_none()
729 if not friend_request:
730 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
732 friend_request.status = FriendStatus.cancelled
733 friend_request.time_responded = func.now()
735 # note no notifications
737 session.commit()
739 return empty_pb2.Empty()
741 def InitiateMediaUpload(self, request, context, session):
742 key = random_hex()
744 created = now()
745 expiry = created + timedelta(minutes=20)
747 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
748 session.add(upload)
749 session.commit()
751 req = media_pb2.UploadRequest(
752 key=upload.key,
753 type=media_pb2.UploadRequest.UploadType.IMAGE,
754 created=Timestamp_from_datetime(upload.created),
755 expiry=Timestamp_from_datetime(upload.expiry),
756 max_width=2000,
757 max_height=1600,
758 ).SerializeToString()
760 data = b64encode(req)
761 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
763 path = "upload?" + urlencode({"data": data, "sig": sig})
765 return api_pb2.InitiateMediaUploadRes(
766 upload_url=urls.media_upload_url(path=path),
767 expiry=Timestamp_from_datetime(expiry),
768 )
770 def ListBadgeUsers(self, request, context, session):
771 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
772 next_user_id = int(request.page_token) if request.page_token else 0
773 badge = get_badge_dict().get(request.badge_id)
774 if not badge:
775 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND)
777 badge_user_ids = (
778 session.execute(
779 select(UserBadge.user_id)
780 .where(UserBadge.badge_id == badge["id"])
781 .where(UserBadge.user_id >= next_user_id)
782 .order_by(UserBadge.user_id)
783 .limit(page_size + 1)
784 )
785 .scalars()
786 .all()
787 )
789 return api_pb2.ListBadgeUsersRes(
790 user_ids=badge_user_ids[:page_size],
791 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
792 )
795def user_model_to_pb(db_user, session, context):
796 num_references = session.execute(
797 select(func.count())
798 .select_from(Reference)
799 .join(User, User.id == Reference.from_user_id)
800 .where(User.is_visible)
801 .where(Reference.to_user_id == db_user.id)
802 ).scalar_one()
804 # returns (lat, lng)
805 # we put people without coords on null island
806 # https://en.wikipedia.org/wiki/Null_Island
807 lat, lng = db_user.coordinates or (0, 0)
809 pending_friend_request = None
810 if db_user.id == context.user_id:
811 friends_status = api_pb2.User.FriendshipStatus.NA
812 else:
813 friend_relationship = session.execute(
814 select(FriendRelationship)
815 .where(
816 or_(
817 and_(
818 FriendRelationship.from_user_id == context.user_id,
819 FriendRelationship.to_user_id == db_user.id,
820 ),
821 and_(
822 FriendRelationship.from_user_id == db_user.id,
823 FriendRelationship.to_user_id == context.user_id,
824 ),
825 )
826 )
827 .where(
828 or_(
829 FriendRelationship.status == FriendStatus.accepted,
830 FriendRelationship.status == FriendStatus.pending,
831 )
832 )
833 ).scalar_one_or_none()
835 if friend_relationship:
836 if friend_relationship.status == FriendStatus.accepted:
837 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
838 else:
839 friends_status = api_pb2.User.FriendshipStatus.PENDING
840 if friend_relationship.from_user_id == context.user_id:
841 # we sent it
842 pending_friend_request = api_pb2.FriendRequest(
843 friend_request_id=friend_relationship.id,
844 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
845 user_id=friend_relationship.to_user.id,
846 sent=True,
847 )
848 else:
849 # we received it
850 pending_friend_request = api_pb2.FriendRequest(
851 friend_request_id=friend_relationship.id,
852 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
853 user_id=friend_relationship.from_user.id,
854 sent=False,
855 )
856 else:
857 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
859 verification_score = 0.0
860 if db_user.phone_verification_verified:
861 verification_score += 1.0 * db_user.phone_is_verified
863 user = api_pb2.User(
864 user_id=db_user.id,
865 username=db_user.username,
866 name=db_user.name,
867 city=db_user.city,
868 hometown=db_user.hometown,
869 timezone=db_user.timezone,
870 lat=lat,
871 lng=lng,
872 radius=db_user.geom_radius,
873 verification=verification_score,
874 community_standing=db_user.community_standing,
875 num_references=num_references,
876 gender=db_user.gender,
877 pronouns=db_user.pronouns,
878 age=int(db_user.age),
879 joined=Timestamp_from_datetime(db_user.display_joined),
880 last_active=Timestamp_from_datetime(db_user.display_last_active),
881 hosting_status=hostingstatus2api[db_user.hosting_status],
882 meetup_status=meetupstatus2api[db_user.meetup_status],
883 occupation=db_user.occupation,
884 education=db_user.education,
885 about_me=db_user.about_me,
886 things_i_like=db_user.things_i_like,
887 about_place=db_user.about_place,
888 language_abilities=[
889 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
890 for ability in db_user.language_abilities
891 ],
892 regions_visited=[region.code for region in db_user.regions_visited],
893 regions_lived=[region.code for region in db_user.regions_lived],
894 additional_information=db_user.additional_information,
895 friends=friends_status,
896 pending_friend_request=pending_friend_request,
897 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
898 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
899 parking_details=parkingdetails2api[db_user.parking_details],
900 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
901 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
902 badges=[badge.badge_id for badge in db_user.badges],
903 **get_strong_verification_fields(session, db_user),
904 )
906 if db_user.max_guests is not None:
907 user.max_guests.value = db_user.max_guests
909 if db_user.last_minute is not None:
910 user.last_minute.value = db_user.last_minute
912 if db_user.has_pets is not None:
913 user.has_pets.value = db_user.has_pets
915 if db_user.accepts_pets is not None:
916 user.accepts_pets.value = db_user.accepts_pets
918 if db_user.pet_details is not None:
919 user.pet_details.value = db_user.pet_details
921 if db_user.has_kids is not None:
922 user.has_kids.value = db_user.has_kids
924 if db_user.accepts_kids is not None:
925 user.accepts_kids.value = db_user.accepts_kids
927 if db_user.kid_details is not None:
928 user.kid_details.value = db_user.kid_details
930 if db_user.has_housemates is not None:
931 user.has_housemates.value = db_user.has_housemates
933 if db_user.housemate_details is not None:
934 user.housemate_details.value = db_user.housemate_details
936 if db_user.wheelchair_accessible is not None:
937 user.wheelchair_accessible.value = db_user.wheelchair_accessible
939 if db_user.smokes_at_home is not None:
940 user.smokes_at_home.value = db_user.smokes_at_home
942 if db_user.drinking_allowed is not None:
943 user.drinking_allowed.value = db_user.drinking_allowed
945 if db_user.drinks_at_home is not None:
946 user.drinks_at_home.value = db_user.drinks_at_home
948 if db_user.other_host_info is not None:
949 user.other_host_info.value = db_user.other_host_info
951 if db_user.sleeping_details is not None:
952 user.sleeping_details.value = db_user.sleeping_details
954 if db_user.area is not None:
955 user.area.value = db_user.area
957 if db_user.house_rules is not None:
958 user.house_rules.value = db_user.house_rules
960 if db_user.parking is not None:
961 user.parking.value = db_user.parking
963 if db_user.camping_ok is not None:
964 user.camping_ok.value = db_user.camping_ok
966 return user
969def lite_user_to_pb(lite_user):
970 lat, lng = get_coordinates(lite_user.geom) or (0, 0)
972 return api_pb2.LiteUser(
973 user_id=lite_user.id,
974 username=lite_user.username,
975 name=lite_user.name,
976 city=lite_user.city,
977 age=int(lite_user.age),
978 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
979 if lite_user.avatar_filename
980 else None,
981 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
982 if lite_user.avatar_filename
983 else None,
984 lat=lat,
985 lng=lng,
986 radius=lite_user.radius,
987 has_strong_verification=lite_user.has_strong_verification,
988 )