mirror of https://github.com/zulip/zulip.git
api: Add REST API endpoint for looking up a user by email address.
Add new rest api endpoint GET users/{email} for looking up a user by email, which is useful especially for corporate API applications that might already have a user's email address. Fixes #14302.
This commit is contained in:
parent
1212083218
commit
dfafdda9b3
|
@ -10,6 +10,10 @@ below features are supported.
|
||||||
|
|
||||||
## Changes in Zulip 4.0
|
## Changes in Zulip 4.0
|
||||||
|
|
||||||
|
**Feature level 39**
|
||||||
|
|
||||||
|
* Added new [GET /users/{email}](/api/get-user-by-email) endpoint.
|
||||||
|
|
||||||
**Feature level 38**
|
**Feature level 38**
|
||||||
|
|
||||||
* [`POST /register`](/api/register-queue): Increased
|
* [`POST /register`](/api/register-queue): Increased
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Get a user by email
|
||||||
|
|
||||||
|
{generate_api_description(/users/{email}:get)}
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
{start_tabs}
|
||||||
|
{tab|python}
|
||||||
|
|
||||||
|
{generate_code_example(python)|/users/{email}:get|example}
|
||||||
|
|
||||||
|
{tab|curl}
|
||||||
|
|
||||||
|
{generate_code_example(curl, include=[""])|/users/{email}:get|example}
|
||||||
|
|
||||||
|
You may pass the `client_gravatar` or `include_custom_profile_fields` query parameter as follows:
|
||||||
|
|
||||||
|
{generate_code_example(curl)|/users/{email}:get|example}
|
||||||
|
|
||||||
|
{end_tabs}
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
**Note**: The following parameters are all URL query parameters.
|
||||||
|
|
||||||
|
{generate_api_arguments_table|zulip.yaml|/users/{email}:get}
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
#### Return values
|
||||||
|
|
||||||
|
{generate_return_values_table|zulip.yaml}|/users/{email}:get}
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
A typical successful JSON response may look like:
|
||||||
|
|
||||||
|
{generate_code_example|/users/{email}:get|fixture(200)}
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
{generate_api_description(/users:get)}
|
{generate_api_description(/users:get)}
|
||||||
|
|
||||||
You can also [fetch details on a single user](/api/get-user).
|
|
||||||
|
|
||||||
## Usage examples
|
## Usage examples
|
||||||
|
|
||||||
{start_tabs}
|
{start_tabs}
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
* [Get all users](/api/get-users)
|
* [Get all users](/api/get-users)
|
||||||
* [Get own user](/api/get-own-user)
|
* [Get own user](/api/get-own-user)
|
||||||
* [Get a user](/api/get-user)
|
* [Get a user](/api/get-user)
|
||||||
|
* [Get a user by email](/api/get-user-by-email)
|
||||||
* [Update a user](/api/update-user)
|
* [Update a user](/api/update-user)
|
||||||
* [Create a user](/api/create-user)
|
* [Create a user](/api/create-user)
|
||||||
* [Deactivate a user](/api/deactivate-user)
|
* [Deactivate a user](/api/deactivate-user)
|
||||||
|
|
|
@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
|
||||||
#
|
#
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in templates/zerver/api/changelog.md.
|
# new level means in templates/zerver/api/changelog.md.
|
||||||
API_FEATURE_LEVEL = 38
|
API_FEATURE_LEVEL = 39
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# 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
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -222,6 +222,20 @@ def get_members(client: Client) -> None:
|
||||||
assert member.get("profile_data", None) is not None
|
assert member.get("profile_data", None) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@openapi_test_function("/users/{email}:get")
|
||||||
|
def get_user_by_email(client: Client) -> None:
|
||||||
|
|
||||||
|
# {code_example|start}
|
||||||
|
# Fetch details on a user given a user ID
|
||||||
|
email = "iago@zulip.com"
|
||||||
|
result = client.call_endpoint(
|
||||||
|
url=f"/users/{email}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
# {code_example|end}
|
||||||
|
validate_against_openapi_schema(result, "/users/{email}", "get", "200")
|
||||||
|
|
||||||
|
|
||||||
@openapi_test_function("/users/{user_id}:get")
|
@openapi_test_function("/users/{user_id}:get")
|
||||||
def get_single_user(client: Client) -> None:
|
def get_single_user(client: Client) -> None:
|
||||||
|
|
||||||
|
@ -1287,6 +1301,7 @@ def test_users(client: Client) -> None:
|
||||||
deactivate_user(client)
|
deactivate_user(client)
|
||||||
reactivate_user(client)
|
reactivate_user(client)
|
||||||
update_user(client)
|
update_user(client)
|
||||||
|
get_user_by_email(client)
|
||||||
get_subscription_status(client)
|
get_subscription_status(client)
|
||||||
get_profile(client)
|
get_profile(client)
|
||||||
update_notification_settings(client)
|
update_notification_settings(client)
|
||||||
|
|
|
@ -4027,6 +4027,8 @@ paths:
|
||||||
includes values of [custom profile field](/help/add-custom-profile-fields).
|
includes values of [custom profile field](/help/add-custom-profile-fields).
|
||||||
|
|
||||||
`GET {{ api_url }}/v1/users`
|
`GET {{ api_url }}/v1/users`
|
||||||
|
|
||||||
|
You can also [fetch details on a single user](/api/get-user).
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/ClientGravatar"
|
- $ref: "#/components/parameters/ClientGravatar"
|
||||||
- $ref: "#/components/parameters/IncludeCustomProfileFields"
|
- $ref: "#/components/parameters/IncludeCustomProfileFields"
|
||||||
|
@ -5450,6 +5452,94 @@ paths:
|
||||||
"result": "success",
|
"result": "success",
|
||||||
"msg": "",
|
"msg": "",
|
||||||
}
|
}
|
||||||
|
/users/{email}:
|
||||||
|
get:
|
||||||
|
operationId: get_user_by_email
|
||||||
|
tags: ["users"]
|
||||||
|
description: |
|
||||||
|
Fetch details for a single user in the organization given a Zulip display
|
||||||
|
email address.
|
||||||
|
|
||||||
|
`GET {{ api_url }}/v1/users/{email}`
|
||||||
|
|
||||||
|
Note that this endpoint uses Zulip display emails addresses
|
||||||
|
for organizations that have configured limited [email address
|
||||||
|
visibility](/help/restrict-visibility-of-email-addresses).
|
||||||
|
|
||||||
|
You can also fetch details on [all users in the organization](/api/get-users) or
|
||||||
|
[by user ID](/api/get-user). Fetching by user ID is generally recommended
|
||||||
|
when possible, as users can
|
||||||
|
[change their email address](/help/change-your-email-address).
|
||||||
|
|
||||||
|
*This endpoint is new in Zulip Server 4.0 (feature level 39).*
|
||||||
|
parameters:
|
||||||
|
- name: email
|
||||||
|
in: path
|
||||||
|
description: |
|
||||||
|
The email address of the user whose details you want to fetch.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: iago@zulip.com
|
||||||
|
required: true
|
||||||
|
- $ref: "#/components/parameters/ClientGravatar"
|
||||||
|
- $ref: "#/components/parameters/IncludeCustomProfileFields"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Success.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/JsonSuccessBase"
|
||||||
|
- additionalProperties: false
|
||||||
|
properties:
|
||||||
|
result: {}
|
||||||
|
msg: {}
|
||||||
|
user:
|
||||||
|
$ref: "#/components/schemas/User"
|
||||||
|
example:
|
||||||
|
{
|
||||||
|
"msg": "",
|
||||||
|
"result": "success",
|
||||||
|
"user":
|
||||||
|
{
|
||||||
|
"date_joined": "2019-10-20T07:50:53.729659+00:00",
|
||||||
|
"full_name": "King Hamlet",
|
||||||
|
"is_guest": false,
|
||||||
|
"profile_data":
|
||||||
|
{
|
||||||
|
"4": {"value": "vim"},
|
||||||
|
"2":
|
||||||
|
{
|
||||||
|
"value": "I am:\n* The prince of Denmark\n* Nephew to the usurping Claudius",
|
||||||
|
"rendered_value": "<p>I am:</p>\n<ul>\n<li>The prince of Denmark</li>\n<li>Nephew to the usurping Claudius</li>\n</ul>",
|
||||||
|
},
|
||||||
|
"5": {"value": "1900-01-01"},
|
||||||
|
"7": {"value": "[11]"},
|
||||||
|
"6": {"value": "https://blog.zulig.org"},
|
||||||
|
"1":
|
||||||
|
{
|
||||||
|
"value": "+0-11-23-456-7890",
|
||||||
|
"rendered_value": "<p>+0-11-23-456-7890</p>",
|
||||||
|
},
|
||||||
|
"8": {"value": "zulipbot"},
|
||||||
|
"3":
|
||||||
|
{
|
||||||
|
"rendered_value": "<p>Dark chocolate</p>",
|
||||||
|
"value": "Dark chocolate",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_id": 10,
|
||||||
|
"is_bot": false,
|
||||||
|
"bot_type": null,
|
||||||
|
"timezone": "",
|
||||||
|
"is_admin": false,
|
||||||
|
"is_owner": false,
|
||||||
|
"avatar_url": "https://secure.gravatar.com/avatar/6d8cad0fd00256e7b40691d27ddfd466?d=identicon&version=1",
|
||||||
|
"is_active": true,
|
||||||
|
"email": "hamlet@zulip.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
/users/{user_id}:
|
/users/{user_id}:
|
||||||
get:
|
get:
|
||||||
operationId: get_user
|
operationId: get_user
|
||||||
|
@ -5459,7 +5549,8 @@ paths:
|
||||||
|
|
||||||
`GET {{ api_url }}/v1/users/{user_id}`
|
`GET {{ api_url }}/v1/users/{user_id}`
|
||||||
|
|
||||||
You can also fetch details on [all users in the organization](/api/get-users).
|
You can also fetch details on [all users in the organization](/api/get-users)
|
||||||
|
or [by email](/api/get-user-by-email).
|
||||||
|
|
||||||
*This endpoint is new in Zulip Server 3.0 (feature level 1).*
|
*This endpoint is new in Zulip Server 3.0 (feature level 1).*
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
@ -1064,6 +1064,7 @@ class OpenAPIRegexTest(ZulipTestCase):
|
||||||
== "/users/{user_id}/subscriptions/{stream_id}"
|
== "/users/{user_id}/subscriptions/{stream_id}"
|
||||||
)
|
)
|
||||||
assert find_openapi_endpoint("/users/iago@zulip.com/presence") == "/users/{email}/presence"
|
assert find_openapi_endpoint("/users/iago@zulip.com/presence") == "/users/{email}/presence"
|
||||||
|
assert find_openapi_endpoint("/users/iago@zulip.com") == "/users/{email}"
|
||||||
assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}"
|
assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}"
|
||||||
assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}"
|
assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}"
|
||||||
|
|
||||||
|
|
|
@ -1815,6 +1815,35 @@ class GetProfileTest(ZulipTestCase):
|
||||||
self.assertEqual(result["user"]["email"], bot.email)
|
self.assertEqual(result["user"]["email"], bot.email)
|
||||||
self.assertTrue(result["user"]["is_bot"])
|
self.assertTrue(result["user"]["is_bot"])
|
||||||
|
|
||||||
|
def test_get_user_by_email(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login("hamlet")
|
||||||
|
result = orjson.loads(self.client_get(f"/json/users/{user.email}").content)
|
||||||
|
|
||||||
|
self.assertEqual(result["user"]["email"], user.email)
|
||||||
|
|
||||||
|
self.assertEqual(result["user"]["full_name"], user.full_name)
|
||||||
|
self.assertIn("user_id", result["user"])
|
||||||
|
self.assertNotIn("profile_data", result["user"])
|
||||||
|
self.assertFalse(result["user"]["is_bot"])
|
||||||
|
self.assertFalse(result["user"]["is_admin"])
|
||||||
|
self.assertFalse(result["user"]["is_owner"])
|
||||||
|
|
||||||
|
result = orjson.loads(
|
||||||
|
self.client_get(
|
||||||
|
f"/json/users/{user.email}", {"include_custom_profile_fields": "true"}
|
||||||
|
).content
|
||||||
|
)
|
||||||
|
self.assertIn("profile_data", result["user"])
|
||||||
|
|
||||||
|
result = self.client_get("/json/users/invalid")
|
||||||
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
|
bot = self.example_user("default_bot")
|
||||||
|
result = orjson.loads(self.client_get(f"/json/users/{bot.email}").content)
|
||||||
|
self.assertEqual(result["user"]["email"], bot.email)
|
||||||
|
self.assertTrue(result["user"]["is_bot"])
|
||||||
|
|
||||||
def test_get_all_profiles_avatar_urls(self) -> None:
|
def test_get_all_profiles_avatar_urls(self) -> None:
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
result = self.api_get(hamlet, "/api/v1/users")
|
result = self.api_get(hamlet, "/api/v1/users")
|
||||||
|
|
|
@ -31,7 +31,7 @@ from zerver.lib.bot_config import set_bot_config
|
||||||
from zerver.lib.email_validation import email_allowed_for_realm
|
from zerver.lib.email_validation import email_allowed_for_realm
|
||||||
from zerver.lib.exceptions import CannotDeactivateLastUserError, OrganizationOwnerRequired
|
from zerver.lib.exceptions import CannotDeactivateLastUserError, OrganizationOwnerRequired
|
||||||
from zerver.lib.integrations import EMBEDDED_BOTS
|
from zerver.lib.integrations import EMBEDDED_BOTS
|
||||||
from zerver.lib.request import REQ, has_request_variables
|
from zerver.lib.request import REQ, JsonableError, has_request_variables
|
||||||
from zerver.lib.response import json_error, json_success
|
from zerver.lib.response import json_error, json_success
|
||||||
from zerver.lib.streams import access_stream_by_id, access_stream_by_name, subscribed_to_stream
|
from zerver.lib.streams import access_stream_by_id, access_stream_by_name, subscribed_to_stream
|
||||||
from zerver.lib.types import Validator
|
from zerver.lib.types import Validator
|
||||||
|
@ -75,6 +75,7 @@ from zerver.models import (
|
||||||
Service,
|
Service,
|
||||||
Stream,
|
Stream,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
get_user,
|
||||||
get_user_by_delivery_email,
|
get_user_by_delivery_email,
|
||||||
get_user_by_id_in_realm_including_cross_realm,
|
get_user_by_id_in_realm_including_cross_realm,
|
||||||
get_user_including_cross_realm,
|
get_user_including_cross_realm,
|
||||||
|
@ -637,3 +638,23 @@ def get_subscription_backend(
|
||||||
subscription_status = {"is_subscribed": subscribed_to_stream(target_user, stream_id)}
|
subscription_status = {"is_subscribed": subscribed_to_stream(target_user, stream_id)}
|
||||||
|
|
||||||
return json_success(subscription_status)
|
return json_success(subscription_status)
|
||||||
|
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def get_user_by_email(
|
||||||
|
request: HttpRequest,
|
||||||
|
user_profile: UserProfile,
|
||||||
|
email: str,
|
||||||
|
include_custom_profile_fields: bool = REQ(validator=check_bool, default=False),
|
||||||
|
client_gravatar: bool = REQ(validator=check_bool, default=False),
|
||||||
|
) -> HttpResponse:
|
||||||
|
realm = user_profile.realm
|
||||||
|
|
||||||
|
target_user = None
|
||||||
|
if email is not None:
|
||||||
|
try:
|
||||||
|
target_user = get_user(email, realm)
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
raise JsonableError(_("No such user"))
|
||||||
|
|
||||||
|
return get_members_backend(request, user_profile, user_id=target_user.id)
|
||||||
|
|
|
@ -199,6 +199,7 @@ from zerver.views.users import (
|
||||||
get_members_backend,
|
get_members_backend,
|
||||||
get_profile_backend,
|
get_profile_backend,
|
||||||
get_subscription_backend,
|
get_subscription_backend,
|
||||||
|
get_user_by_email,
|
||||||
patch_bot_backend,
|
patch_bot_backend,
|
||||||
reactivate_user_backend,
|
reactivate_user_backend,
|
||||||
regenerate_bot_api_key,
|
regenerate_bot_api_key,
|
||||||
|
@ -292,6 +293,7 @@ v1_api_and_json_patterns = [
|
||||||
DELETE=deactivate_user_backend,
|
DELETE=deactivate_user_backend,
|
||||||
),
|
),
|
||||||
rest_path("users/<int:user_id>/subscriptions/<int:stream_id>", GET=get_subscription_backend),
|
rest_path("users/<int:user_id>/subscriptions/<int:stream_id>", GET=get_subscription_backend),
|
||||||
|
rest_path("users/<email>", GET=get_user_by_email),
|
||||||
rest_path("bots", GET=get_bots_backend, POST=add_bot_backend),
|
rest_path("bots", GET=get_bots_backend, POST=add_bot_backend),
|
||||||
rest_path("bots/<int:bot_id>/api_key/regenerate", POST=regenerate_bot_api_key),
|
rest_path("bots/<int:bot_id>/api_key/regenerate", POST=regenerate_bot_api_key),
|
||||||
rest_path("bots/<int:bot_id>", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend),
|
rest_path("bots/<int:bot_id>", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend),
|
||||||
|
|
Loading…
Reference in New Issue