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

369 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-03-11 15:27 +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} 

100 

101sleepingarrangement2api = { 

102 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

103 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

104 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

105 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

106} 

107 

108parkingdetails2sql = { 

109 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

110 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

111 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

112 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

113 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

114} 

115 

116parkingdetails2api = { 

117 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

118 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

119 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

120 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

121 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

122} 

123 

124fluency2sql = { 

125 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

129} 

130 

131fluency2api = { 

132 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

136} 

137 

138 

139class API(api_pb2_grpc.APIServicer): 

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

141 # auth ought to make sure the user exists 

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

143 

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

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

146 message_2 = aliased(Message) 

147 unseen_sent_host_request_count = session.execute( 

148 select(func.count()) 

149 .select_from(Message) 

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

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

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

153 .where_users_column_visible(context, HostRequest.host_user_id) 

154 .where(message_2.id == None) 

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

156 ).scalar_one() 

157 

158 unseen_received_host_request_count = session.execute( 

159 select(func.count()) 

160 .select_from(Message) 

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

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

163 .where_users_column_visible(context, HostRequest.surfer_user_id) 

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

165 .where(message_2.id == None) 

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

167 ).scalar_one() 

168 

169 unseen_message_count = session.execute( 

170 select(func.count()) 

171 .select_from(Message) 

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

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

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

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

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

177 ).scalar_one() 

178 

179 pending_friend_request_count = session.execute( 

180 select(func.count()) 

181 .select_from(FriendRelationship) 

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

183 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

185 ).scalar_one() 

186 

187 return api_pb2.PingRes( 

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

189 unseen_message_count=unseen_message_count, 

190 unseen_sent_host_request_count=unseen_sent_host_request_count, 

191 unseen_received_host_request_count=unseen_received_host_request_count, 

192 pending_friend_request_count=pending_friend_request_count, 

193 ) 

194 

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

196 user = session.execute( 

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

198 ).scalar_one_or_none() 

199 

200 if not user: 

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

202 

203 return user_model_to_pb(user, session, context) 

204 

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

206 lite_user = session.execute( 

207 select(lite_users) 

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

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

210 ).one_or_none() 

211 

212 if not lite_user: 

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

214 

215 return lite_user_to_pb(lite_user) 

216 

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

218 if len(request.users) > MAX_USERS_PER_QUERY: 

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

220 

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

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

223 

224 users = session.execute( 

225 select(lite_users) 

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

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

228 ).all() 

229 

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

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

232 

233 res = api_pb2.GetLiteUsersRes() 

234 

235 for user in request.users: 

236 lite_user = None 

237 if user in users_by_id: 

238 lite_user = users_by_id[user] 

239 elif user in users_by_username: 

240 lite_user = users_by_username[user] 

241 

242 res.responses.append( 

243 api_pb2.LiteUserRes( 

244 query=user, 

245 not_found=lite_user is None, 

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

247 ) 

248 ) 

249 

250 return res 

251 

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

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

254 

255 if request.HasField("name"): 

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

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

258 user.name = request.name.value 

259 

260 if request.HasField("city"): 

261 user.city = request.city.value 

262 

263 if request.HasField("hometown"): 

264 if request.hometown.is_null: 

265 user.hometown = None 

266 else: 

267 user.hometown = request.hometown.value 

268 

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

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

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

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

273 

274 if request.HasField("radius"): 

275 user.geom_radius = request.radius.value 

276 

277 if request.HasField("avatar_key"): 

278 if request.avatar_key.is_null: 

279 user.avatar_key = None 

280 else: 

281 user.avatar_key = request.avatar_key.value 

282 

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

284 # user.gender = request.gender.value 

285 

286 if request.HasField("pronouns"): 

287 if request.pronouns.is_null: 

288 user.pronouns = None 

289 else: 

290 user.pronouns = request.pronouns.value 

291 

292 if request.HasField("occupation"): 

293 if request.occupation.is_null: 

294 user.occupation = None 

295 else: 

296 user.occupation = request.occupation.value 

297 

298 if request.HasField("education"): 

299 if request.education.is_null: 

300 user.education = None 

301 else: 

302 user.education = request.education.value 

303 

304 if request.HasField("about_me"): 

305 if request.about_me.is_null: 

306 user.about_me = None 

307 else: 

308 user.about_me = request.about_me.value 

309 

310 if request.HasField("things_i_like"): 

311 if request.things_i_like.is_null: 

312 user.things_i_like = None 

313 else: 

314 user.things_i_like = request.things_i_like.value 

315 

316 if request.HasField("about_place"): 

317 if request.about_place.is_null: 

318 user.about_place = None 

319 else: 

320 user.about_place = request.about_place.value 

321 

322 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

325 user.hosting_status = hostingstatus2sql[request.hosting_status] 

326 

327 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

330 user.meetup_status = meetupstatus2sql[request.meetup_status] 

331 

332 if request.HasField("language_abilities"): 

333 # delete all existing abilities 

334 for ability in user.language_abilities: 

335 session.delete(ability) 

336 session.flush() 

337 

338 # add the new ones 

339 for language_ability in request.language_abilities.value: 

340 if not language_is_allowed(language_ability.code): 

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

342 session.add( 

343 LanguageAbility( 

344 user=user, 

345 language_code=language_ability.code, 

346 fluency=fluency2sql[language_ability.fluency], 

347 ) 

348 ) 

349 

350 if request.HasField("regions_visited"): 

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

352 

353 for region in request.regions_visited.value: 

354 if not region_is_allowed(region): 

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

356 session.add( 

357 RegionVisited( 

358 user_id=user.id, 

359 region_code=region, 

360 ) 

361 ) 

362 

363 if request.HasField("regions_lived"): 

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

365 

366 for region in request.regions_lived.value: 

367 if not region_is_allowed(region): 

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

369 session.add( 

370 RegionLived( 

371 user_id=user.id, 

372 region_code=region, 

373 ) 

374 ) 

375 

376 if request.HasField("additional_information"): 

377 if request.additional_information.is_null: 

378 user.additional_information = None 

379 else: 

380 user.additional_information = request.additional_information.value 

381 

382 if request.HasField("max_guests"): 

383 if request.max_guests.is_null: 

384 user.max_guests = None 

385 else: 

386 user.max_guests = request.max_guests.value 

387 

388 if request.HasField("last_minute"): 

389 if request.last_minute.is_null: 

390 user.last_minute = None 

391 else: 

392 user.last_minute = request.last_minute.value 

393 

394 if request.HasField("has_pets"): 

395 if request.has_pets.is_null: 

396 user.has_pets = None 

397 else: 

398 user.has_pets = request.has_pets.value 

399 

400 if request.HasField("accepts_pets"): 

401 if request.accepts_pets.is_null: 

402 user.accepts_pets = None 

403 else: 

404 user.accepts_pets = request.accepts_pets.value 

405 

406 if request.HasField("pet_details"): 

407 if request.pet_details.is_null: 

408 user.pet_details = None 

409 else: 

410 user.pet_details = request.pet_details.value 

411 

412 if request.HasField("has_kids"): 

413 if request.has_kids.is_null: 

414 user.has_kids = None 

415 else: 

416 user.has_kids = request.has_kids.value 

417 

418 if request.HasField("accepts_kids"): 

419 if request.accepts_kids.is_null: 

420 user.accepts_kids = None 

421 else: 

422 user.accepts_kids = request.accepts_kids.value 

423 

424 if request.HasField("kid_details"): 

425 if request.kid_details.is_null: 

426 user.kid_details = None 

427 else: 

428 user.kid_details = request.kid_details.value 

429 

430 if request.HasField("has_housemates"): 

431 if request.has_housemates.is_null: 

432 user.has_housemates = None 

433 else: 

434 user.has_housemates = request.has_housemates.value 

435 

436 if request.HasField("housemate_details"): 

437 if request.housemate_details.is_null: 

438 user.housemate_details = None 

439 else: 

440 user.housemate_details = request.housemate_details.value 

441 

442 if request.HasField("wheelchair_accessible"): 

443 if request.wheelchair_accessible.is_null: 

444 user.wheelchair_accessible = None 

445 else: 

446 user.wheelchair_accessible = request.wheelchair_accessible.value 

447 

448 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

449 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

450 

451 if request.HasField("smokes_at_home"): 

452 if request.smokes_at_home.is_null: 

453 user.smokes_at_home = None 

454 else: 

455 user.smokes_at_home = request.smokes_at_home.value 

456 

457 if request.HasField("drinking_allowed"): 

458 if request.drinking_allowed.is_null: 

459 user.drinking_allowed = None 

460 else: 

461 user.drinking_allowed = request.drinking_allowed.value 

462 

463 if request.HasField("drinks_at_home"): 

464 if request.drinks_at_home.is_null: 

465 user.drinks_at_home = None 

466 else: 

467 user.drinks_at_home = request.drinks_at_home.value 

468 

469 if request.HasField("other_host_info"): 

470 if request.other_host_info.is_null: 

471 user.other_host_info = None 

472 else: 

473 user.other_host_info = request.other_host_info.value 

474 

475 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

476 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

477 

478 if request.HasField("sleeping_details"): 

479 if request.sleeping_details.is_null: 

480 user.sleeping_details = None 

481 else: 

482 user.sleeping_details = request.sleeping_details.value 

483 

484 if request.HasField("area"): 

485 if request.area.is_null: 

486 user.area = None 

487 else: 

488 user.area = request.area.value 

489 

490 if request.HasField("house_rules"): 

491 if request.house_rules.is_null: 

492 user.house_rules = None 

493 else: 

494 user.house_rules = request.house_rules.value 

495 

496 if request.HasField("parking"): 

497 if request.parking.is_null: 

498 user.parking = None 

499 else: 

500 user.parking = request.parking.value 

501 

502 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

503 user.parking_details = parkingdetails2sql[request.parking_details] 

504 

505 if request.HasField("camping_ok"): 

506 if request.camping_ok.is_null: 

507 user.camping_ok = None 

508 else: 

509 user.camping_ok = request.camping_ok.value 

510 

511 # save updates 

512 session.commit() 

513 

514 return empty_pb2.Empty() 

515 

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

517 rels = ( 

518 session.execute( 

519 select(FriendRelationship) 

520 .where_users_column_visible(context, FriendRelationship.from_user_id) 

521 .where_users_column_visible(context, FriendRelationship.to_user_id) 

522 .where( 

523 or_( 

524 FriendRelationship.from_user_id == context.user_id, 

525 FriendRelationship.to_user_id == context.user_id, 

526 ) 

527 ) 

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

529 ) 

530 .scalars() 

531 .all() 

532 ) 

533 return api_pb2.ListFriendsRes( 

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

535 ) 

536 

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

538 if context.user_id == request.user_id: 

539 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

540 

541 user = session.execute( 

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

543 ).scalar_one_or_none() 

544 

545 if not user: 

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

547 

548 q1 = ( 

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

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

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

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

553 ) 

554 

555 q2 = ( 

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

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

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

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

560 ) 

561 

562 q3 = ( 

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

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

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

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

567 ) 

568 

569 q4 = ( 

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

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

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

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

574 ) 

575 

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

577 

578 mutual_friends = ( 

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

580 ) 

581 

582 return api_pb2.ListMutualFriendsRes( 

583 mutual_friends=[ 

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

585 for mutual_friend in mutual_friends 

586 ] 

587 ) 

588 

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

590 if context.user_id == request.user_id: 

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

592 

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

594 to_user = session.execute( 

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

596 ).scalar_one_or_none() 

597 

598 if not to_user: 

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

600 

601 if ( 

602 session.execute( 

603 select(FriendRelationship) 

604 .where( 

605 or_( 

606 and_( 

607 FriendRelationship.from_user_id == context.user_id, 

608 FriendRelationship.to_user_id == request.user_id, 

609 ), 

610 and_( 

611 FriendRelationship.from_user_id == request.user_id, 

612 FriendRelationship.to_user_id == context.user_id, 

613 ), 

614 ) 

615 ) 

616 .where( 

617 or_( 

618 FriendRelationship.status == FriendStatus.accepted, 

619 FriendRelationship.status == FriendStatus.pending, 

620 ) 

621 ) 

622 ).scalar_one_or_none() 

623 is not None 

624 ): 

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

626 

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

628 

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

630 session.add(friend_relationship) 

631 session.flush() 

632 

633 notify( 

634 session, 

635 user_id=friend_relationship.to_user_id, 

636 topic_action="friend_request:create", 

637 key=friend_relationship.from_user_id, 

638 data=notification_data_pb2.FriendRequestCreate( 

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

640 ), 

641 ) 

642 

643 return empty_pb2.Empty() 

644 

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

646 # both sent and received 

647 sent_requests = ( 

648 session.execute( 

649 select(FriendRelationship) 

650 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

653 ) 

654 .scalars() 

655 .all() 

656 ) 

657 

658 received_requests = ( 

659 session.execute( 

660 select(FriendRelationship) 

661 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

664 ) 

665 .scalars() 

666 .all() 

667 ) 

668 

669 return api_pb2.ListFriendRequestsRes( 

670 sent=[ 

671 api_pb2.FriendRequest( 

672 friend_request_id=friend_request.id, 

673 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

674 user_id=friend_request.to_user.id, 

675 sent=True, 

676 ) 

677 for friend_request in sent_requests 

678 ], 

679 received=[ 

680 api_pb2.FriendRequest( 

681 friend_request_id=friend_request.id, 

682 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

683 user_id=friend_request.from_user.id, 

684 sent=False, 

685 ) 

686 for friend_request in received_requests 

687 ], 

688 ) 

689 

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

691 friend_request = session.execute( 

692 select(FriendRelationship) 

693 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

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

697 ).scalar_one_or_none() 

698 

699 if not friend_request: 

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

701 

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

703 friend_request.time_responded = func.now() 

704 

705 session.flush() 

706 

707 if friend_request.status == FriendStatus.accepted: 

708 notify( 

709 session, 

710 user_id=friend_request.from_user_id, 

711 topic_action="friend_request:accept", 

712 key=friend_request.to_user_id, 

713 data=notification_data_pb2.FriendRequestAccept( 

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

715 ), 

716 ) 

717 

718 return empty_pb2.Empty() 

719 

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

721 friend_request = session.execute( 

722 select(FriendRelationship) 

723 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

727 ).scalar_one_or_none() 

728 

729 if not friend_request: 

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

731 

732 friend_request.status = FriendStatus.cancelled 

733 friend_request.time_responded = func.now() 

734 

735 # note no notifications 

736 

737 session.commit() 

738 

739 return empty_pb2.Empty() 

740 

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

742 key = random_hex() 

743 

744 created = now() 

745 expiry = created + timedelta(minutes=20) 

746 

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

748 session.add(upload) 

749 session.commit() 

750 

751 req = media_pb2.UploadRequest( 

752 key=upload.key, 

753 type=media_pb2.UploadRequest.UploadType.IMAGE, 

754 created=Timestamp_from_datetime(upload.created), 

755 expiry=Timestamp_from_datetime(upload.expiry), 

756 max_width=2000, 

757 max_height=1600, 

758 ).SerializeToString() 

759 

760 data = b64encode(req) 

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

762 

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

764 

765 return api_pb2.InitiateMediaUploadRes( 

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

767 expiry=Timestamp_from_datetime(expiry), 

768 ) 

769 

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

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

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

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

774 if not badge: 

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

776 

777 badge_user_ids = ( 

778 session.execute( 

779 select(UserBadge.user_id) 

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

781 .where(UserBadge.user_id >= next_user_id) 

782 .order_by(UserBadge.user_id) 

783 .limit(page_size + 1) 

784 ) 

785 .scalars() 

786 .all() 

787 ) 

788 

789 return api_pb2.ListBadgeUsersRes( 

790 user_ids=badge_user_ids[:page_size], 

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

792 ) 

793 

794 

795def user_model_to_pb(db_user, session, context): 

796 num_references = session.execute( 

797 select(func.count()) 

798 .select_from(Reference) 

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

800 .where(User.is_visible) 

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

802 ).scalar_one() 

803 

804 # returns (lat, lng) 

805 # we put people without coords on null island 

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

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

808 

809 pending_friend_request = None 

810 if db_user.id == context.user_id: 

811 friends_status = api_pb2.User.FriendshipStatus.NA 

812 else: 

813 friend_relationship = session.execute( 

814 select(FriendRelationship) 

815 .where( 

816 or_( 

817 and_( 

818 FriendRelationship.from_user_id == context.user_id, 

819 FriendRelationship.to_user_id == db_user.id, 

820 ), 

821 and_( 

822 FriendRelationship.from_user_id == db_user.id, 

823 FriendRelationship.to_user_id == context.user_id, 

824 ), 

825 ) 

826 ) 

827 .where( 

828 or_( 

829 FriendRelationship.status == FriendStatus.accepted, 

830 FriendRelationship.status == FriendStatus.pending, 

831 ) 

832 ) 

833 ).scalar_one_or_none() 

834 

835 if friend_relationship: 

836 if friend_relationship.status == FriendStatus.accepted: 

837 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

838 else: 

839 friends_status = api_pb2.User.FriendshipStatus.PENDING 

840 if friend_relationship.from_user_id == context.user_id: 

841 # we sent it 

842 pending_friend_request = api_pb2.FriendRequest( 

843 friend_request_id=friend_relationship.id, 

844 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

845 user_id=friend_relationship.to_user.id, 

846 sent=True, 

847 ) 

848 else: 

849 # we received it 

850 pending_friend_request = api_pb2.FriendRequest( 

851 friend_request_id=friend_relationship.id, 

852 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

853 user_id=friend_relationship.from_user.id, 

854 sent=False, 

855 ) 

856 else: 

857 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

858 

859 verification_score = 0.0 

860 if db_user.phone_verification_verified: 

861 verification_score += 1.0 * db_user.phone_is_verified 

862 

863 user = api_pb2.User( 

864 user_id=db_user.id, 

865 username=db_user.username, 

866 name=db_user.name, 

867 city=db_user.city, 

868 hometown=db_user.hometown, 

869 timezone=db_user.timezone, 

870 lat=lat, 

871 lng=lng, 

872 radius=db_user.geom_radius, 

873 verification=verification_score, 

874 community_standing=db_user.community_standing, 

875 num_references=num_references, 

876 gender=db_user.gender, 

877 pronouns=db_user.pronouns, 

878 age=int(db_user.age), 

879 joined=Timestamp_from_datetime(db_user.display_joined), 

880 last_active=Timestamp_from_datetime(db_user.display_last_active), 

881 hosting_status=hostingstatus2api[db_user.hosting_status], 

882 meetup_status=meetupstatus2api[db_user.meetup_status], 

883 occupation=db_user.occupation, 

884 education=db_user.education, 

885 about_me=db_user.about_me, 

886 things_i_like=db_user.things_i_like, 

887 about_place=db_user.about_place, 

888 language_abilities=[ 

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

890 for ability in db_user.language_abilities 

891 ], 

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

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

894 additional_information=db_user.additional_information, 

895 friends=friends_status, 

896 pending_friend_request=pending_friend_request, 

897 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

898 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

899 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

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

903 **get_strong_verification_fields(session, db_user), 

904 ) 

905 

906 if db_user.max_guests is not None: 

907 user.max_guests.value = db_user.max_guests 

908 

909 if db_user.last_minute is not None: 

910 user.last_minute.value = db_user.last_minute 

911 

912 if db_user.has_pets is not None: 

913 user.has_pets.value = db_user.has_pets 

914 

915 if db_user.accepts_pets is not None: 

916 user.accepts_pets.value = db_user.accepts_pets 

917 

918 if db_user.pet_details is not None: 

919 user.pet_details.value = db_user.pet_details 

920 

921 if db_user.has_kids is not None: 

922 user.has_kids.value = db_user.has_kids 

923 

924 if db_user.accepts_kids is not None: 

925 user.accepts_kids.value = db_user.accepts_kids 

926 

927 if db_user.kid_details is not None: 

928 user.kid_details.value = db_user.kid_details 

929 

930 if db_user.has_housemates is not None: 

931 user.has_housemates.value = db_user.has_housemates 

932 

933 if db_user.housemate_details is not None: 

934 user.housemate_details.value = db_user.housemate_details 

935 

936 if db_user.wheelchair_accessible is not None: 

937 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

938 

939 if db_user.smokes_at_home is not None: 

940 user.smokes_at_home.value = db_user.smokes_at_home 

941 

942 if db_user.drinking_allowed is not None: 

943 user.drinking_allowed.value = db_user.drinking_allowed 

944 

945 if db_user.drinks_at_home is not None: 

946 user.drinks_at_home.value = db_user.drinks_at_home 

947 

948 if db_user.other_host_info is not None: 

949 user.other_host_info.value = db_user.other_host_info 

950 

951 if db_user.sleeping_details is not None: 

952 user.sleeping_details.value = db_user.sleeping_details 

953 

954 if db_user.area is not None: 

955 user.area.value = db_user.area 

956 

957 if db_user.house_rules is not None: 

958 user.house_rules.value = db_user.house_rules 

959 

960 if db_user.parking is not None: 

961 user.parking.value = db_user.parking 

962 

963 if db_user.camping_ok is not None: 

964 user.camping_ok.value = db_user.camping_ok 

965 

966 return user 

967 

968 

969def lite_user_to_pb(lite_user): 

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

971 

972 return api_pb2.LiteUser( 

973 user_id=lite_user.id, 

974 username=lite_user.username, 

975 name=lite_user.name, 

976 city=lite_user.city, 

977 age=int(lite_user.age), 

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

979 if lite_user.avatar_filename 

980 else None, 

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

982 if lite_user.avatar_filename 

983 else None, 

984 lat=lat, 

985 lng=lng, 

986 radius=lite_user.radius, 

987 has_strong_verification=lite_user.has_strong_verification, 

988 )