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

470 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 09:44 +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 

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

12 

13from couchers import urls 

14from couchers.config import config 

15from couchers.constants import GHOST_USERNAME 

16from couchers.context import CouchersContext 

17from couchers.crypto import b64encode, generate_hash_signature, random_hex 

18from couchers.event_log import log_event 

19from couchers.helpers.strong_verification import get_strong_verification_fields 

20from couchers.materialized_views import LiteUser, UserResponseRate 

21from couchers.models import ( 

22 FriendRelationship, 

23 FriendStatus, 

24 GroupChat, 

25 GroupChatSubscription, 

26 HostingStatus, 

27 HostRequest, 

28 InitiatedUpload, 

29 LanguageAbility, 

30 LanguageFluency, 

31 MeetupStatus, 

32 Message, 

33 ModerationObjectType, 

34 Notification, 

35 NotificationDeliveryType, 

36 ParkingDetails, 

37 RateLimitAction, 

38 Reference, 

39 RegionLived, 

40 RegionVisited, 

41 SleepingArrangement, 

42 SmokingLocation, 

43 User, 

44 UserBadge, 

45) 

46from couchers.models.notifications import NotificationTopicAction 

47from couchers.models.uploads import get_avatar_upload 

48from couchers.moderation.utils import create_moderation 

49from couchers.notifications.notify import notify 

50from couchers.notifications.settings import get_topic_actions_by_delivery_type 

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

52from couchers.rate_limits.check import process_rate_limits_and_check_abort 

53from couchers.rate_limits.definitions import RATE_LIMIT_HOURS 

54from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

55from couchers.servicers.blocking import is_not_visible 

56from couchers.sql import ( 

57 moderation_state_column_visible, 

58 username_or_id, 

59 users_visible, 

60 where_moderated_content_visible, 

61 where_users_column_visible, 

62) 

63from couchers.utils import ( 

64 Duration_from_timedelta, 

65 Timestamp_from_datetime, 

66 create_coordinate, 

67 get_coordinates, 

68 is_valid_name, 

69 is_valid_user_id, 

70 is_valid_username, 

71 not_none, 

72 now, 

73) 

74 

75 

76class GhostUserSerializationError(Exception): 

77 """ 

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

79 """ 

80 

81 pass 

82 

83 

84MAX_USERS_PER_QUERY = 200 

85MAX_PAGINATION_LENGTH = 50 

86 

87hostingstatus2sql = { 

88 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

89 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

90 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

91 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

92} 

93 

94hostingstatus2api = { 

95 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

96 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

97 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

98 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

99} 

100 

101meetupstatus2sql = { 

102 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

103 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

104 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

105 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

106} 

107 

108meetupstatus2api = { 

109 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

110 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

111 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

112 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

113} 

114 

115smokinglocation2sql = { 

116 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

117 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

118 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

119 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

120 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

121} 

122 

123smokinglocation2api = { 

124 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

125 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

126 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

127 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

128 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

129} 

130 

131sleepingarrangement2sql = { 

132 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

133 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

134 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

135 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

136} 

137 

138sleepingarrangement2api = { 

139 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

140 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

141 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

142 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

143} 

144 

145parkingdetails2sql = { 

146 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

147 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

148 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

149 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

150 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

151} 

152 

153parkingdetails2api = { 

154 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

155 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

156 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

157 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

158 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

159} 

160 

161fluency2sql = { 

162 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

166} 

167 

168fluency2api = { 

169 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

173} 

174 

175 

176class API(api_pb2_grpc.APIServicer): 

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

178 # auth ought to make sure the user exists 

179 user = session.execute( 

180 select(User) 

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

182 .options( 

183 selectinload(User.regions_visited), 

184 selectinload(User.regions_lived), 

185 selectinload(User.language_abilities), 

186 ) 

187 ).scalar_one() 

188 

189 sent_reqs_query = select(HostRequest.conversation_id, HostRequest.initiator_last_seen_message_id).where( 

190 HostRequest.initiator_user_id == context.user_id 

191 ) 

192 sent_reqs_query = where_users_column_visible(sent_reqs_query, context, HostRequest.recipient_user_id) 

193 sent_reqs_query = where_moderated_content_visible(sent_reqs_query, context, HostRequest, is_list_operation=True) 

194 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery() 

195 

196 unseen_sent_host_request_count = session.execute( 

197 select(func.count()) 

198 .select_from(sent_reqs_last_seen_message_ids) 

199 .where( 

200 exists( 

201 select(1) 

202 .where(Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id) 

203 .where(Message.id > sent_reqs_last_seen_message_ids.c.initiator_last_seen_message_id) 

204 ) 

205 ) 

206 ).scalar_one() 

207 

208 received_reqs_query = select(HostRequest.conversation_id, HostRequest.recipient_last_seen_message_id).where( 

209 HostRequest.recipient_user_id == context.user_id 

210 ) 

211 received_reqs_query = where_users_column_visible(received_reqs_query, context, HostRequest.initiator_user_id) 

212 received_reqs_query = where_moderated_content_visible( 

213 received_reqs_query, context, HostRequest, is_list_operation=True 

214 ) 

215 received_reqs_last_seen_message_ids = received_reqs_query.subquery() 

216 

217 unseen_received_host_request_count = session.execute( 

218 select(func.count()) 

219 .select_from(received_reqs_last_seen_message_ids) 

220 .where( 

221 exists( 

222 select(1) 

223 .where(Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id) 

224 .where(Message.id > received_reqs_last_seen_message_ids.c.recipient_last_seen_message_id) 

225 ) 

226 ) 

227 ).scalar_one() 

228 

229 unseen_message_query = ( 

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

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

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

233 ) 

234 unseen_message_query = where_moderated_content_visible( 

235 unseen_message_query, context, GroupChat, is_list_operation=True 

236 ) 

237 unseen_message_query = ( 

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

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

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

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

242 ) 

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

244 

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

246 FriendRelationship.to_user_id == context.user_id 

247 ) 

248 pending_friend_request_query = where_users_column_visible( 

249 pending_friend_request_query, context, FriendRelationship.from_user_id 

250 ) 

251 pending_friend_request_query = pending_friend_request_query.where( 

252 FriendRelationship.status == FriendStatus.pending 

253 ) 

254 pending_friend_request_query = where_moderated_content_visible( 

255 pending_friend_request_query, context, FriendRelationship, is_list_operation=True 

256 ) 

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

258 

259 unseen_notification_count = session.execute( 

260 select(func.count()) 

261 .select_from(Notification) 

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

263 .where(Notification.is_seen == False) 

264 .where( 

265 Notification.topic_action.in_( 

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

267 ) 

268 ) 

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

270 ).scalar_one() 

271 

272 return api_pb2.PingRes( 

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

274 unseen_message_count=unseen_message_count, 

275 unseen_sent_host_request_count=unseen_sent_host_request_count, 

276 unseen_received_host_request_count=unseen_received_host_request_count, 

277 pending_friend_request_count=pending_friend_request_count, 

278 unseen_notification_count=unseen_notification_count, 

279 ) 

280 

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

282 user = session.execute( 

283 select(User) 

284 .where(username_or_id(request.user)) 

285 .options( 

286 selectinload(User.regions_visited), 

287 selectinload(User.regions_lived), 

288 selectinload(User.language_abilities), 

289 ) 

290 ).scalar_one_or_none() 

291 

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

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

294 

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

296 

297 def GetLiteUser( 

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

299 ) -> api_pb2.LiteUser: 

300 lite_user = session.execute( 

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

302 ).scalar_one_or_none() 

303 

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

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

306 

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

308 

309 def GetLiteUsers( 

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

311 ) -> api_pb2.GetLiteUsersRes: 

312 if len(request.users) > MAX_USERS_PER_QUERY: 

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

314 

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

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

317 

318 # decomposed where_username_or_id... 

319 users = ( 

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

321 .scalars() 

322 .all() 

323 ) 

324 

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

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

327 

328 res = api_pb2.GetLiteUsersRes() 

329 

330 for user in request.users: 

331 lite_user = None 

332 if user in users_by_id: 

333 lite_user = users_by_id[user] 

334 elif user in users_by_username: 

335 lite_user = users_by_username[user] 

336 

337 res.responses.append( 

338 api_pb2.LiteUserRes( 

339 query=user, 

340 not_found=lite_user is None, 

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

342 if lite_user 

343 else None, 

344 ) 

345 ) 

346 

347 return res 

348 

349 def UpdateProfile( 

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

351 ) -> empty_pb2.Empty: 

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

353 

354 if request.HasField("name"): 

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

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

357 user.name = request.name.value 

358 

359 if request.HasField("city"): 

360 user.city = request.city.value 

361 

362 if request.HasField("hometown"): 

363 if request.hometown.is_null: 

364 user.hometown = None 

365 else: 

366 user.hometown = request.hometown.value 

367 

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

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

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

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

372 user.randomized_geom = None 

373 

374 if request.HasField("radius"): 

375 user.geom_radius = request.radius.value 

376 

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

378 # user.gender = request.gender.value 

379 

380 if request.HasField("pronouns"): 

381 if request.pronouns.is_null: 

382 user.pronouns = None 

383 else: 

384 user.pronouns = request.pronouns.value 

385 

386 if request.HasField("occupation"): 

387 if request.occupation.is_null: 

388 user.occupation = None 

389 else: 

390 user.occupation = request.occupation.value 

391 

392 if request.HasField("education"): 

393 if request.education.is_null: 

394 user.education = None 

395 else: 

396 user.education = request.education.value 

397 

398 if request.HasField("about_me"): 

399 if request.about_me.is_null: 

400 user.about_me = None 

401 else: 

402 user.about_me = request.about_me.value 

403 

404 if request.HasField("things_i_like"): 

405 if request.things_i_like.is_null: 

406 user.things_i_like = None 

407 else: 

408 user.things_i_like = request.things_i_like.value 

409 

410 if request.HasField("about_place"): 

411 if request.about_place.is_null: 

412 user.about_place = None 

413 else: 

414 user.about_place = request.about_place.value 

415 

416 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

420 

421 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

425 

426 if request.HasField("language_abilities"): 

427 # delete all existing abilities 

428 for ability in user.language_abilities: 

429 session.delete(ability) 

430 session.flush() 

431 

432 # add the new ones 

433 for language_ability in request.language_abilities.value: 

434 if not language_is_allowed(language_ability.code): 

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

436 session.add( 

437 LanguageAbility( 

438 user_id=user.id, 

439 language_code=language_ability.code, 

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

441 ) 

442 ) 

443 

444 if request.HasField("regions_visited"): 

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

446 

447 for region in request.regions_visited.value: 

448 if not region_is_allowed(region): 

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

450 session.add( 

451 RegionVisited( 

452 user_id=user.id, 

453 region_code=region, 

454 ) 

455 ) 

456 

457 if request.HasField("regions_lived"): 

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

459 

460 for region in request.regions_lived.value: 

461 if not region_is_allowed(region): 

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

463 session.add( 

464 RegionLived( 

465 user_id=user.id, 

466 region_code=region, 

467 ) 

468 ) 

469 

470 if request.HasField("additional_information"): 

471 if request.additional_information.is_null: 

472 user.additional_information = None 

473 else: 

474 user.additional_information = request.additional_information.value 

475 

476 if request.HasField("max_guests"): 

477 if request.max_guests.is_null: 

478 user.max_guests = None 

479 else: 

480 user.max_guests = request.max_guests.value 

481 

482 if request.HasField("last_minute"): 

483 if request.last_minute.is_null: 

484 user.last_minute = None 

485 else: 

486 user.last_minute = request.last_minute.value 

487 

488 if request.HasField("has_pets"): 

489 if request.has_pets.is_null: 

490 user.has_pets = None 

491 else: 

492 user.has_pets = request.has_pets.value 

493 

494 if request.HasField("accepts_pets"): 

495 if request.accepts_pets.is_null: 

496 user.accepts_pets = None 

497 else: 

498 user.accepts_pets = request.accepts_pets.value 

499 

500 if request.HasField("pet_details"): 

501 if request.pet_details.is_null: 

502 user.pet_details = None 

503 else: 

504 user.pet_details = request.pet_details.value 

505 

506 if request.HasField("has_kids"): 

507 if request.has_kids.is_null: 

508 user.has_kids = None 

509 else: 

510 user.has_kids = request.has_kids.value 

511 

512 if request.HasField("accepts_kids"): 

513 if request.accepts_kids.is_null: 

514 user.accepts_kids = None 

515 else: 

516 user.accepts_kids = request.accepts_kids.value 

517 

518 if request.HasField("kid_details"): 

519 if request.kid_details.is_null: 

520 user.kid_details = None 

521 else: 

522 user.kid_details = request.kid_details.value 

523 

524 if request.HasField("has_housemates"): 

525 if request.has_housemates.is_null: 

526 user.has_housemates = None 

527 else: 

528 user.has_housemates = request.has_housemates.value 

529 

530 if request.HasField("housemate_details"): 

531 if request.housemate_details.is_null: 

532 user.housemate_details = None 

533 else: 

534 user.housemate_details = request.housemate_details.value 

535 

536 if request.HasField("wheelchair_accessible"): 

537 if request.wheelchair_accessible.is_null: 

538 user.wheelchair_accessible = None 

539 else: 

540 user.wheelchair_accessible = request.wheelchair_accessible.value 

541 

542 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

543 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

544 

545 if request.HasField("smokes_at_home"): 

546 if request.smokes_at_home.is_null: 

547 user.smokes_at_home = None 

548 else: 

549 user.smokes_at_home = request.smokes_at_home.value 

550 

551 if request.HasField("drinking_allowed"): 

552 if request.drinking_allowed.is_null: 

553 user.drinking_allowed = None 

554 else: 

555 user.drinking_allowed = request.drinking_allowed.value 

556 

557 if request.HasField("drinks_at_home"): 

558 if request.drinks_at_home.is_null: 

559 user.drinks_at_home = None 

560 else: 

561 user.drinks_at_home = request.drinks_at_home.value 

562 

563 if request.HasField("other_host_info"): 

564 if request.other_host_info.is_null: 

565 user.other_host_info = None 

566 else: 

567 user.other_host_info = request.other_host_info.value 

568 

569 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

570 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

571 

572 if request.HasField("sleeping_details"): 

573 if request.sleeping_details.is_null: 

574 user.sleeping_details = None 

575 else: 

576 user.sleeping_details = request.sleeping_details.value 

577 

578 if request.HasField("area"): 

579 if request.area.is_null: 

580 user.area = None 

581 else: 

582 user.area = request.area.value 

583 

584 if request.HasField("house_rules"): 

585 if request.house_rules.is_null: 

586 user.house_rules = None 

587 else: 

588 user.house_rules = request.house_rules.value 

589 

590 if request.HasField("parking"): 

591 if request.parking.is_null: 

592 user.parking = None 

593 else: 

594 user.parking = request.parking.value 

595 

596 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

597 user.parking_details = parkingdetails2sql[request.parking_details] 

598 

599 if request.HasField("camping_ok"): 

600 if request.camping_ok.is_null: 

601 user.camping_ok = None 

602 else: 

603 user.camping_ok = request.camping_ok.value 

604 

605 user.profile_last_updated = now() 

606 

607 return empty_pb2.Empty() 

608 

609 def ListFriends( 

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

611 ) -> api_pb2.ListFriendsRes: 

612 rels_query = select(FriendRelationship) 

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

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

615 rels_query = rels_query.where( 

616 or_( 

617 FriendRelationship.from_user_id == context.user_id, 

618 FriendRelationship.to_user_id == context.user_id, 

619 ) 

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

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

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

623 return api_pb2.ListFriendsRes( 

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

625 ) 

626 

627 def RemoveFriend( 

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

629 ) -> empty_pb2.Empty: 

630 rel_query = select(FriendRelationship) 

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

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

633 rel_query = rel_query.where( 

634 or_( 

635 and_( 

636 FriendRelationship.from_user_id == request.user_id, 

637 FriendRelationship.to_user_id == context.user_id, 

638 ), 

639 and_( 

640 FriendRelationship.from_user_id == context.user_id, 

641 FriendRelationship.to_user_id == request.user_id, 

642 ), 

643 ) 

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

645 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship) 

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

647 

648 if not rel: 

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

650 

651 session.delete(rel) 

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

653 

654 return empty_pb2.Empty() 

655 

656 def ListMutualFriends( 

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

658 ) -> api_pb2.ListMutualFriendsRes: 

659 if context.user_id == request.user_id: 

660 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

661 

662 user = session.execute( 

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

664 ).scalar_one_or_none() 

665 

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

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

668 

669 q1 = where_moderated_content_visible( 

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

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

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

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

674 context, 

675 FriendRelationship, 

676 ) 

677 

678 q2 = where_moderated_content_visible( 

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

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

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

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

683 context, 

684 FriendRelationship, 

685 ) 

686 

687 q3 = where_moderated_content_visible( 

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

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

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

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

692 context, 

693 FriendRelationship, 

694 ) 

695 

696 q4 = where_moderated_content_visible( 

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

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

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

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

701 context, 

702 FriendRelationship, 

703 ) 

704 

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

706 

707 mutual_friends = ( 

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

709 ) 

710 

711 return api_pb2.ListMutualFriendsRes( 

712 mutual_friends=[ 

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

714 for mutual_friend in mutual_friends 

715 ] 

716 ) 

717 

718 def SendFriendRequest( 

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

720 ) -> empty_pb2.Empty: 

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

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

723 

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

725 to_user = session.execute( 

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

727 ).scalar_one_or_none() 

728 

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

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

731 

732 if ( 

733 session.execute( 

734 select(FriendRelationship) 

735 .where( 

736 or_( 

737 and_( 

738 FriendRelationship.from_user_id == context.user_id, 

739 FriendRelationship.to_user_id == request.user_id, 

740 ), 

741 and_( 

742 FriendRelationship.from_user_id == request.user_id, 

743 FriendRelationship.to_user_id == context.user_id, 

744 ), 

745 ) 

746 ) 

747 .where( 

748 or_( 

749 FriendRelationship.status == FriendStatus.accepted, 

750 FriendRelationship.status == FriendStatus.pending, 

751 ) 

752 ) 

753 ).scalar_one_or_none() 

754 is not None 

755 ): 

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

757 

758 # Check if user has been sending friend requests excessively 

759 if process_rate_limits_and_check_abort( 

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

761 ): 

762 context.abort_with_error_code( 

763 grpc.StatusCode.RESOURCE_EXHAUSTED, 

764 "friend_request_rate_limit2", 

765 substitutions={"count": RATE_LIMIT_HOURS}, 

766 ) 

767 

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

769 

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

771 friend_relationship = None 

772 

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

774 nonlocal friend_relationship 

775 friend_relationship = FriendRelationship( 

776 from_user_id=user.id, 

777 to_user_id=to_user.id, 

778 status=FriendStatus.pending, 

779 moderation_state_id=moderation_state_id, 

780 ) 

781 session.add(friend_relationship) 

782 session.flush() 

783 return friend_relationship.id 

784 

785 moderation_state = create_moderation( 

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

787 ) 

788 

789 assert friend_relationship is not None # set by create_friend_relationship callback 

790 

791 notify( 

792 session, 

793 user_id=friend_relationship.to_user_id, 

794 topic_action=NotificationTopicAction.friend_request__create, 

795 key=str(friend_relationship.from_user_id), 

796 data=notification_data_pb2.FriendRequestCreate( 

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

798 ), 

799 moderation_state_id=moderation_state.id, 

800 ) 

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

802 

803 return empty_pb2.Empty() 

804 

805 def ListFriendRequests( 

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

807 ) -> api_pb2.ListFriendRequestsRes: 

808 # both sent and received 

809 sent_requests_query = select(FriendRelationship) 

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

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

812 FriendRelationship.status == FriendStatus.pending 

813 ) 

814 sent_requests_query = where_moderated_content_visible( 

815 sent_requests_query, context, FriendRelationship, is_list_operation=True 

816 ) 

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

818 

819 received_requests_query = select(FriendRelationship) 

820 received_requests_query = where_users_column_visible( 

821 received_requests_query, context, FriendRelationship.from_user_id 

822 ) 

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

824 FriendRelationship.status == FriendStatus.pending 

825 ) 

826 received_requests_query = where_moderated_content_visible( 

827 received_requests_query, context, FriendRelationship, is_list_operation=True 

828 ) 

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

830 

831 return api_pb2.ListFriendRequestsRes( 

832 sent=[ 

833 api_pb2.FriendRequest( 

834 friend_request_id=friend_request.id, 

835 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

836 user_id=friend_request.to_user.id, 

837 sent=True, 

838 ) 

839 for friend_request in sent_requests 

840 ], 

841 received=[ 

842 api_pb2.FriendRequest( 

843 friend_request_id=friend_request.id, 

844 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

845 user_id=friend_request.from_user.id, 

846 sent=False, 

847 ) 

848 for friend_request in received_requests 

849 ], 

850 ) 

851 

852 def RespondFriendRequest( 

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

854 ) -> empty_pb2.Empty: 

855 friend_request_query = select(FriendRelationship) 

856 friend_request_query = where_users_column_visible( 

857 friend_request_query, context, FriendRelationship.from_user_id 

858 ) 

859 friend_request_query = ( 

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

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

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

863 ) 

864 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

866 

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

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

869 

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

871 friend_request.time_responded = func.now() 

872 

873 session.flush() 

874 

875 if friend_request.status == FriendStatus.accepted: 

876 notify( 

877 session, 

878 user_id=friend_request.from_user_id, 

879 topic_action=NotificationTopicAction.friend_request__accept, 

880 key=str(friend_request.to_user_id), 

881 data=notification_data_pb2.FriendRequestAccept( 

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

883 ), 

884 ) 

885 

886 log_event( 

887 context, 

888 session, 

889 "friendship.request_responded", 

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

891 ) 

892 

893 return empty_pb2.Empty() 

894 

895 def CancelFriendRequest( 

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

897 ) -> empty_pb2.Empty: 

898 friend_request_query = select(FriendRelationship) 

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

900 friend_request_query = ( 

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

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

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

904 ) 

905 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

907 

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

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

910 

911 friend_request.status = FriendStatus.cancelled 

912 friend_request.time_responded = func.now() 

913 

914 # note no notifications 

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

916 

917 session.commit() 

918 

919 return empty_pb2.Empty() 

920 

921 def InitiateMediaUpload( 

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

923 ) -> api_pb2.InitiateMediaUploadRes: 

924 key = random_hex() 

925 

926 created = now() 

927 expiry = created + timedelta(minutes=20) 

928 

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

930 session.add(upload) 

931 session.commit() 

932 

933 req = media_pb2.UploadRequest( 

934 key=upload.key, 

935 type=media_pb2.UploadRequest.UploadType.IMAGE, 

936 created=Timestamp_from_datetime(upload.created), 

937 expiry=Timestamp_from_datetime(upload.expiry), 

938 max_width=2000, 

939 max_height=1600, 

940 ).SerializeToString() 

941 

942 data = b64encode(req) 

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

944 

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

946 

947 return api_pb2.InitiateMediaUploadRes( 

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

949 expiry=Timestamp_from_datetime(expiry), 

950 ) 

951 

952 def ListBadgeUsers( 

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

954 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

960 

961 badge_user_ids_query = ( 

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

963 ) 

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

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

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

967 

968 return api_pb2.ListBadgeUsersRes( 

969 user_ids=badge_user_ids[:page_size], 

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

971 ) 

972 

973 

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

975 if not response_rate: 

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

977 

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

979 if response_rate.requests < 3: 

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

981 

982 if response_rate.response_rate <= 0.33: 

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

984 

985 response_time_p33_coarsened = Duration_from_timedelta( 

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

987 ) 

988 

989 if response_rate.response_rate <= 0.66: 

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

991 

992 response_time_p66_coarsened = Duration_from_timedelta( 

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

994 ) 

995 

996 if response_rate.response_rate <= 0.90: 

997 return { 

998 "most": requests_pb2.ResponseRateMost( 

999 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1000 ) 

1001 } 

1002 else: 

1003 return { 

1004 "almost_all": requests_pb2.ResponseRateAlmostAll( 

1005 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1006 ) 

1007 } 

1008 

1009 

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

1011 query = ( 

1012 select(Reference.to_user_id, func.count(Reference.id)) 

1013 .where(Reference.to_user_id.in_(user_ids)) 

1014 .where(Reference.is_deleted == False) 

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

1016 .where(User.is_visible) 

1017 .group_by(Reference.to_user_id) 

1018 ) 

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

1020 

1021 

1022def user_model_to_pb( 

1023 db_user: User, 

1024 session: Session, 

1025 context: CouchersContext, 

1026 *, 

1027 is_admin_see_ghosts: bool = False, 

1028 is_get_user_return_ghosts: bool = False, 

1029) -> api_pb2.User: 

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

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

1032 

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

1034 if not is_admin_see_ghosts and is_not_visible(session, viewer_user_id, db_user.id): 

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

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

1037 # Return an anonymized "ghost" user profile 

1038 return api_pb2.User( 

1039 user_id=db_user.id, 

1040 is_ghost=True, 

1041 username=GHOST_USERNAME, 

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

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

1044 ) 

1045 raise GhostUserSerializationError( 

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

1047 f"Context user_id: {context.user_id}, db_user id: {db_user.id} (username: {db_user.username})" 

1048 ) 

1049 

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

1051 lat, lng = db_user.coordinates 

1052 

1053 pending_friend_request = None 

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

1055 friends_status = api_pb2.User.FriendshipStatus.NA 

1056 else: 

1057 friend_relationship = session.execute( 

1058 where_moderated_content_visible( 

1059 select(FriendRelationship) 

1060 .where( 

1061 or_( 

1062 and_( 

1063 FriendRelationship.from_user_id == context.user_id, 

1064 FriendRelationship.to_user_id == db_user.id, 

1065 ), 

1066 and_( 

1067 FriendRelationship.from_user_id == db_user.id, 

1068 FriendRelationship.to_user_id == context.user_id, 

1069 ), 

1070 ) 

1071 ) 

1072 .where( 

1073 or_( 

1074 FriendRelationship.status == FriendStatus.accepted, 

1075 FriendRelationship.status == FriendStatus.pending, 

1076 ) 

1077 ), 

1078 context, 

1079 FriendRelationship, 

1080 ) 

1081 ).scalar_one_or_none() 

1082 

1083 if friend_relationship: 

1084 if friend_relationship.status == FriendStatus.accepted: 

1085 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1086 else: 

1087 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1089 # we sent it 

1090 pending_friend_request = api_pb2.FriendRequest( 

1091 friend_request_id=friend_relationship.id, 

1092 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1093 user_id=friend_relationship.to_user.id, 

1094 sent=True, 

1095 ) 

1096 else: 

1097 # we received it 

1098 pending_friend_request = api_pb2.FriendRequest( 

1099 friend_request_id=friend_relationship.id, 

1100 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1101 user_id=friend_relationship.from_user.id, 

1102 sent=False, 

1103 ) 

1104 else: 

1105 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1106 

1107 response_rate = session.execute( 

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

1109 ).scalar_one_or_none() 

1110 

1111 avatar_upload = get_avatar_upload(session, db_user) 

1112 

1113 verification_score = 0.0 

1114 if db_user.phone_verification_verified: 

1115 verification_score += 1.0 * db_user.phone_is_verified 

1116 

1117 user = api_pb2.User( 

1118 user_id=db_user.id, 

1119 username=db_user.username, 

1120 name=db_user.name, 

1121 city=db_user.city, 

1122 hometown=db_user.hometown, 

1123 timezone=db_user.timezone, 

1124 lat=lat, 

1125 lng=lng, 

1126 radius=db_user.geom_radius, 

1127 verification=verification_score, 

1128 community_standing=db_user.community_standing, 

1129 num_references=num_references, 

1130 gender=db_user.gender, 

1131 pronouns=db_user.pronouns, 

1132 age=int(db_user.age), 

1133 joined=Timestamp_from_datetime(db_user.display_joined), 

1134 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1135 hosting_status=hostingstatus2api[db_user.hosting_status], 

1136 meetup_status=meetupstatus2api[db_user.meetup_status], 

1137 occupation=db_user.occupation, 

1138 education=db_user.education, 

1139 about_me=db_user.about_me, 

1140 things_i_like=db_user.things_i_like, 

1141 about_place=db_user.about_place, 

1142 language_abilities=[ 

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

1144 for ability in db_user.language_abilities 

1145 ], 

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

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

1148 additional_information=db_user.additional_information, 

1149 friends=friends_status, 

1150 pending_friend_request=pending_friend_request, 

1151 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1152 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1153 parking_details=parkingdetails2api[db_user.parking_details], 

1154 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1155 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1156 profile_gallery_id=db_user.profile_gallery_id, 

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

1158 .scalars() 

1159 .all(), 

1160 **get_strong_verification_fields(session, db_user), 

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

1162 ) 

1163 

1164 if db_user.max_guests is not None: 

1165 user.max_guests.value = db_user.max_guests 

1166 

1167 if db_user.last_minute is not None: 

1168 user.last_minute.value = db_user.last_minute 

1169 

1170 if db_user.has_pets is not None: 

1171 user.has_pets.value = db_user.has_pets 

1172 

1173 if db_user.accepts_pets is not None: 

1174 user.accepts_pets.value = db_user.accepts_pets 

1175 

1176 if db_user.pet_details is not None: 

1177 user.pet_details.value = db_user.pet_details 

1178 

1179 if db_user.has_kids is not None: 

1180 user.has_kids.value = db_user.has_kids 

1181 

1182 if db_user.accepts_kids is not None: 

1183 user.accepts_kids.value = db_user.accepts_kids 

1184 

1185 if db_user.kid_details is not None: 

1186 user.kid_details.value = db_user.kid_details 

1187 

1188 if db_user.has_housemates is not None: 

1189 user.has_housemates.value = db_user.has_housemates 

1190 

1191 if db_user.housemate_details is not None: 

1192 user.housemate_details.value = db_user.housemate_details 

1193 

1194 if db_user.wheelchair_accessible is not None: 

1195 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1196 

1197 if db_user.smokes_at_home is not None: 

1198 user.smokes_at_home.value = db_user.smokes_at_home 

1199 

1200 if db_user.drinking_allowed is not None: 

1201 user.drinking_allowed.value = db_user.drinking_allowed 

1202 

1203 if db_user.drinks_at_home is not None: 

1204 user.drinks_at_home.value = db_user.drinks_at_home 

1205 

1206 if db_user.other_host_info is not None: 

1207 user.other_host_info.value = db_user.other_host_info 

1208 

1209 if db_user.sleeping_details is not None: 

1210 user.sleeping_details.value = db_user.sleeping_details 

1211 

1212 if db_user.area is not None: 

1213 user.area.value = db_user.area 

1214 

1215 if db_user.house_rules is not None: 

1216 user.house_rules.value = db_user.house_rules 

1217 

1218 if db_user.parking is not None: 

1219 user.parking.value = db_user.parking 

1220 

1221 if db_user.camping_ok is not None: 

1222 user.camping_ok.value = db_user.camping_ok 

1223 

1224 return user 

1225 

1226 

1227def lite_user_to_pb( 

1228 session: Session, 

1229 lite_user: LiteUser, 

1230 context: CouchersContext, 

1231 *, 

1232 is_admin_see_ghosts: bool = False, 

1233 is_get_user_return_ghosts: bool = False, 

1234) -> api_pb2.LiteUser: 

1235 if not is_admin_see_ghosts and is_not_visible(session, context.user_id, lite_user.id): 

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

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

1238 # Return an anonymized "ghost" user profile 

1239 return api_pb2.LiteUser( 

1240 user_id=lite_user.id, 

1241 is_ghost=True, 

1242 username=GHOST_USERNAME, 

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

1244 ) 

1245 raise GhostUserSerializationError( 

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

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

1248 ) 

1249 

1250 lat, lng = get_coordinates(lite_user.geom) 

1251 

1252 return api_pb2.LiteUser( 

1253 user_id=lite_user.id, 

1254 username=lite_user.username, 

1255 name=lite_user.name, 

1256 city=lite_user.city, 

1257 age=int(lite_user.age), 

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

1259 if lite_user.avatar_filename 

1260 else None, 

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

1262 if lite_user.avatar_filename 

1263 else None, 

1264 lat=lat, 

1265 lng=lng, 

1266 radius=lite_user.radius, 

1267 has_strong_verification=lite_user.has_strong_verification, 

1268 )