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:
Tushar912 2021-01-02 19:35:29 +05:30 committed by Tim Abbott
parent 1212083218
commit dfafdda9b3
11 changed files with 205 additions and 5 deletions

View File

@ -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

View File

@ -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)}

View File

@ -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}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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}"

View File

@ -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")

View File

@ -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)

View File

@ -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),