diff --git a/api_docs/changelog.md b/api_docs/changelog.md index c85add7db9..83e550074c 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,24 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 315** + +* [POST /register](/api/register-queue), [`GET + /streams/{stream_id}`](/api/get-stream-by-id), [`GET + /events`](/api/get-events), [GET + /users/me/subscriptions](/api/get-subscriptions): The `is_archived` + property has been added to channel and subscription objects. + +* [`GET /streams`](/api/get-streams): The new parameter + `exclude_archived` controls whether archived channels should be + returned. + +* [`POST /register`](/api/register-queue): The new `archived_channels` + [client + capability](/api/register-queue#parameter-client_capabilities) + allows the client to specify whether it supports archived channels + being present in the response. + **Feature level 314** * `PATCH /realm`, [`POST /register`](/api/register-queue), diff --git a/version.py b/version.py index ffc95a4a31..c4dd04a563 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 314 # Last bumped for create_multiuse_invite_group api changes. +API_FEATURE_LEVEL = 315 # Last bumped for `is_archived` # 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 diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py index 28abb82712..43a23a5754 100644 --- a/zerver/actions/streams.py +++ b/zerver/actions/streams.py @@ -435,6 +435,7 @@ def send_subscription_add_events( stream_weekly_traffic=stream_dict["stream_weekly_traffic"], subscribers=stream_subscribers, # Fields from Stream.API_FIELDS + is_archived=stream_dict["is_archived"], can_remove_subscribers_group=stream_dict["can_remove_subscribers_group"], creator_id=stream_dict["creator_id"], date_created=stream_dict["date_created"], diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 18d413c9e8..694355386c 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -50,6 +50,7 @@ from zerver.models import Realm, RealmUserDefault, Stream, UserProfile # These fields are used for "stream" events, and are included in the # larger "subscription" events that also contain personal settings. default_stream_fields = [ + ("is_archived", bool), ("can_remove_subscribers_group", int), ("creator_id", OptionalType(int)), ("date_created", int), diff --git a/zerver/lib/events.py b/zerver/lib/events.py index cfe243821a..8d7b23d4e7 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -144,6 +144,7 @@ def fetch_initial_state_data( linkifier_url_template: bool = False, user_list_incomplete: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> dict[str, Any]: """When `event_types` is None, fetches the core data powering the web app's `page_params` and `/api/v1/register` (for mobile/terminal @@ -654,6 +655,7 @@ def fetch_initial_state_data( sub_info = gather_subscriptions_helper( user_profile, include_subscribers=include_subscribers, + include_archived_channels=archived_channels, ) else: sub_info = get_web_public_subs(realm) @@ -787,6 +789,7 @@ def apply_events( linkifier_url_template: bool, user_list_incomplete: bool, include_deactivated_groups: bool, + archived_channels: bool = False, ) -> None: for event in events: if fetch_event_types is not None and event["type"] not in fetch_event_types: @@ -809,6 +812,7 @@ def apply_events( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) @@ -823,6 +827,7 @@ def apply_event( linkifier_url_template: bool, user_list_incomplete: bool, include_deactivated_groups: bool, + archived_channels: bool = False, ) -> None: if event["type"] == "message": state["max_message_id"] = max(state["max_message_id"], event["message"]["id"]) @@ -1210,17 +1215,30 @@ def apply_event( s for s in state["streams"] if s["stream_id"] not in deleted_stream_ids ] - state["subscriptions"] = [ - stream - for stream in state["subscriptions"] - if stream["stream_id"] not in deleted_stream_ids - ] + if archived_channels: + for stream in state["subscriptions"]: + if stream["stream_id"] in deleted_stream_ids: + stream["is_archived"] = True - state["unsubscribed"] = [ - stream - for stream in state["unsubscribed"] - if stream["stream_id"] not in deleted_stream_ids - ] + for stream in state["unsubscribed"]: + if stream["stream_id"] in deleted_stream_ids: + stream["is_archived"] = True + stream["first_message_id"] = Stream.objects.get( + id=stream["stream_id"] + ).first_message_id + + else: + state["subscriptions"] = [ + stream + for stream in state["subscriptions"] + if stream["stream_id"] not in deleted_stream_ids + ] + + state["unsubscribed"] = [ + stream + for stream in state["unsubscribed"] + if stream["stream_id"] not in deleted_stream_ids + ] state["never_subscribed"] = [ stream @@ -1720,6 +1738,7 @@ class ClientCapabilities(TypedDict): linkifier_url_template: NotRequired[bool] user_list_incomplete: NotRequired[bool] include_deactivated_groups: NotRequired[bool] + archived_channels: NotRequired[bool] def do_events_register( @@ -1757,6 +1776,7 @@ def do_events_register( linkifier_url_template = client_capabilities.get("linkifier_url_template", False) user_list_incomplete = client_capabilities.get("user_list_incomplete", False) include_deactivated_groups = client_capabilities.get("include_deactivated_groups", False) + archived_channels = client_capabilities.get("archived_channels", False) if fetch_event_types is not None: event_types_set: set[str] | None = set(fetch_event_types) @@ -1789,6 +1809,7 @@ def do_events_register( user_avatar_url_field_optional=user_avatar_url_field_optional, user_settings_object=user_settings_object, user_list_incomplete=user_list_incomplete, + archived_channels=archived_channels, # These presence params are a noop, because presence is not included. slim_presence=True, presence_last_update_id_fetched_by_client=None, @@ -1828,6 +1849,7 @@ def do_events_register( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) if queue_id is None: @@ -1850,6 +1872,7 @@ def do_events_register( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) # Apply events that came in while we were fetching initial data diff --git a/zerver/lib/home.py b/zerver/lib/home.py index 30aef3b275..8a1c650408 100644 --- a/zerver/lib/home.py +++ b/zerver/lib/home.py @@ -156,6 +156,7 @@ def build_page_params_for_home_page_load( linkifier_url_template=True, user_list_incomplete=True, include_deactivated_groups=True, + archived_channels=False, ) if user_profile is not None: diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 275f596552..78c903fefd 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -872,6 +872,7 @@ def stream_to_dict(stream: Stream, recent_traffic: dict[int, int] | None = None) stream_weekly_traffic = None return APIStreamDict( + is_archived=stream.deactivated, can_remove_subscribers_group=stream.can_remove_subscribers_group_id, creator_id=stream.creator_id, date_created=datetime_to_timestamp(stream.date_created), @@ -902,6 +903,7 @@ def get_streams_for_user( include_public: bool = True, include_web_public: bool = False, include_subscribed: bool = True, + exclude_archived: bool = True, include_all_active: bool = False, include_owner_subscribed: bool = False, ) -> list[Stream]: @@ -910,8 +912,11 @@ def get_streams_for_user( include_public = include_public and user_profile.can_access_public_streams() - # Start out with all active streams in the realm. - query = Stream.objects.filter(realm=user_profile.realm, deactivated=False) + # Start out with all streams in the realm. + query = Stream.objects.filter(realm=user_profile.realm) + + if exclude_archived: + query = query.filter(deactivated=False) if include_all_active: streams = query.only(*Stream.API_FIELDS) @@ -965,6 +970,7 @@ def do_get_streams( include_public: bool = True, include_web_public: bool = False, include_subscribed: bool = True, + exclude_archived: bool = True, include_all_active: bool = False, include_default: bool = False, include_owner_subscribed: bool = False, @@ -976,6 +982,7 @@ def do_get_streams( include_public, include_web_public, include_subscribed, + exclude_archived, include_all_active, include_owner_subscribed, ) diff --git a/zerver/lib/subscription_info.py b/zerver/lib/subscription_info.py index a6b13de712..c21290b868 100644 --- a/zerver/lib/subscription_info.py +++ b/zerver/lib/subscription_info.py @@ -27,7 +27,7 @@ from zerver.lib.types import ( SubscriptionStreamDict, ) from zerver.models import Realm, Stream, Subscription, UserProfile -from zerver.models.streams import get_active_streams +from zerver.models.streams import get_all_streams def get_web_public_subs(realm: Realm) -> SubscriptionInfo: @@ -42,6 +42,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo: subscribed = [] for stream in get_web_public_streams_queryset(realm): # Add Stream fields. + is_archived = stream.deactivated can_remove_subscribers_group_id = stream.can_remove_subscribers_group_id creator_id = stream.creator_id date_created = datetime_to_timestamp(stream.date_created) @@ -73,6 +74,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo: wildcard_mentions_notify = True sub = SubscriptionStreamDict( + is_archived=is_archived, audible_notifications=audible_notifications, can_remove_subscribers_group=can_remove_subscribers_group_id, color=color, @@ -115,6 +117,7 @@ def build_unsubscribed_sub_from_stream_dict( can_remove_subscribers_group_id=stream_dict["can_remove_subscribers_group"], creator_id=stream_dict["creator_id"], date_created=timestamp_to_datetime(stream_dict["date_created"]), + deactivated=stream_dict["is_archived"], description=stream_dict["description"], first_message_id=stream_dict["first_message_id"], history_public_to_subscribers=stream_dict["history_public_to_subscribers"], @@ -144,6 +147,7 @@ def build_stream_dict_for_sub( recent_traffic: dict[int, int] | None, ) -> SubscriptionStreamDict: # Handle Stream.API_FIELDS + is_archived = raw_stream_dict["deactivated"] can_remove_subscribers_group_id = raw_stream_dict["can_remove_subscribers_group_id"] creator_id = raw_stream_dict["creator_id"] date_created = datetime_to_timestamp(raw_stream_dict["date_created"]) @@ -187,6 +191,7 @@ def build_stream_dict_for_sub( # Our caller may add a subscribers field. return SubscriptionStreamDict( + is_archived=is_archived, audible_notifications=audible_notifications, can_remove_subscribers_group=can_remove_subscribers_group_id, color=color, @@ -218,6 +223,7 @@ def build_stream_dict_for_never_sub( raw_stream_dict: RawStreamDict, recent_traffic: dict[int, int] | None, ) -> NeverSubscribedStreamDict: + is_archived = raw_stream_dict["deactivated"] can_remove_subscribers_group_id = raw_stream_dict["can_remove_subscribers_group_id"] creator_id = raw_stream_dict["creator_id"] date_created = datetime_to_timestamp(raw_stream_dict["date_created"]) @@ -244,6 +250,7 @@ def build_stream_dict_for_never_sub( # Our caller may add a subscribers field. return NeverSubscribedStreamDict( + is_archived=is_archived, can_remove_subscribers_group=can_remove_subscribers_group_id, creator_id=creator_id, date_created=date_created, @@ -447,9 +454,12 @@ def has_metadata_access_to_previously_subscribed_stream( def gather_subscriptions_helper( user_profile: UserProfile, include_subscribers: bool = True, + include_archived_channels: bool = False, ) -> SubscriptionInfo: realm = user_profile.realm - all_streams = get_active_streams(realm).values( + all_streams = get_all_streams( + realm, include_archived_channels=include_archived_channels + ).values( *Stream.API_FIELDS, # The realm_id and recipient_id are generally not needed in the API. "realm_id", @@ -517,7 +527,9 @@ def gather_subscriptions_helper( unsubscribed.append(stream_dict) if user_profile.can_access_public_streams(): - never_subscribed_stream_ids = set(all_streams_map) - sub_unsub_stream_ids + never_subscribed_stream_ids = { + stream["id"] for stream in all_streams if not stream["deactivated"] + } - sub_unsub_stream_ids else: web_public_stream_ids = {stream["id"] for stream in all_streams if stream["is_web_public"]} never_subscribed_stream_ids = web_public_stream_ids - sub_unsub_stream_ids diff --git a/zerver/lib/types.py b/zerver/lib/types.py index 6937efeee9..f6af9f30fc 100644 --- a/zerver/lib/types.py +++ b/zerver/lib/types.py @@ -144,6 +144,7 @@ class RawStreamDict(TypedDict): can_remove_subscribers_group_id: int creator_id: int | None date_created: datetime + deactivated: bool description: str first_message_id: int | None history_public_to_subscribers: bool @@ -193,6 +194,7 @@ class SubscriptionStreamDict(TypedDict): in_home_view: bool invite_only: bool is_announcement_only: bool + is_archived: bool is_muted: bool is_web_public: bool message_retention_days: int | None @@ -208,6 +210,7 @@ class SubscriptionStreamDict(TypedDict): class NeverSubscribedStreamDict(TypedDict): + is_archived: bool can_remove_subscribers_group: int creator_id: int | None date_created: int @@ -232,6 +235,7 @@ class DefaultStreamDict(TypedDict): with few exceptions and possible additional fields. """ + is_archived: bool can_remove_subscribers_group: int creator_id: int | None date_created: int diff --git a/zerver/models/streams.py b/zerver/models/streams.py index 06b9a71f7e..0036d4592e 100644 --- a/zerver/models/streams.py +++ b/zerver/models/streams.py @@ -182,6 +182,7 @@ class Stream(models.Model): API_FIELDS = [ "creator_id", "date_created", + "deactivated", "description", "first_message_id", "history_public_to_subscribers", @@ -197,6 +198,7 @@ class Stream(models.Model): def to_dict(self) -> DefaultStreamDict: return DefaultStreamDict( + is_archived=self.deactivated, can_remove_subscribers_group=self.can_remove_subscribers_group_id, creator_id=self.creator_id, date_created=datetime_to_timestamp(self.date_created), @@ -229,6 +231,16 @@ def get_active_streams(realm: Realm) -> QuerySet[Stream]: return Stream.objects.filter(realm=realm, deactivated=False) +def get_all_streams(realm: Realm, include_archived_channels: bool = True) -> QuerySet[Stream]: + """ + Return all streams for `include_archived_channels`= true (including invite-only and deactivated streams). + """ + if not include_archived_channels: + return get_active_streams(realm) + + return Stream.objects.filter(realm=realm) + + def get_linkable_streams(realm_id: int) -> QuerySet[Stream]: """ This returns the streams that we are allowed to linkify using diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index d3e18a888f..0f543903e8 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -694,6 +694,7 @@ paths: { "name": "test", "stream_id": 9, + "is_archived": false, "creator_id": null, "description": "", "rendered_description": "", @@ -1337,6 +1338,7 @@ paths: { "name": "private", "stream_id": 12, + "is_archived": false, "description": "", "rendered_description": "", "date_created": 1691057093, @@ -1395,6 +1397,7 @@ paths: { "name": "private", "stream_id": 12, + "is_archived": true, "description": "", "rendered_description": "", "date_created": 1691057093, @@ -1994,6 +1997,7 @@ paths: { "name": "Scotland", "stream_id": 3, + "is_archived": false, "description": "Located in the United Kingdom", "rendered_description": "
Located in the United Kingdom
", "date_created": 1691057093, @@ -2010,6 +2014,7 @@ paths: { "name": "Denmark", "stream_id": 1, + "is_archived": false, "description": "A Scandinavian country", "rendered_description": "A Scandinavian country
", "date_created": 1691057093, @@ -2026,6 +2031,7 @@ paths: { "name": "Verona", "stream_id": 5, + "is_archived": false, "description": "A city in Italy", "rendered_description": "A city in Italy
", "date_created": 1691057093, @@ -2073,6 +2079,7 @@ paths: { "name": "Scotland", "stream_id": 3, + "is_archived": false, "description": "Located in the United Kingdom", "rendered_description": "Located in the United Kingdom
", "date_created": 1691057093, @@ -10178,6 +10185,7 @@ paths: "creator_id": null, "description": "A Scandinavian country", "desktop_notifications": true, + "is_archived": false, "is_muted": false, "invite_only": false, "name": "Denmark", @@ -10192,6 +10200,7 @@ paths: "creator_id": 8, "description": "Located in the United Kingdom", "desktop_notifications": true, + "is_archived": false, "is_muted": false, "invite_only": false, "name": "Scotland", @@ -14079,6 +14088,15 @@ paths: **Changes**: New in Zulip 10.0 (feature level 294). This capability is for backwards-compatibility. + - `archived_channels`: Boolean for whether the client supports processing + [archived channels](/help/archive-a-channel) in the `stream` and + `subscription` event types. If `false`, the server will not include data + related to archived channels in the `register` response or in events. +