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

454 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-13 12:05 +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.notifications.notify import notify 

46from couchers.notifications.settings import get_topic_actions_by_delivery_type 

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

48from couchers.rate_limits.check import process_rate_limits_and_check_abort 

49from couchers.rate_limits.definitions import RATE_LIMIT_HOURS 

50from couchers.resources import get_badge_dict, language_is_allowed, region_is_allowed 

51from couchers.servicers.blocking import is_not_visible 

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

53from couchers.utils import ( 

54 Duration_from_timedelta, 

55 Timestamp_from_datetime, 

56 create_coordinate, 

57 get_coordinates, 

58 is_valid_name, 

59 is_valid_user_id, 

60 is_valid_username, 

61 not_none, 

62 now, 

63) 

64 

65 

66class GhostUserSerializationError(Exception): 

67 """ 

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

69 """ 

70 

71 pass 

72 

73 

74MAX_USERS_PER_QUERY = 200 

75MAX_PAGINATION_LENGTH = 50 

76 

77hostingstatus2sql = { 

78 api_pb2.HOSTING_STATUS_UNKNOWN: None, 

79 api_pb2.HOSTING_STATUS_CAN_HOST: HostingStatus.can_host, 

80 api_pb2.HOSTING_STATUS_MAYBE: HostingStatus.maybe, 

81 api_pb2.HOSTING_STATUS_CANT_HOST: HostingStatus.cant_host, 

82} 

83 

84hostingstatus2api = { 

85 None: api_pb2.HOSTING_STATUS_UNKNOWN, 

86 HostingStatus.can_host: api_pb2.HOSTING_STATUS_CAN_HOST, 

87 HostingStatus.maybe: api_pb2.HOSTING_STATUS_MAYBE, 

88 HostingStatus.cant_host: api_pb2.HOSTING_STATUS_CANT_HOST, 

89} 

90 

91meetupstatus2sql = { 

92 api_pb2.MEETUP_STATUS_UNKNOWN: None, 

93 api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP: MeetupStatus.wants_to_meetup, 

94 api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP: MeetupStatus.open_to_meetup, 

95 api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP: MeetupStatus.does_not_want_to_meetup, 

96} 

97 

98meetupstatus2api = { 

99 None: api_pb2.MEETUP_STATUS_UNKNOWN, 

100 MeetupStatus.wants_to_meetup: api_pb2.MEETUP_STATUS_WANTS_TO_MEETUP, 

101 MeetupStatus.open_to_meetup: api_pb2.MEETUP_STATUS_OPEN_TO_MEETUP, 

102 MeetupStatus.does_not_want_to_meetup: api_pb2.MEETUP_STATUS_DOES_NOT_WANT_TO_MEETUP, 

103} 

104 

105smokinglocation2sql = { 

106 api_pb2.SMOKING_LOCATION_UNKNOWN: None, 

107 api_pb2.SMOKING_LOCATION_YES: SmokingLocation.yes, 

108 api_pb2.SMOKING_LOCATION_WINDOW: SmokingLocation.window, 

109 api_pb2.SMOKING_LOCATION_OUTSIDE: SmokingLocation.outside, 

110 api_pb2.SMOKING_LOCATION_NO: SmokingLocation.no, 

111} 

112 

113smokinglocation2api = { 

114 None: api_pb2.SMOKING_LOCATION_UNKNOWN, 

115 SmokingLocation.yes: api_pb2.SMOKING_LOCATION_YES, 

116 SmokingLocation.window: api_pb2.SMOKING_LOCATION_WINDOW, 

117 SmokingLocation.outside: api_pb2.SMOKING_LOCATION_OUTSIDE, 

118 SmokingLocation.no: api_pb2.SMOKING_LOCATION_NO, 

119} 

120 

121sleepingarrangement2sql = { 

122 api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN: None, 

123 api_pb2.SLEEPING_ARRANGEMENT_PRIVATE: SleepingArrangement.private, 

124 api_pb2.SLEEPING_ARRANGEMENT_COMMON: SleepingArrangement.common, 

125 api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM: SleepingArrangement.shared_room, 

126} 

127 

128sleepingarrangement2api = { 

129 None: api_pb2.SLEEPING_ARRANGEMENT_UNKNOWN, 

130 SleepingArrangement.private: api_pb2.SLEEPING_ARRANGEMENT_PRIVATE, 

131 SleepingArrangement.common: api_pb2.SLEEPING_ARRANGEMENT_COMMON, 

132 SleepingArrangement.shared_room: api_pb2.SLEEPING_ARRANGEMENT_SHARED_ROOM, 

133} 

134 

135parkingdetails2sql = { 

136 api_pb2.PARKING_DETAILS_UNKNOWN: None, 

137 api_pb2.PARKING_DETAILS_FREE_ONSITE: ParkingDetails.free_onsite, 

138 api_pb2.PARKING_DETAILS_FREE_OFFSITE: ParkingDetails.free_offsite, 

139 api_pb2.PARKING_DETAILS_PAID_ONSITE: ParkingDetails.paid_onsite, 

140 api_pb2.PARKING_DETAILS_PAID_OFFSITE: ParkingDetails.paid_offsite, 

141} 

142 

143parkingdetails2api = { 

144 None: api_pb2.PARKING_DETAILS_UNKNOWN, 

145 ParkingDetails.free_onsite: api_pb2.PARKING_DETAILS_FREE_ONSITE, 

146 ParkingDetails.free_offsite: api_pb2.PARKING_DETAILS_FREE_OFFSITE, 

147 ParkingDetails.paid_onsite: api_pb2.PARKING_DETAILS_PAID_ONSITE, 

148 ParkingDetails.paid_offsite: api_pb2.PARKING_DETAILS_PAID_OFFSITE, 

149} 

150 

151fluency2sql = { 

152 api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN: None, 

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

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

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

156} 

157 

158fluency2api = { 

159 None: api_pb2.LanguageAbility.Fluency.FLUENCY_UNKNOWN, 

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

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

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

163} 

164 

165 

166class API(api_pb2_grpc.APIServicer): 

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

168 # auth ought to make sure the user exists 

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

170 

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

172 HostRequest.surfer_user_id == context.user_id 

173 ) 

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

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

176 sent_reqs_last_seen_message_ids = sent_reqs_query.subquery() 

177 

178 unseen_sent_host_request_count = session.execute( 

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

180 .join( 

181 Message, 

182 Message.conversation_id == sent_reqs_last_seen_message_ids.c.conversation_id, 

183 ) 

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

185 .where(Message.id != None) 

186 ).scalar_one() 

187 

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

189 HostRequest.host_user_id == context.user_id 

190 ) 

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

192 received_reqs_query = where_moderated_content_visible( 

193 received_reqs_query, context, HostRequest, is_list_operation=True 

194 ) 

195 received_reqs_last_seen_message_ids = received_reqs_query.subquery() 

196 

197 unseen_received_host_request_count = session.execute( 

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

199 .join( 

200 Message, 

201 Message.conversation_id == received_reqs_last_seen_message_ids.c.conversation_id, 

202 ) 

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

204 .where(Message.id != None) 

205 ).scalar_one() 

206 

207 unseen_message_query = ( 

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

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

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

211 ) 

212 unseen_message_query = where_moderated_content_visible( 

213 unseen_message_query, context, GroupChat, is_list_operation=True 

214 ) 

215 unseen_message_query = ( 

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

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

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

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

220 ) 

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

222 

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

224 FriendRelationship.to_user_id == context.user_id 

225 ) 

226 pending_friend_request_query = where_users_column_visible( 

227 pending_friend_request_query, context, FriendRelationship.from_user_id 

228 ) 

229 pending_friend_request_query = pending_friend_request_query.where( 

230 FriendRelationship.status == FriendStatus.pending 

231 ) 

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

233 

234 unseen_notification_count = session.execute( 

235 select(func.count()) 

236 .select_from(Notification) 

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

238 .where(Notification.is_seen == False) 

239 .where( 

240 Notification.topic_action.in_( 

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

242 ) 

243 ) 

244 ).scalar_one() 

245 

246 return api_pb2.PingRes( 

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

248 unseen_message_count=unseen_message_count, 

249 unseen_sent_host_request_count=unseen_sent_host_request_count, 

250 unseen_received_host_request_count=unseen_received_host_request_count, 

251 pending_friend_request_count=pending_friend_request_count, 

252 unseen_notification_count=unseen_notification_count, 

253 ) 

254 

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

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

257 

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

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

260 

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

262 

263 def GetLiteUser( 

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

265 ) -> api_pb2.LiteUser: 

266 lite_user = session.execute( 

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

268 ).scalar_one_or_none() 

269 

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

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

272 

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

274 

275 def GetLiteUsers( 

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

277 ) -> api_pb2.GetLiteUsersRes: 

278 if len(request.users) > MAX_USERS_PER_QUERY: 

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

280 

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

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

283 

284 # decomposed where_username_or_id... 

285 users = ( 

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

287 .scalars() 

288 .all() 

289 ) 

290 

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

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

293 

294 res = api_pb2.GetLiteUsersRes() 

295 

296 for user in request.users: 

297 lite_user = None 

298 if user in users_by_id: 

299 lite_user = users_by_id[user] 

300 elif user in users_by_username: 

301 lite_user = users_by_username[user] 

302 

303 res.responses.append( 

304 api_pb2.LiteUserRes( 

305 query=user, 

306 not_found=lite_user is None, 

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

308 if lite_user 

309 else None, 

310 ) 

311 ) 

312 

313 return res 

314 

315 def UpdateProfile( 

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

317 ) -> empty_pb2.Empty: 

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

319 

320 if request.HasField("name"): 

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

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

323 user.name = request.name.value 

324 

325 if request.HasField("city"): 

326 user.city = request.city.value 

327 

328 if request.HasField("hometown"): 

329 if request.hometown.is_null: 

330 user.hometown = None 

331 else: 

332 user.hometown = request.hometown.value 

333 

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

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

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

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

338 user.randomized_geom = None 

339 

340 if request.HasField("radius"): 

341 user.geom_radius = request.radius.value 

342 

343 if request.HasField("avatar_key"): 343 ↛ 344line 343 didn't jump to line 344 because the condition on line 343 was never true

344 if request.avatar_key.is_null: 

345 user.avatar_key = None 

346 else: 

347 user.avatar_key = request.avatar_key.value 

348 

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

350 # user.gender = request.gender.value 

351 

352 if request.HasField("pronouns"): 

353 if request.pronouns.is_null: 

354 user.pronouns = None 

355 else: 

356 user.pronouns = request.pronouns.value 

357 

358 if request.HasField("occupation"): 

359 if request.occupation.is_null: 

360 user.occupation = None 

361 else: 

362 user.occupation = request.occupation.value 

363 

364 if request.HasField("education"): 

365 if request.education.is_null: 

366 user.education = None 

367 else: 

368 user.education = request.education.value 

369 

370 if request.HasField("about_me"): 

371 if request.about_me.is_null: 

372 user.about_me = None 

373 else: 

374 user.about_me = request.about_me.value 

375 

376 if request.HasField("things_i_like"): 

377 if request.things_i_like.is_null: 

378 user.things_i_like = None 

379 else: 

380 user.things_i_like = request.things_i_like.value 

381 

382 if request.HasField("about_place"): 

383 if request.about_place.is_null: 

384 user.about_place = None 

385 else: 

386 user.about_place = request.about_place.value 

387 

388 if request.hosting_status != api_pb2.HOSTING_STATUS_UNSPECIFIED: 

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

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

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

392 

393 if request.meetup_status != api_pb2.MEETUP_STATUS_UNSPECIFIED: 

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

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

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

397 

398 if request.HasField("language_abilities"): 

399 # delete all existing abilities 

400 for ability in user.language_abilities: 

401 session.delete(ability) 

402 session.flush() 

403 

404 # add the new ones 

405 for language_ability in request.language_abilities.value: 

406 if not language_is_allowed(language_ability.code): 

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

408 session.add( 

409 LanguageAbility( 

410 user_id=user.id, 

411 language_code=language_ability.code, 

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

413 ) 

414 ) 

415 

416 if request.HasField("regions_visited"): 

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

418 

419 for region in request.regions_visited.value: 

420 if not region_is_allowed(region): 

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

422 session.add( 

423 RegionVisited( 

424 user_id=user.id, 

425 region_code=region, 

426 ) 

427 ) 

428 

429 if request.HasField("regions_lived"): 

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

431 

432 for region in request.regions_lived.value: 

433 if not region_is_allowed(region): 

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

435 session.add( 

436 RegionLived( 

437 user_id=user.id, 

438 region_code=region, 

439 ) 

440 ) 

441 

442 if request.HasField("additional_information"): 

443 if request.additional_information.is_null: 

444 user.additional_information = None 

445 else: 

446 user.additional_information = request.additional_information.value 

447 

448 if request.HasField("max_guests"): 

449 if request.max_guests.is_null: 

450 user.max_guests = None 

451 else: 

452 user.max_guests = request.max_guests.value 

453 

454 if request.HasField("last_minute"): 

455 if request.last_minute.is_null: 

456 user.last_minute = None 

457 else: 

458 user.last_minute = request.last_minute.value 

459 

460 if request.HasField("has_pets"): 

461 if request.has_pets.is_null: 

462 user.has_pets = None 

463 else: 

464 user.has_pets = request.has_pets.value 

465 

466 if request.HasField("accepts_pets"): 

467 if request.accepts_pets.is_null: 

468 user.accepts_pets = None 

469 else: 

470 user.accepts_pets = request.accepts_pets.value 

471 

472 if request.HasField("pet_details"): 

473 if request.pet_details.is_null: 

474 user.pet_details = None 

475 else: 

476 user.pet_details = request.pet_details.value 

477 

478 if request.HasField("has_kids"): 

479 if request.has_kids.is_null: 

480 user.has_kids = None 

481 else: 

482 user.has_kids = request.has_kids.value 

483 

484 if request.HasField("accepts_kids"): 

485 if request.accepts_kids.is_null: 

486 user.accepts_kids = None 

487 else: 

488 user.accepts_kids = request.accepts_kids.value 

489 

490 if request.HasField("kid_details"): 

491 if request.kid_details.is_null: 

492 user.kid_details = None 

493 else: 

494 user.kid_details = request.kid_details.value 

495 

496 if request.HasField("has_housemates"): 

497 if request.has_housemates.is_null: 

498 user.has_housemates = None 

499 else: 

500 user.has_housemates = request.has_housemates.value 

501 

502 if request.HasField("housemate_details"): 

503 if request.housemate_details.is_null: 

504 user.housemate_details = None 

505 else: 

506 user.housemate_details = request.housemate_details.value 

507 

508 if request.HasField("wheelchair_accessible"): 

509 if request.wheelchair_accessible.is_null: 

510 user.wheelchair_accessible = None 

511 else: 

512 user.wheelchair_accessible = request.wheelchair_accessible.value 

513 

514 if request.smoking_allowed != api_pb2.SMOKING_LOCATION_UNSPECIFIED: 

515 user.smoking_allowed = smokinglocation2sql[request.smoking_allowed] 

516 

517 if request.HasField("smokes_at_home"): 

518 if request.smokes_at_home.is_null: 

519 user.smokes_at_home = None 

520 else: 

521 user.smokes_at_home = request.smokes_at_home.value 

522 

523 if request.HasField("drinking_allowed"): 

524 if request.drinking_allowed.is_null: 

525 user.drinking_allowed = None 

526 else: 

527 user.drinking_allowed = request.drinking_allowed.value 

528 

529 if request.HasField("drinks_at_home"): 

530 if request.drinks_at_home.is_null: 

531 user.drinks_at_home = None 

532 else: 

533 user.drinks_at_home = request.drinks_at_home.value 

534 

535 if request.HasField("other_host_info"): 

536 if request.other_host_info.is_null: 

537 user.other_host_info = None 

538 else: 

539 user.other_host_info = request.other_host_info.value 

540 

541 if request.sleeping_arrangement != api_pb2.SLEEPING_ARRANGEMENT_UNSPECIFIED: 

542 user.sleeping_arrangement = sleepingarrangement2sql[request.sleeping_arrangement] 

543 

544 if request.HasField("sleeping_details"): 

545 if request.sleeping_details.is_null: 

546 user.sleeping_details = None 

547 else: 

548 user.sleeping_details = request.sleeping_details.value 

549 

550 if request.HasField("area"): 

551 if request.area.is_null: 

552 user.area = None 

553 else: 

554 user.area = request.area.value 

555 

556 if request.HasField("house_rules"): 

557 if request.house_rules.is_null: 

558 user.house_rules = None 

559 else: 

560 user.house_rules = request.house_rules.value 

561 

562 if request.HasField("parking"): 

563 if request.parking.is_null: 

564 user.parking = None 

565 else: 

566 user.parking = request.parking.value 

567 

568 if request.parking_details != api_pb2.PARKING_DETAILS_UNSPECIFIED: 

569 user.parking_details = parkingdetails2sql[request.parking_details] 

570 

571 if request.HasField("camping_ok"): 

572 if request.camping_ok.is_null: 

573 user.camping_ok = None 

574 else: 

575 user.camping_ok = request.camping_ok.value 

576 

577 user.profile_last_updated = now() 

578 

579 return empty_pb2.Empty() 

580 

581 def ListFriends( 

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

583 ) -> api_pb2.ListFriendsRes: 

584 rels_query = select(FriendRelationship) 

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

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

587 rels_query = rels_query.where( 

588 or_( 

589 FriendRelationship.from_user_id == context.user_id, 

590 FriendRelationship.to_user_id == context.user_id, 

591 ) 

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

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

594 return api_pb2.ListFriendsRes( 

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

596 ) 

597 

598 def RemoveFriend( 

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

600 ) -> empty_pb2.Empty: 

601 rel_query = select(FriendRelationship) 

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

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

604 rel_query = rel_query.where( 

605 or_( 

606 and_( 

607 FriendRelationship.from_user_id == request.user_id, 

608 FriendRelationship.to_user_id == context.user_id, 

609 ), 

610 and_( 

611 FriendRelationship.from_user_id == context.user_id, 

612 FriendRelationship.to_user_id == request.user_id, 

613 ), 

614 ) 

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

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

617 

618 if not rel: 

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

620 

621 session.delete(rel) 

622 

623 return empty_pb2.Empty() 

624 

625 def ListMutualFriends( 

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

627 ) -> api_pb2.ListMutualFriendsRes: 

628 if context.user_id == request.user_id: 

629 return api_pb2.ListMutualFriendsRes(mutual_friends=[]) 

630 

631 user = session.execute( 

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

633 ).scalar_one_or_none() 

634 

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

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

637 

638 q1 = ( 

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

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

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

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

643 ) 

644 

645 q2 = ( 

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

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

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

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

650 ) 

651 

652 q3 = ( 

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

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

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

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

657 ) 

658 

659 q4 = ( 

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

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

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

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

664 ) 

665 

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

667 

668 mutual_friends = ( 

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

670 ) 

671 

672 return api_pb2.ListMutualFriendsRes( 

673 mutual_friends=[ 

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

675 for mutual_friend in mutual_friends 

676 ] 

677 ) 

678 

679 def SendFriendRequest( 

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

681 ) -> empty_pb2.Empty: 

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

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

684 

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

686 to_user = session.execute( 

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

688 ).scalar_one_or_none() 

689 

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

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

692 

693 if ( 

694 session.execute( 

695 select(FriendRelationship) 

696 .where( 

697 or_( 

698 and_( 

699 FriendRelationship.from_user_id == context.user_id, 

700 FriendRelationship.to_user_id == request.user_id, 

701 ), 

702 and_( 

703 FriendRelationship.from_user_id == request.user_id, 

704 FriendRelationship.to_user_id == context.user_id, 

705 ), 

706 ) 

707 ) 

708 .where( 

709 or_( 

710 FriendRelationship.status == FriendStatus.accepted, 

711 FriendRelationship.status == FriendStatus.pending, 

712 ) 

713 ) 

714 ).scalar_one_or_none() 

715 is not None 

716 ): 

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

718 

719 # Check if user has been sending friend requests excessively 

720 if process_rate_limits_and_check_abort( 

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

722 ): 

723 context.abort_with_error_code( 

724 grpc.StatusCode.RESOURCE_EXHAUSTED, 

725 "friend_request_rate_limit", 

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

727 ) 

728 

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

730 

731 friend_relationship = FriendRelationship( 

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

733 ) 

734 session.add(friend_relationship) 

735 session.flush() 

736 

737 notify( 

738 session, 

739 user_id=friend_relationship.to_user_id, 

740 topic_action=NotificationTopicAction.friend_request__create, 

741 key=str(friend_relationship.from_user_id), 

742 data=notification_data_pb2.FriendRequestCreate( 

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

744 ), 

745 ) 

746 

747 return empty_pb2.Empty() 

748 

749 def ListFriendRequests( 

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

751 ) -> api_pb2.ListFriendRequestsRes: 

752 # both sent and received 

753 sent_requests_query = select(FriendRelationship) 

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

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

756 FriendRelationship.status == FriendStatus.pending 

757 ) 

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

759 

760 received_requests_query = select(FriendRelationship) 

761 received_requests_query = where_users_column_visible( 

762 received_requests_query, context, FriendRelationship.from_user_id 

763 ) 

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

765 FriendRelationship.status == FriendStatus.pending 

766 ) 

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

768 

769 return api_pb2.ListFriendRequestsRes( 

770 sent=[ 

771 api_pb2.FriendRequest( 

772 friend_request_id=friend_request.id, 

773 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

774 user_id=friend_request.to_user.id, 

775 sent=True, 

776 ) 

777 for friend_request in sent_requests 

778 ], 

779 received=[ 

780 api_pb2.FriendRequest( 

781 friend_request_id=friend_request.id, 

782 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

783 user_id=friend_request.from_user.id, 

784 sent=False, 

785 ) 

786 for friend_request in received_requests 

787 ], 

788 ) 

789 

790 def RespondFriendRequest( 

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

792 ) -> empty_pb2.Empty: 

793 friend_request_query = select(FriendRelationship) 

794 friend_request_query = where_users_column_visible( 

795 friend_request_query, context, FriendRelationship.from_user_id 

796 ) 

797 friend_request_query = ( 

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

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

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

801 ) 

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

803 

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

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

806 

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

808 friend_request.time_responded = func.now() 

809 

810 session.flush() 

811 

812 if friend_request.status == FriendStatus.accepted: 

813 notify( 

814 session, 

815 user_id=friend_request.from_user_id, 

816 topic_action=NotificationTopicAction.friend_request__accept, 

817 key=str(friend_request.to_user_id), 

818 data=notification_data_pb2.FriendRequestAccept( 

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

820 ), 

821 ) 

822 

823 return empty_pb2.Empty() 

824 

825 def CancelFriendRequest( 

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

827 ) -> empty_pb2.Empty: 

828 friend_request_query = select(FriendRelationship) 

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

830 friend_request_query = ( 

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

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

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

834 ) 

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

836 

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

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

839 

840 friend_request.status = FriendStatus.cancelled 

841 friend_request.time_responded = func.now() 

842 

843 # note no notifications 

844 

845 session.commit() 

846 

847 return empty_pb2.Empty() 

848 

849 def InitiateMediaUpload( 

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

851 ) -> api_pb2.InitiateMediaUploadRes: 

852 key = random_hex() 

853 

854 created = now() 

855 expiry = created + timedelta(minutes=20) 

856 

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

858 session.add(upload) 

859 session.commit() 

860 

861 req = media_pb2.UploadRequest( 

862 key=upload.key, 

863 type=media_pb2.UploadRequest.UploadType.IMAGE, 

864 created=Timestamp_from_datetime(upload.created), 

865 expiry=Timestamp_from_datetime(upload.expiry), 

866 max_width=2000, 

867 max_height=1600, 

868 ).SerializeToString() 

869 

870 data = b64encode(req) 

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

872 

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

874 

875 return api_pb2.InitiateMediaUploadRes( 

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

877 expiry=Timestamp_from_datetime(expiry), 

878 ) 

879 

880 def ListBadgeUsers( 

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

882 ) -> api_pb2.ListBadgeUsersRes: 

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

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

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

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

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

888 

889 badge_user_ids_query = ( 

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

891 ) 

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

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

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

895 

896 return api_pb2.ListBadgeUsersRes( 

897 user_ids=badge_user_ids[:page_size], 

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

899 ) 

900 

901 

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

903 if not response_rate: 

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

905 

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

907 if response_rate.requests < 3: 

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

909 

910 if response_rate.response_rate <= 0.33: 

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

912 

913 response_time_p33_coarsened = Duration_from_timedelta( 

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

915 ) 

916 

917 if response_rate.response_rate <= 0.66: 

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

919 

920 response_time_p66_coarsened = Duration_from_timedelta( 

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

922 ) 

923 

924 if response_rate.response_rate <= 0.90: 

925 return { 

926 "most": requests_pb2.ResponseRateMost( 

927 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

928 ) 

929 } 

930 else: 

931 return { 

932 "almost_all": requests_pb2.ResponseRateAlmostAll( 

933 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

934 ) 

935 } 

936 

937 

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

939 query = ( 

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

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

942 .where(Reference.is_deleted == False) 

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

944 .where(User.is_visible) 

945 .group_by(Reference.to_user_id) 

946 ) 

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

948 

949 

950def user_model_to_pb( 

951 db_user: User, 

952 session: Session, 

953 context: CouchersContext, 

954 *, 

955 is_admin_see_ghosts: bool = False, 

956 is_get_user_return_ghosts: bool = False, 

957) -> api_pb2.User: 

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

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

960 

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

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

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

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

965 # Return an anonymized "ghost" user profile 

966 return api_pb2.User( 

967 user_id=db_user.id, 

968 is_ghost=True, 

969 username=GHOST_USERNAME, 

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

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

972 ) 

973 raise GhostUserSerializationError( 

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

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

976 ) 

977 

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

979 lat, lng = db_user.coordinates 

980 

981 pending_friend_request = None 

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

983 friends_status = api_pb2.User.FriendshipStatus.NA 

984 else: 

985 friend_relationship = session.execute( 

986 select(FriendRelationship) 

987 .where( 

988 or_( 

989 and_( 

990 FriendRelationship.from_user_id == context.user_id, 

991 FriendRelationship.to_user_id == db_user.id, 

992 ), 

993 and_( 

994 FriendRelationship.from_user_id == db_user.id, 

995 FriendRelationship.to_user_id == context.user_id, 

996 ), 

997 ) 

998 ) 

999 .where( 

1000 or_( 

1001 FriendRelationship.status == FriendStatus.accepted, 

1002 FriendRelationship.status == FriendStatus.pending, 

1003 ) 

1004 ) 

1005 ).scalar_one_or_none() 

1006 

1007 if friend_relationship: 

1008 if friend_relationship.status == FriendStatus.accepted: 

1009 friends_status = api_pb2.User.FriendshipStatus.FRIENDS 

1010 else: 

1011 friends_status = api_pb2.User.FriendshipStatus.PENDING 

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

1013 # we sent it 

1014 pending_friend_request = api_pb2.FriendRequest( 

1015 friend_request_id=friend_relationship.id, 

1016 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1017 user_id=friend_relationship.to_user.id, 

1018 sent=True, 

1019 ) 

1020 else: 

1021 # we received it 

1022 pending_friend_request = api_pb2.FriendRequest( 

1023 friend_request_id=friend_relationship.id, 

1024 state=api_pb2.FriendRequest.FriendRequestStatus.PENDING, 

1025 user_id=friend_relationship.from_user.id, 

1026 sent=False, 

1027 ) 

1028 else: 

1029 friends_status = api_pb2.User.FriendshipStatus.NOT_FRIENDS 

1030 

1031 response_rate = session.execute( 

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

1033 ).scalar_one_or_none() 

1034 

1035 verification_score = 0.0 

1036 if db_user.phone_verification_verified: 

1037 verification_score += 1.0 * db_user.phone_is_verified 

1038 

1039 user = api_pb2.User( 

1040 user_id=db_user.id, 

1041 username=db_user.username, 

1042 name=db_user.name, 

1043 city=db_user.city, 

1044 hometown=db_user.hometown, 

1045 timezone=db_user.timezone, 

1046 lat=lat, 

1047 lng=lng, 

1048 radius=db_user.geom_radius, 

1049 verification=verification_score, 

1050 community_standing=db_user.community_standing, 

1051 num_references=num_references, 

1052 gender=db_user.gender, 

1053 pronouns=db_user.pronouns, 

1054 age=int(db_user.age), 

1055 joined=Timestamp_from_datetime(db_user.display_joined), 

1056 last_active=Timestamp_from_datetime(db_user.display_last_active), 

1057 hosting_status=hostingstatus2api[db_user.hosting_status], 

1058 meetup_status=meetupstatus2api[db_user.meetup_status], 

1059 occupation=db_user.occupation, 

1060 education=db_user.education, 

1061 about_me=db_user.about_me, 

1062 things_i_like=db_user.things_i_like, 

1063 about_place=db_user.about_place, 

1064 language_abilities=[ 

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

1066 for ability in db_user.language_abilities 

1067 ], 

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

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

1070 additional_information=db_user.additional_information, 

1071 friends=friends_status, 

1072 pending_friend_request=pending_friend_request, 

1073 smoking_allowed=smokinglocation2api[db_user.smoking_allowed], 

1074 sleeping_arrangement=sleepingarrangement2api[db_user.sleeping_arrangement], 

1075 parking_details=parkingdetails2api[db_user.parking_details], 

1076 avatar_url=db_user.avatar.full_url if db_user.avatar else None, 

1077 avatar_thumbnail_url=db_user.avatar.thumbnail_url if db_user.avatar else None, 

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

1079 .scalars() 

1080 .all(), 

1081 **get_strong_verification_fields(session, db_user), 

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

1083 ) 

1084 

1085 if db_user.max_guests is not None: 

1086 user.max_guests.value = db_user.max_guests 

1087 

1088 if db_user.last_minute is not None: 

1089 user.last_minute.value = db_user.last_minute 

1090 

1091 if db_user.has_pets is not None: 

1092 user.has_pets.value = db_user.has_pets 

1093 

1094 if db_user.accepts_pets is not None: 

1095 user.accepts_pets.value = db_user.accepts_pets 

1096 

1097 if db_user.pet_details is not None: 

1098 user.pet_details.value = db_user.pet_details 

1099 

1100 if db_user.has_kids is not None: 

1101 user.has_kids.value = db_user.has_kids 

1102 

1103 if db_user.accepts_kids is not None: 

1104 user.accepts_kids.value = db_user.accepts_kids 

1105 

1106 if db_user.kid_details is not None: 

1107 user.kid_details.value = db_user.kid_details 

1108 

1109 if db_user.has_housemates is not None: 

1110 user.has_housemates.value = db_user.has_housemates 

1111 

1112 if db_user.housemate_details is not None: 

1113 user.housemate_details.value = db_user.housemate_details 

1114 

1115 if db_user.wheelchair_accessible is not None: 

1116 user.wheelchair_accessible.value = db_user.wheelchair_accessible 

1117 

1118 if db_user.smokes_at_home is not None: 

1119 user.smokes_at_home.value = db_user.smokes_at_home 

1120 

1121 if db_user.drinking_allowed is not None: 

1122 user.drinking_allowed.value = db_user.drinking_allowed 

1123 

1124 if db_user.drinks_at_home is not None: 

1125 user.drinks_at_home.value = db_user.drinks_at_home 

1126 

1127 if db_user.other_host_info is not None: 

1128 user.other_host_info.value = db_user.other_host_info 

1129 

1130 if db_user.sleeping_details is not None: 

1131 user.sleeping_details.value = db_user.sleeping_details 

1132 

1133 if db_user.area is not None: 

1134 user.area.value = db_user.area 

1135 

1136 if db_user.house_rules is not None: 

1137 user.house_rules.value = db_user.house_rules 

1138 

1139 if db_user.parking is not None: 

1140 user.parking.value = db_user.parking 

1141 

1142 if db_user.camping_ok is not None: 

1143 user.camping_ok.value = db_user.camping_ok 

1144 

1145 return user 

1146 

1147 

1148def lite_user_to_pb( 

1149 session: Session, 

1150 lite_user: LiteUser, 

1151 context: CouchersContext, 

1152 *, 

1153 is_admin_see_ghosts: bool = False, 

1154 is_get_user_return_ghosts: bool = False, 

1155) -> api_pb2.LiteUser: 

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

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

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

1159 # Return an anonymized "ghost" user profile 

1160 return api_pb2.LiteUser( 

1161 user_id=lite_user.id, 

1162 is_ghost=True, 

1163 username=GHOST_USERNAME, 

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

1165 ) 

1166 raise GhostUserSerializationError( 

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

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

1169 ) 

1170 

1171 lat, lng = get_coordinates(lite_user.geom) 

1172 

1173 return api_pb2.LiteUser( 

1174 user_id=lite_user.id, 

1175 username=lite_user.username, 

1176 name=lite_user.name, 

1177 city=lite_user.city, 

1178 age=int(lite_user.age), 

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

1180 if lite_user.avatar_filename 

1181 else None, 

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

1183 if lite_user.avatar_filename 

1184 else None, 

1185 lat=lat, 

1186 lng=lng, 

1187 radius=lite_user.radius, 

1188 has_strong_verification=lite_user.has_strong_verification, 

1189 )