mirror of https://github.com/zulip/zulip.git
typing: Support sending stream/topic typing status.
This extends the /json/typing endpoint to also accept stream_id and topic. With this change, the requests sent to /json/typing should have these: * `to`: a list set to - recipients for a PM - stream_id for a stream message * `topic`, in case of stream message along with `op`(start or stop). On receiving a request with stream_id and topic, we send typing events to clients with stream_typing_notifications set to True for all users subscribed to that stream.
This commit is contained in:
parent
734d935d4a
commit
27e4f5da92
|
@ -610,6 +610,7 @@ exports.fixtures = {
|
|||
typing__start: {
|
||||
type: "typing",
|
||||
op: "start",
|
||||
message_type: "private",
|
||||
sender: typing_person1,
|
||||
recipients: [typing_person2],
|
||||
},
|
||||
|
@ -617,6 +618,7 @@ exports.fixtures = {
|
|||
typing__stop: {
|
||||
type: "typing",
|
||||
op: "stop",
|
||||
message_type: "private",
|
||||
sender: typing_person1,
|
||||
recipients: [typing_person2],
|
||||
},
|
||||
|
|
|
@ -10,6 +10,18 @@ below features are supported.
|
|||
|
||||
## Changes in Zulip 4.0
|
||||
|
||||
**Feature level 58**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added the new
|
||||
`stream_typing_notifications` property to supported
|
||||
`client_capabilities`.
|
||||
* [`GET /events`](/api/get-events): Extended format for `typing`
|
||||
events to support typing notifications in stream messages. These new
|
||||
events are only sent to clients with `client_capabilities`
|
||||
showing support for `stream_typing_notifications`.
|
||||
* [`POST /set-typing-status`](/api/set-typing-status): Added support
|
||||
for sending typing notifications for stream messages.
|
||||
|
||||
**Feature level 57**
|
||||
|
||||
* [`PATCH /realm/filters/{filter_id}`](/api/update-linkifier): New
|
||||
|
|
|
@ -17,7 +17,7 @@ More examples and documentation can be found [here](https://github.com/zulip/zul
|
|||
|
||||
{tab|curl}
|
||||
|
||||
{generate_code_example(curl)|/typing:post|example}
|
||||
{generate_code_example(curl, exclude=["topic"])|/typing:post|example}
|
||||
|
||||
{end_tabs}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
|
|||
#
|
||||
# Changes should be accompanied by documentation explaining what the
|
||||
# new level means in templates/zerver/api/changelog.md.
|
||||
API_FEATURE_LEVEL = 57
|
||||
API_FEATURE_LEVEL = 58
|
||||
|
||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||
# only when going from an old version of the code to a newer version. Bump
|
||||
|
|
|
@ -132,6 +132,7 @@ from zerver.lib.stream_subscription import (
|
|||
get_stream_subscriptions_for_user,
|
||||
get_stream_subscriptions_for_users,
|
||||
get_subscribed_stream_ids_for_user,
|
||||
get_user_ids_for_streams,
|
||||
num_subscribers_for_stream_id,
|
||||
subscriber_ids_with_stream_history_access,
|
||||
)
|
||||
|
@ -2227,6 +2228,7 @@ def do_send_typing_notification(
|
|||
]
|
||||
event = dict(
|
||||
type="typing",
|
||||
message_type="private",
|
||||
op=operator,
|
||||
sender=sender_dict,
|
||||
recipients=recipient_dicts,
|
||||
|
@ -2270,6 +2272,26 @@ def check_send_typing_notification(sender: UserProfile, user_ids: List[int], ope
|
|||
)
|
||||
|
||||
|
||||
def do_send_stream_typing_notification(
|
||||
sender: UserProfile, operator: str, stream: Stream, topic: str
|
||||
) -> None:
|
||||
|
||||
sender_dict = {"user_id": sender.id, "email": sender.email}
|
||||
|
||||
event = dict(
|
||||
type="typing",
|
||||
message_type="stream",
|
||||
op=operator,
|
||||
sender=sender_dict,
|
||||
stream_id=stream.id,
|
||||
topic=topic,
|
||||
)
|
||||
|
||||
user_ids_to_notify = get_user_ids_for_streams({stream.id})[stream.id]
|
||||
|
||||
send_event(sender.realm, event, user_ids_to_notify)
|
||||
|
||||
|
||||
def ensure_stream(
|
||||
realm: Realm,
|
||||
stream_name: str,
|
||||
|
|
|
@ -1319,9 +1319,14 @@ typing_start_event = event_dict_type(
|
|||
required_keys=[
|
||||
("type", Equals("typing")),
|
||||
("op", Equals("start")),
|
||||
("message_type", str),
|
||||
("sender", typing_person_type),
|
||||
],
|
||||
optional_keys=[
|
||||
("recipients", ListType(typing_person_type)),
|
||||
]
|
||||
("stream_id", int),
|
||||
("topic", str),
|
||||
],
|
||||
)
|
||||
check_typing_start = make_checker(typing_start_event)
|
||||
|
||||
|
@ -1329,9 +1334,14 @@ typing_stop_event = event_dict_type(
|
|||
required_keys=[
|
||||
("type", Equals("typing")),
|
||||
("op", Equals("stop")),
|
||||
("message_type", str),
|
||||
("sender", typing_person_type),
|
||||
],
|
||||
optional_keys=[
|
||||
("recipients", ListType(typing_person_type)),
|
||||
]
|
||||
("stream_id", int),
|
||||
("topic", str),
|
||||
],
|
||||
)
|
||||
check_typing_stop = make_checker(typing_stop_event)
|
||||
|
||||
|
|
|
@ -1186,6 +1186,7 @@ def set_typing_status(client: Client) -> None:
|
|||
"to": [user_id1, user_id2],
|
||||
}
|
||||
result = client.set_typing_status(request)
|
||||
|
||||
# {code_example|end}
|
||||
|
||||
validate_against_openapi_schema(result, "/typing", "post", "200")
|
||||
|
@ -1200,6 +1201,41 @@ def set_typing_status(client: Client) -> None:
|
|||
"to": [user_id1, user_id2],
|
||||
}
|
||||
result = client.set_typing_status(request)
|
||||
|
||||
# {code_example|end}
|
||||
|
||||
validate_against_openapi_schema(result, "/typing", "post", "200")
|
||||
|
||||
# {code_example|start}
|
||||
# The user has started to type in topic "typing status" of stream "Denmark"
|
||||
stream_id = client.get_stream_id("Denmark")["stream_id"]
|
||||
topic = "typing status"
|
||||
|
||||
request = {
|
||||
"type": "stream",
|
||||
"op": "start",
|
||||
"to": [stream_id],
|
||||
"topic": topic,
|
||||
}
|
||||
result = client.set_typing_status(request)
|
||||
|
||||
# {code_example|end}
|
||||
|
||||
validate_against_openapi_schema(result, "/typing", "post", "200")
|
||||
|
||||
# {code_example|start}
|
||||
# The user has finished typing in topic "typing status" of stream "Denmark"
|
||||
stream_id = client.get_stream_id("Denmark")["stream_id"]
|
||||
topic = "typing status"
|
||||
|
||||
request = {
|
||||
"type": "stream",
|
||||
"op": "stop",
|
||||
"to": [stream_id],
|
||||
"topic": topic,
|
||||
}
|
||||
result = client.set_typing_status(request)
|
||||
|
||||
# {code_example|end}
|
||||
|
||||
validate_against_openapi_schema(result, "/typing", "post", "200")
|
||||
|
|
|
@ -1934,8 +1934,16 @@ paths:
|
|||
- type: object
|
||||
additionalProperties: false
|
||||
description: |
|
||||
Event sent when a user starts typing a private or group private message. Sent
|
||||
to all clients for users who would receive the message being typed.
|
||||
Event sent when a user starts typing a message.
|
||||
|
||||
Sent to all clients for users who would receive the
|
||||
message being typed, with the additional rule that typing
|
||||
notifications for stream messages are only sent to clients
|
||||
that included `stream_typing_notifications` in their
|
||||
`client_capabilities` when registering the event queue.
|
||||
|
||||
**Changes**: Typing notifications for stream messages are new in
|
||||
Zulip 4.0 (feature level 58).
|
||||
|
||||
See the [typing endpoint docs](/api/set-typing-status) for more details.
|
||||
properties:
|
||||
|
@ -1950,6 +1958,17 @@ paths:
|
|||
type: string
|
||||
enum:
|
||||
- start
|
||||
message_type:
|
||||
type: string
|
||||
description: |
|
||||
Type of message being composed. Must be "stream" or "private",
|
||||
as with sending a message.
|
||||
|
||||
**Changes**: New in Zulip 4.0 (feature level 58). Previously,
|
||||
all typing notifications were implicitly private `private`.
|
||||
enum:
|
||||
- private
|
||||
- stream
|
||||
sender:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -1967,6 +1986,8 @@ paths:
|
|||
recipients:
|
||||
type: array
|
||||
description: |
|
||||
Only present if `message_type` is `private`.
|
||||
|
||||
Array of dictionaries describing the set of users who would be recipients
|
||||
of the message being typed. Each dictionary contains details on one
|
||||
one of the recipients users; the sending user is guaranteed to appear
|
||||
|
@ -1985,6 +2006,24 @@ paths:
|
|||
type: string
|
||||
description: |
|
||||
The Zulip display email address for the user.
|
||||
stream_id:
|
||||
type: integer
|
||||
description: |
|
||||
Only present if `message_type` is `stream`.
|
||||
|
||||
The unique ID of the stream to which message is being typed.
|
||||
|
||||
**Changes**: New in Zulip 4.0 (feature level 58). Previously,
|
||||
typing notifications were only for private messages.
|
||||
topic:
|
||||
type: string
|
||||
description: |
|
||||
Only present if `message_type` is `stream`.
|
||||
|
||||
Topic within the stream where the message is being typed.
|
||||
|
||||
**Changes**: New in Zulip 4.0 (feature level 58). Previously,
|
||||
typing notifications were only for private messages.
|
||||
example:
|
||||
{
|
||||
"type": "typing",
|
||||
|
@ -2010,9 +2049,16 @@ paths:
|
|||
- type: object
|
||||
additionalProperties: false
|
||||
description: |
|
||||
Event sent when a user stops typing a private or group private message. Sent
|
||||
to all clients for users who would receive the message that was
|
||||
previously being typed.
|
||||
Event sent when a user stops typing a message.
|
||||
|
||||
Sent to all clients for users who would receive the message
|
||||
that was previously being typed, with the additional rule
|
||||
that typing notifications for stream messages are only sent to
|
||||
clients that included `stream_typing_notifications` in their
|
||||
`client_capabilities` when registering the event queue.
|
||||
|
||||
**Changes**: Typing notifications for stream messages are new in
|
||||
Zulip 4.0 (feature level 58).
|
||||
|
||||
See the [typing endpoint docs](/api/set-typing-status) for more details.
|
||||
properties:
|
||||
|
@ -2027,6 +2073,17 @@ paths:
|
|||
type: string
|
||||
enum:
|
||||
- stop
|
||||
message_type:
|
||||
type: string
|
||||
description: |
|
||||
Type of message being composed. Must be "stream" or "private",
|
||||
as with sending a message.
|
||||
|
||||
**Changes**: New in Zulip 4.0 (feature level 58). Previously,
|
||||
all typing notifications were implicitly private `private`.
|
||||
enum:
|
||||
- private
|
||||
- stream
|
||||
sender:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -2045,6 +2102,8 @@ paths:
|
|||
recipients:
|
||||
type: array
|
||||
description: |
|
||||
Only present for typing notifications for (group) private messages.
|
||||
|
||||
Array of dictionaries describing the set of users who would be recipients
|
||||
of the message that stopped being typed. Each dictionary contains
|
||||
details on one one of the recipients users; the sending user is
|
||||
|
@ -2063,6 +2122,24 @@ paths:
|
|||
type: string
|
||||
description: |
|
||||
The Zulip display email address for the user.
|
||||
stream_id:
|
||||
type: integer
|
||||
description: |
|
||||
Only present if `message_type` is `stream`.
|
||||
|
||||
The unique ID of the stream to which message is being typed.
|
||||
|
||||
**Changes**: New in Zulip 4.0 (feature level 58). Previously,
|
||||
typing notifications were only for private messages.
|
||||
topic:
|
||||
type: string
|
||||
description: |
|
||||
Only present if `message_type` is `stream`.
|
||||
|
||||
Topic within the stream where the message is being typed.
|
||||
|
||||
**Changes**: New in Zulip 4.0 (feature level 58). Previously,
|
||||
typing notifications were only for private messages.
|
||||
example:
|
||||
{
|
||||
"type": "typing",
|
||||
|
@ -6888,6 +6965,10 @@ paths:
|
|||
|
||||
* `stream_typing_notifications`: Boolean for whether the client
|
||||
supports stream typing notifications.
|
||||
|
||||
New in Zulip 4.0 (feature level 58). This capability is
|
||||
for backwards-compatibility; it will be required in a
|
||||
future server release.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
@ -9743,11 +9824,10 @@ paths:
|
|||
|
||||
Clients implementing Zulip's typing notifications protocol should work as follows:
|
||||
|
||||
* Send a request to this endpoint with `op="start"` when a user starts typing
|
||||
a private message or group private message, and also every
|
||||
`TYPING_STARTED_WAIT_PERIOD=10` seconds that the user continues to actively type
|
||||
or otherwise interact with the compose UI (E.g. interacting with the compose box
|
||||
emoji picker).
|
||||
* Send a request to this endpoint with `op="start"` when a user starts typing a message,
|
||||
and also every `TYPING_STARTED_WAIT_PERIOD=10` seconds that the user continues to
|
||||
actively type or otherwise interact with the compose UI (E.g. interacting with the
|
||||
compose box emoji picker).
|
||||
* Send a request to this endpoint with `op="stop"` when a user pauses using the
|
||||
compose UI for at least `TYPING_STOPPED_WAIT_PERIOD=5` seconds or cancels
|
||||
the compose action (if it had previously sent a "start" operation for that
|
||||
|
@ -9757,6 +9837,9 @@ paths:
|
|||
* Continue displaying "Sender is typing" until they receive an `op="stop"` event
|
||||
from the [events API](/api/get-events) or `TYPING_STARTED_EXPIRY_PERIOD=15`
|
||||
seconds have passed without a new `op="start"` event for that conversation.
|
||||
* Clients that support displaying stream typing notifications (new in Zulip 4.0)
|
||||
should indicate they support processing stream typing events via the
|
||||
`stream_typing_notifications` in the `client_capabilities` parameter to `/register`.
|
||||
|
||||
This protocol is designed to allow the server-side typing notifications implementation
|
||||
to be stateless while being resilient; network failures cannot result in a user being
|
||||
|
@ -9774,6 +9857,7 @@ paths:
|
|||
type: string
|
||||
enum:
|
||||
- private
|
||||
- stream
|
||||
default: private
|
||||
example: private
|
||||
- name: op
|
||||
|
@ -9790,11 +9874,13 @@ paths:
|
|||
- name: to
|
||||
in: query
|
||||
description: |
|
||||
The user_ids of the recipients of the message being typed. Typing
|
||||
notifications are only supported for private messages. Send a
|
||||
JSON-encoded list of user_ids. (Use a list even if there is only one
|
||||
For 'private' type it is the user_ids of the recipients of the message being typed.
|
||||
Send a JSON-encoded list of user_ids. (Use a list even if there is only one
|
||||
recipient.)
|
||||
|
||||
For 'stream' type it is a single element list containing ID of stream in
|
||||
which the message being typed.
|
||||
|
||||
**Changes**: Before Zulip 2.0, this parameter accepted only a JSON-encoded
|
||||
list of email addresses. Support for the email address-based format was
|
||||
removed in Zulip 3.0 (feature level 11).
|
||||
|
@ -9805,7 +9891,14 @@ paths:
|
|||
items:
|
||||
type: integer
|
||||
example: [9, 10]
|
||||
required: true
|
||||
- name: topic
|
||||
in: query
|
||||
description: |
|
||||
Topic to which message is being typed. Required for the 'stream' type.
|
||||
Ignored in case of 'private' type.
|
||||
schema:
|
||||
type: string
|
||||
example: typing notifications
|
||||
responses:
|
||||
"200":
|
||||
description: Success.
|
||||
|
@ -9813,6 +9906,19 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/JsonSuccess"
|
||||
"400":
|
||||
description: Bad request.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/JsonError"
|
||||
- example:
|
||||
{
|
||||
"code": "BAD_REQUEST",
|
||||
"msg": "Cannot send to multiple streams",
|
||||
"result": "error",
|
||||
}
|
||||
|
||||
/user_groups/create:
|
||||
post:
|
||||
|
|
|
@ -76,6 +76,7 @@ from zerver.lib.actions import (
|
|||
do_rename_stream,
|
||||
do_revoke_multi_use_invite,
|
||||
do_revoke_user_invite,
|
||||
do_send_stream_typing_notification,
|
||||
do_set_realm_authentication_methods,
|
||||
do_set_realm_message_editing,
|
||||
do_set_realm_notifications_stream,
|
||||
|
@ -708,6 +709,60 @@ class NormalActionsTest(BaseAction):
|
|||
)
|
||||
check_typing_stop("events[0]", events[0])
|
||||
|
||||
def test_stream_typing_events(self) -> None:
|
||||
stream = get_stream("Denmark", self.user_profile.realm)
|
||||
topic = "streams typing"
|
||||
|
||||
events = self.verify_action(
|
||||
lambda: do_send_stream_typing_notification(
|
||||
self.user_profile,
|
||||
"start",
|
||||
stream,
|
||||
topic,
|
||||
),
|
||||
state_change_expected=False,
|
||||
)
|
||||
check_typing_start("events[0]", events[0])
|
||||
|
||||
events = self.verify_action(
|
||||
lambda: do_send_stream_typing_notification(
|
||||
self.user_profile,
|
||||
"stop",
|
||||
stream,
|
||||
topic,
|
||||
),
|
||||
state_change_expected=False,
|
||||
)
|
||||
check_typing_stop("events[0]", events[0])
|
||||
|
||||
# Having client_capability `stream_typing_notification=False`
|
||||
# shouldn't produce any events.
|
||||
events = self.verify_action(
|
||||
lambda: do_send_stream_typing_notification(
|
||||
self.user_profile,
|
||||
"start",
|
||||
stream,
|
||||
topic,
|
||||
),
|
||||
state_change_expected=False,
|
||||
stream_typing_notifications=False,
|
||||
num_events=0,
|
||||
)
|
||||
self.assertEqual(events, [])
|
||||
|
||||
events = self.verify_action(
|
||||
lambda: do_send_stream_typing_notification(
|
||||
self.user_profile,
|
||||
"stop",
|
||||
stream,
|
||||
topic,
|
||||
),
|
||||
state_change_expected=False,
|
||||
stream_typing_notifications=False,
|
||||
num_events=0,
|
||||
)
|
||||
self.assertEqual(events, [])
|
||||
|
||||
def test_custom_profile_fields_events(self) -> None:
|
||||
realm = self.user_profile.realm
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class TypingValidateOperatorTest(ZulipTestCase):
|
|||
result = self.api_post(sender, "/api/v1/typing", params)
|
||||
self.assert_json_error(result, "Missing 'op' argument")
|
||||
|
||||
def test_invalid_parameter(self) -> None:
|
||||
def test_invalid_parameter_pm(self) -> None:
|
||||
"""
|
||||
Sending typing notification with invalid value for op parameter fails
|
||||
"""
|
||||
|
@ -31,6 +31,14 @@ class TypingValidateOperatorTest(ZulipTestCase):
|
|||
result = self.api_post(sender, "/api/v1/typing", params)
|
||||
self.assert_json_error(result, "Invalid op")
|
||||
|
||||
def test_invalid_parameter_stream(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
|
||||
result = self.api_post(
|
||||
sender, "/api/v1/typing", {"op": "foo", "stream_id": 1, "topic": "topic"}
|
||||
)
|
||||
self.assert_json_error(result, "Invalid op")
|
||||
|
||||
|
||||
class TypingMessagetypeTest(ZulipTestCase):
|
||||
def test_invalid_type(self) -> None:
|
||||
|
@ -44,14 +52,24 @@ class TypingMessagetypeTest(ZulipTestCase):
|
|||
self.assert_json_error(result, "Invalid type")
|
||||
|
||||
|
||||
class TypingValidateUsersTest(ZulipTestCase):
|
||||
def test_empty_array(self) -> None:
|
||||
class TypingValidateToArgumentsTest(ZulipTestCase):
|
||||
def test_empty_to_array_pms(self) -> None:
|
||||
"""
|
||||
Sending typing notification without recipient fails
|
||||
Sending pms typing notification without recipient fails
|
||||
"""
|
||||
sender = self.example_user("hamlet")
|
||||
result = self.api_post(sender, "/api/v1/typing", {"op": "start", "to": "[]"})
|
||||
self.assert_json_error(result, "Missing parameter: 'to' (recipient)")
|
||||
self.assert_json_error(result, "Empty 'to' list")
|
||||
|
||||
def test_empty_to_array_stream(self) -> None:
|
||||
"""
|
||||
Sending stream typing notification without recipient fails
|
||||
"""
|
||||
sender = self.example_user("hamlet")
|
||||
result = self.api_post(
|
||||
sender, "/api/v1/typing", {"type": "stream", "op": "start", "to": "[]"}
|
||||
)
|
||||
self.assert_json_error(result, "Empty 'to' list")
|
||||
|
||||
def test_missing_recipient(self) -> None:
|
||||
"""
|
||||
|
@ -79,8 +97,44 @@ class TypingValidateUsersTest(ZulipTestCase):
|
|||
result = self.api_post(sender, "/api/v1/typing", {"op": "start", "to": invalid})
|
||||
self.assert_json_error(result, "Invalid user ID 9999999")
|
||||
|
||||
def test_send_multiple_stream_ids(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
|
||||
class TypingHappyPathTest(ZulipTestCase):
|
||||
result = self.api_post(
|
||||
sender, "/api/v1/typing", {"type": "stream", "op": "stop", "to": "[1, 2, 3]"}
|
||||
)
|
||||
self.assert_json_error(result, "Cannot send to multiple streams")
|
||||
|
||||
def test_includes_stream_id_but_not_topic(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
stream_id = self.get_stream_id("general")
|
||||
|
||||
result = self.api_post(
|
||||
sender,
|
||||
"/api/v1/typing",
|
||||
{"type": "stream", "op": "start", "to": orjson.dumps([stream_id]).decode()},
|
||||
)
|
||||
self.assert_json_error(result, "Missing topic")
|
||||
|
||||
def test_stream_doesnt_exist(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
stream_id = self.INVALID_STREAM_ID
|
||||
topic = "some topic"
|
||||
|
||||
result = self.api_post(
|
||||
sender,
|
||||
"/api/v1/typing",
|
||||
{
|
||||
"type": "stream",
|
||||
"op": "start",
|
||||
"to": orjson.dumps([stream_id]).decode(),
|
||||
"topic": topic,
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "Invalid stream id")
|
||||
|
||||
|
||||
class TypingHappyPathTestPMs(ZulipTestCase):
|
||||
def test_start_to_single_recipient(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
recipient_user = self.example_user("othello")
|
||||
|
@ -290,3 +344,77 @@ class TypingHappyPathTest(ZulipTestCase):
|
|||
self.assertEqual(event["sender"]["email"], sender.email)
|
||||
self.assertEqual(event["type"], "typing")
|
||||
self.assertEqual(event["op"], "stop")
|
||||
|
||||
|
||||
class TypingHappyPathTestStreams(ZulipTestCase):
|
||||
def test_start(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
stream_name = self.get_streams(sender)[0]
|
||||
stream_id = self.get_stream_id(stream_name)
|
||||
topic = "Some topic"
|
||||
|
||||
expected_user_ids = {
|
||||
user_profile.id
|
||||
for user_profile in self.users_subscribed_to_stream(stream_name, sender.realm)
|
||||
}
|
||||
|
||||
params = dict(
|
||||
type="stream",
|
||||
op="start",
|
||||
to=orjson.dumps([stream_id]).decode(),
|
||||
topic=topic,
|
||||
)
|
||||
|
||||
events: List[Mapping[str, Any]] = []
|
||||
with queries_captured() as queries:
|
||||
with tornado_redirected_to_list(events):
|
||||
result = self.api_post(sender, "/api/v1/typing", params)
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(len(queries), 5)
|
||||
|
||||
event = events[0]["event"]
|
||||
event_user_ids = set(events[0]["users"])
|
||||
|
||||
self.assertEqual(expected_user_ids, event_user_ids)
|
||||
self.assertEqual(sender.email, event["sender"]["email"])
|
||||
self.assertEqual(stream_id, event["stream_id"])
|
||||
self.assertEqual(topic, event["topic"])
|
||||
self.assertEqual("typing", event["type"])
|
||||
self.assertEqual("start", event["op"])
|
||||
|
||||
def test_stop(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
stream_name = self.get_streams(sender)[0]
|
||||
stream_id = self.get_stream_id(stream_name)
|
||||
topic = "Some topic"
|
||||
|
||||
expected_user_ids = {
|
||||
user_profile.id
|
||||
for user_profile in self.users_subscribed_to_stream(stream_name, sender.realm)
|
||||
}
|
||||
|
||||
params = dict(
|
||||
type="stream",
|
||||
op="stop",
|
||||
to=orjson.dumps([stream_id]).decode(),
|
||||
topic=topic,
|
||||
)
|
||||
|
||||
events: List[Mapping[str, Any]] = []
|
||||
with queries_captured() as queries:
|
||||
with tornado_redirected_to_list(events):
|
||||
result = self.api_post(sender, "/api/v1/typing", params)
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(len(queries), 5)
|
||||
|
||||
event = events[0]["event"]
|
||||
event_user_ids = set(events[0]["users"])
|
||||
|
||||
self.assertEqual(expected_user_ids, event_user_ids)
|
||||
self.assertEqual(sender.email, event["sender"]["email"])
|
||||
self.assertEqual(stream_id, event["stream_id"])
|
||||
self.assertEqual(topic, event["topic"])
|
||||
self.assertEqual("typing", event["type"])
|
||||
self.assertEqual("stop", event["op"])
|
||||
|
|
|
@ -204,6 +204,11 @@ class ClientDescriptor:
|
|||
return False
|
||||
if event["type"] == "message":
|
||||
return self.narrow_filter(event)
|
||||
if event["type"] == "typing" and "stream_id" in event:
|
||||
# Typing notifications for stream messages are only
|
||||
# delivered if the stream_typing_notifications
|
||||
# client_capability is enabled, for backwards compatibility.
|
||||
return self.stream_typing_notifications
|
||||
return True
|
||||
|
||||
# TODO: Refactor so we don't need this function
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from zerver.decorator import REQ, has_request_variables
|
||||
from zerver.lib.actions import check_send_typing_notification
|
||||
from zerver.lib.actions import check_send_typing_notification, do_send_stream_typing_notification
|
||||
from zerver.lib.response import json_error, json_success
|
||||
from zerver.lib.streams import access_stream_by_id, access_stream_for_send_message
|
||||
from zerver.lib.validator import check_int, check_list, check_string_in
|
||||
from zerver.models import UserProfile
|
||||
|
||||
VALID_OPERATOR_TYPES = ["start", "stop"]
|
||||
VALID_MESSAGE_TYPES = ["private"]
|
||||
VALID_MESSAGE_TYPES = ["private", "stream"]
|
||||
|
||||
|
||||
@has_request_variables
|
||||
|
@ -21,10 +22,29 @@ def send_notification_backend(
|
|||
"type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private"
|
||||
),
|
||||
operator: str = REQ("op", str_validator=check_string_in(VALID_OPERATOR_TYPES)),
|
||||
user_ids: List[int] = REQ("to", json_validator=check_list(check_int)),
|
||||
notification_to: List[int] = REQ("to", json_validator=check_list(check_int)),
|
||||
topic: Optional[str] = REQ("topic", default=None),
|
||||
) -> HttpResponse:
|
||||
if len(user_ids) == 0:
|
||||
return json_error(_("Missing parameter: 'to' (recipient)"))
|
||||
to_length = len(notification_to)
|
||||
|
||||
if to_length == 0:
|
||||
return json_error(_("Empty 'to' list"))
|
||||
|
||||
if message_type == "stream":
|
||||
if to_length > 1:
|
||||
return json_error(_("Cannot send to multiple streams"))
|
||||
|
||||
if topic is None:
|
||||
return json_error(_("Missing topic"))
|
||||
|
||||
stream_id = notification_to[0]
|
||||
# Verify that the user has access to the stream and has
|
||||
# permission to send messages to it.
|
||||
stream = access_stream_by_id(user_profile, stream_id)[0]
|
||||
access_stream_for_send_message(user_profile, stream, forwarder_user_profile=None)
|
||||
do_send_stream_typing_notification(user_profile, operator, stream, topic)
|
||||
else:
|
||||
user_ids = notification_to
|
||||
check_send_typing_notification(user_profile, user_ids, operator)
|
||||
|
||||
return json_success()
|
||||
|
|
Loading…
Reference in New Issue