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

478 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-23 04:10 +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.abuse import maybe_log_nonvisible_user_access 

15from couchers.config import config 

16from couchers.constants import GHOST_USERNAME 

17from couchers.context import CouchersContext, make_notification_user_context 

18from couchers.crypto import b64encode, generate_hash_signature, random_hex 

19from couchers.event_log import log_event 

20from couchers.helpers.completed_profile import has_completed_profile 

21from couchers.helpers.references import where_references_not_hidden_by_reciprocity 

22from couchers.helpers.strong_verification import get_strong_verification_fields 

23from couchers.materialized_views import LiteUser, UserResponseRate 

24from couchers.models import ( 

25 FriendRelationship, 

26 FriendStatus, 

27 GroupChat, 

28 GroupChatSubscription, 

29 HostingStatus, 

30 HostRequest, 

31 InitiatedUpload, 

32 LanguageAbility, 

33 LanguageFluency, 

34 MeetupStatus, 

35 Message, 

36 ModerationObjectType, 

37 NonvisibleUserAccessType, 

38 Notification, 

39 NotificationDeliveryType, 

40 ParkingDetails, 

41 RateLimitAction, 

42 Reference, 

43 RegionLived, 

44 RegionVisited, 

45 SleepingArrangement, 

46 SmokingLocation, 

47 User, 

48 UserBadge, 

49) 

50from couchers.models.notifications import NotificationTopicAction 

51from couchers.models.uploads import get_avatar_upload 

52from couchers.moderation.utils import create_moderation 

53from couchers.notifications.notify import notify 

54from couchers.notifications.settings import get_topic_actions_by_delivery_type 

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

56from couchers.rate_limits.check import process_rate_limits_and_check_abort 

57from couchers.rate_limits.definitions import RATE_LIMIT_HOURS 

58from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

59from couchers.servicers.blocking import is_not_visible 

60from couchers.sql import ( 

61 moderation_state_column_visible, 

62 username_or_id, 

63 users_visible, 

64 where_moderated_content_visible, 

65 where_users_column_visible, 

66) 

67from couchers.utils import ( 

68 Duration_from_timedelta, 

69 Timestamp_from_datetime, 

70 create_coordinate, 

71 get_coordinates, 

72 is_valid_name, 

73 is_valid_user_id, 

74 is_valid_username, 

75 not_none, 

76 now, 

77) 

78 

79 

80class GhostUserSerializationError(Exception): 

81 """ 

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

83 """ 

84 

85 pass 

86 

87 

88MAX_USERS_PER_QUERY = 200 

89MAX_PAGINATION_LENGTH = 50 

90 

91hostingstatus2sql = { 

92 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

93 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

94 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

95 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

96} 

97 

98hostingstatus2api = { 

99 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

100 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

101 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

102 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

103} 

104 

105meetupstatus2sql = { 

106 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

107 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

108 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

109 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

110} 

111 

112meetupstatus2api = { 

113 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

114 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

115 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

116 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

117} 

118 

119smokinglocation2sql = { 

120 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

121 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

122 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

123 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

124 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

125} 

126 

127smokinglocation2api = { 

128 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

129 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

130 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

131 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

132 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

133} 

134 

135sleepingarrangement2sql = { 

136 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

137 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

138 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

139 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

140} 

141 

142sleepingarrangement2api = { 

143 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

144 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

145 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

146 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

147} 

148 

149parkingdetails2sql = { 

150 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

151 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

152 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

153 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

154 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

155} 

156 

157parkingdetails2api = { 

158 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

159 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

160 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

161 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

162 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

163} 

164 

165fluency2sql = { 

166 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

170} 

171 

172fluency2api = { 

173 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

177} 

178 

179 

180class API(api_pb2_grpc.APIServicer): 

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

182 # auth ought to make sure the user exists 

183 user = session.execute( 

184 select(User) 

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

186 .options( 

187 selectinload(User.regions_visited), 

188 selectinload(User.regions_lived), 

189 selectinload(User.language_abilities), 

190 ) 

191 ).scalar_one() 

192 

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

194 HostRequest.initiator_user_id == context.user_id 

195 ) 

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

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

198 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery() 

199 

200 unseen_sent_host_request_count = session.execute( 

201 select(func.count()) 

202 .select_from(sent_reqs_last_seen_message_ids) 

203 .where( 

204 exists( 

205 select(1) 

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

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

208 ) 

209 ) 

210 ).scalar_one() 

211 

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

213 HostRequest.recipient_user_id == context.user_id 

214 ) 

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

216 received_reqs_query = where_moderated_content_visible( 

217 received_reqs_query, context, HostRequest, is_list_operation=True 

218 ) 

219 received_reqs_last_seen_message_ids = received_reqs_query.subquery() 

220 

221 unseen_received_host_request_count = session.execute( 

222 select(func.count()) 

223 .select_from(received_reqs_last_seen_message_ids) 

224 .where( 

225 exists( 

226 select(1) 

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

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

229 ) 

230 ) 

231 ).scalar_one() 

232 

233 unseen_message_query = ( 

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

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

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

237 ) 

238 unseen_message_query = where_moderated_content_visible( 

239 unseen_message_query, context, GroupChat, is_list_operation=True 

240 ) 

241 unseen_message_query = ( 

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

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

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

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

246 ) 

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

248 

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

250 FriendRelationship.to_user_id == context.user_id 

251 ) 

252 pending_friend_request_query = where_users_column_visible( 

253 pending_friend_request_query, context, FriendRelationship.from_user_id 

254 ) 

255 pending_friend_request_query = pending_friend_request_query.where( 

256 FriendRelationship.status == FriendStatus.pending 

257 ) 

258 pending_friend_request_query = where_moderated_content_visible( 

259 pending_friend_request_query, context, FriendRelationship, is_list_operation=True 

260 ) 

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

262 

263 unseen_notification_count = session.execute( 

264 select(func.count()) 

265 .select_from(Notification) 

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

267 .where(Notification.is_seen == False) 

268 .where( 

269 Notification.topic_action.in_( 

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

271 ) 

272 ) 

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

274 ).scalar_one() 

275 

276 return api_pb2.PingRes( 

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

278 unseen_message_count=unseen_message_count, 

279 unseen_sent_host_request_count=unseen_sent_host_request_count, 

280 unseen_received_host_request_count=unseen_received_host_request_count, 

281 pending_friend_request_count=pending_friend_request_count, 

282 unseen_notification_count=unseen_notification_count, 

283 ) 

284 

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

286 user = session.execute( 

287 select(User) 

288 .where(username_or_id(request.user)) 

289 .options( 

290 selectinload(User.regions_visited), 

291 selectinload(User.regions_lived), 

292 selectinload(User.language_abilities), 

293 ) 

294 ).scalar_one_or_none() 

295 

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

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

298 

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

300 

301 def GetLiteUser( 

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

303 ) -> api_pb2.LiteUser: 

304 lite_user = session.execute( 

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

306 ).scalar_one_or_none() 

307 

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

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

310 

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

312 

313 def GetLiteUsers( 

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

315 ) -> api_pb2.GetLiteUsersRes: 

316 if len(request.users) > MAX_USERS_PER_QUERY: 

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

318 

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

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

321 

322 # decomposed where_username_or_id... 

323 users = ( 

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

325 .scalars() 

326 .all() 

327 ) 

328 

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

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

331 

332 res = api_pb2.GetLiteUsersRes() 

333 

334 for user in request.users: 

335 lite_user = None 

336 if user in users_by_id: 

337 lite_user = users_by_id[user] 

338 elif user in users_by_username: 

339 lite_user = users_by_username[user] 

340 

341 res.responses.append( 

342 api_pb2.LiteUserRes( 

343 query=user, 

344 not_found=lite_user is None, 

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

346 if lite_user 

347 else None, 

348 ) 

349 ) 

350 

351 return res 

352 

353 def UpdateProfile( 

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

355 ) -> empty_pb2.Empty: 

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

357 

358 if request.HasField("name"): 

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

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

361 user.name = request.name.value 

362 

363 if request.HasField("city"): 

364 user.city = request.city.value 

365 

366 if request.HasField("hometown"): 

367 if request.hometown.is_null: 

368 user.hometown = None 

369 else: 

370 user.hometown = request.hometown.value 

371 

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

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

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

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

376 user.randomized_geom = None 

377 

378 if request.HasField("radius"): 

379 user.geom_radius = request.radius.value 

380 

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

382 # user.gender = request.gender.value 

383 

384 if request.HasField("pronouns"): 

385 if request.pronouns.is_null: 

386 user.pronouns = None 

387 else: 

388 user.pronouns = request.pronouns.value 

389 

390 if request.HasField("occupation"): 

391 if request.occupation.is_null: 

392 user.occupation = None 

393 else: 

394 user.occupation = request.occupation.value 

395 

396 if request.HasField("education"): 

397 if request.education.is_null: 

398 user.education = None 

399 else: 

400 user.education = request.education.value 

401 

402 if request.HasField("about_me"): 

403 if request.about_me.is_null: 

404 user.about_me = None 

405 else: 

406 user.about_me = request.about_me.value 

407 

408 if request.HasField("things_i_like"): 

409 if request.things_i_like.is_null: 

410 user.things_i_like = None 

411 else: 

412 user.things_i_like = request.things_i_like.value 

413 

414 if request.HasField("about_place"): 

415 if request.about_place.is_null: 

416 user.about_place = None 

417 else: 

418 user.about_place = request.about_place.value 

419 

420 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

424 

425 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

429 

430 if request.HasField("language_abilities"): 

431 # delete all existing abilities 

432 for ability in user.language_abilities: 

433 session.delete(ability) 

434 session.flush() 

435 

436 # add the new ones 

437 for language_ability in request.language_abilities.value: 

438 if not language_is_allowed(language_ability.code): 

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

440 session.add( 

441 LanguageAbility( 

442 user_id=user.id, 

443 language_code=language_ability.code, 

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

445 ) 

446 ) 

447 

448 if request.HasField("regions_visited"): 

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

450 

451 for region in request.regions_visited.value: 

452 if not region_is_allowed(region): 

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

454 session.add( 

455 RegionVisited( 

456 user_id=user.id, 

457 region_code=region, 

458 ) 

459 ) 

460 

461 if request.HasField("regions_lived"): 

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

463 

464 for region in request.regions_lived.value: 

465 if not region_is_allowed(region): 

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

467 session.add( 

468 RegionLived( 

469 user_id=user.id, 

470 region_code=region, 

471 ) 

472 ) 

473 

474 if request.HasField("additional_information"): 

475 if request.additional_information.is_null: 

476 user.additional_information = None 

477 else: 

478 user.additional_information = request.additional_information.value 

479 

480 if request.HasField("max_guests"): 

481 if request.max_guests.is_null: 

482 user.max_guests = None 

483 else: 

484 user.max_guests = request.max_guests.value 

485 

486 if request.HasField("last_minute"): 

487 if request.last_minute.is_null: 

488 user.last_minute = None 

489 else: 

490 user.last_minute = request.last_minute.value 

491 

492 if request.HasField("has_pets"): 

493 if request.has_pets.is_null: 

494 user.has_pets = None 

495 else: 

496 user.has_pets = request.has_pets.value 

497 

498 if request.HasField("accepts_pets"): 

499 if request.accepts_pets.is_null: 

500 user.accepts_pets = None 

501 else: 

502 user.accepts_pets = request.accepts_pets.value 

503 

504 if request.HasField("pet_details"): 

505 if request.pet_details.is_null: 

506 user.pet_details = None 

507 else: 

508 user.pet_details = request.pet_details.value 

509 

510 if request.HasField("has_kids"): 

511 if request.has_kids.is_null: 

512 user.has_kids = None 

513 else: 

514 user.has_kids = request.has_kids.value 

515 

516 if request.HasField("accepts_kids"): 

517 if request.accepts_kids.is_null: 

518 user.accepts_kids = None 

519 else: 

520 user.accepts_kids = request.accepts_kids.value 

521 

522 if request.HasField("kid_details"): 

523 if request.kid_details.is_null: 

524 user.kid_details = None 

525 else: 

526 user.kid_details = request.kid_details.value 

527 

528 if request.HasField("has_housemates"): 

529 if request.has_housemates.is_null: 

530 user.has_housemates = None 

531 else: 

532 user.has_housemates = request.has_housemates.value 

533 

534 if request.HasField("housemate_details"): 

535 if request.housemate_details.is_null: 

536 user.housemate_details = None 

537 else: 

538 user.housemate_details = request.housemate_details.value 

539 

540 if request.HasField("wheelchair_accessible"): 

541 if request.wheelchair_accessible.is_null: 

542 user.wheelchair_accessible = None 

543 else: 

544 user.wheelchair_accessible = request.wheelchair_accessible.value 

545 

546 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

547 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

548 

549 if request.HasField("smokes_at_home"): 

550 if request.smokes_at_home.is_null: 

551 user.smokes_at_home = None 

552 else: 

553 user.smokes_at_home = request.smokes_at_home.value 

554 

555 if request.HasField("drinking_allowed"): 

556 if request.drinking_allowed.is_null: 

557 user.drinking_allowed = None 

558 else: 

559 user.drinking_allowed = request.drinking_allowed.value 

560 

561 if request.HasField("drinks_at_home"): 

562 if request.drinks_at_home.is_null: 

563 user.drinks_at_home = None 

564 else: 

565 user.drinks_at_home = request.drinks_at_home.value 

566 

567 if request.HasField("other_host_info"): 

568 if request.other_host_info.is_null: 

569 user.other_host_info = None 

570 else: 

571 user.other_host_info = request.other_host_info.value 

572 

573 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

574 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

575 

576 if request.HasField("sleeping_details"): 

577 if request.sleeping_details.is_null: 

578 user.sleeping_details = None 

579 else: 

580 user.sleeping_details = request.sleeping_details.value 

581 

582 if request.HasField("area"): 

583 if request.area.is_null: 

584 user.area = None 

585 else: 

586 user.area = request.area.value 

587 

588 if request.HasField("house_rules"): 

589 if request.house_rules.is_null: 

590 user.house_rules = None 

591 else: 

592 user.house_rules = request.house_rules.value 

593 

594 if request.HasField("parking"): 

595 if request.parking.is_null: 

596 user.parking = None 

597 else: 

598 user.parking = request.parking.value 

599 

600 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

601 user.parking_details = parkingdetails2sql[request.parking_details] 

602 

603 if request.HasField("camping_ok"): 

604 if request.camping_ok.is_null: 

605 user.camping_ok = None 

606 else: 

607 user.camping_ok = request.camping_ok.value 

608 

609 user.profile_last_updated = now() 

610 

611 return empty_pb2.Empty() 

612 

613 def ListFriends( 

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

615 ) -> api_pb2.ListFriendsRes: 

616 rels_query = select(FriendRelationship) 

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

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

619 rels_query = rels_query.where( 

620 or_( 

621 FriendRelationship.from_user_id == context.user_id, 

622 FriendRelationship.to_user_id == context.user_id, 

623 ) 

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

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

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

627 return api_pb2.ListFriendsRes( 

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

629 ) 

630 

631 def RemoveFriend( 

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

633 ) -> empty_pb2.Empty: 

634 rel_query = select(FriendRelationship) 

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

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

637 rel_query = rel_query.where( 

638 or_( 

639 and_( 

640 FriendRelationship.from_user_id == request.user_id, 

641 FriendRelationship.to_user_id == context.user_id, 

642 ), 

643 and_( 

644 FriendRelationship.from_user_id == context.user_id, 

645 FriendRelationship.to_user_id == request.user_id, 

646 ), 

647 ) 

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

649 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship) 

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

651 

652 if not rel: 

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

654 

655 session.delete(rel) 

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

657 

658 return empty_pb2.Empty() 

659 

660 def ListMutualFriends( 

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

662 ) -> api_pb2.ListMutualFriendsRes: 

663 if context.user_id == request.user_id: 

664 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

665 

666 user = session.execute( 

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

668 ).scalar_one_or_none() 

669 

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

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

672 

673 q1 = where_moderated_content_visible( 

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

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

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

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

678 context, 

679 FriendRelationship, 

680 ) 

681 

682 q2 = where_moderated_content_visible( 

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

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

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

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

687 context, 

688 FriendRelationship, 

689 ) 

690 

691 q3 = where_moderated_content_visible( 

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

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

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

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

696 context, 

697 FriendRelationship, 

698 ) 

699 

700 q4 = where_moderated_content_visible( 

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

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

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

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

705 context, 

706 FriendRelationship, 

707 ) 

708 

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

710 

711 mutual_friends = ( 

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

713 ) 

714 

715 return api_pb2.ListMutualFriendsRes( 

716 mutual_friends=[ 

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

718 for mutual_friend in mutual_friends 

719 ] 

720 ) 

721 

722 def SendFriendRequest( 

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

724 ) -> empty_pb2.Empty: 

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

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

727 

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

729 if not has_completed_profile(session, user): 

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

731 

732 to_user = session.execute( 

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

734 ).scalar_one_or_none() 

735 

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

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

738 

739 if ( 

740 session.execute( 

741 select(FriendRelationship) 

742 .where( 

743 or_( 

744 and_( 

745 FriendRelationship.from_user_id == context.user_id, 

746 FriendRelationship.to_user_id == request.user_id, 

747 ), 

748 and_( 

749 FriendRelationship.from_user_id == request.user_id, 

750 FriendRelationship.to_user_id == context.user_id, 

751 ), 

752 ) 

753 ) 

754 .where( 

755 or_( 

756 FriendRelationship.status == FriendStatus.accepted, 

757 FriendRelationship.status == FriendStatus.pending, 

758 ) 

759 ) 

760 ).scalar_one_or_none() 

761 is not None 

762 ): 

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

764 

765 # Check if user has been sending friend requests excessively 

766 if process_rate_limits_and_check_abort( 

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

768 ): 

769 context.abort_with_error_code( 

770 grpc.StatusCode.RESOURCE_EXHAUSTED, 

771 "friend_request_rate_limit2", 

772 substitutions={"count": RATE_LIMIT_HOURS}, 

773 ) 

774 

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

776 

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

778 friend_relationship = None 

779 

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

781 nonlocal friend_relationship 

782 friend_relationship = FriendRelationship( 

783 from_user_id=user.id, 

784 to_user_id=to_user.id, 

785 status=FriendStatus.pending, 

786 moderation_state_id=moderation_state_id, 

787 ) 

788 session.add(friend_relationship) 

789 session.flush() 

790 return friend_relationship.id 

791 

792 moderation_state = create_moderation( 

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

794 ) 

795 

796 assert friend_relationship is not None # set by create_friend_relationship callback 

797 

798 notify( 

799 session, 

800 user_id=friend_relationship.to_user_id, 

801 topic_action=NotificationTopicAction.friend_request__create, 

802 key=str(friend_relationship.from_user_id), 

803 data=notification_data_pb2.FriendRequestCreate( 

804 other_user=user_model_to_pb( 

805 friend_relationship.from_user, 

806 session, 

807 make_notification_user_context(user_id=friend_relationship.to_user_id), 

808 ), 

809 ), 

810 moderation_state_id=moderation_state.id, 

811 ) 

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

813 

814 return empty_pb2.Empty() 

815 

816 def ListFriendRequests( 

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

818 ) -> api_pb2.ListFriendRequestsRes: 

819 # both sent and received 

820 sent_requests_query = select(FriendRelationship) 

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

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

823 FriendRelationship.status == FriendStatus.pending 

824 ) 

825 sent_requests_query = where_moderated_content_visible( 

826 sent_requests_query, context, FriendRelationship, is_list_operation=True 

827 ) 

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

829 

830 received_requests_query = select(FriendRelationship) 

831 received_requests_query = where_users_column_visible( 

832 received_requests_query, context, FriendRelationship.from_user_id 

833 ) 

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

835 FriendRelationship.status == FriendStatus.pending 

836 ) 

837 received_requests_query = where_moderated_content_visible( 

838 received_requests_query, context, FriendRelationship, is_list_operation=True 

839 ) 

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

841 

842 return api_pb2.ListFriendRequestsRes( 

843 sent=[ 

844 api_pb2.FriendRequest( 

845 friend_request_id=friend_request.id, 

846 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

847 user_id=friend_request.to_user.id, 

848 sent=True, 

849 ) 

850 for friend_request in sent_requests 

851 ], 

852 received=[ 

853 api_pb2.FriendRequest( 

854 friend_request_id=friend_request.id, 

855 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

856 user_id=friend_request.from_user.id, 

857 sent=False, 

858 ) 

859 for friend_request in received_requests 

860 ], 

861 ) 

862 

863 def RespondFriendRequest( 

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

865 ) -> empty_pb2.Empty: 

866 friend_request_query = select(FriendRelationship) 

867 friend_request_query = where_users_column_visible( 

868 friend_request_query, context, FriendRelationship.from_user_id 

869 ) 

870 friend_request_query = ( 

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

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

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

874 ) 

875 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

877 

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

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

880 

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

882 friend_request.time_responded = func.now() 

883 

884 session.flush() 

885 

886 if friend_request.status == FriendStatus.accepted: 

887 notify( 

888 session, 

889 user_id=friend_request.from_user_id, 

890 topic_action=NotificationTopicAction.friend_request__accept, 

891 key=str(friend_request.to_user_id), 

892 data=notification_data_pb2.FriendRequestAccept( 

893 other_user=user_model_to_pb( 

894 friend_request.to_user, 

895 session, 

896 make_notification_user_context(user_id=friend_request.from_user_id), 

897 ), 

898 ), 

899 ) 

900 

901 log_event( 

902 context, 

903 session, 

904 "friendship.request_responded", 

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

906 ) 

907 

908 return empty_pb2.Empty() 

909 

910 def CancelFriendRequest( 

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

912 ) -> empty_pb2.Empty: 

913 friend_request_query = select(FriendRelationship) 

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

915 friend_request_query = ( 

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

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

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

919 ) 

920 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

922 

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

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

925 

926 friend_request.status = FriendStatus.cancelled 

927 friend_request.time_responded = func.now() 

928 

929 # note no notifications 

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

931 

932 session.commit() 

933 

934 return empty_pb2.Empty() 

935 

936 def InitiateMediaUpload( 

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

938 ) -> api_pb2.InitiateMediaUploadRes: 

939 key = random_hex() 

940 

941 created = now() 

942 expiry = created + timedelta(minutes=20) 

943 

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

945 session.add(upload) 

946 session.commit() 

947 

948 req = media_pb2.UploadRequest( 

949 key=upload.key, 

950 type=media_pb2.UploadRequest.UploadType.IMAGE, 

951 created=Timestamp_from_datetime(upload.created), 

952 expiry=Timestamp_from_datetime(upload.expiry), 

953 max_width=2000, 

954 max_height=1600, 

955 ).SerializeToString() 

956 

957 data = b64encode(req) 

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

959 

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

961 

962 return api_pb2.InitiateMediaUploadRes( 

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

964 expiry=Timestamp_from_datetime(expiry), 

965 ) 

966 

967 def ListBadgeUsers( 

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

969 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

975 

976 badge_user_ids_query = ( 

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

978 ) 

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

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

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

982 

983 return api_pb2.ListBadgeUsersRes( 

984 user_ids=badge_user_ids[:page_size], 

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

986 ) 

987 

988 

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

990 if not response_rate: 

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

992 

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

994 if response_rate.requests < 3: 

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

996 

997 if response_rate.response_rate <= 0.33: 

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

999 

1000 response_time_p33_coarsened = Duration_from_timedelta( 

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

1002 ) 

1003 

1004 if response_rate.response_rate <= 0.66: 

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

1006 

1007 response_time_p66_coarsened = Duration_from_timedelta( 

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

1009 ) 

1010 

1011 if response_rate.response_rate <= 0.90: 

1012 return { 

1013 "most": requests_pb2.ResponseRateMost( 

1014 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1015 ) 

1016 } 

1017 else: 

1018 return { 

1019 "almost_all": requests_pb2.ResponseRateAlmostAll( 

1020 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1021 ) 

1022 } 

1023 

1024 

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

1026 query = where_moderated_content_visible( 

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

1028 ) 

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

1030 query = where_references_not_hidden_by_reciprocity(query) 

1031 query = ( 

1032 query.where(Reference.to_user_id.in_(user_ids)) 

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

1034 .where(User.is_visible) 

1035 .group_by(Reference.to_user_id) 

1036 ) 

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

1038 

1039 

1040def user_model_to_pb( 

1041 db_user: User, 

1042 session: Session, 

1043 context: CouchersContext, 

1044 *, 

1045 is_admin_see_ghosts: bool = False, 

1046 is_get_user_return_ghosts: bool = False, 

1047) -> api_pb2.User: 

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

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

1050 

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

1052 if not is_admin_see_ghosts and is_not_visible( 

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

1054 ): 

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

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

1057 maybe_log_nonvisible_user_access( 

1058 context, 

1059 db_user, 

1060 access_type=NonvisibleUserAccessType.ghost_served, 

1061 actor_user_id=viewer_user_id, 

1062 ) 

1063 # Return an anonymized "ghost" user profile 

1064 return api_pb2.User( 

1065 user_id=db_user.id, 

1066 is_ghost=True, 

1067 username=GHOST_USERNAME, 

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

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

1070 ) 

1071 raise GhostUserSerializationError( 

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

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

1074 ) 

1075 

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

1077 lat, lng = db_user.coordinates 

1078 

1079 pending_friend_request = None 

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

1081 friends_status = api_pb2.User.FriendshipStatus.NA 

1082 else: 

1083 friend_relationship = session.execute( 

1084 where_moderated_content_visible( 

1085 select(FriendRelationship) 

1086 .where( 

1087 or_( 

1088 and_( 

1089 FriendRelationship.from_user_id == context.user_id, 

1090 FriendRelationship.to_user_id == db_user.id, 

1091 ), 

1092 and_( 

1093 FriendRelationship.from_user_id == db_user.id, 

1094 FriendRelationship.to_user_id == context.user_id, 

1095 ), 

1096 ) 

1097 ) 

1098 .where( 

1099 or_( 

1100 FriendRelationship.status == FriendStatus.accepted, 

1101 FriendRelationship.status == FriendStatus.pending, 

1102 ) 

1103 ), 

1104 context, 

1105 FriendRelationship, 

1106 ) 

1107 ).scalar_one_or_none() 

1108 

1109 if friend_relationship: 

1110 if friend_relationship.status == FriendStatus.accepted: 

1111 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1112 else: 

1113 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1115 # we sent it 

1116 pending_friend_request = api_pb2.FriendRequest( 

1117 friend_request_id=friend_relationship.id, 

1118 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1119 user_id=friend_relationship.to_user.id, 

1120 sent=True, 

1121 ) 

1122 else: 

1123 # we received it 

1124 pending_friend_request = api_pb2.FriendRequest( 

1125 friend_request_id=friend_relationship.id, 

1126 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1127 user_id=friend_relationship.from_user.id, 

1128 sent=False, 

1129 ) 

1130 else: 

1131 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1132 

1133 response_rate = session.execute( 

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

1135 ).scalar_one_or_none() 

1136 

1137 avatar_upload = get_avatar_upload(session, db_user) 

1138 

1139 verification_score = 0.0 

1140 if db_user.phone_verification_verified: 

1141 verification_score += 1.0 * db_user.phone_is_verified 

1142 

1143 user = api_pb2.User( 

1144 user_id=db_user.id, 

1145 username=db_user.username, 

1146 name=db_user.name, 

1147 city=db_user.city, 

1148 hometown=db_user.hometown, 

1149 timezone=db_user.timezone, 

1150 lat=lat, 

1151 lng=lng, 

1152 radius=db_user.geom_radius, 

1153 verification=verification_score, 

1154 community_standing=db_user.community_standing, 

1155 num_references=num_references, 

1156 gender=db_user.gender, 

1157 pronouns=db_user.pronouns, 

1158 age=int(db_user.age), 

1159 joined=Timestamp_from_datetime(db_user.display_joined), 

1160 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1161 hosting_status=hostingstatus2api[db_user.hosting_status], 

1162 meetup_status=meetupstatus2api[db_user.meetup_status], 

1163 occupation=db_user.occupation, 

1164 education=db_user.education, 

1165 about_me=db_user.about_me, 

1166 things_i_like=db_user.things_i_like, 

1167 about_place=db_user.about_place, 

1168 language_abilities=[ 

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

1170 for ability in db_user.language_abilities 

1171 ], 

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

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

1174 additional_information=db_user.additional_information, 

1175 friends=friends_status, 

1176 pending_friend_request=pending_friend_request, 

1177 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1178 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1179 parking_details=parkingdetails2api[db_user.parking_details], 

1180 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1181 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1182 profile_gallery_id=db_user.profile_gallery_id, 

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

1184 .scalars() 

1185 .all(), 

1186 **get_strong_verification_fields(session, db_user), 

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

1188 ) 

1189 

1190 if db_user.max_guests is not None: 

1191 user.max_guests.value = db_user.max_guests 

1192 

1193 if db_user.last_minute is not None: 

1194 user.last_minute.value = db_user.last_minute 

1195 

1196 if db_user.has_pets is not None: 

1197 user.has_pets.value = db_user.has_pets 

1198 

1199 if db_user.accepts_pets is not None: 

1200 user.accepts_pets.value = db_user.accepts_pets 

1201 

1202 if db_user.pet_details is not None: 

1203 user.pet_details.value = db_user.pet_details 

1204 

1205 if db_user.has_kids is not None: 

1206 user.has_kids.value = db_user.has_kids 

1207 

1208 if db_user.accepts_kids is not None: 

1209 user.accepts_kids.value = db_user.accepts_kids 

1210 

1211 if db_user.kid_details is not None: 

1212 user.kid_details.value = db_user.kid_details 

1213 

1214 if db_user.has_housemates is not None: 

1215 user.has_housemates.value = db_user.has_housemates 

1216 

1217 if db_user.housemate_details is not None: 

1218 user.housemate_details.value = db_user.housemate_details 

1219 

1220 if db_user.wheelchair_accessible is not None: 

1221 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1222 

1223 if db_user.smokes_at_home is not None: 

1224 user.smokes_at_home.value = db_user.smokes_at_home 

1225 

1226 if db_user.drinking_allowed is not None: 

1227 user.drinking_allowed.value = db_user.drinking_allowed 

1228 

1229 if db_user.drinks_at_home is not None: 

1230 user.drinks_at_home.value = db_user.drinks_at_home 

1231 

1232 if db_user.other_host_info is not None: 

1233 user.other_host_info.value = db_user.other_host_info 

1234 

1235 if db_user.sleeping_details is not None: 

1236 user.sleeping_details.value = db_user.sleeping_details 

1237 

1238 if db_user.area is not None: 

1239 user.area.value = db_user.area 

1240 

1241 if db_user.house_rules is not None: 

1242 user.house_rules.value = db_user.house_rules 

1243 

1244 if db_user.parking is not None: 

1245 user.parking.value = db_user.parking 

1246 

1247 if db_user.camping_ok is not None: 

1248 user.camping_ok.value = db_user.camping_ok 

1249 

1250 return user 

1251 

1252 

1253def lite_user_to_pb( 

1254 session: Session, 

1255 lite_user: LiteUser, 

1256 context: CouchersContext, 

1257 *, 

1258 is_admin_see_ghosts: bool = False, 

1259 is_get_user_return_ghosts: bool = False, 

1260) -> api_pb2.LiteUser: 

1261 if not is_admin_see_ghosts and is_not_visible( 

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

1263 ): 

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

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

1266 # Return an anonymized "ghost" user profile 

1267 return api_pb2.LiteUser( 

1268 user_id=lite_user.id, 

1269 is_ghost=True, 

1270 username=GHOST_USERNAME, 

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

1272 ) 

1273 raise GhostUserSerializationError( 

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

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

1276 ) 

1277 

1278 lat, lng = get_coordinates(lite_user.geom) 

1279 

1280 return api_pb2.LiteUser( 

1281 user_id=lite_user.id, 

1282 username=lite_user.username, 

1283 name=lite_user.name, 

1284 city=lite_user.city, 

1285 age=int(lite_user.age), 

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

1287 if lite_user.avatar_filename 

1288 else None, 

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

1290 if lite_user.avatar_filename 

1291 else None, 

1292 lat=lat, 

1293 lng=lng, 

1294 radius=lite_user.radius, 

1295 has_strong_verification=lite_user.has_strong_verification, 

1296 )