Coverage for src/couchers/servicers/api.py: 97%

397 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-08-28 14:55 +0000

1from datetime import timedelta 

2from urllib.parse import urlencode 

3 

4import grpc 

5from google.protobuf import empty_pb2 

6from sqlalchemy.sql import and_, delete, distinct, func, intersect, or_, union 

7 

8from couchers import errors, urls 

9from couchers.config import config 

10from couchers.crypto import b64encode, generate_hash_signature, random_hex 

11from couchers.helpers.strong_verification import get_strong_verification_fields 

12from couchers.materialized_views import LiteUser, UserResponseRate 

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 Notification, 

25 NotificationDeliveryType, 

26 ParkingDetails, 

27 RateLimitAction, 

28 Reference, 

29 RegionLived, 

30 RegionVisited, 

31 SleepingArrangement, 

32 SmokingLocation, 

33 User, 

34 UserBadge, 

35) 

36from couchers.notifications.notify import notify 

37from couchers.notifications.settings import get_topic_actions_by_delivery_type 

38from couchers.rate_limits.check import process_rate_limits_and_check_abort 

39from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

40from couchers.sql import couchers_select as select 

41from couchers.sql import is_valid_user_id, is_valid_username 

42from couchers.utils import ( 

43 Duration_from_timedelta, 

44 Timestamp_from_datetime, 

45 create_coordinate, 

46 get_coordinates, 

47 is_valid_name, 

48 now, 

49) 

50from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2 

51 

52MAX_USERS_PER_QUERY = 200 

53MAX_PAGINATION_LENGTH = 50 

54 

55hostingstatus2sql = { 

56 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

57 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

58 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

59 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

60} 

61 

62hostingstatus2api = { 

63 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

64 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

65 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

66 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

67} 

68 

69meetupstatus2sql = { 

70 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

71 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

72 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

73 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

74} 

75 

76meetupstatus2api = { 

77 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

78 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

79 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

80 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

81} 

82 

83smokinglocation2sql = { 

84 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

85 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

86 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

87 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

88 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

89} 

90 

91smokinglocation2api = { 

92 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

93 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

94 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

95 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

96 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

97} 

98 

99sleepingarrangement2sql = { 

100 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

101 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

102 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

103 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

104} 

105 

106sleepingarrangement2api = { 

107 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

108 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

109 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

110 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

111} 

112 

113parkingdetails2sql = { 

114 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

115 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

116 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

117 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

118 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

119} 

120 

121parkingdetails2api = { 

122 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

123 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

124 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

125 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

126 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

127} 

128 

129fluency2sql = { 

130 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

131 api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER: LanguageFluency.beginner, 

132 api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL: LanguageFluency.conversational, 

133 api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT: LanguageFluency.fluent, 

134} 

135 

136fluency2api = { 

137 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

138 LanguageFluency.beginner: api_pb2.LanguageAbility.Fluency.FLUENCY_BEGINNER, 

139 LanguageFluency.conversational: api_pb2.LanguageAbility.Fluency.FLUENCY_CONVERSATIONAL, 

140 LanguageFluency.fluent: api_pb2.LanguageAbility.Fluency.FLUENCY_FLUENT, 

141} 

142 

143 

144class API(api_pb2_grpc.APIServicer): 

145 def Ping(self, request, context, session): 

146 # auth ought to make sure the user exists 

147 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

148 

149 sent_reqs_last_seen_message_ids = ( 

150 select(HostRequest.conversation_id, HostRequest.surfer_last_seen_message_id) 

151 .where(HostRequest.surfer_user_id == context.user_id) 

152 .where_users_column_visible(context, HostRequest.host_user_id) 

153 ).subquery() 

154 

155 unseen_sent_host_request_count = session.execute( 

156 select(func.count(distinct(sent_reqs_last_seen_message_ids.c.conversation_id))) 

157 .join( 

158 Message, 

159 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id, 

160 ) 

161 .where(sent_reqs_last_seen_message_ids.c.surfer_last_seen_message_id < Message.id) 

162 .where(Message.id != None) 

163 ).scalar_one() 

164 

165 received_reqs_last_seen_message_ids = ( 

166 select(HostRequest.conversation_id, HostRequest.host_last_seen_message_id) 

167 .where(HostRequest.host_user_id == context.user_id) 

168 .where_users_column_visible(context, HostRequest.surfer_user_id) 

169 ).subquery() 

170 

171 unseen_received_host_request_count = session.execute( 

172 select(func.count(distinct(received_reqs_last_seen_message_ids.c.conversation_id))) 

173 .join( 

174 Message, 

175 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id, 

176 ) 

177 .where(received_reqs_last_seen_message_ids.c.host_last_seen_message_id < Message.id) 

178 .where(Message.id != None) 

179 ).scalar_one() 

180 

181 unseen_message_count = session.execute( 

182 select(func.count(Message.id)) 

183 .outerjoin(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id) 

184 .where(GroupChatSubscription.user_id == context.user_id) 

185 .where(Message.time >= GroupChatSubscription.joined) 

186 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

187 .where(Message.id > GroupChatSubscription.last_seen_message_id) 

188 ).scalar_one() 

189 

190 pending_friend_request_count = session.execute( 

191 select(func.count(FriendRelationship.id)) 

192 .where(FriendRelationship.to_user_id == context.user_id) 

193 .where_users_column_visible(context, FriendRelationship.from_user_id) 

194 .where(FriendRelationship.status == FriendStatus.pending) 

195 ).scalar_one() 

196 

197 unseen_notification_count = session.execute( 

198 select(func.count()) 

199 .select_from(Notification) 

200 .where(Notification.user_id == context.user_id) 

201 .where(Notification.is_seen == False) 

202 .where( 

203 Notification.topic_action.in_( 

204 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push) 

205 ) 

206 ) 

207 ).scalar_one() 

208 

209 return api_pb2.PingRes( 

210 user=user_model_to_pb(user, session, context), 

211 unseen_message_count=unseen_message_count, 

212 unseen_sent_host_request_count=unseen_sent_host_request_count, 

213 unseen_received_host_request_count=unseen_received_host_request_count, 

214 pending_friend_request_count=pending_friend_request_count, 

215 unseen_notification_count=unseen_notification_count, 

216 ) 

217 

218 def GetUser(self, request, context, session): 

219 user = session.execute( 

220 select(User).where_users_visible(context).where_username_or_id(request.user) 

221 ).scalar_one_or_none() 

222 

223 if not user: 

224 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

225 

226 return user_model_to_pb(user, session, context) 

227 

228 def GetLiteUser(self, request, context, session): 

229 lite_user = session.execute( 

230 select(LiteUser) 

231 .where_users_visible(context, table=LiteUser) 

232 .where_username_or_id(request.user, table=LiteUser) 

233 ).scalar_one_or_none() 

234 

235 if not lite_user: 

236 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

237 

238 return lite_user_to_pb(lite_user) 

239 

240 def GetLiteUsers(self, request, context, session): 

241 if len(request.users) > MAX_USERS_PER_QUERY: 

242 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REQUESTED_TOO_MANY_USERS) 

243 

244 usernames = {u for u in request.users if is_valid_username(u)} 

245 ids = {u for u in request.users if is_valid_user_id(u)} 

246 

247 users = ( 

248 session.execute( 

249 select(LiteUser) 

250 .where_users_visible(context, table=LiteUser) 

251 .where(or_(LiteUser.username.in_(usernames), LiteUser.id.in_(ids))) 

252 ) 

253 .scalars() 

254 .all() 

255 ) 

256 

257 users_by_id = {str(user.id): user for user in users} 

258 users_by_username = {user.username: user for user in users} 

259 

260 res = api_pb2.GetLiteUsersRes() 

261 

262 for user in request.users: 

263 lite_user = None 

264 if user in users_by_id: 

265 lite_user = users_by_id[user] 

266 elif user in users_by_username: 

267 lite_user = users_by_username[user] 

268 

269 res.responses.append( 

270 api_pb2.LiteUserRes( 

271 query=user, 

272 not_found=lite_user is None, 

273 user=lite_user_to_pb(lite_user) if lite_user else None, 

274 ) 

275 ) 

276 

277 return res 

278 

279 def UpdateProfile(self, request, context, session): 

280 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

281 

282 if request.HasField("name"): 

283 if not is_valid_name(request.name.value): 

284 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME) 

285 user.name = request.name.value 

286 

287 if request.HasField("city"): 

288 user.city = request.city.value 

289 

290 if request.HasField("hometown"): 

291 if request.hometown.is_null: 

292 user.hometown = None 

293 else: 

294 user.hometown = request.hometown.value 

295 

296 if request.HasField("lat") and request.HasField("lng"): 

297 if request.lat.value == 0 and request.lng.value == 0: 

298 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) 

299 user.geom = create_coordinate(request.lat.value, request.lng.value) 

300 user.randomized_geom = None 

301 

302 if request.HasField("radius"): 

303 user.geom_radius = request.radius.value 

304 

305 if request.HasField("avatar_key"): 

306 if request.avatar_key.is_null: 

307 user.avatar_key = None 

308 else: 

309 user.avatar_key = request.avatar_key.value 

310 

311 # if request.HasField("gender"): 

312 # user.gender = request.gender.value 

313 

314 if request.HasField("pronouns"): 

315 if request.pronouns.is_null: 

316 user.pronouns = None 

317 else: 

318 user.pronouns = request.pronouns.value 

319 

320 if request.HasField("occupation"): 

321 if request.occupation.is_null: 

322 user.occupation = None 

323 else: 

324 user.occupation = request.occupation.value 

325 

326 if request.HasField("education"): 

327 if request.education.is_null: 

328 user.education = None 

329 else: 

330 user.education = request.education.value 

331 

332 if request.HasField("about_me"): 

333 if request.about_me.is_null: 

334 user.about_me = None 

335 else: 

336 user.about_me = request.about_me.value 

337 

338 if request.HasField("things_i_like"): 

339 if request.things_i_like.is_null: 

340 user.things_i_like = None 

341 else: 

342 user.things_i_like = request.things_i_like.value 

343 

344 if request.HasField("about_place"): 

345 if request.about_place.is_null: 

346 user.about_place = None 

347 else: 

348 user.about_place = request.about_place.value 

349 

350 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

351 if user.do_not_email and request.hosting_status != api_pb2.HOSTING_STATUS_CANT_HOST: 

352 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_HOST) 

353 user.hosting_status = hostingstatus2sql[request.hosting_status] 

354 

355 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

356 if user.do_not_email and request.meetup_status != api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: 

357 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.DO_NOT_EMAIL_CANNOT_MEET) 

358 user.meetup_status = meetupstatus2sql[request.meetup_status] 

359 

360 if request.HasField("language_abilities"): 

361 # delete all existing abilities 

362 for ability in user.language_abilities: 

363 session.delete(ability) 

364 session.flush() 

365 

366 # add the new ones 

367 for language_ability in request.language_abilities.value: 

368 if not language_is_allowed(language_ability.code): 

369 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_LANGUAGE) 

370 session.add( 

371 LanguageAbility( 

372 user=user, 

373 language_code=language_ability.code, 

374 fluency=fluency2sql[language_ability.fluency], 

375 ) 

376 ) 

377 

378 if request.HasField("regions_visited"): 

379 session.execute(delete(RegionVisited).where(RegionVisited.user_id == context.user_id)) 

380 

381 for region in request.regions_visited.value: 

382 if not region_is_allowed(region): 

383 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION) 

384 session.add( 

385 RegionVisited( 

386 user_id=user.id, 

387 region_code=region, 

388 ) 

389 ) 

390 

391 if request.HasField("regions_lived"): 

392 session.execute(delete(RegionLived).where(RegionLived.user_id == context.user_id)) 

393 

394 for region in request.regions_lived.value: 

395 if not region_is_allowed(region): 

396 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_REGION) 

397 session.add( 

398 RegionLived( 

399 user_id=user.id, 

400 region_code=region, 

401 ) 

402 ) 

403 

404 if request.HasField("additional_information"): 

405 if request.additional_information.is_null: 

406 user.additional_information = None 

407 else: 

408 user.additional_information = request.additional_information.value 

409 

410 if request.HasField("max_guests"): 

411 if request.max_guests.is_null: 

412 user.max_guests = None 

413 else: 

414 user.max_guests = request.max_guests.value 

415 

416 if request.HasField("last_minute"): 

417 if request.last_minute.is_null: 

418 user.last_minute = None 

419 else: 

420 user.last_minute = request.last_minute.value 

421 

422 if request.HasField("has_pets"): 

423 if request.has_pets.is_null: 

424 user.has_pets = None 

425 else: 

426 user.has_pets = request.has_pets.value 

427 

428 if request.HasField("accepts_pets"): 

429 if request.accepts_pets.is_null: 

430 user.accepts_pets = None 

431 else: 

432 user.accepts_pets = request.accepts_pets.value 

433 

434 if request.HasField("pet_details"): 

435 if request.pet_details.is_null: 

436 user.pet_details = None 

437 else: 

438 user.pet_details = request.pet_details.value 

439 

440 if request.HasField("has_kids"): 

441 if request.has_kids.is_null: 

442 user.has_kids = None 

443 else: 

444 user.has_kids = request.has_kids.value 

445 

446 if request.HasField("accepts_kids"): 

447 if request.accepts_kids.is_null: 

448 user.accepts_kids = None 

449 else: 

450 user.accepts_kids = request.accepts_kids.value 

451 

452 if request.HasField("kid_details"): 

453 if request.kid_details.is_null: 

454 user.kid_details = None 

455 else: 

456 user.kid_details = request.kid_details.value 

457 

458 if request.HasField("has_housemates"): 

459 if request.has_housemates.is_null: 

460 user.has_housemates = None 

461 else: 

462 user.has_housemates = request.has_housemates.value 

463 

464 if request.HasField("housemate_details"): 

465 if request.housemate_details.is_null: 

466 user.housemate_details = None 

467 else: 

468 user.housemate_details = request.housemate_details.value 

469 

470 if request.HasField("wheelchair_accessible"): 

471 if request.wheelchair_accessible.is_null: 

472 user.wheelchair_accessible = None 

473 else: 

474 user.wheelchair_accessible = request.wheelchair_accessible.value 

475 

476 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

477 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

478 

479 if request.HasField("smokes_at_home"): 

480 if request.smokes_at_home.is_null: 

481 user.smokes_at_home = None 

482 else: 

483 user.smokes_at_home = request.smokes_at_home.value 

484 

485 if request.HasField("drinking_allowed"): 

486 if request.drinking_allowed.is_null: 

487 user.drinking_allowed = None 

488 else: 

489 user.drinking_allowed = request.drinking_allowed.value 

490 

491 if request.HasField("drinks_at_home"): 

492 if request.drinks_at_home.is_null: 

493 user.drinks_at_home = None 

494 else: 

495 user.drinks_at_home = request.drinks_at_home.value 

496 

497 if request.HasField("other_host_info"): 

498 if request.other_host_info.is_null: 

499 user.other_host_info = None 

500 else: 

501 user.other_host_info = request.other_host_info.value 

502 

503 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

504 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

505 

506 if request.HasField("sleeping_details"): 

507 if request.sleeping_details.is_null: 

508 user.sleeping_details = None 

509 else: 

510 user.sleeping_details = request.sleeping_details.value 

511 

512 if request.HasField("area"): 

513 if request.area.is_null: 

514 user.area = None 

515 else: 

516 user.area = request.area.value 

517 

518 if request.HasField("house_rules"): 

519 if request.house_rules.is_null: 

520 user.house_rules = None 

521 else: 

522 user.house_rules = request.house_rules.value 

523 

524 if request.HasField("parking"): 

525 if request.parking.is_null: 

526 user.parking = None 

527 else: 

528 user.parking = request.parking.value 

529 

530 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

531 user.parking_details = parkingdetails2sql[request.parking_details] 

532 

533 if request.HasField("camping_ok"): 

534 if request.camping_ok.is_null: 

535 user.camping_ok = None 

536 else: 

537 user.camping_ok = request.camping_ok.value 

538 

539 return empty_pb2.Empty() 

540 

541 def ListFriends(self, request, context, session): 

542 rels = ( 

543 session.execute( 

544 select(FriendRelationship) 

545 .where_users_column_visible(context, FriendRelationship.from_user_id) 

546 .where_users_column_visible(context, FriendRelationship.to_user_id) 

547 .where( 

548 or_( 

549 FriendRelationship.from_user_id == context.user_id, 

550 FriendRelationship.to_user_id == context.user_id, 

551 ) 

552 ) 

553 .where(FriendRelationship.status == FriendStatus.accepted) 

554 ) 

555 .scalars() 

556 .all() 

557 ) 

558 return api_pb2.ListFriendsRes( 

559 user_ids=[rel.from_user.id if rel.from_user.id != context.user_id else rel.to_user.id for rel in rels], 

560 ) 

561 

562 def RemoveFriend(self, request, context, session): 

563 rel = session.execute( 

564 select(FriendRelationship) 

565 .where_users_column_visible(context, FriendRelationship.from_user_id) 

566 .where_users_column_visible(context, FriendRelationship.to_user_id) 

567 .where( 

568 or_( 

569 and_( 

570 FriendRelationship.from_user_id == request.user_id, 

571 FriendRelationship.to_user_id == context.user_id, 

572 ), 

573 and_( 

574 FriendRelationship.from_user_id == context.user_id, 

575 FriendRelationship.to_user_id == request.user_id, 

576 ), 

577 ) 

578 ) 

579 .where(FriendRelationship.status == FriendStatus.accepted) 

580 ).scalar_one_or_none() 

581 

582 if not rel: 

583 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.NOT_FRIENDS) 

584 

585 session.delete(rel) 

586 

587 return empty_pb2.Empty() 

588 

589 def ListMutualFriends(self, request, context, session): 

590 if context.user_id == request.user_id: 

591 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

592 

593 user = session.execute( 

594 select(User).where_users_visible(context).where(User.id == request.user_id) 

595 ).scalar_one_or_none() 

596 

597 if not user: 

598 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

599 

600 q1 = ( 

601 select(FriendRelationship.from_user_id.label("user_id")) 

602 .where(FriendRelationship.to_user_id == context.user_id) 

603 .where(FriendRelationship.from_user_id != request.user_id) 

604 .where(FriendRelationship.status == FriendStatus.accepted) 

605 ) 

606 

607 q2 = ( 

608 select(FriendRelationship.to_user_id.label("user_id")) 

609 .where(FriendRelationship.from_user_id == context.user_id) 

610 .where(FriendRelationship.to_user_id != request.user_id) 

611 .where(FriendRelationship.status == FriendStatus.accepted) 

612 ) 

613 

614 q3 = ( 

615 select(FriendRelationship.from_user_id.label("user_id")) 

616 .where(FriendRelationship.to_user_id == request.user_id) 

617 .where(FriendRelationship.from_user_id != context.user_id) 

618 .where(FriendRelationship.status == FriendStatus.accepted) 

619 ) 

620 

621 q4 = ( 

622 select(FriendRelationship.to_user_id.label("user_id")) 

623 .where(FriendRelationship.from_user_id == request.user_id) 

624 .where(FriendRelationship.to_user_id != context.user_id) 

625 .where(FriendRelationship.status == FriendStatus.accepted) 

626 ) 

627 

628 mutual = select(intersect(union(q1, q2), union(q3, q4)).subquery()) 

629 

630 mutual_friends = ( 

631 session.execute(select(User).where_users_visible(context).where(User.id.in_(mutual))).scalars().all() 

632 ) 

633 

634 return api_pb2.ListMutualFriendsRes( 

635 mutual_friends=[ 

636 api_pb2.MutualFriend(user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name) 

637 for mutual_friend in mutual_friends 

638 ] 

639 ) 

640 

641 def SendFriendRequest(self, request, context, session): 

642 if context.user_id == request.user_id: 

643 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF) 

644 

645 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

646 to_user = session.execute( 

647 select(User).where_users_visible(context).where(User.id == request.user_id) 

648 ).scalar_one_or_none() 

649 

650 if not to_user: 

651 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

652 

653 if ( 

654 session.execute( 

655 select(FriendRelationship) 

656 .where( 

657 or_( 

658 and_( 

659 FriendRelationship.from_user_id == context.user_id, 

660 FriendRelationship.to_user_id == request.user_id, 

661 ), 

662 and_( 

663 FriendRelationship.from_user_id == request.user_id, 

664 FriendRelationship.to_user_id == context.user_id, 

665 ), 

666 ) 

667 ) 

668 .where( 

669 or_( 

670 FriendRelationship.status == FriendStatus.accepted, 

671 FriendRelationship.status == FriendStatus.pending, 

672 ) 

673 ) 

674 ).scalar_one_or_none() 

675 is not None 

676 ): 

677 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING) 

678 

679 # Check if user has been sending friend requests excessively 

680 if process_rate_limits_and_check_abort( 

681 session=session, user_id=context.user_id, action=RateLimitAction.friend_request 

682 ): 

683 context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, errors.FRIEND_REQUEST_RATE_LIMIT) 

684 

685 # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table 

686 

687 friend_relationship = FriendRelationship(from_user=user, to_user=to_user, status=FriendStatus.pending) 

688 session.add(friend_relationship) 

689 session.flush() 

690 

691 notify( 

692 session, 

693 user_id=friend_relationship.to_user_id, 

694 topic_action="friend_request:create", 

695 key=friend_relationship.from_user_id, 

696 data=notification_data_pb2.FriendRequestCreate( 

697 other_user=user_model_to_pb(friend_relationship.from_user, session, context), 

698 ), 

699 ) 

700 

701 return empty_pb2.Empty() 

702 

703 def ListFriendRequests(self, request, context, session): 

704 # both sent and received 

705 sent_requests = ( 

706 session.execute( 

707 select(FriendRelationship) 

708 .where_users_column_visible(context, FriendRelationship.to_user_id) 

709 .where(FriendRelationship.from_user_id == context.user_id) 

710 .where(FriendRelationship.status == FriendStatus.pending) 

711 ) 

712 .scalars() 

713 .all() 

714 ) 

715 

716 received_requests = ( 

717 session.execute( 

718 select(FriendRelationship) 

719 .where_users_column_visible(context, FriendRelationship.from_user_id) 

720 .where(FriendRelationship.to_user_id == context.user_id) 

721 .where(FriendRelationship.status == FriendStatus.pending) 

722 ) 

723 .scalars() 

724 .all() 

725 ) 

726 

727 return api_pb2.ListFriendRequestsRes( 

728 sent=[ 

729 api_pb2.FriendRequest( 

730 friend_request_id=friend_request.id, 

731 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

732 user_id=friend_request.to_user.id, 

733 sent=True, 

734 ) 

735 for friend_request in sent_requests 

736 ], 

737 received=[ 

738 api_pb2.FriendRequest( 

739 friend_request_id=friend_request.id, 

740 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

741 user_id=friend_request.from_user.id, 

742 sent=False, 

743 ) 

744 for friend_request in received_requests 

745 ], 

746 ) 

747 

748 def RespondFriendRequest(self, request, context, session): 

749 friend_request = session.execute( 

750 select(FriendRelationship) 

751 .where_users_column_visible(context, FriendRelationship.from_user_id) 

752 .where(FriendRelationship.to_user_id == context.user_id) 

753 .where(FriendRelationship.status == FriendStatus.pending) 

754 .where(FriendRelationship.id == request.friend_request_id) 

755 ).scalar_one_or_none() 

756 

757 if not friend_request: 

758 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND) 

759 

760 friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected 

761 friend_request.time_responded = func.now() 

762 

763 session.flush() 

764 

765 if friend_request.status == FriendStatus.accepted: 

766 notify( 

767 session, 

768 user_id=friend_request.from_user_id, 

769 topic_action="friend_request:accept", 

770 key=friend_request.to_user_id, 

771 data=notification_data_pb2.FriendRequestAccept( 

772 other_user=user_model_to_pb(friend_request.to_user, session, context), 

773 ), 

774 ) 

775 

776 return empty_pb2.Empty() 

777 

778 def CancelFriendRequest(self, request, context, session): 

779 friend_request = session.execute( 

780 select(FriendRelationship) 

781 .where_users_column_visible(context, FriendRelationship.to_user_id) 

782 .where(FriendRelationship.from_user_id == context.user_id) 

783 .where(FriendRelationship.status == FriendStatus.pending) 

784 .where(FriendRelationship.id == request.friend_request_id) 

785 ).scalar_one_or_none() 

786 

787 if not friend_request: 

788 context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND) 

789 

790 friend_request.status = FriendStatus.cancelled 

791 friend_request.time_responded = func.now() 

792 

793 # note no notifications 

794 

795 session.commit() 

796 

797 return empty_pb2.Empty() 

798 

799 def InitiateMediaUpload(self, request, context, session): 

800 key = random_hex() 

801 

802 created = now() 

803 expiry = created + timedelta(minutes=20) 

804 

805 upload = InitiatedUpload(key=key, created=created, expiry=expiry, initiator_user_id=context.user_id) 

806 session.add(upload) 

807 session.commit() 

808 

809 req = media_pb2.UploadRequest( 

810 key=upload.key, 

811 type=media_pb2.UploadRequest.UploadType.IMAGE, 

812 created=Timestamp_from_datetime(upload.created), 

813 expiry=Timestamp_from_datetime(upload.expiry), 

814 max_width=2000, 

815 max_height=1600, 

816 ).SerializeToString() 

817 

818 data = b64encode(req) 

819 sig = b64encode(generate_hash_signature(req, config["MEDIA_SERVER_SECRET_KEY"])) 

820 

821 path = "upload?" + urlencode({"data": data, "sig": sig}) 

822 

823 return api_pb2.InitiateMediaUploadRes( 

824 upload_url=urls.media_upload_url(path=path), 

825 expiry=Timestamp_from_datetime(expiry), 

826 ) 

827 

828 def ListBadgeUsers(self, request, context, session): 

829 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

830 next_user_id = int(request.page_token) if request.page_token else 0 

831 badge = get_badge_dict().get(request.badge_id) 

832 if not badge: 

833 context.abort(grpc.StatusCode.NOT_FOUND, errors.BADGE_NOT_FOUND) 

834 

835 badge_user_ids = ( 

836 session.execute( 

837 select(UserBadge.user_id) 

838 .where(UserBadge.badge_id == badge["id"]) 

839 .where(UserBadge.user_id >= next_user_id) 

840 .order_by(UserBadge.user_id) 

841 .limit(page_size + 1) 

842 ) 

843 .scalars() 

844 .all() 

845 ) 

846 

847 return api_pb2.ListBadgeUsersRes( 

848 user_ids=badge_user_ids[:page_size], 

849 next_page_token=str(badge_user_ids[-1]) if len(badge_user_ids) > page_size else None, 

850 ) 

851 

852 

853def response_rate_to_pb(response_rate: UserResponseRate): 

854 if not response_rate: 

855 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()} 

856 

857 # if n is None, the user is new or they have no requests 

858 if response_rate.requests < 3: 

859 return {"insufficient_data": requests_pb2.ResponseRateInsufficientData()} 

860 

861 if response_rate.response_rate <= 0.33: 

862 return {"low": requests_pb2.ResponseRateLow()} 

863 

864 response_time_p33_coarsened = Duration_from_timedelta( 

865 timedelta(seconds=round(response_rate.response_time_33p.total_seconds() / 60) * 60) 

866 ) 

867 

868 if response_rate.response_rate <= 0.66: 

869 return {"some": requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened)} 

870 

871 response_time_p66_coarsened = Duration_from_timedelta( 

872 timedelta(seconds=round(response_rate.response_time_66p.total_seconds() / 60) * 60) 

873 ) 

874 

875 if response_rate.response_rate <= 0.90: 

876 return { 

877 "most": requests_pb2.ResponseRateMost( 

878 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

879 ) 

880 } 

881 else: 

882 return { 

883 "almost_all": requests_pb2.ResponseRateAlmostAll( 

884 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

885 ) 

886 } 

887 

888 

889def get_num_references(session, user_ids): 

890 return dict( 

891 session.execute( 

892 select(Reference.to_user_id, func.count(Reference.id)) 

893 .where(Reference.to_user_id.in_(user_ids)) 

894 .where(Reference.is_deleted == False) 

895 .join(User, User.id == Reference.from_user_id) 

896 .where(User.is_visible) 

897 .group_by(Reference.to_user_id) 

898 ).all() 

899 ) 

900 

901 

902def user_model_to_pb(db_user, session, context): 

903 # note that this function should work also for banned/deleted users as it's called from Admin.GetUser 

904 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context 

905 num_references = get_num_references(session, [db_user.id]).get(db_user.id, 0) 

906 

907 # returns (lat, lng) 

908 # we put people without coords on null island 

909 # https://en.wikipedia.org/wiki/Null_Island 

910 lat, lng = db_user.coordinates or (0, 0) 

911 

912 pending_friend_request = None 

913 if db_user.id == context.user_id: 

914 friends_status = api_pb2.User.FriendshipStatus.NA 

915 else: 

916 friend_relationship = session.execute( 

917 select(FriendRelationship) 

918 .where( 

919 or_( 

920 and_( 

921 FriendRelationship.from_user_id == context.user_id, 

922 FriendRelationship.to_user_id == db_user.id, 

923 ), 

924 and_( 

925 FriendRelationship.from_user_id == db_user.id, 

926 FriendRelationship.to_user_id == context.user_id, 

927 ), 

928 ) 

929 ) 

930 .where( 

931 or_( 

932 FriendRelationship.status == FriendStatus.accepted, 

933 FriendRelationship.status == FriendStatus.pending, 

934 ) 

935 ) 

936 ).scalar_one_or_none() 

937 

938 if friend_relationship: 

939 if friend_relationship.status == FriendStatus.accepted: 

940 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

941 else: 

942 friends_status = api_pb2.User.FriendshipStatus.PENDING 

943 if friend_relationship.from_user_id == context.user_id: 

944 # we sent it 

945 pending_friend_request = api_pb2.FriendRequest( 

946 friend_request_id=friend_relationship.id, 

947 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

948 user_id=friend_relationship.to_user.id, 

949 sent=True, 

950 ) 

951 else: 

952 # we received it 

953 pending_friend_request = api_pb2.FriendRequest( 

954 friend_request_id=friend_relationship.id, 

955 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

956 user_id=friend_relationship.from_user.id, 

957 sent=False, 

958 ) 

959 else: 

960 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

961 

962 response_rate = session.execute( 

963 select(UserResponseRate).where(UserResponseRate.user_id == db_user.id) 

964 ).scalar_one_or_none() 

965 

966 verification_score = 0.0 

967 if db_user.phone_verification_verified: 

968 verification_score += 1.0 * db_user.phone_is_verified 

969 

970 user = api_pb2.User( 

971 user_id=db_user.id, 

972 username=db_user.username, 

973 name=db_user.name, 

974 city=db_user.city, 

975 hometown=db_user.hometown, 

976 timezone=db_user.timezone, 

977 lat=lat, 

978 lng=lng, 

979 radius=db_user.geom_radius, 

980 verification=verification_score, 

981 community_standing=db_user.community_standing, 

982 num_references=num_references, 

983 gender=db_user.gender, 

984 pronouns=db_user.pronouns, 

985 age=int(db_user.age), 

986 joined=Timestamp_from_datetime(db_user.display_joined), 

987 last_active=Timestamp_from_datetime(db_user.display_last_active), 

988 hosting_status=hostingstatus2api[db_user.hosting_status], 

989 meetup_status=meetupstatus2api[db_user.meetup_status], 

990 occupation=db_user.occupation, 

991 education=db_user.education, 

992 about_me=db_user.about_me, 

993 things_i_like=db_user.things_i_like, 

994 about_place=db_user.about_place, 

995 language_abilities=[ 

996 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency]) 

997 for ability in db_user.language_abilities 

998 ], 

999 regions_visited=[region.code for region in db_user.regions_visited], 

1000 regions_lived=[region.code for region in db_user.regions_lived], 

1001 additional_information=db_user.additional_information, 

1002 friends=friends_status, 

1003 pending_friend_request=pending_friend_request, 

1004 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1005 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1006 parking_details=parkingdetails2api[db_user.parking_details], 

1007 avatar_url=db_user.avatar.full_url if db_user.avatar else None, 

1008 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None, 

1009 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id)) 

1010 .scalars() 

1011 .all(), 

1012 **get_strong_verification_fields(session, db_user), 

1013 **response_rate_to_pb(response_rate), 

1014 ) 

1015 

1016 if db_user.max_guests is not None: 

1017 user.max_guests.value = db_user.max_guests 

1018 

1019 if db_user.last_minute is not None: 

1020 user.last_minute.value = db_user.last_minute 

1021 

1022 if db_user.has_pets is not None: 

1023 user.has_pets.value = db_user.has_pets 

1024 

1025 if db_user.accepts_pets is not None: 

1026 user.accepts_pets.value = db_user.accepts_pets 

1027 

1028 if db_user.pet_details is not None: 

1029 user.pet_details.value = db_user.pet_details 

1030 

1031 if db_user.has_kids is not None: 

1032 user.has_kids.value = db_user.has_kids 

1033 

1034 if db_user.accepts_kids is not None: 

1035 user.accepts_kids.value = db_user.accepts_kids 

1036 

1037 if db_user.kid_details is not None: 

1038 user.kid_details.value = db_user.kid_details 

1039 

1040 if db_user.has_housemates is not None: 

1041 user.has_housemates.value = db_user.has_housemates 

1042 

1043 if db_user.housemate_details is not None: 

1044 user.housemate_details.value = db_user.housemate_details 

1045 

1046 if db_user.wheelchair_accessible is not None: 

1047 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1048 

1049 if db_user.smokes_at_home is not None: 

1050 user.smokes_at_home.value = db_user.smokes_at_home 

1051 

1052 if db_user.drinking_allowed is not None: 

1053 user.drinking_allowed.value = db_user.drinking_allowed 

1054 

1055 if db_user.drinks_at_home is not None: 

1056 user.drinks_at_home.value = db_user.drinks_at_home 

1057 

1058 if db_user.other_host_info is not None: 

1059 user.other_host_info.value = db_user.other_host_info 

1060 

1061 if db_user.sleeping_details is not None: 

1062 user.sleeping_details.value = db_user.sleeping_details 

1063 

1064 if db_user.area is not None: 

1065 user.area.value = db_user.area 

1066 

1067 if db_user.house_rules is not None: 

1068 user.house_rules.value = db_user.house_rules 

1069 

1070 if db_user.parking is not None: 

1071 user.parking.value = db_user.parking 

1072 

1073 if db_user.camping_ok is not None: 

1074 user.camping_ok.value = db_user.camping_ok 

1075 

1076 return user 

1077 

1078 

1079def lite_user_to_pb(lite_user: LiteUser): 

1080 lat, lng = get_coordinates(lite_user.geom) or (0, 0) 

1081 

1082 return api_pb2.LiteUser( 

1083 user_id=lite_user.id, 

1084 username=lite_user.username, 

1085 name=lite_user.name, 

1086 city=lite_user.city, 

1087 age=int(lite_user.age), 

1088 avatar_url=urls.media_url(filename=lite_user.avatar_filename, size="full") 

1089 if lite_user.avatar_filename 

1090 else None, 

1091 avatar_thumbnail_url=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail") 

1092 if lite_user.avatar_filename 

1093 else None, 

1094 lat=lat, 

1095 lng=lng, 

1096 radius=lite_user.radius, 

1097 has_strong_verification=lite_user.has_strong_verification, 

1098 )