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

395 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-06-01 15:07 +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.materialized_views import lite_users, user_response_rates 

12from couchers.models import ( 

13 FriendRelationship, 

14 FriendStatus, 

15 GroupChatSubscription, 

16 HostingStatus, 

17 HostRequest, 

18 InitiatedUpload, 

19 LanguageAbility, 

20 LanguageFluency, 

21 MeetupStatus, 

22 Message, 

23 Notification, 

24 NotificationDeliveryType, 

25 ParkingDetails, 

26 Reference, 

27 RegionLived, 

28 RegionVisited, 

29 SleepingArrangement, 

30 SmokingLocation, 

31 User, 

32 UserBadge, 

33) 

34from couchers.notifications.notify import notify 

35from couchers.notifications.settings import get_topic_actions_by_delivery_type 

36from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

37from couchers.servicers.account import get_strong_verification_fields 

38from couchers.sql import couchers_select as select 

39from couchers.sql import is_valid_user_id, is_valid_username 

40from couchers.utils import ( 

41 Duration_from_timedelta, 

42 Timestamp_from_datetime, 

43 create_coordinate, 

44 get_coordinates, 

45 is_valid_name, 

46 now, 

47) 

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

49 

50MAX_USERS_PER_QUERY = 200 

51MAX_PAGINATION_LENGTH = 50 

52 

53hostingstatus2sql = { 

54 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

55 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

56 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

57 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

58} 

59 

60hostingstatus2api = { 

61 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

62 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

63 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

64 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

65} 

66 

67meetupstatus2sql = { 

68 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

69 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

70 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

71 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

72} 

73 

74meetupstatus2api = { 

75 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

76 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

77 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

78 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

79} 

80 

81smokinglocation2sql = { 

82 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

83 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

84 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

85 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

86 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

87} 

88 

89smokinglocation2api = { 

90 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

91 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

92 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

93 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

94 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

95} 

96 

97sleepingarrangement2sql = { 

98 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

99 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

100 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

101 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

102} 

103 

104sleepingarrangement2api = { 

105 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

106 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

107 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

108 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

109} 

110 

111parkingdetails2sql = { 

112 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

113 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

114 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

115 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

116 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

117} 

118 

119parkingdetails2api = { 

120 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

121 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

122 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

123 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

124 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

125} 

126 

127fluency2sql = { 

128 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

132} 

133 

134fluency2api = { 

135 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

139} 

140 

141 

142class API(api_pb2_grpc.APIServicer): 

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

144 # auth ought to make sure the user exists 

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

146 

147 sent_reqs_last_seen_message_ids = ( 

148 select(HostRequest.conversation_id, HostRequest.surfer_last_seen_message_id) 

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

150 .where_users_column_visible(context, HostRequest.host_user_id) 

151 ).subquery() 

152 

153 unseen_sent_host_request_count = session.execute( 

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

155 .join( 

156 Message, 

157 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id, 

158 ) 

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

160 .where(Message.id != None) 

161 ).scalar_one() 

162 

163 received_reqs_last_seen_message_ids = ( 

164 select(HostRequest.conversation_id, HostRequest.host_last_seen_message_id) 

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

166 .where_users_column_visible(context, HostRequest.surfer_user_id) 

167 ).subquery() 

168 

169 unseen_received_host_request_count = session.execute( 

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

171 .join( 

172 Message, 

173 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id, 

174 ) 

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

176 .where(Message.id != None) 

177 ).scalar_one() 

178 

179 unseen_message_count = session.execute( 

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

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

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

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

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

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

186 ).scalar_one() 

187 

188 pending_friend_request_count = session.execute( 

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

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

191 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

193 ).scalar_one() 

194 

195 unseen_notification_count = session.execute( 

196 select(func.count()) 

197 .select_from(Notification) 

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

199 .where(Notification.is_seen == False) 

200 .where( 

201 Notification.topic_action.in_( 

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

203 ) 

204 ) 

205 ).scalar_one() 

206 

207 return api_pb2.PingRes( 

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

209 unseen_message_count=unseen_message_count, 

210 unseen_sent_host_request_count=unseen_sent_host_request_count, 

211 unseen_received_host_request_count=unseen_received_host_request_count, 

212 pending_friend_request_count=pending_friend_request_count, 

213 unseen_notification_count=unseen_notification_count, 

214 ) 

215 

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

217 user = session.execute( 

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

219 ).scalar_one_or_none() 

220 

221 if not user: 

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

223 

224 return user_model_to_pb(user, session, context) 

225 

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

227 lite_user = session.execute( 

228 select(lite_users) 

229 .where_users_visible(context, table=lite_users.c) 

230 .where_username_or_id(request.user, table=lite_users.c) 

231 ).one_or_none() 

232 

233 if not lite_user: 

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

235 

236 return lite_user_to_pb(lite_user) 

237 

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

239 if len(request.users) > MAX_USERS_PER_QUERY: 

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

241 

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

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

244 

245 users = session.execute( 

246 select(lite_users) 

247 .where_users_visible(context, table=lite_users.c) 

248 .where(or_(lite_users.c.username.in_(usernames), lite_users.c.id.in_(ids))) 

249 ).all() 

250 

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

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

253 

254 res = api_pb2.GetLiteUsersRes() 

255 

256 for user in request.users: 

257 lite_user = None 

258 if user in users_by_id: 

259 lite_user = users_by_id[user] 

260 elif user in users_by_username: 

261 lite_user = users_by_username[user] 

262 

263 res.responses.append( 

264 api_pb2.LiteUserRes( 

265 query=user, 

266 not_found=lite_user is None, 

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

268 ) 

269 ) 

270 

271 return res 

272 

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

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

275 

276 if request.HasField("name"): 

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

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

279 user.name = request.name.value 

280 

281 if request.HasField("city"): 

282 user.city = request.city.value 

283 

284 if request.HasField("hometown"): 

285 if request.hometown.is_null: 

286 user.hometown = None 

287 else: 

288 user.hometown = request.hometown.value 

289 

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

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

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

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

294 user.randomized_geom = None 

295 

296 if request.HasField("radius"): 

297 user.geom_radius = request.radius.value 

298 

299 if request.HasField("avatar_key"): 

300 if request.avatar_key.is_null: 

301 user.avatar_key = None 

302 else: 

303 user.avatar_key = request.avatar_key.value 

304 

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

306 # user.gender = request.gender.value 

307 

308 if request.HasField("pronouns"): 

309 if request.pronouns.is_null: 

310 user.pronouns = None 

311 else: 

312 user.pronouns = request.pronouns.value 

313 

314 if request.HasField("occupation"): 

315 if request.occupation.is_null: 

316 user.occupation = None 

317 else: 

318 user.occupation = request.occupation.value 

319 

320 if request.HasField("education"): 

321 if request.education.is_null: 

322 user.education = None 

323 else: 

324 user.education = request.education.value 

325 

326 if request.HasField("about_me"): 

327 if request.about_me.is_null: 

328 user.about_me = None 

329 else: 

330 user.about_me = request.about_me.value 

331 

332 if request.HasField("things_i_like"): 

333 if request.things_i_like.is_null: 

334 user.things_i_like = None 

335 else: 

336 user.things_i_like = request.things_i_like.value 

337 

338 if request.HasField("about_place"): 

339 if request.about_place.is_null: 

340 user.about_place = None 

341 else: 

342 user.about_place = request.about_place.value 

343 

344 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

347 user.hosting_status = hostingstatus2sql[request.hosting_status] 

348 

349 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

352 user.meetup_status = meetupstatus2sql[request.meetup_status] 

353 

354 if request.HasField("language_abilities"): 

355 # delete all existing abilities 

356 for ability in user.language_abilities: 

357 session.delete(ability) 

358 session.flush() 

359 

360 # add the new ones 

361 for language_ability in request.language_abilities.value: 

362 if not language_is_allowed(language_ability.code): 

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

364 session.add( 

365 LanguageAbility( 

366 user=user, 

367 language_code=language_ability.code, 

368 fluency=fluency2sql[language_ability.fluency], 

369 ) 

370 ) 

371 

372 if request.HasField("regions_visited"): 

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

374 

375 for region in request.regions_visited.value: 

376 if not region_is_allowed(region): 

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

378 session.add( 

379 RegionVisited( 

380 user_id=user.id, 

381 region_code=region, 

382 ) 

383 ) 

384 

385 if request.HasField("regions_lived"): 

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

387 

388 for region in request.regions_lived.value: 

389 if not region_is_allowed(region): 

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

391 session.add( 

392 RegionLived( 

393 user_id=user.id, 

394 region_code=region, 

395 ) 

396 ) 

397 

398 if request.HasField("additional_information"): 

399 if request.additional_information.is_null: 

400 user.additional_information = None 

401 else: 

402 user.additional_information = request.additional_information.value 

403 

404 if request.HasField("max_guests"): 

405 if request.max_guests.is_null: 

406 user.max_guests = None 

407 else: 

408 user.max_guests = request.max_guests.value 

409 

410 if request.HasField("last_minute"): 

411 if request.last_minute.is_null: 

412 user.last_minute = None 

413 else: 

414 user.last_minute = request.last_minute.value 

415 

416 if request.HasField("has_pets"): 

417 if request.has_pets.is_null: 

418 user.has_pets = None 

419 else: 

420 user.has_pets = request.has_pets.value 

421 

422 if request.HasField("accepts_pets"): 

423 if request.accepts_pets.is_null: 

424 user.accepts_pets = None 

425 else: 

426 user.accepts_pets = request.accepts_pets.value 

427 

428 if request.HasField("pet_details"): 

429 if request.pet_details.is_null: 

430 user.pet_details = None 

431 else: 

432 user.pet_details = request.pet_details.value 

433 

434 if request.HasField("has_kids"): 

435 if request.has_kids.is_null: 

436 user.has_kids = None 

437 else: 

438 user.has_kids = request.has_kids.value 

439 

440 if request.HasField("accepts_kids"): 

441 if request.accepts_kids.is_null: 

442 user.accepts_kids = None 

443 else: 

444 user.accepts_kids = request.accepts_kids.value 

445 

446 if request.HasField("kid_details"): 

447 if request.kid_details.is_null: 

448 user.kid_details = None 

449 else: 

450 user.kid_details = request.kid_details.value 

451 

452 if request.HasField("has_housemates"): 

453 if request.has_housemates.is_null: 

454 user.has_housemates = None 

455 else: 

456 user.has_housemates = request.has_housemates.value 

457 

458 if request.HasField("housemate_details"): 

459 if request.housemate_details.is_null: 

460 user.housemate_details = None 

461 else: 

462 user.housemate_details = request.housemate_details.value 

463 

464 if request.HasField("wheelchair_accessible"): 

465 if request.wheelchair_accessible.is_null: 

466 user.wheelchair_accessible = None 

467 else: 

468 user.wheelchair_accessible = request.wheelchair_accessible.value 

469 

470 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

471 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

472 

473 if request.HasField("smokes_at_home"): 

474 if request.smokes_at_home.is_null: 

475 user.smokes_at_home = None 

476 else: 

477 user.smokes_at_home = request.smokes_at_home.value 

478 

479 if request.HasField("drinking_allowed"): 

480 if request.drinking_allowed.is_null: 

481 user.drinking_allowed = None 

482 else: 

483 user.drinking_allowed = request.drinking_allowed.value 

484 

485 if request.HasField("drinks_at_home"): 

486 if request.drinks_at_home.is_null: 

487 user.drinks_at_home = None 

488 else: 

489 user.drinks_at_home = request.drinks_at_home.value 

490 

491 if request.HasField("other_host_info"): 

492 if request.other_host_info.is_null: 

493 user.other_host_info = None 

494 else: 

495 user.other_host_info = request.other_host_info.value 

496 

497 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

498 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

499 

500 if request.HasField("sleeping_details"): 

501 if request.sleeping_details.is_null: 

502 user.sleeping_details = None 

503 else: 

504 user.sleeping_details = request.sleeping_details.value 

505 

506 if request.HasField("area"): 

507 if request.area.is_null: 

508 user.area = None 

509 else: 

510 user.area = request.area.value 

511 

512 if request.HasField("house_rules"): 

513 if request.house_rules.is_null: 

514 user.house_rules = None 

515 else: 

516 user.house_rules = request.house_rules.value 

517 

518 if request.HasField("parking"): 

519 if request.parking.is_null: 

520 user.parking = None 

521 else: 

522 user.parking = request.parking.value 

523 

524 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

525 user.parking_details = parkingdetails2sql[request.parking_details] 

526 

527 if request.HasField("camping_ok"): 

528 if request.camping_ok.is_null: 

529 user.camping_ok = None 

530 else: 

531 user.camping_ok = request.camping_ok.value 

532 

533 return empty_pb2.Empty() 

534 

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

536 rels = ( 

537 session.execute( 

538 select(FriendRelationship) 

539 .where_users_column_visible(context, FriendRelationship.from_user_id) 

540 .where_users_column_visible(context, FriendRelationship.to_user_id) 

541 .where( 

542 or_( 

543 FriendRelationship.from_user_id == context.user_id, 

544 FriendRelationship.to_user_id == context.user_id, 

545 ) 

546 ) 

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

548 ) 

549 .scalars() 

550 .all() 

551 ) 

552 return api_pb2.ListFriendsRes( 

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

554 ) 

555 

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

557 rel = session.execute( 

558 select(FriendRelationship) 

559 .where_users_column_visible(context, FriendRelationship.from_user_id) 

560 .where_users_column_visible(context, FriendRelationship.to_user_id) 

561 .where( 

562 or_( 

563 and_( 

564 FriendRelationship.from_user_id == request.user_id, 

565 FriendRelationship.to_user_id == context.user_id, 

566 ), 

567 and_( 

568 FriendRelationship.from_user_id == context.user_id, 

569 FriendRelationship.to_user_id == request.user_id, 

570 ), 

571 ) 

572 ) 

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

574 ).scalar_one_or_none() 

575 

576 if not rel: 

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

578 

579 session.delete(rel) 

580 

581 return empty_pb2.Empty() 

582 

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

584 if context.user_id == request.user_id: 

585 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

586 

587 user = session.execute( 

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

589 ).scalar_one_or_none() 

590 

591 if not user: 

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

593 

594 q1 = ( 

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

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

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

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

599 ) 

600 

601 q2 = ( 

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

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

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

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

606 ) 

607 

608 q3 = ( 

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

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

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

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

613 ) 

614 

615 q4 = ( 

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

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

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

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

620 ) 

621 

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

623 

624 mutual_friends = ( 

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

626 ) 

627 

628 return api_pb2.ListMutualFriendsRes( 

629 mutual_friends=[ 

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

631 for mutual_friend in mutual_friends 

632 ] 

633 ) 

634 

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

636 if context.user_id == request.user_id: 

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

638 

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

640 to_user = session.execute( 

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

642 ).scalar_one_or_none() 

643 

644 if not to_user: 

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

646 

647 if ( 

648 session.execute( 

649 select(FriendRelationship) 

650 .where( 

651 or_( 

652 and_( 

653 FriendRelationship.from_user_id == context.user_id, 

654 FriendRelationship.to_user_id == request.user_id, 

655 ), 

656 and_( 

657 FriendRelationship.from_user_id == request.user_id, 

658 FriendRelationship.to_user_id == context.user_id, 

659 ), 

660 ) 

661 ) 

662 .where( 

663 or_( 

664 FriendRelationship.status == FriendStatus.accepted, 

665 FriendRelationship.status == FriendStatus.pending, 

666 ) 

667 ) 

668 ).scalar_one_or_none() 

669 is not None 

670 ): 

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

672 

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

674 

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

676 session.add(friend_relationship) 

677 session.flush() 

678 

679 notify( 

680 session, 

681 user_id=friend_relationship.to_user_id, 

682 topic_action="friend_request:create", 

683 key=friend_relationship.from_user_id, 

684 data=notification_data_pb2.FriendRequestCreate( 

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

686 ), 

687 ) 

688 

689 return empty_pb2.Empty() 

690 

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

692 # both sent and received 

693 sent_requests = ( 

694 session.execute( 

695 select(FriendRelationship) 

696 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

699 ) 

700 .scalars() 

701 .all() 

702 ) 

703 

704 received_requests = ( 

705 session.execute( 

706 select(FriendRelationship) 

707 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

710 ) 

711 .scalars() 

712 .all() 

713 ) 

714 

715 return api_pb2.ListFriendRequestsRes( 

716 sent=[ 

717 api_pb2.FriendRequest( 

718 friend_request_id=friend_request.id, 

719 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

720 user_id=friend_request.to_user.id, 

721 sent=True, 

722 ) 

723 for friend_request in sent_requests 

724 ], 

725 received=[ 

726 api_pb2.FriendRequest( 

727 friend_request_id=friend_request.id, 

728 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

729 user_id=friend_request.from_user.id, 

730 sent=False, 

731 ) 

732 for friend_request in received_requests 

733 ], 

734 ) 

735 

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

737 friend_request = session.execute( 

738 select(FriendRelationship) 

739 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

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

743 ).scalar_one_or_none() 

744 

745 if not friend_request: 

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

747 

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

749 friend_request.time_responded = func.now() 

750 

751 session.flush() 

752 

753 if friend_request.status == FriendStatus.accepted: 

754 notify( 

755 session, 

756 user_id=friend_request.from_user_id, 

757 topic_action="friend_request:accept", 

758 key=friend_request.to_user_id, 

759 data=notification_data_pb2.FriendRequestAccept( 

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

761 ), 

762 ) 

763 

764 return empty_pb2.Empty() 

765 

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

767 friend_request = session.execute( 

768 select(FriendRelationship) 

769 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

773 ).scalar_one_or_none() 

774 

775 if not friend_request: 

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

777 

778 friend_request.status = FriendStatus.cancelled 

779 friend_request.time_responded = func.now() 

780 

781 # note no notifications 

782 

783 session.commit() 

784 

785 return empty_pb2.Empty() 

786 

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

788 key = random_hex() 

789 

790 created = now() 

791 expiry = created + timedelta(minutes=20) 

792 

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

794 session.add(upload) 

795 session.commit() 

796 

797 req = media_pb2.UploadRequest( 

798 key=upload.key, 

799 type=media_pb2.UploadRequest.UploadType.IMAGE, 

800 created=Timestamp_from_datetime(upload.created), 

801 expiry=Timestamp_from_datetime(upload.expiry), 

802 max_width=2000, 

803 max_height=1600, 

804 ).SerializeToString() 

805 

806 data = b64encode(req) 

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

808 

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

810 

811 return api_pb2.InitiateMediaUploadRes( 

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

813 expiry=Timestamp_from_datetime(expiry), 

814 ) 

815 

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

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

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

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

820 if not badge: 

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

822 

823 badge_user_ids = ( 

824 session.execute( 

825 select(UserBadge.user_id) 

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

827 .where(UserBadge.user_id >= next_user_id) 

828 .order_by(UserBadge.user_id) 

829 .limit(page_size + 1) 

830 ) 

831 .scalars() 

832 .all() 

833 ) 

834 

835 return api_pb2.ListBadgeUsersRes( 

836 user_ids=badge_user_ids[:page_size], 

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

838 ) 

839 

840 

841def response_rate_to_pb(response_rates): 

842 if not response_rates: 

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

844 

845 _, n, response_rate, _, response_time_p33, response_time_p66 = response_rates 

846 

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

848 if not n or n < 3: 

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

850 

851 if response_rate <= 0.33: 

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

853 

854 response_time_p33_coarsened = Duration_from_timedelta( 

855 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60) 

856 ) 

857 

858 if response_rate <= 0.66: 

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

860 

861 response_time_p66_coarsened = Duration_from_timedelta( 

862 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60) 

863 ) 

864 

865 if response_rate <= 0.90: 

866 return { 

867 "most": requests_pb2.ResponseRateMost( 

868 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

869 ) 

870 } 

871 else: 

872 return { 

873 "almost_all": requests_pb2.ResponseRateAlmostAll( 

874 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

875 ) 

876 } 

877 

878 

879def get_num_references(session, user_ids): 

880 return dict( 

881 session.execute( 

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

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

884 .where(Reference.is_deleted == False) 

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

886 .where(User.is_visible) 

887 .group_by(Reference.to_user_id) 

888 ).all() 

889 ) 

890 

891 

892def user_model_to_pb(db_user, session, context): 

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

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

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

896 

897 # returns (lat, lng) 

898 # we put people without coords on null island 

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

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

901 

902 pending_friend_request = None 

903 if db_user.id == context.user_id: 

904 friends_status = api_pb2.User.FriendshipStatus.NA 

905 else: 

906 friend_relationship = session.execute( 

907 select(FriendRelationship) 

908 .where( 

909 or_( 

910 and_( 

911 FriendRelationship.from_user_id == context.user_id, 

912 FriendRelationship.to_user_id == db_user.id, 

913 ), 

914 and_( 

915 FriendRelationship.from_user_id == db_user.id, 

916 FriendRelationship.to_user_id == context.user_id, 

917 ), 

918 ) 

919 ) 

920 .where( 

921 or_( 

922 FriendRelationship.status == FriendStatus.accepted, 

923 FriendRelationship.status == FriendStatus.pending, 

924 ) 

925 ) 

926 ).scalar_one_or_none() 

927 

928 if friend_relationship: 

929 if friend_relationship.status == FriendStatus.accepted: 

930 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

931 else: 

932 friends_status = api_pb2.User.FriendshipStatus.PENDING 

933 if friend_relationship.from_user_id == context.user_id: 

934 # we sent it 

935 pending_friend_request = api_pb2.FriendRequest( 

936 friend_request_id=friend_relationship.id, 

937 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

938 user_id=friend_relationship.to_user.id, 

939 sent=True, 

940 ) 

941 else: 

942 # we received it 

943 pending_friend_request = api_pb2.FriendRequest( 

944 friend_request_id=friend_relationship.id, 

945 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

946 user_id=friend_relationship.from_user.id, 

947 sent=False, 

948 ) 

949 else: 

950 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

951 

952 response_rates = session.execute( 

953 select(user_response_rates).where(user_response_rates.c.user_id == db_user.id) 

954 ).one_or_none() 

955 

956 verification_score = 0.0 

957 if db_user.phone_verification_verified: 

958 verification_score += 1.0 * db_user.phone_is_verified 

959 

960 user = api_pb2.User( 

961 user_id=db_user.id, 

962 username=db_user.username, 

963 name=db_user.name, 

964 city=db_user.city, 

965 hometown=db_user.hometown, 

966 timezone=db_user.timezone, 

967 lat=lat, 

968 lng=lng, 

969 radius=db_user.geom_radius, 

970 verification=verification_score, 

971 community_standing=db_user.community_standing, 

972 num_references=num_references, 

973 gender=db_user.gender, 

974 pronouns=db_user.pronouns, 

975 age=int(db_user.age), 

976 joined=Timestamp_from_datetime(db_user.display_joined), 

977 last_active=Timestamp_from_datetime(db_user.display_last_active), 

978 hosting_status=hostingstatus2api[db_user.hosting_status], 

979 meetup_status=meetupstatus2api[db_user.meetup_status], 

980 occupation=db_user.occupation, 

981 education=db_user.education, 

982 about_me=db_user.about_me, 

983 things_i_like=db_user.things_i_like, 

984 about_place=db_user.about_place, 

985 language_abilities=[ 

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

987 for ability in db_user.language_abilities 

988 ], 

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

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

991 additional_information=db_user.additional_information, 

992 friends=friends_status, 

993 pending_friend_request=pending_friend_request, 

994 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

995 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

996 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

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

1000 .scalars() 

1001 .all(), 

1002 **get_strong_verification_fields(session, db_user), 

1003 **response_rate_to_pb(response_rates), 

1004 ) 

1005 

1006 if db_user.max_guests is not None: 

1007 user.max_guests.value = db_user.max_guests 

1008 

1009 if db_user.last_minute is not None: 

1010 user.last_minute.value = db_user.last_minute 

1011 

1012 if db_user.has_pets is not None: 

1013 user.has_pets.value = db_user.has_pets 

1014 

1015 if db_user.accepts_pets is not None: 

1016 user.accepts_pets.value = db_user.accepts_pets 

1017 

1018 if db_user.pet_details is not None: 

1019 user.pet_details.value = db_user.pet_details 

1020 

1021 if db_user.has_kids is not None: 

1022 user.has_kids.value = db_user.has_kids 

1023 

1024 if db_user.accepts_kids is not None: 

1025 user.accepts_kids.value = db_user.accepts_kids 

1026 

1027 if db_user.kid_details is not None: 

1028 user.kid_details.value = db_user.kid_details 

1029 

1030 if db_user.has_housemates is not None: 

1031 user.has_housemates.value = db_user.has_housemates 

1032 

1033 if db_user.housemate_details is not None: 

1034 user.housemate_details.value = db_user.housemate_details 

1035 

1036 if db_user.wheelchair_accessible is not None: 

1037 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1038 

1039 if db_user.smokes_at_home is not None: 

1040 user.smokes_at_home.value = db_user.smokes_at_home 

1041 

1042 if db_user.drinking_allowed is not None: 

1043 user.drinking_allowed.value = db_user.drinking_allowed 

1044 

1045 if db_user.drinks_at_home is not None: 

1046 user.drinks_at_home.value = db_user.drinks_at_home 

1047 

1048 if db_user.other_host_info is not None: 

1049 user.other_host_info.value = db_user.other_host_info 

1050 

1051 if db_user.sleeping_details is not None: 

1052 user.sleeping_details.value = db_user.sleeping_details 

1053 

1054 if db_user.area is not None: 

1055 user.area.value = db_user.area 

1056 

1057 if db_user.house_rules is not None: 

1058 user.house_rules.value = db_user.house_rules 

1059 

1060 if db_user.parking is not None: 

1061 user.parking.value = db_user.parking 

1062 

1063 if db_user.camping_ok is not None: 

1064 user.camping_ok.value = db_user.camping_ok 

1065 

1066 return user 

1067 

1068 

1069def lite_user_to_pb(lite_user): 

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

1071 

1072 return api_pb2.LiteUser( 

1073 user_id=lite_user.id, 

1074 username=lite_user.username, 

1075 name=lite_user.name, 

1076 city=lite_user.city, 

1077 age=int(lite_user.age), 

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

1079 if lite_user.avatar_filename 

1080 else None, 

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

1082 if lite_user.avatar_filename 

1083 else None, 

1084 lat=lat, 

1085 lng=lng, 

1086 radius=lite_user.radius, 

1087 has_strong_verification=lite_user.has_strong_verification, 

1088 )