Add an API endpoint to send typing notification events.

POST to /typing creates a typing event
Required parameters are 'op' ('start' or 'stop') and 'to' (recipient
emails). If there are multiple recipients, the 'to' parameter
should be a JSON string of the list of recipient emails.
The event created looks like:
{
  'type': 'typing',
  'op': 'start',
  'sender': 'hamlet@zulip.com',
  'recipients': [{
    'id': 1,
    'email': 'othello@zulip.com'
  }]
}
This commit is contained in:
Arpith Siromoney 2016-10-13 00:27:59 +05:30 committed by Tim Abbott
parent bc6421fbfb
commit ee97ba04fe
4 changed files with 290 additions and 0 deletions

View File

@ -818,6 +818,72 @@ def do_send_messages(messages):
# intermingle sending zephyr messages with other messages.
return already_sent_ids + [message['message'].id for message in messages]
def get_typing_notification_recipients(notification):
# type: (Dict[str, Any]) -> Dict[str, Any]
if notification['recipient'].type == Recipient.PERSONAL:
recipients = [notification['sender'],
get_user_profile_by_id(notification['recipient'].type_id)]
elif notification['recipient'].type == Recipient.HUDDLE:
# We use select_related()/only() here, while the PERSONAL case above uses
# get_user_profile_by_id() to get UserProfile objects from cache. Streams will
# typically have more recipients than PMs, so get_user_profile_by_id() would be
# a bit more expensive here, given that we need to hit the DB anyway and only
# care about the email from the user profile.
fields = [
'user_profile__id',
'user_profile__email',
'user_profile__is_active',
'user_profile__realm__domain'
]
query = Subscription.objects.select_related("user_profile", "user_profile__realm").only(*fields).filter(
recipient=notification['recipient'], active=True)
recipients = [s.user_profile for s in query]
elif notification['recipient'].type == Recipient.STREAM:
raise ValueError('Forbidden recipient type')
else:
raise ValueError('Bad recipient type')
notification['recipients'] = [{'id': profile.id, 'email': profile.email} for profile in recipients]
notification['active_recipients'] = [profile for profile in recipients if profile.is_active]
return notification
def do_send_typing_notification(notification):
# type: (Dict[str, Any]) -> None
notification = get_typing_notification_recipients(notification)
event = dict(
type = 'typing',
op = notification['op'],
sender = notification['sender'],
recipients = notification['recipients'])
# Only deliver the message to active user recipients
send_event(event, notification['active_recipients'])
# check_send_typing_notification:
# Checks the typing notification and sends it
def check_send_typing_notification(sender, notification_to, operator):
# type: (UserProfile, Sequence[text_type], text_type) -> None
typing_notification = check_typing_notification(sender, notification_to, operator)
do_send_typing_notification(typing_notification)
# check_typing_notification:
# Returns typing notification ready for sending with do_send_typing_notification on success
# or the error message (string) on error.
def check_typing_notification(sender, notification_to, operator):
# type: (UserProfile, Sequence[text_type], text_type) -> Dict[str, Any]
if len(notification_to) == 0:
raise JsonableError(_('Missing parameter: \'to\' (recipient)'))
elif operator not in ('start', 'stop'):
raise JsonableError(_('Invalid \'op\' value (should be start or stop)'))
else:
try:
recipient = recipient_for_emails(notification_to, False,
sender, sender)
except ValidationError as e:
assert isinstance(e.messages[0], six.string_types)
raise JsonableError(e.messages[0])
typing_notification = {'sender': sender, 'recipient': recipient, 'op': operator}
return typing_notification
def do_create_stream(realm, stream_name):
# type: (Realm, text_type) -> None
# This is used by a management command now, mostly to facilitate testing. It

203
zerver/tests/test_typing.py Normal file
View File

@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import ujson
from typing import Any, Dict, List
from six import string_types
from zerver.models import get_user_profile_by_email
from zerver.lib.test_helpers import ZulipTestCase, tornado_redirected_to_list, get_display_recipient
class TypingNotificationOperatorTest(ZulipTestCase):
def test_missing_parameter(self):
# type: () -> None
"""
Sending typing notification without op parameter fails
"""
sender = 'hamlet@zulip.com'
recipient = 'othello@zulip.com'
result = self.client_post('/api/v1/typing', {'to': recipient},
**self.api_auth(sender))
self.assert_json_error(result, 'Missing \'op\' argument')
def test_invalid_parameter(self):
# type: () -> None
"""
Sending typing notification with invalid value for op parameter fails
"""
sender = 'hamlet@zulip.com'
recipient = 'othello@zulip.com'
result = self.client_post('/api/v1/typing', {'to': recipient, 'op': 'foo'},
**self.api_auth(sender))
self.assert_json_error(result, 'Invalid \'op\' value (should be start or stop)')
class TypingNotificationRecipientsTest(ZulipTestCase):
def test_missing_recipient(self):
# type: () -> None
"""
Sending typing notification without recipient fails
"""
sender = 'hamlet@zulip.com'
result = self.client_post('/api/v1/typing', {'op': 'start'},
**self.api_auth(sender))
self.assert_json_error(result, 'Missing parameter: \'to\' (recipient)')
def test_invalid_recipient(self):
# type: () -> None
"""
Sending typing notification to invalid recipient fails
"""
sender = 'hamlet@zulip.com'
invalid = 'invalid email'
result = self.client_post('/api/v1/typing', {'op': 'start', 'to': invalid},
**self.api_auth(sender))
self.assert_json_error(result, 'Invalid email \'' + invalid + '\'')
def test_single_recipient(self):
# type: () -> None
"""
Sending typing notification to a single recipient is successful
"""
sender = 'hamlet@zulip.com'
recipient = 'othello@zulip.com'
expected_recipients = set([sender, recipient])
events = [] # type: List[Dict[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/typing', {'to': recipient,
'op': 'start'},
**self.api_auth(sender))
self.assert_json_success(result)
self.assertTrue(len(events) == 1)
event = events[0]['event']
event_recipient_emails = set(user['email'] for user in event['recipients'])
self.assertTrue(event['sender'] == get_user_profile_by_email(sender))
self.assertTrue(event_recipient_emails == expected_recipients)
self.assertTrue(event['type'] == 'typing')
self.assertTrue(event['op'] == 'start')
def test_multiple_recipients(self):
# type: () -> None
"""
Sending typing notification to a single recipient is successful
"""
sender = 'hamlet@zulip.com'
recipient = ['othello@zulip.com', 'cordelia@zulip.com']
expected_recipients = set(recipient) | set([sender])
events = [] # type: List[Dict[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/typing', {'to': ujson.dumps(recipient),
'op': 'start'},
**self.api_auth(sender))
self.assert_json_success(result)
self.assertTrue(len(events) == 1)
event = events[0]['event']
event_recipient_emails = set(user['email'] for user in event['recipients'])
self.assertTrue(event['sender'] == get_user_profile_by_email(sender))
self.assertTrue(event_recipient_emails == expected_recipients)
self.assertTrue(event['type'] == 'typing')
self.assertTrue(event['op'] == 'start')
class TypingStartedNotificationTest(ZulipTestCase):
def test_send_notification_to_self_event(self):
# type: () -> None
"""
Sending typing notification to yourself
is successful.
"""
email = 'hamlet@zulip.com'
events = [] # type: List[Dict[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/typing', {'to': email,
'op': 'start'},
**self.api_auth(email))
self.assert_json_success(result)
self.assertTrue(len(events) == 1)
event = events[0]['event']
event_recipient_emails = set(user['email'] for user in event['recipients'])
self.assertTrue(event['sender'] == get_user_profile_by_email(email))
self.assertTrue(event_recipient_emails == set([email]))
self.assertTrue(event['type'] == 'typing')
self.assertTrue(event['op'] == 'start')
def test_send_notification_to_another_user_event(self):
# type: () -> None
"""
Sending typing notification to another user
is successful.
"""
sender = 'hamlet@zulip.com'
recipient = 'othello@zulip.com'
expected_recipients = set([sender, recipient])
events = [] # type: List[Dict[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/typing', {'to': recipient,
'op': 'start'},
**self.api_auth(sender))
self.assert_json_success(result)
self.assertTrue(len(events) == 1)
event = events[0]['event']
event_recipient_emails = set(user['email'] for user in event['recipients'])
self.assertTrue(event['sender'] == get_user_profile_by_email(sender))
self.assertTrue(event_recipient_emails == expected_recipients)
self.assertTrue(event['type'] == 'typing')
self.assertTrue(event['op'] == 'start')
class StoppedTypingNotificationTest(ZulipTestCase):
def test_send_notification_to_self_event(self):
# type: () -> None
"""
Sending stopped typing notification to yourself
is successful.
"""
email = 'hamlet@zulip.com'
events = [] # type: List[Dict[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/typing', {'to': email,
'op': 'stop'},
**self.api_auth(email))
self.assert_json_success(result)
self.assertTrue(len(events) == 1)
event = events[0]['event']
event_recipient_emails = set(user['email'] for user in event['recipients'])
self.assertTrue(event['sender'] == get_user_profile_by_email(email))
self.assertTrue(event_recipient_emails == set([email]))
self.assertTrue(event['type'] == 'typing')
self.assertTrue(event['op'] == 'stop')
def test_send_notification_to_another_user_event(self):
# type: () -> None
"""
Sending stopped typing notification to another user
is successful.
"""
sender = 'hamlet@zulip.com'
recipient = 'othello@zulip.com'
expected_recipients = set([sender, recipient])
events = [] # type: List[Dict[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/typing', {'to': recipient,
'op': 'stop'},
**self.api_auth(sender))
self.assert_json_success(result)
self.assertTrue(len(events) == 1)
event = events[0]['event']
event_recipient_emails = set(user['email'] for user in event['recipients'])
self.assertTrue(event['sender'] == get_user_profile_by_email(sender))
self.assertTrue(event_recipient_emails == expected_recipients)
self.assertTrue(event['type'] == 'typing')
self.assertTrue(event['op'] == 'stop')

16
zerver/views/typing.py Normal file
View File

@ -0,0 +1,16 @@
from django.http import HttpRequest, HttpResponse
from six import text_type
from zerver.decorator import authenticated_json_post_view,\
has_request_variables, REQ, JsonableError
from zerver.lib.actions import check_send_typing_notification, \
extract_recipients
from zerver.lib.response import json_success
from zerver.models import UserProfile
@has_request_variables
def send_notification_backend(request, user_profile, operator=REQ('op'),
notification_to = REQ('to', converter=extract_recipients, default=[])):
# type: (HttpRequest, UserProfile, text_type, List[text_type]) -> HttpResponse
check_send_typing_notification(user_profile, notification_to, operator)
return json_success()

View File

@ -169,6 +169,11 @@ v1_api_and_json_patterns = [
url(r'^messages/flags$', 'zerver.lib.rest.rest_dispatch',
{'POST': 'zerver.views.messages.update_message_flags'}),
# typing -> zerver.views.typing
# POST sends a typing notification event to recipients
url(r'^typing$', 'zerver.lib.rest.rest_dispatch',
{'POST': 'zerver.views.typing.send_notification_backend'}),
# user_uploads -> zerver.views.upload
url(r'^user_uploads$', 'zerver.lib.rest.rest_dispatch',
{'POST': 'zerver.views.upload.upload_file_backend'}),