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: {
|
typing__start: {
|
||||||
type: "typing",
|
type: "typing",
|
||||||
op: "start",
|
op: "start",
|
||||||
|
message_type: "private",
|
||||||
sender: typing_person1,
|
sender: typing_person1,
|
||||||
recipients: [typing_person2],
|
recipients: [typing_person2],
|
||||||
},
|
},
|
||||||
|
@ -617,6 +618,7 @@ exports.fixtures = {
|
||||||
typing__stop: {
|
typing__stop: {
|
||||||
type: "typing",
|
type: "typing",
|
||||||
op: "stop",
|
op: "stop",
|
||||||
|
message_type: "private",
|
||||||
sender: typing_person1,
|
sender: typing_person1,
|
||||||
recipients: [typing_person2],
|
recipients: [typing_person2],
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,18 @@ below features are supported.
|
||||||
|
|
||||||
## Changes in Zulip 4.0
|
## 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**
|
**Feature level 57**
|
||||||
|
|
||||||
* [`PATCH /realm/filters/{filter_id}`](/api/update-linkifier): New
|
* [`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}
|
{tab|curl}
|
||||||
|
|
||||||
{generate_code_example(curl)|/typing:post|example}
|
{generate_code_example(curl, exclude=["topic"])|/typing:post|example}
|
||||||
|
|
||||||
{end_tabs}
|
{end_tabs}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
|
||||||
#
|
#
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in templates/zerver/api/changelog.md.
|
# 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
|
# 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
|
# 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_user,
|
||||||
get_stream_subscriptions_for_users,
|
get_stream_subscriptions_for_users,
|
||||||
get_subscribed_stream_ids_for_user,
|
get_subscribed_stream_ids_for_user,
|
||||||
|
get_user_ids_for_streams,
|
||||||
num_subscribers_for_stream_id,
|
num_subscribers_for_stream_id,
|
||||||
subscriber_ids_with_stream_history_access,
|
subscriber_ids_with_stream_history_access,
|
||||||
)
|
)
|
||||||
|
@ -2227,6 +2228,7 @@ def do_send_typing_notification(
|
||||||
]
|
]
|
||||||
event = dict(
|
event = dict(
|
||||||
type="typing",
|
type="typing",
|
||||||
|
message_type="private",
|
||||||
op=operator,
|
op=operator,
|
||||||
sender=sender_dict,
|
sender=sender_dict,
|
||||||
recipients=recipient_dicts,
|
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(
|
def ensure_stream(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
stream_name: str,
|
stream_name: str,
|
||||||
|
|
|
@ -1319,9 +1319,14 @@ typing_start_event = event_dict_type(
|
||||||
required_keys=[
|
required_keys=[
|
||||||
("type", Equals("typing")),
|
("type", Equals("typing")),
|
||||||
("op", Equals("start")),
|
("op", Equals("start")),
|
||||||
|
("message_type", str),
|
||||||
("sender", typing_person_type),
|
("sender", typing_person_type),
|
||||||
|
],
|
||||||
|
optional_keys=[
|
||||||
("recipients", ListType(typing_person_type)),
|
("recipients", ListType(typing_person_type)),
|
||||||
]
|
("stream_id", int),
|
||||||
|
("topic", str),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
check_typing_start = make_checker(typing_start_event)
|
check_typing_start = make_checker(typing_start_event)
|
||||||
|
|
||||||
|
@ -1329,9 +1334,14 @@ typing_stop_event = event_dict_type(
|
||||||
required_keys=[
|
required_keys=[
|
||||||
("type", Equals("typing")),
|
("type", Equals("typing")),
|
||||||
("op", Equals("stop")),
|
("op", Equals("stop")),
|
||||||
|
("message_type", str),
|
||||||
("sender", typing_person_type),
|
("sender", typing_person_type),
|
||||||
|
],
|
||||||
|
optional_keys=[
|
||||||
("recipients", ListType(typing_person_type)),
|
("recipients", ListType(typing_person_type)),
|
||||||
]
|
("stream_id", int),
|
||||||
|
("topic", str),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
check_typing_stop = make_checker(typing_stop_event)
|
check_typing_stop = make_checker(typing_stop_event)
|
||||||
|
|
||||||
|
|
|
@ -1186,6 +1186,7 @@ def set_typing_status(client: Client) -> None:
|
||||||
"to": [user_id1, user_id2],
|
"to": [user_id1, user_id2],
|
||||||
}
|
}
|
||||||
result = client.set_typing_status(request)
|
result = client.set_typing_status(request)
|
||||||
|
|
||||||
# {code_example|end}
|
# {code_example|end}
|
||||||
|
|
||||||
validate_against_openapi_schema(result, "/typing", "post", "200")
|
validate_against_openapi_schema(result, "/typing", "post", "200")
|
||||||
|
@ -1200,6 +1201,41 @@ def set_typing_status(client: Client) -> None:
|
||||||
"to": [user_id1, user_id2],
|
"to": [user_id1, user_id2],
|
||||||
}
|
}
|
||||||
result = client.set_typing_status(request)
|
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}
|
# {code_example|end}
|
||||||
|
|
||||||
validate_against_openapi_schema(result, "/typing", "post", "200")
|
validate_against_openapi_schema(result, "/typing", "post", "200")
|
||||||
|
|
|
@ -1934,8 +1934,16 @@ paths:
|
||||||
- type: object
|
- type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
description: |
|
description: |
|
||||||
Event sent when a user starts typing a private or group private message. Sent
|
Event sent when a user starts typing a message.
|
||||||
to all clients for users who would receive the message being typed.
|
|
||||||
|
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.
|
See the [typing endpoint docs](/api/set-typing-status) for more details.
|
||||||
properties:
|
properties:
|
||||||
|
@ -1950,6 +1958,17 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- start
|
- 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:
|
sender:
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
type: object
|
type: object
|
||||||
|
@ -1967,6 +1986,8 @@ paths:
|
||||||
recipients:
|
recipients:
|
||||||
type: array
|
type: array
|
||||||
description: |
|
description: |
|
||||||
|
Only present if `message_type` is `private`.
|
||||||
|
|
||||||
Array of dictionaries describing the set of users who would be recipients
|
Array of dictionaries describing the set of users who would be recipients
|
||||||
of the message being typed. Each dictionary contains details on one
|
of the message being typed. Each dictionary contains details on one
|
||||||
one of the recipients users; the sending user is guaranteed to appear
|
one of the recipients users; the sending user is guaranteed to appear
|
||||||
|
@ -1985,6 +2006,24 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The Zulip display email address for the user.
|
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:
|
example:
|
||||||
{
|
{
|
||||||
"type": "typing",
|
"type": "typing",
|
||||||
|
@ -2010,9 +2049,16 @@ paths:
|
||||||
- type: object
|
- type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
description: |
|
description: |
|
||||||
Event sent when a user stops typing a private or group private message. Sent
|
Event sent when a user stops typing a message.
|
||||||
to all clients for users who would receive the message that was
|
|
||||||
previously being typed.
|
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.
|
See the [typing endpoint docs](/api/set-typing-status) for more details.
|
||||||
properties:
|
properties:
|
||||||
|
@ -2027,6 +2073,17 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- stop
|
- 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:
|
sender:
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
type: object
|
type: object
|
||||||
|
@ -2045,6 +2102,8 @@ paths:
|
||||||
recipients:
|
recipients:
|
||||||
type: array
|
type: array
|
||||||
description: |
|
description: |
|
||||||
|
Only present for typing notifications for (group) private messages.
|
||||||
|
|
||||||
Array of dictionaries describing the set of users who would be recipients
|
Array of dictionaries describing the set of users who would be recipients
|
||||||
of the message that stopped being typed. Each dictionary contains
|
of the message that stopped being typed. Each dictionary contains
|
||||||
details on one one of the recipients users; the sending user is
|
details on one one of the recipients users; the sending user is
|
||||||
|
@ -2063,6 +2122,24 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The Zulip display email address for the user.
|
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:
|
example:
|
||||||
{
|
{
|
||||||
"type": "typing",
|
"type": "typing",
|
||||||
|
@ -6888,6 +6965,10 @@ paths:
|
||||||
|
|
||||||
* `stream_typing_notifications`: Boolean for whether the client
|
* `stream_typing_notifications`: Boolean for whether the client
|
||||||
supports stream typing notifications.
|
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:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
@ -9743,11 +9824,10 @@ paths:
|
||||||
|
|
||||||
Clients implementing Zulip's typing notifications protocol should work as follows:
|
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
|
* Send a request to this endpoint with `op="start"` when a user starts typing a message,
|
||||||
a private message or group private message, and also every
|
and also every `TYPING_STARTED_WAIT_PERIOD=10` seconds that the user continues to
|
||||||
`TYPING_STARTED_WAIT_PERIOD=10` seconds that the user continues to actively type
|
actively type or otherwise interact with the compose UI (E.g. interacting with the
|
||||||
or otherwise interact with the compose UI (E.g. interacting with the compose box
|
compose box emoji picker).
|
||||||
emoji picker).
|
|
||||||
* Send a request to this endpoint with `op="stop"` when a user pauses using the
|
* 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
|
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
|
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
|
* 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`
|
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.
|
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
|
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
|
to be stateless while being resilient; network failures cannot result in a user being
|
||||||
|
@ -9774,6 +9857,7 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- private
|
- private
|
||||||
|
- stream
|
||||||
default: private
|
default: private
|
||||||
example: private
|
example: private
|
||||||
- name: op
|
- name: op
|
||||||
|
@ -9790,11 +9874,13 @@ paths:
|
||||||
- name: to
|
- name: to
|
||||||
in: query
|
in: query
|
||||||
description: |
|
description: |
|
||||||
The user_ids of the recipients of the message being typed. Typing
|
For 'private' type it is the user_ids of the recipients of the message being typed.
|
||||||
notifications are only supported for private messages. Send a
|
Send a JSON-encoded list of user_ids. (Use a list even if there is only one
|
||||||
JSON-encoded list of user_ids. (Use a list even if there is only one
|
|
||||||
recipient.)
|
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
|
**Changes**: Before Zulip 2.0, this parameter accepted only a JSON-encoded
|
||||||
list of email addresses. Support for the email address-based format was
|
list of email addresses. Support for the email address-based format was
|
||||||
removed in Zulip 3.0 (feature level 11).
|
removed in Zulip 3.0 (feature level 11).
|
||||||
|
@ -9805,7 +9891,14 @@ paths:
|
||||||
items:
|
items:
|
||||||
type: integer
|
type: integer
|
||||||
example: [9, 10]
|
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:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success.
|
description: Success.
|
||||||
|
@ -9813,6 +9906,19 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/JsonSuccess"
|
$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:
|
/user_groups/create:
|
||||||
post:
|
post:
|
||||||
|
|
|
@ -76,6 +76,7 @@ from zerver.lib.actions import (
|
||||||
do_rename_stream,
|
do_rename_stream,
|
||||||
do_revoke_multi_use_invite,
|
do_revoke_multi_use_invite,
|
||||||
do_revoke_user_invite,
|
do_revoke_user_invite,
|
||||||
|
do_send_stream_typing_notification,
|
||||||
do_set_realm_authentication_methods,
|
do_set_realm_authentication_methods,
|
||||||
do_set_realm_message_editing,
|
do_set_realm_message_editing,
|
||||||
do_set_realm_notifications_stream,
|
do_set_realm_notifications_stream,
|
||||||
|
@ -708,6 +709,60 @@ class NormalActionsTest(BaseAction):
|
||||||
)
|
)
|
||||||
check_typing_stop("events[0]", events[0])
|
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:
|
def test_custom_profile_fields_events(self) -> None:
|
||||||
realm = self.user_profile.realm
|
realm = self.user_profile.realm
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ class TypingValidateOperatorTest(ZulipTestCase):
|
||||||
result = self.api_post(sender, "/api/v1/typing", params)
|
result = self.api_post(sender, "/api/v1/typing", params)
|
||||||
self.assert_json_error(result, "Missing 'op' argument")
|
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
|
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)
|
result = self.api_post(sender, "/api/v1/typing", params)
|
||||||
self.assert_json_error(result, "Invalid op")
|
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):
|
class TypingMessagetypeTest(ZulipTestCase):
|
||||||
def test_invalid_type(self) -> None:
|
def test_invalid_type(self) -> None:
|
||||||
|
@ -44,14 +52,24 @@ class TypingMessagetypeTest(ZulipTestCase):
|
||||||
self.assert_json_error(result, "Invalid type")
|
self.assert_json_error(result, "Invalid type")
|
||||||
|
|
||||||
|
|
||||||
class TypingValidateUsersTest(ZulipTestCase):
|
class TypingValidateToArgumentsTest(ZulipTestCase):
|
||||||
def test_empty_array(self) -> None:
|
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")
|
sender = self.example_user("hamlet")
|
||||||
result = self.api_post(sender, "/api/v1/typing", {"op": "start", "to": "[]"})
|
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:
|
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})
|
result = self.api_post(sender, "/api/v1/typing", {"op": "start", "to": invalid})
|
||||||
self.assert_json_error(result, "Invalid user ID 9999999")
|
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:
|
def test_start_to_single_recipient(self) -> None:
|
||||||
sender = self.example_user("hamlet")
|
sender = self.example_user("hamlet")
|
||||||
recipient_user = self.example_user("othello")
|
recipient_user = self.example_user("othello")
|
||||||
|
@ -290,3 +344,77 @@ class TypingHappyPathTest(ZulipTestCase):
|
||||||
self.assertEqual(event["sender"]["email"], sender.email)
|
self.assertEqual(event["sender"]["email"], sender.email)
|
||||||
self.assertEqual(event["type"], "typing")
|
self.assertEqual(event["type"], "typing")
|
||||||
self.assertEqual(event["op"], "stop")
|
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
|
return False
|
||||||
if event["type"] == "message":
|
if event["type"] == "message":
|
||||||
return self.narrow_filter(event)
|
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
|
return True
|
||||||
|
|
||||||
# TODO: Refactor so we don't need this function
|
# 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.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from zerver.decorator import REQ, has_request_variables
|
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.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.lib.validator import check_int, check_list, check_string_in
|
||||||
from zerver.models import UserProfile
|
from zerver.models import UserProfile
|
||||||
|
|
||||||
VALID_OPERATOR_TYPES = ["start", "stop"]
|
VALID_OPERATOR_TYPES = ["start", "stop"]
|
||||||
VALID_MESSAGE_TYPES = ["private"]
|
VALID_MESSAGE_TYPES = ["private", "stream"]
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
|
@ -21,10 +22,29 @@ def send_notification_backend(
|
||||||
"type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private"
|
"type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private"
|
||||||
),
|
),
|
||||||
operator: str = REQ("op", str_validator=check_string_in(VALID_OPERATOR_TYPES)),
|
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:
|
) -> HttpResponse:
|
||||||
if len(user_ids) == 0:
|
to_length = len(notification_to)
|
||||||
return json_error(_("Missing parameter: 'to' (recipient)"))
|
|
||||||
|
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)
|
||||||
|
|
||||||
check_send_typing_notification(user_profile, user_ids, operator)
|
|
||||||
return json_success()
|
return json_success()
|
||||||
|
|
Loading…
Reference in New Issue