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

385 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-04-16 15:13 +0000

1from datetime import timedelta 

2from urllib.parse import urlencode 

3 

4import grpc 

5from google.protobuf import empty_pb2 

6from sqlalchemy.orm import aliased 

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

8 

9from couchers import errors, urls 

10from couchers.config import config 

11from couchers.crypto import b64encode, generate_hash_signature, random_hex 

12from couchers.materialized_views import lite_users, user_response_rates 

13from couchers.models import ( 

14 FriendRelationship, 

15 FriendStatus, 

16 GroupChatSubscription, 

17 HostingStatus, 

18 HostRequest, 

19 InitiatedUpload, 

20 LanguageAbility, 

21 LanguageFluency, 

22 MeetupStatus, 

23 Message, 

24 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 Duration_from_timedelta, 

40 Timestamp_from_datetime, 

41 create_coordinate, 

42 get_coordinates, 

43 is_valid_name, 

44 now, 

45) 

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

47 

48MAX_USERS_PER_QUERY = 200 

49MAX_PAGINATION_LENGTH = 50 

50 

51hostingstatus2sql = { 

52 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

53 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

54 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

55 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

56} 

57 

58hostingstatus2api = { 

59 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

60 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

61 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

62 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

63} 

64 

65meetupstatus2sql = { 

66 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

67 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

68 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

69 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

70} 

71 

72meetupstatus2api = { 

73 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

74 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

75 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

76 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

77} 

78 

79smokinglocation2sql = { 

80 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

81 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

82 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

83 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

84 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

85} 

86 

87smokinglocation2api = { 

88 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

89 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

90 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

91 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

92 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

93} 

94 

95sleepingarrangement2sql = { 

96 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

97 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

98 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

99 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

100} 

101 

102sleepingarrangement2api = { 

103 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

104 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

105 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

106 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

107} 

108 

109parkingdetails2sql = { 

110 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

111 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

112 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

113 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

114 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

115} 

116 

117parkingdetails2api = { 

118 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

119 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

120 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

121 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

122 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

123} 

124 

125fluency2sql = { 

126 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

130} 

131 

132fluency2api = { 

133 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

137} 

138 

139 

140class API(api_pb2_grpc.APIServicer): 

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

142 # auth ought to make sure the user exists 

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

144 

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

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

147 message_2 = aliased(Message) 

148 unseen_sent_host_request_count = session.execute( 

149 select(func.count()) 

150 .select_from(Message) 

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

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

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

154 .where_users_column_visible(context, HostRequest.host_user_id) 

155 .where(message_2.id == None) 

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

157 ).scalar_one() 

158 

159 unseen_received_host_request_count = session.execute( 

160 select(func.count()) 

161 .select_from(Message) 

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

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

164 .where_users_column_visible(context, HostRequest.surfer_user_id) 

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

166 .where(message_2.id == None) 

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

168 ).scalar_one() 

169 

170 unseen_message_count = session.execute( 

171 select(func.count()) 

172 .select_from(Message) 

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

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

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

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

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

178 ).scalar_one() 

179 

180 pending_friend_request_count = session.execute( 

181 select(func.count()) 

182 .select_from(FriendRelationship) 

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

184 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

186 ).scalar_one() 

187 

188 return api_pb2.PingRes( 

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

190 unseen_message_count=unseen_message_count, 

191 unseen_sent_host_request_count=unseen_sent_host_request_count, 

192 unseen_received_host_request_count=unseen_received_host_request_count, 

193 pending_friend_request_count=pending_friend_request_count, 

194 ) 

195 

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

197 user = session.execute( 

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

199 ).scalar_one_or_none() 

200 

201 if not user: 

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

203 

204 return user_model_to_pb(user, session, context) 

205 

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

207 lite_user = session.execute( 

208 select(lite_users) 

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

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

211 ).one_or_none() 

212 

213 if not lite_user: 

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

215 

216 return lite_user_to_pb(lite_user) 

217 

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

219 if len(request.users) > MAX_USERS_PER_QUERY: 

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

221 

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

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

224 

225 users = session.execute( 

226 select(lite_users) 

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

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

229 ).all() 

230 

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

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

233 

234 res = api_pb2.GetLiteUsersRes() 

235 

236 for user in request.users: 

237 lite_user = None 

238 if user in users_by_id: 

239 lite_user = users_by_id[user] 

240 elif user in users_by_username: 

241 lite_user = users_by_username[user] 

242 

243 res.responses.append( 

244 api_pb2.LiteUserRes( 

245 query=user, 

246 not_found=lite_user is None, 

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

248 ) 

249 ) 

250 

251 return res 

252 

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

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

255 

256 if request.HasField("name"): 

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

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

259 user.name = request.name.value 

260 

261 if request.HasField("city"): 

262 user.city = request.city.value 

263 

264 if request.HasField("hometown"): 

265 if request.hometown.is_null: 

266 user.hometown = None 

267 else: 

268 user.hometown = request.hometown.value 

269 

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

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

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

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

274 

275 if request.HasField("radius"): 

276 user.geom_radius = request.radius.value 

277 

278 if request.HasField("avatar_key"): 

279 if request.avatar_key.is_null: 

280 user.avatar_key = None 

281 else: 

282 user.avatar_key = request.avatar_key.value 

283 

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

285 # user.gender = request.gender.value 

286 

287 if request.HasField("pronouns"): 

288 if request.pronouns.is_null: 

289 user.pronouns = None 

290 else: 

291 user.pronouns = request.pronouns.value 

292 

293 if request.HasField("occupation"): 

294 if request.occupation.is_null: 

295 user.occupation = None 

296 else: 

297 user.occupation = request.occupation.value 

298 

299 if request.HasField("education"): 

300 if request.education.is_null: 

301 user.education = None 

302 else: 

303 user.education = request.education.value 

304 

305 if request.HasField("about_me"): 

306 if request.about_me.is_null: 

307 user.about_me = None 

308 else: 

309 user.about_me = request.about_me.value 

310 

311 if request.HasField("things_i_like"): 

312 if request.things_i_like.is_null: 

313 user.things_i_like = None 

314 else: 

315 user.things_i_like = request.things_i_like.value 

316 

317 if request.HasField("about_place"): 

318 if request.about_place.is_null: 

319 user.about_place = None 

320 else: 

321 user.about_place = request.about_place.value 

322 

323 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

326 user.hosting_status = hostingstatus2sql[request.hosting_status] 

327 

328 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

331 user.meetup_status = meetupstatus2sql[request.meetup_status] 

332 

333 if request.HasField("language_abilities"): 

334 # delete all existing abilities 

335 for ability in user.language_abilities: 

336 session.delete(ability) 

337 session.flush() 

338 

339 # add the new ones 

340 for language_ability in request.language_abilities.value: 

341 if not language_is_allowed(language_ability.code): 

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

343 session.add( 

344 LanguageAbility( 

345 user=user, 

346 language_code=language_ability.code, 

347 fluency=fluency2sql[language_ability.fluency], 

348 ) 

349 ) 

350 

351 if request.HasField("regions_visited"): 

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

353 

354 for region in request.regions_visited.value: 

355 if not region_is_allowed(region): 

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

357 session.add( 

358 RegionVisited( 

359 user_id=user.id, 

360 region_code=region, 

361 ) 

362 ) 

363 

364 if request.HasField("regions_lived"): 

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

366 

367 for region in request.regions_lived.value: 

368 if not region_is_allowed(region): 

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

370 session.add( 

371 RegionLived( 

372 user_id=user.id, 

373 region_code=region, 

374 ) 

375 ) 

376 

377 if request.HasField("additional_information"): 

378 if request.additional_information.is_null: 

379 user.additional_information = None 

380 else: 

381 user.additional_information = request.additional_information.value 

382 

383 if request.HasField("max_guests"): 

384 if request.max_guests.is_null: 

385 user.max_guests = None 

386 else: 

387 user.max_guests = request.max_guests.value 

388 

389 if request.HasField("last_minute"): 

390 if request.last_minute.is_null: 

391 user.last_minute = None 

392 else: 

393 user.last_minute = request.last_minute.value 

394 

395 if request.HasField("has_pets"): 

396 if request.has_pets.is_null: 

397 user.has_pets = None 

398 else: 

399 user.has_pets = request.has_pets.value 

400 

401 if request.HasField("accepts_pets"): 

402 if request.accepts_pets.is_null: 

403 user.accepts_pets = None 

404 else: 

405 user.accepts_pets = request.accepts_pets.value 

406 

407 if request.HasField("pet_details"): 

408 if request.pet_details.is_null: 

409 user.pet_details = None 

410 else: 

411 user.pet_details = request.pet_details.value 

412 

413 if request.HasField("has_kids"): 

414 if request.has_kids.is_null: 

415 user.has_kids = None 

416 else: 

417 user.has_kids = request.has_kids.value 

418 

419 if request.HasField("accepts_kids"): 

420 if request.accepts_kids.is_null: 

421 user.accepts_kids = None 

422 else: 

423 user.accepts_kids = request.accepts_kids.value 

424 

425 if request.HasField("kid_details"): 

426 if request.kid_details.is_null: 

427 user.kid_details = None 

428 else: 

429 user.kid_details = request.kid_details.value 

430 

431 if request.HasField("has_housemates"): 

432 if request.has_housemates.is_null: 

433 user.has_housemates = None 

434 else: 

435 user.has_housemates = request.has_housemates.value 

436 

437 if request.HasField("housemate_details"): 

438 if request.housemate_details.is_null: 

439 user.housemate_details = None 

440 else: 

441 user.housemate_details = request.housemate_details.value 

442 

443 if request.HasField("wheelchair_accessible"): 

444 if request.wheelchair_accessible.is_null: 

445 user.wheelchair_accessible = None 

446 else: 

447 user.wheelchair_accessible = request.wheelchair_accessible.value 

448 

449 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

450 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

451 

452 if request.HasField("smokes_at_home"): 

453 if request.smokes_at_home.is_null: 

454 user.smokes_at_home = None 

455 else: 

456 user.smokes_at_home = request.smokes_at_home.value 

457 

458 if request.HasField("drinking_allowed"): 

459 if request.drinking_allowed.is_null: 

460 user.drinking_allowed = None 

461 else: 

462 user.drinking_allowed = request.drinking_allowed.value 

463 

464 if request.HasField("drinks_at_home"): 

465 if request.drinks_at_home.is_null: 

466 user.drinks_at_home = None 

467 else: 

468 user.drinks_at_home = request.drinks_at_home.value 

469 

470 if request.HasField("other_host_info"): 

471 if request.other_host_info.is_null: 

472 user.other_host_info = None 

473 else: 

474 user.other_host_info = request.other_host_info.value 

475 

476 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

477 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

478 

479 if request.HasField("sleeping_details"): 

480 if request.sleeping_details.is_null: 

481 user.sleeping_details = None 

482 else: 

483 user.sleeping_details = request.sleeping_details.value 

484 

485 if request.HasField("area"): 

486 if request.area.is_null: 

487 user.area = None 

488 else: 

489 user.area = request.area.value 

490 

491 if request.HasField("house_rules"): 

492 if request.house_rules.is_null: 

493 user.house_rules = None 

494 else: 

495 user.house_rules = request.house_rules.value 

496 

497 if request.HasField("parking"): 

498 if request.parking.is_null: 

499 user.parking = None 

500 else: 

501 user.parking = request.parking.value 

502 

503 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

504 user.parking_details = parkingdetails2sql[request.parking_details] 

505 

506 if request.HasField("camping_ok"): 

507 if request.camping_ok.is_null: 

508 user.camping_ok = None 

509 else: 

510 user.camping_ok = request.camping_ok.value 

511 

512 # save updates 

513 session.commit() 

514 

515 return empty_pb2.Empty() 

516 

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

518 rels = ( 

519 session.execute( 

520 select(FriendRelationship) 

521 .where_users_column_visible(context, FriendRelationship.from_user_id) 

522 .where_users_column_visible(context, FriendRelationship.to_user_id) 

523 .where( 

524 or_( 

525 FriendRelationship.from_user_id == context.user_id, 

526 FriendRelationship.to_user_id == context.user_id, 

527 ) 

528 ) 

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

530 ) 

531 .scalars() 

532 .all() 

533 ) 

534 return api_pb2.ListFriendsRes( 

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

536 ) 

537 

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

539 if context.user_id == request.user_id: 

540 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

541 

542 user = session.execute( 

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

544 ).scalar_one_or_none() 

545 

546 if not user: 

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

548 

549 q1 = ( 

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

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

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

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

554 ) 

555 

556 q2 = ( 

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

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

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

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

561 ) 

562 

563 q3 = ( 

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

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

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

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

568 ) 

569 

570 q4 = ( 

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

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

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

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

575 ) 

576 

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

578 

579 mutual_friends = ( 

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

581 ) 

582 

583 return api_pb2.ListMutualFriendsRes( 

584 mutual_friends=[ 

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

586 for mutual_friend in mutual_friends 

587 ] 

588 ) 

589 

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

591 if context.user_id == request.user_id: 

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

593 

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

595 to_user = session.execute( 

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

597 ).scalar_one_or_none() 

598 

599 if not to_user: 

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

601 

602 if ( 

603 session.execute( 

604 select(FriendRelationship) 

605 .where( 

606 or_( 

607 and_( 

608 FriendRelationship.from_user_id == context.user_id, 

609 FriendRelationship.to_user_id == request.user_id, 

610 ), 

611 and_( 

612 FriendRelationship.from_user_id == request.user_id, 

613 FriendRelationship.to_user_id == context.user_id, 

614 ), 

615 ) 

616 ) 

617 .where( 

618 or_( 

619 FriendRelationship.status == FriendStatus.accepted, 

620 FriendRelationship.status == FriendStatus.pending, 

621 ) 

622 ) 

623 ).scalar_one_or_none() 

624 is not None 

625 ): 

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

627 

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

629 

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

631 session.add(friend_relationship) 

632 session.flush() 

633 

634 notify( 

635 session, 

636 user_id=friend_relationship.to_user_id, 

637 topic_action="friend_request:create", 

638 key=friend_relationship.from_user_id, 

639 data=notification_data_pb2.FriendRequestCreate( 

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

641 ), 

642 ) 

643 

644 return empty_pb2.Empty() 

645 

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

647 # both sent and received 

648 sent_requests = ( 

649 session.execute( 

650 select(FriendRelationship) 

651 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

654 ) 

655 .scalars() 

656 .all() 

657 ) 

658 

659 received_requests = ( 

660 session.execute( 

661 select(FriendRelationship) 

662 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

665 ) 

666 .scalars() 

667 .all() 

668 ) 

669 

670 return api_pb2.ListFriendRequestsRes( 

671 sent=[ 

672 api_pb2.FriendRequest( 

673 friend_request_id=friend_request.id, 

674 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

675 user_id=friend_request.to_user.id, 

676 sent=True, 

677 ) 

678 for friend_request in sent_requests 

679 ], 

680 received=[ 

681 api_pb2.FriendRequest( 

682 friend_request_id=friend_request.id, 

683 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

684 user_id=friend_request.from_user.id, 

685 sent=False, 

686 ) 

687 for friend_request in received_requests 

688 ], 

689 ) 

690 

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

692 friend_request = session.execute( 

693 select(FriendRelationship) 

694 .where_users_column_visible(context, FriendRelationship.from_user_id) 

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

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

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

698 ).scalar_one_or_none() 

699 

700 if not friend_request: 

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

702 

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

704 friend_request.time_responded = func.now() 

705 

706 session.flush() 

707 

708 if friend_request.status == FriendStatus.accepted: 

709 notify( 

710 session, 

711 user_id=friend_request.from_user_id, 

712 topic_action="friend_request:accept", 

713 key=friend_request.to_user_id, 

714 data=notification_data_pb2.FriendRequestAccept( 

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

716 ), 

717 ) 

718 

719 return empty_pb2.Empty() 

720 

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

722 friend_request = session.execute( 

723 select(FriendRelationship) 

724 .where_users_column_visible(context, FriendRelationship.to_user_id) 

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

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

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

728 ).scalar_one_or_none() 

729 

730 if not friend_request: 

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

732 

733 friend_request.status = FriendStatus.cancelled 

734 friend_request.time_responded = func.now() 

735 

736 # note no notifications 

737 

738 session.commit() 

739 

740 return empty_pb2.Empty() 

741 

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

743 key = random_hex() 

744 

745 created = now() 

746 expiry = created + timedelta(minutes=20) 

747 

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

749 session.add(upload) 

750 session.commit() 

751 

752 req = media_pb2.UploadRequest( 

753 key=upload.key, 

754 type=media_pb2.UploadRequest.UploadType.IMAGE, 

755 created=Timestamp_from_datetime(upload.created), 

756 expiry=Timestamp_from_datetime(upload.expiry), 

757 max_width=2000, 

758 max_height=1600, 

759 ).SerializeToString() 

760 

761 data = b64encode(req) 

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

763 

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

765 

766 return api_pb2.InitiateMediaUploadRes( 

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

768 expiry=Timestamp_from_datetime(expiry), 

769 ) 

770 

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

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

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

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

775 if not badge: 

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

777 

778 badge_user_ids = ( 

779 session.execute( 

780 select(UserBadge.user_id) 

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

782 .where(UserBadge.user_id >= next_user_id) 

783 .order_by(UserBadge.user_id) 

784 .limit(page_size + 1) 

785 ) 

786 .scalars() 

787 .all() 

788 ) 

789 

790 return api_pb2.ListBadgeUsersRes( 

791 user_ids=badge_user_ids[:page_size], 

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

793 ) 

794 

795 

796def response_rate_to_pb(response_rates): 

797 if not response_rates: 

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

799 

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

801 

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

803 if not n or n < 3: 

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

805 

806 if response_rate <= 0.33: 

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

808 

809 response_time_p33_coarsened = Duration_from_timedelta( 

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

811 ) 

812 

813 if response_rate <= 0.66: 

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

815 

816 response_time_p66_coarsened = Duration_from_timedelta( 

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

818 ) 

819 

820 if response_rate <= 0.90: 

821 return { 

822 "most": requests_pb2.ResponseRateMost( 

823 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

824 ) 

825 } 

826 else: 

827 return { 

828 "almost_all": requests_pb2.ResponseRateAlmostAll( 

829 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

830 ) 

831 } 

832 

833 

834def user_model_to_pb(db_user, session, context): 

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

836 num_references = session.execute( 

837 select(func.count()) 

838 .select_from(Reference) 

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

840 .where(User.is_visible) 

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

842 .where(Reference.is_deleted == False) 

843 ).scalar_one() 

844 

845 # returns (lat, lng) 

846 # we put people without coords on null island 

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

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

849 

850 pending_friend_request = None 

851 if db_user.id == context.user_id: 

852 friends_status = api_pb2.User.FriendshipStatus.NA 

853 else: 

854 friend_relationship = session.execute( 

855 select(FriendRelationship) 

856 .where( 

857 or_( 

858 and_( 

859 FriendRelationship.from_user_id == context.user_id, 

860 FriendRelationship.to_user_id == db_user.id, 

861 ), 

862 and_( 

863 FriendRelationship.from_user_id == db_user.id, 

864 FriendRelationship.to_user_id == context.user_id, 

865 ), 

866 ) 

867 ) 

868 .where( 

869 or_( 

870 FriendRelationship.status == FriendStatus.accepted, 

871 FriendRelationship.status == FriendStatus.pending, 

872 ) 

873 ) 

874 ).scalar_one_or_none() 

875 

876 if friend_relationship: 

877 if friend_relationship.status == FriendStatus.accepted: 

878 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

879 else: 

880 friends_status = api_pb2.User.FriendshipStatus.PENDING 

881 if friend_relationship.from_user_id == context.user_id: 

882 # we sent it 

883 pending_friend_request = api_pb2.FriendRequest( 

884 friend_request_id=friend_relationship.id, 

885 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

886 user_id=friend_relationship.to_user.id, 

887 sent=True, 

888 ) 

889 else: 

890 # we received it 

891 pending_friend_request = api_pb2.FriendRequest( 

892 friend_request_id=friend_relationship.id, 

893 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

894 user_id=friend_relationship.from_user.id, 

895 sent=False, 

896 ) 

897 else: 

898 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

899 

900 response_rates = session.execute( 

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

902 ).one_or_none() 

903 

904 verification_score = 0.0 

905 if db_user.phone_verification_verified: 

906 verification_score += 1.0 * db_user.phone_is_verified 

907 

908 user = api_pb2.User( 

909 user_id=db_user.id, 

910 username=db_user.username, 

911 name=db_user.name, 

912 city=db_user.city, 

913 hometown=db_user.hometown, 

914 timezone=db_user.timezone, 

915 lat=lat, 

916 lng=lng, 

917 radius=db_user.geom_radius, 

918 verification=verification_score, 

919 community_standing=db_user.community_standing, 

920 num_references=num_references, 

921 gender=db_user.gender, 

922 pronouns=db_user.pronouns, 

923 age=int(db_user.age), 

924 joined=Timestamp_from_datetime(db_user.display_joined), 

925 last_active=Timestamp_from_datetime(db_user.display_last_active), 

926 hosting_status=hostingstatus2api[db_user.hosting_status], 

927 meetup_status=meetupstatus2api[db_user.meetup_status], 

928 occupation=db_user.occupation, 

929 education=db_user.education, 

930 about_me=db_user.about_me, 

931 things_i_like=db_user.things_i_like, 

932 about_place=db_user.about_place, 

933 language_abilities=[ 

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

935 for ability in db_user.language_abilities 

936 ], 

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

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

939 additional_information=db_user.additional_information, 

940 friends=friends_status, 

941 pending_friend_request=pending_friend_request, 

942 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

943 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

944 parking_details=parkingdetails2api[db_user.parking_details], 

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

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

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

948 **get_strong_verification_fields(session, db_user), 

949 **response_rate_to_pb(response_rates), 

950 ) 

951 

952 if db_user.max_guests is not None: 

953 user.max_guests.value = db_user.max_guests 

954 

955 if db_user.last_minute is not None: 

956 user.last_minute.value = db_user.last_minute 

957 

958 if db_user.has_pets is not None: 

959 user.has_pets.value = db_user.has_pets 

960 

961 if db_user.accepts_pets is not None: 

962 user.accepts_pets.value = db_user.accepts_pets 

963 

964 if db_user.pet_details is not None: 

965 user.pet_details.value = db_user.pet_details 

966 

967 if db_user.has_kids is not None: 

968 user.has_kids.value = db_user.has_kids 

969 

970 if db_user.accepts_kids is not None: 

971 user.accepts_kids.value = db_user.accepts_kids 

972 

973 if db_user.kid_details is not None: 

974 user.kid_details.value = db_user.kid_details 

975 

976 if db_user.has_housemates is not None: 

977 user.has_housemates.value = db_user.has_housemates 

978 

979 if db_user.housemate_details is not None: 

980 user.housemate_details.value = db_user.housemate_details 

981 

982 if db_user.wheelchair_accessible is not None: 

983 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

984 

985 if db_user.smokes_at_home is not None: 

986 user.smokes_at_home.value = db_user.smokes_at_home 

987 

988 if db_user.drinking_allowed is not None: 

989 user.drinking_allowed.value = db_user.drinking_allowed 

990 

991 if db_user.drinks_at_home is not None: 

992 user.drinks_at_home.value = db_user.drinks_at_home 

993 

994 if db_user.other_host_info is not None: 

995 user.other_host_info.value = db_user.other_host_info 

996 

997 if db_user.sleeping_details is not None: 

998 user.sleeping_details.value = db_user.sleeping_details 

999 

1000 if db_user.area is not None: 

1001 user.area.value = db_user.area 

1002 

1003 if db_user.house_rules is not None: 

1004 user.house_rules.value = db_user.house_rules 

1005 

1006 if db_user.parking is not None: 

1007 user.parking.value = db_user.parking 

1008 

1009 if db_user.camping_ok is not None: 

1010 user.camping_ok.value = db_user.camping_ok 

1011 

1012 return user 

1013 

1014 

1015def lite_user_to_pb(lite_user): 

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

1017 

1018 return api_pb2.LiteUser( 

1019 user_id=lite_user.id, 

1020 username=lite_user.username, 

1021 name=lite_user.name, 

1022 city=lite_user.city, 

1023 age=int(lite_user.age), 

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

1025 if lite_user.avatar_filename 

1026 else None, 

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

1028 if lite_user.avatar_filename 

1029 else None, 

1030 lat=lat, 

1031 lng=lng, 

1032 radius=lite_user.radius, 

1033 has_strong_verification=lite_user.has_strong_verification, 

1034 )