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

347 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-07-20 21:46 +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.db import session_scope 

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.utils import Timestamp_from_datetime, create_coordinate, is_valid_name, now 

37from proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2 

38 

39hostingstatus2sql = { 

40 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

41 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

42 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

43 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

44} 

45 

46hostingstatus2api = { 

47 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

48 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

49 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

50 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

51} 

52 

53meetupstatus2sql = { 

54 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

55 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

56 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

57 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

58} 

59 

60meetupstatus2api = { 

61 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

62 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

63 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

64 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

65} 

66 

67smokinglocation2sql = { 

68 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

69 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

70 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

71 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

72 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

73} 

74 

75smokinglocation2api = { 

76 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

77 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

78 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

79 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

80 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

81} 

82 

83sleepingarrangement2sql = { 

84 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

85 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

86 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

87 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

88 api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE: SleepingArrangement.shared_space, 

89} 

90 

91sleepingarrangement2api = { 

92 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

93 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

94 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

95 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

96 SleepingArrangement.shared_space: api_pb2.SLEEPING_ARRANGEMENT_SHARED_SPACE, 

97} 

98 

99parkingdetails2sql = { 

100 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

101 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

102 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

103 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

104 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

105} 

106 

107parkingdetails2api = { 

108 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

109 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

110 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

111 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

112 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

113} 

114 

115fluency2sql = { 

116 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

120} 

121 

122fluency2api = { 

123 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

127} 

128 

129 

130class API(api_pb2_grpc.APIServicer): 

131 def Ping(self, request, context): 

132 with session_scope() as session: 

133 # auth ought to make sure the user exists 

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

135 

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

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

138 message_2 = aliased(Message) 

139 unseen_sent_host_request_count = session.execute( 

140 select(func.count()) 

141 .select_from(Message) 

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

143 .outerjoin( 

144 message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id) 

145 ) 

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( 

157 message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id) 

158 ) 

159 .where_users_column_visible(context, HostRequest.surfer_user_id) 

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

161 .where(message_2.id == None) 

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

163 ).scalar_one() 

164 

165 unseen_message_count = session.execute( 

166 select(func.count()) 

167 .select_from(Message) 

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

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

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

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

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

173 ).scalar_one() 

174 

175 pending_friend_request_count = session.execute( 

176 select(func.count()) 

177 .select_from(FriendRelationship) 

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

179 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

181 ).scalar_one() 

182 

183 return api_pb2.PingRes( 

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

185 unseen_message_count=unseen_message_count, 

186 unseen_sent_host_request_count=unseen_sent_host_request_count, 

187 unseen_received_host_request_count=unseen_received_host_request_count, 

188 pending_friend_request_count=pending_friend_request_count, 

189 ) 

190 

191 def GetUser(self, request, context): 

192 with session_scope() as session: 

193 user = session.execute( 

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

195 ).scalar_one_or_none() 

196 

197 if not user: 

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

199 

200 return user_model_to_pb(user, session, context) 

201 

202 def UpdateProfile(self, request, context): 

203 with session_scope() as session: 

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

205 

206 if request.HasField("name"): 

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

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

209 user.name = request.name.value 

210 

211 if request.HasField("city"): 

212 user.city = request.city.value 

213 

214 if request.HasField("hometown"): 

215 if request.hometown.is_null: 

216 user.hometown = None 

217 else: 

218 user.hometown = request.hometown.value 

219 

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

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

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

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

224 

225 if request.HasField("radius"): 

226 user.geom_radius = request.radius.value 

227 

228 if request.HasField("avatar_key"): 

229 if request.avatar_key.is_null: 

230 user.avatar_key = None 

231 else: 

232 user.avatar_key = request.avatar_key.value 

233 

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

235 # user.gender = request.gender.value 

236 

237 if request.HasField("pronouns"): 

238 if request.pronouns.is_null: 

239 user.pronouns = None 

240 else: 

241 user.pronouns = request.pronouns.value 

242 

243 if request.HasField("occupation"): 

244 if request.occupation.is_null: 

245 user.occupation = None 

246 else: 

247 user.occupation = request.occupation.value 

248 

249 if request.HasField("education"): 

250 if request.education.is_null: 

251 user.education = None 

252 else: 

253 user.education = request.education.value 

254 

255 if request.HasField("about_me"): 

256 if request.about_me.is_null: 

257 user.about_me = None 

258 else: 

259 user.about_me = request.about_me.value 

260 

261 if request.HasField("my_travels"): 

262 if request.my_travels.is_null: 

263 user.my_travels = None 

264 else: 

265 user.my_travels = request.my_travels.value 

266 

267 if request.HasField("things_i_like"): 

268 if request.things_i_like.is_null: 

269 user.things_i_like = None 

270 else: 

271 user.things_i_like = request.things_i_like.value 

272 

273 if request.HasField("about_place"): 

274 if request.about_place.is_null: 

275 user.about_place = None 

276 else: 

277 user.about_place = request.about_place.value 

278 

279 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

282 user.hosting_status = hostingstatus2sql[request.hosting_status] 

283 

284 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

287 user.meetup_status = meetupstatus2sql[request.meetup_status] 

288 

289 if request.HasField("language_abilities"): 

290 # delete all existing abilities 

291 for ability in user.language_abilities: 

292 session.delete(ability) 

293 session.flush() 

294 

295 # add the new ones 

296 for language_ability in request.language_abilities.value: 

297 if not language_is_allowed(language_ability.code): 

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

299 session.add( 

300 LanguageAbility( 

301 user=user, 

302 language_code=language_ability.code, 

303 fluency=fluency2sql[language_ability.fluency], 

304 ) 

305 ) 

306 

307 if request.HasField("regions_visited"): 

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

309 

310 for region in request.regions_visited.value: 

311 if not region_is_allowed(region): 

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

313 session.add( 

314 RegionVisited( 

315 user_id=user.id, 

316 region_code=region, 

317 ) 

318 ) 

319 

320 if request.HasField("regions_lived"): 

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

322 

323 for region in request.regions_lived.value: 

324 if not region_is_allowed(region): 

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

326 session.add( 

327 RegionLived( 

328 user_id=user.id, 

329 region_code=region, 

330 ) 

331 ) 

332 

333 if request.HasField("additional_information"): 

334 if request.additional_information.is_null: 

335 user.additional_information = None 

336 else: 

337 user.additional_information = request.additional_information.value 

338 

339 if request.HasField("max_guests"): 

340 if request.max_guests.is_null: 

341 user.max_guests = None 

342 else: 

343 user.max_guests = request.max_guests.value 

344 

345 if request.HasField("last_minute"): 

346 if request.last_minute.is_null: 

347 user.last_minute = None 

348 else: 

349 user.last_minute = request.last_minute.value 

350 

351 if request.HasField("has_pets"): 

352 if request.has_pets.is_null: 

353 user.has_pets = None 

354 else: 

355 user.has_pets = request.has_pets.value 

356 

357 if request.HasField("accepts_pets"): 

358 if request.accepts_pets.is_null: 

359 user.accepts_pets = None 

360 else: 

361 user.accepts_pets = request.accepts_pets.value 

362 

363 if request.HasField("pet_details"): 

364 if request.pet_details.is_null: 

365 user.pet_details = None 

366 else: 

367 user.pet_details = request.pet_details.value 

368 

369 if request.HasField("has_kids"): 

370 if request.has_kids.is_null: 

371 user.has_kids = None 

372 else: 

373 user.has_kids = request.has_kids.value 

374 

375 if request.HasField("accepts_kids"): 

376 if request.accepts_kids.is_null: 

377 user.accepts_kids = None 

378 else: 

379 user.accepts_kids = request.accepts_kids.value 

380 

381 if request.HasField("kid_details"): 

382 if request.kid_details.is_null: 

383 user.kid_details = None 

384 else: 

385 user.kid_details = request.kid_details.value 

386 

387 if request.HasField("has_housemates"): 

388 if request.has_housemates.is_null: 

389 user.has_housemates = None 

390 else: 

391 user.has_housemates = request.has_housemates.value 

392 

393 if request.HasField("housemate_details"): 

394 if request.housemate_details.is_null: 

395 user.housemate_details = None 

396 else: 

397 user.housemate_details = request.housemate_details.value 

398 

399 if request.HasField("wheelchair_accessible"): 

400 if request.wheelchair_accessible.is_null: 

401 user.wheelchair_accessible = None 

402 else: 

403 user.wheelchair_accessible = request.wheelchair_accessible.value 

404 

405 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

406 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

407 

408 if request.HasField("smokes_at_home"): 

409 if request.smokes_at_home.is_null: 

410 user.smokes_at_home = None 

411 else: 

412 user.smokes_at_home = request.smokes_at_home.value 

413 

414 if request.HasField("drinking_allowed"): 

415 if request.drinking_allowed.is_null: 

416 user.drinking_allowed = None 

417 else: 

418 user.drinking_allowed = request.drinking_allowed.value 

419 

420 if request.HasField("drinks_at_home"): 

421 if request.drinks_at_home.is_null: 

422 user.drinks_at_home = None 

423 else: 

424 user.drinks_at_home = request.drinks_at_home.value 

425 

426 if request.HasField("other_host_info"): 

427 if request.other_host_info.is_null: 

428 user.other_host_info = None 

429 else: 

430 user.other_host_info = request.other_host_info.value 

431 

432 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

433 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

434 

435 if request.HasField("sleeping_details"): 

436 if request.sleeping_details.is_null: 

437 user.sleeping_details = None 

438 else: 

439 user.sleeping_details = request.sleeping_details.value 

440 

441 if request.HasField("area"): 

442 if request.area.is_null: 

443 user.area = None 

444 else: 

445 user.area = request.area.value 

446 

447 if request.HasField("house_rules"): 

448 if request.house_rules.is_null: 

449 user.house_rules = None 

450 else: 

451 user.house_rules = request.house_rules.value 

452 

453 if request.HasField("parking"): 

454 if request.parking.is_null: 

455 user.parking = None 

456 else: 

457 user.parking = request.parking.value 

458 

459 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

460 user.parking_details = parkingdetails2sql[request.parking_details] 

461 

462 if request.HasField("camping_ok"): 

463 if request.camping_ok.is_null: 

464 user.camping_ok = None 

465 else: 

466 user.camping_ok = request.camping_ok.value 

467 

468 # save updates 

469 session.commit() 

470 

471 return empty_pb2.Empty() 

472 

473 def ListFriends(self, request, context): 

474 with session_scope() as session: 

475 rels = ( 

476 session.execute( 

477 select(FriendRelationship) 

478 .where_users_column_visible(context, FriendRelationship.from_user_id) 

479 .where_users_column_visible(context, FriendRelationship.to_user_id) 

480 .where( 

481 or_( 

482 FriendRelationship.from_user_id == context.user_id, 

483 FriendRelationship.to_user_id == context.user_id, 

484 ) 

485 ) 

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

487 ) 

488 .scalars() 

489 .all() 

490 ) 

491 return api_pb2.ListFriendsRes( 

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

493 ) 

494 

495 def ListMutualFriends(self, request, context): 

496 if context.user_id == request.user_id: 

497 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

498 

499 with session_scope() as session: 

500 user = session.execute( 

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

502 ).scalar_one_or_none() 

503 

504 if not user: 

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

506 

507 q1 = ( 

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

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

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

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

512 ) 

513 

514 q2 = ( 

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

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

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

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

519 ) 

520 

521 q3 = ( 

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

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

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

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

526 ) 

527 

528 q4 = ( 

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

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

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

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

533 ) 

534 

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

536 

537 mutual_friends = ( 

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

539 ) 

540 

541 return api_pb2.ListMutualFriendsRes( 

542 mutual_friends=[ 

543 api_pb2.MutualFriend( 

544 user_id=mutual_friend.id, username=mutual_friend.username, name=mutual_friend.name 

545 ) 

546 for mutual_friend in mutual_friends 

547 ] 

548 ) 

549 

550 def SendFriendRequest(self, request, context): 

551 if context.user_id == request.user_id: 

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

553 

554 with session_scope() as session: 

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

556 to_user = session.execute( 

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

558 ).scalar_one_or_none() 

559 

560 if not to_user: 

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

562 

563 if ( 

564 session.execute( 

565 select(FriendRelationship) 

566 .where( 

567 or_( 

568 and_( 

569 FriendRelationship.from_user_id == context.user_id, 

570 FriendRelationship.to_user_id == request.user_id, 

571 ), 

572 and_( 

573 FriendRelationship.from_user_id == request.user_id, 

574 FriendRelationship.to_user_id == context.user_id, 

575 ), 

576 ) 

577 ) 

578 .where( 

579 or_( 

580 FriendRelationship.status == FriendStatus.accepted, 

581 FriendRelationship.status == FriendStatus.pending, 

582 ) 

583 ) 

584 ).scalar_one_or_none() 

585 is not None 

586 ): 

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

588 

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

590 

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

592 session.add(friend_relationship) 

593 session.flush() 

594 

595 notify( 

596 user_id=friend_relationship.to_user_id, 

597 topic_action="friend_request:create", 

598 key=friend_relationship.from_user_id, 

599 data=notification_data_pb2.FriendRequestCreate( 

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

601 ), 

602 ) 

603 

604 return empty_pb2.Empty() 

605 

606 def ListFriendRequests(self, request, context): 

607 # both sent and received 

608 with session_scope() as session: 

609 sent_requests = ( 

610 session.execute( 

611 select(FriendRelationship) 

612 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

615 ) 

616 .scalars() 

617 .all() 

618 ) 

619 

620 received_requests = ( 

621 session.execute( 

622 select(FriendRelationship) 

623 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

626 ) 

627 .scalars() 

628 .all() 

629 ) 

630 

631 return api_pb2.ListFriendRequestsRes( 

632 sent=[ 

633 api_pb2.FriendRequest( 

634 friend_request_id=friend_request.id, 

635 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

636 user_id=friend_request.to_user.id, 

637 sent=True, 

638 ) 

639 for friend_request in sent_requests 

640 ], 

641 received=[ 

642 api_pb2.FriendRequest( 

643 friend_request_id=friend_request.id, 

644 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

645 user_id=friend_request.from_user.id, 

646 sent=False, 

647 ) 

648 for friend_request in received_requests 

649 ], 

650 ) 

651 

652 def RespondFriendRequest(self, request, context): 

653 with session_scope() as session: 

654 friend_request = session.execute( 

655 select(FriendRelationship) 

656 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

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

660 ).scalar_one_or_none() 

661 

662 if not friend_request: 

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

664 

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

666 friend_request.time_responded = func.now() 

667 

668 session.flush() 

669 

670 if friend_request.status == FriendStatus.accepted: 

671 notify( 

672 user_id=friend_request.from_user_id, 

673 topic_action="friend_request:accept", 

674 key=friend_request.to_user_id, 

675 data=notification_data_pb2.FriendRequestAccept( 

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

677 ), 

678 ) 

679 

680 return empty_pb2.Empty() 

681 

682 def CancelFriendRequest(self, request, context): 

683 with session_scope() as session: 

684 friend_request = session.execute( 

685 select(FriendRelationship) 

686 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

690 ).scalar_one_or_none() 

691 

692 if not friend_request: 

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

694 

695 friend_request.status = FriendStatus.cancelled 

696 friend_request.time_responded = func.now() 

697 

698 # note no notifications 

699 

700 session.commit() 

701 

702 return empty_pb2.Empty() 

703 

704 def InitiateMediaUpload(self, request, context): 

705 key = random_hex() 

706 

707 created = now() 

708 expiry = created + timedelta(minutes=20) 

709 

710 with session_scope() as session: 

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

712 session.add(upload) 

713 session.commit() 

714 

715 req = media_pb2.UploadRequest( 

716 key=upload.key, 

717 type=media_pb2.UploadRequest.UploadType.IMAGE, 

718 created=Timestamp_from_datetime(upload.created), 

719 expiry=Timestamp_from_datetime(upload.expiry), 

720 max_width=2000, 

721 max_height=1600, 

722 ).SerializeToString() 

723 

724 data = b64encode(req) 

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

726 

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

728 

729 return api_pb2.InitiateMediaUploadRes( 

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

731 expiry=Timestamp_from_datetime(expiry), 

732 ) 

733 

734 

735def user_model_to_pb(db_user, session, context): 

736 num_references = session.execute( 

737 select(func.count()) 

738 .select_from(Reference) 

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

740 .where(User.is_visible) 

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

742 ).scalar_one() 

743 

744 # returns (lat, lng) 

745 # we put people without coords on null island 

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

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

748 

749 pending_friend_request = None 

750 if db_user.id == context.user_id: 

751 friends_status = api_pb2.User.FriendshipStatus.NA 

752 else: 

753 friend_relationship = session.execute( 

754 select(FriendRelationship) 

755 .where( 

756 or_( 

757 and_( 

758 FriendRelationship.from_user_id == context.user_id, 

759 FriendRelationship.to_user_id == db_user.id, 

760 ), 

761 and_( 

762 FriendRelationship.from_user_id == db_user.id, 

763 FriendRelationship.to_user_id == context.user_id, 

764 ), 

765 ) 

766 ) 

767 .where( 

768 or_( 

769 FriendRelationship.status == FriendStatus.accepted, 

770 FriendRelationship.status == FriendStatus.pending, 

771 ) 

772 ) 

773 ).scalar_one_or_none() 

774 

775 if friend_relationship: 

776 if friend_relationship.status == FriendStatus.accepted: 

777 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

778 else: 

779 friends_status = api_pb2.User.FriendshipStatus.PENDING 

780 if friend_relationship.from_user_id == context.user_id: 

781 # we sent it 

782 pending_friend_request = api_pb2.FriendRequest( 

783 friend_request_id=friend_relationship.id, 

784 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

785 user_id=friend_relationship.to_user.id, 

786 sent=True, 

787 ) 

788 else: 

789 # we received it 

790 pending_friend_request = api_pb2.FriendRequest( 

791 friend_request_id=friend_relationship.id, 

792 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

793 user_id=friend_relationship.from_user.id, 

794 sent=False, 

795 ) 

796 else: 

797 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

798 

799 verification_score = 0.0 

800 if db_user.phone_verification_verified: 

801 verification_score += 1.0 * db_user.phone_is_verified 

802 

803 user = api_pb2.User( 

804 user_id=db_user.id, 

805 username=db_user.username, 

806 name=db_user.name, 

807 city=db_user.city, 

808 hometown=db_user.hometown, 

809 timezone=db_user.timezone, 

810 lat=lat, 

811 lng=lng, 

812 radius=db_user.geom_radius, 

813 verification=verification_score, 

814 community_standing=db_user.community_standing, 

815 num_references=num_references, 

816 gender=db_user.gender, 

817 pronouns=db_user.pronouns, 

818 age=db_user.age, 

819 joined=Timestamp_from_datetime(db_user.display_joined), 

820 last_active=Timestamp_from_datetime(db_user.display_last_active), 

821 hosting_status=hostingstatus2api[db_user.hosting_status], 

822 meetup_status=meetupstatus2api[db_user.meetup_status], 

823 occupation=db_user.occupation, 

824 education=db_user.education, 

825 about_me=db_user.about_me, 

826 my_travels=db_user.my_travels, 

827 things_i_like=db_user.things_i_like, 

828 about_place=db_user.about_place, 

829 language_abilities=[ 

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

831 for ability in db_user.language_abilities 

832 ], 

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

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

835 additional_information=db_user.additional_information, 

836 friends=friends_status, 

837 pending_friend_request=pending_friend_request, 

838 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

839 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

840 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

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

844 **get_strong_verification_fields(session, db_user), 

845 ) 

846 

847 if db_user.max_guests is not None: 

848 user.max_guests.value = db_user.max_guests 

849 

850 if db_user.last_minute is not None: 

851 user.last_minute.value = db_user.last_minute 

852 

853 if db_user.has_pets is not None: 

854 user.has_pets.value = db_user.has_pets 

855 

856 if db_user.accepts_pets is not None: 

857 user.accepts_pets.value = db_user.accepts_pets 

858 

859 if db_user.pet_details is not None: 

860 user.pet_details.value = db_user.pet_details 

861 

862 if db_user.has_kids is not None: 

863 user.has_kids.value = db_user.has_kids 

864 

865 if db_user.accepts_kids is not None: 

866 user.accepts_kids.value = db_user.accepts_kids 

867 

868 if db_user.kid_details is not None: 

869 user.kid_details.value = db_user.kid_details 

870 

871 if db_user.has_housemates is not None: 

872 user.has_housemates.value = db_user.has_housemates 

873 

874 if db_user.housemate_details is not None: 

875 user.housemate_details.value = db_user.housemate_details 

876 

877 if db_user.wheelchair_accessible is not None: 

878 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

879 

880 if db_user.smokes_at_home is not None: 

881 user.smokes_at_home.value = db_user.smokes_at_home 

882 

883 if db_user.drinking_allowed is not None: 

884 user.drinking_allowed.value = db_user.drinking_allowed 

885 

886 if db_user.drinks_at_home is not None: 

887 user.drinks_at_home.value = db_user.drinks_at_home 

888 

889 if db_user.other_host_info is not None: 

890 user.other_host_info.value = db_user.other_host_info 

891 

892 if db_user.sleeping_details is not None: 

893 user.sleeping_details.value = db_user.sleeping_details 

894 

895 if db_user.area is not None: 

896 user.area.value = db_user.area 

897 

898 if db_user.house_rules is not None: 

899 user.house_rules.value = db_user.house_rules 

900 

901 if db_user.parking is not None: 

902 user.parking.value = db_user.parking 

903 

904 if db_user.camping_ok is not None: 

905 user.camping_ok.value = db_user.camping_ok 

906 

907 return user