Coverage for src/couchers/servicers/api.py: 96%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.db import session_scope
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.sql import couchers_select as select
35from couchers.tasks import send_friend_request_accepted_email, send_friend_request_email
36from couchers.utils import Timestamp_from_datetime, create_coordinate, is_valid_name, now
37from proto import api_pb2, api_pb2_grpc, media_pb2
39hostingstatus2sql = {
40 api_pb2.HOSTING_STATUS_UNKNOWN: None,
41 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host,
42 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe,
43 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host,
44}
46hostingstatus2api = {
47 None: api_pb2.HOSTING_STATUS_UNKNOWN,
48 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST,
49 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE,
50 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST,
51}
53meetupstatus2sql = {
54 api_pb2.MEETUP_STATUS_UNKNOWN: None,
55 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup,
56 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup,
57 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup,
58}
60meetupstatus2api = {
61 None: api_pb2.MEETUP_STATUS_UNKNOWN,
62 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP,
63 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP,
64 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP,
65}
67smokinglocation2sql = {
68 api_pb2.SMOKING_LOCATION_UNKNOWN: None,
69 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes,
70 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window,
71 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside,
72 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no,
73}
75smokinglocation2api = {
76 None: api_pb2.SMOKING_LOCATION_UNKNOWN,
77 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES,
78 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW,
79 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE,
80 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO,
81}
83sleepingarrangement2sql = {
84 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None,
85 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private,
86 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common,
87 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room,
88 api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE: SleepingArrangement.shared_space,
89}
91sleepingarrangement2api = {
92 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN,
93 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE,
94 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON,
95 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM,
96 SleepingArrangement.shared_space: api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE,
97}
99parkingdetails2sql = {
100 api_pb2.PARKING_DETAILS_UNKNOWN: None,
101 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite,
102 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite,
103 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite,
104 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite,
105}
107parkingdetails2api = {
108 None: api_pb2.PARKING_DETAILS_UNKNOWN,
109 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE,
110 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE,
111 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE,
112 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE,
113}
115fluency2sql = {
116 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None,
117 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner,
118 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational,
119 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent,
120}
122fluency2api = {
123 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN,
124 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER,
125 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL,
126 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT,
127}
130class API(api_pb2_grpc.APIServicer):
131 def Ping(self, request, context):
132 with session_scope() as session:
133 # auth ought to make sure the user exists
134 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
136 # gets only the max message by self-joining messages which have a greater id
137 # if it doesn't have a greater id, it's the biggest
138 message_2 = aliased(Message)
139 unseen_sent_host_request_count = session.execute(
140 select(func.count())
141 .select_from(Message)
142 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id)
143 .outerjoin(
144 message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id)
145 )
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(
157 message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id)
158 )
159 .where_users_column_visible(context, HostRequest.surfer_user_id)
160 .where(HostRequest.host_user_id == context.user_id)
161 .where(message_2.id == None)
162 .where(HostRequest.host_last_seen_message_id < Message.id)
163 ).scalar_one()
165 unseen_message_count = session.execute(
166 select(func.count())
167 .select_from(Message)
168 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id)
169 .where(GroupChatSubscription.user_id == context.user_id)
170 .where(Message.time >= GroupChatSubscription.joined)
171 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None))
172 .where(Message.id > GroupChatSubscription.last_seen_message_id)
173 ).scalar_one()
175 pending_friend_request_count = session.execute(
176 select(func.count())
177 .select_from(FriendRelationship)
178 .where(FriendRelationship.to_user_id == context.user_id)
179 .where_users_column_visible(context, FriendRelationship.from_user_id)
180 .where(FriendRelationship.status == FriendStatus.pending)
181 ).scalar_one()
183 return api_pb2.PingRes(
184 user=user_model_to_pb(user, session, context),
185 unseen_message_count=unseen_message_count,
186 unseen_sent_host_request_count=unseen_sent_host_request_count,
187 unseen_received_host_request_count=unseen_received_host_request_count,
188 pending_friend_request_count=pending_friend_request_count,
189 )
191 def GetUser(self, request, context):
192 with session_scope() as session:
193 user = session.execute(
194 select(User).where_users_visible(context).where_username_or_id(request.user)
195 ).scalar_one_or_none()
197 if not user:
198 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
200 return user_model_to_pb(user, session, context)
202 def UpdateProfile(self, request, context):
203 with session_scope() as session:
204 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
206 if request.HasField("name"):
207 if not is_valid_name(request.name.value):
208 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME)
209 user.name = request.name.value
211 if request.HasField("city"):
212 user.city = request.city.value
214 if request.HasField("hometown"):
215 if request.hometown.is_null:
216 user.hometown = None
217 else:
218 user.hometown = request.hometown.value
220 if request.HasField("lat") and request.HasField("lng"):
221 if request.lat.value == 0 and request.lng.value == 0:
222 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE)
223 user.geom = create_coordinate(request.lat.value, request.lng.value)
225 if request.HasField("radius"):
226 user.geom_radius = request.radius.value
228 if request.HasField("avatar_key"):
229 if request.avatar_key.is_null:
230 user.avatar_key = None
231 else:
232 user.avatar_key = request.avatar_key.value
234 # if request.HasField("gender"):
235 # user.gender = request.gender.value
237 if request.HasField("pronouns"):
238 if request.pronouns.is_null:
239 user.pronouns = None
240 else:
241 user.pronouns = request.pronouns.value
243 if request.HasField("occupation"):
244 if request.occupation.is_null:
245 user.occupation = None
246 else:
247 user.occupation = request.occupation.value
249 if request.HasField("education"):
250 if request.education.is_null:
251 user.education = None
252 else:
253 user.education = request.education.value
255 if request.HasField("about_me"):
256 if request.about_me.is_null:
257 user.about_me = None
258 else:
259 user.about_me = request.about_me.value
261 if request.HasField("my_travels"):
262 if request.my_travels.is_null:
263 user.my_travels = None
264 else:
265 user.my_travels = request.my_travels.value
267 if request.HasField("things_i_like"):
268 if request.things_i_like.is_null:
269 user.things_i_like = None
270 else:
271 user.things_i_like = request.things_i_like.value
273 if request.HasField("about_place"):
274 if request.about_place.is_null:
275 user.about_place = None
276 else:
277 user.about_place = request.about_place.value
279 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED:
280 user.hosting_status = hostingstatus2sql[request.hosting_status]
282 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED:
283 user.meetup_status = meetupstatus2sql[request.meetup_status]
285 if request.HasField("language_abilities"):
286 # delete all existing abilities
287 for ability in user.language_abilities:
288 session.delete(ability)
289 session.flush()
291 # add the new ones
292 for language_ability in request.language_abilities.value:
293 if not language_is_allowed(language_ability.code):
294 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE)
295 session.add(
296 LanguageAbility(
297 user=user,
298 language_code=language_ability.code,
299 fluency=fluency2sql[language_ability.fluency],
300 )
301 )
303 if request.HasField("regions_visited"):
304 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id))
306 for region in request.regions_visited.value:
307 if not region_is_allowed(region):
308 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
309 session.add(
310 RegionVisited(
311 user_id=user.id,
312 region_code=region,
313 )
314 )
316 if request.HasField("regions_lived"):
317 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id))
319 for region in request.regions_lived.value:
320 if not region_is_allowed(region):
321 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION)
322 session.add(
323 RegionLived(
324 user_id=user.id,
325 region_code=region,
326 )
327 )
329 if request.HasField("additional_information"):
330 if request.additional_information.is_null:
331 user.additional_information = None
332 else:
333 user.additional_information = request.additional_information.value
335 if request.HasField("max_guests"):
336 if request.max_guests.is_null:
337 user.max_guests = None
338 else:
339 user.max_guests = request.max_guests.value
341 if request.HasField("last_minute"):
342 if request.last_minute.is_null:
343 user.last_minute = None
344 else:
345 user.last_minute = request.last_minute.value
347 if request.HasField("has_pets"):
348 if request.has_pets.is_null:
349 user.has_pets = None
350 else:
351 user.has_pets = request.has_pets.value
353 if request.HasField("accepts_pets"):
354 if request.accepts_pets.is_null:
355 user.accepts_pets = None
356 else:
357 user.accepts_pets = request.accepts_pets.value
359 if request.HasField("pet_details"):
360 if request.pet_details.is_null:
361 user.pet_details = None
362 else:
363 user.pet_details = request.pet_details.value
365 if request.HasField("has_kids"):
366 if request.has_kids.is_null:
367 user.has_kids = None
368 else:
369 user.has_kids = request.has_kids.value
371 if request.HasField("accepts_kids"):
372 if request.accepts_kids.is_null:
373 user.accepts_kids = None
374 else:
375 user.accepts_kids = request.accepts_kids.value
377 if request.HasField("kid_details"):
378 if request.kid_details.is_null:
379 user.kid_details = None
380 else:
381 user.kid_details = request.kid_details.value
383 if request.HasField("has_housemates"):
384 if request.has_housemates.is_null:
385 user.has_housemates = None
386 else:
387 user.has_housemates = request.has_housemates.value
389 if request.HasField("housemate_details"):
390 if request.housemate_details.is_null:
391 user.housemate_details = None
392 else:
393 user.housemate_details = request.housemate_details.value
395 if request.HasField("wheelchair_accessible"):
396 if request.wheelchair_accessible.is_null:
397 user.wheelchair_accessible = None
398 else:
399 user.wheelchair_accessible = request.wheelchair_accessible.value
401 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED:
402 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed]
404 if request.HasField("smokes_at_home"):
405 if request.smokes_at_home.is_null:
406 user.smokes_at_home = None
407 else:
408 user.smokes_at_home = request.smokes_at_home.value
410 if request.HasField("drinking_allowed"):
411 if request.drinking_allowed.is_null:
412 user.drinking_allowed = None
413 else:
414 user.drinking_allowed = request.drinking_allowed.value
416 if request.HasField("drinks_at_home"):
417 if request.drinks_at_home.is_null:
418 user.drinks_at_home = None
419 else:
420 user.drinks_at_home = request.drinks_at_home.value
422 if request.HasField("other_host_info"):
423 if request.other_host_info.is_null:
424 user.other_host_info = None
425 else:
426 user.other_host_info = request.other_host_info.value
428 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED:
429 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement]
431 if request.HasField("sleeping_details"):
432 if request.sleeping_details.is_null:
433 user.sleeping_details = None
434 else:
435 user.sleeping_details = request.sleeping_details.value
437 if request.HasField("area"):
438 if request.area.is_null:
439 user.area = None
440 else:
441 user.area = request.area.value
443 if request.HasField("house_rules"):
444 if request.house_rules.is_null:
445 user.house_rules = None
446 else:
447 user.house_rules = request.house_rules.value
449 if request.HasField("parking"):
450 if request.parking.is_null:
451 user.parking = None
452 else:
453 user.parking = request.parking.value
455 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED:
456 user.parking_details = parkingdetails2sql[request.parking_details]
458 if request.HasField("camping_ok"):
459 if request.camping_ok.is_null:
460 user.camping_ok = None
461 else:
462 user.camping_ok = request.camping_ok.value
464 # save updates
465 session.commit()
467 return empty_pb2.Empty()
469 def ListFriends(self, request, context):
470 with session_scope() as session:
471 rels = (
472 session.execute(
473 select(FriendRelationship)
474 .where_users_column_visible(context, FriendRelationship.from_user_id)
475 .where_users_column_visible(context, FriendRelationship.to_user_id)
476 .where(
477 or_(
478 FriendRelationship.from_user_id == context.user_id,
479 FriendRelationship.to_user_id == context.user_id,
480 )
481 )
482 .where(FriendRelationship.status == FriendStatus.accepted)
483 )
484 .scalars()
485 .all()
486 )
487 return api_pb2.ListFriendsRes(
488 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels],
489 )
491 def ListMutualFriends(self, request, context):
492 if context.user_id == request.user_id:
493 return api_pb2.ListMutualFriendsRes(mutual_friends=[])
495 with session_scope() as session:
496 user = session.execute(
497 select(User).where_users_visible(context).where(User.id == request.user_id)
498 ).scalar_one_or_none()
500 if not user:
501 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
503 q1 = (
504 select(FriendRelationship.from_user_id.label("user_id"))
505 .where(FriendRelationship.to_user_id == context.user_id)
506 .where(FriendRelationship.from_user_id != request.user_id)
507 .where(FriendRelationship.status == FriendStatus.accepted)
508 )
510 q2 = (
511 select(FriendRelationship.to_user_id.label("user_id"))
512 .where(FriendRelationship.from_user_id == context.user_id)
513 .where(FriendRelationship.to_user_id != request.user_id)
514 .where(FriendRelationship.status == FriendStatus.accepted)
515 )
517 q3 = (
518 select(FriendRelationship.from_user_id.label("user_id"))
519 .where(FriendRelationship.to_user_id == request.user_id)
520 .where(FriendRelationship.from_user_id != context.user_id)
521 .where(FriendRelationship.status == FriendStatus.accepted)
522 )
524 q4 = (
525 select(FriendRelationship.to_user_id.label("user_id"))
526 .where(FriendRelationship.from_user_id == request.user_id)
527 .where(FriendRelationship.to_user_id != context.user_id)
528 .where(FriendRelationship.status == FriendStatus.accepted)
529 )
531 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery())
533 mutual_friends = (
534 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all()
535 )
537 return api_pb2.ListMutualFriendsRes(
538 mutual_friends=[
539 api_pb2.MutualFriend(
540 user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name
541 )
542 for mutual_friend in mutual_friends
543 ]
544 )
546 def SendFriendRequest(self, request, context):
547 if context.user_id == request.user_id:
548 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF)
550 with session_scope() as session:
551 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
552 to_user = session.execute(
553 select(User).where_users_visible(context).where(User.id == request.user_id)
554 ).scalar_one_or_none()
556 if not to_user:
557 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
559 if (
560 session.execute(
561 select(FriendRelationship)
562 .where(
563 or_(
564 and_(
565 FriendRelationship.from_user_id == context.user_id,
566 FriendRelationship.to_user_id == request.user_id,
567 ),
568 and_(
569 FriendRelationship.from_user_id == request.user_id,
570 FriendRelationship.to_user_id == context.user_id,
571 ),
572 )
573 )
574 .where(
575 or_(
576 FriendRelationship.status == FriendStatus.accepted,
577 FriendRelationship.status == FriendStatus.pending,
578 )
579 )
580 ).scalar_one_or_none()
581 is not None
582 ):
583 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING)
585 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table
587 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending)
588 session.add(friend_relationship)
589 session.commit()
591 send_friend_request_email(friend_relationship)
593 notify(
594 user_id=friend_relationship.to_user_id,
595 topic="friend_request",
596 key=str(friend_relationship.from_user_id),
597 action="send",
598 avatar_key=user.avatar.thumbnail_url if user.avatar else None,
599 icon="person",
600 title=f"**{user.name}** sent you a friend request",
601 link=urls.friend_requests_link(),
602 )
604 return empty_pb2.Empty()
606 def ListFriendRequests(self, request, context):
607 # both sent and received
608 with session_scope() as session:
609 sent_requests = (
610 session.execute(
611 select(FriendRelationship)
612 .where_users_column_visible(context, FriendRelationship.to_user_id)
613 .where(FriendRelationship.from_user_id == context.user_id)
614 .where(FriendRelationship.status == FriendStatus.pending)
615 )
616 .scalars()
617 .all()
618 )
620 received_requests = (
621 session.execute(
622 select(FriendRelationship)
623 .where_users_column_visible(context, FriendRelationship.from_user_id)
624 .where(FriendRelationship.to_user_id == context.user_id)
625 .where(FriendRelationship.status == FriendStatus.pending)
626 )
627 .scalars()
628 .all()
629 )
631 return api_pb2.ListFriendRequestsRes(
632 sent=[
633 api_pb2.FriendRequest(
634 friend_request_id=friend_request.id,
635 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
636 user_id=friend_request.to_user.id,
637 sent=True,
638 )
639 for friend_request in sent_requests
640 ],
641 received=[
642 api_pb2.FriendRequest(
643 friend_request_id=friend_request.id,
644 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
645 user_id=friend_request.from_user.id,
646 sent=False,
647 )
648 for friend_request in received_requests
649 ],
650 )
652 def RespondFriendRequest(self, request, context):
653 with session_scope() as session:
654 friend_request = session.execute(
655 select(FriendRelationship)
656 .where_users_column_visible(context, FriendRelationship.from_user_id)
657 .where(FriendRelationship.to_user_id == context.user_id)
658 .where(FriendRelationship.status == FriendStatus.pending)
659 .where(FriendRelationship.id == request.friend_request_id)
660 ).scalar_one_or_none()
662 if not friend_request:
663 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
665 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected
666 friend_request.time_responded = func.now()
668 if friend_request.status == FriendStatus.accepted:
669 send_friend_request_accepted_email(friend_request)
671 session.commit()
673 if friend_request.status == FriendStatus.accepted:
674 notify(
675 user_id=friend_request.from_user_id,
676 topic="friend_request",
677 key=str(friend_request.to_user_id),
678 action="accept",
679 avatar_key=friend_request.to_user.avatar.thumbnail_url if friend_request.to_user.avatar else None,
680 icon="person",
681 title=f"**{friend_request.from_user.name}** accepted your friend request",
682 link=urls.user_link(username=friend_request.to_user.username),
683 )
685 return empty_pb2.Empty()
687 def CancelFriendRequest(self, request, context):
688 with session_scope() as session:
689 friend_request = session.execute(
690 select(FriendRelationship)
691 .where_users_column_visible(context, FriendRelationship.to_user_id)
692 .where(FriendRelationship.from_user_id == context.user_id)
693 .where(FriendRelationship.status == FriendStatus.pending)
694 .where(FriendRelationship.id == request.friend_request_id)
695 ).scalar_one_or_none()
697 if not friend_request:
698 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND)
700 friend_request.status = FriendStatus.cancelled
701 friend_request.time_responded = func.now()
703 # note no notifications
705 session.commit()
707 return empty_pb2.Empty()
709 def InitiateMediaUpload(self, request, context):
710 key = random_hex()
712 created = now()
713 expiry = created + timedelta(minutes=20)
715 with session_scope() as session:
716 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id)
717 session.add(upload)
718 session.commit()
720 req = media_pb2.UploadRequest(
721 key=upload.key,
722 type=media_pb2.UploadRequest.UploadType.IMAGE,
723 created=Timestamp_from_datetime(upload.created),
724 expiry=Timestamp_from_datetime(upload.expiry),
725 max_width=2000,
726 max_height=1600,
727 ).SerializeToString()
729 data = b64encode(req)
730 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"]))
732 path = "upload?" + urlencode({"data": data, "sig": sig})
734 return api_pb2.InitiateMediaUploadRes(
735 upload_url=urls.media_upload_url(path=path),
736 expiry=Timestamp_from_datetime(expiry),
737 )
740def user_model_to_pb(db_user, session, context):
741 num_references = session.execute(
742 select(func.count())
743 .select_from(Reference)
744 .join(User, User.id == Reference.from_user_id)
745 .where(User.is_visible)
746 .where(Reference.to_user_id == db_user.id)
747 ).scalar_one()
749 # returns (lat, lng)
750 # we put people without coords on null island
751 # https://en.wikipedia.org/wiki/Null_Island
752 lat, lng = db_user.coordinates or (0, 0)
754 pending_friend_request = None
755 if db_user.id == context.user_id:
756 friends_status = api_pb2.User.FriendshipStatus.NA
757 else:
758 friend_relationship = session.execute(
759 select(FriendRelationship)
760 .where(
761 or_(
762 and_(
763 FriendRelationship.from_user_id == context.user_id,
764 FriendRelationship.to_user_id == db_user.id,
765 ),
766 and_(
767 FriendRelationship.from_user_id == db_user.id,
768 FriendRelationship.to_user_id == context.user_id,
769 ),
770 )
771 )
772 .where(
773 or_(
774 FriendRelationship.status == FriendStatus.accepted,
775 FriendRelationship.status == FriendStatus.pending,
776 )
777 )
778 ).scalar_one_or_none()
780 if friend_relationship:
781 if friend_relationship.status == FriendStatus.accepted:
782 friends_status = api_pb2.User.FriendshipStatus.FRIENDS
783 else:
784 friends_status = api_pb2.User.FriendshipStatus.PENDING
785 if friend_relationship.from_user_id == context.user_id:
786 # we sent it
787 pending_friend_request = api_pb2.FriendRequest(
788 friend_request_id=friend_relationship.id,
789 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
790 user_id=friend_relationship.to_user.id,
791 sent=True,
792 )
793 else:
794 # we received it
795 pending_friend_request = api_pb2.FriendRequest(
796 friend_request_id=friend_relationship.id,
797 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING,
798 user_id=friend_relationship.from_user.id,
799 sent=False,
800 )
801 else:
802 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS
804 verification_score = 0.0
805 if db_user.phone_verification_verified:
806 verification_score += 1.0 * db_user.phone_is_verified
808 user = api_pb2.User(
809 user_id=db_user.id,
810 username=db_user.username,
811 name=db_user.name,
812 city=db_user.city,
813 hometown=db_user.hometown,
814 timezone=db_user.timezone,
815 lat=lat,
816 lng=lng,
817 radius=db_user.geom_radius,
818 verification=verification_score,
819 community_standing=db_user.community_standing,
820 num_references=num_references,
821 gender=db_user.gender,
822 pronouns=db_user.pronouns,
823 age=db_user.age,
824 joined=Timestamp_from_datetime(db_user.display_joined),
825 last_active=Timestamp_from_datetime(db_user.display_last_active),
826 hosting_status=hostingstatus2api[db_user.hosting_status],
827 meetup_status=meetupstatus2api[db_user.meetup_status],
828 occupation=db_user.occupation,
829 education=db_user.education,
830 about_me=db_user.about_me,
831 my_travels=db_user.my_travels,
832 things_i_like=db_user.things_i_like,
833 about_place=db_user.about_place,
834 language_abilities=[
835 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
836 for ability in db_user.language_abilities
837 ],
838 regions_visited=[region.code for region in db_user.regions_visited],
839 regions_lived=[region.code for region in db_user.regions_lived],
840 additional_information=db_user.additional_information,
841 friends=friends_status,
842 pending_friend_request=pending_friend_request,
843 smoking_allowed=smokinglocation2api[db_user.smoking_allowed],
844 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement],
845 parking_details=parkingdetails2api[db_user.parking_details],
846 avatar_url=db_user.avatar.full_url if db_user.avatar else None,
847 )
849 if db_user.max_guests is not None:
850 user.max_guests.value = db_user.max_guests
852 if db_user.last_minute is not None:
853 user.last_minute.value = db_user.last_minute
855 if db_user.has_pets is not None:
856 user.has_pets.value = db_user.has_pets
858 if db_user.accepts_pets is not None:
859 user.accepts_pets.value = db_user.accepts_pets
861 if db_user.pet_details is not None:
862 user.pet_details.value = db_user.pet_details
864 if db_user.has_kids is not None:
865 user.has_kids.value = db_user.has_kids
867 if db_user.accepts_kids is not None:
868 user.accepts_kids.value = db_user.accepts_kids
870 if db_user.kid_details is not None:
871 user.kid_details.value = db_user.kid_details
873 if db_user.has_housemates is not None:
874 user.has_housemates.value = db_user.has_housemates
876 if db_user.housemate_details is not None:
877 user.housemate_details.value = db_user.housemate_details
879 if db_user.wheelchair_accessible is not None:
880 user.wheelchair_accessible.value = db_user.wheelchair_accessible
882 if db_user.smokes_at_home is not None:
883 user.smokes_at_home.value = db_user.smokes_at_home
885 if db_user.drinking_allowed is not None:
886 user.drinking_allowed.value = db_user.drinking_allowed
888 if db_user.drinks_at_home is not None:
889 user.drinks_at_home.value = db_user.drinks_at_home
891 if db_user.other_host_info is not None:
892 user.other_host_info.value = db_user.other_host_info
894 if db_user.sleeping_details is not None:
895 user.sleeping_details.value = db_user.sleeping_details
897 if db_user.area is not None:
898 user.area.value = db_user.area
900 if db_user.house_rules is not None:
901 user.house_rules.value = db_user.house_rules
903 if db_user.parking is not None:
904 user.parking.value = db_user.parking
906 if db_user.camping_ok is not None:
907 user.camping_ok.value = db_user.camping_ok
909 return user