diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 0f56d1e14b..5707b09844 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -378,10 +378,11 @@ No changes; feature level used for Zulip 8.0 release. to create reusable invitation links. Previously, this endpoint was restricted to admin users only. -* `GET /invites`: Endpoint response for non-admin users now includes both - email invitations and reusable invitation links that they have created. - Previously, non-admin users could only create email invitations, and - therefore the response did not include reusable invitation links for these users. +* [`GET /invites`](/api/get-invites): Endpoint response for non-admin users now + includes both email invitations and reusable invitation links that they have + created. Previously, non-admin users could only create email invitations, and + therefore the response did not include reusable invitation links for these + users. * `DELETE /invites/multiuse/{invite_id}`: Non-admin users can now revoke reusable invitation links they have created. Previously, only admin users could @@ -611,8 +612,8 @@ No changes; feature level used for Zulip 7.0 release. **Feature level 180** -* `POST /invites`: Added support for invitations specifying the empty - list as the user's initial stream subscriptions. Previously, this +* [`POST /invites`](/api/send-invites): Added support for invitations specifying + the empty list as the user's initial stream subscriptions. Previously, this returned an error. This change was also backported to Zulip 6.2, and is available at feature levels 157-158 as well. @@ -848,8 +849,8 @@ releases. **Feature level 157** -* `POST /invites`: Added support for invitations specifying the empty - list as the user's initial stream subscriptions. Previously, this +* [`POST /invites`](/api/send-invites): Added support for invitations specifying + the empty list as the user's initial stream subscriptions. Previously, this returned an error. This change was backported from the Zulip 7.0 branch, and thus is available at feature levels 157-158 and 180+. @@ -1107,8 +1108,8 @@ user's profile. **Feature level 126** -* `POST /invites`, `POST /invites/multiuse`: Replaced `invite_expires_in_days` - parameter with `invite_expires_in_minutes`. +* [`POST /invites`](/api/send-invites), `POST /invites/multiuse`: Replaced + `invite_expires_in_days` parameter with `invite_expires_in_minutes`. **Feature level 125** @@ -1167,9 +1168,9 @@ No changes; feature level used for Zulip 5.0 release. **Feature level 117** -* `POST /invites`, `POST /invites/multiuse`: Added support for passing - `null` as the `invite_expires_in_days` parameter to request an - invitation that never expires. +* [`POST /invites`](/api/send-invites), `POST /invites/multiuse`: Added + support for passing `null` as the `invite_expires_in_days` parameter + to request an invitation that never expires. **Feature level 116** @@ -1327,7 +1328,7 @@ No changes; feature level used for Zulip 5.0 release. * [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults): Added new endpoint to update default values of user settings in a realm. -* `POST /invites`, `POST /invites/multiuse`: Added +* [`POST /invites`](/api/send-invites), `POST /invites/multiuse`: Added `invite_expires_in_days` parameter encoding the number days before the invitation should expire. @@ -1588,8 +1589,8 @@ No changes; feature level used for Zulip 4.0 release. **Feature level 61** -* `POST /invites`, `POST /invites/multiuse`: Added support for - inviting users as moderators. +* [`POST /invites`](/api/send-invites), `POST /invites/multiuse`: Added + support for inviting users as moderators. **Feature level 60** @@ -1867,8 +1868,8 @@ No changes; feature level used for Zulip 3.0 release. encoded as integers; (previously the implementation could send floats incorrectly suggesting that microsecond precision is relevant). -* `GET /invites`: Now encodes the user ID of the person who created - the invitation as `invited_by_user_id`, replacing the previous +* [`GET /invites`](/api/get-invites): Now encodes the user ID of the person + who created the invitation as `invited_by_user_id`, replacing the previous `ref` field (which had that user's Zulip display email address). * [`POST /register`](/api/register-queue): The encoding of an unlimited `realm_message_retention_days` in the response was changed diff --git a/api_docs/include/rest-endpoints.md b/api_docs/include/rest-endpoints.md index 3e9c54307b..5d1b3a0ce1 100644 --- a/api_docs/include/rest-endpoints.md +++ b/api_docs/include/rest-endpoints.md @@ -88,6 +88,11 @@ * [Add alert words](/api/add-alert-words) * [Remove alert words](/api/remove-alert-words) +#### Invitations + +* [Get all invitations](/api/get-invites) +* [Send invitations](/api/send-invites) + #### Server & organizations * [Get server settings](/api/get-server-settings) diff --git a/zerver/openapi/python_examples.py b/zerver/openapi/python_examples.py index 1cb811951f..47c8b46edd 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -303,6 +303,32 @@ def get_user_by_email(client: Client) -> None: validate_against_openapi_schema(result, "/users/{email}", "get", "200") +@openapi_test_function("/invites:get") +def get_invitations(client: Client) -> None: + # {code_example|start} + # Get all invitations + result = client.call_endpoint(url="/invites", method="GET") + # {code_example|end} + + validate_against_openapi_schema(result, "/invites", "get", "200") + + +@openapi_test_function("/invites:post") +def send_invitations(client: Client) -> None: + # {code_example|start} + # Send invitations + request = { + "invitee_emails": "example@zulip.com, logan@zulip.com", + "invite_expires_in_minutes": 14400, + "invite_as": 400, + "stream_ids": [1, 8, 9], + } + result = client.call_endpoint(url="/invites", method="POST", request=request) + # {code_example|end} + + validate_against_openapi_schema(result, "/invites", "post", "200") + + @openapi_test_function("/users/{user_id}:get") def get_single_user(client: Client) -> None: ensure_users([8], ["cordelia"]) @@ -1658,6 +1684,11 @@ def test_errors(client: Client) -> None: test_invalid_stream_error(client) +def test_invitations(client: Client) -> None: + send_invitations(client) + get_invitations(client) + + def test_the_api(client: Client, nonadmin_client: Client, owner_client: Client) -> None: get_user_agent(client) test_users(client, owner_client) @@ -1666,6 +1697,7 @@ def test_the_api(client: Client, nonadmin_client: Client, owner_client: Client) test_queues(client) test_server_organizations(client) test_errors(client) + test_invitations(client) sys.stdout.flush() if REGISTERED_TEST_FUNCTIONS != CALLED_TEST_FUNCTIONS: diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index a43ac6d1b5..9ba052a0ae 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -11785,6 +11785,235 @@ paths: responses: "200": $ref: "#/components/responses/SimpleSuccess" + /invites: + get: + operationId: get-invites + summary: Get all invitations + tags: ["invites"] + description: | + Fetch all unexpired [invitations](/help/invite-new-users) (i.e. email + invitations and reusable invitation links) that can be managed by the user. + + Note that administrators can manage invitations that were created by other users. + + **Changes**: Prior to Zulip 8.0 (feature level 209), non-admin users could + only create email invitations, and therefore the response would never include + reusable invitation links for these users. + responses: + "200": + description: Success. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - additionalProperties: false + properties: + result: {} + msg: {} + ignored_parameters_unsupported: {} + invites: + type: array + description: | + An array of objects, each representing a single unexpired + [invitation](/help/invite-new-users). + items: + $ref: "#/components/schemas/Invite" + example: + { + "result": "success", + "msg": "", + "invites": + [ + { + email: "example@zulip.com", + expiry_date: null, + id: 1, + invited: 1710606654, + invited_as: 200, + invited_by_user_id: 9, + is_multiuse: false, + }, + { + expiry_date: 1711463862, + id: 1, + invited: 1710599862, + invited_as: 400, + invited_by_user_id: 9, + is_multiuse: true, + link_url: "http://localhost:9991/join/6i4324pzbtyvmwv5nwyghoof/", + }, + ], + } + post: + operationId: send-invites + summary: Send invitations + tags: ["invites"] + description: | + Send [invitations](/help/invite-new-users) to specified email addresses. + + **Changes**: In Zulip 6.0 (feature level 126), the `invite_expires_in_days` + parameter was removed and replaced by `invite_expires_in_minutes`. + + In Zulip 5.0 (feature level 117), added support for passing `null` as + the `invite_expires_in_days` parameter to request an invitation that never + expires. + + In Zulip 5.0 (feature level 96), the `invite_expires_in_days` parameter was + added which specified the number of days before the invitation would expire. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + invitee_emails: + description: | + The string containing the email addresses, separated by commas or + newlines, that will be sent an invitation. + type: string + example: example@zulip.com, logan@zulip.com + invite_expires_in_minutes: + description: | + The number of minutes before the invitation will expire. If `null`, the + invitation will never expire. If unspecified, the server will use a default + value (`INVITATION_LINK_VALIDITY_MINUTES`) for when the invitation will expire. + + **Changes**: New in Zulip 6.0 (feature level 126). Previously, there was an + `invite_expires_in_days` parameter, which specified the duration in days instead + of minutes. + type: integer + nullable: true + example: 14400 + invite_as: + description: | + The [organization-level role](/api/roles-and-permissions) of the user that is + created when the invitation is accepted. + + Users can only send invitations for + [roles with equal or stricter restrictions](/api/roles-and-permissions#permission-levels) + as their own. For example, a moderator cannot invite someone to be an owner + or administrator, but they can invite them to be a moderator or member. + + **Changes**: In Zulip 4.0 (feature level 61), added support for inviting + users as moderators. + type: integer + enum: + - 100 + - 200 + - 300 + - 400 + - 600 + default: 400 + example: 600 + stream_ids: + description: | + A list containing the [IDs of the streams](/api/get-stream-id) that the + newly created user will be automatically subscribed to if the invitation + is accepted. If the list is empty, then the new user will not be + subscribed to any streams. + + **Changes**: Before Zulip 7.0 (feature level 180), specifying `stream_ids` + as an empty list resulted in an error. + type: array + items: + type: integer + example: [1, 2] + required: + - invitee_emails + - stream_ids + encoding: + invite_expires_in_minutes: + contentType: application/json + stream_ids: + contentType: application/json + responses: + "200": + description: Success. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - additionalProperties: false + properties: + result: {} + msg: {} + ignored_parameters_unsupported: {} + example: {"msg": "", "result": "success"} + "400": + description: Bad request. + content: + application/json: + schema: + oneOf: + - allOf: + - $ref: "#/components/schemas/InvitationFailedError" + - example: + { + "result": "error", + "msg": "Some of those addresses are already using Zulip, so we didn't send them an invitation. We did send invitations to everyone else!", + "errors": + [ + [ + "hamlet@zulip.com", + "Already has an account.", + false, + ], + ], + "sent_invitations": true, + "license_limit_reached": false, + "daily_limit_reached": false, + "code": "INVITATION_FAILED", + } + description: | + An example JSON error response for when some of the specified email addresses + have existing Zulip accounts. + - allOf: + - $ref: "#/components/schemas/CodedError" + - example: + { + "code": "BAD_REQUEST", + "msg": "Insufficient permission", + "result": "error", + } + description: | + An example JSON error response for when the user doesn't have permission + to send invitations. + - allOf: + - $ref: "#/components/schemas/CodedError" + - example: + { + "code": "BAD_REQUEST", + "msg": "You must specify at least one email address.", + "result": "error", + } + description: | + An example JSON error response for when no email address is specified. + - allOf: + - $ref: "#/components/schemas/CodedError" + - example: + { + "code": "BAD_REQUEST", + "msg": "Stream does not exist with id: 11. No invites were sent.", + "result": "error", + } + description: | + An example JSON error response for when any of the specified streams + does not exist or the user does not have permission to access one of + the targeted streams. + - allOf: + - $ref: "#/components/schemas/CodedError" + - example: + { + "code": "BAD_REQUEST", + "msg": "You do not have permission to subscribe other users to streams.", + "result": "error", + } + description: | + An example JSON error response for when the user doesn't have permission + to subscribe other users to streams and `stream_ids` is not empty. /register: post: operationId: register-queue @@ -19315,6 +19544,60 @@ components: New in Zulip 8.0 (feature level 191). Previously, groups could be mentioned if and only if they were not system groups. + Invite: + type: object + description: | + A dictionary containing details about an [invitation](/help/invite-new-users). + additionalProperties: false + properties: + id: + type: integer + description: | + The unique ID of the invitation. + invited_by_user_id: + type: integer + description: | + The [user ID](/api/get-user) of the user who created the invitation. + + **Changes**: New in Zulip 3.0 (feature level 22), replacing the `ref` + field which contained the Zulip display email address of the user who + created the invitation. + invited: + type: integer + description: | + The UNIX timestamp for when the invitation was created, in UTC seconds. + expiry_date: + type: integer + nullable: true + description: | + The UNIX timestamp for when the invitation will expire, in UTC seconds. + If `null`, the invitation never expires. + invited_as: + type: integer + enum: + - 100 + - 200 + - 300 + - 400 + - 600 + description: | + The [organization-level role](/api/roles-and-permissions) of the user that + is created when the invitation is accepted. + email: + type: string + description: | + The email address the invitation was sent to. This will not be present when + `is_multiuse` is `true` (i.e. the invitation is a reusable invitation link). + link_url: + type: string + description: | + The URL of the reusable invitation link. This will not be present when + `is_multiuse` is `false` (i.e. the invitation is an email invitation). + is_multiuse: + type: boolean + description: | + A boolean specifying whether the [invitation](/help/invite-new-users) is a + reusable invitation link or an email invitation. Subscriptions: type: object additionalProperties: false @@ -20682,6 +20965,41 @@ components: ## Invalid API key A typical failed JSON response for when the API key is invalid: + InvitationFailedError: + allOf: + - $ref: "#/components/schemas/CodedErrorBase" + - additionalProperties: false + properties: + result: {} + msg: {} + code: {} + errors: + type: array + items: + type: array + items: + oneOf: + - type: string + - type: boolean + description: | + An array of arrays of length 3, where each inner array consists of (a) an email + address that was skipped while sending invitations, (b) the corresponding error + message, and (c) a boolean which is `true` when the email address already uses Zulip + and the corresponding user is deactivated in the organization. + sent_invitations: + description: | + A boolean specifying whether any invitations were sent. + type: boolean + daily_limit_reached: + type: boolean + description: | + A boolean specifying whether the limit on the number of invitations that can + be sent in the organization in a day has been reached. + license_limit_reached: + type: boolean + description: | + A boolean specifying whether the organization have enough unused Zulip licenses + to invite specified number of users. MissingArgumentError: allOf: - $ref: "#/components/schemas/CodedErrorBase" diff --git a/zerver/tests/test_openapi.py b/zerver/tests/test_openapi.py index d15d3eb97b..233b2f84df 100644 --- a/zerver/tests/test_openapi.py +++ b/zerver/tests/test_openapi.py @@ -239,7 +239,6 @@ class OpenAPIArgumentsTest(ZulipTestCase): "/default_stream_groups/{group_id}", "/default_stream_groups/{group_id}/streams", # Administer invitations - "/invites", "/invites/multiuse", "/invites/{prereg_id}", "/invites/{prereg_id}/resend", @@ -1023,6 +1022,7 @@ class OpenAPIAttributesTest(ZulipTestCase): "webhooks", "scheduled_messages", "mobile", + "invites", ] paths = OpenAPISpec(OPENAPI_SPEC_PATH).openapi()["paths"] for path, path_item in paths.items():