diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index 284a7d8724..d061c10825 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -20,6 +20,11 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 6.0 +**Feature level 132** + +* [`GET /streams/{stream_id}`](/api/get-stream-by-id): + Added new endpoint to get a stream by ID. + **Feature level 131** * [`GET /user_groups`](/api/get-user-groups),[`POST diff --git a/templates/zerver/help/include/rest-endpoints.md b/templates/zerver/help/include/rest-endpoints.md index e803234ca8..c9dde8fd0e 100644 --- a/templates/zerver/help/include/rest-endpoints.md +++ b/templates/zerver/help/include/rest-endpoints.md @@ -31,6 +31,7 @@ * [Get all subscribers](/api/get-subscribers) * [Update subscription settings](/api/update-subscription-settings) * [Get all streams](/api/get-streams) +* [Get a stream by ID](/api/get-stream-by-id) * [Get stream ID](/api/get-stream-id) * [Create a stream](/api/create-stream) * [Update a stream](/api/update-stream) diff --git a/version.py b/version.py index 1b9fa1baf1..d554b618be 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3" # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md, as well as # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 131 +API_FEATURE_LEVEL = 132 # 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/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 148e4148a4..862fb7a40e 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -13428,6 +13428,67 @@ paths: `include_all_active` parameter (i.e. because they are not an organization administrator): /streams/{stream_id}: + get: + operationId: get-stream-by-id + summary: Get a stream by ID + tags: ["streams"] + description: | + Fetch details for the stream with the ID `stream_id`. + + `GET {{ api_url }}/v1/streams/{stream_id}` + + **Changes**: New in Zulip 6.0 (feature level 132). + parameters: + - $ref: "#/components/parameters/StreamIdInPath" + responses: + "200": + description: Success. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - $ref: "#/components/schemas/SuccessDescription" + - additionalProperties: false + properties: + result: {} + msg: {} + stream: + $ref: "#/components/schemas/BasicStream" + example: + { + "msg": "", + "result": "success", + "stream": + { + "description": "A Scandinavian country", + "first_message_id": 1, + "history_public_to_subscribers": True, + "invite_only": False, + "is_announcement_only": False, + "is_web_public": False, + "message_retention_days": null, + "name": "Denmark", + "rendered_description": "

A Scandinavian country

", + "stream_id": 7, + "stream_post_policy": 1, + }, + } + "400": + description: Bad request. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/CodedError" + - example: + { + "code": "BAD_REQUEST", + "msg": "Invalid stream id", + "result": "error", + } + description: | + An example JSON response for when the stream ID is not valid. delete: operationId: archive-stream summary: Archive a stream diff --git a/zerver/tests/test_decorators.py b/zerver/tests/test_decorators.py index daf84e9c46..39f9662649 100644 --- a/zerver/tests/test_decorators.py +++ b/zerver/tests/test_decorators.py @@ -1983,7 +1983,7 @@ class RestAPITest(ZulipTestCase): result = self.client_options("/json/streams/15") self.assertEqual(result.status_code, 204) - self.assertEqual(str(result["Allow"]), "DELETE, PATCH") + self.assertEqual(str(result["Allow"]), "DELETE, GET, HEAD, PATCH") def test_http_accept_redirect(self) -> None: result = self.client_get("/json/users", HTTP_ACCEPT="text/html") diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 66eab3b33a..f4f7b47ef9 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -5457,6 +5457,40 @@ class GetStreamsTest(ZulipTestCase): ] self.assertEqual(sorted(s["name"] for s in json["streams"]), sorted(all_streams)) + def test_get_single_stream_api(self) -> None: + self.login("hamlet") + realm = get_realm("zulip") + denmark_stream = get_stream("Denmark", realm) + result = self.client_get(f"/json/streams/{denmark_stream.id}") + self.assert_json_success(result) + json = result.json() + self.assertEqual(json["stream"]["name"], "Denmark") + self.assertEqual(json["stream"]["stream_id"], denmark_stream.id) + + result = self.client_get("/json/streams/9999") + self.assert_json_error(result, "Invalid stream id") + + private_stream = self.make_stream("private_stream", invite_only=True) + self.subscribe(self.example_user("cordelia"), "private_stream") + + # Non-admins cannot access unsubscribed private streams. + result = self.client_get(f"/json/streams/{private_stream.id}") + self.assert_json_error(result, "Invalid stream id") + + self.login("iago") + result = self.client_get(f"/json/streams/{private_stream.id}") + self.assert_json_success(result) + json = result.json() + self.assertEqual(json["stream"]["name"], "private_stream") + self.assertEqual(json["stream"]["stream_id"], private_stream.id) + + self.login("cordelia") + result = self.client_get(f"/json/streams/{private_stream.id}") + self.assert_json_success(result) + json = result.json() + self.assertEqual(json["stream"]["name"], "private_stream") + self.assertEqual(json["stream"]["stream_id"], private_stream.id) + class StreamIdTest(ZulipTestCase): def test_get_stream_id(self) -> None: diff --git a/zerver/views/streams.py b/zerver/views/streams.py index 7cb598460d..ec573b2889 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -754,6 +754,16 @@ def get_streams_backend( return json_success(request, data={"streams": streams}) +@has_request_variables +def get_stream_backend( + request: HttpRequest, + user_profile: UserProfile, + stream_id: int, +) -> HttpResponse: + (stream, sub) = access_stream_by_id(user_profile, stream_id, allow_realm_admin=True) + return json_success(request, data={"stream": stream.to_dict()}) + + @has_request_variables def get_topics_backend( request: HttpRequest, diff --git a/zproject/urls.py b/zproject/urls.py index 5cadc3db80..796efd6219 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -145,6 +145,7 @@ from zerver.views.streams import ( create_default_stream_group, deactivate_stream_backend, delete_in_topic, + get_stream_backend, get_streams_backend, get_subscribers_backend, get_topics_backend, @@ -443,7 +444,10 @@ v1_api_and_json_patterns = [ # GET returns "stream info" (undefined currently?), HEAD returns whether stream exists (200 or 404) rest_path("streams//members", GET=get_subscribers_backend), rest_path( - "streams/", PATCH=update_stream_backend, DELETE=deactivate_stream_backend + "streams/", + GET=get_stream_backend, + PATCH=update_stream_backend, + DELETE=deactivate_stream_backend, ), # Delete topic in stream rest_path("streams//delete_topic", POST=delete_in_topic),