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
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
1import logging
2from datetime import timedelta
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
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
33logger = logging.getLogger(__name__)
35DEFAULT_PAGINATION_LENGTH = 10
36MAX_PAGE_SIZE = 50
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}
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 )
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)
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)
88 from_date = parse_date(request.from_date)
89 to_date = parse_date(request.to_date)
91 if not from_date or not to_date:
92 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_DATE)
94 today = today_in_timezone(host.timezone)
96 # request starts from the past
97 if from_date < today:
98 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_BEFORE_TODAY)
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)
104 # No need to check today > to_date
106 if from_date - today > timedelta(days=365):
107 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_AFTER_ONE_YEAR)
109 if to_date - from_date > timedelta(days=365):
110 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_TO_AFTER_ONE_YEAR)
112 conversation = Conversation()
113 session.add(conversation)
114 session.flush()
116 session.add(
117 Message(
118 conversation_id=conversation.id,
119 author_id=context.user_id,
120 message_type=MessageType.chat_created,
121 )
122 )
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()
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()
147 send_new_host_request_email(host_request)
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 )
160 return requests_pb2.CreateHostRequestRes(host_request_id=host_request.conversation_id)
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()
172 if not host_request:
173 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
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()
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()
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 )
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)
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)
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 )
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 )
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())
250 statement = statement.order_by(Message.id.desc()).limit(pagination + 1)
251 results = session.execute(statement).all()
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
274 return requests_pb2.ListHostRequestsRes(
275 last_request_id=last_request_id, no_more=no_more, host_requests=host_requests
276 )
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()
287 if not host_request:
288 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
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)
293 if request.status == conversations_pb2.HOST_REQUEST_STATUS_PENDING:
294 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS)
296 if host_request.end_time < now():
297 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_IN_PAST)
299 control_message = Message()
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
315 send_host_request_accepted_email_to_guest(host_request)
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 )
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
340 send_host_request_rejected_email_to_guest(host_request)
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 )
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
362 send_host_request_confirmed_email_to_host(host_request)
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 )
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
387 send_host_request_cancelled_email_to_host(host_request)
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 )
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)
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
414 session.flush()
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()
422 return empty_pb2.Empty()
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()
430 if not host_request:
431 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
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)
436 pagination = request.number if request.number > 0 else DEFAULT_PAGINATION_LENGTH
437 pagination = min(pagination, MAX_PAGE_SIZE)
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 )
451 no_more = len(messages) <= pagination
453 last_message_id = min(map(lambda m: m.id if m else 1, messages[:pagination])) if len(messages) > 0 else 0
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 )
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()
469 if not host_request:
470 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
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)
475 if host_request.status == HostRequestStatus.rejected or host_request.status == HostRequestStatus.cancelled:
476 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.HOST_REQUEST_CLOSED)
478 if host_request.end_time < now():
479 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_IN_PAST)
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()
489 if host_request.surfer_user_id == context.user_id:
490 host_request.surfer_last_seen_message_id = message.id
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 )
502 else:
503 host_request.host_last_seen_message_id = message.id
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 )
515 session.commit()
517 return empty_pb2.Empty()
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)
523 with session_scope() as session:
524 if request.newest_message_id == 0:
525 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_MESSAGE)
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)
530 pagination = request.number if request.number > 0 else DEFAULT_PAGINATION_LENGTH
531 pagination = min(pagination, MAX_PAGE_SIZE)
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 )
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 )
552 statement = statement.order_by(Message.id.asc()).limit(pagination + 1)
553 res = session.execute(statement).all()
555 no_more = len(res) <= pagination
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
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 )
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()
579 if not host_request:
580 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
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)
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
594 session.commit()
595 return empty_pb2.Empty()
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 )
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()
639 if not res:
640 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
642 _, n, response_rate, response_time_p33, response_time_p66 = res
644 if n < 3:
645 return requests_pb2.GetResponseRateRes(
646 insufficient_data=requests_pb2.ResponseRateInsufficientData(),
647 )
649 if response_rate <= 0.33:
650 return requests_pb2.GetResponseRateRes(
651 low=requests_pb2.ResponseRateLow(),
652 )
654 response_time_p33_coarsened = Duration_from_timedelta(
655 timedelta(seconds=round(response_time_p33.total_seconds() / 60) * 60)
656 )
658 if response_rate <= 0.66:
659 return requests_pb2.GetResponseRateRes(
660 some=requests_pb2.ResponseRateSome(response_time_p33=response_time_p33_coarsened),
661 )
663 response_time_p66_coarsened = Duration_from_timedelta(
664 timedelta(seconds=round(response_time_p66.total_seconds() / 60) * 60)
665 )
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 )