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

479 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +0000

1from collections.abc import Iterable 

2from datetime import timedelta 

3from typing import cast 

4from urllib.parse import urlencode 

5 

6import google.protobuf.message 

7import grpc 

8from google.protobuf import empty_pb2 

9from sqlalchemy import select 

10from sqlalchemy.orm import Session, selectinload, undefer 

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

12 

13from couchers import urls 

14from couchers.abuse import maybe_log_nonvisible_user_access 

15from couchers.config import config 

16from couchers.constants import GHOST_USERNAME 

17from couchers.context import CouchersContext, make_notification_user_context 

18from couchers.crypto import b64encode, generate_hash_signature, random_hex 

19from couchers.event_log import log_event 

20from couchers.helpers.completed_profile import has_completed_profile 

21from couchers.helpers.group_chats import is_newest_subscription, is_unseen 

22from couchers.helpers.host_requests import ( 

23 has_unseen_host_request_messages, 

24 is_hosting_party, 

25 is_public_trip_offer_recipient, 

26 is_surfing_party, 

27) 

28from couchers.helpers.hosting_meetup_status import record_hosting_meetup_status 

29from couchers.helpers.references import where_reference_user_visible, where_references_not_hidden_by_reciprocity 

30from couchers.helpers.strong_verification import get_strong_verification_fields 

31from couchers.materialized_views import LiteUser, UserResponseRate 

32from couchers.models import ( 

33 FriendRelationship, 

34 FriendStatus, 

35 GroupChat, 

36 GroupChatSubscription, 

37 HostingMeetupStatusSource, 

38 HostingStatus, 

39 HostRequest, 

40 InitiatedUpload, 

41 LanguageAbility, 

42 LanguageFluency, 

43 MeetupStatus, 

44 Message, 

45 ModerationObjectType, 

46 NonvisibleUserAccessType, 

47 Notification, 

48 NotificationDeliveryType, 

49 ParkingDetails, 

50 RateLimitAction, 

51 Reference, 

52 RegionLived, 

53 RegionVisited, 

54 SleepingArrangement, 

55 SmokingLocation, 

56 User, 

57 UserBadge, 

58) 

59from couchers.models.notifications import NotificationTopicAction 

60from couchers.models.uploads import get_avatar_upload 

61from couchers.moderation.utils import create_moderation 

62from couchers.notifications.notify import notify 

63from couchers.notifications.settings import get_topic_actions_by_delivery_type 

64from couchers.proto import api_pb2, api_pb2_grpc, media_pb2, notification_data_pb2, requests_pb2 

65from couchers.rate_limits.check import process_rate_limits_and_check_abort 

66from couchers.rate_limits.definitions import RATE_LIMIT_HOURS 

67from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

68from couchers.servicers.blocking import is_not_visible 

69from couchers.sql import ( 

70 moderation_state_column_visible, 

71 username_or_id, 

72 users_visible, 

73 where_moderated_content_visible, 

74 where_users_column_visible, 

75) 

76from couchers.utils import ( 

77 Duration_from_timedelta, 

78 Timestamp_from_datetime, 

79 create_coordinate, 

80 get_coordinates, 

81 is_valid_name, 

82 is_valid_user_id, 

83 is_valid_username, 

84 not_none, 

85 now, 

86) 

87 

88 

89class GhostUserSerializationError(Exception): 

90 """ 

91 Raised when attempting to serialize a ghost user (deleted/banned/blocked) 

92 """ 

93 

94 pass 

95 

96 

97MAX_USERS_PER_QUERY = 200 

98MAX_PAGINATION_LENGTH = 50 

99 

100hostingstatus2sql = { 

101 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

102 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

103 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

104 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

105} 

106 

107hostingstatus2api = { 

108 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

109 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

110 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

111 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

112} 

113 

114meetupstatus2sql = { 

115 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

116 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

117 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

118 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

119} 

120 

121meetupstatus2api = { 

122 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

123 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

124 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

125 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

126} 

127 

128smokinglocation2sql = { 

129 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

130 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

131 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

132 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

133 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

134} 

135 

136smokinglocation2api = { 

137 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

138 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

139 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

140 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

141 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

142} 

143 

144sleepingarrangement2sql = { 

145 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

146 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

147 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

148 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

149} 

150 

151sleepingarrangement2api = { 

152 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

153 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

154 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

155 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

156} 

157 

158parkingdetails2sql = { 

159 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

160 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

161 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

162 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

163 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

164} 

165 

166parkingdetails2api = { 

167 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

168 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

169 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

170 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

171 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

172} 

173 

174fluency2sql = { 

175 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

179} 

180 

181fluency2api = { 

182 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

186} 

187 

188 

189class API(api_pb2_grpc.APIServicer): 

190 def Ping(self, request: api_pb2.PingReq, context: CouchersContext, session: Session) -> api_pb2.PingRes: 

191 # auth ought to make sure the user exists 

192 user = session.execute( 

193 select(User) 

194 .where(User.id == context.user_id) 

195 .options( 

196 selectinload(User.regions_visited), 

197 selectinload(User.regions_lived), 

198 selectinload(User.language_abilities), 

199 # deferred, and user_model_to_pb below reads it; loading it here saves a round trip on 

200 # what is the most frequently polled endpoint we have 

201 undefer(User.timezone), 

202 ) 

203 ).scalar_one() 

204 

205 # One pass over the requests the viewer is a party to that have unread messages, tallied both by 

206 # direction (sent/received) and by stay-role. The role-based tallies bucket public-trip offers 

207 # correctly despite the role reversal: the offering host counts under hosting, the traveller 

208 # under surfing. 

209 other_party_user_id = case( 

210 (HostRequest.initiator_user_id == context.user_id, HostRequest.recipient_user_id), 

211 else_=HostRequest.initiator_user_id, 

212 ) 

213 unseen_host_request_counts_query = ( 

214 select( 

215 func.count().filter(HostRequest.initiator_user_id == context.user_id).label("sent"), 

216 func.count().filter(HostRequest.recipient_user_id == context.user_id).label("received"), 

217 func.count().filter(is_hosting_party(context.user_id)).label("hosting"), 

218 func.count().filter(is_surfing_party(context.user_id)).label("surfing"), 

219 func.count().filter(is_public_trip_offer_recipient(context.user_id)).label("public_trip_offer"), 

220 ) 

221 .select_from(HostRequest) 

222 .where( 

223 or_( 

224 HostRequest.initiator_user_id == context.user_id, 

225 HostRequest.recipient_user_id == context.user_id, 

226 ) 

227 ) 

228 .where(has_unseen_host_request_messages(context.user_id)) 

229 ) 

230 unseen_host_request_counts_query = where_users_column_visible( 

231 unseen_host_request_counts_query, context, other_party_user_id 

232 ) 

233 unseen_host_request_counts_query = where_moderated_content_visible( 

234 unseen_host_request_counts_query, context, HostRequest, is_list_operation=True 

235 ) 

236 unseen_host_request_counts = session.execute(unseen_host_request_counts_query).one() 

237 

238 unseen_public_trip_offer_count = ( 

239 unseen_host_request_counts.public_trip_offer 

240 if context.get_boolean_value("public_trips_enabled", False) 

241 else 0 

242 ) 

243 

244 unseen_message_query = ( 

245 select(func.count(Message.id)) 

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

247 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id) 

248 ) 

249 unseen_message_query = where_moderated_content_visible( 

250 unseen_message_query, context, GroupChat, is_list_operation=True 

251 ) 

252 unseen_message_query = ( 

253 unseen_message_query.where(GroupChatSubscription.user_id == context.user_id) 

254 .where(is_newest_subscription(context.user_id)) 

255 .where(is_unseen(Message, GroupChatSubscription)) 

256 ) 

257 unseen_message_count = session.execute(unseen_message_query).scalar_one() 

258 

259 pending_friend_request_query = select(func.count(FriendRelationship.id)).where( 

260 FriendRelationship.to_user_id == context.user_id 

261 ) 

262 pending_friend_request_query = where_users_column_visible( 

263 pending_friend_request_query, context, FriendRelationship.from_user_id 

264 ) 

265 pending_friend_request_query = pending_friend_request_query.where( 

266 FriendRelationship.status == FriendStatus.pending 

267 ) 

268 pending_friend_request_query = where_moderated_content_visible( 

269 pending_friend_request_query, context, FriendRelationship, is_list_operation=True 

270 ) 

271 pending_friend_request_count = session.execute(pending_friend_request_query).scalar_one() 

272 

273 unseen_notification_count = session.execute( 

274 select(func.count()) 

275 .select_from(Notification) 

276 .where(Notification.user_id == context.user_id) 

277 .where(Notification.is_seen == False) 

278 .where( 

279 Notification.topic_action.in_( 

280 get_topic_actions_by_delivery_type(session, user.id, NotificationDeliveryType.push) 

281 ) 

282 ) 

283 .where(moderation_state_column_visible(context, Notification.moderation_state_id)) 

284 ).scalar_one() 

285 

286 return api_pb2.PingRes( 

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

288 unseen_message_count=unseen_message_count, 

289 unseen_sent_host_request_count=unseen_host_request_counts.sent, 

290 unseen_received_host_request_count=unseen_host_request_counts.received, 

291 unseen_hosting_host_request_count=unseen_host_request_counts.hosting, 

292 unseen_surfing_host_request_count=unseen_host_request_counts.surfing, 

293 unseen_public_trip_offer_count=unseen_public_trip_offer_count, 

294 pending_friend_request_count=pending_friend_request_count, 

295 unseen_notification_count=unseen_notification_count, 

296 ) 

297 

298 def GetUser(self, request: api_pb2.GetUserReq, context: CouchersContext, session: Session) -> api_pb2.User: 

299 user = session.execute( 

300 select(User) 

301 .where(username_or_id(request.user)) 

302 .options( 

303 selectinload(User.regions_visited), 

304 selectinload(User.regions_lived), 

305 selectinload(User.language_abilities), 

306 ) 

307 ).scalar_one_or_none() 

308 

309 if not user: 309 ↛ 310line 309 didn't jump to line 310 because the condition on line 309 was never true

310 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found") 

311 

312 return user_model_to_pb(user, session, context, is_get_user_return_ghosts=True) 

313 

314 def GetLiteUser( 

315 self, request: api_pb2.GetLiteUserReq, context: CouchersContext, session: Session 

316 ) -> api_pb2.LiteUser: 

317 lite_user = session.execute( 

318 select(LiteUser).where(username_or_id(request.user, table=LiteUser)) 

319 ).scalar_one_or_none() 

320 

321 if not lite_user: 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true

322 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found") 

323 

324 return lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True) 

325 

326 def GetLiteUsers( 

327 self, request: api_pb2.GetLiteUsersReq, context: CouchersContext, session: Session 

328 ) -> api_pb2.GetLiteUsersRes: 

329 if len(request.users) > MAX_USERS_PER_QUERY: 

330 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "requested_too_many_users") 

331 

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

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

334 

335 # decomposed where_username_or_id... 

336 users = ( 

337 session.execute(select(LiteUser).where(or_(LiteUser.username.in_(usernames), LiteUser.id.in_(ids)))) 

338 .scalars() 

339 .all() 

340 ) 

341 

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

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

344 

345 res = api_pb2.GetLiteUsersRes() 

346 

347 for user in request.users: 

348 lite_user = None 

349 if user in users_by_id: 

350 lite_user = users_by_id[user] 

351 elif user in users_by_username: 

352 lite_user = users_by_username[user] 

353 

354 res.responses.append( 

355 api_pb2.LiteUserRes( 

356 query=user, 

357 not_found=lite_user is None, 

358 user=lite_user_to_pb(session, lite_user, context, is_get_user_return_ghosts=True) 

359 if lite_user 

360 else None, 

361 ) 

362 ) 

363 

364 return res 

365 

366 def UpdateProfile( 

367 self, request: api_pb2.UpdateProfileReq, context: CouchersContext, session: Session 

368 ) -> empty_pb2.Empty: 

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

370 

371 if request.HasField("name"): 

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

373 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_name") 

374 user.name = request.name.value 

375 

376 if request.HasField("city"): 

377 user.city = request.city.value 

378 

379 if request.HasField("hometown"): 

380 if request.hometown.is_null: 

381 user.hometown = None 

382 else: 

383 user.hometown = request.hometown.value 

384 

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

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

387 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate") 

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

389 user.randomized_geom = None 

390 

391 if request.HasField("radius"): 

392 user.geom_radius = request.radius.value 

393 

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

395 # user.gender = request.gender.value 

396 

397 if request.HasField("pronouns"): 

398 if request.pronouns.is_null: 

399 user.pronouns = None 

400 else: 

401 user.pronouns = request.pronouns.value 

402 

403 if request.HasField("occupation"): 

404 if request.occupation.is_null: 

405 user.occupation = None 

406 else: 

407 user.occupation = request.occupation.value 

408 

409 if request.HasField("education"): 

410 if request.education.is_null: 

411 user.education = None 

412 else: 

413 user.education = request.education.value 

414 

415 if request.HasField("about_me"): 

416 if request.about_me.is_null: 

417 user.about_me = None 

418 else: 

419 user.about_me = request.about_me.value 

420 

421 if request.HasField("things_i_like"): 

422 if request.things_i_like.is_null: 

423 user.things_i_like = None 

424 else: 

425 user.things_i_like = request.things_i_like.value 

426 

427 if request.HasField("about_place"): 

428 if request.about_place.is_null: 

429 user.about_place = None 

430 else: 

431 user.about_place = request.about_place.value 

432 

433 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

435 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_host") 

436 user.hosting_status = hostingstatus2sql[request.hosting_status] # type: ignore[assignment] 

437 

438 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

440 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "do_not_email_cannot_meet") 

441 user.meetup_status = meetupstatus2sql[request.meetup_status] # type: ignore[assignment] 

442 

443 if ( 

444 request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED 

445 or request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED 

446 ): 

447 record_hosting_meetup_status(session, user, HostingMeetupStatusSource.profile_edit) 

448 

449 if request.HasField("language_abilities"): 

450 # delete all existing abilities 

451 for ability in user.language_abilities: 

452 session.delete(ability) 

453 session.flush() 

454 

455 # add the new ones 

456 for language_ability in request.language_abilities.value: 

457 if not language_is_allowed(language_ability.code): 

458 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_language") 

459 session.add( 

460 LanguageAbility( 

461 user_id=user.id, 

462 language_code=language_ability.code, 

463 fluency=not_none(fluency2sql[language_ability.fluency]), 

464 ) 

465 ) 

466 

467 if request.HasField("regions_visited"): 

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

469 

470 for region in request.regions_visited.value: 

471 if not region_is_allowed(region): 

472 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region") 

473 session.add( 

474 RegionVisited( 

475 user_id=user.id, 

476 region_code=region, 

477 ) 

478 ) 

479 

480 if request.HasField("regions_lived"): 

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

482 

483 for region in request.regions_lived.value: 

484 if not region_is_allowed(region): 

485 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_region") 

486 session.add( 

487 RegionLived( 

488 user_id=user.id, 

489 region_code=region, 

490 ) 

491 ) 

492 

493 if request.HasField("additional_information"): 

494 if request.additional_information.is_null: 

495 user.additional_information = None 

496 else: 

497 user.additional_information = request.additional_information.value 

498 

499 if request.HasField("max_guests"): 

500 if request.max_guests.is_null: 

501 user.max_guests = None 

502 else: 

503 user.max_guests = request.max_guests.value 

504 

505 if request.HasField("last_minute"): 

506 if request.last_minute.is_null: 

507 user.last_minute = None 

508 else: 

509 user.last_minute = request.last_minute.value 

510 

511 if request.HasField("has_pets"): 

512 if request.has_pets.is_null: 

513 user.has_pets = None 

514 else: 

515 user.has_pets = request.has_pets.value 

516 

517 if request.HasField("accepts_pets"): 

518 if request.accepts_pets.is_null: 

519 user.accepts_pets = None 

520 else: 

521 user.accepts_pets = request.accepts_pets.value 

522 

523 if request.HasField("pet_details"): 

524 if request.pet_details.is_null: 

525 user.pet_details = None 

526 else: 

527 user.pet_details = request.pet_details.value 

528 

529 if request.HasField("has_kids"): 

530 if request.has_kids.is_null: 

531 user.has_kids = None 

532 else: 

533 user.has_kids = request.has_kids.value 

534 

535 if request.HasField("accepts_kids"): 

536 if request.accepts_kids.is_null: 

537 user.accepts_kids = None 

538 else: 

539 user.accepts_kids = request.accepts_kids.value 

540 

541 if request.HasField("kid_details"): 

542 if request.kid_details.is_null: 

543 user.kid_details = None 

544 else: 

545 user.kid_details = request.kid_details.value 

546 

547 if request.HasField("has_housemates"): 

548 if request.has_housemates.is_null: 

549 user.has_housemates = None 

550 else: 

551 user.has_housemates = request.has_housemates.value 

552 

553 if request.HasField("housemate_details"): 

554 if request.housemate_details.is_null: 

555 user.housemate_details = None 

556 else: 

557 user.housemate_details = request.housemate_details.value 

558 

559 if request.HasField("wheelchair_accessible"): 

560 if request.wheelchair_accessible.is_null: 

561 user.wheelchair_accessible = None 

562 else: 

563 user.wheelchair_accessible = request.wheelchair_accessible.value 

564 

565 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

566 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

567 

568 if request.HasField("smokes_at_home"): 

569 if request.smokes_at_home.is_null: 

570 user.smokes_at_home = None 

571 else: 

572 user.smokes_at_home = request.smokes_at_home.value 

573 

574 if request.HasField("drinking_allowed"): 

575 if request.drinking_allowed.is_null: 

576 user.drinking_allowed = None 

577 else: 

578 user.drinking_allowed = request.drinking_allowed.value 

579 

580 if request.HasField("drinks_at_home"): 

581 if request.drinks_at_home.is_null: 

582 user.drinks_at_home = None 

583 else: 

584 user.drinks_at_home = request.drinks_at_home.value 

585 

586 if request.HasField("other_host_info"): 

587 if request.other_host_info.is_null: 

588 user.other_host_info = None 

589 else: 

590 user.other_host_info = request.other_host_info.value 

591 

592 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

593 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

594 

595 if request.HasField("sleeping_details"): 

596 if request.sleeping_details.is_null: 

597 user.sleeping_details = None 

598 else: 

599 user.sleeping_details = request.sleeping_details.value 

600 

601 if request.HasField("area"): 

602 if request.area.is_null: 

603 user.area = None 

604 else: 

605 user.area = request.area.value 

606 

607 if request.HasField("house_rules"): 

608 if request.house_rules.is_null: 

609 user.house_rules = None 

610 else: 

611 user.house_rules = request.house_rules.value 

612 

613 if request.HasField("parking"): 

614 if request.parking.is_null: 

615 user.parking = None 

616 else: 

617 user.parking = request.parking.value 

618 

619 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

620 user.parking_details = parkingdetails2sql[request.parking_details] 

621 

622 if request.HasField("camping_ok"): 

623 if request.camping_ok.is_null: 

624 user.camping_ok = None 

625 else: 

626 user.camping_ok = request.camping_ok.value 

627 

628 user.profile_last_updated = now() 

629 

630 return empty_pb2.Empty() 

631 

632 def ListFriends( 

633 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

634 ) -> api_pb2.ListFriendsRes: 

635 rels_query = select(FriendRelationship) 

636 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.from_user_id) 

637 rels_query = where_users_column_visible(rels_query, context, FriendRelationship.to_user_id) 

638 rels_query = rels_query.where( 

639 or_( 

640 FriendRelationship.from_user_id == context.user_id, 

641 FriendRelationship.to_user_id == context.user_id, 

642 ) 

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

644 rels_query = where_moderated_content_visible(rels_query, context, FriendRelationship, is_list_operation=True) 

645 rels = session.execute(rels_query).scalars().all() 

646 return api_pb2.ListFriendsRes( 

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

648 ) 

649 

650 def RemoveFriend( 

651 self, request: api_pb2.RemoveFriendReq, context: CouchersContext, session: Session 

652 ) -> empty_pb2.Empty: 

653 rel_query = select(FriendRelationship) 

654 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.from_user_id) 

655 rel_query = where_users_column_visible(rel_query, context, FriendRelationship.to_user_id) 

656 rel_query = rel_query.where( 

657 or_( 

658 and_( 

659 FriendRelationship.from_user_id == request.user_id, 

660 FriendRelationship.to_user_id == context.user_id, 

661 ), 

662 and_( 

663 FriendRelationship.from_user_id == context.user_id, 

664 FriendRelationship.to_user_id == request.user_id, 

665 ), 

666 ) 

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

668 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship) 

669 rel = session.execute(rel_query).scalar_one_or_none() 

670 

671 if not rel: 

672 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "not_friends") 

673 

674 session.delete(rel) 

675 log_event(context, session, "friendship.removed", {"other_user_id": request.user_id}) 

676 

677 return empty_pb2.Empty() 

678 

679 def ListMutualFriends( 

680 self, request: api_pb2.ListMutualFriendsReq, context: CouchersContext, session: Session 

681 ) -> api_pb2.ListMutualFriendsRes: 

682 if context.user_id == request.user_id: 

683 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

684 

685 user = session.execute( 

686 select(User).where(users_visible(context)).where(User.id == request.user_id) 

687 ).scalar_one_or_none() 

688 

689 if not user: 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true

690 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found") 

691 

692 q1 = where_moderated_content_visible( 

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

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

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

696 .where(FriendRelationship.status == FriendStatus.accepted), 

697 context, 

698 FriendRelationship, 

699 ) 

700 

701 q2 = where_moderated_content_visible( 

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

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

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

705 .where(FriendRelationship.status == FriendStatus.accepted), 

706 context, 

707 FriendRelationship, 

708 ) 

709 

710 q3 = where_moderated_content_visible( 

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

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

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

714 .where(FriendRelationship.status == FriendStatus.accepted), 

715 context, 

716 FriendRelationship, 

717 ) 

718 

719 q4 = where_moderated_content_visible( 

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

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

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

723 .where(FriendRelationship.status == FriendStatus.accepted), 

724 context, 

725 FriendRelationship, 

726 ) 

727 

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

729 

730 mutual_friends = ( 

731 session.execute(select(User).where(users_visible(context)).where(User.id.in_(mutual))).scalars().all() 

732 ) 

733 

734 return api_pb2.ListMutualFriendsRes( 

735 mutual_friends=[ 

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

737 for mutual_friend in mutual_friends 

738 ] 

739 ) 

740 

741 def SendFriendRequest( 

742 self, request: api_pb2.SendFriendRequestReq, context: CouchersContext, session: Session 

743 ) -> empty_pb2.Empty: 

744 if context.user_id == request.user_id: 744 ↛ 745line 744 didn't jump to line 745 because the condition on line 744 was never true

745 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cant_friend_self") 

746 

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

748 if not has_completed_profile(session, user): 

749 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_send_friend_request") 

750 

751 to_user = session.execute( 

752 select(User).where(users_visible(context)).where(User.id == request.user_id) 

753 ).scalar_one_or_none() 

754 

755 if not to_user: 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true

756 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found") 

757 

758 if ( 

759 session.execute( 

760 select(FriendRelationship) 

761 .where( 

762 or_( 

763 and_( 

764 FriendRelationship.from_user_id == context.user_id, 

765 FriendRelationship.to_user_id == request.user_id, 

766 ), 

767 and_( 

768 FriendRelationship.from_user_id == request.user_id, 

769 FriendRelationship.to_user_id == context.user_id, 

770 ), 

771 ) 

772 ) 

773 .where( 

774 or_( 

775 FriendRelationship.status == FriendStatus.accepted, 

776 FriendRelationship.status == FriendStatus.pending, 

777 ) 

778 ) 

779 ).scalar_one_or_none() 

780 is not None 

781 ): 

782 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "friends_already_or_pending") 

783 

784 # Check if user has been sending friend requests excessively 

785 if process_rate_limits_and_check_abort( 

786 session=session, user_id=context.user_id, action=RateLimitAction.friend_request 

787 ): 

788 context.abort_with_error_code( 

789 grpc.StatusCode.RESOURCE_EXHAUSTED, 

790 "friend_request_rate_limit2", 

791 substitutions={"count": RATE_LIMIT_HOURS}, 

792 ) 

793 

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

795 

796 # Use callback pattern to handle circular dependency between FriendRelationship and ModerationState 

797 friend_relationship = None 

798 

799 def create_friend_relationship(moderation_state_id: int) -> int: 

800 nonlocal friend_relationship 

801 friend_relationship = FriendRelationship( 

802 from_user_id=user.id, 

803 to_user_id=to_user.id, 

804 status=FriendStatus.pending, 

805 moderation_state_id=moderation_state_id, 

806 ) 

807 session.add(friend_relationship) 

808 session.flush() 

809 return friend_relationship.id 

810 

811 moderation_state = create_moderation( 

812 session, ModerationObjectType.friend_request, create_friend_relationship, context.user_id 

813 ) 

814 

815 assert friend_relationship is not None # set by create_friend_relationship callback 

816 

817 notify( 

818 session, 

819 user_id=friend_relationship.to_user_id, 

820 topic_action=NotificationTopicAction.friend_request__create, 

821 key=str(friend_relationship.from_user_id), 

822 data=notification_data_pb2.FriendRequestCreate( 

823 other_user=user_model_to_pb( 

824 friend_relationship.from_user, 

825 session, 

826 make_notification_user_context(user_id=friend_relationship.to_user_id), 

827 ), 

828 ), 

829 moderation_state_id=moderation_state.id, 

830 ) 

831 log_event(context, session, "friendship.request_sent", {"to_user_id": to_user.id}) 

832 

833 return empty_pb2.Empty() 

834 

835 def ListFriendRequests( 

836 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

837 ) -> api_pb2.ListFriendRequestsRes: 

838 # both sent and received 

839 sent_requests_query = select(FriendRelationship) 

840 sent_requests_query = where_users_column_visible(sent_requests_query, context, FriendRelationship.to_user_id) 

841 sent_requests_query = sent_requests_query.where(FriendRelationship.from_user_id == context.user_id).where( 

842 FriendRelationship.status == FriendStatus.pending 

843 ) 

844 sent_requests_query = where_moderated_content_visible( 

845 sent_requests_query, context, FriendRelationship, is_list_operation=True 

846 ) 

847 sent_requests = session.execute(sent_requests_query).scalars().all() 

848 

849 received_requests_query = select(FriendRelationship) 

850 received_requests_query = where_users_column_visible( 

851 received_requests_query, context, FriendRelationship.from_user_id 

852 ) 

853 received_requests_query = received_requests_query.where(FriendRelationship.to_user_id == context.user_id).where( 

854 FriendRelationship.status == FriendStatus.pending 

855 ) 

856 received_requests_query = where_moderated_content_visible( 

857 received_requests_query, context, FriendRelationship, is_list_operation=True 

858 ) 

859 received_requests = session.execute(received_requests_query).scalars().all() 

860 

861 return api_pb2.ListFriendRequestsRes( 

862 sent=[ 

863 api_pb2.FriendRequest( 

864 friend_request_id=friend_request.id, 

865 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

866 user_id=friend_request.to_user.id, 

867 sent=True, 

868 ) 

869 for friend_request in sent_requests 

870 ], 

871 received=[ 

872 api_pb2.FriendRequest( 

873 friend_request_id=friend_request.id, 

874 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

875 user_id=friend_request.from_user.id, 

876 sent=False, 

877 ) 

878 for friend_request in received_requests 

879 ], 

880 ) 

881 

882 def RespondFriendRequest( 

883 self, request: api_pb2.RespondFriendRequestReq, context: CouchersContext, session: Session 

884 ) -> empty_pb2.Empty: 

885 friend_request_query = select(FriendRelationship) 

886 friend_request_query = where_users_column_visible( 

887 friend_request_query, context, FriendRelationship.from_user_id 

888 ) 

889 friend_request_query = ( 

890 friend_request_query.where(FriendRelationship.to_user_id == context.user_id) 

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

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

893 ) 

894 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

895 friend_request = session.execute(friend_request_query).scalar_one_or_none() 

896 

897 if not friend_request: 897 ↛ 898line 897 didn't jump to line 898 because the condition on line 897 was never true

898 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found") 

899 

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

901 friend_request.time_responded = func.now() 

902 

903 session.flush() 

904 

905 if friend_request.status == FriendStatus.accepted: 

906 notify( 

907 session, 

908 user_id=friend_request.from_user_id, 

909 topic_action=NotificationTopicAction.friend_request__accept, 

910 key=str(friend_request.to_user_id), 

911 data=notification_data_pb2.FriendRequestAccept( 

912 other_user=user_model_to_pb( 

913 friend_request.to_user, 

914 session, 

915 make_notification_user_context(user_id=friend_request.from_user_id), 

916 ), 

917 ), 

918 ) 

919 

920 log_event( 

921 context, 

922 session, 

923 "friendship.request_responded", 

924 {"from_user_id": friend_request.from_user_id, "accepted": request.accept}, 

925 ) 

926 

927 return empty_pb2.Empty() 

928 

929 def CancelFriendRequest( 

930 self, request: api_pb2.CancelFriendRequestReq, context: CouchersContext, session: Session 

931 ) -> empty_pb2.Empty: 

932 friend_request_query = select(FriendRelationship) 

933 friend_request_query = where_users_column_visible(friend_request_query, context, FriendRelationship.to_user_id) 

934 friend_request_query = ( 

935 friend_request_query.where(FriendRelationship.from_user_id == context.user_id) 

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

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

938 ) 

939 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

940 friend_request = session.execute(friend_request_query).scalar_one_or_none() 

941 

942 if not friend_request: 942 ↛ 943line 942 didn't jump to line 943 because the condition on line 942 was never true

943 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "friend_request_not_found") 

944 

945 friend_request.status = FriendStatus.cancelled 

946 friend_request.time_responded = func.now() 

947 

948 # note no notifications 

949 log_event(context, session, "friendship.request_cancelled", {"to_user_id": friend_request.to_user_id}) 

950 

951 session.commit() 

952 

953 return empty_pb2.Empty() 

954 

955 def InitiateMediaUpload( 

956 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

957 ) -> api_pb2.InitiateMediaUploadRes: 

958 key = random_hex() 

959 

960 created = now() 

961 expiry = created + timedelta(minutes=20) 

962 

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

964 session.add(upload) 

965 session.commit() 

966 

967 req = media_pb2.UploadRequest( 

968 key=upload.key, 

969 type=media_pb2.UploadRequest.UploadType.IMAGE, 

970 created=Timestamp_from_datetime(upload.created), 

971 expiry=Timestamp_from_datetime(upload.expiry), 

972 max_width=2000, 

973 max_height=1600, 

974 ).SerializeToString() 

975 

976 data = b64encode(req) 

977 sig = b64encode(generate_hash_signature(req, config.MEDIA_SERVER_SECRET_KEY)) 

978 

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

980 

981 return api_pb2.InitiateMediaUploadRes( 

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

983 expiry=Timestamp_from_datetime(expiry), 

984 ) 

985 

986 def ListBadgeUsers( 

987 self, request: api_pb2.ListBadgeUsersReq, context: CouchersContext, session: Session 

988 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

992 if not badge: 992 ↛ 993line 992 didn't jump to line 993 because the condition on line 992 was never true

993 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "badge_not_found") 

994 

995 badge_user_ids_query = ( 

996 select(UserBadge.user_id).where(UserBadge.badge_id == badge.id).where(UserBadge.user_id >= next_user_id) 

997 ) 

998 badge_user_ids_query = where_users_column_visible(badge_user_ids_query, context, UserBadge.user_id) 

999 badge_user_ids_query = badge_user_ids_query.order_by(UserBadge.user_id).limit(page_size + 1) 

1000 badge_user_ids = session.execute(badge_user_ids_query).scalars().all() 

1001 

1002 return api_pb2.ListBadgeUsersRes( 

1003 user_ids=badge_user_ids[:page_size], 

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

1005 ) 

1006 

1007 

1008def response_rate_to_pb(response_rate: UserResponseRate | None) -> dict[str, google.protobuf.message.Message]: 

1009 if not response_rate: 

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

1011 

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

1013 if response_rate.requests < 3: 

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

1015 

1016 if response_rate.response_rate <= 0.33: 

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

1018 

1019 response_time_p33_coarsened = Duration_from_timedelta( 

1020 timedelta(seconds=round(response_rate.response_time_33p.total_seconds() / 60) * 60) 

1021 ) 

1022 

1023 if response_rate.response_rate <= 0.66: 

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

1025 

1026 response_time_p66_coarsened = Duration_from_timedelta( 

1027 timedelta(seconds=round(response_rate.response_time_66p.total_seconds() / 60) * 60) 

1028 ) 

1029 

1030 if response_rate.response_rate <= 0.90: 

1031 return { 

1032 "most": requests_pb2.ResponseRateMost( 

1033 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1034 ) 

1035 } 

1036 else: 

1037 return { 

1038 "almost_all": requests_pb2.ResponseRateAlmostAll( 

1039 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1040 ) 

1041 } 

1042 

1043 

1044def get_num_references(session: Session, context: CouchersContext, user_ids: Iterable[int]) -> dict[int, int]: 

1045 query = where_moderated_content_visible( 

1046 select(Reference.to_user_id, func.count(Reference.id)), context, Reference, is_list_operation=True 

1047 ) 

1048 # exclude references still hidden by the reciprocal-reference rule, matching ListReferences 

1049 query = where_references_not_hidden_by_reciprocity(query) 

1050 query = where_reference_user_visible( 

1051 query.where(Reference.to_user_id.in_(user_ids)), context, Reference.from_user_id 

1052 ).group_by(Reference.to_user_id) 

1053 return cast(dict[int, int], dict(session.execute(query).all())) # type: ignore[arg-type] 

1054 

1055 

1056def user_model_to_pb( 

1057 db_user: User, 

1058 session: Session, 

1059 context: CouchersContext, 

1060 *, 

1061 is_admin_see_ghosts: bool = False, 

1062 is_get_user_return_ghosts: bool = False, 

1063) -> api_pb2.User: 

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

1065 # note that this function is sometimes called by a logged out user, in which case context comes from make_logged_out_context 

1066 

1067 viewer_user_id = context.user_id if context.is_logged_in() else None 

1068 if not is_admin_see_ghosts and is_not_visible( 

1069 session, viewer_user_id, db_user.id, ignore_shadow=context.serialize_shadowed 

1070 ): 

1071 # User is not visible (deleted, banned, or blocked) 

1072 if is_get_user_return_ghosts: 1072 ↛ 1087line 1072 didn't jump to line 1087 because the condition on line 1072 was always true

1073 maybe_log_nonvisible_user_access( 

1074 context, 

1075 db_user, 

1076 access_type=NonvisibleUserAccessType.ghost_served, 

1077 actor_user_id=viewer_user_id, 

1078 ) 

1079 # Return an anonymized "ghost" user profile 

1080 return api_pb2.User( 

1081 user_id=db_user.id, 

1082 is_ghost=True, 

1083 username=GHOST_USERNAME, 

1084 name=context.localization.localize_string("ghost_users.display_name"), 

1085 about_me=context.localization.localize_string("ghost_users.about_me"), 

1086 ) 

1087 raise GhostUserSerializationError( 

1088 f"Tried to serialize ghost profile in user_model_to_pb without appropriate flags. " 

1089 f"Context user_id: {viewer_user_id}, db_user id: {db_user.id} (username: {db_user.username})" 

1090 ) 

1091 

1092 num_references = get_num_references(session, context, [db_user.id]).get(db_user.id, 0) 

1093 lat, lng = db_user.coordinates 

1094 

1095 pending_friend_request = None 

1096 if context.is_logged_out() or db_user.id == context.user_id: 

1097 friends_status = api_pb2.User.FriendshipStatus.NA 

1098 else: 

1099 friend_relationship = session.execute( 

1100 where_moderated_content_visible( 

1101 select(FriendRelationship) 

1102 .where( 

1103 or_( 

1104 and_( 

1105 FriendRelationship.from_user_id == context.user_id, 

1106 FriendRelationship.to_user_id == db_user.id, 

1107 ), 

1108 and_( 

1109 FriendRelationship.from_user_id == db_user.id, 

1110 FriendRelationship.to_user_id == context.user_id, 

1111 ), 

1112 ) 

1113 ) 

1114 .where( 

1115 or_( 

1116 FriendRelationship.status == FriendStatus.accepted, 

1117 FriendRelationship.status == FriendStatus.pending, 

1118 ) 

1119 ), 

1120 context, 

1121 FriendRelationship, 

1122 ) 

1123 ).scalar_one_or_none() 

1124 

1125 if friend_relationship: 

1126 if friend_relationship.status == FriendStatus.accepted: 

1127 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1128 else: 

1129 friends_status = api_pb2.User.FriendshipStatus.PENDING 

1130 if friend_relationship.from_user_id == context.user_id: 1130 ↛ 1132line 1130 didn't jump to line 1132 because the condition on line 1130 was never true

1131 # we sent it 

1132 pending_friend_request = api_pb2.FriendRequest( 

1133 friend_request_id=friend_relationship.id, 

1134 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1135 user_id=friend_relationship.to_user.id, 

1136 sent=True, 

1137 ) 

1138 else: 

1139 # we received it 

1140 pending_friend_request = api_pb2.FriendRequest( 

1141 friend_request_id=friend_relationship.id, 

1142 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1143 user_id=friend_relationship.from_user.id, 

1144 sent=False, 

1145 ) 

1146 else: 

1147 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1148 

1149 response_rate = session.execute( 

1150 select(UserResponseRate).where(UserResponseRate.user_id == db_user.id) 

1151 ).scalar_one_or_none() 

1152 

1153 avatar_upload = get_avatar_upload(session, db_user) 

1154 

1155 verification_score = 0.0 

1156 if db_user.phone_verification_verified: 

1157 verification_score += 1.0 * db_user.phone_is_verified 

1158 

1159 user = api_pb2.User( 

1160 user_id=db_user.id, 

1161 username=db_user.username, 

1162 name=db_user.name, 

1163 city=db_user.city, 

1164 hometown=db_user.hometown, 

1165 timezone=db_user.timezone, 

1166 lat=lat, 

1167 lng=lng, 

1168 radius=db_user.geom_radius, 

1169 verification=verification_score, 

1170 community_standing=db_user.community_standing, 

1171 num_references=num_references, 

1172 gender=db_user.gender, 

1173 pronouns=db_user.pronouns, 

1174 age=int(db_user.age), 

1175 joined=Timestamp_from_datetime(db_user.display_joined), 

1176 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1177 hosting_status=hostingstatus2api[db_user.hosting_status], 

1178 meetup_status=meetupstatus2api[db_user.meetup_status], 

1179 occupation=db_user.occupation, 

1180 education=db_user.education, 

1181 about_me=db_user.about_me, 

1182 things_i_like=db_user.things_i_like, 

1183 about_place=db_user.about_place, 

1184 language_abilities=[ 

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

1186 for ability in db_user.language_abilities 

1187 ], 

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

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

1190 additional_information=db_user.additional_information, 

1191 friends=friends_status, 

1192 pending_friend_request=pending_friend_request, 

1193 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1194 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1195 parking_details=parkingdetails2api[db_user.parking_details], 

1196 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1197 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1198 profile_gallery_id=db_user.profile_gallery_id, 

1199 badges=session.execute(select(UserBadge.badge_id).where(UserBadge.user_id == db_user.id).order_by(UserBadge.id)) 

1200 .scalars() 

1201 .all(), 

1202 **get_strong_verification_fields(session, db_user), 

1203 **response_rate_to_pb(response_rate), # type: ignore[arg-type] 

1204 ) 

1205 

1206 if db_user.max_guests is not None: 

1207 user.max_guests.value = db_user.max_guests 

1208 

1209 if db_user.last_minute is not None: 

1210 user.last_minute.value = db_user.last_minute 

1211 

1212 if db_user.has_pets is not None: 

1213 user.has_pets.value = db_user.has_pets 

1214 

1215 if db_user.accepts_pets is not None: 

1216 user.accepts_pets.value = db_user.accepts_pets 

1217 

1218 if db_user.pet_details is not None: 

1219 user.pet_details.value = db_user.pet_details 

1220 

1221 if db_user.has_kids is not None: 

1222 user.has_kids.value = db_user.has_kids 

1223 

1224 if db_user.accepts_kids is not None: 

1225 user.accepts_kids.value = db_user.accepts_kids 

1226 

1227 if db_user.kid_details is not None: 

1228 user.kid_details.value = db_user.kid_details 

1229 

1230 if db_user.has_housemates is not None: 

1231 user.has_housemates.value = db_user.has_housemates 

1232 

1233 if db_user.housemate_details is not None: 

1234 user.housemate_details.value = db_user.housemate_details 

1235 

1236 if db_user.wheelchair_accessible is not None: 

1237 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1238 

1239 if db_user.smokes_at_home is not None: 

1240 user.smokes_at_home.value = db_user.smokes_at_home 

1241 

1242 if db_user.drinking_allowed is not None: 

1243 user.drinking_allowed.value = db_user.drinking_allowed 

1244 

1245 if db_user.drinks_at_home is not None: 

1246 user.drinks_at_home.value = db_user.drinks_at_home 

1247 

1248 if db_user.other_host_info is not None: 

1249 user.other_host_info.value = db_user.other_host_info 

1250 

1251 if db_user.sleeping_details is not None: 

1252 user.sleeping_details.value = db_user.sleeping_details 

1253 

1254 if db_user.area is not None: 

1255 user.area.value = db_user.area 

1256 

1257 if db_user.house_rules is not None: 

1258 user.house_rules.value = db_user.house_rules 

1259 

1260 if db_user.parking is not None: 

1261 user.parking.value = db_user.parking 

1262 

1263 if db_user.camping_ok is not None: 

1264 user.camping_ok.value = db_user.camping_ok 

1265 

1266 return user 

1267 

1268 

1269def lite_user_to_pb( 

1270 session: Session, 

1271 lite_user: LiteUser, 

1272 context: CouchersContext, 

1273 *, 

1274 is_admin_see_ghosts: bool = False, 

1275 is_get_user_return_ghosts: bool = False, 

1276) -> api_pb2.LiteUser: 

1277 if not is_admin_see_ghosts and is_not_visible( 

1278 session, context.user_id, lite_user.id, ignore_shadow=context.serialize_shadowed 

1279 ): 

1280 # User is not visible (deleted, banned, or blocked) 

1281 if is_get_user_return_ghosts: 1281 ↛ 1289line 1281 didn't jump to line 1289 because the condition on line 1281 was always true

1282 # Return an anonymized "ghost" user profile 

1283 return api_pb2.LiteUser( 

1284 user_id=lite_user.id, 

1285 is_ghost=True, 

1286 username=GHOST_USERNAME, 

1287 name=context.localization.localize_string("ghost_users.display_name"), 

1288 ) 

1289 raise GhostUserSerializationError( 

1290 f"Tried to serialize ghost profile in lite_user_to_pb without appropriate flags. " 

1291 f"Context user_id: {context.user_id}, lite_user id: {lite_user.id} (username: {lite_user.username})" 

1292 ) 

1293 

1294 lat, lng = get_coordinates(lite_user.geom) 

1295 

1296 return api_pb2.LiteUser( 

1297 user_id=lite_user.id, 

1298 username=lite_user.username, 

1299 name=lite_user.name, 

1300 city=lite_user.city, 

1301 age=int(lite_user.age), 

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

1303 if lite_user.avatar_filename 

1304 else None, 

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

1306 if lite_user.avatar_filename 

1307 else None, 

1308 lat=lat, 

1309 lng=lng, 

1310 radius=lite_user.radius, 

1311 has_strong_verification=lite_user.has_strong_verification, 

1312 )