mirror of https://github.com/zulip/zulip.git
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:
parent
bc6421fbfb
commit
ee97ba04fe
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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()
|
|
@ -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'}),
|
||||
|
|
Loading…
Reference in New Issue