Coverage for src/couchers/servicers/api.py: 97%
369 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +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 api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE: SleepingArrangement.shared_space,
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 SleepingArrangement.shared_space: api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE,
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 return api_pb2.PingRes(
190 user=user_model_to_pb(user, session, context),
191 unseen_message_count=unseen_message_count,
192 unseen_sent_host_request_count=unseen_sent_host_request_count,
193 unseen_received_host_request_count=unseen_received_host_request_count,
194 pending_friend_request_count=pending_friend_request_count,
195 )
197 def GetUser(self, request, context, session):
198 user = session.execute(
199 select(User).where_users_visible(context).where_username_or_id(request.user)
200 ).scalar_one_or_none()
202 if not user:
203 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
205 return user_model_to_pb(user, session, context)
207 def GetLiteUser(self, request, context, session):
208 lite_user = session.execute(
209 select(lite_users)
210 .where_users_visible(context, table=lite_users.c)
211 .where_username_or_id(request.user, table=lite_users.c)
212 ).one_or_none()
214 if not lite_user:
215 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
217 return lite_user_to_pb(lite_user)
219 def GetLiteUsers(self, request, context, session):
220 if len(request.users) > MAX_USERS_PER_QUERY:
221 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
223 usernames = {u for u in request.users if is_valid_username(u)}
224 ids = {u for u in request.users if is_valid_user_id(u)}
226 users = session.execute(
227 select(lite_users)
228 .where_users_visible(context, table=lite_users.c)
229 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
230 ).all()
232 users_by_id = {str(user.id): user for user in users}
233 users_by_username = {user.username: user for user in users}
235 res = api_pb2.GetLiteUsersRes()
237 for user in request.users:
238 lite_user = None
239 if user in users_by_id:
240 lite_user = users_by_id[user]
241 elif user in users_by_username:
242 lite_user = users_by_username[user]
244 res.responses.append(
245 api_pb2.LiteUserRes(
246 query=user,
247 not_found=lite_user is None,
248 user=lite_user_to_pb(lite_user) if lite_user else None,
249 )
250 )
252 return res
254 def UpdateProfile(self, request, context, session):
255 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
257 if request.HasField("name"):
258 if not is_valid_name(request.name.value):
259 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
260 user.name = request.name.value
262 if request.HasField("city"):
263 user.city = request.city.value
265 if request.HasField("hometown"):
266 if request.hometown.is_null:
267 user.hometown = None
268 else:
269 user.hometown = request.hometown.value
271 if request.HasField("lat") and request.HasField("lng"):
272 if request.lat.value == 0 and request.lng.value == 0:
273 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
274 user.geom = create_coordinate(request.lat.value, request.lng.value)
276 if request.HasField("radius"):
277 user.geom_radius = request.radius.value
279 if request.HasField("avatar_key"):
280 if request.avatar_key.is_null:
281 user.avatar_key = None
282 else:
283 user.avatar_key = request.avatar_key.value
285 # if request.HasField("gender"):
286 # user.gender = request.gender.value
288 if request.HasField("pronouns"):
289 if request.pronouns.is_null:
290 user.pronouns = None
291 else:
292 user.pronouns = request.pronouns.value
294 if request.HasField("occupation"):
295 if request.occupation.is_null:
296 user.occupation = None
297 else:
298 user.occupation = request.occupation.value
300 if request.HasField("education"):
301 if request.education.is_null:
302 user.education = None
303 else:
304 user.education = request.education.value
306 if request.HasField("about_me"):
307 if request.about_me.is_null:
308 user.about_me = None
309 else:
310 user.about_me = request.about_me.value
312 if request.HasField("things_i_like"):
313 if request.things_i_like.is_null:
314 user.things_i_like = None
315 else:
316 user.things_i_like = request.things_i_like.value
318 if request.HasField("about_place"):
319 if request.about_place.is_null:
320 user.about_place = None
321 else:
322 user.about_place = request.about_place.value
324 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
325 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
326 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
327 user.hosting_status = hostingstatus2sql[request.hosting_status]
329 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
330 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
331 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
332 user.meetup_status = meetupstatus2sql[request.meetup_status]
334 if request.HasField("language_abilities"):
335 # delete all existing abilities
336 for ability in user.language_abilities:
337 session.delete(ability)
338 session.flush()
340 # add the new ones
341 for language_ability in request.language_abilities.value:
342 if not language_is_allowed(language_ability.code):
343 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
344 session.add(
345 LanguageAbility(
346 user=user,
347 language_code=language_ability.code,
348 fluency=fluency2sql[language_ability.fluency],
349 )
350 )
352 if request.HasField("regions_visited"):
353 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
355 for region in request.regions_visited.value:
356 if not region_is_allowed(region):
357 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
358 session.add(
359 RegionVisited(
360 user_id=user.id,
361 region_code=region,
362 )
363 )
365 if request.HasField("regions_lived"):
366 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
368 for region in request.regions_lived.value:
369 if not region_is_allowed(region):
370 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
371 session.add(
372 RegionLived(
373 user_id=user.id,
374 region_code=region,
375 )
376 )
378 if request.HasField("additional_information"):
379 if request.additional_information.is_null:
380 user.additional_information = None
381 else:
382 user.additional_information = request.additional_information.value
384 if request.HasField("max_guests"):
385 if request.max_guests.is_null:
386 user.max_guests = None
387 else:
388 user.max_guests = request.max_guests.value
390 if request.HasField("last_minute"):
391 if request.last_minute.is_null:
392 user.last_minute = None
393 else:
394 user.last_minute = request.last_minute.value
396 if request.HasField("has_pets"):
397 if request.has_pets.is_null:
398 user.has_pets = None
399 else:
400 user.has_pets = request.has_pets.value
402 if request.HasField("accepts_pets"):
403 if request.accepts_pets.is_null:
404 user.accepts_pets = None
405 else:
406 user.accepts_pets = request.accepts_pets.value
408 if request.HasField("pet_details"):
409 if request.pet_details.is_null:
410 user.pet_details = None
411 else:
412 user.pet_details = request.pet_details.value
414 if request.HasField("has_kids"):
415 if request.has_kids.is_null:
416 user.has_kids = None
417 else:
418 user.has_kids = request.has_kids.value
420 if request.HasField("accepts_kids"):
421 if request.accepts_kids.is_null:
422 user.accepts_kids = None
423 else:
424 user.accepts_kids = request.accepts_kids.value
426 if request.HasField("kid_details"):
427 if request.kid_details.is_null:
428 user.kid_details = None
429 else:
430 user.kid_details = request.kid_details.value
432 if request.HasField("has_housemates"):
433 if request.has_housemates.is_null:
434 user.has_housemates = None
435 else:
436 user.has_housemates = request.has_housemates.value
438 if request.HasField("housemate_details"):
439 if request.housemate_details.is_null:
440 user.housemate_details = None
441 else:
442 user.housemate_details = request.housemate_details.value
444 if request.HasField("wheelchair_accessible"):
445 if request.wheelchair_accessible.is_null:
446 user.wheelchair_accessible = None
447 else:
448 user.wheelchair_accessible = request.wheelchair_accessible.value
450 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
451 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
453 if request.HasField("smokes_at_home"):
454 if request.smokes_at_home.is_null:
455 user.smokes_at_home = None
456 else:
457 user.smokes_at_home = request.smokes_at_home.value
459 if request.HasField("drinking_allowed"):
460 if request.drinking_allowed.is_null:
461 user.drinking_allowed = None
462 else:
463 user.drinking_allowed = request.drinking_allowed.value
465 if request.HasField("drinks_at_home"):
466 if request.drinks_at_home.is_null:
467 user.drinks_at_home = None
468 else:
469 user.drinks_at_home = request.drinks_at_home.value
471 if request.HasField("other_host_info"):
472 if request.other_host_info.is_null:
473 user.other_host_info = None
474 else:
475 user.other_host_info = request.other_host_info.value
477 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
478 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
480 if request.HasField("sleeping_details"):
481 if request.sleeping_details.is_null:
482 user.sleeping_details = None
483 else:
484 user.sleeping_details = request.sleeping_details.value
486 if request.HasField("area"):
487 if request.area.is_null:
488 user.area = None
489 else:
490 user.area = request.area.value
492 if request.HasField("house_rules"):
493 if request.house_rules.is_null:
494 user.house_rules = None
495 else:
496 user.house_rules = request.house_rules.value
498 if request.HasField("parking"):
499 if request.parking.is_null:
500 user.parking = None
501 else:
502 user.parking = request.parking.value
504 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
505 user.parking_details = parkingdetails2sql[request.parking_details]
507 if request.HasField("camping_ok"):
508 if request.camping_ok.is_null:
509 user.camping_ok = None
510 else:
511 user.camping_ok = request.camping_ok.value
513 # save updates
514 session.commit()
516 return empty_pb2.Empty()
518 def ListFriends(self, request, context, session):
519 rels = (
520 session.execute(
521 select(FriendRelationship)
522 .where_users_column_visible(context, FriendRelationship.from_user_id)
523 .where_users_column_visible(context, FriendRelationship.to_user_id)
524 .where(
525 or_(
526 FriendRelationship.from_user_id == context.user_id,
527 FriendRelationship.to_user_id == context.user_id,
528 )
529 )
530 .where(FriendRelationship.status == FriendStatus.accepted)
531 )
532 .scalars()
533 .all()
534 )
535 return api_pb2.ListFriendsRes(
536 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
537 )
539 def ListMutualFriends(self, request, context, session):
540 if context.user_id == request.user_id:
541 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
543 user = session.execute(
544 select(User).where_users_visible(context).where(User.id == request.user_id)
545 ).scalar_one_or_none()
547 if not user:
548 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
550 q1 = (
551 select(FriendRelationship.from_user_id.label("user_id"))
552 .where(FriendRelationship.to_user_id == context.user_id)
553 .where(FriendRelationship.from_user_id != request.user_id)
554 .where(FriendRelationship.status == FriendStatus.accepted)
555 )
557 q2 = (
558 select(FriendRelationship.to_user_id.label("user_id"))
559 .where(FriendRelationship.from_user_id == context.user_id)
560 .where(FriendRelationship.to_user_id != request.user_id)
561 .where(FriendRelationship.status == FriendStatus.accepted)
562 )
564 q3 = (
565 select(FriendRelationship.from_user_id.label("user_id"))
566 .where(FriendRelationship.to_user_id == request.user_id)
567 .where(FriendRelationship.from_user_id != context.user_id)
568 .where(FriendRelationship.status == FriendStatus.accepted)
569 )
571 q4 = (
572 select(FriendRelationship.to_user_id.label("user_id"))
573 .where(FriendRelationship.from_user_id == request.user_id)
574 .where(FriendRelationship.to_user_id != context.user_id)
575 .where(FriendRelationship.status == FriendStatus.accepted)
576 )
578 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
580 mutual_friends = (
581 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
582 )
584 return api_pb2.ListMutualFriendsRes(
585 mutual_friends=[
586 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
587 for mutual_friend in mutual_friends
588 ]
589 )
591 def SendFriendRequest(self, request, context, session):
592 if context.user_id == request.user_id:
593 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
595 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
596 to_user = session.execute(
597 select(User).where_users_visible(context).where(User.id == request.user_id)
598 ).scalar_one_or_none()
600 if not to_user:
601 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
603 if (
604 session.execute(
605 select(FriendRelationship)
606 .where(
607 or_(
608 and_(
609 FriendRelationship.from_user_id == context.user_id,
610 FriendRelationship.to_user_id == request.user_id,
611 ),
612 and_(
613 FriendRelationship.from_user_id == request.user_id,
614 FriendRelationship.to_user_id == context.user_id,
615 ),
616 )
617 )
618 .where(
619 or_(
620 FriendRelationship.status == FriendStatus.accepted,
621 FriendRelationship.status == FriendStatus.pending,
622 )
623 )
624 ).scalar_one_or_none()
625 is not None
626 ):
627 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
629 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
631 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
632 session.add(friend_relationship)
633 session.flush()
635 notify(
636 session,
637 user_id=friend_relationship.to_user_id,
638 topic_action="friend_request:create",
639 key=friend_relationship.from_user_id,
640 data=notification_data_pb2.FriendRequestCreate(
641 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
642 ),
643 )
645 return empty_pb2.Empty()
647 def ListFriendRequests(self, request, context, session):
648 # both sent and received
649 sent_requests = (
650 session.execute(
651 select(FriendRelationship)
652 .where_users_column_visible(context, FriendRelationship.to_user_id)
653 .where(FriendRelationship.from_user_id == context.user_id)
654 .where(FriendRelationship.status == FriendStatus.pending)
655 )
656 .scalars()
657 .all()
658 )
660 received_requests = (
661 session.execute(
662 select(FriendRelationship)
663 .where_users_column_visible(context, FriendRelationship.from_user_id)
664 .where(FriendRelationship.to_user_id == context.user_id)
665 .where(FriendRelationship.status == FriendStatus.pending)
666 )
667 .scalars()
668 .all()
669 )
671 return api_pb2.ListFriendRequestsRes(
672 sent=[
673 api_pb2.FriendRequest(
674 friend_request_id=friend_request.id,
675 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
676 user_id=friend_request.to_user.id,
677 sent=True,
678 )
679 for friend_request in sent_requests
680 ],
681 received=[
682 api_pb2.FriendRequest(
683 friend_request_id=friend_request.id,
684 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
685 user_id=friend_request.from_user.id,
686 sent=False,
687 )
688 for friend_request in received_requests
689 ],
690 )
692 def RespondFriendRequest(self, request, context, session):
693 friend_request = session.execute(
694 select(FriendRelationship)
695 .where_users_column_visible(context, FriendRelationship.from_user_id)
696 .where(FriendRelationship.to_user_id == context.user_id)
697 .where(FriendRelationship.status == FriendStatus.pending)
698 .where(FriendRelationship.id == request.friend_request_id)
699 ).scalar_one_or_none()
701 if not friend_request:
702 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
704 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
705 friend_request.time_responded = func.now()
707 session.flush()
709 if friend_request.status == FriendStatus.accepted:
710 notify(
711 session,
712 user_id=friend_request.from_user_id,
713 topic_action="friend_request:accept",
714 key=friend_request.to_user_id,
715 data=notification_data_pb2.FriendRequestAccept(
716 other_user=user_model_to_pb(friend_request.to_user, session, context),
717 ),
718 )
720 return empty_pb2.Empty()
722 def CancelFriendRequest(self, request, context, session):
723 friend_request = session.execute(
724 select(FriendRelationship)
725 .where_users_column_visible(context, FriendRelationship.to_user_id)
726 .where(FriendRelationship.from_user_id == context.user_id)
727 .where(FriendRelationship.status == FriendStatus.pending)
728 .where(FriendRelationship.id == request.friend_request_id)
729 ).scalar_one_or_none()
731 if not friend_request:
732 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
734 friend_request.status = FriendStatus.cancelled
735 friend_request.time_responded = func.now()
737 # note no notifications
739 session.commit()
741 return empty_pb2.Empty()
743 def InitiateMediaUpload(self, request, context, session):
744 key = random_hex()
746 created = now()
747 expiry = created + timedelta(minutes=20)
749 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
750 session.add(upload)
751 session.commit()
753 req = media_pb2.UploadRequest(
754 key=upload.key,
755 type=media_pb2.UploadRequest.UploadType.IMAGE,
756 created=Timestamp_from_datetime(upload.created),
757 expiry=Timestamp_from_datetime(upload.expiry),
758 max_width=2000,
759 max_height=1600,
760 ).SerializeToString()
762 data = b64encode(req)
763 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
765 path = "upload?" + urlencode({"data": data, "sig": sig})
767 return api_pb2.InitiateMediaUploadRes(
768 upload_url=urls.media_upload_url(path=path),
769 expiry=Timestamp_from_datetime(expiry),
770 )
772 def ListBadgeUsers(self, request, context, session):
773 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
774 next_user_id = int(request.page_token) if request.page_token else 0
775 badge = get_badge_dict().get(request.badge_id)
776 if not badge:
777 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND)
779 badge_user_ids = (
780 session.execute(
781 select(UserBadge.user_id)
782 .where(UserBadge.badge_id == badge["id"])
783 .where(UserBadge.user_id >= next_user_id)
784 .order_by(UserBadge.user_id)
785 .limit(page_size + 1)
786 )
787 .scalars()
788 .all()
789 )
791 return api_pb2.ListBadgeUsersRes(
792 user_ids=badge_user_ids[:page_size],
793 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None,
794 )
797def user_model_to_pb(db_user, session, context):
798 num_references = session.execute(
799 select(func.count())
800 .select_from(Reference)
801 .join(User, User.id == Reference.from_user_id)
802 .where(User.is_visible)
803 .where(Reference.to_user_id == db_user.id)
804 ).scalar_one()
806 # returns (lat, lng)
807 # we put people without coords on null island
808 # https://en.wikipedia.org/wiki/Null_Island
809 lat, lng = db_user.coordinates or (0, 0)
811 pending_friend_request = None
812 if db_user.id == context.user_id:
813 friends_status = api_pb2.User.FriendshipStatus.NA
814 else:
815 friend_relationship = session.execute(
816 select(FriendRelationship)
817 .where(
818 or_(
819 and_(
820 FriendRelationship.from_user_id == context.user_id,
821 FriendRelationship.to_user_id == db_user.id,
822 ),
823 and_(
824 FriendRelationship.from_user_id == db_user.id,
825 FriendRelationship.to_user_id == context.user_id,
826 ),
827 )
828 )
829 .where(
830 or_(
831 FriendRelationship.status == FriendStatus.accepted,
832 FriendRelationship.status == FriendStatus.pending,
833 )
834 )
835 ).scalar_one_or_none()
837 if friend_relationship:
838 if friend_relationship.status == FriendStatus.accepted:
839 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
840 else:
841 friends_status = api_pb2.User.FriendshipStatus.PENDING
842 if friend_relationship.from_user_id == context.user_id:
843 # we sent it
844 pending_friend_request = api_pb2.FriendRequest(
845 friend_request_id=friend_relationship.id,
846 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
847 user_id=friend_relationship.to_user.id,
848 sent=True,
849 )
850 else:
851 # we received it
852 pending_friend_request = api_pb2.FriendRequest(
853 friend_request_id=friend_relationship.id,
854 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
855 user_id=friend_relationship.from_user.id,
856 sent=False,
857 )
858 else:
859 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
861 verification_score = 0.0
862 if db_user.phone_verification_verified:
863 verification_score += 1.0 * db_user.phone_is_verified
865 user = api_pb2.User(
866 user_id=db_user.id,
867 username=db_user.username,
868 name=db_user.name,
869 city=db_user.city,
870 hometown=db_user.hometown,
871 timezone=db_user.timezone,
872 lat=lat,
873 lng=lng,
874 radius=db_user.geom_radius,
875 verification=verification_score,
876 community_standing=db_user.community_standing,
877 num_references=num_references,
878 gender=db_user.gender,
879 pronouns=db_user.pronouns,
880 age=int(db_user.age),
881 joined=Timestamp_from_datetime(db_user.display_joined),
882 last_active=Timestamp_from_datetime(db_user.display_last_active),
883 hosting_status=hostingstatus2api[db_user.hosting_status],
884 meetup_status=meetupstatus2api[db_user.meetup_status],
885 occupation=db_user.occupation,
886 education=db_user.education,
887 about_me=db_user.about_me,
888 things_i_like=db_user.things_i_like,
889 about_place=db_user.about_place,
890 language_abilities=[
891 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
892 for ability in db_user.language_abilities
893 ],
894 regions_visited=[region.code for region in db_user.regions_visited],
895 regions_lived=[region.code for region in db_user.regions_lived],
896 additional_information=db_user.additional_information,
897 friends=friends_status,
898 pending_friend_request=pending_friend_request,
899 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
900 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
901 parking_details=parkingdetails2api[db_user.parking_details],
902 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
903 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
904 badges=[badge.badge_id for badge in db_user.badges],
905 **get_strong_verification_fields(session, db_user),
906 )
908 if db_user.max_guests is not None:
909 user.max_guests.value = db_user.max_guests
911 if db_user.last_minute is not None:
912 user.last_minute.value = db_user.last_minute
914 if db_user.has_pets is not None:
915 user.has_pets.value = db_user.has_pets
917 if db_user.accepts_pets is not None:
918 user.accepts_pets.value = db_user.accepts_pets
920 if db_user.pet_details is not None:
921 user.pet_details.value = db_user.pet_details
923 if db_user.has_kids is not None:
924 user.has_kids.value = db_user.has_kids
926 if db_user.accepts_kids is not None:
927 user.accepts_kids.value = db_user.accepts_kids
929 if db_user.kid_details is not None:
930 user.kid_details.value = db_user.kid_details
932 if db_user.has_housemates is not None:
933 user.has_housemates.value = db_user.has_housemates
935 if db_user.housemate_details is not None:
936 user.housemate_details.value = db_user.housemate_details
938 if db_user.wheelchair_accessible is not None:
939 user.wheelchair_accessible.value = db_user.wheelchair_accessible
941 if db_user.smokes_at_home is not None:
942 user.smokes_at_home.value = db_user.smokes_at_home
944 if db_user.drinking_allowed is not None:
945 user.drinking_allowed.value = db_user.drinking_allowed
947 if db_user.drinks_at_home is not None:
948 user.drinks_at_home.value = db_user.drinks_at_home
950 if db_user.other_host_info is not None:
951 user.other_host_info.value = db_user.other_host_info
953 if db_user.sleeping_details is not None:
954 user.sleeping_details.value = db_user.sleeping_details
956 if db_user.area is not None:
957 user.area.value = db_user.area
959 if db_user.house_rules is not None:
960 user.house_rules.value = db_user.house_rules
962 if db_user.parking is not None:
963 user.parking.value = db_user.parking
965 if db_user.camping_ok is not None:
966 user.camping_ok.value = db_user.camping_ok
968 return user
971def lite_user_to_pb(lite_user):
972 lat, lng = get_coordinates(lite_user.geom) or (0, 0)
974 return api_pb2.LiteUser(
975 user_id=lite_user.id,
976 username=lite_user.username,
977 name=lite_user.name,
978 city=lite_user.city,
979 age=int(lite_user.age),
980 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
981 if lite_user.avatar_filename
982 else None,
983 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
984 if lite_user.avatar_filename
985 else None,
986 lat=lat,
987 lng=lng,
988 radius=lite_user.radius,
989 has_strong_verification=lite_user.has_strong_verification,
990 )