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

452 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-29 02:10 +0000

1from collections.abc import Iterable 

2from datetime import timedelta 

3from typing import cast 

4from urllib.parse import urlencode 

5 

6import google.protobuf.message 

7import grpc 

8from google.protobuf import empty_pb2 

9from sqlalchemy import select 

10from sqlalchemy.orm import Session 

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

12 

13from couchers import urls 

14from couchers.config import config 

15from couchers.constants import GHOST_USERNAME 

16from couchers.context import CouchersContext 

17from couchers.crypto import b64encode, generate_hash_signature, random_hex 

18from couchers.helpers.strong_verification import get_strong_verification_fields 

19from couchers.materialized_views import LiteUser, UserResponseRate 

20from couchers.models import ( 

21 FriendRelationship, 

22 FriendStatus, 

23 GroupChat, 

24 GroupChatSubscription, 

25 HostingStatus, 

26 HostRequest, 

27 InitiatedUpload, 

28 LanguageAbility, 

29 LanguageFluency, 

30 MeetupStatus, 

31 Message, 

32 Notification, 

33 NotificationDeliveryType, 

34 ParkingDetails, 

35 RateLimitAction, 

36 Reference, 

37 RegionLived, 

38 RegionVisited, 

39 SleepingArrangement, 

40 SmokingLocation, 

41 User, 

42 UserBadge, 

43) 

44from couchers.models.notifications import NotificationTopicAction 

45from couchers.models.uploads import get_avatar_upload 

46from couchers.notifications.notify import notify 

47from couchers.notifications.settings import get_topic_actions_by_delivery_type 

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

49from couchers.rate_limits.check import process_rate_limits_and_check_abort 

50from couchers.rate_limits.definitions import RATE_LIMIT_HOURS 

51from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

52from couchers.servicers.blocking import is_not_visible 

53from couchers.sql import username_or_id, users_visible, where_moderated_content_visible, where_users_column_visible 

54from couchers.utils import ( 

55 Duration_from_timedelta, 

56 Timestamp_from_datetime, 

57 create_coordinate, 

58 get_coordinates, 

59 is_valid_name, 

60 is_valid_user_id, 

61 is_valid_username, 

62 not_none, 

63 now, 

64) 

65 

66 

67class GhostUserSerializationError(Exception): 

68 """ 

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

70 """ 

71 

72 pass 

73 

74 

75MAX_USERS_PER_QUERY = 200 

76MAX_PAGINATION_LENGTH = 50 

77 

78hostingstatus2sql = { 

79 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

80 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

81 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

82 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

83} 

84 

85hostingstatus2api = { 

86 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

87 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

88 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

89 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

90} 

91 

92meetupstatus2sql = { 

93 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

94 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

95 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

96 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

97} 

98 

99meetupstatus2api = { 

100 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

101 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

102 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

103 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

104} 

105 

106smokinglocation2sql = { 

107 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

108 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

109 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

110 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

111 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

112} 

113 

114smokinglocation2api = { 

115 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

116 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

117 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

118 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

119 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

120} 

121 

122sleepingarrangement2sql = { 

123 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

124 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

125 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

126 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

127} 

128 

129sleepingarrangement2api = { 

130 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

131 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

132 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

133 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

134} 

135 

136parkingdetails2sql = { 

137 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

138 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

139 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

140 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

141 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

142} 

143 

144parkingdetails2api = { 

145 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

146 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

147 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

148 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

149 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

150} 

151 

152fluency2sql = { 

153 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

157} 

158 

159fluency2api = { 

160 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

164} 

165 

166 

167class API(api_pb2_grpc.APIServicer): 

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

169 # auth ought to make sure the user exists 

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

171 

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

173 HostRequest.surfer_user_id == context.user_id 

174 ) 

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

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

177 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery() 

178 

179 unseen_sent_host_request_count = session.execute( 

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

181 .join( 

182 Message, 

183 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id, 

184 ) 

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

186 .where(Message.id != None) 

187 ).scalar_one() 

188 

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

190 HostRequest.host_user_id == context.user_id 

191 ) 

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

193 received_reqs_query = where_moderated_content_visible( 

194 received_reqs_query, context, HostRequest, is_list_operation=True 

195 ) 

196 received_reqs_last_seen_message_ids = received_reqs_query.subquery() 

197 

198 unseen_received_host_request_count = session.execute( 

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

200 .join( 

201 Message, 

202 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id, 

203 ) 

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

205 .where(Message.id != None) 

206 ).scalar_one() 

207 

208 unseen_message_query = ( 

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

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

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

212 ) 

213 unseen_message_query = where_moderated_content_visible( 

214 unseen_message_query, context, GroupChat, is_list_operation=True 

215 ) 

216 unseen_message_query = ( 

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

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

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

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

221 ) 

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

223 

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

225 FriendRelationship.to_user_id == context.user_id 

226 ) 

227 pending_friend_request_query = where_users_column_visible( 

228 pending_friend_request_query, context, FriendRelationship.from_user_id 

229 ) 

230 pending_friend_request_query = pending_friend_request_query.where( 

231 FriendRelationship.status == FriendStatus.pending 

232 ) 

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

234 

235 unseen_notification_count = session.execute( 

236 select(func.count()) 

237 .select_from(Notification) 

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

239 .where(Notification.is_seen == False) 

240 .where( 

241 Notification.topic_action.in_( 

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

243 ) 

244 ) 

245 ).scalar_one() 

246 

247 return api_pb2.PingRes( 

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

249 unseen_message_count=unseen_message_count, 

250 unseen_sent_host_request_count=unseen_sent_host_request_count, 

251 unseen_received_host_request_count=unseen_received_host_request_count, 

252 pending_friend_request_count=pending_friend_request_count, 

253 unseen_notification_count=unseen_notification_count, 

254 ) 

255 

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

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

258 

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

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

261 

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

263 

264 def GetLiteUser( 

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

266 ) -> api_pb2.LiteUser: 

267 lite_user = session.execute( 

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

269 ).scalar_one_or_none() 

270 

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

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

273 

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

275 

276 def GetLiteUsers( 

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

278 ) -> api_pb2.GetLiteUsersRes: 

279 if len(request.users) > MAX_USERS_PER_QUERY: 

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

281 

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

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

284 

285 # decomposed where_username_or_id... 

286 users = ( 

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

288 .scalars() 

289 .all() 

290 ) 

291 

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

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

294 

295 res = api_pb2.GetLiteUsersRes() 

296 

297 for user in request.users: 

298 lite_user = None 

299 if user in users_by_id: 

300 lite_user = users_by_id[user] 

301 elif user in users_by_username: 

302 lite_user = users_by_username[user] 

303 

304 res.responses.append( 

305 api_pb2.LiteUserRes( 

306 query=user, 

307 not_found=lite_user is None, 

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

309 if lite_user 

310 else None, 

311 ) 

312 ) 

313 

314 return res 

315 

316 def UpdateProfile( 

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

318 ) -> empty_pb2.Empty: 

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

320 

321 if request.HasField("name"): 

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

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

324 user.name = request.name.value 

325 

326 if request.HasField("city"): 

327 user.city = request.city.value 

328 

329 if request.HasField("hometown"): 

330 if request.hometown.is_null: 

331 user.hometown = None 

332 else: 

333 user.hometown = request.hometown.value 

334 

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

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

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

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

339 user.randomized_geom = None 

340 

341 if request.HasField("radius"): 

342 user.geom_radius = request.radius.value 

343 

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

345 # user.gender = request.gender.value 

346 

347 if request.HasField("pronouns"): 

348 if request.pronouns.is_null: 

349 user.pronouns = None 

350 else: 

351 user.pronouns = request.pronouns.value 

352 

353 if request.HasField("occupation"): 

354 if request.occupation.is_null: 

355 user.occupation = None 

356 else: 

357 user.occupation = request.occupation.value 

358 

359 if request.HasField("education"): 

360 if request.education.is_null: 

361 user.education = None 

362 else: 

363 user.education = request.education.value 

364 

365 if request.HasField("about_me"): 

366 if request.about_me.is_null: 

367 user.about_me = None 

368 else: 

369 user.about_me = request.about_me.value 

370 

371 if request.HasField("things_i_like"): 

372 if request.things_i_like.is_null: 

373 user.things_i_like = None 

374 else: 

375 user.things_i_like = request.things_i_like.value 

376 

377 if request.HasField("about_place"): 

378 if request.about_place.is_null: 

379 user.about_place = None 

380 else: 

381 user.about_place = request.about_place.value 

382 

383 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

387 

388 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

392 

393 if request.HasField("language_abilities"): 

394 # delete all existing abilities 

395 for ability in user.language_abilities: 

396 session.delete(ability) 

397 session.flush() 

398 

399 # add the new ones 

400 for language_ability in request.language_abilities.value: 

401 if not language_is_allowed(language_ability.code): 

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

403 session.add( 

404 LanguageAbility( 

405 user_id=user.id, 

406 language_code=language_ability.code, 

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

408 ) 

409 ) 

410 

411 if request.HasField("regions_visited"): 

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

413 

414 for region in request.regions_visited.value: 

415 if not region_is_allowed(region): 

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

417 session.add( 

418 RegionVisited( 

419 user_id=user.id, 

420 region_code=region, 

421 ) 

422 ) 

423 

424 if request.HasField("regions_lived"): 

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

426 

427 for region in request.regions_lived.value: 

428 if not region_is_allowed(region): 

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

430 session.add( 

431 RegionLived( 

432 user_id=user.id, 

433 region_code=region, 

434 ) 

435 ) 

436 

437 if request.HasField("additional_information"): 

438 if request.additional_information.is_null: 

439 user.additional_information = None 

440 else: 

441 user.additional_information = request.additional_information.value 

442 

443 if request.HasField("max_guests"): 

444 if request.max_guests.is_null: 

445 user.max_guests = None 

446 else: 

447 user.max_guests = request.max_guests.value 

448 

449 if request.HasField("last_minute"): 

450 if request.last_minute.is_null: 

451 user.last_minute = None 

452 else: 

453 user.last_minute = request.last_minute.value 

454 

455 if request.HasField("has_pets"): 

456 if request.has_pets.is_null: 

457 user.has_pets = None 

458 else: 

459 user.has_pets = request.has_pets.value 

460 

461 if request.HasField("accepts_pets"): 

462 if request.accepts_pets.is_null: 

463 user.accepts_pets = None 

464 else: 

465 user.accepts_pets = request.accepts_pets.value 

466 

467 if request.HasField("pet_details"): 

468 if request.pet_details.is_null: 

469 user.pet_details = None 

470 else: 

471 user.pet_details = request.pet_details.value 

472 

473 if request.HasField("has_kids"): 

474 if request.has_kids.is_null: 

475 user.has_kids = None 

476 else: 

477 user.has_kids = request.has_kids.value 

478 

479 if request.HasField("accepts_kids"): 

480 if request.accepts_kids.is_null: 

481 user.accepts_kids = None 

482 else: 

483 user.accepts_kids = request.accepts_kids.value 

484 

485 if request.HasField("kid_details"): 

486 if request.kid_details.is_null: 

487 user.kid_details = None 

488 else: 

489 user.kid_details = request.kid_details.value 

490 

491 if request.HasField("has_housemates"): 

492 if request.has_housemates.is_null: 

493 user.has_housemates = None 

494 else: 

495 user.has_housemates = request.has_housemates.value 

496 

497 if request.HasField("housemate_details"): 

498 if request.housemate_details.is_null: 

499 user.housemate_details = None 

500 else: 

501 user.housemate_details = request.housemate_details.value 

502 

503 if request.HasField("wheelchair_accessible"): 

504 if request.wheelchair_accessible.is_null: 

505 user.wheelchair_accessible = None 

506 else: 

507 user.wheelchair_accessible = request.wheelchair_accessible.value 

508 

509 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

510 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

511 

512 if request.HasField("smokes_at_home"): 

513 if request.smokes_at_home.is_null: 

514 user.smokes_at_home = None 

515 else: 

516 user.smokes_at_home = request.smokes_at_home.value 

517 

518 if request.HasField("drinking_allowed"): 

519 if request.drinking_allowed.is_null: 

520 user.drinking_allowed = None 

521 else: 

522 user.drinking_allowed = request.drinking_allowed.value 

523 

524 if request.HasField("drinks_at_home"): 

525 if request.drinks_at_home.is_null: 

526 user.drinks_at_home = None 

527 else: 

528 user.drinks_at_home = request.drinks_at_home.value 

529 

530 if request.HasField("other_host_info"): 

531 if request.other_host_info.is_null: 

532 user.other_host_info = None 

533 else: 

534 user.other_host_info = request.other_host_info.value 

535 

536 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

537 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

538 

539 if request.HasField("sleeping_details"): 

540 if request.sleeping_details.is_null: 

541 user.sleeping_details = None 

542 else: 

543 user.sleeping_details = request.sleeping_details.value 

544 

545 if request.HasField("area"): 

546 if request.area.is_null: 

547 user.area = None 

548 else: 

549 user.area = request.area.value 

550 

551 if request.HasField("house_rules"): 

552 if request.house_rules.is_null: 

553 user.house_rules = None 

554 else: 

555 user.house_rules = request.house_rules.value 

556 

557 if request.HasField("parking"): 

558 if request.parking.is_null: 

559 user.parking = None 

560 else: 

561 user.parking = request.parking.value 

562 

563 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

564 user.parking_details = parkingdetails2sql[request.parking_details] 

565 

566 if request.HasField("camping_ok"): 

567 if request.camping_ok.is_null: 

568 user.camping_ok = None 

569 else: 

570 user.camping_ok = request.camping_ok.value 

571 

572 user.profile_last_updated = now() 

573 

574 return empty_pb2.Empty() 

575 

576 def ListFriends( 

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

578 ) -> api_pb2.ListFriendsRes: 

579 rels_query = select(FriendRelationship) 

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

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

582 rels_query = rels_query.where( 

583 or_( 

584 FriendRelationship.from_user_id == context.user_id, 

585 FriendRelationship.to_user_id == context.user_id, 

586 ) 

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

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

589 return api_pb2.ListFriendsRes( 

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

591 ) 

592 

593 def RemoveFriend( 

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

595 ) -> empty_pb2.Empty: 

596 rel_query = select(FriendRelationship) 

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

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

599 rel_query = rel_query.where( 

600 or_( 

601 and_( 

602 FriendRelationship.from_user_id == request.user_id, 

603 FriendRelationship.to_user_id == context.user_id, 

604 ), 

605 and_( 

606 FriendRelationship.from_user_id == context.user_id, 

607 FriendRelationship.to_user_id == request.user_id, 

608 ), 

609 ) 

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

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

612 

613 if not rel: 

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

615 

616 session.delete(rel) 

617 

618 return empty_pb2.Empty() 

619 

620 def ListMutualFriends( 

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

622 ) -> api_pb2.ListMutualFriendsRes: 

623 if context.user_id == request.user_id: 

624 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

625 

626 user = session.execute( 

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

628 ).scalar_one_or_none() 

629 

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

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

632 

633 q1 = ( 

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

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

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

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

638 ) 

639 

640 q2 = ( 

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

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

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

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

645 ) 

646 

647 q3 = ( 

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

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

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

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

652 ) 

653 

654 q4 = ( 

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

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

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

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

659 ) 

660 

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

662 

663 mutual_friends = ( 

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

665 ) 

666 

667 return api_pb2.ListMutualFriendsRes( 

668 mutual_friends=[ 

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

670 for mutual_friend in mutual_friends 

671 ] 

672 ) 

673 

674 def SendFriendRequest( 

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

676 ) -> empty_pb2.Empty: 

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

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

679 

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

681 to_user = session.execute( 

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

683 ).scalar_one_or_none() 

684 

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

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

687 

688 if ( 

689 session.execute( 

690 select(FriendRelationship) 

691 .where( 

692 or_( 

693 and_( 

694 FriendRelationship.from_user_id == context.user_id, 

695 FriendRelationship.to_user_id == request.user_id, 

696 ), 

697 and_( 

698 FriendRelationship.from_user_id == request.user_id, 

699 FriendRelationship.to_user_id == context.user_id, 

700 ), 

701 ) 

702 ) 

703 .where( 

704 or_( 

705 FriendRelationship.status == FriendStatus.accepted, 

706 FriendRelationship.status == FriendStatus.pending, 

707 ) 

708 ) 

709 ).scalar_one_or_none() 

710 is not None 

711 ): 

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

713 

714 # Check if user has been sending friend requests excessively 

715 if process_rate_limits_and_check_abort( 

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

717 ): 

718 context.abort_with_error_code( 

719 grpc.StatusCode.RESOURCE_EXHAUSTED, 

720 "friend_request_rate_limit", 

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

722 ) 

723 

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

725 

726 friend_relationship = FriendRelationship( 

727 from_user_id=user.id, to_user_id=to_user.id, status=FriendStatus.pending 

728 ) 

729 session.add(friend_relationship) 

730 session.flush() 

731 

732 notify( 

733 session, 

734 user_id=friend_relationship.to_user_id, 

735 topic_action=NotificationTopicAction.friend_request__create, 

736 key=str(friend_relationship.from_user_id), 

737 data=notification_data_pb2.FriendRequestCreate( 

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

739 ), 

740 ) 

741 

742 return empty_pb2.Empty() 

743 

744 def ListFriendRequests( 

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

746 ) -> api_pb2.ListFriendRequestsRes: 

747 # both sent and received 

748 sent_requests_query = select(FriendRelationship) 

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

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

751 FriendRelationship.status == FriendStatus.pending 

752 ) 

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

754 

755 received_requests_query = select(FriendRelationship) 

756 received_requests_query = where_users_column_visible( 

757 received_requests_query, context, FriendRelationship.from_user_id 

758 ) 

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

760 FriendRelationship.status == FriendStatus.pending 

761 ) 

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

763 

764 return api_pb2.ListFriendRequestsRes( 

765 sent=[ 

766 api_pb2.FriendRequest( 

767 friend_request_id=friend_request.id, 

768 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

769 user_id=friend_request.to_user.id, 

770 sent=True, 

771 ) 

772 for friend_request in sent_requests 

773 ], 

774 received=[ 

775 api_pb2.FriendRequest( 

776 friend_request_id=friend_request.id, 

777 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

778 user_id=friend_request.from_user.id, 

779 sent=False, 

780 ) 

781 for friend_request in received_requests 

782 ], 

783 ) 

784 

785 def RespondFriendRequest( 

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

787 ) -> empty_pb2.Empty: 

788 friend_request_query = select(FriendRelationship) 

789 friend_request_query = where_users_column_visible( 

790 friend_request_query, context, FriendRelationship.from_user_id 

791 ) 

792 friend_request_query = ( 

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

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

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

796 ) 

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

798 

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

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

801 

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

803 friend_request.time_responded = func.now() 

804 

805 session.flush() 

806 

807 if friend_request.status == FriendStatus.accepted: 

808 notify( 

809 session, 

810 user_id=friend_request.from_user_id, 

811 topic_action=NotificationTopicAction.friend_request__accept, 

812 key=str(friend_request.to_user_id), 

813 data=notification_data_pb2.FriendRequestAccept( 

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

815 ), 

816 ) 

817 

818 return empty_pb2.Empty() 

819 

820 def CancelFriendRequest( 

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

822 ) -> empty_pb2.Empty: 

823 friend_request_query = select(FriendRelationship) 

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

825 friend_request_query = ( 

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

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

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

829 ) 

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

831 

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

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

834 

835 friend_request.status = FriendStatus.cancelled 

836 friend_request.time_responded = func.now() 

837 

838 # note no notifications 

839 

840 session.commit() 

841 

842 return empty_pb2.Empty() 

843 

844 def InitiateMediaUpload( 

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

846 ) -> api_pb2.InitiateMediaUploadRes: 

847 key = random_hex() 

848 

849 created = now() 

850 expiry = created + timedelta(minutes=20) 

851 

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

853 session.add(upload) 

854 session.commit() 

855 

856 req = media_pb2.UploadRequest( 

857 key=upload.key, 

858 type=media_pb2.UploadRequest.UploadType.IMAGE, 

859 created=Timestamp_from_datetime(upload.created), 

860 expiry=Timestamp_from_datetime(upload.expiry), 

861 max_width=2000, 

862 max_height=1600, 

863 ).SerializeToString() 

864 

865 data = b64encode(req) 

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

867 

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

869 

870 return api_pb2.InitiateMediaUploadRes( 

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

872 expiry=Timestamp_from_datetime(expiry), 

873 ) 

874 

875 def ListBadgeUsers( 

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

877 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

883 

884 badge_user_ids_query = ( 

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

886 ) 

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

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

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

890 

891 return api_pb2.ListBadgeUsersRes( 

892 user_ids=badge_user_ids[:page_size], 

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

894 ) 

895 

896 

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

898 if not response_rate: 

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

900 

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

902 if response_rate.requests < 3: 

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

904 

905 if response_rate.response_rate <= 0.33: 

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

907 

908 response_time_p33_coarsened = Duration_from_timedelta( 

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

910 ) 

911 

912 if response_rate.response_rate <= 0.66: 

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

914 

915 response_time_p66_coarsened = Duration_from_timedelta( 

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

917 ) 

918 

919 if response_rate.response_rate <= 0.90: 

920 return { 

921 "most": requests_pb2.ResponseRateMost( 

922 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

923 ) 

924 } 

925 else: 

926 return { 

927 "almost_all": requests_pb2.ResponseRateAlmostAll( 

928 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

929 ) 

930 } 

931 

932 

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

934 query = ( 

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

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

937 .where(Reference.is_deleted == False) 

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

939 .where(User.is_visible) 

940 .group_by(Reference.to_user_id) 

941 ) 

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

943 

944 

945def user_model_to_pb( 

946 db_user: User, 

947 session: Session, 

948 context: CouchersContext, 

949 *, 

950 is_admin_see_ghosts: bool = False, 

951 is_get_user_return_ghosts: bool = False, 

952) -> api_pb2.User: 

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

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

955 

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

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

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

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

960 # Return an anonymized "ghost" user profile 

961 return api_pb2.User( 

962 user_id=db_user.id, 

963 is_ghost=True, 

964 username=GHOST_USERNAME, 

965 name=context.get_localized_string("ghost_users.display_name"), 

966 about_me=context.get_localized_string("ghost_users.about_me"), 

967 ) 

968 raise GhostUserSerializationError( 

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

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

971 ) 

972 

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

974 lat, lng = db_user.coordinates 

975 

976 pending_friend_request = None 

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

978 friends_status = api_pb2.User.FriendshipStatus.NA 

979 else: 

980 friend_relationship = session.execute( 

981 select(FriendRelationship) 

982 .where( 

983 or_( 

984 and_( 

985 FriendRelationship.from_user_id == context.user_id, 

986 FriendRelationship.to_user_id == db_user.id, 

987 ), 

988 and_( 

989 FriendRelationship.from_user_id == db_user.id, 

990 FriendRelationship.to_user_id == context.user_id, 

991 ), 

992 ) 

993 ) 

994 .where( 

995 or_( 

996 FriendRelationship.status == FriendStatus.accepted, 

997 FriendRelationship.status == FriendStatus.pending, 

998 ) 

999 ) 

1000 ).scalar_one_or_none() 

1001 

1002 if friend_relationship: 

1003 if friend_relationship.status == FriendStatus.accepted: 

1004 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1005 else: 

1006 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1008 # we sent it 

1009 pending_friend_request = api_pb2.FriendRequest( 

1010 friend_request_id=friend_relationship.id, 

1011 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1012 user_id=friend_relationship.to_user.id, 

1013 sent=True, 

1014 ) 

1015 else: 

1016 # we received it 

1017 pending_friend_request = api_pb2.FriendRequest( 

1018 friend_request_id=friend_relationship.id, 

1019 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1020 user_id=friend_relationship.from_user.id, 

1021 sent=False, 

1022 ) 

1023 else: 

1024 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1025 

1026 response_rate = session.execute( 

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

1028 ).scalar_one_or_none() 

1029 

1030 avatar_upload = get_avatar_upload(session, db_user) 

1031 

1032 verification_score = 0.0 

1033 if db_user.phone_verification_verified: 

1034 verification_score += 1.0 * db_user.phone_is_verified 

1035 

1036 user = api_pb2.User( 

1037 user_id=db_user.id, 

1038 username=db_user.username, 

1039 name=db_user.name, 

1040 city=db_user.city, 

1041 hometown=db_user.hometown, 

1042 timezone=db_user.timezone, 

1043 lat=lat, 

1044 lng=lng, 

1045 radius=db_user.geom_radius, 

1046 verification=verification_score, 

1047 community_standing=db_user.community_standing, 

1048 num_references=num_references, 

1049 gender=db_user.gender, 

1050 pronouns=db_user.pronouns, 

1051 age=int(db_user.age), 

1052 joined=Timestamp_from_datetime(db_user.display_joined), 

1053 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1054 hosting_status=hostingstatus2api[db_user.hosting_status], 

1055 meetup_status=meetupstatus2api[db_user.meetup_status], 

1056 occupation=db_user.occupation, 

1057 education=db_user.education, 

1058 about_me=db_user.about_me, 

1059 things_i_like=db_user.things_i_like, 

1060 about_place=db_user.about_place, 

1061 language_abilities=[ 

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

1063 for ability in db_user.language_abilities 

1064 ], 

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

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

1067 additional_information=db_user.additional_information, 

1068 friends=friends_status, 

1069 pending_friend_request=pending_friend_request, 

1070 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1071 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1072 parking_details=parkingdetails2api[db_user.parking_details], 

1073 avatar_url=avatar_upload.full_url if avatar_upload else None, 

1074 avatar_thumbnail_url=avatar_upload.thumbnail_url if avatar_upload else None, 

1075 profile_gallery_id=db_user.profile_gallery_id, 

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

1077 .scalars() 

1078 .all(), 

1079 **get_strong_verification_fields(session, db_user), 

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

1081 ) 

1082 

1083 if db_user.max_guests is not None: 

1084 user.max_guests.value = db_user.max_guests 

1085 

1086 if db_user.last_minute is not None: 

1087 user.last_minute.value = db_user.last_minute 

1088 

1089 if db_user.has_pets is not None: 

1090 user.has_pets.value = db_user.has_pets 

1091 

1092 if db_user.accepts_pets is not None: 

1093 user.accepts_pets.value = db_user.accepts_pets 

1094 

1095 if db_user.pet_details is not None: 

1096 user.pet_details.value = db_user.pet_details 

1097 

1098 if db_user.has_kids is not None: 

1099 user.has_kids.value = db_user.has_kids 

1100 

1101 if db_user.accepts_kids is not None: 

1102 user.accepts_kids.value = db_user.accepts_kids 

1103 

1104 if db_user.kid_details is not None: 

1105 user.kid_details.value = db_user.kid_details 

1106 

1107 if db_user.has_housemates is not None: 

1108 user.has_housemates.value = db_user.has_housemates 

1109 

1110 if db_user.housemate_details is not None: 

1111 user.housemate_details.value = db_user.housemate_details 

1112 

1113 if db_user.wheelchair_accessible is not None: 

1114 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1115 

1116 if db_user.smokes_at_home is not None: 

1117 user.smokes_at_home.value = db_user.smokes_at_home 

1118 

1119 if db_user.drinking_allowed is not None: 

1120 user.drinking_allowed.value = db_user.drinking_allowed 

1121 

1122 if db_user.drinks_at_home is not None: 

1123 user.drinks_at_home.value = db_user.drinks_at_home 

1124 

1125 if db_user.other_host_info is not None: 

1126 user.other_host_info.value = db_user.other_host_info 

1127 

1128 if db_user.sleeping_details is not None: 

1129 user.sleeping_details.value = db_user.sleeping_details 

1130 

1131 if db_user.area is not None: 

1132 user.area.value = db_user.area 

1133 

1134 if db_user.house_rules is not None: 

1135 user.house_rules.value = db_user.house_rules 

1136 

1137 if db_user.parking is not None: 

1138 user.parking.value = db_user.parking 

1139 

1140 if db_user.camping_ok is not None: 

1141 user.camping_ok.value = db_user.camping_ok 

1142 

1143 return user 

1144 

1145 

1146def lite_user_to_pb( 

1147 session: Session, 

1148 lite_user: LiteUser, 

1149 context: CouchersContext, 

1150 *, 

1151 is_admin_see_ghosts: bool = False, 

1152 is_get_user_return_ghosts: bool = False, 

1153) -> api_pb2.LiteUser: 

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

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

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

1157 # Return an anonymized "ghost" user profile 

1158 return api_pb2.LiteUser( 

1159 user_id=lite_user.id, 

1160 is_ghost=True, 

1161 username=GHOST_USERNAME, 

1162 name=context.get_localized_string("ghost_users.display_name"), 

1163 ) 

1164 raise GhostUserSerializationError( 

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

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

1167 ) 

1168 

1169 lat, lng = get_coordinates(lite_user.geom) 

1170 

1171 return api_pb2.LiteUser( 

1172 user_id=lite_user.id, 

1173 username=lite_user.username, 

1174 name=lite_user.name, 

1175 city=lite_user.city, 

1176 age=int(lite_user.age), 

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

1178 if lite_user.avatar_filename 

1179 else None, 

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

1181 if lite_user.avatar_filename 

1182 else None, 

1183 lat=lat, 

1184 lng=lng, 

1185 radius=lite_user.radius, 

1186 has_strong_verification=lite_user.has_strong_verification, 

1187 )