mirror of
https://github.com/zulip/zulip.git
synced 2025-11-16 03:41:58 +00:00
streams: Add subscriber_count to page load data.
This commit is contained in:
@@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with.
|
|||||||
|
|
||||||
## Changes in Zulip 11.0
|
## Changes in Zulip 11.0
|
||||||
|
|
||||||
|
**Feature level 394**
|
||||||
|
|
||||||
|
* [`POST /register`](/api/register-queue), [`GET
|
||||||
|
/events`](/api/get-events), [`GET /streams`](/api/get-streams),
|
||||||
|
[`GET /streams/{stream_id}`](/api/get-stream-by-id):: Added a new
|
||||||
|
field `subscriber_count` to Stream and Subscription objects with the
|
||||||
|
total number of non-deactivated users who are subscribed to the
|
||||||
|
channel.
|
||||||
|
|
||||||
**Feature level 393**
|
**Feature level 393**
|
||||||
|
|
||||||
* [`PATCH /messages/{message_id}`](/api/delete-message),
|
* [`PATCH /messages/{message_id}`](/api/delete-message),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
|||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
|
|
||||||
API_FEATURE_LEVEL = 393
|
API_FEATURE_LEVEL = 394
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ def send_subscription_add_events(
|
|||||||
rendered_description=stream_dict["rendered_description"],
|
rendered_description=stream_dict["rendered_description"],
|
||||||
stream_id=stream_dict["stream_id"],
|
stream_id=stream_dict["stream_id"],
|
||||||
stream_post_policy=stream_dict["stream_post_policy"],
|
stream_post_policy=stream_dict["stream_post_policy"],
|
||||||
|
subscriber_count=stream_dict["subscriber_count"],
|
||||||
topics_policy=stream_dict["topics_policy"],
|
topics_policy=stream_dict["topics_policy"],
|
||||||
# Computed fields not present in Stream.API_FIELDS
|
# Computed fields not present in Stream.API_FIELDS
|
||||||
is_announcement_only=stream_dict["is_announcement_only"],
|
is_announcement_only=stream_dict["is_announcement_only"],
|
||||||
|
|||||||
@@ -1630,6 +1630,10 @@ def apply_event(
|
|||||||
if sub["stream_id"] == event["stream_id"]:
|
if sub["stream_id"] == event["stream_id"]:
|
||||||
sub[event["property"]] = event["value"]
|
sub[event["property"]] = event["value"]
|
||||||
elif event["op"] == "peer_add":
|
elif event["op"] == "peer_add":
|
||||||
|
# Note: We don't update subscriber_count here, since we
|
||||||
|
# have no way to know whether the added subscriber is
|
||||||
|
# already in our count or not. The opposite decision would
|
||||||
|
# be defensible, but this is less code.
|
||||||
if include_subscribers:
|
if include_subscribers:
|
||||||
stream_ids = set(event["stream_ids"])
|
stream_ids = set(event["stream_ids"])
|
||||||
user_ids = set(event["user_ids"])
|
user_ids = set(event["user_ids"])
|
||||||
@@ -1644,6 +1648,7 @@ def apply_event(
|
|||||||
subscribers = set(sub["subscribers"]) | user_ids
|
subscribers = set(sub["subscribers"]) | user_ids
|
||||||
sub["subscribers"] = sorted(subscribers)
|
sub["subscribers"] = sorted(subscribers)
|
||||||
elif event["op"] == "peer_remove":
|
elif event["op"] == "peer_remove":
|
||||||
|
# Note: We don't update subscriber_count here, as with peer_add.
|
||||||
if include_subscribers:
|
if include_subscribers:
|
||||||
stream_ids = set(event["stream_ids"])
|
stream_ids = set(event["stream_ids"])
|
||||||
user_ids = set(event["user_ids"])
|
user_ids = set(event["user_ids"])
|
||||||
|
|||||||
@@ -1538,6 +1538,7 @@ def stream_to_dict(
|
|||||||
stream_post_policy=stream_post_policy,
|
stream_post_policy=stream_post_policy,
|
||||||
is_announcement_only=stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS,
|
is_announcement_only=stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS,
|
||||||
stream_weekly_traffic=stream_weekly_traffic,
|
stream_weekly_traffic=stream_weekly_traffic,
|
||||||
|
subscriber_count=stream.subscriber_count,
|
||||||
topics_policy=StreamTopicsPolicyEnum(stream.topics_policy).name,
|
topics_policy=StreamTopicsPolicyEnum(stream.topics_policy).name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ def get_web_public_subs(
|
|||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
stream_post_policy=stream_post_policy,
|
stream_post_policy=stream_post_policy,
|
||||||
stream_weekly_traffic=stream_weekly_traffic,
|
stream_weekly_traffic=stream_weekly_traffic,
|
||||||
|
subscriber_count=stream.subscriber_count,
|
||||||
topics_policy=StreamTopicsPolicyEnum(topics_policy).name,
|
topics_policy=StreamTopicsPolicyEnum(topics_policy).name,
|
||||||
wildcard_mentions_notify=wildcard_mentions_notify,
|
wildcard_mentions_notify=wildcard_mentions_notify,
|
||||||
)
|
)
|
||||||
@@ -219,6 +220,7 @@ def build_stream_api_dict(
|
|||||||
stream_id=raw_stream_dict["id"],
|
stream_id=raw_stream_dict["id"],
|
||||||
stream_post_policy=raw_stream_dict["stream_post_policy"],
|
stream_post_policy=raw_stream_dict["stream_post_policy"],
|
||||||
stream_weekly_traffic=stream_weekly_traffic,
|
stream_weekly_traffic=stream_weekly_traffic,
|
||||||
|
subscriber_count=raw_stream_dict["subscriber_count"],
|
||||||
topics_policy=raw_stream_dict["topics_policy"],
|
topics_policy=raw_stream_dict["topics_policy"],
|
||||||
is_announcement_only=is_announcement_only,
|
is_announcement_only=is_announcement_only,
|
||||||
is_recently_active=raw_stream_dict["is_recently_active"],
|
is_recently_active=raw_stream_dict["is_recently_active"],
|
||||||
@@ -251,6 +253,7 @@ def build_stream_dict_for_sub(
|
|||||||
stream_id = stream_dict["stream_id"]
|
stream_id = stream_dict["stream_id"]
|
||||||
stream_post_policy = stream_dict["stream_post_policy"]
|
stream_post_policy = stream_dict["stream_post_policy"]
|
||||||
stream_weekly_traffic = stream_dict["stream_weekly_traffic"]
|
stream_weekly_traffic = stream_dict["stream_weekly_traffic"]
|
||||||
|
subscriber_count = stream_dict["subscriber_count"]
|
||||||
topics_policy = stream_dict["topics_policy"]
|
topics_policy = stream_dict["topics_policy"]
|
||||||
is_announcement_only = stream_dict["is_announcement_only"]
|
is_announcement_only = stream_dict["is_announcement_only"]
|
||||||
is_recently_active = stream_dict["is_recently_active"]
|
is_recently_active = stream_dict["is_recently_active"]
|
||||||
@@ -301,6 +304,7 @@ def build_stream_dict_for_sub(
|
|||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
stream_post_policy=stream_post_policy,
|
stream_post_policy=stream_post_policy,
|
||||||
stream_weekly_traffic=stream_weekly_traffic,
|
stream_weekly_traffic=stream_weekly_traffic,
|
||||||
|
subscriber_count=subscriber_count,
|
||||||
topics_policy=topics_policy,
|
topics_policy=topics_policy,
|
||||||
wildcard_mentions_notify=wildcard_mentions_notify,
|
wildcard_mentions_notify=wildcard_mentions_notify,
|
||||||
)
|
)
|
||||||
@@ -326,6 +330,7 @@ def build_stream_dict_for_never_sub(
|
|||||||
rendered_description = raw_stream_dict["rendered_description"]
|
rendered_description = raw_stream_dict["rendered_description"]
|
||||||
stream_id = raw_stream_dict["id"]
|
stream_id = raw_stream_dict["id"]
|
||||||
stream_post_policy = raw_stream_dict["stream_post_policy"]
|
stream_post_policy = raw_stream_dict["stream_post_policy"]
|
||||||
|
subscriber_count = raw_stream_dict["subscriber_count"]
|
||||||
topics_policy = raw_stream_dict["topics_policy"]
|
topics_policy = raw_stream_dict["topics_policy"]
|
||||||
|
|
||||||
if recent_traffic is not None:
|
if recent_traffic is not None:
|
||||||
@@ -378,6 +383,7 @@ def build_stream_dict_for_never_sub(
|
|||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
stream_post_policy=stream_post_policy,
|
stream_post_policy=stream_post_policy,
|
||||||
stream_weekly_traffic=stream_weekly_traffic,
|
stream_weekly_traffic=stream_weekly_traffic,
|
||||||
|
subscriber_count=subscriber_count,
|
||||||
topics_policy=topics_policy,
|
topics_policy=topics_policy,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ class RawStreamDict(TypedDict):
|
|||||||
name: str
|
name: str
|
||||||
rendered_description: str
|
rendered_description: str
|
||||||
stream_post_policy: int
|
stream_post_policy: int
|
||||||
|
subscriber_count: int
|
||||||
topics_policy: str
|
topics_policy: str
|
||||||
|
|
||||||
|
|
||||||
@@ -235,6 +236,7 @@ class SubscriptionStreamDict(TypedDict):
|
|||||||
stream_id: int
|
stream_id: int
|
||||||
stream_post_policy: int
|
stream_post_policy: int
|
||||||
stream_weekly_traffic: int | None
|
stream_weekly_traffic: int | None
|
||||||
|
subscriber_count: int
|
||||||
subscribers: NotRequired[list[int]]
|
subscribers: NotRequired[list[int]]
|
||||||
partial_subscribers: NotRequired[list[int]]
|
partial_subscribers: NotRequired[list[int]]
|
||||||
topics_policy: str
|
topics_policy: str
|
||||||
@@ -264,6 +266,7 @@ class NeverSubscribedStreamDict(TypedDict):
|
|||||||
stream_id: int
|
stream_id: int
|
||||||
stream_post_policy: int
|
stream_post_policy: int
|
||||||
stream_weekly_traffic: int | None
|
stream_weekly_traffic: int | None
|
||||||
|
subscriber_count: int
|
||||||
subscribers: NotRequired[list[int]]
|
subscribers: NotRequired[list[int]]
|
||||||
partial_subscribers: NotRequired[list[int]]
|
partial_subscribers: NotRequired[list[int]]
|
||||||
topics_policy: str
|
topics_policy: str
|
||||||
@@ -295,6 +298,7 @@ class DefaultStreamDict(TypedDict):
|
|||||||
rendered_description: str
|
rendered_description: str
|
||||||
stream_id: int # `stream_id` represents `id` of the `Stream` object in `API_FIELDS`
|
stream_id: int # `stream_id` represents `id` of the `Stream` object in `API_FIELDS`
|
||||||
stream_post_policy: int
|
stream_post_policy: int
|
||||||
|
subscriber_count: int
|
||||||
topics_policy: str
|
topics_policy: str
|
||||||
# Computed fields not specified in `Stream.API_FIELDS`
|
# Computed fields not specified in `Stream.API_FIELDS`
|
||||||
is_announcement_only: bool
|
is_announcement_only: bool
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ class Stream(models.Model):
|
|||||||
"message_retention_days",
|
"message_retention_days",
|
||||||
"name",
|
"name",
|
||||||
"rendered_description",
|
"rendered_description",
|
||||||
|
"subscriber_count",
|
||||||
"can_add_subscribers_group_id",
|
"can_add_subscribers_group_id",
|
||||||
"can_administer_channel_group_id",
|
"can_administer_channel_group_id",
|
||||||
"can_send_message_group_id",
|
"can_send_message_group_id",
|
||||||
|
|||||||
@@ -1367,6 +1367,7 @@ paths:
|
|||||||
"can_remove_subscribers_group": 2,
|
"can_remove_subscribers_group": 2,
|
||||||
"can_subscribe_group": 2,
|
"can_subscribe_group": 2,
|
||||||
"stream_weekly_traffic": null,
|
"stream_weekly_traffic": null,
|
||||||
|
"subscriber_count": 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"id": 0,
|
"id": 0,
|
||||||
@@ -16231,6 +16232,7 @@ paths:
|
|||||||
can_administer_channel_group: {}
|
can_administer_channel_group: {}
|
||||||
can_send_message_group: {}
|
can_send_message_group: {}
|
||||||
can_subscribe_group: {}
|
can_subscribe_group: {}
|
||||||
|
subscriber_count: {}
|
||||||
stream_weekly_traffic:
|
stream_weekly_traffic:
|
||||||
type: integer
|
type: integer
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -21470,6 +21472,7 @@ paths:
|
|||||||
can_administer_channel_group: {}
|
can_administer_channel_group: {}
|
||||||
can_send_message_group: {}
|
can_send_message_group: {}
|
||||||
can_subscribe_group: {}
|
can_subscribe_group: {}
|
||||||
|
subscriber_count: {}
|
||||||
stream_weekly_traffic:
|
stream_weekly_traffic:
|
||||||
type: integer
|
type: integer
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -21515,6 +21518,7 @@ paths:
|
|||||||
- can_subscribe_group
|
- can_subscribe_group
|
||||||
- stream_weekly_traffic
|
- stream_weekly_traffic
|
||||||
- is_recently_active
|
- is_recently_active
|
||||||
|
- subscriber_count
|
||||||
example:
|
example:
|
||||||
{
|
{
|
||||||
"msg": "",
|
"msg": "",
|
||||||
@@ -21543,6 +21547,7 @@ paths:
|
|||||||
"stream_id": 2,
|
"stream_id": 2,
|
||||||
"stream_post_policy": 1,
|
"stream_post_policy": 1,
|
||||||
"stream_weekly_traffic": null,
|
"stream_weekly_traffic": null,
|
||||||
|
subscriber_count: 20,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"can_add_subscribers_group": 9,
|
"can_add_subscribers_group": 9,
|
||||||
@@ -21566,6 +21571,7 @@ paths:
|
|||||||
"stream_id": 1,
|
"stream_id": 1,
|
||||||
"stream_post_policy": 1,
|
"stream_post_policy": 1,
|
||||||
"stream_weekly_traffic": null,
|
"stream_weekly_traffic": null,
|
||||||
|
subscriber_count: 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -21621,6 +21627,7 @@ paths:
|
|||||||
"can_remove_subscribers_group": 2,
|
"can_remove_subscribers_group": 2,
|
||||||
"can_subscribe_group": 2,
|
"can_subscribe_group": 2,
|
||||||
"stream_weekly_traffic": null,
|
"stream_weekly_traffic": null,
|
||||||
|
subscriber_count: 12,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
"400":
|
"400":
|
||||||
@@ -23939,6 +23946,7 @@ components:
|
|||||||
can_administer_channel_group: {}
|
can_administer_channel_group: {}
|
||||||
can_send_message_group: {}
|
can_send_message_group: {}
|
||||||
can_subscribe_group: {}
|
can_subscribe_group: {}
|
||||||
|
subscriber_count: {}
|
||||||
stream_weekly_traffic:
|
stream_weekly_traffic:
|
||||||
type: integer
|
type: integer
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -23965,6 +23973,7 @@ components:
|
|||||||
- rendered_description
|
- rendered_description
|
||||||
- is_web_public
|
- is_web_public
|
||||||
- stream_post_policy
|
- stream_post_policy
|
||||||
|
- subscriber_count
|
||||||
- message_retention_days
|
- message_retention_days
|
||||||
- history_public_to_subscribers
|
- history_public_to_subscribers
|
||||||
- first_message_id
|
- first_message_id
|
||||||
@@ -24134,6 +24143,27 @@ components:
|
|||||||
$ref: "#/components/schemas/CanSendMessageGroup"
|
$ref: "#/components/schemas/CanSendMessageGroup"
|
||||||
can_subscribe_group:
|
can_subscribe_group:
|
||||||
$ref: "#/components/schemas/CanSubscribeGroup"
|
$ref: "#/components/schemas/CanSubscribeGroup"
|
||||||
|
subscriber_count:
|
||||||
|
type: number
|
||||||
|
description: |
|
||||||
|
The total number of non-deactivated users (including bots) who
|
||||||
|
are subscribed to the channel. Clients are responsible for updating
|
||||||
|
this value using `peer_add` and `peer_remove` events.
|
||||||
|
|
||||||
|
The server's internals cannot guarantee this value is correctly
|
||||||
|
synced with `peer_add` and `peer_remove` events for the channel. As
|
||||||
|
a result, if a (rare) race occurs between a change in the channel's
|
||||||
|
subscribers and fetching this value, it is possible for a client
|
||||||
|
that is correctly following the events protocol to end up with a
|
||||||
|
permanently off-by-one error in the channel's subscriber count.
|
||||||
|
|
||||||
|
Clients are recommended to fetch full subscriber data for a channel
|
||||||
|
in contexts where it is important to avoid this risk. The official
|
||||||
|
web application, for example, uses this field primarily while
|
||||||
|
waiting to fetch a given channel's full subscriber list from the
|
||||||
|
server.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 11.0 (feature level 394).
|
||||||
BasicBot:
|
BasicBot:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/components/schemas/BasicBotBase"
|
- $ref: "#/components/schemas/BasicBotBase"
|
||||||
@@ -25214,6 +25244,27 @@ components:
|
|||||||
channels. Note that some endpoints will never return archived
|
channels. Note that some endpoints will never return archived
|
||||||
channels unless the client declares explicit support for
|
channels unless the client declares explicit support for
|
||||||
them via the `archived_channels` client capability.
|
them via the `archived_channels` client capability.
|
||||||
|
subscriber_count:
|
||||||
|
type: number
|
||||||
|
description: |
|
||||||
|
The total number of non-deactivated users (including bots) who
|
||||||
|
are subscribed to the channel. Clients are responsible for updating
|
||||||
|
this value using `peer_add` and `peer_remove` events.
|
||||||
|
|
||||||
|
The server's internals cannot guarantee this value is correctly
|
||||||
|
synced with `peer_add` and `peer_remove` events for the channel. As
|
||||||
|
a result, if a (rare) race occurs between a change in the channel's
|
||||||
|
subscribers and fetching this value, it is possible for a client
|
||||||
|
that is correctly following the events protocol to end up with a
|
||||||
|
permanently off-by-one error in the channel's subscriber count.
|
||||||
|
|
||||||
|
Clients are recommended to fetch full subscriber data for a channel
|
||||||
|
in contexts where it is important to avoid this risk. The official
|
||||||
|
web application, for example, uses this field primarily while
|
||||||
|
waiting to fetch a given channel's full subscriber list from the
|
||||||
|
server.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 11.0 (feature level 394).
|
||||||
DefaultChannelGroup:
|
DefaultChannelGroup:
|
||||||
type: object
|
type: object
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -496,13 +496,25 @@ class BaseAction(ZulipTestCase):
|
|||||||
for u in state["never_subscribed"]:
|
for u in state["never_subscribed"]:
|
||||||
if "subscribers" in u:
|
if "subscribers" in u:
|
||||||
u["subscribers"].sort()
|
u["subscribers"].sort()
|
||||||
|
# this isn't guaranteed to match
|
||||||
|
del u["subscriber_count"]
|
||||||
if "subscriptions" in state:
|
if "subscriptions" in state:
|
||||||
for u in state["subscriptions"]:
|
for u in state["subscriptions"]:
|
||||||
if "subscribers" in u:
|
if "subscribers" in u:
|
||||||
u["subscribers"].sort()
|
u["subscribers"].sort()
|
||||||
|
# this isn't guaranteed to match
|
||||||
|
del u["subscriber_count"]
|
||||||
state["subscriptions"] = {u["name"]: u for u in state["subscriptions"]}
|
state["subscriptions"] = {u["name"]: u for u in state["subscriptions"]}
|
||||||
if "unsubscribed" in state:
|
if "unsubscribed" in state:
|
||||||
|
for u in state["unsubscribed"]:
|
||||||
|
# this isn't guaranteed to match
|
||||||
|
del u["subscriber_count"]
|
||||||
state["unsubscribed"] = {u["name"]: u for u in state["unsubscribed"]}
|
state["unsubscribed"] = {u["name"]: u for u in state["unsubscribed"]}
|
||||||
|
if "streams" in state:
|
||||||
|
for stream in state["streams"]:
|
||||||
|
if "subscriber_count" in stream:
|
||||||
|
# this isn't guaranteed to match
|
||||||
|
del stream["subscriber_count"]
|
||||||
if "realm_bots" in state:
|
if "realm_bots" in state:
|
||||||
state["realm_bots"] = {u["email"]: u for u in state["realm_bots"]}
|
state["realm_bots"] = {u["email"]: u for u in state["realm_bots"]}
|
||||||
# Since time is different for every call, just fix the value
|
# Since time is different for every call, just fix the value
|
||||||
|
|||||||
Reference in New Issue
Block a user