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

470 statements  

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

11from sqlalchemy.sql import and_, delete, distinct, 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 username_or_id, users_visible, where_moderated_content_visible, where_users_column_visible 

57from couchers.utils import ( 

58 Duration_from_timedelta, 

59 Timestamp_from_datetime, 

60 create_coordinate, 

61 get_coordinates, 

62 is_valid_name, 

63 is_valid_user_id, 

64 is_valid_username, 

65 not_none, 

66 now, 

67) 

68 

69 

70class GhostUserSerializationError(Exception): 

71 """ 

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

73 """ 

74 

75 pass 

76 

77 

78MAX_USERS_PER_QUERY = 200 

79MAX_PAGINATION_LENGTH = 50 

80 

81hostingstatus2sql = { 

82 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

83 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

84 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

85 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

86} 

87 

88hostingstatus2api = { 

89 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

90 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

91 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

92 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

93} 

94 

95meetupstatus2sql = { 

96 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

97 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

98 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

99 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

100} 

101 

102meetupstatus2api = { 

103 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

104 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

105 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

106 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

107} 

108 

109smokinglocation2sql = { 

110 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

111 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

112 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

113 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

114 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

115} 

116 

117smokinglocation2api = { 

118 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

119 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

120 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

121 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

122 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

123} 

124 

125sleepingarrangement2sql = { 

126 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

127 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

128 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

129 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

130} 

131 

132sleepingarrangement2api = { 

133 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

134 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

135 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

136 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

137} 

138 

139parkingdetails2sql = { 

140 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

141 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

142 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

143 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

144 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

145} 

146 

147parkingdetails2api = { 

148 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

149 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

150 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

151 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

152 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

153} 

154 

155fluency2sql = { 

156 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

160} 

161 

162fluency2api = { 

163 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

167} 

168 

169 

170class API(api_pb2_grpc.APIServicer): 

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

172 # auth ought to make sure the user exists 

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

174 

175 sent_reqs_query = select(HostRequest.conversation_id, HostRequest.surfer_last_seen_message_id).where( 

176 HostRequest.surfer_user_id == context.user_id 

177 ) 

178 sent_reqs_query = where_users_column_visible(sent_reqs_query, context, HostRequest.host_user_id) 

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

180 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery() 

181 

182 unseen_sent_host_request_count = session.execute( 

183 select(func.count(distinct(sent_reqs_last_seen_message_ids.c.conversation_id))) 

184 .join( 

185 Message, 

186 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id, 

187 ) 

188 .where(sent_reqs_last_seen_message_ids.c.surfer_last_seen_message_id < Message.id) 

189 .where(Message.id != None) 

190 ).scalar_one() 

191 

192 received_reqs_query = select(HostRequest.conversation_id, HostRequest.host_last_seen_message_id).where( 

193 HostRequest.host_user_id == context.user_id 

194 ) 

195 received_reqs_query = where_users_column_visible(received_reqs_query, context, HostRequest.surfer_user_id) 

196 received_reqs_query = where_moderated_content_visible( 

197 received_reqs_query, context, HostRequest, is_list_operation=True 

198 ) 

199 received_reqs_last_seen_message_ids = received_reqs_query.subquery() 

200 

201 unseen_received_host_request_count = session.execute( 

202 select(func.count(distinct(received_reqs_last_seen_message_ids.c.conversation_id))) 

203 .join( 

204 Message, 

205 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id, 

206 ) 

207 .where(received_reqs_last_seen_message_ids.c.host_last_seen_message_id < Message.id) 

208 .where(Message.id != None) 

209 ).scalar_one() 

210 

211 unseen_message_query = ( 

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

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

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

215 ) 

216 unseen_message_query = where_moderated_content_visible( 

217 unseen_message_query, context, GroupChat, is_list_operation=True 

218 ) 

219 unseen_message_query = ( 

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

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

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

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

224 ) 

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

226 

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

228 FriendRelationship.to_user_id == context.user_id 

229 ) 

230 pending_friend_request_query = where_users_column_visible( 

231 pending_friend_request_query, context, FriendRelationship.from_user_id 

232 ) 

233 pending_friend_request_query = pending_friend_request_query.where( 

234 FriendRelationship.status == FriendStatus.pending 

235 ) 

236 pending_friend_request_query = where_moderated_content_visible( 

237 pending_friend_request_query, context, FriendRelationship, is_list_operation=True 

238 ) 

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

240 

241 unseen_notification_count = session.execute( 

242 select(func.count()) 

243 .select_from(Notification) 

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

245 .where(Notification.is_seen == False) 

246 .where( 

247 Notification.topic_action.in_( 

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

249 ) 

250 ) 

251 ).scalar_one() 

252 

253 return api_pb2.PingRes( 

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

255 unseen_message_count=unseen_message_count, 

256 unseen_sent_host_request_count=unseen_sent_host_request_count, 

257 unseen_received_host_request_count=unseen_received_host_request_count, 

258 pending_friend_request_count=pending_friend_request_count, 

259 unseen_notification_count=unseen_notification_count, 

260 ) 

261 

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

263 user = session.execute(select(User).where(username_or_id(request.user))).scalar_one_or_none() 

264 

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

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

267 

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

269 

270 def GetLiteUser( 

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

272 ) -> api_pb2.LiteUser: 

273 lite_user = session.execute( 

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

275 ).scalar_one_or_none() 

276 

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

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

279 

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

281 

282 def GetLiteUsers( 

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

284 ) -> api_pb2.GetLiteUsersRes: 

285 if len(request.users) > MAX_USERS_PER_QUERY: 

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

287 

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

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

290 

291 # decomposed where_username_or_id... 

292 users = ( 

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

294 .scalars() 

295 .all() 

296 ) 

297 

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

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

300 

301 res = api_pb2.GetLiteUsersRes() 

302 

303 for user in request.users: 

304 lite_user = None 

305 if user in users_by_id: 

306 lite_user = users_by_id[user] 

307 elif user in users_by_username: 

308 lite_user = users_by_username[user] 

309 

310 res.responses.append( 

311 api_pb2.LiteUserRes( 

312 query=user, 

313 not_found=lite_user is None, 

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

315 if lite_user 

316 else None, 

317 ) 

318 ) 

319 

320 return res 

321 

322 def UpdateProfile( 

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

324 ) -> empty_pb2.Empty: 

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

326 

327 if request.HasField("name"): 

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

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

330 user.name = request.name.value 

331 

332 if request.HasField("city"): 

333 user.city = request.city.value 

334 

335 if request.HasField("hometown"): 

336 if request.hometown.is_null: 

337 user.hometown = None 

338 else: 

339 user.hometown = request.hometown.value 

340 

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

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

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

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

345 user.randomized_geom = None 

346 

347 if request.HasField("radius"): 

348 user.geom_radius = request.radius.value 

349 

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

351 # user.gender = request.gender.value 

352 

353 if request.HasField("pronouns"): 

354 if request.pronouns.is_null: 

355 user.pronouns = None 

356 else: 

357 user.pronouns = request.pronouns.value 

358 

359 if request.HasField("occupation"): 

360 if request.occupation.is_null: 

361 user.occupation = None 

362 else: 

363 user.occupation = request.occupation.value 

364 

365 if request.HasField("education"): 

366 if request.education.is_null: 

367 user.education = None 

368 else: 

369 user.education = request.education.value 

370 

371 if request.HasField("about_me"): 

372 if request.about_me.is_null: 

373 user.about_me = None 

374 else: 

375 user.about_me = request.about_me.value 

376 

377 if request.HasField("things_i_like"): 

378 if request.things_i_like.is_null: 

379 user.things_i_like = None 

380 else: 

381 user.things_i_like = request.things_i_like.value 

382 

383 if request.HasField("about_place"): 

384 if request.about_place.is_null: 

385 user.about_place = None 

386 else: 

387 user.about_place = request.about_place.value 

388 

389 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

393 

394 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

398 

399 if request.HasField("language_abilities"): 

400 # delete all existing abilities 

401 for ability in user.language_abilities: 

402 session.delete(ability) 

403 session.flush() 

404 

405 # add the new ones 

406 for language_ability in request.language_abilities.value: 

407 if not language_is_allowed(language_ability.code): 

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

409 session.add( 

410 LanguageAbility( 

411 user_id=user.id, 

412 language_code=language_ability.code, 

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

414 ) 

415 ) 

416 

417 if request.HasField("regions_visited"): 

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

419 

420 for region in request.regions_visited.value: 

421 if not region_is_allowed(region): 

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

423 session.add( 

424 RegionVisited( 

425 user_id=user.id, 

426 region_code=region, 

427 ) 

428 ) 

429 

430 if request.HasField("regions_lived"): 

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

432 

433 for region in request.regions_lived.value: 

434 if not region_is_allowed(region): 

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

436 session.add( 

437 RegionLived( 

438 user_id=user.id, 

439 region_code=region, 

440 ) 

441 ) 

442 

443 if request.HasField("additional_information"): 

444 if request.additional_information.is_null: 

445 user.additional_information = None 

446 else: 

447 user.additional_information = request.additional_information.value 

448 

449 if request.HasField("max_guests"): 

450 if request.max_guests.is_null: 

451 user.max_guests = None 

452 else: 

453 user.max_guests = request.max_guests.value 

454 

455 if request.HasField("last_minute"): 

456 if request.last_minute.is_null: 

457 user.last_minute = None 

458 else: 

459 user.last_minute = request.last_minute.value 

460 

461 if request.HasField("has_pets"): 

462 if request.has_pets.is_null: 

463 user.has_pets = None 

464 else: 

465 user.has_pets = request.has_pets.value 

466 

467 if request.HasField("accepts_pets"): 

468 if request.accepts_pets.is_null: 

469 user.accepts_pets = None 

470 else: 

471 user.accepts_pets = request.accepts_pets.value 

472 

473 if request.HasField("pet_details"): 

474 if request.pet_details.is_null: 

475 user.pet_details = None 

476 else: 

477 user.pet_details = request.pet_details.value 

478 

479 if request.HasField("has_kids"): 

480 if request.has_kids.is_null: 

481 user.has_kids = None 

482 else: 

483 user.has_kids = request.has_kids.value 

484 

485 if request.HasField("accepts_kids"): 

486 if request.accepts_kids.is_null: 

487 user.accepts_kids = None 

488 else: 

489 user.accepts_kids = request.accepts_kids.value 

490 

491 if request.HasField("kid_details"): 

492 if request.kid_details.is_null: 

493 user.kid_details = None 

494 else: 

495 user.kid_details = request.kid_details.value 

496 

497 if request.HasField("has_housemates"): 

498 if request.has_housemates.is_null: 

499 user.has_housemates = None 

500 else: 

501 user.has_housemates = request.has_housemates.value 

502 

503 if request.HasField("housemate_details"): 

504 if request.housemate_details.is_null: 

505 user.housemate_details = None 

506 else: 

507 user.housemate_details = request.housemate_details.value 

508 

509 if request.HasField("wheelchair_accessible"): 

510 if request.wheelchair_accessible.is_null: 

511 user.wheelchair_accessible = None 

512 else: 

513 user.wheelchair_accessible = request.wheelchair_accessible.value 

514 

515 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

516 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

517 

518 if request.HasField("smokes_at_home"): 

519 if request.smokes_at_home.is_null: 

520 user.smokes_at_home = None 

521 else: 

522 user.smokes_at_home = request.smokes_at_home.value 

523 

524 if request.HasField("drinking_allowed"): 

525 if request.drinking_allowed.is_null: 

526 user.drinking_allowed = None 

527 else: 

528 user.drinking_allowed = request.drinking_allowed.value 

529 

530 if request.HasField("drinks_at_home"): 

531 if request.drinks_at_home.is_null: 

532 user.drinks_at_home = None 

533 else: 

534 user.drinks_at_home = request.drinks_at_home.value 

535 

536 if request.HasField("other_host_info"): 

537 if request.other_host_info.is_null: 

538 user.other_host_info = None 

539 else: 

540 user.other_host_info = request.other_host_info.value 

541 

542 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

543 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

544 

545 if request.HasField("sleeping_details"): 

546 if request.sleeping_details.is_null: 

547 user.sleeping_details = None 

548 else: 

549 user.sleeping_details = request.sleeping_details.value 

550 

551 if request.HasField("area"): 

552 if request.area.is_null: 

553 user.area = None 

554 else: 

555 user.area = request.area.value 

556 

557 if request.HasField("house_rules"): 

558 if request.house_rules.is_null: 

559 user.house_rules = None 

560 else: 

561 user.house_rules = request.house_rules.value 

562 

563 if request.HasField("parking"): 

564 if request.parking.is_null: 

565 user.parking = None 

566 else: 

567 user.parking = request.parking.value 

568 

569 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

570 user.parking_details = parkingdetails2sql[request.parking_details] 

571 

572 if request.HasField("camping_ok"): 

573 if request.camping_ok.is_null: 

574 user.camping_ok = None 

575 else: 

576 user.camping_ok = request.camping_ok.value 

577 

578 user.profile_last_updated = now() 

579 

580 return empty_pb2.Empty() 

581 

582 def ListFriends( 

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

584 ) -> api_pb2.ListFriendsRes: 

585 rels_query = select(FriendRelationship) 

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

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

588 rels_query = rels_query.where( 

589 or_( 

590 FriendRelationship.from_user_id == context.user_id, 

591 FriendRelationship.to_user_id == context.user_id, 

592 ) 

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

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

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

596 return api_pb2.ListFriendsRes( 

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

598 ) 

599 

600 def RemoveFriend( 

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

602 ) -> empty_pb2.Empty: 

603 rel_query = select(FriendRelationship) 

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

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

606 rel_query = rel_query.where( 

607 or_( 

608 and_( 

609 FriendRelationship.from_user_id == request.user_id, 

610 FriendRelationship.to_user_id == context.user_id, 

611 ), 

612 and_( 

613 FriendRelationship.from_user_id == context.user_id, 

614 FriendRelationship.to_user_id == request.user_id, 

615 ), 

616 ) 

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

618 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship) 

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

620 

621 if not rel: 

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

623 

624 session.delete(rel) 

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

626 

627 return empty_pb2.Empty() 

628 

629 def ListMutualFriends( 

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

631 ) -> api_pb2.ListMutualFriendsRes: 

632 if context.user_id == request.user_id: 

633 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

634 

635 user = session.execute( 

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

637 ).scalar_one_or_none() 

638 

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

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

641 

642 q1 = where_moderated_content_visible( 

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

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

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

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

647 context, 

648 FriendRelationship, 

649 ) 

650 

651 q2 = where_moderated_content_visible( 

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

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

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

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

656 context, 

657 FriendRelationship, 

658 ) 

659 

660 q3 = where_moderated_content_visible( 

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

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

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

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

665 context, 

666 FriendRelationship, 

667 ) 

668 

669 q4 = where_moderated_content_visible( 

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

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

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

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

674 context, 

675 FriendRelationship, 

676 ) 

677 

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

679 

680 mutual_friends = ( 

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

682 ) 

683 

684 return api_pb2.ListMutualFriendsRes( 

685 mutual_friends=[ 

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

687 for mutual_friend in mutual_friends 

688 ] 

689 ) 

690 

691 def SendFriendRequest( 

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

693 ) -> empty_pb2.Empty: 

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

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

696 

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

698 to_user = session.execute( 

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

700 ).scalar_one_or_none() 

701 

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

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

704 

705 if ( 

706 session.execute( 

707 select(FriendRelationship) 

708 .where( 

709 or_( 

710 and_( 

711 FriendRelationship.from_user_id == context.user_id, 

712 FriendRelationship.to_user_id == request.user_id, 

713 ), 

714 and_( 

715 FriendRelationship.from_user_id == request.user_id, 

716 FriendRelationship.to_user_id == context.user_id, 

717 ), 

718 ) 

719 ) 

720 .where( 

721 or_( 

722 FriendRelationship.status == FriendStatus.accepted, 

723 FriendRelationship.status == FriendStatus.pending, 

724 ) 

725 ) 

726 ).scalar_one_or_none() 

727 is not None 

728 ): 

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

730 

731 # Check if user has been sending friend requests excessively 

732 if process_rate_limits_and_check_abort( 

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

734 ): 

735 context.abort_with_error_code( 

736 grpc.StatusCode.RESOURCE_EXHAUSTED, 

737 "friend_request_rate_limit", 

738 substitutions={"hours": str(RATE_LIMIT_HOURS)}, 

739 ) 

740 

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

742 

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

744 friend_relationship = None 

745 

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

747 nonlocal friend_relationship 

748 friend_relationship = FriendRelationship( 

749 from_user_id=user.id, 

750 to_user_id=to_user.id, 

751 status=FriendStatus.pending, 

752 moderation_state_id=moderation_state_id, 

753 ) 

754 session.add(friend_relationship) 

755 session.flush() 

756 return friend_relationship.id 

757 

758 moderation_state = create_moderation( 

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

760 ) 

761 

762 assert friend_relationship is not None # set by create_friend_relationship callback 

763 

764 notify( 

765 session, 

766 user_id=friend_relationship.to_user_id, 

767 topic_action=NotificationTopicAction.friend_request__create, 

768 key=str(friend_relationship.from_user_id), 

769 data=notification_data_pb2.FriendRequestCreate( 

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

771 ), 

772 moderation_state_id=moderation_state.id, 

773 ) 

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

775 

776 return empty_pb2.Empty() 

777 

778 def ListFriendRequests( 

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

780 ) -> api_pb2.ListFriendRequestsRes: 

781 # both sent and received 

782 sent_requests_query = select(FriendRelationship) 

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

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

785 FriendRelationship.status == FriendStatus.pending 

786 ) 

787 sent_requests_query = where_moderated_content_visible( 

788 sent_requests_query, context, FriendRelationship, is_list_operation=True 

789 ) 

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

791 

792 received_requests_query = select(FriendRelationship) 

793 received_requests_query = where_users_column_visible( 

794 received_requests_query, context, FriendRelationship.from_user_id 

795 ) 

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

797 FriendRelationship.status == FriendStatus.pending 

798 ) 

799 received_requests_query = where_moderated_content_visible( 

800 received_requests_query, context, FriendRelationship, is_list_operation=True 

801 ) 

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

803 

804 return api_pb2.ListFriendRequestsRes( 

805 sent=[ 

806 api_pb2.FriendRequest( 

807 friend_request_id=friend_request.id, 

808 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

809 user_id=friend_request.to_user.id, 

810 sent=True, 

811 ) 

812 for friend_request in sent_requests 

813 ], 

814 received=[ 

815 api_pb2.FriendRequest( 

816 friend_request_id=friend_request.id, 

817 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

818 user_id=friend_request.from_user.id, 

819 sent=False, 

820 ) 

821 for friend_request in received_requests 

822 ], 

823 ) 

824 

825 def RespondFriendRequest( 

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

827 ) -> empty_pb2.Empty: 

828 friend_request_query = select(FriendRelationship) 

829 friend_request_query = where_users_column_visible( 

830 friend_request_query, context, FriendRelationship.from_user_id 

831 ) 

832 friend_request_query = ( 

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

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

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

836 ) 

837 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

839 

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

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

842 

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

844 friend_request.time_responded = func.now() 

845 

846 session.flush() 

847 

848 if friend_request.status == FriendStatus.accepted: 

849 notify( 

850 session, 

851 user_id=friend_request.from_user_id, 

852 topic_action=NotificationTopicAction.friend_request__accept, 

853 key=str(friend_request.to_user_id), 

854 data=notification_data_pb2.FriendRequestAccept( 

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

856 ), 

857 ) 

858 

859 log_event( 

860 context, 

861 session, 

862 "friendship.request_responded", 

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

864 ) 

865 

866 return empty_pb2.Empty() 

867 

868 def CancelFriendRequest( 

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

870 ) -> empty_pb2.Empty: 

871 friend_request_query = select(FriendRelationship) 

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

873 friend_request_query = ( 

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

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

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

877 ) 

878 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

880 

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

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

883 

884 friend_request.status = FriendStatus.cancelled 

885 friend_request.time_responded = func.now() 

886 

887 # note no notifications 

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

889 

890 session.commit() 

891 

892 return empty_pb2.Empty() 

893 

894 def InitiateMediaUpload( 

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

896 ) -> api_pb2.InitiateMediaUploadRes: 

897 key = random_hex() 

898 

899 created = now() 

900 expiry = created + timedelta(minutes=20) 

901 

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

903 session.add(upload) 

904 session.commit() 

905 

906 req = media_pb2.UploadRequest( 

907 key=upload.key, 

908 type=media_pb2.UploadRequest.UploadType.IMAGE, 

909 created=Timestamp_from_datetime(upload.created), 

910 expiry=Timestamp_from_datetime(upload.expiry), 

911 max_width=2000, 

912 max_height=1600, 

913 ).SerializeToString() 

914 

915 data = b64encode(req) 

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

917 

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

919 

920 return api_pb2.InitiateMediaUploadRes( 

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

922 expiry=Timestamp_from_datetime(expiry), 

923 ) 

924 

925 def ListBadgeUsers( 

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

927 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

933 

934 badge_user_ids_query = ( 

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

936 ) 

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

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

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

940 

941 return api_pb2.ListBadgeUsersRes( 

942 user_ids=badge_user_ids[:page_size], 

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

944 ) 

945 

946 

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

948 if not response_rate: 

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

950 

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

952 if response_rate.requests < 3: 

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

954 

955 if response_rate.response_rate <= 0.33: 

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

957 

958 response_time_p33_coarsened = Duration_from_timedelta( 

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

960 ) 

961 

962 if response_rate.response_rate <= 0.66: 

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

964 

965 response_time_p66_coarsened = Duration_from_timedelta( 

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

967 ) 

968 

969 if response_rate.response_rate <= 0.90: 

970 return { 

971 "most": requests_pb2.ResponseRateMost( 

972 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

973 ) 

974 } 

975 else: 

976 return { 

977 "almost_all": requests_pb2.ResponseRateAlmostAll( 

978 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

979 ) 

980 } 

981 

982 

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

984 query = ( 

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

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

987 .where(Reference.is_deleted == False) 

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

989 .where(User.is_visible) 

990 .group_by(Reference.to_user_id) 

991 ) 

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

993 

994 

995def user_model_to_pb( 

996 db_user: User, 

997 session: Session, 

998 context: CouchersContext, 

999 *, 

1000 is_admin_see_ghosts: bool = False, 

1001 is_get_user_return_ghosts: bool = False, 

1002) -> api_pb2.User: 

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

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

1005 

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

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

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

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

1010 # Return an anonymized "ghost" user profile 

1011 return api_pb2.User( 

1012 user_id=db_user.id, 

1013 is_ghost=True, 

1014 username=GHOST_USERNAME, 

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

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

1017 ) 

1018 raise GhostUserSerializationError( 

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

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

1021 ) 

1022 

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

1024 lat, lng = db_user.coordinates 

1025 

1026 pending_friend_request = None 

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

1028 friends_status = api_pb2.User.FriendshipStatus.NA 

1029 else: 

1030 friend_relationship = session.execute( 

1031 where_moderated_content_visible( 

1032 select(FriendRelationship) 

1033 .where( 

1034 or_( 

1035 and_( 

1036 FriendRelationship.from_user_id == context.user_id, 

1037 FriendRelationship.to_user_id == db_user.id, 

1038 ), 

1039 and_( 

1040 FriendRelationship.from_user_id == db_user.id, 

1041 FriendRelationship.to_user_id == context.user_id, 

1042 ), 

1043 ) 

1044 ) 

1045 .where( 

1046 or_( 

1047 FriendRelationship.status == FriendStatus.accepted, 

1048 FriendRelationship.status == FriendStatus.pending, 

1049 ) 

1050 ), 

1051 context, 

1052 FriendRelationship, 

1053 ) 

1054 ).scalar_one_or_none() 

1055 

1056 if friend_relationship: 

1057 if friend_relationship.status == FriendStatus.accepted: 

1058 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1059 else: 

1060 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1062 # we sent it 

1063 pending_friend_request = api_pb2.FriendRequest( 

1064 friend_request_id=friend_relationship.id, 

1065 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1066 user_id=friend_relationship.to_user.id, 

1067 sent=True, 

1068 ) 

1069 else: 

1070 # we received it 

1071 pending_friend_request = api_pb2.FriendRequest( 

1072 friend_request_id=friend_relationship.id, 

1073 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1074 user_id=friend_relationship.from_user.id, 

1075 sent=False, 

1076 ) 

1077 else: 

1078 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1079 

1080 response_rate = session.execute( 

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

1082 ).scalar_one_or_none() 

1083 

1084 avatar_upload = get_avatar_upload(session, db_user) 

1085 

1086 verification_score = 0.0 

1087 if db_user.phone_verification_verified: 

1088 verification_score += 1.0 * db_user.phone_is_verified 

1089 

1090 user = api_pb2.User( 

1091 user_id=db_user.id, 

1092 username=db_user.username, 

1093 name=db_user.name, 

1094 city=db_user.city, 

1095 hometown=db_user.hometown, 

1096 timezone=db_user.timezone, 

1097 lat=lat, 

1098 lng=lng, 

1099 radius=db_user.geom_radius, 

1100 verification=verification_score, 

1101 community_standing=db_user.community_standing, 

1102 num_references=num_references, 

1103 gender=db_user.gender, 

1104 pronouns=db_user.pronouns, 

1105 age=int(db_user.age), 

1106 joined=Timestamp_from_datetime(db_user.display_joined), 

1107 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1108 hosting_status=hostingstatus2api[db_user.hosting_status], 

1109 meetup_status=meetupstatus2api[db_user.meetup_status], 

1110 occupation=db_user.occupation, 

1111 education=db_user.education, 

1112 about_me=db_user.about_me, 

1113 things_i_like=db_user.things_i_like, 

1114 about_place=db_user.about_place, 

1115 language_abilities=[ 

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

1117 for ability in db_user.language_abilities 

1118 ], 

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

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

1121 additional_information=db_user.additional_information, 

1122 friends=friends_status, 

1123 pending_friend_request=pending_friend_request, 

1124 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1125 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1126 parking_details=parkingdetails2api[db_user.parking_details], 

1127 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1128 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1129 profile_gallery_id=db_user.profile_gallery_id, 

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

1131 .scalars() 

1132 .all(), 

1133 **get_strong_verification_fields(session, db_user), 

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

1135 ) 

1136 

1137 if db_user.max_guests is not None: 

1138 user.max_guests.value = db_user.max_guests 

1139 

1140 if db_user.last_minute is not None: 

1141 user.last_minute.value = db_user.last_minute 

1142 

1143 if db_user.has_pets is not None: 

1144 user.has_pets.value = db_user.has_pets 

1145 

1146 if db_user.accepts_pets is not None: 

1147 user.accepts_pets.value = db_user.accepts_pets 

1148 

1149 if db_user.pet_details is not None: 

1150 user.pet_details.value = db_user.pet_details 

1151 

1152 if db_user.has_kids is not None: 

1153 user.has_kids.value = db_user.has_kids 

1154 

1155 if db_user.accepts_kids is not None: 

1156 user.accepts_kids.value = db_user.accepts_kids 

1157 

1158 if db_user.kid_details is not None: 

1159 user.kid_details.value = db_user.kid_details 

1160 

1161 if db_user.has_housemates is not None: 

1162 user.has_housemates.value = db_user.has_housemates 

1163 

1164 if db_user.housemate_details is not None: 

1165 user.housemate_details.value = db_user.housemate_details 

1166 

1167 if db_user.wheelchair_accessible is not None: 

1168 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1169 

1170 if db_user.smokes_at_home is not None: 

1171 user.smokes_at_home.value = db_user.smokes_at_home 

1172 

1173 if db_user.drinking_allowed is not None: 

1174 user.drinking_allowed.value = db_user.drinking_allowed 

1175 

1176 if db_user.drinks_at_home is not None: 

1177 user.drinks_at_home.value = db_user.drinks_at_home 

1178 

1179 if db_user.other_host_info is not None: 

1180 user.other_host_info.value = db_user.other_host_info 

1181 

1182 if db_user.sleeping_details is not None: 

1183 user.sleeping_details.value = db_user.sleeping_details 

1184 

1185 if db_user.area is not None: 

1186 user.area.value = db_user.area 

1187 

1188 if db_user.house_rules is not None: 

1189 user.house_rules.value = db_user.house_rules 

1190 

1191 if db_user.parking is not None: 

1192 user.parking.value = db_user.parking 

1193 

1194 if db_user.camping_ok is not None: 

1195 user.camping_ok.value = db_user.camping_ok 

1196 

1197 return user 

1198 

1199 

1200def lite_user_to_pb( 

1201 session: Session, 

1202 lite_user: LiteUser, 

1203 context: CouchersContext, 

1204 *, 

1205 is_admin_see_ghosts: bool = False, 

1206 is_get_user_return_ghosts: bool = False, 

1207) -> api_pb2.LiteUser: 

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

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

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

1211 # Return an anonymized "ghost" user profile 

1212 return api_pb2.LiteUser( 

1213 user_id=lite_user.id, 

1214 is_ghost=True, 

1215 username=GHOST_USERNAME, 

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

1217 ) 

1218 raise GhostUserSerializationError( 

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

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

1221 ) 

1222 

1223 lat, lng = get_coordinates(lite_user.geom) 

1224 

1225 return api_pb2.LiteUser( 

1226 user_id=lite_user.id, 

1227 username=lite_user.username, 

1228 name=lite_user.name, 

1229 city=lite_user.city, 

1230 age=int(lite_user.age), 

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

1232 if lite_user.avatar_filename 

1233 else None, 

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

1235 if lite_user.avatar_filename 

1236 else None, 

1237 lat=lat, 

1238 lng=lng, 

1239 radius=lite_user.radius, 

1240 has_strong_verification=lite_user.has_strong_verification, 

1241 )