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

361 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-10-15 13: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) 

32from couchers.notifications.notify import notify 

33from couchers.resources import language_is_allowed, region_is_allowed 

34from couchers.servicers.account import get_strong_verification_fields 

35from couchers.sql import couchers_select as select 

36from couchers.sql import is_valid_user_id, is_valid_username 

37from couchers.utils import Timestamp_from_datetime, create_coordinate, is_valid_name, now 

38from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2 

39 

40MAX_USERS_PER_QUERY = 200 

41 

42hostingstatus2sql = { 

43 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

44 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

45 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

46 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

47} 

48 

49hostingstatus2api = { 

50 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

51 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

52 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

53 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

54} 

55 

56meetupstatus2sql = { 

57 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

58 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

59 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

60 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

61} 

62 

63meetupstatus2api = { 

64 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

65 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

66 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

67 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

68} 

69 

70smokinglocation2sql = { 

71 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

72 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

73 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

74 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

75 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

76} 

77 

78smokinglocation2api = { 

79 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

80 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

81 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

82 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

83 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

84} 

85 

86sleepingarrangement2sql = { 

87 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

88 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

89 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

90 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

91 api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE: SleepingArrangement.shared_space, 

92} 

93 

94sleepingarrangement2api = { 

95 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

96 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

97 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

98 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

99 SleepingArrangement.shared_space: api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE, 

100} 

101 

102parkingdetails2sql = { 

103 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

104 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

105 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

106 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

107 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

108} 

109 

110parkingdetails2api = { 

111 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

112 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

113 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

114 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

115 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

116} 

117 

118fluency2sql = { 

119 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

123} 

124 

125fluency2api = { 

126 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

130} 

131 

132 

133class API(api_pb2_grpc.APIServicer): 

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

135 # auth ought to make sure the user exists 

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

137 

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

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

140 message_2 = aliased(Message) 

141 unseen_sent_host_request_count = session.execute( 

142 select(func.count()) 

143 .select_from(Message) 

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

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

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

147 .where_users_column_visible(context, HostRequest.host_user_id) 

148 .where(message_2.id == None) 

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

150 ).scalar_one() 

151 

152 unseen_received_host_request_count = session.execute( 

153 select(func.count()) 

154 .select_from(Message) 

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

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

157 .where_users_column_visible(context, HostRequest.surfer_user_id) 

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

159 .where(message_2.id == None) 

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

161 ).scalar_one() 

162 

163 unseen_message_count = session.execute( 

164 select(func.count()) 

165 .select_from(Message) 

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

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

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

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

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

171 ).scalar_one() 

172 

173 pending_friend_request_count = session.execute( 

174 select(func.count()) 

175 .select_from(FriendRelationship) 

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

177 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

179 ).scalar_one() 

180 

181 return api_pb2.PingRes( 

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

183 unseen_message_count=unseen_message_count, 

184 unseen_sent_host_request_count=unseen_sent_host_request_count, 

185 unseen_received_host_request_count=unseen_received_host_request_count, 

186 pending_friend_request_count=pending_friend_request_count, 

187 ) 

188 

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

190 user = session.execute( 

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

192 ).scalar_one_or_none() 

193 

194 if not user: 

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

196 

197 return user_model_to_pb(user, session, context) 

198 

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

200 lite_user = session.execute( 

201 select(lite_users) 

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

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

204 ).one_or_none() 

205 

206 if not lite_user: 

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

208 

209 return api_pb2.LiteUser( 

210 user_id=lite_user.id, 

211 username=lite_user.username, 

212 name=lite_user.name, 

213 city=lite_user.city, 

214 age=int(lite_user.age), 

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

216 if lite_user.avatar_filename 

217 else None, 

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

219 if lite_user.avatar_filename 

220 else None, 

221 lat=lite_user.lat, 

222 lng=lite_user.lng, 

223 radius=lite_user.radius, 

224 ) 

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=api_pb2.LiteUser( 

256 user_id=lite_user.id, 

257 username=lite_user.username, 

258 name=lite_user.name, 

259 city=lite_user.city, 

260 age=int(lite_user.age), 

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

262 if lite_user.avatar_filename 

263 else None, 

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

265 if lite_user.avatar_filename 

266 else None, 

267 lat=lite_user.lat, 

268 lng=lite_user.lng, 

269 radius=lite_user.radius, 

270 ) 

271 if lite_user 

272 else None, 

273 ) 

274 ) 

275 

276 return res 

277 

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

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

280 

281 if request.HasField("name"): 

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

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

284 user.name = request.name.value 

285 

286 if request.HasField("city"): 

287 user.city = request.city.value 

288 

289 if request.HasField("hometown"): 

290 if request.hometown.is_null: 

291 user.hometown = None 

292 else: 

293 user.hometown = request.hometown.value 

294 

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

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

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

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

299 

300 if request.HasField("radius"): 

301 user.geom_radius = request.radius.value 

302 

303 if request.HasField("avatar_key"): 

304 if request.avatar_key.is_null: 

305 user.avatar_key = None 

306 else: 

307 user.avatar_key = request.avatar_key.value 

308 

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

310 # user.gender = request.gender.value 

311 

312 if request.HasField("pronouns"): 

313 if request.pronouns.is_null: 

314 user.pronouns = None 

315 else: 

316 user.pronouns = request.pronouns.value 

317 

318 if request.HasField("occupation"): 

319 if request.occupation.is_null: 

320 user.occupation = None 

321 else: 

322 user.occupation = request.occupation.value 

323 

324 if request.HasField("education"): 

325 if request.education.is_null: 

326 user.education = None 

327 else: 

328 user.education = request.education.value 

329 

330 if request.HasField("about_me"): 

331 if request.about_me.is_null: 

332 user.about_me = None 

333 else: 

334 user.about_me = request.about_me.value 

335 

336 if request.HasField("my_travels"): 

337 if request.my_travels.is_null: 

338 user.my_travels = None 

339 else: 

340 user.my_travels = request.my_travels.value 

341 

342 if request.HasField("things_i_like"): 

343 if request.things_i_like.is_null: 

344 user.things_i_like = None 

345 else: 

346 user.things_i_like = request.things_i_like.value 

347 

348 if request.HasField("about_place"): 

349 if request.about_place.is_null: 

350 user.about_place = None 

351 else: 

352 user.about_place = request.about_place.value 

353 

354 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

357 user.hosting_status = hostingstatus2sql[request.hosting_status] 

358 

359 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

362 user.meetup_status = meetupstatus2sql[request.meetup_status] 

363 

364 if request.HasField("language_abilities"): 

365 # delete all existing abilities 

366 for ability in user.language_abilities: 

367 session.delete(ability) 

368 session.flush() 

369 

370 # add the new ones 

371 for language_ability in request.language_abilities.value: 

372 if not language_is_allowed(language_ability.code): 

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

374 session.add( 

375 LanguageAbility( 

376 user=user, 

377 language_code=language_ability.code, 

378 fluency=fluency2sql[language_ability.fluency], 

379 ) 

380 ) 

381 

382 if request.HasField("regions_visited"): 

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

384 

385 for region in request.regions_visited.value: 

386 if not region_is_allowed(region): 

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

388 session.add( 

389 RegionVisited( 

390 user_id=user.id, 

391 region_code=region, 

392 ) 

393 ) 

394 

395 if request.HasField("regions_lived"): 

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

397 

398 for region in request.regions_lived.value: 

399 if not region_is_allowed(region): 

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

401 session.add( 

402 RegionLived( 

403 user_id=user.id, 

404 region_code=region, 

405 ) 

406 ) 

407 

408 if request.HasField("additional_information"): 

409 if request.additional_information.is_null: 

410 user.additional_information = None 

411 else: 

412 user.additional_information = request.additional_information.value 

413 

414 if request.HasField("max_guests"): 

415 if request.max_guests.is_null: 

416 user.max_guests = None 

417 else: 

418 user.max_guests = request.max_guests.value 

419 

420 if request.HasField("last_minute"): 

421 if request.last_minute.is_null: 

422 user.last_minute = None 

423 else: 

424 user.last_minute = request.last_minute.value 

425 

426 if request.HasField("has_pets"): 

427 if request.has_pets.is_null: 

428 user.has_pets = None 

429 else: 

430 user.has_pets = request.has_pets.value 

431 

432 if request.HasField("accepts_pets"): 

433 if request.accepts_pets.is_null: 

434 user.accepts_pets = None 

435 else: 

436 user.accepts_pets = request.accepts_pets.value 

437 

438 if request.HasField("pet_details"): 

439 if request.pet_details.is_null: 

440 user.pet_details = None 

441 else: 

442 user.pet_details = request.pet_details.value 

443 

444 if request.HasField("has_kids"): 

445 if request.has_kids.is_null: 

446 user.has_kids = None 

447 else: 

448 user.has_kids = request.has_kids.value 

449 

450 if request.HasField("accepts_kids"): 

451 if request.accepts_kids.is_null: 

452 user.accepts_kids = None 

453 else: 

454 user.accepts_kids = request.accepts_kids.value 

455 

456 if request.HasField("kid_details"): 

457 if request.kid_details.is_null: 

458 user.kid_details = None 

459 else: 

460 user.kid_details = request.kid_details.value 

461 

462 if request.HasField("has_housemates"): 

463 if request.has_housemates.is_null: 

464 user.has_housemates = None 

465 else: 

466 user.has_housemates = request.has_housemates.value 

467 

468 if request.HasField("housemate_details"): 

469 if request.housemate_details.is_null: 

470 user.housemate_details = None 

471 else: 

472 user.housemate_details = request.housemate_details.value 

473 

474 if request.HasField("wheelchair_accessible"): 

475 if request.wheelchair_accessible.is_null: 

476 user.wheelchair_accessible = None 

477 else: 

478 user.wheelchair_accessible = request.wheelchair_accessible.value 

479 

480 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

481 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

482 

483 if request.HasField("smokes_at_home"): 

484 if request.smokes_at_home.is_null: 

485 user.smokes_at_home = None 

486 else: 

487 user.smokes_at_home = request.smokes_at_home.value 

488 

489 if request.HasField("drinking_allowed"): 

490 if request.drinking_allowed.is_null: 

491 user.drinking_allowed = None 

492 else: 

493 user.drinking_allowed = request.drinking_allowed.value 

494 

495 if request.HasField("drinks_at_home"): 

496 if request.drinks_at_home.is_null: 

497 user.drinks_at_home = None 

498 else: 

499 user.drinks_at_home = request.drinks_at_home.value 

500 

501 if request.HasField("other_host_info"): 

502 if request.other_host_info.is_null: 

503 user.other_host_info = None 

504 else: 

505 user.other_host_info = request.other_host_info.value 

506 

507 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

508 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

509 

510 if request.HasField("sleeping_details"): 

511 if request.sleeping_details.is_null: 

512 user.sleeping_details = None 

513 else: 

514 user.sleeping_details = request.sleeping_details.value 

515 

516 if request.HasField("area"): 

517 if request.area.is_null: 

518 user.area = None 

519 else: 

520 user.area = request.area.value 

521 

522 if request.HasField("house_rules"): 

523 if request.house_rules.is_null: 

524 user.house_rules = None 

525 else: 

526 user.house_rules = request.house_rules.value 

527 

528 if request.HasField("parking"): 

529 if request.parking.is_null: 

530 user.parking = None 

531 else: 

532 user.parking = request.parking.value 

533 

534 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

535 user.parking_details = parkingdetails2sql[request.parking_details] 

536 

537 if request.HasField("camping_ok"): 

538 if request.camping_ok.is_null: 

539 user.camping_ok = None 

540 else: 

541 user.camping_ok = request.camping_ok.value 

542 

543 # save updates 

544 session.commit() 

545 

546 return empty_pb2.Empty() 

547 

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

549 rels = ( 

550 session.execute( 

551 select(FriendRelationship) 

552 .where_users_column_visible(context, FriendRelationship.from_user_id) 

553 .where_users_column_visible(context, FriendRelationship.to_user_id) 

554 .where( 

555 or_( 

556 FriendRelationship.from_user_id == context.user_id, 

557 FriendRelationship.to_user_id == context.user_id, 

558 ) 

559 ) 

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

561 ) 

562 .scalars() 

563 .all() 

564 ) 

565 return api_pb2.ListFriendsRes( 

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

567 ) 

568 

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

570 if context.user_id == request.user_id: 

571 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

572 

573 user = session.execute( 

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

575 ).scalar_one_or_none() 

576 

577 if not user: 

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

579 

580 q1 = ( 

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

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

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

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

585 ) 

586 

587 q2 = ( 

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

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

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

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

592 ) 

593 

594 q3 = ( 

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

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

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

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

599 ) 

600 

601 q4 = ( 

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

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

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

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

606 ) 

607 

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

609 

610 mutual_friends = ( 

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

612 ) 

613 

614 return api_pb2.ListMutualFriendsRes( 

615 mutual_friends=[ 

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

617 for mutual_friend in mutual_friends 

618 ] 

619 ) 

620 

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

622 if context.user_id == request.user_id: 

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

624 

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

626 to_user = session.execute( 

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

628 ).scalar_one_or_none() 

629 

630 if not to_user: 

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

632 

633 if ( 

634 session.execute( 

635 select(FriendRelationship) 

636 .where( 

637 or_( 

638 and_( 

639 FriendRelationship.from_user_id == context.user_id, 

640 FriendRelationship.to_user_id == request.user_id, 

641 ), 

642 and_( 

643 FriendRelationship.from_user_id == request.user_id, 

644 FriendRelationship.to_user_id == context.user_id, 

645 ), 

646 ) 

647 ) 

648 .where( 

649 or_( 

650 FriendRelationship.status == FriendStatus.accepted, 

651 FriendRelationship.status == FriendStatus.pending, 

652 ) 

653 ) 

654 ).scalar_one_or_none() 

655 is not None 

656 ): 

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

658 

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

660 

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

662 session.add(friend_relationship) 

663 session.flush() 

664 

665 notify( 

666 session, 

667 user_id=friend_relationship.to_user_id, 

668 topic_action="friend_request:create", 

669 key=friend_relationship.from_user_id, 

670 data=notification_data_pb2.FriendRequestCreate( 

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

672 ), 

673 ) 

674 

675 return empty_pb2.Empty() 

676 

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

678 # both sent and received 

679 sent_requests = ( 

680 session.execute( 

681 select(FriendRelationship) 

682 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

685 ) 

686 .scalars() 

687 .all() 

688 ) 

689 

690 received_requests = ( 

691 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 ) 

697 .scalars() 

698 .all() 

699 ) 

700 

701 return api_pb2.ListFriendRequestsRes( 

702 sent=[ 

703 api_pb2.FriendRequest( 

704 friend_request_id=friend_request.id, 

705 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

706 user_id=friend_request.to_user.id, 

707 sent=True, 

708 ) 

709 for friend_request in sent_requests 

710 ], 

711 received=[ 

712 api_pb2.FriendRequest( 

713 friend_request_id=friend_request.id, 

714 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

715 user_id=friend_request.from_user.id, 

716 sent=False, 

717 ) 

718 for friend_request in received_requests 

719 ], 

720 ) 

721 

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

723 friend_request = session.execute( 

724 select(FriendRelationship) 

725 .where_users_column_visible(context, FriendRelationship.from_user_id) 

726 .where(FriendRelationship.to_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.accepted if request.accept else FriendStatus.rejected 

735 friend_request.time_responded = func.now() 

736 

737 session.flush() 

738 

739 if friend_request.status == FriendStatus.accepted: 

740 notify( 

741 session, 

742 user_id=friend_request.from_user_id, 

743 topic_action="friend_request:accept", 

744 key=friend_request.to_user_id, 

745 data=notification_data_pb2.FriendRequestAccept( 

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

747 ), 

748 ) 

749 

750 return empty_pb2.Empty() 

751 

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

753 friend_request = session.execute( 

754 select(FriendRelationship) 

755 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

759 ).scalar_one_or_none() 

760 

761 if not friend_request: 

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

763 

764 friend_request.status = FriendStatus.cancelled 

765 friend_request.time_responded = func.now() 

766 

767 # note no notifications 

768 

769 session.commit() 

770 

771 return empty_pb2.Empty() 

772 

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

774 key = random_hex() 

775 

776 created = now() 

777 expiry = created + timedelta(minutes=20) 

778 

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

780 session.add(upload) 

781 session.commit() 

782 

783 req = media_pb2.UploadRequest( 

784 key=upload.key, 

785 type=media_pb2.UploadRequest.UploadType.IMAGE, 

786 created=Timestamp_from_datetime(upload.created), 

787 expiry=Timestamp_from_datetime(upload.expiry), 

788 max_width=2000, 

789 max_height=1600, 

790 ).SerializeToString() 

791 

792 data = b64encode(req) 

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

794 

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

796 

797 return api_pb2.InitiateMediaUploadRes( 

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

799 expiry=Timestamp_from_datetime(expiry), 

800 ) 

801 

802 

803def user_model_to_pb(db_user, session, context): 

804 num_references = session.execute( 

805 select(func.count()) 

806 .select_from(Reference) 

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

808 .where(User.is_visible) 

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

810 ).scalar_one() 

811 

812 # returns (lat, lng) 

813 # we put people without coords on null island 

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

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

816 

817 pending_friend_request = None 

818 if db_user.id == context.user_id: 

819 friends_status = api_pb2.User.FriendshipStatus.NA 

820 else: 

821 friend_relationship = session.execute( 

822 select(FriendRelationship) 

823 .where( 

824 or_( 

825 and_( 

826 FriendRelationship.from_user_id == context.user_id, 

827 FriendRelationship.to_user_id == db_user.id, 

828 ), 

829 and_( 

830 FriendRelationship.from_user_id == db_user.id, 

831 FriendRelationship.to_user_id == context.user_id, 

832 ), 

833 ) 

834 ) 

835 .where( 

836 or_( 

837 FriendRelationship.status == FriendStatus.accepted, 

838 FriendRelationship.status == FriendStatus.pending, 

839 ) 

840 ) 

841 ).scalar_one_or_none() 

842 

843 if friend_relationship: 

844 if friend_relationship.status == FriendStatus.accepted: 

845 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

846 else: 

847 friends_status = api_pb2.User.FriendshipStatus.PENDING 

848 if friend_relationship.from_user_id == context.user_id: 

849 # we sent 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.to_user.id, 

854 sent=True, 

855 ) 

856 else: 

857 # we received it 

858 pending_friend_request = api_pb2.FriendRequest( 

859 friend_request_id=friend_relationship.id, 

860 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

861 user_id=friend_relationship.from_user.id, 

862 sent=False, 

863 ) 

864 else: 

865 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

866 

867 verification_score = 0.0 

868 if db_user.phone_verification_verified: 

869 verification_score += 1.0 * db_user.phone_is_verified 

870 

871 user = api_pb2.User( 

872 user_id=db_user.id, 

873 username=db_user.username, 

874 name=db_user.name, 

875 city=db_user.city, 

876 hometown=db_user.hometown, 

877 timezone=db_user.timezone, 

878 lat=lat, 

879 lng=lng, 

880 radius=db_user.geom_radius, 

881 verification=verification_score, 

882 community_standing=db_user.community_standing, 

883 num_references=num_references, 

884 gender=db_user.gender, 

885 pronouns=db_user.pronouns, 

886 age=int(db_user.age), 

887 joined=Timestamp_from_datetime(db_user.display_joined), 

888 last_active=Timestamp_from_datetime(db_user.display_last_active), 

889 hosting_status=hostingstatus2api[db_user.hosting_status], 

890 meetup_status=meetupstatus2api[db_user.meetup_status], 

891 occupation=db_user.occupation, 

892 education=db_user.education, 

893 about_me=db_user.about_me, 

894 my_travels=db_user.my_travels, 

895 things_i_like=db_user.things_i_like, 

896 about_place=db_user.about_place, 

897 language_abilities=[ 

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

899 for ability in db_user.language_abilities 

900 ], 

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

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

903 additional_information=db_user.additional_information, 

904 friends=friends_status, 

905 pending_friend_request=pending_friend_request, 

906 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

907 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

908 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

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

912 **get_strong_verification_fields(session, db_user), 

913 ) 

914 

915 if db_user.max_guests is not None: 

916 user.max_guests.value = db_user.max_guests 

917 

918 if db_user.last_minute is not None: 

919 user.last_minute.value = db_user.last_minute 

920 

921 if db_user.has_pets is not None: 

922 user.has_pets.value = db_user.has_pets 

923 

924 if db_user.accepts_pets is not None: 

925 user.accepts_pets.value = db_user.accepts_pets 

926 

927 if db_user.pet_details is not None: 

928 user.pet_details.value = db_user.pet_details 

929 

930 if db_user.has_kids is not None: 

931 user.has_kids.value = db_user.has_kids 

932 

933 if db_user.accepts_kids is not None: 

934 user.accepts_kids.value = db_user.accepts_kids 

935 

936 if db_user.kid_details is not None: 

937 user.kid_details.value = db_user.kid_details 

938 

939 if db_user.has_housemates is not None: 

940 user.has_housemates.value = db_user.has_housemates 

941 

942 if db_user.housemate_details is not None: 

943 user.housemate_details.value = db_user.housemate_details 

944 

945 if db_user.wheelchair_accessible is not None: 

946 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

947 

948 if db_user.smokes_at_home is not None: 

949 user.smokes_at_home.value = db_user.smokes_at_home 

950 

951 if db_user.drinking_allowed is not None: 

952 user.drinking_allowed.value = db_user.drinking_allowed 

953 

954 if db_user.drinks_at_home is not None: 

955 user.drinks_at_home.value = db_user.drinks_at_home 

956 

957 if db_user.other_host_info is not None: 

958 user.other_host_info.value = db_user.other_host_info 

959 

960 if db_user.sleeping_details is not None: 

961 user.sleeping_details.value = db_user.sleeping_details 

962 

963 if db_user.area is not None: 

964 user.area.value = db_user.area 

965 

966 if db_user.house_rules is not None: 

967 user.house_rules.value = db_user.house_rules 

968 

969 if db_user.parking is not None: 

970 user.parking.value = db_user.parking 

971 

972 if db_user.camping_ok is not None: 

973 user.camping_ok.value = db_user.camping_ok 

974 

975 return user