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

476 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +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.strong_verification import get_strong_verification_fields 

22from couchers.materialized_views import LiteUser, UserResponseRate 

23from couchers.models import ( 

24 FriendRelationship, 

25 FriendStatus, 

26 GroupChat, 

27 GroupChatSubscription, 

28 HostingStatus, 

29 HostRequest, 

30 InitiatedUpload, 

31 LanguageAbility, 

32 LanguageFluency, 

33 MeetupStatus, 

34 Message, 

35 ModerationObjectType, 

36 NonvisibleUserAccessType, 

37 Notification, 

38 NotificationDeliveryType, 

39 ParkingDetails, 

40 RateLimitAction, 

41 Reference, 

42 RegionLived, 

43 RegionVisited, 

44 SleepingArrangement, 

45 SmokingLocation, 

46 User, 

47 UserBadge, 

48) 

49from couchers.models.notifications import NotificationTopicAction 

50from couchers.models.uploads import get_avatar_upload 

51from couchers.moderation.utils import create_moderation 

52from couchers.notifications.notify import notify 

53from couchers.notifications.settings import get_topic_actions_by_delivery_type 

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

55from couchers.rate_limits.check import process_rate_limits_and_check_abort 

56from couchers.rate_limits.definitions import RATE_LIMIT_HOURS 

57from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

58from couchers.servicers.blocking import is_not_visible 

59from couchers.sql import ( 

60 moderation_state_column_visible, 

61 username_or_id, 

62 users_visible, 

63 where_moderated_content_visible, 

64 where_users_column_visible, 

65) 

66from couchers.utils import ( 

67 Duration_from_timedelta, 

68 Timestamp_from_datetime, 

69 create_coordinate, 

70 get_coordinates, 

71 is_valid_name, 

72 is_valid_user_id, 

73 is_valid_username, 

74 not_none, 

75 now, 

76) 

77 

78 

79class GhostUserSerializationError(Exception): 

80 """ 

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

82 """ 

83 

84 pass 

85 

86 

87MAX_USERS_PER_QUERY = 200 

88MAX_PAGINATION_LENGTH = 50 

89 

90hostingstatus2sql = { 

91 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

92 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

93 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

94 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

95} 

96 

97hostingstatus2api = { 

98 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

99 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

100 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

101 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

102} 

103 

104meetupstatus2sql = { 

105 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

106 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

107 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

108 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

109} 

110 

111meetupstatus2api = { 

112 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

113 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

114 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

115 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

116} 

117 

118smokinglocation2sql = { 

119 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

120 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

121 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

122 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

123 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

124} 

125 

126smokinglocation2api = { 

127 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

128 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

129 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

130 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

131 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

132} 

133 

134sleepingarrangement2sql = { 

135 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

136 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

137 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

138 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

139} 

140 

141sleepingarrangement2api = { 

142 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

143 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

144 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

145 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

146} 

147 

148parkingdetails2sql = { 

149 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

150 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

151 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

152 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

153 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

154} 

155 

156parkingdetails2api = { 

157 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

158 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

159 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

160 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

161 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

162} 

163 

164fluency2sql = { 

165 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

169} 

170 

171fluency2api = { 

172 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

176} 

177 

178 

179class API(api_pb2_grpc.APIServicer): 

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

181 # auth ought to make sure the user exists 

182 user = session.execute( 

183 select(User) 

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

185 .options( 

186 selectinload(User.regions_visited), 

187 selectinload(User.regions_lived), 

188 selectinload(User.language_abilities), 

189 ) 

190 ).scalar_one() 

191 

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

193 HostRequest.initiator_user_id == context.user_id 

194 ) 

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

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

197 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery() 

198 

199 unseen_sent_host_request_count = session.execute( 

200 select(func.count()) 

201 .select_from(sent_reqs_last_seen_message_ids) 

202 .where( 

203 exists( 

204 select(1) 

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

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

207 ) 

208 ) 

209 ).scalar_one() 

210 

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

212 HostRequest.recipient_user_id == context.user_id 

213 ) 

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

215 received_reqs_query = where_moderated_content_visible( 

216 received_reqs_query, context, HostRequest, is_list_operation=True 

217 ) 

218 received_reqs_last_seen_message_ids = received_reqs_query.subquery() 

219 

220 unseen_received_host_request_count = session.execute( 

221 select(func.count()) 

222 .select_from(received_reqs_last_seen_message_ids) 

223 .where( 

224 exists( 

225 select(1) 

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

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

228 ) 

229 ) 

230 ).scalar_one() 

231 

232 unseen_message_query = ( 

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

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

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

236 ) 

237 unseen_message_query = where_moderated_content_visible( 

238 unseen_message_query, context, GroupChat, is_list_operation=True 

239 ) 

240 unseen_message_query = ( 

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

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

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

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

245 ) 

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

247 

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

249 FriendRelationship.to_user_id == context.user_id 

250 ) 

251 pending_friend_request_query = where_users_column_visible( 

252 pending_friend_request_query, context, FriendRelationship.from_user_id 

253 ) 

254 pending_friend_request_query = pending_friend_request_query.where( 

255 FriendRelationship.status == FriendStatus.pending 

256 ) 

257 pending_friend_request_query = where_moderated_content_visible( 

258 pending_friend_request_query, context, FriendRelationship, is_list_operation=True 

259 ) 

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

261 

262 unseen_notification_count = session.execute( 

263 select(func.count()) 

264 .select_from(Notification) 

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

266 .where(Notification.is_seen == False) 

267 .where( 

268 Notification.topic_action.in_( 

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

270 ) 

271 ) 

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

273 ).scalar_one() 

274 

275 return api_pb2.PingRes( 

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

277 unseen_message_count=unseen_message_count, 

278 unseen_sent_host_request_count=unseen_sent_host_request_count, 

279 unseen_received_host_request_count=unseen_received_host_request_count, 

280 pending_friend_request_count=pending_friend_request_count, 

281 unseen_notification_count=unseen_notification_count, 

282 ) 

283 

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

285 user = session.execute( 

286 select(User) 

287 .where(username_or_id(request.user)) 

288 .options( 

289 selectinload(User.regions_visited), 

290 selectinload(User.regions_lived), 

291 selectinload(User.language_abilities), 

292 ) 

293 ).scalar_one_or_none() 

294 

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

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

297 

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

299 

300 def GetLiteUser( 

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

302 ) -> api_pb2.LiteUser: 

303 lite_user = session.execute( 

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

305 ).scalar_one_or_none() 

306 

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

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

309 

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

311 

312 def GetLiteUsers( 

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

314 ) -> api_pb2.GetLiteUsersRes: 

315 if len(request.users) > MAX_USERS_PER_QUERY: 

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

317 

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

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

320 

321 # decomposed where_username_or_id... 

322 users = ( 

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

324 .scalars() 

325 .all() 

326 ) 

327 

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

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

330 

331 res = api_pb2.GetLiteUsersRes() 

332 

333 for user in request.users: 

334 lite_user = None 

335 if user in users_by_id: 

336 lite_user = users_by_id[user] 

337 elif user in users_by_username: 

338 lite_user = users_by_username[user] 

339 

340 res.responses.append( 

341 api_pb2.LiteUserRes( 

342 query=user, 

343 not_found=lite_user is None, 

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

345 if lite_user 

346 else None, 

347 ) 

348 ) 

349 

350 return res 

351 

352 def UpdateProfile( 

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

354 ) -> empty_pb2.Empty: 

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

356 

357 if request.HasField("name"): 

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

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

360 user.name = request.name.value 

361 

362 if request.HasField("city"): 

363 user.city = request.city.value 

364 

365 if request.HasField("hometown"): 

366 if request.hometown.is_null: 

367 user.hometown = None 

368 else: 

369 user.hometown = request.hometown.value 

370 

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

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

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

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

375 user.randomized_geom = None 

376 

377 if request.HasField("radius"): 

378 user.geom_radius = request.radius.value 

379 

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

381 # user.gender = request.gender.value 

382 

383 if request.HasField("pronouns"): 

384 if request.pronouns.is_null: 

385 user.pronouns = None 

386 else: 

387 user.pronouns = request.pronouns.value 

388 

389 if request.HasField("occupation"): 

390 if request.occupation.is_null: 

391 user.occupation = None 

392 else: 

393 user.occupation = request.occupation.value 

394 

395 if request.HasField("education"): 

396 if request.education.is_null: 

397 user.education = None 

398 else: 

399 user.education = request.education.value 

400 

401 if request.HasField("about_me"): 

402 if request.about_me.is_null: 

403 user.about_me = None 

404 else: 

405 user.about_me = request.about_me.value 

406 

407 if request.HasField("things_i_like"): 

408 if request.things_i_like.is_null: 

409 user.things_i_like = None 

410 else: 

411 user.things_i_like = request.things_i_like.value 

412 

413 if request.HasField("about_place"): 

414 if request.about_place.is_null: 

415 user.about_place = None 

416 else: 

417 user.about_place = request.about_place.value 

418 

419 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

423 

424 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

428 

429 if request.HasField("language_abilities"): 

430 # delete all existing abilities 

431 for ability in user.language_abilities: 

432 session.delete(ability) 

433 session.flush() 

434 

435 # add the new ones 

436 for language_ability in request.language_abilities.value: 

437 if not language_is_allowed(language_ability.code): 

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

439 session.add( 

440 LanguageAbility( 

441 user_id=user.id, 

442 language_code=language_ability.code, 

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

444 ) 

445 ) 

446 

447 if request.HasField("regions_visited"): 

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

449 

450 for region in request.regions_visited.value: 

451 if not region_is_allowed(region): 

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

453 session.add( 

454 RegionVisited( 

455 user_id=user.id, 

456 region_code=region, 

457 ) 

458 ) 

459 

460 if request.HasField("regions_lived"): 

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

462 

463 for region in request.regions_lived.value: 

464 if not region_is_allowed(region): 

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

466 session.add( 

467 RegionLived( 

468 user_id=user.id, 

469 region_code=region, 

470 ) 

471 ) 

472 

473 if request.HasField("additional_information"): 

474 if request.additional_information.is_null: 

475 user.additional_information = None 

476 else: 

477 user.additional_information = request.additional_information.value 

478 

479 if request.HasField("max_guests"): 

480 if request.max_guests.is_null: 

481 user.max_guests = None 

482 else: 

483 user.max_guests = request.max_guests.value 

484 

485 if request.HasField("last_minute"): 

486 if request.last_minute.is_null: 

487 user.last_minute = None 

488 else: 

489 user.last_minute = request.last_minute.value 

490 

491 if request.HasField("has_pets"): 

492 if request.has_pets.is_null: 

493 user.has_pets = None 

494 else: 

495 user.has_pets = request.has_pets.value 

496 

497 if request.HasField("accepts_pets"): 

498 if request.accepts_pets.is_null: 

499 user.accepts_pets = None 

500 else: 

501 user.accepts_pets = request.accepts_pets.value 

502 

503 if request.HasField("pet_details"): 

504 if request.pet_details.is_null: 

505 user.pet_details = None 

506 else: 

507 user.pet_details = request.pet_details.value 

508 

509 if request.HasField("has_kids"): 

510 if request.has_kids.is_null: 

511 user.has_kids = None 

512 else: 

513 user.has_kids = request.has_kids.value 

514 

515 if request.HasField("accepts_kids"): 

516 if request.accepts_kids.is_null: 

517 user.accepts_kids = None 

518 else: 

519 user.accepts_kids = request.accepts_kids.value 

520 

521 if request.HasField("kid_details"): 

522 if request.kid_details.is_null: 

523 user.kid_details = None 

524 else: 

525 user.kid_details = request.kid_details.value 

526 

527 if request.HasField("has_housemates"): 

528 if request.has_housemates.is_null: 

529 user.has_housemates = None 

530 else: 

531 user.has_housemates = request.has_housemates.value 

532 

533 if request.HasField("housemate_details"): 

534 if request.housemate_details.is_null: 

535 user.housemate_details = None 

536 else: 

537 user.housemate_details = request.housemate_details.value 

538 

539 if request.HasField("wheelchair_accessible"): 

540 if request.wheelchair_accessible.is_null: 

541 user.wheelchair_accessible = None 

542 else: 

543 user.wheelchair_accessible = request.wheelchair_accessible.value 

544 

545 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

546 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

547 

548 if request.HasField("smokes_at_home"): 

549 if request.smokes_at_home.is_null: 

550 user.smokes_at_home = None 

551 else: 

552 user.smokes_at_home = request.smokes_at_home.value 

553 

554 if request.HasField("drinking_allowed"): 

555 if request.drinking_allowed.is_null: 

556 user.drinking_allowed = None 

557 else: 

558 user.drinking_allowed = request.drinking_allowed.value 

559 

560 if request.HasField("drinks_at_home"): 

561 if request.drinks_at_home.is_null: 

562 user.drinks_at_home = None 

563 else: 

564 user.drinks_at_home = request.drinks_at_home.value 

565 

566 if request.HasField("other_host_info"): 

567 if request.other_host_info.is_null: 

568 user.other_host_info = None 

569 else: 

570 user.other_host_info = request.other_host_info.value 

571 

572 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

573 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

574 

575 if request.HasField("sleeping_details"): 

576 if request.sleeping_details.is_null: 

577 user.sleeping_details = None 

578 else: 

579 user.sleeping_details = request.sleeping_details.value 

580 

581 if request.HasField("area"): 

582 if request.area.is_null: 

583 user.area = None 

584 else: 

585 user.area = request.area.value 

586 

587 if request.HasField("house_rules"): 

588 if request.house_rules.is_null: 

589 user.house_rules = None 

590 else: 

591 user.house_rules = request.house_rules.value 

592 

593 if request.HasField("parking"): 

594 if request.parking.is_null: 

595 user.parking = None 

596 else: 

597 user.parking = request.parking.value 

598 

599 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

600 user.parking_details = parkingdetails2sql[request.parking_details] 

601 

602 if request.HasField("camping_ok"): 

603 if request.camping_ok.is_null: 

604 user.camping_ok = None 

605 else: 

606 user.camping_ok = request.camping_ok.value 

607 

608 user.profile_last_updated = now() 

609 

610 return empty_pb2.Empty() 

611 

612 def ListFriends( 

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

614 ) -> api_pb2.ListFriendsRes: 

615 rels_query = select(FriendRelationship) 

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

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

618 rels_query = rels_query.where( 

619 or_( 

620 FriendRelationship.from_user_id == context.user_id, 

621 FriendRelationship.to_user_id == context.user_id, 

622 ) 

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

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

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

626 return api_pb2.ListFriendsRes( 

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

628 ) 

629 

630 def RemoveFriend( 

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

632 ) -> empty_pb2.Empty: 

633 rel_query = select(FriendRelationship) 

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

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

636 rel_query = rel_query.where( 

637 or_( 

638 and_( 

639 FriendRelationship.from_user_id == request.user_id, 

640 FriendRelationship.to_user_id == context.user_id, 

641 ), 

642 and_( 

643 FriendRelationship.from_user_id == context.user_id, 

644 FriendRelationship.to_user_id == request.user_id, 

645 ), 

646 ) 

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

648 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship) 

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

650 

651 if not rel: 

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

653 

654 session.delete(rel) 

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

656 

657 return empty_pb2.Empty() 

658 

659 def ListMutualFriends( 

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

661 ) -> api_pb2.ListMutualFriendsRes: 

662 if context.user_id == request.user_id: 

663 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

664 

665 user = session.execute( 

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

667 ).scalar_one_or_none() 

668 

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

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

671 

672 q1 = where_moderated_content_visible( 

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

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

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

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

677 context, 

678 FriendRelationship, 

679 ) 

680 

681 q2 = where_moderated_content_visible( 

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

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

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

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

686 context, 

687 FriendRelationship, 

688 ) 

689 

690 q3 = where_moderated_content_visible( 

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

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

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

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

695 context, 

696 FriendRelationship, 

697 ) 

698 

699 q4 = where_moderated_content_visible( 

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

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

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

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

704 context, 

705 FriendRelationship, 

706 ) 

707 

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

709 

710 mutual_friends = ( 

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

712 ) 

713 

714 return api_pb2.ListMutualFriendsRes( 

715 mutual_friends=[ 

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

717 for mutual_friend in mutual_friends 

718 ] 

719 ) 

720 

721 def SendFriendRequest( 

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

723 ) -> empty_pb2.Empty: 

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

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

726 

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

728 if not has_completed_profile(session, user): 

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

730 

731 to_user = session.execute( 

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

733 ).scalar_one_or_none() 

734 

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

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

737 

738 if ( 

739 session.execute( 

740 select(FriendRelationship) 

741 .where( 

742 or_( 

743 and_( 

744 FriendRelationship.from_user_id == context.user_id, 

745 FriendRelationship.to_user_id == request.user_id, 

746 ), 

747 and_( 

748 FriendRelationship.from_user_id == request.user_id, 

749 FriendRelationship.to_user_id == context.user_id, 

750 ), 

751 ) 

752 ) 

753 .where( 

754 or_( 

755 FriendRelationship.status == FriendStatus.accepted, 

756 FriendRelationship.status == FriendStatus.pending, 

757 ) 

758 ) 

759 ).scalar_one_or_none() 

760 is not None 

761 ): 

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

763 

764 # Check if user has been sending friend requests excessively 

765 if process_rate_limits_and_check_abort( 

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

767 ): 

768 context.abort_with_error_code( 

769 grpc.StatusCode.RESOURCE_EXHAUSTED, 

770 "friend_request_rate_limit2", 

771 substitutions={"count": RATE_LIMIT_HOURS}, 

772 ) 

773 

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

775 

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

777 friend_relationship = None 

778 

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

780 nonlocal friend_relationship 

781 friend_relationship = FriendRelationship( 

782 from_user_id=user.id, 

783 to_user_id=to_user.id, 

784 status=FriendStatus.pending, 

785 moderation_state_id=moderation_state_id, 

786 ) 

787 session.add(friend_relationship) 

788 session.flush() 

789 return friend_relationship.id 

790 

791 moderation_state = create_moderation( 

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

793 ) 

794 

795 assert friend_relationship is not None # set by create_friend_relationship callback 

796 

797 notify( 

798 session, 

799 user_id=friend_relationship.to_user_id, 

800 topic_action=NotificationTopicAction.friend_request__create, 

801 key=str(friend_relationship.from_user_id), 

802 data=notification_data_pb2.FriendRequestCreate( 

803 other_user=user_model_to_pb( 

804 friend_relationship.from_user, 

805 session, 

806 make_notification_user_context(user_id=friend_relationship.to_user_id), 

807 ), 

808 ), 

809 moderation_state_id=moderation_state.id, 

810 ) 

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

812 

813 return empty_pb2.Empty() 

814 

815 def ListFriendRequests( 

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

817 ) -> api_pb2.ListFriendRequestsRes: 

818 # both sent and received 

819 sent_requests_query = select(FriendRelationship) 

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

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

822 FriendRelationship.status == FriendStatus.pending 

823 ) 

824 sent_requests_query = where_moderated_content_visible( 

825 sent_requests_query, context, FriendRelationship, is_list_operation=True 

826 ) 

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

828 

829 received_requests_query = select(FriendRelationship) 

830 received_requests_query = where_users_column_visible( 

831 received_requests_query, context, FriendRelationship.from_user_id 

832 ) 

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

834 FriendRelationship.status == FriendStatus.pending 

835 ) 

836 received_requests_query = where_moderated_content_visible( 

837 received_requests_query, context, FriendRelationship, is_list_operation=True 

838 ) 

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

840 

841 return api_pb2.ListFriendRequestsRes( 

842 sent=[ 

843 api_pb2.FriendRequest( 

844 friend_request_id=friend_request.id, 

845 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

846 user_id=friend_request.to_user.id, 

847 sent=True, 

848 ) 

849 for friend_request in sent_requests 

850 ], 

851 received=[ 

852 api_pb2.FriendRequest( 

853 friend_request_id=friend_request.id, 

854 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

855 user_id=friend_request.from_user.id, 

856 sent=False, 

857 ) 

858 for friend_request in received_requests 

859 ], 

860 ) 

861 

862 def RespondFriendRequest( 

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

864 ) -> empty_pb2.Empty: 

865 friend_request_query = select(FriendRelationship) 

866 friend_request_query = where_users_column_visible( 

867 friend_request_query, context, FriendRelationship.from_user_id 

868 ) 

869 friend_request_query = ( 

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

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

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

873 ) 

874 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

876 

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

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

879 

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

881 friend_request.time_responded = func.now() 

882 

883 session.flush() 

884 

885 if friend_request.status == FriendStatus.accepted: 

886 notify( 

887 session, 

888 user_id=friend_request.from_user_id, 

889 topic_action=NotificationTopicAction.friend_request__accept, 

890 key=str(friend_request.to_user_id), 

891 data=notification_data_pb2.FriendRequestAccept( 

892 other_user=user_model_to_pb( 

893 friend_request.to_user, 

894 session, 

895 make_notification_user_context(user_id=friend_request.from_user_id), 

896 ), 

897 ), 

898 ) 

899 

900 log_event( 

901 context, 

902 session, 

903 "friendship.request_responded", 

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

905 ) 

906 

907 return empty_pb2.Empty() 

908 

909 def CancelFriendRequest( 

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

911 ) -> empty_pb2.Empty: 

912 friend_request_query = select(FriendRelationship) 

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

914 friend_request_query = ( 

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

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

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

918 ) 

919 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

921 

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

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

924 

925 friend_request.status = FriendStatus.cancelled 

926 friend_request.time_responded = func.now() 

927 

928 # note no notifications 

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

930 

931 session.commit() 

932 

933 return empty_pb2.Empty() 

934 

935 def InitiateMediaUpload( 

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

937 ) -> api_pb2.InitiateMediaUploadRes: 

938 key = random_hex() 

939 

940 created = now() 

941 expiry = created + timedelta(minutes=20) 

942 

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

944 session.add(upload) 

945 session.commit() 

946 

947 req = media_pb2.UploadRequest( 

948 key=upload.key, 

949 type=media_pb2.UploadRequest.UploadType.IMAGE, 

950 created=Timestamp_from_datetime(upload.created), 

951 expiry=Timestamp_from_datetime(upload.expiry), 

952 max_width=2000, 

953 max_height=1600, 

954 ).SerializeToString() 

955 

956 data = b64encode(req) 

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

958 

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

960 

961 return api_pb2.InitiateMediaUploadRes( 

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

963 expiry=Timestamp_from_datetime(expiry), 

964 ) 

965 

966 def ListBadgeUsers( 

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

968 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

974 

975 badge_user_ids_query = ( 

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

977 ) 

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

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

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

981 

982 return api_pb2.ListBadgeUsersRes( 

983 user_ids=badge_user_ids[:page_size], 

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

985 ) 

986 

987 

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

989 if not response_rate: 

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

991 

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

993 if response_rate.requests < 3: 

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

995 

996 if response_rate.response_rate <= 0.33: 

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

998 

999 response_time_p33_coarsened = Duration_from_timedelta( 

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

1001 ) 

1002 

1003 if response_rate.response_rate <= 0.66: 

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

1005 

1006 response_time_p66_coarsened = Duration_from_timedelta( 

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

1008 ) 

1009 

1010 if response_rate.response_rate <= 0.90: 

1011 return { 

1012 "most": requests_pb2.ResponseRateMost( 

1013 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1014 ) 

1015 } 

1016 else: 

1017 return { 

1018 "almost_all": requests_pb2.ResponseRateAlmostAll( 

1019 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1020 ) 

1021 } 

1022 

1023 

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

1025 query = where_moderated_content_visible( 

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

1027 ) 

1028 query = ( 

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

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

1031 .where(User.is_visible) 

1032 .group_by(Reference.to_user_id) 

1033 ) 

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

1035 

1036 

1037def user_model_to_pb( 

1038 db_user: User, 

1039 session: Session, 

1040 context: CouchersContext, 

1041 *, 

1042 is_admin_see_ghosts: bool = False, 

1043 is_get_user_return_ghosts: bool = False, 

1044) -> api_pb2.User: 

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

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

1047 

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

1049 if not is_admin_see_ghosts and is_not_visible( 

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

1051 ): 

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

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

1054 maybe_log_nonvisible_user_access( 

1055 context, 

1056 db_user, 

1057 access_type=NonvisibleUserAccessType.ghost_served, 

1058 actor_user_id=viewer_user_id, 

1059 ) 

1060 # Return an anonymized "ghost" user profile 

1061 return api_pb2.User( 

1062 user_id=db_user.id, 

1063 is_ghost=True, 

1064 username=GHOST_USERNAME, 

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

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

1067 ) 

1068 raise GhostUserSerializationError( 

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

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

1071 ) 

1072 

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

1074 lat, lng = db_user.coordinates 

1075 

1076 pending_friend_request = None 

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

1078 friends_status = api_pb2.User.FriendshipStatus.NA 

1079 else: 

1080 friend_relationship = session.execute( 

1081 where_moderated_content_visible( 

1082 select(FriendRelationship) 

1083 .where( 

1084 or_( 

1085 and_( 

1086 FriendRelationship.from_user_id == context.user_id, 

1087 FriendRelationship.to_user_id == db_user.id, 

1088 ), 

1089 and_( 

1090 FriendRelationship.from_user_id == db_user.id, 

1091 FriendRelationship.to_user_id == context.user_id, 

1092 ), 

1093 ) 

1094 ) 

1095 .where( 

1096 or_( 

1097 FriendRelationship.status == FriendStatus.accepted, 

1098 FriendRelationship.status == FriendStatus.pending, 

1099 ) 

1100 ), 

1101 context, 

1102 FriendRelationship, 

1103 ) 

1104 ).scalar_one_or_none() 

1105 

1106 if friend_relationship: 

1107 if friend_relationship.status == FriendStatus.accepted: 

1108 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1109 else: 

1110 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1112 # we sent it 

1113 pending_friend_request = api_pb2.FriendRequest( 

1114 friend_request_id=friend_relationship.id, 

1115 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1116 user_id=friend_relationship.to_user.id, 

1117 sent=True, 

1118 ) 

1119 else: 

1120 # we received it 

1121 pending_friend_request = api_pb2.FriendRequest( 

1122 friend_request_id=friend_relationship.id, 

1123 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1124 user_id=friend_relationship.from_user.id, 

1125 sent=False, 

1126 ) 

1127 else: 

1128 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1129 

1130 response_rate = session.execute( 

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

1132 ).scalar_one_or_none() 

1133 

1134 avatar_upload = get_avatar_upload(session, db_user) 

1135 

1136 verification_score = 0.0 

1137 if db_user.phone_verification_verified: 

1138 verification_score += 1.0 * db_user.phone_is_verified 

1139 

1140 user = api_pb2.User( 

1141 user_id=db_user.id, 

1142 username=db_user.username, 

1143 name=db_user.name, 

1144 city=db_user.city, 

1145 hometown=db_user.hometown, 

1146 timezone=db_user.timezone, 

1147 lat=lat, 

1148 lng=lng, 

1149 radius=db_user.geom_radius, 

1150 verification=verification_score, 

1151 community_standing=db_user.community_standing, 

1152 num_references=num_references, 

1153 gender=db_user.gender, 

1154 pronouns=db_user.pronouns, 

1155 age=int(db_user.age), 

1156 joined=Timestamp_from_datetime(db_user.display_joined), 

1157 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1158 hosting_status=hostingstatus2api[db_user.hosting_status], 

1159 meetup_status=meetupstatus2api[db_user.meetup_status], 

1160 occupation=db_user.occupation, 

1161 education=db_user.education, 

1162 about_me=db_user.about_me, 

1163 things_i_like=db_user.things_i_like, 

1164 about_place=db_user.about_place, 

1165 language_abilities=[ 

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

1167 for ability in db_user.language_abilities 

1168 ], 

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

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

1171 additional_information=db_user.additional_information, 

1172 friends=friends_status, 

1173 pending_friend_request=pending_friend_request, 

1174 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1175 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1176 parking_details=parkingdetails2api[db_user.parking_details], 

1177 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1178 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1179 profile_gallery_id=db_user.profile_gallery_id, 

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

1181 .scalars() 

1182 .all(), 

1183 **get_strong_verification_fields(session, db_user), 

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

1185 ) 

1186 

1187 if db_user.max_guests is not None: 

1188 user.max_guests.value = db_user.max_guests 

1189 

1190 if db_user.last_minute is not None: 

1191 user.last_minute.value = db_user.last_minute 

1192 

1193 if db_user.has_pets is not None: 

1194 user.has_pets.value = db_user.has_pets 

1195 

1196 if db_user.accepts_pets is not None: 

1197 user.accepts_pets.value = db_user.accepts_pets 

1198 

1199 if db_user.pet_details is not None: 

1200 user.pet_details.value = db_user.pet_details 

1201 

1202 if db_user.has_kids is not None: 

1203 user.has_kids.value = db_user.has_kids 

1204 

1205 if db_user.accepts_kids is not None: 

1206 user.accepts_kids.value = db_user.accepts_kids 

1207 

1208 if db_user.kid_details is not None: 

1209 user.kid_details.value = db_user.kid_details 

1210 

1211 if db_user.has_housemates is not None: 

1212 user.has_housemates.value = db_user.has_housemates 

1213 

1214 if db_user.housemate_details is not None: 

1215 user.housemate_details.value = db_user.housemate_details 

1216 

1217 if db_user.wheelchair_accessible is not None: 

1218 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1219 

1220 if db_user.smokes_at_home is not None: 

1221 user.smokes_at_home.value = db_user.smokes_at_home 

1222 

1223 if db_user.drinking_allowed is not None: 

1224 user.drinking_allowed.value = db_user.drinking_allowed 

1225 

1226 if db_user.drinks_at_home is not None: 

1227 user.drinks_at_home.value = db_user.drinks_at_home 

1228 

1229 if db_user.other_host_info is not None: 

1230 user.other_host_info.value = db_user.other_host_info 

1231 

1232 if db_user.sleeping_details is not None: 

1233 user.sleeping_details.value = db_user.sleeping_details 

1234 

1235 if db_user.area is not None: 

1236 user.area.value = db_user.area 

1237 

1238 if db_user.house_rules is not None: 

1239 user.house_rules.value = db_user.house_rules 

1240 

1241 if db_user.parking is not None: 

1242 user.parking.value = db_user.parking 

1243 

1244 if db_user.camping_ok is not None: 

1245 user.camping_ok.value = db_user.camping_ok 

1246 

1247 return user 

1248 

1249 

1250def lite_user_to_pb( 

1251 session: Session, 

1252 lite_user: LiteUser, 

1253 context: CouchersContext, 

1254 *, 

1255 is_admin_see_ghosts: bool = False, 

1256 is_get_user_return_ghosts: bool = False, 

1257) -> api_pb2.LiteUser: 

1258 if not is_admin_see_ghosts and is_not_visible( 

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

1260 ): 

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

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

1263 # Return an anonymized "ghost" user profile 

1264 return api_pb2.LiteUser( 

1265 user_id=lite_user.id, 

1266 is_ghost=True, 

1267 username=GHOST_USERNAME, 

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

1269 ) 

1270 raise GhostUserSerializationError( 

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

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

1273 ) 

1274 

1275 lat, lng = get_coordinates(lite_user.geom) 

1276 

1277 return api_pb2.LiteUser( 

1278 user_id=lite_user.id, 

1279 username=lite_user.username, 

1280 name=lite_user.name, 

1281 city=lite_user.city, 

1282 age=int(lite_user.age), 

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

1284 if lite_user.avatar_filename 

1285 else None, 

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

1287 if lite_user.avatar_filename 

1288 else None, 

1289 lat=lat, 

1290 lng=lng, 

1291 radius=lite_user.radius, 

1292 has_strong_verification=lite_user.has_strong_verification, 

1293 )