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

386 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-05-12 02:28 +0000

1from datetime import timedelta 

2from urllib.parse import urlencode 

3 

4import grpc 

5from google.protobuf import empty_pb2 

6from sqlalchemy.orm import aliased 

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

8 

9from couchers import errors, urls 

10from couchers.config import config 

11from couchers.crypto import b64encode, generate_hash_signature, random_hex 

12from couchers.materialized_views import lite_users, user_response_rates 

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

26 Reference, 

27 RegionLived, 

28 RegionVisited, 

29 SleepingArrangement, 

30 SmokingLocation, 

31 User, 

32 UserBadge, 

33) 

34from couchers.notifications.notify import notify 

35from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

36from couchers.servicers.account import get_strong_verification_fields 

37from couchers.sql import couchers_select as select 

38from couchers.sql import is_valid_user_id, is_valid_username 

39from couchers.utils import ( 

40 Duration_from_timedelta, 

41 Timestamp_from_datetime, 

42 create_coordinate, 

43 get_coordinates, 

44 is_valid_name, 

45 now, 

46) 

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

48 

49MAX_USERS_PER_QUERY = 200 

50MAX_PAGINATION_LENGTH = 50 

51 

52hostingstatus2sql = { 

53 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

54 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

55 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

56 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

57} 

58 

59hostingstatus2api = { 

60 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

61 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

62 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

63 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

64} 

65 

66meetupstatus2sql = { 

67 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

68 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

69 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

70 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

71} 

72 

73meetupstatus2api = { 

74 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

75 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

76 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

77 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

78} 

79 

80smokinglocation2sql = { 

81 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

82 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

83 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

84 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

85 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

86} 

87 

88smokinglocation2api = { 

89 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

90 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

91 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

92 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

93 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

94} 

95 

96sleepingarrangement2sql = { 

97 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

98 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

99 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

100 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

101} 

102 

103sleepingarrangement2api = { 

104 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

105 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

106 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

107 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

108} 

109 

110parkingdetails2sql = { 

111 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

112 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

113 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

114 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

115 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

116} 

117 

118parkingdetails2api = { 

119 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

120 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

121 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

122 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

123 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

124} 

125 

126fluency2sql = { 

127 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

131} 

132 

133fluency2api = { 

134 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

138} 

139 

140 

141class API(api_pb2_grpc.APIServicer): 

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

143 # auth ought to make sure the user exists 

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

145 

146 # gets only the max message by self-joining messages which have a greater id 

147 # if it doesn't have a greater id, it's the biggest 

148 message_2 = aliased(Message) 

149 unseen_sent_host_request_count = session.execute( 

150 select(func.count()) 

151 .select_from(Message) 

152 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id) 

153 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id)) 

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

155 .where_users_column_visible(context, HostRequest.host_user_id) 

156 .where(message_2.id == None) 

157 .where(HostRequest.surfer_last_seen_message_id < Message.id) 

158 ).scalar_one() 

159 

160 unseen_received_host_request_count = session.execute( 

161 select(func.count()) 

162 .select_from(Message) 

163 .join(HostRequest, Message.conversation_id == HostRequest.conversation_id) 

164 .outerjoin(message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id)) 

165 .where_users_column_visible(context, HostRequest.surfer_user_id) 

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

167 .where(message_2.id == None) 

168 .where(HostRequest.host_last_seen_message_id < Message.id) 

169 ).scalar_one() 

170 

171 unseen_message_count = session.execute( 

172 select(func.count()) 

173 .select_from(Message) 

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

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

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

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

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

179 ).scalar_one() 

180 

181 pending_friend_request_count = session.execute( 

182 select(func.count()) 

183 .select_from(FriendRelationship) 

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

185 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

187 ).scalar_one() 

188 

189 unseen_notification_count = session.execute( 

190 select(func.count(Notification.id)) 

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

192 .where(Notification.is_seen == False) 

193 ).scalar_one() 

194 

195 return api_pb2.PingRes( 

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

197 unseen_message_count=unseen_message_count, 

198 unseen_sent_host_request_count=unseen_sent_host_request_count, 

199 unseen_received_host_request_count=unseen_received_host_request_count, 

200 pending_friend_request_count=pending_friend_request_count, 

201 unseen_notification_count=unseen_notification_count, 

202 ) 

203 

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

205 user = session.execute( 

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

207 ).scalar_one_or_none() 

208 

209 if not user: 

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

211 

212 return user_model_to_pb(user, session, context) 

213 

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

215 lite_user = session.execute( 

216 select(lite_users) 

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

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

219 ).one_or_none() 

220 

221 if not lite_user: 

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

223 

224 return lite_user_to_pb(lite_user) 

225 

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

227 if len(request.users) > MAX_USERS_PER_QUERY: 

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

229 

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

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

232 

233 users = session.execute( 

234 select(lite_users) 

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

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

237 ).all() 

238 

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

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

241 

242 res = api_pb2.GetLiteUsersRes() 

243 

244 for user in request.users: 

245 lite_user = None 

246 if user in users_by_id: 

247 lite_user = users_by_id[user] 

248 elif user in users_by_username: 

249 lite_user = users_by_username[user] 

250 

251 res.responses.append( 

252 api_pb2.LiteUserRes( 

253 query=user, 

254 not_found=lite_user is None, 

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

256 ) 

257 ) 

258 

259 return res 

260 

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

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

263 

264 if request.HasField("name"): 

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

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

267 user.name = request.name.value 

268 

269 if request.HasField("city"): 

270 user.city = request.city.value 

271 

272 if request.HasField("hometown"): 

273 if request.hometown.is_null: 

274 user.hometown = None 

275 else: 

276 user.hometown = request.hometown.value 

277 

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

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

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

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

282 

283 if request.HasField("radius"): 

284 user.geom_radius = request.radius.value 

285 

286 if request.HasField("avatar_key"): 

287 if request.avatar_key.is_null: 

288 user.avatar_key = None 

289 else: 

290 user.avatar_key = request.avatar_key.value 

291 

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

293 # user.gender = request.gender.value 

294 

295 if request.HasField("pronouns"): 

296 if request.pronouns.is_null: 

297 user.pronouns = None 

298 else: 

299 user.pronouns = request.pronouns.value 

300 

301 if request.HasField("occupation"): 

302 if request.occupation.is_null: 

303 user.occupation = None 

304 else: 

305 user.occupation = request.occupation.value 

306 

307 if request.HasField("education"): 

308 if request.education.is_null: 

309 user.education = None 

310 else: 

311 user.education = request.education.value 

312 

313 if request.HasField("about_me"): 

314 if request.about_me.is_null: 

315 user.about_me = None 

316 else: 

317 user.about_me = request.about_me.value 

318 

319 if request.HasField("things_i_like"): 

320 if request.things_i_like.is_null: 

321 user.things_i_like = None 

322 else: 

323 user.things_i_like = request.things_i_like.value 

324 

325 if request.HasField("about_place"): 

326 if request.about_place.is_null: 

327 user.about_place = None 

328 else: 

329 user.about_place = request.about_place.value 

330 

331 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

334 user.hosting_status = hostingstatus2sql[request.hosting_status] 

335 

336 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

339 user.meetup_status = meetupstatus2sql[request.meetup_status] 

340 

341 if request.HasField("language_abilities"): 

342 # delete all existing abilities 

343 for ability in user.language_abilities: 

344 session.delete(ability) 

345 session.flush() 

346 

347 # add the new ones 

348 for language_ability in request.language_abilities.value: 

349 if not language_is_allowed(language_ability.code): 

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

351 session.add( 

352 LanguageAbility( 

353 user=user, 

354 language_code=language_ability.code, 

355 fluency=fluency2sql[language_ability.fluency], 

356 ) 

357 ) 

358 

359 if request.HasField("regions_visited"): 

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

361 

362 for region in request.regions_visited.value: 

363 if not region_is_allowed(region): 

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

365 session.add( 

366 RegionVisited( 

367 user_id=user.id, 

368 region_code=region, 

369 ) 

370 ) 

371 

372 if request.HasField("regions_lived"): 

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

374 

375 for region in request.regions_lived.value: 

376 if not region_is_allowed(region): 

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

378 session.add( 

379 RegionLived( 

380 user_id=user.id, 

381 region_code=region, 

382 ) 

383 ) 

384 

385 if request.HasField("additional_information"): 

386 if request.additional_information.is_null: 

387 user.additional_information = None 

388 else: 

389 user.additional_information = request.additional_information.value 

390 

391 if request.HasField("max_guests"): 

392 if request.max_guests.is_null: 

393 user.max_guests = None 

394 else: 

395 user.max_guests = request.max_guests.value 

396 

397 if request.HasField("last_minute"): 

398 if request.last_minute.is_null: 

399 user.last_minute = None 

400 else: 

401 user.last_minute = request.last_minute.value 

402 

403 if request.HasField("has_pets"): 

404 if request.has_pets.is_null: 

405 user.has_pets = None 

406 else: 

407 user.has_pets = request.has_pets.value 

408 

409 if request.HasField("accepts_pets"): 

410 if request.accepts_pets.is_null: 

411 user.accepts_pets = None 

412 else: 

413 user.accepts_pets = request.accepts_pets.value 

414 

415 if request.HasField("pet_details"): 

416 if request.pet_details.is_null: 

417 user.pet_details = None 

418 else: 

419 user.pet_details = request.pet_details.value 

420 

421 if request.HasField("has_kids"): 

422 if request.has_kids.is_null: 

423 user.has_kids = None 

424 else: 

425 user.has_kids = request.has_kids.value 

426 

427 if request.HasField("accepts_kids"): 

428 if request.accepts_kids.is_null: 

429 user.accepts_kids = None 

430 else: 

431 user.accepts_kids = request.accepts_kids.value 

432 

433 if request.HasField("kid_details"): 

434 if request.kid_details.is_null: 

435 user.kid_details = None 

436 else: 

437 user.kid_details = request.kid_details.value 

438 

439 if request.HasField("has_housemates"): 

440 if request.has_housemates.is_null: 

441 user.has_housemates = None 

442 else: 

443 user.has_housemates = request.has_housemates.value 

444 

445 if request.HasField("housemate_details"): 

446 if request.housemate_details.is_null: 

447 user.housemate_details = None 

448 else: 

449 user.housemate_details = request.housemate_details.value 

450 

451 if request.HasField("wheelchair_accessible"): 

452 if request.wheelchair_accessible.is_null: 

453 user.wheelchair_accessible = None 

454 else: 

455 user.wheelchair_accessible = request.wheelchair_accessible.value 

456 

457 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

458 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

459 

460 if request.HasField("smokes_at_home"): 

461 if request.smokes_at_home.is_null: 

462 user.smokes_at_home = None 

463 else: 

464 user.smokes_at_home = request.smokes_at_home.value 

465 

466 if request.HasField("drinking_allowed"): 

467 if request.drinking_allowed.is_null: 

468 user.drinking_allowed = None 

469 else: 

470 user.drinking_allowed = request.drinking_allowed.value 

471 

472 if request.HasField("drinks_at_home"): 

473 if request.drinks_at_home.is_null: 

474 user.drinks_at_home = None 

475 else: 

476 user.drinks_at_home = request.drinks_at_home.value 

477 

478 if request.HasField("other_host_info"): 

479 if request.other_host_info.is_null: 

480 user.other_host_info = None 

481 else: 

482 user.other_host_info = request.other_host_info.value 

483 

484 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

485 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

486 

487 if request.HasField("sleeping_details"): 

488 if request.sleeping_details.is_null: 

489 user.sleeping_details = None 

490 else: 

491 user.sleeping_details = request.sleeping_details.value 

492 

493 if request.HasField("area"): 

494 if request.area.is_null: 

495 user.area = None 

496 else: 

497 user.area = request.area.value 

498 

499 if request.HasField("house_rules"): 

500 if request.house_rules.is_null: 

501 user.house_rules = None 

502 else: 

503 user.house_rules = request.house_rules.value 

504 

505 if request.HasField("parking"): 

506 if request.parking.is_null: 

507 user.parking = None 

508 else: 

509 user.parking = request.parking.value 

510 

511 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

512 user.parking_details = parkingdetails2sql[request.parking_details] 

513 

514 if request.HasField("camping_ok"): 

515 if request.camping_ok.is_null: 

516 user.camping_ok = None 

517 else: 

518 user.camping_ok = request.camping_ok.value 

519 

520 # save updates 

521 session.commit() 

522 

523 return empty_pb2.Empty() 

524 

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

526 rels = ( 

527 session.execute( 

528 select(FriendRelationship) 

529 .where_users_column_visible(context, FriendRelationship.from_user_id) 

530 .where_users_column_visible(context, FriendRelationship.to_user_id) 

531 .where( 

532 or_( 

533 FriendRelationship.from_user_id == context.user_id, 

534 FriendRelationship.to_user_id == context.user_id, 

535 ) 

536 ) 

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

538 ) 

539 .scalars() 

540 .all() 

541 ) 

542 return api_pb2.ListFriendsRes( 

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

544 ) 

545 

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

547 if context.user_id == request.user_id: 

548 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

549 

550 user = session.execute( 

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

552 ).scalar_one_or_none() 

553 

554 if not user: 

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

556 

557 q1 = ( 

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

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

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

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

562 ) 

563 

564 q2 = ( 

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

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

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

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

569 ) 

570 

571 q3 = ( 

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

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

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

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

576 ) 

577 

578 q4 = ( 

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

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

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

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

583 ) 

584 

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

586 

587 mutual_friends = ( 

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

589 ) 

590 

591 return api_pb2.ListMutualFriendsRes( 

592 mutual_friends=[ 

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

594 for mutual_friend in mutual_friends 

595 ] 

596 ) 

597 

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

599 if context.user_id == request.user_id: 

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

601 

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

603 to_user = session.execute( 

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

605 ).scalar_one_or_none() 

606 

607 if not to_user: 

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

609 

610 if ( 

611 session.execute( 

612 select(FriendRelationship) 

613 .where( 

614 or_( 

615 and_( 

616 FriendRelationship.from_user_id == context.user_id, 

617 FriendRelationship.to_user_id == request.user_id, 

618 ), 

619 and_( 

620 FriendRelationship.from_user_id == request.user_id, 

621 FriendRelationship.to_user_id == context.user_id, 

622 ), 

623 ) 

624 ) 

625 .where( 

626 or_( 

627 FriendRelationship.status == FriendStatus.accepted, 

628 FriendRelationship.status == FriendStatus.pending, 

629 ) 

630 ) 

631 ).scalar_one_or_none() 

632 is not None 

633 ): 

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

635 

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

637 

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

639 session.add(friend_relationship) 

640 session.flush() 

641 

642 notify( 

643 session, 

644 user_id=friend_relationship.to_user_id, 

645 topic_action="friend_request:create", 

646 key=friend_relationship.from_user_id, 

647 data=notification_data_pb2.FriendRequestCreate( 

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

649 ), 

650 ) 

651 

652 return empty_pb2.Empty() 

653 

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

655 # both sent and received 

656 sent_requests = ( 

657 session.execute( 

658 select(FriendRelationship) 

659 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

662 ) 

663 .scalars() 

664 .all() 

665 ) 

666 

667 received_requests = ( 

668 session.execute( 

669 select(FriendRelationship) 

670 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

673 ) 

674 .scalars() 

675 .all() 

676 ) 

677 

678 return api_pb2.ListFriendRequestsRes( 

679 sent=[ 

680 api_pb2.FriendRequest( 

681 friend_request_id=friend_request.id, 

682 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

683 user_id=friend_request.to_user.id, 

684 sent=True, 

685 ) 

686 for friend_request in sent_requests 

687 ], 

688 received=[ 

689 api_pb2.FriendRequest( 

690 friend_request_id=friend_request.id, 

691 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

692 user_id=friend_request.from_user.id, 

693 sent=False, 

694 ) 

695 for friend_request in received_requests 

696 ], 

697 ) 

698 

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

700 friend_request = session.execute( 

701 select(FriendRelationship) 

702 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

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

706 ).scalar_one_or_none() 

707 

708 if not friend_request: 

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

710 

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

712 friend_request.time_responded = func.now() 

713 

714 session.flush() 

715 

716 if friend_request.status == FriendStatus.accepted: 

717 notify( 

718 session, 

719 user_id=friend_request.from_user_id, 

720 topic_action="friend_request:accept", 

721 key=friend_request.to_user_id, 

722 data=notification_data_pb2.FriendRequestAccept( 

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

724 ), 

725 ) 

726 

727 return empty_pb2.Empty() 

728 

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

730 friend_request = session.execute( 

731 select(FriendRelationship) 

732 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

736 ).scalar_one_or_none() 

737 

738 if not friend_request: 

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

740 

741 friend_request.status = FriendStatus.cancelled 

742 friend_request.time_responded = func.now() 

743 

744 # note no notifications 

745 

746 session.commit() 

747 

748 return empty_pb2.Empty() 

749 

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

751 key = random_hex() 

752 

753 created = now() 

754 expiry = created + timedelta(minutes=20) 

755 

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

757 session.add(upload) 

758 session.commit() 

759 

760 req = media_pb2.UploadRequest( 

761 key=upload.key, 

762 type=media_pb2.UploadRequest.UploadType.IMAGE, 

763 created=Timestamp_from_datetime(upload.created), 

764 expiry=Timestamp_from_datetime(upload.expiry), 

765 max_width=2000, 

766 max_height=1600, 

767 ).SerializeToString() 

768 

769 data = b64encode(req) 

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

771 

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

773 

774 return api_pb2.InitiateMediaUploadRes( 

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

776 expiry=Timestamp_from_datetime(expiry), 

777 ) 

778 

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

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

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

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

783 if not badge: 

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

785 

786 badge_user_ids = ( 

787 session.execute( 

788 select(UserBadge.user_id) 

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

790 .where(UserBadge.user_id >= next_user_id) 

791 .order_by(UserBadge.user_id) 

792 .limit(page_size + 1) 

793 ) 

794 .scalars() 

795 .all() 

796 ) 

797 

798 return api_pb2.ListBadgeUsersRes( 

799 user_ids=badge_user_ids[:page_size], 

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

801 ) 

802 

803 

804def response_rate_to_pb(response_rates): 

805 if not response_rates: 

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

807 

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

809 

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

811 if not n or n < 3: 

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

813 

814 if response_rate <= 0.33: 

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

816 

817 response_time_p33_coarsened = Duration_from_timedelta( 

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

819 ) 

820 

821 if response_rate <= 0.66: 

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

823 

824 response_time_p66_coarsened = Duration_from_timedelta( 

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

826 ) 

827 

828 if response_rate <= 0.90: 

829 return { 

830 "most": requests_pb2.ResponseRateMost( 

831 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

832 ) 

833 } 

834 else: 

835 return { 

836 "almost_all": requests_pb2.ResponseRateAlmostAll( 

837 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

838 ) 

839 } 

840 

841 

842def user_model_to_pb(db_user, session, context): 

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

844 num_references = session.execute( 

845 select(func.count()) 

846 .select_from(Reference) 

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

848 .where(User.is_visible) 

849 .where(Reference.to_user_id == db_user.id) 

850 .where(Reference.is_deleted == False) 

851 ).scalar_one() 

852 

853 # returns (lat, lng) 

854 # we put people without coords on null island 

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

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

857 

858 pending_friend_request = None 

859 if db_user.id == context.user_id: 

860 friends_status = api_pb2.User.FriendshipStatus.NA 

861 else: 

862 friend_relationship = session.execute( 

863 select(FriendRelationship) 

864 .where( 

865 or_( 

866 and_( 

867 FriendRelationship.from_user_id == context.user_id, 

868 FriendRelationship.to_user_id == db_user.id, 

869 ), 

870 and_( 

871 FriendRelationship.from_user_id == db_user.id, 

872 FriendRelationship.to_user_id == context.user_id, 

873 ), 

874 ) 

875 ) 

876 .where( 

877 or_( 

878 FriendRelationship.status == FriendStatus.accepted, 

879 FriendRelationship.status == FriendStatus.pending, 

880 ) 

881 ) 

882 ).scalar_one_or_none() 

883 

884 if friend_relationship: 

885 if friend_relationship.status == FriendStatus.accepted: 

886 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

887 else: 

888 friends_status = api_pb2.User.FriendshipStatus.PENDING 

889 if friend_relationship.from_user_id == context.user_id: 

890 # we sent it 

891 pending_friend_request = api_pb2.FriendRequest( 

892 friend_request_id=friend_relationship.id, 

893 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

894 user_id=friend_relationship.to_user.id, 

895 sent=True, 

896 ) 

897 else: 

898 # we received it 

899 pending_friend_request = api_pb2.FriendRequest( 

900 friend_request_id=friend_relationship.id, 

901 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

902 user_id=friend_relationship.from_user.id, 

903 sent=False, 

904 ) 

905 else: 

906 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

907 

908 response_rates = session.execute( 

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

910 ).one_or_none() 

911 

912 verification_score = 0.0 

913 if db_user.phone_verification_verified: 

914 verification_score += 1.0 * db_user.phone_is_verified 

915 

916 user = api_pb2.User( 

917 user_id=db_user.id, 

918 username=db_user.username, 

919 name=db_user.name, 

920 city=db_user.city, 

921 hometown=db_user.hometown, 

922 timezone=db_user.timezone, 

923 lat=lat, 

924 lng=lng, 

925 radius=db_user.geom_radius, 

926 verification=verification_score, 

927 community_standing=db_user.community_standing, 

928 num_references=num_references, 

929 gender=db_user.gender, 

930 pronouns=db_user.pronouns, 

931 age=int(db_user.age), 

932 joined=Timestamp_from_datetime(db_user.display_joined), 

933 last_active=Timestamp_from_datetime(db_user.display_last_active), 

934 hosting_status=hostingstatus2api[db_user.hosting_status], 

935 meetup_status=meetupstatus2api[db_user.meetup_status], 

936 occupation=db_user.occupation, 

937 education=db_user.education, 

938 about_me=db_user.about_me, 

939 things_i_like=db_user.things_i_like, 

940 about_place=db_user.about_place, 

941 language_abilities=[ 

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

943 for ability in db_user.language_abilities 

944 ], 

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

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

947 additional_information=db_user.additional_information, 

948 friends=friends_status, 

949 pending_friend_request=pending_friend_request, 

950 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

951 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

952 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

955 badges=[badge.badge_id for badge in db_user.badges], 

956 **get_strong_verification_fields(session, db_user), 

957 **response_rate_to_pb(response_rates), 

958 ) 

959 

960 if db_user.max_guests is not None: 

961 user.max_guests.value = db_user.max_guests 

962 

963 if db_user.last_minute is not None: 

964 user.last_minute.value = db_user.last_minute 

965 

966 if db_user.has_pets is not None: 

967 user.has_pets.value = db_user.has_pets 

968 

969 if db_user.accepts_pets is not None: 

970 user.accepts_pets.value = db_user.accepts_pets 

971 

972 if db_user.pet_details is not None: 

973 user.pet_details.value = db_user.pet_details 

974 

975 if db_user.has_kids is not None: 

976 user.has_kids.value = db_user.has_kids 

977 

978 if db_user.accepts_kids is not None: 

979 user.accepts_kids.value = db_user.accepts_kids 

980 

981 if db_user.kid_details is not None: 

982 user.kid_details.value = db_user.kid_details 

983 

984 if db_user.has_housemates is not None: 

985 user.has_housemates.value = db_user.has_housemates 

986 

987 if db_user.housemate_details is not None: 

988 user.housemate_details.value = db_user.housemate_details 

989 

990 if db_user.wheelchair_accessible is not None: 

991 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

992 

993 if db_user.smokes_at_home is not None: 

994 user.smokes_at_home.value = db_user.smokes_at_home 

995 

996 if db_user.drinking_allowed is not None: 

997 user.drinking_allowed.value = db_user.drinking_allowed 

998 

999 if db_user.drinks_at_home is not None: 

1000 user.drinks_at_home.value = db_user.drinks_at_home 

1001 

1002 if db_user.other_host_info is not None: 

1003 user.other_host_info.value = db_user.other_host_info 

1004 

1005 if db_user.sleeping_details is not None: 

1006 user.sleeping_details.value = db_user.sleeping_details 

1007 

1008 if db_user.area is not None: 

1009 user.area.value = db_user.area 

1010 

1011 if db_user.house_rules is not None: 

1012 user.house_rules.value = db_user.house_rules 

1013 

1014 if db_user.parking is not None: 

1015 user.parking.value = db_user.parking 

1016 

1017 if db_user.camping_ok is not None: 

1018 user.camping_ok.value = db_user.camping_ok 

1019 

1020 return user 

1021 

1022 

1023def lite_user_to_pb(lite_user): 

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

1025 

1026 return api_pb2.LiteUser( 

1027 user_id=lite_user.id, 

1028 username=lite_user.username, 

1029 name=lite_user.name, 

1030 city=lite_user.city, 

1031 age=int(lite_user.age), 

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

1033 if lite_user.avatar_filename 

1034 else None, 

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

1036 if lite_user.avatar_filename 

1037 else None, 

1038 lat=lat, 

1039 lng=lng, 

1040 radius=lite_user.radius, 

1041 has_strong_verification=lite_user.has_strong_verification, 

1042 )