diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index 58a282631e..7c46ef6385 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -10,6 +10,11 @@ below features are supported. ## Changes in Zulip 2.2 +**Feature level 12** + +* [`GET users/{user_id}/subscriptions/{stream_id}`](/api/get-subscription-status): + New endpoint added for checking if another user is subscribed to a stream. + **Feature level 11** * [`POST /register`](/api/register-queue): Added diff --git a/templates/zerver/api/get-subscription-status.md b/templates/zerver/api/get-subscription-status.md new file mode 100644 index 0000000000..28f708c021 --- /dev/null +++ b/templates/zerver/api/get-subscription-status.md @@ -0,0 +1,28 @@ +# Get user subscription status + +{generate_api_description(/users/{user_id}/subscriptions/{stream_id}:get)} + +## Usage examples + +{start_tabs} +{tab|python} + +{generate_code_example(python)|/users/{user_id}/subscriptions/{stream_id}:get|example} + +{tab|curl} + +{generate_code_example(curl)|/users/{user_id}/subscriptions/{stream_id}:get|example} + +{end_tabs} + +## Arguments + +{generate_api_arguments_table|zulip.yaml|/users/{user_id}/subscriptions/{stream_id}:get} + +## Response + +#### Example response + +A typical successful JSON response may look like: + +{generate_code_example|/users/{user_id}/subscriptions/{stream_id}:get|fixture(200)} diff --git a/templates/zerver/help/include/rest-endpoints.md b/templates/zerver/help/include/rest-endpoints.md index e68cfeb0d7..2bab2d3f36 100644 --- a/templates/zerver/help/include/rest-endpoints.md +++ b/templates/zerver/help/include/rest-endpoints.md @@ -21,6 +21,7 @@ * [Add subscriptions](/api/add-subscriptions) * [Update subscription settings](/api/update-subscription-properties) * [Remove subscriptions](/api/remove-subscriptions) +* [Get subscription status](/api/get-subscription-status) * [Get topics in a stream](/api/get-stream-topics) * [Topic muting](/api/mute-topics) * [Create a stream](/api/create-stream) diff --git a/version.py b/version.py index 28aa9f4448..0602d67f84 100644 --- a/version.py +++ b/version.py @@ -29,7 +29,7 @@ DESKTOP_WARNING_VERSION = "5.2.0" # # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md. -API_FEATURE_LEVEL = 11 +API_FEATURE_LEVEL = 12 # 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/python_examples.py b/zerver/openapi/python_examples.py index a5f3cd064f..5b0523dd1e 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -253,6 +253,19 @@ def update_user(client: Client) -> None: # {code_example|end} validate_against_openapi_schema(result, '/users/{user_id}', 'patch', '400') +@openapi_test_function("/users/{user_id}/subscriptions/{stream_id}:get") +def get_subscription_status(client: Client) -> None: + # {code_example|start} + # Check whether a user is a subscriber to a given stream. + user_id = 7 + stream_id = 1 + result = client.call_endpoint( + url='/users/{}/subscriptions/{}'.format(user_id, stream_id), + method='GET', + ) + # {code_example|end} + validate_against_openapi_schema(result, '/users/{user_id}/subscriptions/{stream_id}', 'get', '200') + @openapi_test_function("/realm/filters:get") def get_realm_filters(client: Client) -> None: @@ -1117,6 +1130,7 @@ def test_users(client: Client) -> None: deactivate_user(client) reactivate_user(client) update_user(client) + get_subscription_status(client) get_profile(client) update_notification_settings(client) upload_file(client) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index db0d0297a4..fe8766db53 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -2628,6 +2628,40 @@ paths: application/json: schema: $ref: '#/components/schemas/NonExistingStreamError' + /users/{user_id}/subscriptions/{stream_id}: + get: + operationId: get_subscription_status + tags: ["streams"] + description: | + Check whether a user is subscribed to a stream. + + `GET {{ api_url }}/v1/users/{user_id}/subscriptions/{stream_id}` + + **Changes**: New in Zulip 2.2 (feature level 11). + parameters: + - $ref: '#/components/parameters/UserId' + example: 7 + - $ref: '#/components/parameters/StreamIdInPath' + example: 1 + responses: + '200': + description: Success + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/JsonSuccess' + - properties: + is_subscribed: + type: boolean + description: | + Whether the user is subscribed to the stream. + - example: + { + "msg": "", + "result": "success", + "is_subscribed": false + } /users/me/subscriptions/muted_topics: patch: operationId: mute_topic diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index dd51c50c97..7a4760e1de 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -1093,6 +1093,38 @@ class UserProfileTest(ZulipTestCase): get_user_by_id_in_realm_including_cross_realm( hamlet.id, None) + def test_get_user_subscription_status(self) -> None: + self.login('hamlet') + iago = self.example_user('iago') + stream = get_stream('Rome', iago.realm) + + # Invalid User ID. + result = self.client_get("/json/users/25/subscriptions/{}".format(stream.id)) + self.assert_json_error(result, "No such user") + + # Invalid Stream ID. + result = self.client_get("/json/users/{}/subscriptions/25".format(iago.id)) + self.assert_json_error(result, "Invalid stream id") + + result = ujson.loads(self.client_get("/json/users/{}/subscriptions/{}".format(iago.id, stream.id)).content) + self.assertFalse(result['is_subscribed']) + + # Subscribe to the stream. + self.subscribe(iago, stream.name) + with queries_captured() as queries: + result = ujson.loads(self.client_get("/json/users/{}/subscriptions/{}".format(iago.id, stream.id)).content) + + self.assert_length(queries, 7) + self.assertTrue(result['is_subscribed']) + + # Logging in with a Guest user. + polonius = self.example_user("polonius") + self.login('polonius') + self.assertTrue(polonius.is_guest) + + result = self.client_get("/json/users/{}/subscriptions/{}".format(iago.id, stream.id)) + self.assert_json_error(result, "Invalid stream id") + class ActivateTest(ZulipTestCase): def test_basics(self) -> None: user = self.example_user('hamlet') diff --git a/zerver/views/users.py b/zerver/views/users.py index a4b13d9465..547e582707 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -22,7 +22,7 @@ from zerver.lib.exceptions import CannotDeactivateLastUserError from zerver.lib.integrations import EMBEDDED_BOTS from zerver.lib.request import has_request_variables, REQ from zerver.lib.response import json_error, json_success -from zerver.lib.streams import access_stream_by_name +from zerver.lib.streams import access_stream_by_name, access_stream_by_id, subscribed_to_stream from zerver.lib.upload import upload_avatar_image from zerver.lib.validator import check_bool, check_string, check_int, check_url, check_dict, check_list, \ check_int_in @@ -471,3 +471,15 @@ def get_profile_backend(request: HttpRequest, user_profile: UserProfile) -> Http result['max_message_id'] = messages[0].id return json_success(result) + +@has_request_variables +def get_subscription_backend(request: HttpRequest, user_profile: UserProfile, + user_id: int=REQ(validator=check_int, path_only=True), + stream_id: int=REQ(validator=check_int, path_only=True) + ) -> HttpResponse: + target_user = access_user_by_id(user_profile, user_id, read_only=True) + (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) + + subscription_status = {'is_subscribed': subscribed_to_stream(target_user, stream_id)} + + return json_success(subscription_status) diff --git a/zproject/urls.py b/zproject/urls.py index 48686c3b94..70aa953011 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -149,6 +149,8 @@ v1_api_and_json_patterns = [ {'GET': 'zerver.views.users.get_members_backend', 'PATCH': 'zerver.views.users.update_user_backend', 'DELETE': 'zerver.views.users.deactivate_user_backend'}), + url(r'^users/(?P[0-9]+)/subscriptions/(?P[0-9]+)$', rest_dispatch, + {'GET': 'zerver.views.users.get_subscription_backend'}), url(r'^bots$', rest_dispatch, {'GET': 'zerver.views.users.get_bots_backend', 'POST': 'zerver.views.users.add_bot_backend'}),