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. +
+ **Changes**: New in Zulip 10.0 (feature level 315). This allows clients to + access archived channels, without breaking backwards-compatibility for + existing clients. + [help-linkifiers]: /help/add-a-custom-linkifier [rfc6570]: https://www.rfc-editor.org/rfc/rfc6570.html [events-linkifiers]: /api/get-events#realm_linkifiers @@ -14784,6 +14802,7 @@ paths: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -19562,6 +19581,16 @@ paths: type: boolean default: true example: false + - name: exclude_archived + in: query + description: | + Whether to exclude archived streams from the results. + + **Changes**: New in Zulip 10.0 (feature level 315). + schema: + type: boolean + default: true + example: true - name: include_all_active in: query description: | @@ -19612,6 +19641,7 @@ paths: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -19655,6 +19685,7 @@ paths: required: - stream_id - name + - is_archived - description - date_created - creator_id @@ -19683,6 +19714,7 @@ paths: "history_public_to_subscribers": false, "invite_only": true, "is_announcement_only": false, + "is_archived": false, "is_default": false, "is_web_public": false, "message_retention_days": null, @@ -19701,6 +19733,7 @@ paths: "history_public_to_subscribers": true, "invite_only": false, "is_announcement_only": false, + "is_archived": false, "is_default": true, "is_web_public": false, "message_retention_days": null, @@ -19768,6 +19801,7 @@ paths: "creator_id": null, "invite_only": false, "is_announcement_only": false, + "is_archived": false, "is_web_public": false, "message_retention_days": null, "name": "Denmark", @@ -21555,6 +21589,7 @@ components: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -21588,6 +21623,7 @@ components: required: - stream_id - name + - is_archived - description - date_created - creator_id @@ -21608,6 +21644,7 @@ components: properties: stream_id: {} name: {} + is_archived: {} description: {} date_created: {} creator_id: @@ -21626,6 +21663,7 @@ components: required: - stream_id - name + - is_archived - description - date_created - creator_id @@ -21651,6 +21689,13 @@ components: type: string description: | The name of the channel. + is_archived: + type: boolean + description: | + A boolean indicating whether the channel is [archived](/help/archive-a-channel). + + **Changes**: New in Zulip 10.0 (feature level 315). + Previously, this endpoint never returned archived channels. description: type: string description: | @@ -22753,6 +22798,16 @@ components: was named `can_remove_subscribers_group_id`. New in Zulip 6.0 (feature level 142). + is_archived: + type: boolean + description: | + A boolean indicating whether the channel is [archived](/help/archive-a-channel). + + **Changes**: New in Zulip 10.0 (feature level 315). + Previously, subscriptions only included active + channels. Note that some endpoints will never return archived + channels unless the client declares explicit support for + them via the `archived_channels` client capability. DefaultChannelGroup: type: object description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 905caed49f..ca0ac83765 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -305,6 +305,7 @@ class BaseAction(ZulipTestCase): user_list_incomplete: bool = False, client_is_old: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> Iterator[list[dict[str, Any]]]: """ Make sure we have a clean slate of client descriptors for these tests. @@ -354,6 +355,7 @@ class BaseAction(ZulipTestCase): linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) if client_is_old: @@ -395,6 +397,7 @@ class BaseAction(ZulipTestCase): linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) post_process_state(self.user_profile, hybrid_state, notification_settings_null) after = orjson.dumps(hybrid_state) @@ -425,6 +428,7 @@ class BaseAction(ZulipTestCase): linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) post_process_state(self.user_profile, normal_state, notification_settings_null) self.match_states(hybrid_state, normal_state, events) @@ -3355,6 +3359,23 @@ class NormalActionsTest(BaseAction): check_stream_delete("events[0]", events[0]) self.assertIsNone(events[0]["streams"][0]["stream_weekly_traffic"]) + def test_admin_deactivate_unsubscribed_stream(self) -> None: + self.set_up_db_for_testing_user_access() + stream = self.make_stream("test_stream") + iago = self.example_user("iago") + realm = iago.realm + self.user_profile = self.example_user("iago") + + self.subscribe(iago, stream.name) + self.assertCountEqual(self.users_subscribed_to_stream(stream.name, realm), [iago]) + + self.unsubscribe(iago, stream.name) + self.assertCountEqual(self.users_subscribed_to_stream(stream.name, realm), []) + + with self.verify_action(num_events=1, archived_channels=True) as events: + do_deactivate_stream(stream, acting_user=iago) + check_stream_delete("events[0]", events[0]) + def test_user_losing_access_on_deactivating_stream(self) -> None: self.set_up_db_for_testing_user_access() polonius = self.example_user("polonius") @@ -3367,7 +3388,7 @@ class NormalActionsTest(BaseAction): self.users_subscribed_to_stream(stream.name, realm), [hamlet, polonius] ) - with self.verify_action(num_events=2) as events: + with self.verify_action(num_events=2, archived_channels=True) as events: do_deactivate_stream(stream, acting_user=None) check_stream_delete("events[0]", events[0]) check_realm_user_remove("events[1]", events[1]) @@ -3383,7 +3404,7 @@ class NormalActionsTest(BaseAction): self.users_subscribed_to_stream(stream.name, realm), [iago, polonius, shiva] ) - with self.verify_action(num_events=2) as events: + with self.verify_action(num_events=2, archived_channels=True) as events: do_deactivate_stream(stream, acting_user=None) check_stream_delete("events[0]", events[0]) check_realm_user_remove("events[1]", events[1]) diff --git a/zerver/tests/test_outgoing_webhook_system.py b/zerver/tests/test_outgoing_webhook_system.py index 470c29c901..b0ca446964 100644 --- a/zerver/tests/test_outgoing_webhook_system.py +++ b/zerver/tests/test_outgoing_webhook_system.py @@ -648,7 +648,7 @@ class TestOutgoingWebhookMessaging(ZulipTestCase): prev_message = self.get_second_to_last_message() self.assertIn( - "tried to send a message to channel #**Denmark**, but that channel does not exist", + "Failure! Bot is unavailable", prev_message.content, ) diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index f3a6fd3a7a..16ebe32c91 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -238,8 +238,8 @@ class TestMiscStuff(ZulipTestCase): """Verify that all the fields from `Stream.API_FIELDS` and `Subscription.API_FIELDS` present in `APIStreamDict` and `APISubscriptionDict`, respectively. """ - expected_fields = set(Stream.API_FIELDS) | {"stream_id"} - expected_fields -= {"id", "can_remove_subscribers_group_id"} + expected_fields = set(Stream.API_FIELDS) | {"stream_id", "is_archived"} + expected_fields -= {"id", "can_remove_subscribers_group_id", "deactivated"} expected_fields |= {"can_remove_subscribers_group"} stream_dict_fields = set(APIStreamDict.__annotations__.keys()) @@ -2508,6 +2508,13 @@ class StreamAdminTest(ZulipTestCase): public_streams = [s["name"] for s in self.assert_json_success(result)["streams"]] self.assertNotIn(deactivated_stream_name, public_streams) + # It shows up with `exclude_archived` parameter set to false. + result = self.client_get( + "/json/streams", {"exclude_archived": "false", "include_all_active": "true"} + ) + streams = [s["name"] for s in self.assert_json_success(result)["streams"]] + self.assertIn(deactivated_stream_name, streams) + # You can't subscribe to archived stream. result = self.common_subscribe_to_streams( self.example_user("hamlet"), [deactivated_stream_name], allow_fail=True @@ -5464,10 +5471,10 @@ class SubscriptionAPITest(ZulipTestCase): self.assert_length(result, 1) self.assertEqual(result[0]["stream_id"], stream1.id) - def test_gather_subscriptions_excludes_deactivated_streams(self) -> None: + def test_gather_subscriptions_deactivated_streams(self) -> None: """ - Check that gather_subscriptions_helper does not include deactivated streams in its - results. + Check that gather_subscriptions_helper does/doesn't include deactivated streams in its + results with `exclude_archived` parameter. """ realm = get_realm("zulip") admin_user = self.example_user("iago") @@ -5497,6 +5504,10 @@ class SubscriptionAPITest(ZulipTestCase): admin_after_delete = gather_subscriptions_helper(admin_user) non_admin_after_delete = gather_subscriptions_helper(non_admin_user) + admin_after_delete_include_archived = gather_subscriptions_helper( + admin_user, include_archived_channels=True + ) + # Compare results - should be 1 stream less self.assertTrue( len(admin_before_delete.subscriptions) == len(admin_after_delete.subscriptions) + 1, @@ -5508,6 +5519,14 @@ class SubscriptionAPITest(ZulipTestCase): "Expected exactly 1 less stream from gather_subscriptions_helper", ) + # Compare results - should be the same number of streams + self.assertTrue( + len(admin_before_delete.subscriptions) + len(admin_before_delete.unsubscribed) + == len(admin_after_delete_include_archived.subscriptions) + + len(admin_after_delete_include_archived.unsubscribed), + "Expected exact number of streams from gather_subscriptions_helper", + ) + def test_validate_user_access_to_subscribers_helper(self) -> None: """ Ensure the validate_user_access_to_subscribers_helper is properly raising @@ -5944,6 +5963,7 @@ class GetSubscribersTest(ZulipTestCase): def verify_sub_fields(self, sub_data: SubscriptionInfo) -> None: other_fields = { + "is_archived", "is_announcement_only", "in_home_view", "stream_id", @@ -5952,7 +5972,7 @@ class GetSubscribersTest(ZulipTestCase): } expected_fields = set(Stream.API_FIELDS) | set(Subscription.API_FIELDS) | other_fields - expected_fields -= {"id", "can_remove_subscribers_group_id"} + expected_fields -= {"id", "can_remove_subscribers_group_id", "deactivated"} expected_fields |= {"can_remove_subscribers_group"} for lst in [sub_data.subscriptions, sub_data.unsubscribed]: @@ -5960,6 +5980,7 @@ class GetSubscribersTest(ZulipTestCase): self.assertEqual(set(sub), expected_fields) other_fields = { + "is_archived", "is_announcement_only", "stream_id", "stream_weekly_traffic", @@ -5967,7 +5988,7 @@ class GetSubscribersTest(ZulipTestCase): } expected_fields = set(Stream.API_FIELDS) | other_fields - expected_fields -= {"id", "can_remove_subscribers_group_id"} + expected_fields -= {"id", "can_remove_subscribers_group_id", "deactivated"} expected_fields |= {"can_remove_subscribers_group"} for never_sub in sub_data.never_subscribed: diff --git a/zerver/tornado/django_api.py b/zerver/tornado/django_api.py index b9bc56b0cc..d7043401d8 100644 --- a/zerver/tornado/django_api.py +++ b/zerver/tornado/django_api.py @@ -91,6 +91,7 @@ def request_event_queue( linkifier_url_template: bool = False, user_list_incomplete: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> str | None: if not settings.USING_TORNADO: return None @@ -115,6 +116,7 @@ def request_event_queue( "linkifier_url_template": orjson.dumps(linkifier_url_template), "user_list_incomplete": orjson.dumps(user_list_incomplete), "include_deactivated_groups": orjson.dumps(include_deactivated_groups), + "archived_channels": orjson.dumps(archived_channels), } if event_types is not None: diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index 6b0f246424..84964eb044 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -79,6 +79,7 @@ class ClientDescriptor: linkifier_url_template: bool = False, user_list_incomplete: bool = False, include_deactivated_groups: bool = False, + archived_channels: bool = False, ) -> None: # TODO: We eventually want to upstream this code to the caller, but # serialization concerns make it a bit difficult. @@ -110,6 +111,7 @@ class ClientDescriptor: self.linkifier_url_template = linkifier_url_template self.user_list_incomplete = user_list_incomplete self.include_deactivated_groups = include_deactivated_groups + self.archived_channels = archived_channels # Default for lifespan_secs is DEFAULT_EVENT_QUEUE_TIMEOUT_SECS; # but users can set it as high as MAX_QUEUE_TIMEOUT_SECS. @@ -141,6 +143,7 @@ class ClientDescriptor: linkifier_url_template=self.linkifier_url_template, user_list_incomplete=self.user_list_incomplete, include_deactivated_groups=self.include_deactivated_groups, + archived_channels=self.archived_channels, ) @override @@ -178,6 +181,7 @@ class ClientDescriptor: d.get("linkifier_url_template", False), d.get("user_list_incomplete", False), d.get("include_deactivated_groups", False), + d.get("archived_channels", False), ) ret.last_connection_time = d["last_connection_time"] return ret diff --git a/zerver/tornado/views.py b/zerver/tornado/views.py index f2a9060bdd..3835cd8af3 100644 --- a/zerver/tornado/views.py +++ b/zerver/tornado/views.py @@ -210,6 +210,10 @@ def get_events_backend( Json[bool], ApiParamConfig(documentation_status=DocumentationStatus.INTENTIONALLY_UNDOCUMENTED), ] = False, + archived_channels: Annotated[ + Json[bool], + ApiParamConfig(documentation_status=DocumentationStatus.INTENTIONALLY_UNDOCUMENTED), + ] = False, ) -> HttpResponse: if narrow is None: narrow = [] @@ -248,6 +252,7 @@ def get_events_backend( linkifier_url_template=linkifier_url_template, user_list_incomplete=user_list_incomplete, include_deactivated_groups=include_deactivated_groups, + archived_channels=archived_channels, ) result = in_tornado_thread(fetch_events)( diff --git a/zerver/views/streams.py b/zerver/views/streams.py index add9fe0880..d618d473e5 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -847,6 +847,7 @@ def get_streams_backend( include_public: Json[bool] = True, include_web_public: Json[bool] = False, include_subscribed: Json[bool] = True, + exclude_archived: Json[bool] = True, include_all_active: Json[bool] = False, include_default: Json[bool] = False, include_owner_subscribed: Json[bool] = False, @@ -856,6 +857,7 @@ def get_streams_backend( include_public=include_public, include_web_public=include_web_public, include_subscribed=include_subscribed, + exclude_archived=exclude_archived, include_all_active=include_all_active, include_default=include_default, include_owner_subscribed=include_owner_subscribed,