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

369 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-12-20 18:03 +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 

13from couchers.models import ( 

14 FriendRelationship, 

15 FriendStatus, 

16 GroupChatSubscription, 

17 HostingStatus, 

18 HostRequest, 

19 InitiatedUpload, 

20 LanguageAbility, 

21 LanguageFluency, 

22 MeetupStatus, 

23 Message, 

24 ParkingDetails, 

25 Reference, 

26 RegionLived, 

27 RegionVisited, 

28 SleepingArrangement, 

29 SmokingLocation, 

30 User, 

31 UserBadge, 

32) 

33from couchers.notifications.notify import notify 

34from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

35from couchers.servicers.account import get_strong_verification_fields 

36from couchers.sql import couchers_select as select 

37from couchers.sql import is_valid_user_id, is_valid_username 

38from couchers.utils import ( 

39 Timestamp_from_datetime, 

40 create_coordinate, 

41 get_coordinates, 

42 is_valid_name, 

43 now, 

44) 

45from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2 

46 

47MAX_USERS_PER_QUERY = 200 

48MAX_PAGINATION_LENGTH = 50 

49 

50hostingstatus2sql = { 

51 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

52 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

53 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

54 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

55} 

56 

57hostingstatus2api = { 

58 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

59 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

60 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

61 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

62} 

63 

64meetupstatus2sql = { 

65 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

66 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

67 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

68 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

69} 

70 

71meetupstatus2api = { 

72 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

73 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

74 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

75 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

76} 

77 

78smokinglocation2sql = { 

79 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

80 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

81 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

82 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

83 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

84} 

85 

86smokinglocation2api = { 

87 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

88 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

89 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

90 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

91 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

92} 

93 

94sleepingarrangement2sql = { 

95 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

96 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

97 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

98 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

99 api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE: SleepingArrangement.shared_space, 

100} 

101 

102sleepingarrangement2api = { 

103 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

104 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

105 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

106 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

107 SleepingArrangement.shared_space: api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE, 

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 return api_pb2.PingRes( 

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

191 unseen_message_count=unseen_message_count, 

192 unseen_sent_host_request_count=unseen_sent_host_request_count, 

193 unseen_received_host_request_count=unseen_received_host_request_count, 

194 pending_friend_request_count=pending_friend_request_count, 

195 ) 

196 

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

198 user = session.execute( 

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

200 ).scalar_one_or_none() 

201 

202 if not user: 

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

204 

205 return user_model_to_pb(user, session, context) 

206 

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

208 lite_user = session.execute( 

209 select(lite_users) 

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

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

212 ).one_or_none() 

213 

214 if not lite_user: 

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

216 

217 return lite_user_to_pb(lite_user) 

218 

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

220 if len(request.users) > MAX_USERS_PER_QUERY: 

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

222 

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

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

225 

226 users = session.execute( 

227 select(lite_users) 

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

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

230 ).all() 

231 

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

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

234 

235 res = api_pb2.GetLiteUsersRes() 

236 

237 for user in request.users: 

238 lite_user = None 

239 if user in users_by_id: 

240 lite_user = users_by_id[user] 

241 elif user in users_by_username: 

242 lite_user = users_by_username[user] 

243 

244 res.responses.append( 

245 api_pb2.LiteUserRes( 

246 query=user, 

247 not_found=lite_user is None, 

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

249 ) 

250 ) 

251 

252 return res 

253 

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

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

256 

257 if request.HasField("name"): 

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

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

260 user.name = request.name.value 

261 

262 if request.HasField("city"): 

263 user.city = request.city.value 

264 

265 if request.HasField("hometown"): 

266 if request.hometown.is_null: 

267 user.hometown = None 

268 else: 

269 user.hometown = request.hometown.value 

270 

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

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

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

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

275 

276 if request.HasField("radius"): 

277 user.geom_radius = request.radius.value 

278 

279 if request.HasField("avatar_key"): 

280 if request.avatar_key.is_null: 

281 user.avatar_key = None 

282 else: 

283 user.avatar_key = request.avatar_key.value 

284 

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

286 # user.gender = request.gender.value 

287 

288 if request.HasField("pronouns"): 

289 if request.pronouns.is_null: 

290 user.pronouns = None 

291 else: 

292 user.pronouns = request.pronouns.value 

293 

294 if request.HasField("occupation"): 

295 if request.occupation.is_null: 

296 user.occupation = None 

297 else: 

298 user.occupation = request.occupation.value 

299 

300 if request.HasField("education"): 

301 if request.education.is_null: 

302 user.education = None 

303 else: 

304 user.education = request.education.value 

305 

306 if request.HasField("about_me"): 

307 if request.about_me.is_null: 

308 user.about_me = None 

309 else: 

310 user.about_me = request.about_me.value 

311 

312 if request.HasField("things_i_like"): 

313 if request.things_i_like.is_null: 

314 user.things_i_like = None 

315 else: 

316 user.things_i_like = request.things_i_like.value 

317 

318 if request.HasField("about_place"): 

319 if request.about_place.is_null: 

320 user.about_place = None 

321 else: 

322 user.about_place = request.about_place.value 

323 

324 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

327 user.hosting_status = hostingstatus2sql[request.hosting_status] 

328 

329 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

332 user.meetup_status = meetupstatus2sql[request.meetup_status] 

333 

334 if request.HasField("language_abilities"): 

335 # delete all existing abilities 

336 for ability in user.language_abilities: 

337 session.delete(ability) 

338 session.flush() 

339 

340 # add the new ones 

341 for language_ability in request.language_abilities.value: 

342 if not language_is_allowed(language_ability.code): 

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

344 session.add( 

345 LanguageAbility( 

346 user=user, 

347 language_code=language_ability.code, 

348 fluency=fluency2sql[language_ability.fluency], 

349 ) 

350 ) 

351 

352 if request.HasField("regions_visited"): 

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

354 

355 for region in request.regions_visited.value: 

356 if not region_is_allowed(region): 

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

358 session.add( 

359 RegionVisited( 

360 user_id=user.id, 

361 region_code=region, 

362 ) 

363 ) 

364 

365 if request.HasField("regions_lived"): 

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

367 

368 for region in request.regions_lived.value: 

369 if not region_is_allowed(region): 

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

371 session.add( 

372 RegionLived( 

373 user_id=user.id, 

374 region_code=region, 

375 ) 

376 ) 

377 

378 if request.HasField("additional_information"): 

379 if request.additional_information.is_null: 

380 user.additional_information = None 

381 else: 

382 user.additional_information = request.additional_information.value 

383 

384 if request.HasField("max_guests"): 

385 if request.max_guests.is_null: 

386 user.max_guests = None 

387 else: 

388 user.max_guests = request.max_guests.value 

389 

390 if request.HasField("last_minute"): 

391 if request.last_minute.is_null: 

392 user.last_minute = None 

393 else: 

394 user.last_minute = request.last_minute.value 

395 

396 if request.HasField("has_pets"): 

397 if request.has_pets.is_null: 

398 user.has_pets = None 

399 else: 

400 user.has_pets = request.has_pets.value 

401 

402 if request.HasField("accepts_pets"): 

403 if request.accepts_pets.is_null: 

404 user.accepts_pets = None 

405 else: 

406 user.accepts_pets = request.accepts_pets.value 

407 

408 if request.HasField("pet_details"): 

409 if request.pet_details.is_null: 

410 user.pet_details = None 

411 else: 

412 user.pet_details = request.pet_details.value 

413 

414 if request.HasField("has_kids"): 

415 if request.has_kids.is_null: 

416 user.has_kids = None 

417 else: 

418 user.has_kids = request.has_kids.value 

419 

420 if request.HasField("accepts_kids"): 

421 if request.accepts_kids.is_null: 

422 user.accepts_kids = None 

423 else: 

424 user.accepts_kids = request.accepts_kids.value 

425 

426 if request.HasField("kid_details"): 

427 if request.kid_details.is_null: 

428 user.kid_details = None 

429 else: 

430 user.kid_details = request.kid_details.value 

431 

432 if request.HasField("has_housemates"): 

433 if request.has_housemates.is_null: 

434 user.has_housemates = None 

435 else: 

436 user.has_housemates = request.has_housemates.value 

437 

438 if request.HasField("housemate_details"): 

439 if request.housemate_details.is_null: 

440 user.housemate_details = None 

441 else: 

442 user.housemate_details = request.housemate_details.value 

443 

444 if request.HasField("wheelchair_accessible"): 

445 if request.wheelchair_accessible.is_null: 

446 user.wheelchair_accessible = None 

447 else: 

448 user.wheelchair_accessible = request.wheelchair_accessible.value 

449 

450 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

451 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

452 

453 if request.HasField("smokes_at_home"): 

454 if request.smokes_at_home.is_null: 

455 user.smokes_at_home = None 

456 else: 

457 user.smokes_at_home = request.smokes_at_home.value 

458 

459 if request.HasField("drinking_allowed"): 

460 if request.drinking_allowed.is_null: 

461 user.drinking_allowed = None 

462 else: 

463 user.drinking_allowed = request.drinking_allowed.value 

464 

465 if request.HasField("drinks_at_home"): 

466 if request.drinks_at_home.is_null: 

467 user.drinks_at_home = None 

468 else: 

469 user.drinks_at_home = request.drinks_at_home.value 

470 

471 if request.HasField("other_host_info"): 

472 if request.other_host_info.is_null: 

473 user.other_host_info = None 

474 else: 

475 user.other_host_info = request.other_host_info.value 

476 

477 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

478 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

479 

480 if request.HasField("sleeping_details"): 

481 if request.sleeping_details.is_null: 

482 user.sleeping_details = None 

483 else: 

484 user.sleeping_details = request.sleeping_details.value 

485 

486 if request.HasField("area"): 

487 if request.area.is_null: 

488 user.area = None 

489 else: 

490 user.area = request.area.value 

491 

492 if request.HasField("house_rules"): 

493 if request.house_rules.is_null: 

494 user.house_rules = None 

495 else: 

496 user.house_rules = request.house_rules.value 

497 

498 if request.HasField("parking"): 

499 if request.parking.is_null: 

500 user.parking = None 

501 else: 

502 user.parking = request.parking.value 

503 

504 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

505 user.parking_details = parkingdetails2sql[request.parking_details] 

506 

507 if request.HasField("camping_ok"): 

508 if request.camping_ok.is_null: 

509 user.camping_ok = None 

510 else: 

511 user.camping_ok = request.camping_ok.value 

512 

513 # save updates 

514 session.commit() 

515 

516 return empty_pb2.Empty() 

517 

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

519 rels = ( 

520 session.execute( 

521 select(FriendRelationship) 

522 .where_users_column_visible(context, FriendRelationship.from_user_id) 

523 .where_users_column_visible(context, FriendRelationship.to_user_id) 

524 .where( 

525 or_( 

526 FriendRelationship.from_user_id == context.user_id, 

527 FriendRelationship.to_user_id == context.user_id, 

528 ) 

529 ) 

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

531 ) 

532 .scalars() 

533 .all() 

534 ) 

535 return api_pb2.ListFriendsRes( 

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

537 ) 

538 

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

540 if context.user_id == request.user_id: 

541 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

542 

543 user = session.execute( 

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

545 ).scalar_one_or_none() 

546 

547 if not user: 

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

549 

550 q1 = ( 

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

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

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

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

555 ) 

556 

557 q2 = ( 

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

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

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

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

562 ) 

563 

564 q3 = ( 

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

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

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

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

569 ) 

570 

571 q4 = ( 

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

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

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

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

576 ) 

577 

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

579 

580 mutual_friends = ( 

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

582 ) 

583 

584 return api_pb2.ListMutualFriendsRes( 

585 mutual_friends=[ 

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

587 for mutual_friend in mutual_friends 

588 ] 

589 ) 

590 

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

592 if context.user_id == request.user_id: 

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

594 

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

596 to_user = session.execute( 

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

598 ).scalar_one_or_none() 

599 

600 if not to_user: 

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

602 

603 if ( 

604 session.execute( 

605 select(FriendRelationship) 

606 .where( 

607 or_( 

608 and_( 

609 FriendRelationship.from_user_id == context.user_id, 

610 FriendRelationship.to_user_id == request.user_id, 

611 ), 

612 and_( 

613 FriendRelationship.from_user_id == request.user_id, 

614 FriendRelationship.to_user_id == context.user_id, 

615 ), 

616 ) 

617 ) 

618 .where( 

619 or_( 

620 FriendRelationship.status == FriendStatus.accepted, 

621 FriendRelationship.status == FriendStatus.pending, 

622 ) 

623 ) 

624 ).scalar_one_or_none() 

625 is not None 

626 ): 

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

628 

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

630 

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

632 session.add(friend_relationship) 

633 session.flush() 

634 

635 notify( 

636 session, 

637 user_id=friend_relationship.to_user_id, 

638 topic_action="friend_request:create", 

639 key=friend_relationship.from_user_id, 

640 data=notification_data_pb2.FriendRequestCreate( 

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

642 ), 

643 ) 

644 

645 return empty_pb2.Empty() 

646 

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

648 # both sent and received 

649 sent_requests = ( 

650 session.execute( 

651 select(FriendRelationship) 

652 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

655 ) 

656 .scalars() 

657 .all() 

658 ) 

659 

660 received_requests = ( 

661 session.execute( 

662 select(FriendRelationship) 

663 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

666 ) 

667 .scalars() 

668 .all() 

669 ) 

670 

671 return api_pb2.ListFriendRequestsRes( 

672 sent=[ 

673 api_pb2.FriendRequest( 

674 friend_request_id=friend_request.id, 

675 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

676 user_id=friend_request.to_user.id, 

677 sent=True, 

678 ) 

679 for friend_request in sent_requests 

680 ], 

681 received=[ 

682 api_pb2.FriendRequest( 

683 friend_request_id=friend_request.id, 

684 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

685 user_id=friend_request.from_user.id, 

686 sent=False, 

687 ) 

688 for friend_request in received_requests 

689 ], 

690 ) 

691 

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

693 friend_request = session.execute( 

694 select(FriendRelationship) 

695 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

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

699 ).scalar_one_or_none() 

700 

701 if not friend_request: 

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

703 

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

705 friend_request.time_responded = func.now() 

706 

707 session.flush() 

708 

709 if friend_request.status == FriendStatus.accepted: 

710 notify( 

711 session, 

712 user_id=friend_request.from_user_id, 

713 topic_action="friend_request:accept", 

714 key=friend_request.to_user_id, 

715 data=notification_data_pb2.FriendRequestAccept( 

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

717 ), 

718 ) 

719 

720 return empty_pb2.Empty() 

721 

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

723 friend_request = session.execute( 

724 select(FriendRelationship) 

725 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

729 ).scalar_one_or_none() 

730 

731 if not friend_request: 

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

733 

734 friend_request.status = FriendStatus.cancelled 

735 friend_request.time_responded = func.now() 

736 

737 # note no notifications 

738 

739 session.commit() 

740 

741 return empty_pb2.Empty() 

742 

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

744 key = random_hex() 

745 

746 created = now() 

747 expiry = created + timedelta(minutes=20) 

748 

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

750 session.add(upload) 

751 session.commit() 

752 

753 req = media_pb2.UploadRequest( 

754 key=upload.key, 

755 type=media_pb2.UploadRequest.UploadType.IMAGE, 

756 created=Timestamp_from_datetime(upload.created), 

757 expiry=Timestamp_from_datetime(upload.expiry), 

758 max_width=2000, 

759 max_height=1600, 

760 ).SerializeToString() 

761 

762 data = b64encode(req) 

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

764 

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

766 

767 return api_pb2.InitiateMediaUploadRes( 

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

769 expiry=Timestamp_from_datetime(expiry), 

770 ) 

771 

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

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

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

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

776 if not badge: 

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

778 

779 badge_user_ids = ( 

780 session.execute( 

781 select(UserBadge.user_id) 

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

783 .where(UserBadge.user_id >= next_user_id) 

784 .order_by(UserBadge.user_id) 

785 .limit(page_size + 1) 

786 ) 

787 .scalars() 

788 .all() 

789 ) 

790 

791 return api_pb2.ListBadgeUsersRes( 

792 user_ids=badge_user_ids[:page_size], 

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

794 ) 

795 

796 

797def user_model_to_pb(db_user, session, context): 

798 num_references = session.execute( 

799 select(func.count()) 

800 .select_from(Reference) 

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

802 .where(User.is_visible) 

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

804 ).scalar_one() 

805 

806 # returns (lat, lng) 

807 # we put people without coords on null island 

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

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

810 

811 pending_friend_request = None 

812 if db_user.id == context.user_id: 

813 friends_status = api_pb2.User.FriendshipStatus.NA 

814 else: 

815 friend_relationship = session.execute( 

816 select(FriendRelationship) 

817 .where( 

818 or_( 

819 and_( 

820 FriendRelationship.from_user_id == context.user_id, 

821 FriendRelationship.to_user_id == db_user.id, 

822 ), 

823 and_( 

824 FriendRelationship.from_user_id == db_user.id, 

825 FriendRelationship.to_user_id == context.user_id, 

826 ), 

827 ) 

828 ) 

829 .where( 

830 or_( 

831 FriendRelationship.status == FriendStatus.accepted, 

832 FriendRelationship.status == FriendStatus.pending, 

833 ) 

834 ) 

835 ).scalar_one_or_none() 

836 

837 if friend_relationship: 

838 if friend_relationship.status == FriendStatus.accepted: 

839 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

840 else: 

841 friends_status = api_pb2.User.FriendshipStatus.PENDING 

842 if friend_relationship.from_user_id == context.user_id: 

843 # we sent it 

844 pending_friend_request = api_pb2.FriendRequest( 

845 friend_request_id=friend_relationship.id, 

846 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

847 user_id=friend_relationship.to_user.id, 

848 sent=True, 

849 ) 

850 else: 

851 # we received it 

852 pending_friend_request = api_pb2.FriendRequest( 

853 friend_request_id=friend_relationship.id, 

854 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

855 user_id=friend_relationship.from_user.id, 

856 sent=False, 

857 ) 

858 else: 

859 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

860 

861 verification_score = 0.0 

862 if db_user.phone_verification_verified: 

863 verification_score += 1.0 * db_user.phone_is_verified 

864 

865 user = api_pb2.User( 

866 user_id=db_user.id, 

867 username=db_user.username, 

868 name=db_user.name, 

869 city=db_user.city, 

870 hometown=db_user.hometown, 

871 timezone=db_user.timezone, 

872 lat=lat, 

873 lng=lng, 

874 radius=db_user.geom_radius, 

875 verification=verification_score, 

876 community_standing=db_user.community_standing, 

877 num_references=num_references, 

878 gender=db_user.gender, 

879 pronouns=db_user.pronouns, 

880 age=int(db_user.age), 

881 joined=Timestamp_from_datetime(db_user.display_joined), 

882 last_active=Timestamp_from_datetime(db_user.display_last_active), 

883 hosting_status=hostingstatus2api[db_user.hosting_status], 

884 meetup_status=meetupstatus2api[db_user.meetup_status], 

885 occupation=db_user.occupation, 

886 education=db_user.education, 

887 about_me=db_user.about_me, 

888 things_i_like=db_user.things_i_like, 

889 about_place=db_user.about_place, 

890 language_abilities=[ 

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

892 for ability in db_user.language_abilities 

893 ], 

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

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

896 additional_information=db_user.additional_information, 

897 friends=friends_status, 

898 pending_friend_request=pending_friend_request, 

899 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

900 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

901 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

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

905 **get_strong_verification_fields(session, db_user), 

906 ) 

907 

908 if db_user.max_guests is not None: 

909 user.max_guests.value = db_user.max_guests 

910 

911 if db_user.last_minute is not None: 

912 user.last_minute.value = db_user.last_minute 

913 

914 if db_user.has_pets is not None: 

915 user.has_pets.value = db_user.has_pets 

916 

917 if db_user.accepts_pets is not None: 

918 user.accepts_pets.value = db_user.accepts_pets 

919 

920 if db_user.pet_details is not None: 

921 user.pet_details.value = db_user.pet_details 

922 

923 if db_user.has_kids is not None: 

924 user.has_kids.value = db_user.has_kids 

925 

926 if db_user.accepts_kids is not None: 

927 user.accepts_kids.value = db_user.accepts_kids 

928 

929 if db_user.kid_details is not None: 

930 user.kid_details.value = db_user.kid_details 

931 

932 if db_user.has_housemates is not None: 

933 user.has_housemates.value = db_user.has_housemates 

934 

935 if db_user.housemate_details is not None: 

936 user.housemate_details.value = db_user.housemate_details 

937 

938 if db_user.wheelchair_accessible is not None: 

939 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

940 

941 if db_user.smokes_at_home is not None: 

942 user.smokes_at_home.value = db_user.smokes_at_home 

943 

944 if db_user.drinking_allowed is not None: 

945 user.drinking_allowed.value = db_user.drinking_allowed 

946 

947 if db_user.drinks_at_home is not None: 

948 user.drinks_at_home.value = db_user.drinks_at_home 

949 

950 if db_user.other_host_info is not None: 

951 user.other_host_info.value = db_user.other_host_info 

952 

953 if db_user.sleeping_details is not None: 

954 user.sleeping_details.value = db_user.sleeping_details 

955 

956 if db_user.area is not None: 

957 user.area.value = db_user.area 

958 

959 if db_user.house_rules is not None: 

960 user.house_rules.value = db_user.house_rules 

961 

962 if db_user.parking is not None: 

963 user.parking.value = db_user.parking 

964 

965 if db_user.camping_ok is not None: 

966 user.camping_ok.value = db_user.camping_ok 

967 

968 return user 

969 

970 

971def lite_user_to_pb(lite_user): 

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

973 

974 return api_pb2.LiteUser( 

975 user_id=lite_user.id, 

976 username=lite_user.username, 

977 name=lite_user.name, 

978 city=lite_user.city, 

979 age=int(lite_user.age), 

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

981 if lite_user.avatar_filename 

982 else None, 

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

984 if lite_user.avatar_filename 

985 else None, 

986 lat=lat, 

987 lng=lng, 

988 radius=lite_user.radius, 

989 has_strong_verification=lite_user.has_strong_verification, 

990 )