Coverage for src/couchers/servicers/api.py: 97%
361 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-10-21 08:09 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-10-21 08:09 +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)
32from couchers.notifications.notify import notify
33from couchers.resources import language_is_allowed, region_is_allowed
34from couchers.servicers.account import get_strong_verification_fields
35from couchers.sql import couchers_select as select
36from couchers.sql import is_valid_user_id, is_valid_username
37from couchers.utils import Timestamp_from_datetime, create_coordinate, is_valid_name, now
38from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2
40MAX_USERS_PER_QUERY = 200
42hostingstatus2sql = {
43 api_pb2.HOSTING_STATUS_UNKNOWN: None,
44 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
45 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
46 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
47}
49hostingstatus2api = {
50 None: api_pb2.HOSTING_STATUS_UNKNOWN,
51 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
52 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
53 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
54}
56meetupstatus2sql = {
57 api_pb2.MEETUP_STATUS_UNKNOWN: None,
58 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
59 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
60 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
61}
63meetupstatus2api = {
64 None: api_pb2.MEETUP_STATUS_UNKNOWN,
65 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
66 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
67 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
68}
70smokinglocation2sql = {
71 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
72 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
73 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
74 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
75 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
76}
78smokinglocation2api = {
79 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
80 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
81 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
82 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
83 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
84}
86sleepingarrangement2sql = {
87 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
88 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
89 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
90 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
91 api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE: SleepingArrangement.shared_space,
92}
94sleepingarrangement2api = {
95 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
96 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
97 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
98 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
99 SleepingArrangement.shared_space: api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE,
100}
102parkingdetails2sql = {
103 api_pb2.PARKING_DETAILS_UNKNOWN: None,
104 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
105 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
106 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
107 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
108}
110parkingdetails2api = {
111 None: api_pb2.PARKING_DETAILS_UNKNOWN,
112 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
113 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
114 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
115 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
116}
118fluency2sql = {
119 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
120 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
121 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
122 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
123}
125fluency2api = {
126 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
127 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
128 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
129 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
130}
133class API(api_pb2_grpc.APIServicer):
134 def Ping(self, request, context, session):
135 # auth ought to make sure the user exists
136 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
138 # gets only the max message by self-joining messages which have a greater id
139 # if it doesn't have a greater id, it's the biggest
140 message_2 = aliased(Message)
141 unseen_sent_host_request_count = session.execute(
142 select(func.count())
143 .select_from(Message)
144 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
145 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
146 .where(HostRequest.surfer_user_id == context.user_id)
147 .where_users_column_visible(context, HostRequest.host_user_id)
148 .where(message_2.id == None)
149 .where(HostRequest.surfer_last_seen_message_id < Message.id)
150 ).scalar_one()
152 unseen_received_host_request_count = session.execute(
153 select(func.count())
154 .select_from(Message)
155 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
156 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id))
157 .where_users_column_visible(context, HostRequest.surfer_user_id)
158 .where(HostRequest.host_user_id == context.user_id)
159 .where(message_2.id == None)
160 .where(HostRequest.host_last_seen_message_id < Message.id)
161 ).scalar_one()
163 unseen_message_count = session.execute(
164 select(func.count())
165 .select_from(Message)
166 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
167 .where(GroupChatSubscription.user_id == context.user_id)
168 .where(Message.time >= GroupChatSubscription.joined)
169 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
170 .where(Message.id > GroupChatSubscription.last_seen_message_id)
171 ).scalar_one()
173 pending_friend_request_count = session.execute(
174 select(func.count())
175 .select_from(FriendRelationship)
176 .where(FriendRelationship.to_user_id == context.user_id)
177 .where_users_column_visible(context, FriendRelationship.from_user_id)
178 .where(FriendRelationship.status == FriendStatus.pending)
179 ).scalar_one()
181 return api_pb2.PingRes(
182 user=user_model_to_pb(user, session, context),
183 unseen_message_count=unseen_message_count,
184 unseen_sent_host_request_count=unseen_sent_host_request_count,
185 unseen_received_host_request_count=unseen_received_host_request_count,
186 pending_friend_request_count=pending_friend_request_count,
187 )
189 def GetUser(self, request, context, session):
190 user = session.execute(
191 select(User).where_users_visible(context).where_username_or_id(request.user)
192 ).scalar_one_or_none()
194 if not user:
195 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
197 return user_model_to_pb(user, session, context)
199 def GetLiteUser(self, request, context, session):
200 lite_user = session.execute(
201 select(lite_users)
202 .where_users_visible(context, table=lite_users.c)
203 .where_username_or_id(request.user, table=lite_users.c)
204 ).one_or_none()
206 if not lite_user:
207 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
209 return api_pb2.LiteUser(
210 user_id=lite_user.id,
211 username=lite_user.username,
212 name=lite_user.name,
213 city=lite_user.city,
214 age=int(lite_user.age),
215 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
216 if lite_user.avatar_filename
217 else None,
218 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
219 if lite_user.avatar_filename
220 else None,
221 lat=lite_user.lat,
222 lng=lite_user.lng,
223 radius=lite_user.radius,
224 )
226 def GetLiteUsers(self, request, context, session):
227 if len(request.users) > MAX_USERS_PER_QUERY:
228 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS)
230 usernames = {u for u in request.users if is_valid_username(u)}
231 ids = {u for u in request.users if is_valid_user_id(u)}
233 users = session.execute(
234 select(lite_users)
235 .where_users_visible(context, table=lite_users.c)
236 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids)))
237 ).all()
239 users_by_id = {str(user.id): user for user in users}
240 users_by_username = {user.username: user for user in users}
242 res = api_pb2.GetLiteUsersRes()
244 for user in request.users:
245 lite_user = None
246 if user in users_by_id:
247 lite_user = users_by_id[user]
248 elif user in users_by_username:
249 lite_user = users_by_username[user]
251 res.responses.append(
252 api_pb2.LiteUserRes(
253 query=user,
254 not_found=lite_user is None,
255 user=api_pb2.LiteUser(
256 user_id=lite_user.id,
257 username=lite_user.username,
258 name=lite_user.name,
259 city=lite_user.city,
260 age=int(lite_user.age),
261 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full")
262 if lite_user.avatar_filename
263 else None,
264 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
265 if lite_user.avatar_filename
266 else None,
267 lat=lite_user.lat,
268 lng=lite_user.lng,
269 radius=lite_user.radius,
270 )
271 if lite_user
272 else None,
273 )
274 )
276 return res
278 def UpdateProfile(self, request, context, session):
279 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
281 if request.HasField("name"):
282 if not is_valid_name(request.name.value):
283 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
284 user.name = request.name.value
286 if request.HasField("city"):
287 user.city = request.city.value
289 if request.HasField("hometown"):
290 if request.hometown.is_null:
291 user.hometown = None
292 else:
293 user.hometown = request.hometown.value
295 if request.HasField("lat") and request.HasField("lng"):
296 if request.lat.value == 0 and request.lng.value == 0:
297 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
298 user.geom = create_coordinate(request.lat.value, request.lng.value)
300 if request.HasField("radius"):
301 user.geom_radius = request.radius.value
303 if request.HasField("avatar_key"):
304 if request.avatar_key.is_null:
305 user.avatar_key = None
306 else:
307 user.avatar_key = request.avatar_key.value
309 # if request.HasField("gender"):
310 # user.gender = request.gender.value
312 if request.HasField("pronouns"):
313 if request.pronouns.is_null:
314 user.pronouns = None
315 else:
316 user.pronouns = request.pronouns.value
318 if request.HasField("occupation"):
319 if request.occupation.is_null:
320 user.occupation = None
321 else:
322 user.occupation = request.occupation.value
324 if request.HasField("education"):
325 if request.education.is_null:
326 user.education = None
327 else:
328 user.education = request.education.value
330 if request.HasField("about_me"):
331 if request.about_me.is_null:
332 user.about_me = None
333 else:
334 user.about_me = request.about_me.value
336 if request.HasField("my_travels"):
337 if request.my_travels.is_null:
338 user.my_travels = None
339 else:
340 user.my_travels = request.my_travels.value
342 if request.HasField("things_i_like"):
343 if request.things_i_like.is_null:
344 user.things_i_like = None
345 else:
346 user.things_i_like = request.things_i_like.value
348 if request.HasField("about_place"):
349 if request.about_place.is_null:
350 user.about_place = None
351 else:
352 user.about_place = request.about_place.value
354 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
355 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST:
356 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST)
357 user.hosting_status = hostingstatus2sql[request.hosting_status]
359 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
360 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP:
361 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET)
362 user.meetup_status = meetupstatus2sql[request.meetup_status]
364 if request.HasField("language_abilities"):
365 # delete all existing abilities
366 for ability in user.language_abilities:
367 session.delete(ability)
368 session.flush()
370 # add the new ones
371 for language_ability in request.language_abilities.value:
372 if not language_is_allowed(language_ability.code):
373 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
374 session.add(
375 LanguageAbility(
376 user=user,
377 language_code=language_ability.code,
378 fluency=fluency2sql[language_ability.fluency],
379 )
380 )
382 if request.HasField("regions_visited"):
383 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
385 for region in request.regions_visited.value:
386 if not region_is_allowed(region):
387 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
388 session.add(
389 RegionVisited(
390 user_id=user.id,
391 region_code=region,
392 )
393 )
395 if request.HasField("regions_lived"):
396 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
398 for region in request.regions_lived.value:
399 if not region_is_allowed(region):
400 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
401 session.add(
402 RegionLived(
403 user_id=user.id,
404 region_code=region,
405 )
406 )
408 if request.HasField("additional_information"):
409 if request.additional_information.is_null:
410 user.additional_information = None
411 else:
412 user.additional_information = request.additional_information.value
414 if request.HasField("max_guests"):
415 if request.max_guests.is_null:
416 user.max_guests = None
417 else:
418 user.max_guests = request.max_guests.value
420 if request.HasField("last_minute"):
421 if request.last_minute.is_null:
422 user.last_minute = None
423 else:
424 user.last_minute = request.last_minute.value
426 if request.HasField("has_pets"):
427 if request.has_pets.is_null:
428 user.has_pets = None
429 else:
430 user.has_pets = request.has_pets.value
432 if request.HasField("accepts_pets"):
433 if request.accepts_pets.is_null:
434 user.accepts_pets = None
435 else:
436 user.accepts_pets = request.accepts_pets.value
438 if request.HasField("pet_details"):
439 if request.pet_details.is_null:
440 user.pet_details = None
441 else:
442 user.pet_details = request.pet_details.value
444 if request.HasField("has_kids"):
445 if request.has_kids.is_null:
446 user.has_kids = None
447 else:
448 user.has_kids = request.has_kids.value
450 if request.HasField("accepts_kids"):
451 if request.accepts_kids.is_null:
452 user.accepts_kids = None
453 else:
454 user.accepts_kids = request.accepts_kids.value
456 if request.HasField("kid_details"):
457 if request.kid_details.is_null:
458 user.kid_details = None
459 else:
460 user.kid_details = request.kid_details.value
462 if request.HasField("has_housemates"):
463 if request.has_housemates.is_null:
464 user.has_housemates = None
465 else:
466 user.has_housemates = request.has_housemates.value
468 if request.HasField("housemate_details"):
469 if request.housemate_details.is_null:
470 user.housemate_details = None
471 else:
472 user.housemate_details = request.housemate_details.value
474 if request.HasField("wheelchair_accessible"):
475 if request.wheelchair_accessible.is_null:
476 user.wheelchair_accessible = None
477 else:
478 user.wheelchair_accessible = request.wheelchair_accessible.value
480 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
481 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
483 if request.HasField("smokes_at_home"):
484 if request.smokes_at_home.is_null:
485 user.smokes_at_home = None
486 else:
487 user.smokes_at_home = request.smokes_at_home.value
489 if request.HasField("drinking_allowed"):
490 if request.drinking_allowed.is_null:
491 user.drinking_allowed = None
492 else:
493 user.drinking_allowed = request.drinking_allowed.value
495 if request.HasField("drinks_at_home"):
496 if request.drinks_at_home.is_null:
497 user.drinks_at_home = None
498 else:
499 user.drinks_at_home = request.drinks_at_home.value
501 if request.HasField("other_host_info"):
502 if request.other_host_info.is_null:
503 user.other_host_info = None
504 else:
505 user.other_host_info = request.other_host_info.value
507 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
508 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
510 if request.HasField("sleeping_details"):
511 if request.sleeping_details.is_null:
512 user.sleeping_details = None
513 else:
514 user.sleeping_details = request.sleeping_details.value
516 if request.HasField("area"):
517 if request.area.is_null:
518 user.area = None
519 else:
520 user.area = request.area.value
522 if request.HasField("house_rules"):
523 if request.house_rules.is_null:
524 user.house_rules = None
525 else:
526 user.house_rules = request.house_rules.value
528 if request.HasField("parking"):
529 if request.parking.is_null:
530 user.parking = None
531 else:
532 user.parking = request.parking.value
534 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
535 user.parking_details = parkingdetails2sql[request.parking_details]
537 if request.HasField("camping_ok"):
538 if request.camping_ok.is_null:
539 user.camping_ok = None
540 else:
541 user.camping_ok = request.camping_ok.value
543 # save updates
544 session.commit()
546 return empty_pb2.Empty()
548 def ListFriends(self, request, context, session):
549 rels = (
550 session.execute(
551 select(FriendRelationship)
552 .where_users_column_visible(context, FriendRelationship.from_user_id)
553 .where_users_column_visible(context, FriendRelationship.to_user_id)
554 .where(
555 or_(
556 FriendRelationship.from_user_id == context.user_id,
557 FriendRelationship.to_user_id == context.user_id,
558 )
559 )
560 .where(FriendRelationship.status == FriendStatus.accepted)
561 )
562 .scalars()
563 .all()
564 )
565 return api_pb2.ListFriendsRes(
566 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
567 )
569 def ListMutualFriends(self, request, context, session):
570 if context.user_id == request.user_id:
571 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
573 user = session.execute(
574 select(User).where_users_visible(context).where(User.id == request.user_id)
575 ).scalar_one_or_none()
577 if not user:
578 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
580 q1 = (
581 select(FriendRelationship.from_user_id.label("user_id"))
582 .where(FriendRelationship.to_user_id == context.user_id)
583 .where(FriendRelationship.from_user_id != request.user_id)
584 .where(FriendRelationship.status == FriendStatus.accepted)
585 )
587 q2 = (
588 select(FriendRelationship.to_user_id.label("user_id"))
589 .where(FriendRelationship.from_user_id == context.user_id)
590 .where(FriendRelationship.to_user_id != request.user_id)
591 .where(FriendRelationship.status == FriendStatus.accepted)
592 )
594 q3 = (
595 select(FriendRelationship.from_user_id.label("user_id"))
596 .where(FriendRelationship.to_user_id == request.user_id)
597 .where(FriendRelationship.from_user_id != context.user_id)
598 .where(FriendRelationship.status == FriendStatus.accepted)
599 )
601 q4 = (
602 select(FriendRelationship.to_user_id.label("user_id"))
603 .where(FriendRelationship.from_user_id == request.user_id)
604 .where(FriendRelationship.to_user_id != context.user_id)
605 .where(FriendRelationship.status == FriendStatus.accepted)
606 )
608 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
610 mutual_friends = (
611 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
612 )
614 return api_pb2.ListMutualFriendsRes(
615 mutual_friends=[
616 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name)
617 for mutual_friend in mutual_friends
618 ]
619 )
621 def SendFriendRequest(self, request, context, session):
622 if context.user_id == request.user_id:
623 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
625 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
626 to_user = session.execute(
627 select(User).where_users_visible(context).where(User.id == request.user_id)
628 ).scalar_one_or_none()
630 if not to_user:
631 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
633 if (
634 session.execute(
635 select(FriendRelationship)
636 .where(
637 or_(
638 and_(
639 FriendRelationship.from_user_id == context.user_id,
640 FriendRelationship.to_user_id == request.user_id,
641 ),
642 and_(
643 FriendRelationship.from_user_id == request.user_id,
644 FriendRelationship.to_user_id == context.user_id,
645 ),
646 )
647 )
648 .where(
649 or_(
650 FriendRelationship.status == FriendStatus.accepted,
651 FriendRelationship.status == FriendStatus.pending,
652 )
653 )
654 ).scalar_one_or_none()
655 is not None
656 ):
657 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
659 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
661 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
662 session.add(friend_relationship)
663 session.flush()
665 notify(
666 session,
667 user_id=friend_relationship.to_user_id,
668 topic_action="friend_request:create",
669 key=friend_relationship.from_user_id,
670 data=notification_data_pb2.FriendRequestCreate(
671 other_user=user_model_to_pb(friend_relationship.from_user, session, context),
672 ),
673 )
675 return empty_pb2.Empty()
677 def ListFriendRequests(self, request, context, session):
678 # both sent and received
679 sent_requests = (
680 session.execute(
681 select(FriendRelationship)
682 .where_users_column_visible(context, FriendRelationship.to_user_id)
683 .where(FriendRelationship.from_user_id == context.user_id)
684 .where(FriendRelationship.status == FriendStatus.pending)
685 )
686 .scalars()
687 .all()
688 )
690 received_requests = (
691 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 )
697 .scalars()
698 .all()
699 )
701 return api_pb2.ListFriendRequestsRes(
702 sent=[
703 api_pb2.FriendRequest(
704 friend_request_id=friend_request.id,
705 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
706 user_id=friend_request.to_user.id,
707 sent=True,
708 )
709 for friend_request in sent_requests
710 ],
711 received=[
712 api_pb2.FriendRequest(
713 friend_request_id=friend_request.id,
714 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
715 user_id=friend_request.from_user.id,
716 sent=False,
717 )
718 for friend_request in received_requests
719 ],
720 )
722 def RespondFriendRequest(self, request, context, session):
723 friend_request = session.execute(
724 select(FriendRelationship)
725 .where_users_column_visible(context, FriendRelationship.from_user_id)
726 .where(FriendRelationship.to_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.accepted if request.accept else FriendStatus.rejected
735 friend_request.time_responded = func.now()
737 session.flush()
739 if friend_request.status == FriendStatus.accepted:
740 notify(
741 session,
742 user_id=friend_request.from_user_id,
743 topic_action="friend_request:accept",
744 key=friend_request.to_user_id,
745 data=notification_data_pb2.FriendRequestAccept(
746 other_user=user_model_to_pb(friend_request.to_user, session, context),
747 ),
748 )
750 return empty_pb2.Empty()
752 def CancelFriendRequest(self, request, context, session):
753 friend_request = session.execute(
754 select(FriendRelationship)
755 .where_users_column_visible(context, FriendRelationship.to_user_id)
756 .where(FriendRelationship.from_user_id == context.user_id)
757 .where(FriendRelationship.status == FriendStatus.pending)
758 .where(FriendRelationship.id == request.friend_request_id)
759 ).scalar_one_or_none()
761 if not friend_request:
762 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
764 friend_request.status = FriendStatus.cancelled
765 friend_request.time_responded = func.now()
767 # note no notifications
769 session.commit()
771 return empty_pb2.Empty()
773 def InitiateMediaUpload(self, request, context, session):
774 key = random_hex()
776 created = now()
777 expiry = created + timedelta(minutes=20)
779 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
780 session.add(upload)
781 session.commit()
783 req = media_pb2.UploadRequest(
784 key=upload.key,
785 type=media_pb2.UploadRequest.UploadType.IMAGE,
786 created=Timestamp_from_datetime(upload.created),
787 expiry=Timestamp_from_datetime(upload.expiry),
788 max_width=2000,
789 max_height=1600,
790 ).SerializeToString()
792 data = b64encode(req)
793 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
795 path = "upload?" + urlencode({"data": data, "sig": sig})
797 return api_pb2.InitiateMediaUploadRes(
798 upload_url=urls.media_upload_url(path=path),
799 expiry=Timestamp_from_datetime(expiry),
800 )
803def user_model_to_pb(db_user, session, context):
804 num_references = session.execute(
805 select(func.count())
806 .select_from(Reference)
807 .join(User, User.id == Reference.from_user_id)
808 .where(User.is_visible)
809 .where(Reference.to_user_id == db_user.id)
810 ).scalar_one()
812 # returns (lat, lng)
813 # we put people without coords on null island
814 # https://en.wikipedia.org/wiki/Null_Island
815 lat, lng = db_user.coordinates or (0, 0)
817 pending_friend_request = None
818 if db_user.id == context.user_id:
819 friends_status = api_pb2.User.FriendshipStatus.NA
820 else:
821 friend_relationship = session.execute(
822 select(FriendRelationship)
823 .where(
824 or_(
825 and_(
826 FriendRelationship.from_user_id == context.user_id,
827 FriendRelationship.to_user_id == db_user.id,
828 ),
829 and_(
830 FriendRelationship.from_user_id == db_user.id,
831 FriendRelationship.to_user_id == context.user_id,
832 ),
833 )
834 )
835 .where(
836 or_(
837 FriendRelationship.status == FriendStatus.accepted,
838 FriendRelationship.status == FriendStatus.pending,
839 )
840 )
841 ).scalar_one_or_none()
843 if friend_relationship:
844 if friend_relationship.status == FriendStatus.accepted:
845 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
846 else:
847 friends_status = api_pb2.User.FriendshipStatus.PENDING
848 if friend_relationship.from_user_id == context.user_id:
849 # we sent 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.to_user.id,
854 sent=True,
855 )
856 else:
857 # we received it
858 pending_friend_request = api_pb2.FriendRequest(
859 friend_request_id=friend_relationship.id,
860 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
861 user_id=friend_relationship.from_user.id,
862 sent=False,
863 )
864 else:
865 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
867 verification_score = 0.0
868 if db_user.phone_verification_verified:
869 verification_score += 1.0 * db_user.phone_is_verified
871 user = api_pb2.User(
872 user_id=db_user.id,
873 username=db_user.username,
874 name=db_user.name,
875 city=db_user.city,
876 hometown=db_user.hometown,
877 timezone=db_user.timezone,
878 lat=lat,
879 lng=lng,
880 radius=db_user.geom_radius,
881 verification=verification_score,
882 community_standing=db_user.community_standing,
883 num_references=num_references,
884 gender=db_user.gender,
885 pronouns=db_user.pronouns,
886 age=int(db_user.age),
887 joined=Timestamp_from_datetime(db_user.display_joined),
888 last_active=Timestamp_from_datetime(db_user.display_last_active),
889 hosting_status=hostingstatus2api[db_user.hosting_status],
890 meetup_status=meetupstatus2api[db_user.meetup_status],
891 occupation=db_user.occupation,
892 education=db_user.education,
893 about_me=db_user.about_me,
894 my_travels=db_user.my_travels,
895 things_i_like=db_user.things_i_like,
896 about_place=db_user.about_place,
897 language_abilities=[
898 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
899 for ability in db_user.language_abilities
900 ],
901 regions_visited=[region.code for region in db_user.regions_visited],
902 regions_lived=[region.code for region in db_user.regions_lived],
903 additional_information=db_user.additional_information,
904 friends=friends_status,
905 pending_friend_request=pending_friend_request,
906 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
907 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
908 parking_details=parkingdetails2api[db_user.parking_details],
909 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
910 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None,
911 badges=[badge.badge_id for badge in db_user.badges],
912 **get_strong_verification_fields(session, db_user),
913 )
915 if db_user.max_guests is not None:
916 user.max_guests.value = db_user.max_guests
918 if db_user.last_minute is not None:
919 user.last_minute.value = db_user.last_minute
921 if db_user.has_pets is not None:
922 user.has_pets.value = db_user.has_pets
924 if db_user.accepts_pets is not None:
925 user.accepts_pets.value = db_user.accepts_pets
927 if db_user.pet_details is not None:
928 user.pet_details.value = db_user.pet_details
930 if db_user.has_kids is not None:
931 user.has_kids.value = db_user.has_kids
933 if db_user.accepts_kids is not None:
934 user.accepts_kids.value = db_user.accepts_kids
936 if db_user.kid_details is not None:
937 user.kid_details.value = db_user.kid_details
939 if db_user.has_housemates is not None:
940 user.has_housemates.value = db_user.has_housemates
942 if db_user.housemate_details is not None:
943 user.housemate_details.value = db_user.housemate_details
945 if db_user.wheelchair_accessible is not None:
946 user.wheelchair_accessible.value = db_user.wheelchair_accessible
948 if db_user.smokes_at_home is not None:
949 user.smokes_at_home.value = db_user.smokes_at_home
951 if db_user.drinking_allowed is not None:
952 user.drinking_allowed.value = db_user.drinking_allowed
954 if db_user.drinks_at_home is not None:
955 user.drinks_at_home.value = db_user.drinks_at_home
957 if db_user.other_host_info is not None:
958 user.other_host_info.value = db_user.other_host_info
960 if db_user.sleeping_details is not None:
961 user.sleeping_details.value = db_user.sleeping_details
963 if db_user.area is not None:
964 user.area.value = db_user.area
966 if db_user.house_rules is not None:
967 user.house_rules.value = db_user.house_rules
969 if db_user.parking is not None:
970 user.parking.value = db_user.parking
972 if db_user.camping_ok is not None:
973 user.camping_ok.value = db_user.camping_ok
975 return user