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

490 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-22 16:01 +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 ColumnElement, select 

10from sqlalchemy.orm import Session, selectinload 

11from sqlalchemy.sql import and_, case, 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 # Role-based unread host-request counts. Unlike sent/received above, these 

233 # classify by stay-role, so public-trip offers (role reversal) bucket 

234 # correctly: the offering host counts under hosting, the traveller under surfing. 

235 def role_based_unseen_host_request_count(party_predicate: ColumnElement[bool]) -> int: 

236 viewer_last_seen = case( 

237 (HostRequest.initiator_user_id == context.user_id, HostRequest.initiator_last_seen_message_id), 

238 else_=HostRequest.recipient_last_seen_message_id, 

239 ) 

240 reqs_query = select(HostRequest.conversation_id, viewer_last_seen.label("last_seen")).where(party_predicate) 

241 reqs_query = where_users_column_visible(reqs_query, context, HostRequest.initiator_user_id) 

242 reqs_query = where_users_column_visible(reqs_query, context, HostRequest.recipient_user_id) 

243 reqs_query = where_moderated_content_visible(reqs_query, context, HostRequest, is_list_operation=True) 

244 reqs = reqs_query.subquery() 

245 return session.execute( 

246 select(func.count()) 

247 .select_from(reqs) 

248 .where( 

249 exists( 

250 select(1) 

251 .where(Message.conversation_id == reqs.c.conversation_id) 

252 .where(Message.id > reqs.c.last_seen) 

253 ) 

254 ) 

255 ).scalar_one() 

256 

257 me = context.user_id 

258 unseen_hosting_host_request_count = role_based_unseen_host_request_count( 

259 or_( 

260 and_(HostRequest.public_trip_id.is_(None), HostRequest.recipient_user_id == me), 

261 and_(HostRequest.public_trip_id.isnot(None), HostRequest.initiator_user_id == me), 

262 ) 

263 ) 

264 unseen_surfing_host_request_count = role_based_unseen_host_request_count( 

265 or_( 

266 and_(HostRequest.public_trip_id.is_(None), HostRequest.initiator_user_id == me), 

267 and_(HostRequest.public_trip_id.isnot(None), HostRequest.recipient_user_id == me), 

268 ) 

269 ) 

270 if context.get_boolean_value("public_trips_enabled", False): 

271 unseen_public_trip_offer_count = role_based_unseen_host_request_count( 

272 and_(HostRequest.public_trip_id.isnot(None), HostRequest.recipient_user_id == me) 

273 ) 

274 else: 

275 unseen_public_trip_offer_count = 0 

276 

277 unseen_message_query = ( 

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

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

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

281 ) 

282 unseen_message_query = where_moderated_content_visible( 

283 unseen_message_query, context, GroupChat, is_list_operation=True 

284 ) 

285 unseen_message_query = ( 

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

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

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

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

290 ) 

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

292 

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

294 FriendRelationship.to_user_id == context.user_id 

295 ) 

296 pending_friend_request_query = where_users_column_visible( 

297 pending_friend_request_query, context, FriendRelationship.from_user_id 

298 ) 

299 pending_friend_request_query = pending_friend_request_query.where( 

300 FriendRelationship.status == FriendStatus.pending 

301 ) 

302 pending_friend_request_query = where_moderated_content_visible( 

303 pending_friend_request_query, context, FriendRelationship, is_list_operation=True 

304 ) 

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

306 

307 unseen_notification_count = session.execute( 

308 select(func.count()) 

309 .select_from(Notification) 

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

311 .where(Notification.is_seen == False) 

312 .where( 

313 Notification.topic_action.in_( 

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

315 ) 

316 ) 

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

318 ).scalar_one() 

319 

320 return api_pb2.PingRes( 

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

322 unseen_message_count=unseen_message_count, 

323 unseen_sent_host_request_count=unseen_sent_host_request_count, 

324 unseen_received_host_request_count=unseen_received_host_request_count, 

325 unseen_hosting_host_request_count=unseen_hosting_host_request_count, 

326 unseen_surfing_host_request_count=unseen_surfing_host_request_count, 

327 unseen_public_trip_offer_count=unseen_public_trip_offer_count, 

328 pending_friend_request_count=pending_friend_request_count, 

329 unseen_notification_count=unseen_notification_count, 

330 ) 

331 

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

333 user = session.execute( 

334 select(User) 

335 .where(username_or_id(request.user)) 

336 .options( 

337 selectinload(User.regions_visited), 

338 selectinload(User.regions_lived), 

339 selectinload(User.language_abilities), 

340 ) 

341 ).scalar_one_or_none() 

342 

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

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

345 

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

347 

348 def GetLiteUser( 

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

350 ) -> api_pb2.LiteUser: 

351 lite_user = session.execute( 

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

353 ).scalar_one_or_none() 

354 

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

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

357 

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

359 

360 def GetLiteUsers( 

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

362 ) -> api_pb2.GetLiteUsersRes: 

363 if len(request.users) > MAX_USERS_PER_QUERY: 

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

365 

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

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

368 

369 # decomposed where_username_or_id... 

370 users = ( 

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

372 .scalars() 

373 .all() 

374 ) 

375 

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

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

378 

379 res = api_pb2.GetLiteUsersRes() 

380 

381 for user in request.users: 

382 lite_user = None 

383 if user in users_by_id: 

384 lite_user = users_by_id[user] 

385 elif user in users_by_username: 

386 lite_user = users_by_username[user] 

387 

388 res.responses.append( 

389 api_pb2.LiteUserRes( 

390 query=user, 

391 not_found=lite_user is None, 

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

393 if lite_user 

394 else None, 

395 ) 

396 ) 

397 

398 return res 

399 

400 def UpdateProfile( 

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

402 ) -> empty_pb2.Empty: 

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

404 

405 if request.HasField("name"): 

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

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

408 user.name = request.name.value 

409 

410 if request.HasField("city"): 

411 user.city = request.city.value 

412 

413 if request.HasField("hometown"): 

414 if request.hometown.is_null: 

415 user.hometown = None 

416 else: 

417 user.hometown = request.hometown.value 

418 

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

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

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

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

423 user.randomized_geom = None 

424 

425 if request.HasField("radius"): 

426 user.geom_radius = request.radius.value 

427 

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

429 # user.gender = request.gender.value 

430 

431 if request.HasField("pronouns"): 

432 if request.pronouns.is_null: 

433 user.pronouns = None 

434 else: 

435 user.pronouns = request.pronouns.value 

436 

437 if request.HasField("occupation"): 

438 if request.occupation.is_null: 

439 user.occupation = None 

440 else: 

441 user.occupation = request.occupation.value 

442 

443 if request.HasField("education"): 

444 if request.education.is_null: 

445 user.education = None 

446 else: 

447 user.education = request.education.value 

448 

449 if request.HasField("about_me"): 

450 if request.about_me.is_null: 

451 user.about_me = None 

452 else: 

453 user.about_me = request.about_me.value 

454 

455 if request.HasField("things_i_like"): 

456 if request.things_i_like.is_null: 

457 user.things_i_like = None 

458 else: 

459 user.things_i_like = request.things_i_like.value 

460 

461 if request.HasField("about_place"): 

462 if request.about_place.is_null: 

463 user.about_place = None 

464 else: 

465 user.about_place = request.about_place.value 

466 

467 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

471 

472 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

476 

477 if request.HasField("language_abilities"): 

478 # delete all existing abilities 

479 for ability in user.language_abilities: 

480 session.delete(ability) 

481 session.flush() 

482 

483 # add the new ones 

484 for language_ability in request.language_abilities.value: 

485 if not language_is_allowed(language_ability.code): 

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

487 session.add( 

488 LanguageAbility( 

489 user_id=user.id, 

490 language_code=language_ability.code, 

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

492 ) 

493 ) 

494 

495 if request.HasField("regions_visited"): 

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

497 

498 for region in request.regions_visited.value: 

499 if not region_is_allowed(region): 

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

501 session.add( 

502 RegionVisited( 

503 user_id=user.id, 

504 region_code=region, 

505 ) 

506 ) 

507 

508 if request.HasField("regions_lived"): 

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

510 

511 for region in request.regions_lived.value: 

512 if not region_is_allowed(region): 

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

514 session.add( 

515 RegionLived( 

516 user_id=user.id, 

517 region_code=region, 

518 ) 

519 ) 

520 

521 if request.HasField("additional_information"): 

522 if request.additional_information.is_null: 

523 user.additional_information = None 

524 else: 

525 user.additional_information = request.additional_information.value 

526 

527 if request.HasField("max_guests"): 

528 if request.max_guests.is_null: 

529 user.max_guests = None 

530 else: 

531 user.max_guests = request.max_guests.value 

532 

533 if request.HasField("last_minute"): 

534 if request.last_minute.is_null: 

535 user.last_minute = None 

536 else: 

537 user.last_minute = request.last_minute.value 

538 

539 if request.HasField("has_pets"): 

540 if request.has_pets.is_null: 

541 user.has_pets = None 

542 else: 

543 user.has_pets = request.has_pets.value 

544 

545 if request.HasField("accepts_pets"): 

546 if request.accepts_pets.is_null: 

547 user.accepts_pets = None 

548 else: 

549 user.accepts_pets = request.accepts_pets.value 

550 

551 if request.HasField("pet_details"): 

552 if request.pet_details.is_null: 

553 user.pet_details = None 

554 else: 

555 user.pet_details = request.pet_details.value 

556 

557 if request.HasField("has_kids"): 

558 if request.has_kids.is_null: 

559 user.has_kids = None 

560 else: 

561 user.has_kids = request.has_kids.value 

562 

563 if request.HasField("accepts_kids"): 

564 if request.accepts_kids.is_null: 

565 user.accepts_kids = None 

566 else: 

567 user.accepts_kids = request.accepts_kids.value 

568 

569 if request.HasField("kid_details"): 

570 if request.kid_details.is_null: 

571 user.kid_details = None 

572 else: 

573 user.kid_details = request.kid_details.value 

574 

575 if request.HasField("has_housemates"): 

576 if request.has_housemates.is_null: 

577 user.has_housemates = None 

578 else: 

579 user.has_housemates = request.has_housemates.value 

580 

581 if request.HasField("housemate_details"): 

582 if request.housemate_details.is_null: 

583 user.housemate_details = None 

584 else: 

585 user.housemate_details = request.housemate_details.value 

586 

587 if request.HasField("wheelchair_accessible"): 

588 if request.wheelchair_accessible.is_null: 

589 user.wheelchair_accessible = None 

590 else: 

591 user.wheelchair_accessible = request.wheelchair_accessible.value 

592 

593 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

594 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

595 

596 if request.HasField("smokes_at_home"): 

597 if request.smokes_at_home.is_null: 

598 user.smokes_at_home = None 

599 else: 

600 user.smokes_at_home = request.smokes_at_home.value 

601 

602 if request.HasField("drinking_allowed"): 

603 if request.drinking_allowed.is_null: 

604 user.drinking_allowed = None 

605 else: 

606 user.drinking_allowed = request.drinking_allowed.value 

607 

608 if request.HasField("drinks_at_home"): 

609 if request.drinks_at_home.is_null: 

610 user.drinks_at_home = None 

611 else: 

612 user.drinks_at_home = request.drinks_at_home.value 

613 

614 if request.HasField("other_host_info"): 

615 if request.other_host_info.is_null: 

616 user.other_host_info = None 

617 else: 

618 user.other_host_info = request.other_host_info.value 

619 

620 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

621 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

622 

623 if request.HasField("sleeping_details"): 

624 if request.sleeping_details.is_null: 

625 user.sleeping_details = None 

626 else: 

627 user.sleeping_details = request.sleeping_details.value 

628 

629 if request.HasField("area"): 

630 if request.area.is_null: 

631 user.area = None 

632 else: 

633 user.area = request.area.value 

634 

635 if request.HasField("house_rules"): 

636 if request.house_rules.is_null: 

637 user.house_rules = None 

638 else: 

639 user.house_rules = request.house_rules.value 

640 

641 if request.HasField("parking"): 

642 if request.parking.is_null: 

643 user.parking = None 

644 else: 

645 user.parking = request.parking.value 

646 

647 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

648 user.parking_details = parkingdetails2sql[request.parking_details] 

649 

650 if request.HasField("camping_ok"): 

651 if request.camping_ok.is_null: 

652 user.camping_ok = None 

653 else: 

654 user.camping_ok = request.camping_ok.value 

655 

656 user.profile_last_updated = now() 

657 

658 return empty_pb2.Empty() 

659 

660 def ListFriends( 

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

662 ) -> api_pb2.ListFriendsRes: 

663 rels_query = select(FriendRelationship) 

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

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

666 rels_query = rels_query.where( 

667 or_( 

668 FriendRelationship.from_user_id == context.user_id, 

669 FriendRelationship.to_user_id == context.user_id, 

670 ) 

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

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

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

674 return api_pb2.ListFriendsRes( 

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

676 ) 

677 

678 def RemoveFriend( 

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

680 ) -> empty_pb2.Empty: 

681 rel_query = select(FriendRelationship) 

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

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

684 rel_query = rel_query.where( 

685 or_( 

686 and_( 

687 FriendRelationship.from_user_id == request.user_id, 

688 FriendRelationship.to_user_id == context.user_id, 

689 ), 

690 and_( 

691 FriendRelationship.from_user_id == context.user_id, 

692 FriendRelationship.to_user_id == request.user_id, 

693 ), 

694 ) 

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

696 rel_query = where_moderated_content_visible(rel_query, context, FriendRelationship) 

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

698 

699 if not rel: 

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

701 

702 session.delete(rel) 

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

704 

705 return empty_pb2.Empty() 

706 

707 def ListMutualFriends( 

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

709 ) -> api_pb2.ListMutualFriendsRes: 

710 if context.user_id == request.user_id: 

711 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

712 

713 user = session.execute( 

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

715 ).scalar_one_or_none() 

716 

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

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

719 

720 q1 = where_moderated_content_visible( 

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

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

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

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

725 context, 

726 FriendRelationship, 

727 ) 

728 

729 q2 = where_moderated_content_visible( 

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

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

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

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

734 context, 

735 FriendRelationship, 

736 ) 

737 

738 q3 = where_moderated_content_visible( 

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

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

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

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

743 context, 

744 FriendRelationship, 

745 ) 

746 

747 q4 = where_moderated_content_visible( 

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

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

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

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

752 context, 

753 FriendRelationship, 

754 ) 

755 

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

757 

758 mutual_friends = ( 

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

760 ) 

761 

762 return api_pb2.ListMutualFriendsRes( 

763 mutual_friends=[ 

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

765 for mutual_friend in mutual_friends 

766 ] 

767 ) 

768 

769 def SendFriendRequest( 

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

771 ) -> empty_pb2.Empty: 

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

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

774 

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

776 if not has_completed_profile(session, user): 

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

778 

779 to_user = session.execute( 

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

781 ).scalar_one_or_none() 

782 

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

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

785 

786 if ( 

787 session.execute( 

788 select(FriendRelationship) 

789 .where( 

790 or_( 

791 and_( 

792 FriendRelationship.from_user_id == context.user_id, 

793 FriendRelationship.to_user_id == request.user_id, 

794 ), 

795 and_( 

796 FriendRelationship.from_user_id == request.user_id, 

797 FriendRelationship.to_user_id == context.user_id, 

798 ), 

799 ) 

800 ) 

801 .where( 

802 or_( 

803 FriendRelationship.status == FriendStatus.accepted, 

804 FriendRelationship.status == FriendStatus.pending, 

805 ) 

806 ) 

807 ).scalar_one_or_none() 

808 is not None 

809 ): 

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

811 

812 # Check if user has been sending friend requests excessively 

813 if process_rate_limits_and_check_abort( 

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

815 ): 

816 context.abort_with_error_code( 

817 grpc.StatusCode.RESOURCE_EXHAUSTED, 

818 "friend_request_rate_limit2", 

819 substitutions={"count": RATE_LIMIT_HOURS}, 

820 ) 

821 

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

823 

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

825 friend_relationship = None 

826 

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

828 nonlocal friend_relationship 

829 friend_relationship = FriendRelationship( 

830 from_user_id=user.id, 

831 to_user_id=to_user.id, 

832 status=FriendStatus.pending, 

833 moderation_state_id=moderation_state_id, 

834 ) 

835 session.add(friend_relationship) 

836 session.flush() 

837 return friend_relationship.id 

838 

839 moderation_state = create_moderation( 

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

841 ) 

842 

843 assert friend_relationship is not None # set by create_friend_relationship callback 

844 

845 notify( 

846 session, 

847 user_id=friend_relationship.to_user_id, 

848 topic_action=NotificationTopicAction.friend_request__create, 

849 key=str(friend_relationship.from_user_id), 

850 data=notification_data_pb2.FriendRequestCreate( 

851 other_user=user_model_to_pb( 

852 friend_relationship.from_user, 

853 session, 

854 make_notification_user_context(user_id=friend_relationship.to_user_id), 

855 ), 

856 ), 

857 moderation_state_id=moderation_state.id, 

858 ) 

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

860 

861 return empty_pb2.Empty() 

862 

863 def ListFriendRequests( 

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

865 ) -> api_pb2.ListFriendRequestsRes: 

866 # both sent and received 

867 sent_requests_query = select(FriendRelationship) 

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

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

870 FriendRelationship.status == FriendStatus.pending 

871 ) 

872 sent_requests_query = where_moderated_content_visible( 

873 sent_requests_query, context, FriendRelationship, is_list_operation=True 

874 ) 

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

876 

877 received_requests_query = select(FriendRelationship) 

878 received_requests_query = where_users_column_visible( 

879 received_requests_query, context, FriendRelationship.from_user_id 

880 ) 

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

882 FriendRelationship.status == FriendStatus.pending 

883 ) 

884 received_requests_query = where_moderated_content_visible( 

885 received_requests_query, context, FriendRelationship, is_list_operation=True 

886 ) 

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

888 

889 return api_pb2.ListFriendRequestsRes( 

890 sent=[ 

891 api_pb2.FriendRequest( 

892 friend_request_id=friend_request.id, 

893 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

894 user_id=friend_request.to_user.id, 

895 sent=True, 

896 ) 

897 for friend_request in sent_requests 

898 ], 

899 received=[ 

900 api_pb2.FriendRequest( 

901 friend_request_id=friend_request.id, 

902 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

903 user_id=friend_request.from_user.id, 

904 sent=False, 

905 ) 

906 for friend_request in received_requests 

907 ], 

908 ) 

909 

910 def RespondFriendRequest( 

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

912 ) -> empty_pb2.Empty: 

913 friend_request_query = select(FriendRelationship) 

914 friend_request_query = where_users_column_visible( 

915 friend_request_query, context, FriendRelationship.from_user_id 

916 ) 

917 friend_request_query = ( 

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

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

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

921 ) 

922 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

924 

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

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

927 

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

929 friend_request.time_responded = func.now() 

930 

931 session.flush() 

932 

933 if friend_request.status == FriendStatus.accepted: 

934 notify( 

935 session, 

936 user_id=friend_request.from_user_id, 

937 topic_action=NotificationTopicAction.friend_request__accept, 

938 key=str(friend_request.to_user_id), 

939 data=notification_data_pb2.FriendRequestAccept( 

940 other_user=user_model_to_pb( 

941 friend_request.to_user, 

942 session, 

943 make_notification_user_context(user_id=friend_request.from_user_id), 

944 ), 

945 ), 

946 ) 

947 

948 log_event( 

949 context, 

950 session, 

951 "friendship.request_responded", 

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

953 ) 

954 

955 return empty_pb2.Empty() 

956 

957 def CancelFriendRequest( 

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

959 ) -> empty_pb2.Empty: 

960 friend_request_query = select(FriendRelationship) 

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

962 friend_request_query = ( 

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

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

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

966 ) 

967 friend_request_query = where_moderated_content_visible(friend_request_query, context, FriendRelationship) 

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

969 

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

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

972 

973 friend_request.status = FriendStatus.cancelled 

974 friend_request.time_responded = func.now() 

975 

976 # note no notifications 

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

978 

979 session.commit() 

980 

981 return empty_pb2.Empty() 

982 

983 def InitiateMediaUpload( 

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

985 ) -> api_pb2.InitiateMediaUploadRes: 

986 key = random_hex() 

987 

988 created = now() 

989 expiry = created + timedelta(minutes=20) 

990 

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

992 session.add(upload) 

993 session.commit() 

994 

995 req = media_pb2.UploadRequest( 

996 key=upload.key, 

997 type=media_pb2.UploadRequest.UploadType.IMAGE, 

998 created=Timestamp_from_datetime(upload.created), 

999 expiry=Timestamp_from_datetime(upload.expiry), 

1000 max_width=2000, 

1001 max_height=1600, 

1002 ).SerializeToString() 

1003 

1004 data = b64encode(req) 

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

1006 

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

1008 

1009 return api_pb2.InitiateMediaUploadRes( 

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

1011 expiry=Timestamp_from_datetime(expiry), 

1012 ) 

1013 

1014 def ListBadgeUsers( 

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

1016 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

1022 

1023 badge_user_ids_query = ( 

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

1025 ) 

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

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

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

1029 

1030 return api_pb2.ListBadgeUsersRes( 

1031 user_ids=badge_user_ids[:page_size], 

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

1033 ) 

1034 

1035 

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

1037 if not response_rate: 

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

1039 

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

1041 if response_rate.requests < 3: 

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

1043 

1044 if response_rate.response_rate <= 0.33: 

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

1046 

1047 response_time_p33_coarsened = Duration_from_timedelta( 

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

1049 ) 

1050 

1051 if response_rate.response_rate <= 0.66: 

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

1053 

1054 response_time_p66_coarsened = Duration_from_timedelta( 

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

1056 ) 

1057 

1058 if response_rate.response_rate <= 0.90: 

1059 return { 

1060 "most": requests_pb2.ResponseRateMost( 

1061 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1062 ) 

1063 } 

1064 else: 

1065 return { 

1066 "almost_all": requests_pb2.ResponseRateAlmostAll( 

1067 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

1068 ) 

1069 } 

1070 

1071 

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

1073 query = where_moderated_content_visible( 

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

1075 ) 

1076 query = ( 

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

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

1079 .where(User.is_visible) 

1080 .group_by(Reference.to_user_id) 

1081 ) 

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

1083 

1084 

1085def user_model_to_pb( 

1086 db_user: User, 

1087 session: Session, 

1088 context: CouchersContext, 

1089 *, 

1090 is_admin_see_ghosts: bool = False, 

1091 is_get_user_return_ghosts: bool = False, 

1092) -> api_pb2.User: 

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

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

1095 

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

1097 if not is_admin_see_ghosts and is_not_visible( 

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

1099 ): 

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

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

1102 maybe_log_nonvisible_user_access( 

1103 context, 

1104 db_user, 

1105 access_type=NonvisibleUserAccessType.ghost_served, 

1106 actor_user_id=viewer_user_id, 

1107 ) 

1108 # Return an anonymized "ghost" user profile 

1109 return api_pb2.User( 

1110 user_id=db_user.id, 

1111 is_ghost=True, 

1112 username=GHOST_USERNAME, 

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

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

1115 ) 

1116 raise GhostUserSerializationError( 

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

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

1119 ) 

1120 

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

1122 lat, lng = db_user.coordinates 

1123 

1124 pending_friend_request = None 

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

1126 friends_status = api_pb2.User.FriendshipStatus.NA 

1127 else: 

1128 friend_relationship = session.execute( 

1129 where_moderated_content_visible( 

1130 select(FriendRelationship) 

1131 .where( 

1132 or_( 

1133 and_( 

1134 FriendRelationship.from_user_id == context.user_id, 

1135 FriendRelationship.to_user_id == db_user.id, 

1136 ), 

1137 and_( 

1138 FriendRelationship.from_user_id == db_user.id, 

1139 FriendRelationship.to_user_id == context.user_id, 

1140 ), 

1141 ) 

1142 ) 

1143 .where( 

1144 or_( 

1145 FriendRelationship.status == FriendStatus.accepted, 

1146 FriendRelationship.status == FriendStatus.pending, 

1147 ) 

1148 ), 

1149 context, 

1150 FriendRelationship, 

1151 ) 

1152 ).scalar_one_or_none() 

1153 

1154 if friend_relationship: 

1155 if friend_relationship.status == FriendStatus.accepted: 

1156 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1157 else: 

1158 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1160 # we sent it 

1161 pending_friend_request = api_pb2.FriendRequest( 

1162 friend_request_id=friend_relationship.id, 

1163 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1164 user_id=friend_relationship.to_user.id, 

1165 sent=True, 

1166 ) 

1167 else: 

1168 # we received it 

1169 pending_friend_request = api_pb2.FriendRequest( 

1170 friend_request_id=friend_relationship.id, 

1171 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1172 user_id=friend_relationship.from_user.id, 

1173 sent=False, 

1174 ) 

1175 else: 

1176 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1177 

1178 response_rate = session.execute( 

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

1180 ).scalar_one_or_none() 

1181 

1182 avatar_upload = get_avatar_upload(session, db_user) 

1183 

1184 verification_score = 0.0 

1185 if db_user.phone_verification_verified: 

1186 verification_score += 1.0 * db_user.phone_is_verified 

1187 

1188 user = api_pb2.User( 

1189 user_id=db_user.id, 

1190 username=db_user.username, 

1191 name=db_user.name, 

1192 city=db_user.city, 

1193 hometown=db_user.hometown, 

1194 timezone=db_user.timezone, 

1195 lat=lat, 

1196 lng=lng, 

1197 radius=db_user.geom_radius, 

1198 verification=verification_score, 

1199 community_standing=db_user.community_standing, 

1200 num_references=num_references, 

1201 gender=db_user.gender, 

1202 pronouns=db_user.pronouns, 

1203 age=int(db_user.age), 

1204 joined=Timestamp_from_datetime(db_user.display_joined), 

1205 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1206 hosting_status=hostingstatus2api[db_user.hosting_status], 

1207 meetup_status=meetupstatus2api[db_user.meetup_status], 

1208 occupation=db_user.occupation, 

1209 education=db_user.education, 

1210 about_me=db_user.about_me, 

1211 things_i_like=db_user.things_i_like, 

1212 about_place=db_user.about_place, 

1213 language_abilities=[ 

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

1215 for ability in db_user.language_abilities 

1216 ], 

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

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

1219 additional_information=db_user.additional_information, 

1220 friends=friends_status, 

1221 pending_friend_request=pending_friend_request, 

1222 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1223 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1224 parking_details=parkingdetails2api[db_user.parking_details], 

1225 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1226 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1227 profile_gallery_id=db_user.profile_gallery_id, 

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

1229 .scalars() 

1230 .all(), 

1231 **get_strong_verification_fields(session, db_user), 

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

1233 ) 

1234 

1235 if db_user.max_guests is not None: 

1236 user.max_guests.value = db_user.max_guests 

1237 

1238 if db_user.last_minute is not None: 

1239 user.last_minute.value = db_user.last_minute 

1240 

1241 if db_user.has_pets is not None: 

1242 user.has_pets.value = db_user.has_pets 

1243 

1244 if db_user.accepts_pets is not None: 

1245 user.accepts_pets.value = db_user.accepts_pets 

1246 

1247 if db_user.pet_details is not None: 

1248 user.pet_details.value = db_user.pet_details 

1249 

1250 if db_user.has_kids is not None: 

1251 user.has_kids.value = db_user.has_kids 

1252 

1253 if db_user.accepts_kids is not None: 

1254 user.accepts_kids.value = db_user.accepts_kids 

1255 

1256 if db_user.kid_details is not None: 

1257 user.kid_details.value = db_user.kid_details 

1258 

1259 if db_user.has_housemates is not None: 

1260 user.has_housemates.value = db_user.has_housemates 

1261 

1262 if db_user.housemate_details is not None: 

1263 user.housemate_details.value = db_user.housemate_details 

1264 

1265 if db_user.wheelchair_accessible is not None: 

1266 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1267 

1268 if db_user.smokes_at_home is not None: 

1269 user.smokes_at_home.value = db_user.smokes_at_home 

1270 

1271 if db_user.drinking_allowed is not None: 

1272 user.drinking_allowed.value = db_user.drinking_allowed 

1273 

1274 if db_user.drinks_at_home is not None: 

1275 user.drinks_at_home.value = db_user.drinks_at_home 

1276 

1277 if db_user.other_host_info is not None: 

1278 user.other_host_info.value = db_user.other_host_info 

1279 

1280 if db_user.sleeping_details is not None: 

1281 user.sleeping_details.value = db_user.sleeping_details 

1282 

1283 if db_user.area is not None: 

1284 user.area.value = db_user.area 

1285 

1286 if db_user.house_rules is not None: 

1287 user.house_rules.value = db_user.house_rules 

1288 

1289 if db_user.parking is not None: 

1290 user.parking.value = db_user.parking 

1291 

1292 if db_user.camping_ok is not None: 

1293 user.camping_ok.value = db_user.camping_ok 

1294 

1295 return user 

1296 

1297 

1298def lite_user_to_pb( 

1299 session: Session, 

1300 lite_user: LiteUser, 

1301 context: CouchersContext, 

1302 *, 

1303 is_admin_see_ghosts: bool = False, 

1304 is_get_user_return_ghosts: bool = False, 

1305) -> api_pb2.LiteUser: 

1306 if not is_admin_see_ghosts and is_not_visible( 

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

1308 ): 

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

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

1311 # Return an anonymized "ghost" user profile 

1312 return api_pb2.LiteUser( 

1313 user_id=lite_user.id, 

1314 is_ghost=True, 

1315 username=GHOST_USERNAME, 

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

1317 ) 

1318 raise GhostUserSerializationError( 

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

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

1321 ) 

1322 

1323 lat, lng = get_coordinates(lite_user.geom) 

1324 

1325 return api_pb2.LiteUser( 

1326 user_id=lite_user.id, 

1327 username=lite_user.username, 

1328 name=lite_user.name, 

1329 city=lite_user.city, 

1330 age=int(lite_user.age), 

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

1332 if lite_user.avatar_filename 

1333 else None, 

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

1335 if lite_user.avatar_filename 

1336 else None, 

1337 lat=lat, 

1338 lng=lng, 

1339 radius=lite_user.radius, 

1340 has_strong_verification=lite_user.has_strong_verification, 

1341 )