Coverage for src/couchers/servicers/requests.py: 92%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

250 statements  

1import logging 

2from datetime import timedelta 

3 

4import grpc 

5from google.protobuf import empty_pb2 

6from sqlalchemy import Float 

7from sqlalchemy.orm import aliased 

8from sqlalchemy.sql import and_, func, or_ 

9from sqlalchemy.sql.functions import percentile_disc 

10 

11from couchers import errors, urls 

12from couchers.db import session_scope 

13from couchers.models import Conversation, HostRequest, HostRequestStatus, Message, MessageType, User 

14from couchers.notifications.notify import notify 

15from couchers.sql import couchers_select as select 

16from couchers.tasks import ( 

17 send_host_request_accepted_email_to_guest, 

18 send_host_request_cancelled_email_to_host, 

19 send_host_request_confirmed_email_to_host, 

20 send_host_request_rejected_email_to_guest, 

21 send_new_host_request_email, 

22) 

23from couchers.utils import ( 

24 Duration_from_timedelta, 

25 Timestamp_from_datetime, 

26 date_to_api, 

27 now, 

28 parse_date, 

29 today_in_timezone, 

30) 

31from proto import conversations_pb2, requests_pb2, requests_pb2_grpc 

32 

33logger = logging.getLogger(__name__) 

34 

35DEFAULT_PAGINATION_LENGTH = 10 

36MAX_PAGE_SIZE = 50 

37 

38 

39hostrequeststatus2api = { 

40 HostRequestStatus.pending: conversations_pb2.HOST_REQUEST_STATUS_PENDING, 

41 HostRequestStatus.accepted: conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

42 HostRequestStatus.rejected: conversations_pb2.HOST_REQUEST_STATUS_REJECTED, 

43 HostRequestStatus.confirmed: conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

44 HostRequestStatus.cancelled: conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

45} 

46 

47 

48def message_to_pb(message: Message): 

49 """ 

50 Turns the given message to a protocol buffer 

51 """ 

52 if message.is_normal_message: 

53 return conversations_pb2.Message( 

54 message_id=message.id, 

55 author_user_id=message.author_id, 

56 time=Timestamp_from_datetime(message.time), 

57 text=conversations_pb2.MessageContentText(text=message.text), 

58 ) 

59 else: 

60 return conversations_pb2.Message( 

61 message_id=message.id, 

62 author_user_id=message.author_id, 

63 time=Timestamp_from_datetime(message.time), 

64 chat_created=conversations_pb2.MessageContentChatCreated() 

65 if message.message_type == MessageType.chat_created 

66 else None, 

67 host_request_status_changed=conversations_pb2.MessageContentHostRequestStatusChanged( 

68 status=hostrequeststatus2api[message.host_request_status_target] 

69 ) 

70 if message.message_type == MessageType.host_request_status_changed 

71 else None, 

72 ) 

73 

74 

75class Requests(requests_pb2_grpc.RequestsServicer): 

76 def CreateHostRequest(self, request, context): 

77 with session_scope() as session: 

78 if request.host_user_id == context.user_id: 

79 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.CANT_REQUEST_SELF) 

80 

81 # just to check host exists and is visible 

82 host = session.execute( 

83 select(User).where_users_visible(context).where(User.id == request.host_user_id) 

84 ).scalar_one_or_none() 

85 if not host: 

86 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

87 

88 from_date = parse_date(request.from_date) 

89 to_date = parse_date(request.to_date) 

90 

91 if not from_date or not to_date: 

92 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_DATE) 

93 

94 today = today_in_timezone(host.timezone) 

95 

96 # request starts from the past 

97 if from_date < today: 

98 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_BEFORE_TODAY) 

99 

100 # from_date is not >= to_date 

101 if from_date >= to_date: 

102 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_AFTER_TO) 

103 

104 # No need to check today > to_date 

105 

106 if from_date - today > timedelta(days=365): 

107 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_AFTER_ONE_YEAR) 

108 

109 if to_date - from_date > timedelta(days=365): 

110 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_TO_AFTER_ONE_YEAR) 

111 

112 conversation = Conversation() 

113 session.add(conversation) 

114 session.flush() 

115 

116 session.add( 

117 Message( 

118 conversation_id=conversation.id, 

119 author_id=context.user_id, 

120 message_type=MessageType.chat_created, 

121 ) 

122 ) 

123 

124 message = Message( 

125 conversation_id=conversation.id, 

126 author_id=context.user_id, 

127 text=request.text, 

128 message_type=MessageType.text, 

129 ) 

130 session.add(message) 

131 session.flush() 

132 

133 host_request = HostRequest( 

134 conversation_id=conversation.id, 

135 surfer_user_id=context.user_id, 

136 host_user_id=host.id, 

137 from_date=from_date, 

138 to_date=to_date, 

139 status=HostRequestStatus.pending, 

140 surfer_last_seen_message_id=message.id, 

141 # TODO: tz 

142 # timezone=host.timezone, 

143 ) 

144 session.add(host_request) 

145 session.commit() 

146 

147 send_new_host_request_email(host_request) 

148 

149 notify( 

150 user_id=host_request.host_user_id, 

151 topic="host_request", 

152 action="create", 

153 key=str(host_request.surfer_user_id), 

154 avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, 

155 title=f"**{host_request.surfer.name}** sent you a hosting request", 

156 content=request.text, 

157 link=urls.host_request_link_host(), 

158 ) 

159 

160 return requests_pb2.CreateHostRequestRes(host_request_id=host_request.conversation_id) 

161 

162 def GetHostRequest(self, request, context): 

163 with session_scope() as session: 

164 host_request = session.execute( 

165 select(HostRequest) 

166 .where_users_column_visible(context, HostRequest.surfer_user_id) 

167 .where_users_column_visible(context, HostRequest.host_user_id) 

168 .where(HostRequest.conversation_id == request.host_request_id) 

169 .where(or_(HostRequest.surfer_user_id == context.user_id, HostRequest.host_user_id == context.user_id)) 

170 ).scalar_one_or_none() 

171 

172 if not host_request: 

173 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

174 

175 initial_message = session.execute( 

176 select(Message) 

177 .where(Message.conversation_id == host_request.conversation_id) 

178 .order_by(Message.id.asc()) 

179 .limit(1) 

180 ).scalar_one() 

181 

182 latest_message = session.execute( 

183 select(Message) 

184 .where(Message.conversation_id == host_request.conversation_id) 

185 .order_by(Message.id.desc()) 

186 .limit(1) 

187 ).scalar_one() 

188 

189 return requests_pb2.HostRequest( 

190 host_request_id=host_request.conversation_id, 

191 surfer_user_id=host_request.surfer_user_id, 

192 host_user_id=host_request.host_user_id, 

193 status=hostrequeststatus2api[host_request.status], 

194 created=Timestamp_from_datetime(initial_message.time), 

195 from_date=date_to_api(host_request.from_date), 

196 to_date=date_to_api(host_request.to_date), 

197 last_seen_message_id=host_request.surfer_last_seen_message_id 

198 if context.user_id == host_request.surfer_user_id 

199 else host_request.host_last_seen_message_id, 

200 latest_message=message_to_pb(latest_message), 

201 ) 

202 

203 def ListHostRequests(self, request, context): 

204 if request.only_sent and request.only_received: 

205 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_SENT_OR_RECEIVED) 

206 

207 with session_scope() as session: 

208 pagination = request.number if request.number > 0 else DEFAULT_PAGINATION_LENGTH 

209 pagination = min(pagination, MAX_PAGE_SIZE) 

210 

211 # By outer joining messages on itself where the second id is bigger, only the highest IDs will have 

212 # none as message_2.id. So just filter for these ones to get highest messages only. 

213 # See https://stackoverflow.com/a/27802817/6115336 

214 message_2 = aliased(Message) 

215 statement = ( 

216 select(Message, HostRequest, Conversation) 

217 .outerjoin( 

218 message_2, and_(Message.conversation_id == message_2.conversation_id, Message.id < message_2.id) 

219 ) 

220 .join(HostRequest, HostRequest.conversation_id == Message.conversation_id) 

221 .join(Conversation, Conversation.id == HostRequest.conversation_id) 

222 .where_users_column_visible(context, HostRequest.surfer_user_id) 

223 .where_users_column_visible(context, HostRequest.host_user_id) 

224 .where(message_2.id == None) 

225 .where(or_(Message.id < request.last_request_id, request.last_request_id == 0)) 

226 ) 

227 

228 if request.only_sent: 

229 statement = statement.where(HostRequest.surfer_user_id == context.user_id) 

230 elif request.only_received: 

231 statement = statement.where(HostRequest.host_user_id == context.user_id) 

232 else: 

233 statement = statement.where( 

234 or_(HostRequest.host_user_id == context.user_id, HostRequest.surfer_user_id == context.user_id) 

235 ) 

236 

237 # TODO: I considered having the latest control message be the single source of truth for 

238 # the HostRequest.status, but decided against it because of this filter. 

239 # Another possibility is to filter in the python instead of SQL, but that's slower 

240 if request.only_active: 

241 statement = statement.where( 

242 or_( 

243 HostRequest.status == HostRequestStatus.pending, 

244 HostRequest.status == HostRequestStatus.accepted, 

245 HostRequest.status == HostRequestStatus.confirmed, 

246 ) 

247 ) 

248 statement = statement.where(HostRequest.end_time <= func.now()) 

249 

250 statement = statement.order_by(Message.id.desc()).limit(pagination + 1) 

251 results = session.execute(statement).all() 

252 

253 host_requests = [ 

254 requests_pb2.HostRequest( 

255 host_request_id=result.HostRequest.conversation_id, 

256 surfer_user_id=result.HostRequest.surfer_user_id, 

257 host_user_id=result.HostRequest.host_user_id, 

258 status=hostrequeststatus2api[result.HostRequest.status], 

259 created=Timestamp_from_datetime(result.Conversation.created), 

260 from_date=date_to_api(result.HostRequest.from_date), 

261 to_date=date_to_api(result.HostRequest.to_date), 

262 last_seen_message_id=result.HostRequest.surfer_last_seen_message_id 

263 if context.user_id == result.HostRequest.surfer_user_id 

264 else result.HostRequest.host_last_seen_message_id, 

265 latest_message=message_to_pb(result.Message), 

266 ) 

267 for result in results[:pagination] 

268 ] 

269 last_request_id = ( 

270 min(g.Message.id for g in results[:pagination]) if len(results) > pagination else 0 

271 ) # TODO 

272 no_more = len(results) <= pagination 

273 

274 return requests_pb2.ListHostRequestsRes( 

275 last_request_id=last_request_id, no_more=no_more, host_requests=host_requests 

276 ) 

277 

278 def RespondHostRequest(self, request, context): 

279 with session_scope() as session: 

280 host_request = session.execute( 

281 select(HostRequest) 

282 .where_users_column_visible(context, HostRequest.surfer_user_id) 

283 .where_users_column_visible(context, HostRequest.host_user_id) 

284 .where(HostRequest.conversation_id == request.host_request_id) 

285 ).scalar_one_or_none() 

286 

287 if not host_request: 

288 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

289 

290 if host_request.surfer_user_id != context.user_id and host_request.host_user_id != context.user_id: 

291 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

292 

293 if request.status == conversations_pb2.HOST_REQUEST_STATUS_PENDING: 

294 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

295 

296 if host_request.end_time < now(): 

297 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_IN_PAST) 

298 

299 control_message = Message() 

300 

301 if request.status == conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED: 

302 # only host can accept 

303 if context.user_id != host_request.host_user_id: 

304 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.NOT_THE_HOST) 

305 # can't accept a cancelled or confirmed request (only reject), or already accepted 

306 if ( 

307 host_request.status == HostRequestStatus.cancelled 

308 or host_request.status == HostRequestStatus.confirmed 

309 or host_request.status == HostRequestStatus.accepted 

310 ): 

311 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

312 control_message.host_request_status_target = HostRequestStatus.accepted 

313 host_request.status = HostRequestStatus.accepted 

314 

315 send_host_request_accepted_email_to_guest(host_request) 

316 

317 notify( 

318 user_id=host_request.surfer_user_id, 

319 topic="host_request", 

320 action="accept", 

321 key=str(host_request.host_user_id), 

322 avatar_key=host_request.host.avatar.thumbnail_url if host_request.host.avatar else None, 

323 title=f"**{host_request.host.name}** accepted your host request", 

324 link=urls.host_request_link_guest(), 

325 ) 

326 

327 if request.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED: 

328 # only host can reject 

329 if context.user_id != host_request.host_user_id: 

330 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

331 # can't reject a cancelled or already rejected request 

332 if ( 

333 host_request.status == HostRequestStatus.cancelled 

334 or host_request.status == HostRequestStatus.rejected 

335 ): 

336 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

337 control_message.host_request_status_target = HostRequestStatus.rejected 

338 host_request.status = HostRequestStatus.rejected 

339 

340 send_host_request_rejected_email_to_guest(host_request) 

341 

342 notify( 

343 user_id=host_request.surfer_user_id, 

344 topic="host_request", 

345 action="reject", 

346 key=str(host_request.host_user_id), 

347 avatar_key=host_request.host.avatar.thumbnail_url if host_request.host.avatar else None, 

348 title=f"**{host_request.host.name}** rejected your host request", 

349 link=urls.host_request_link_guest(), 

350 ) 

351 

352 if request.status == conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED: 

353 # only hostee can confirm 

354 if context.user_id != host_request.surfer_user_id: 

355 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

356 # can only confirm an accepted request 

357 if host_request.status != HostRequestStatus.accepted: 

358 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

359 control_message.host_request_status_target = HostRequestStatus.confirmed 

360 host_request.status = HostRequestStatus.confirmed 

361 

362 send_host_request_confirmed_email_to_host(host_request) 

363 

364 notify( 

365 user_id=host_request.host_user_id, 

366 topic="host_request", 

367 action="confirm", 

368 key=str(host_request.surfer_user_id), 

369 avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, 

370 title=f"**{host_request.surfer.name}** confirmed their host request", 

371 link=urls.host_request_link_host(), 

372 ) 

373 

374 if request.status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED: 

375 # only hostee can cancel 

376 if context.user_id != host_request.surfer_user_id: 

377 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

378 # can't' cancel an already cancelled or rejected request 

379 if ( 

380 host_request.status == HostRequestStatus.rejected 

381 or host_request.status == HostRequestStatus.cancelled 

382 ): 

383 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) 

384 control_message.host_request_status_target = HostRequestStatus.cancelled 

385 host_request.status = HostRequestStatus.cancelled 

386 

387 send_host_request_cancelled_email_to_host(host_request) 

388 

389 notify( 

390 user_id=host_request.host_user_id, 

391 topic="host_request", 

392 action="cancel", 

393 key=str(host_request.surfer_user_id), 

394 avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, 

395 title=f"**{host_request.surfer.name}** cancelled their host request", 

396 link=urls.host_request_link_host(), 

397 ) 

398 

399 control_message.message_type = MessageType.host_request_status_changed 

400 control_message.conversation_id = host_request.conversation_id 

401 control_message.author_id = context.user_id 

402 session.add(control_message) 

403 

404 if request.text: 

405 latest_message = Message() 

406 latest_message.conversation_id = host_request.conversation_id 

407 latest_message.text = request.text 

408 latest_message.author_id = context.user_id 

409 latest_message.message_type = MessageType.text 

410 session.add(latest_message) 

411 else: 

412 latest_message = control_message 

413 

414 session.flush() 

415 

416 if host_request.surfer_user_id == context.user_id: 

417 host_request.surfer_last_seen_message_id = latest_message.id 

418 else: 

419 host_request.host_last_seen_message_id = latest_message.id 

420 session.commit() 

421 

422 return empty_pb2.Empty() 

423 

424 def GetHostRequestMessages(self, request, context): 

425 with session_scope() as session: 

426 host_request = session.execute( 

427 select(HostRequest).where(HostRequest.conversation_id == request.host_request_id) 

428 ).scalar_one_or_none() 

429 

430 if not host_request: 

431 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

432 

433 if host_request.surfer_user_id != context.user_id and host_request.host_user_id != context.user_id: 

434 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

435 

436 pagination = request.number if request.number > 0 else DEFAULT_PAGINATION_LENGTH 

437 pagination = min(pagination, MAX_PAGE_SIZE) 

438 

439 messages = ( 

440 session.execute( 

441 select(Message) 

442 .where(Message.conversation_id == host_request.conversation_id) 

443 .where(or_(Message.id < request.last_message_id, request.last_message_id == 0)) 

444 .order_by(Message.id.desc()) 

445 .limit(pagination + 1) 

446 ) 

447 .scalars() 

448 .all() 

449 ) 

450 

451 no_more = len(messages) <= pagination 

452 

453 last_message_id = min(map(lambda m: m.id if m else 1, messages[:pagination])) if len(messages) > 0 else 0 

454 

455 return requests_pb2.GetHostRequestMessagesRes( 

456 last_message_id=last_message_id, 

457 no_more=no_more, 

458 messages=[message_to_pb(message) for message in messages[:pagination]], 

459 ) 

460 

461 def SendHostRequestMessage(self, request, context): 

462 if request.text == "": 

463 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_MESSAGE) 

464 with session_scope() as session: 

465 host_request = session.execute( 

466 select(HostRequest).where(HostRequest.conversation_id == request.host_request_id) 

467 ).scalar_one_or_none() 

468 

469 if not host_request: 

470 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

471 

472 if host_request.surfer_user_id != context.user_id and host_request.host_user_id != context.user_id: 

473 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

474 

475 if host_request.status == HostRequestStatus.rejected or host_request.status == HostRequestStatus.cancelled: 

476 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.HOST_REQUEST_CLOSED) 

477 

478 if host_request.end_time < now(): 

479 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_IN_PAST) 

480 

481 message = Message() 

482 message.conversation_id = host_request.conversation_id 

483 message.author_id = context.user_id 

484 message.message_type = MessageType.text 

485 message.text = request.text 

486 session.add(message) 

487 session.flush() 

488 

489 if host_request.surfer_user_id == context.user_id: 

490 host_request.surfer_last_seen_message_id = message.id 

491 

492 notify( 

493 user_id=host_request.host_user_id, 

494 topic="host_request", 

495 action="message", 

496 key=str(host_request.surfer_user_id), 

497 avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, 

498 title=f"**{host_request.surfer.name}** sent a message in their host request", 

499 link=urls.host_request_link_host(), 

500 ) 

501 

502 else: 

503 host_request.host_last_seen_message_id = message.id 

504 

505 notify( 

506 user_id=host_request.surfer_user_id, 

507 topic="host_request", 

508 action="message", 

509 key=str(host_request.host_user_id), 

510 avatar_key=host_request.host.avatar.thumbnail_url if host_request.host.avatar else None, 

511 title=f"**{host_request.host.name}** sent a message in your host request", 

512 link=urls.host_request_link_guest(), 

513 ) 

514 

515 session.commit() 

516 

517 return empty_pb2.Empty() 

518 

519 def GetHostRequestUpdates(self, request, context): 

520 if request.only_sent and request.only_received: 

521 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_SENT_OR_RECEIVED) 

522 

523 with session_scope() as session: 

524 if request.newest_message_id == 0: 

525 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_MESSAGE) 

526 

527 if not session.execute(select(Message).where(Message.id == request.newest_message_id)).scalar_one_or_none(): 

528 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_MESSAGE) 

529 

530 pagination = request.number if request.number > 0 else DEFAULT_PAGINATION_LENGTH 

531 pagination = min(pagination, MAX_PAGE_SIZE) 

532 

533 statement = ( 

534 select( 

535 Message, 

536 HostRequest.status.label("host_request_status"), 

537 HostRequest.conversation_id.label("host_request_id"), 

538 ) 

539 .join(HostRequest, HostRequest.conversation_id == Message.conversation_id) 

540 .where(Message.id > request.newest_message_id) 

541 ) 

542 

543 if request.only_sent: 

544 statement = statement.where(HostRequest.surfer_user_id == context.user_id) 

545 elif request.only_received: 

546 statement = statement.where(HostRequest.host_user_id == context.user_id) 

547 else: 

548 statement = statement.where( 

549 or_(HostRequest.host_user_id == context.user_id, HostRequest.surfer_user_id == context.user_id) 

550 ) 

551 

552 statement = statement.order_by(Message.id.asc()).limit(pagination + 1) 

553 res = session.execute(statement).all() 

554 

555 no_more = len(res) <= pagination 

556 

557 last_message_id = ( 

558 min(map(lambda m: m.Message.id if m else 1, res[:pagination])) if len(res) > 0 else 0 

559 ) # TODO 

560 

561 return requests_pb2.GetHostRequestUpdatesRes( 

562 no_more=no_more, 

563 updates=[ 

564 requests_pb2.HostRequestUpdate( 

565 host_request_id=result.host_request_id, 

566 status=hostrequeststatus2api[result.host_request_status], 

567 message=message_to_pb(result.Message), 

568 ) 

569 for result in res[:pagination] 

570 ], 

571 ) 

572 

573 def MarkLastSeenHostRequest(self, request, context): 

574 with session_scope() as session: 

575 host_request = session.execute( 

576 select(HostRequest).where(HostRequest.conversation_id == request.host_request_id) 

577 ).scalar_one_or_none() 

578 

579 if not host_request: 

580 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

581 

582 if host_request.surfer_user_id != context.user_id and host_request.host_user_id != context.user_id: 

583 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

584 

585 if host_request.surfer_user_id == context.user_id: 

586 if not host_request.surfer_last_seen_message_id <= request.last_seen_message_id: 

587 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_UNSEE_MESSAGES) 

588 host_request.surfer_last_seen_message_id = request.last_seen_message_id 

589 else: 

590 if not host_request.host_last_seen_message_id <= request.last_seen_message_id: 

591 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_UNSEE_MESSAGES) 

592 host_request.host_last_seen_message_id = request.last_seen_message_id 

593 

594 session.commit() 

595 return empty_pb2.Empty() 

596 

597 def GetResponseRate(self, request, context): 

598 with session_scope() as session: 

599 # this subquery gets the time that the request was sent 

600 t = ( 

601 select(Message.conversation_id, Message.time) 

602 .where(Message.message_type == MessageType.chat_created) 

603 .subquery() 

604 ) 

605 # this subquery gets the time that the user responded to the request 

606 s = ( 

607 select(Message.conversation_id, func.min(Message.time).label("time")) 

608 .where(Message.author_id == request.user_id) 

609 .group_by(Message.conversation_id) 

610 .subquery() 

611 ) 

612 

613 res = session.execute( 

614 select( 

615 User.id, 

616 # number of requests received 

617 func.count().label("n"), 

618 # percentage of requests responded to 

619 (func.count(s.c.time) / func.cast(func.greatest(func.count(t.c.time), 1.0), Float)).label( 

620 "response_rate" 

621 ), 

622 # the 33rd percentile response time 

623 percentile_disc(0.33) 

624 .within_group(func.coalesce(s.c.time - t.c.time, timedelta(days=1000))) 

625 .label("response_time_p33"), 

626 # the 66th percentile response time 

627 percentile_disc(0.66) 

628 .within_group(func.coalesce(s.c.time - t.c.time, timedelta(days=1000))) 

629 .label("response_time_p66"), 

630 ) 

631 .where_users_visible(context) 

632 .where(User.id == request.user_id) 

633 .outerjoin(HostRequest, HostRequest.host_user_id == User.id) 

634 .outerjoin(t, t.c.conversation_id == HostRequest.conversation_id) 

635 .outerjoin(s, s.c.conversation_id == HostRequest.conversation_id) 

636 .group_by(User.id) 

637 ).one_or_none() 

638 

639 if not res: 

640 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

641 

642 _, n, response_rate, response_time_p33, response_time_p66 = res 

643 

644 if n < 3: 

645 return requests_pb2.GetResponseRateRes( 

646 insufficient_data=requests_pb2.ResponseRateInsufficientData(), 

647 ) 

648 

649 if response_rate <= 0.33: 

650 return requests_pb2.GetResponseRateRes( 

651 low=requests_pb2.ResponseRateLow(), 

652 ) 

653 

654 response_time_p33_coarsened = Duration_from_timedelta( 

655 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60) 

656 ) 

657 

658 if response_rate <= 0.66: 

659 return requests_pb2.GetResponseRateRes( 

660 some=requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened), 

661 ) 

662 

663 response_time_p66_coarsened = Duration_from_timedelta( 

664 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60) 

665 ) 

666 

667 if response_rate <= 0.90: 

668 return requests_pb2.GetResponseRateRes( 

669 most=requests_pb2.ResponseRateMost( 

670 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

671 ), 

672 ) 

673 else: 

674 return requests_pb2.GetResponseRateRes( 

675 almost_all=requests_pb2.ResponseRateAlmostAll( 

676 response_time_p33=response_time_p33_coarsened, response_time_p66=response_time_p66_coarsened 

677 ), 

678 )