Skip to content

Commit b68598e

Browse files
committed
api: Add bindings for fetching user avatar.
1 parent b172347 commit b68598e

2 files changed

Lines changed: 82 additions & 6 deletions

File tree

zulip/zulip/__init__.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646

4747
API_VERSTRING = "v1/"
4848

49+
# Endpoints that respond with an HTTP 302 redirect to a resource (e.g. the
50+
# avatar endpoints redirect to the actual avatar image) rather than a JSON
51+
# body.
52+
REDIRECT_ENDPOINTS = ["avatar/"]
53+
4954
# An optional parameter to `move_topic` and `update_message` actions
5055
# See eg. https://zulip.com/api/update-message#parameter-propagate_mode
5156
EditPropagateMode = Literal["change_one", "change_all", "change_later"]
@@ -583,6 +588,7 @@ def do_api_query(
583588
longpolling: bool = False,
584589
files: Optional[List[IO[Any]]] = None,
585590
timeout: Optional[float] = None,
591+
non_api_or_json_url: bool = False,
586592
) -> Dict[str, Any]:
587593
if files is None:
588594
files = []
@@ -636,6 +642,8 @@ def end_error_retry(succeeded: bool) -> None:
636642
else:
637643
print("Failed!")
638644

645+
base_url = self.base_url.removesuffix("api/") if non_api_or_json_url else self.base_url
646+
is_redirect_endpoint = any(url.startswith(prefix) for prefix in REDIRECT_ENDPOINTS)
639647
while True:
640648
try:
641649
kwarg = "params" if method == "GET" else "data"
@@ -648,8 +656,9 @@ def end_error_retry(succeeded: bool) -> None:
648656
# Actually make the request!
649657
res = self.session.request(
650658
method,
651-
urllib.parse.urljoin(self.base_url, url),
659+
urllib.parse.urljoin(base_url, url),
652660
timeout=request_timeout,
661+
allow_redirects=not is_redirect_endpoint,
653662
**kwargs,
654663
)
655664

@@ -683,9 +692,7 @@ def end_error_retry(succeeded: bool) -> None:
683692
# go into retry logic, because the most likely scenario here is
684693
# that somebody just hasn't started their server, or they passed
685694
# in an invalid site.
686-
raise UnrecoverableNetworkError(
687-
"cannot connect to server " + self.base_url
688-
) from e
695+
raise UnrecoverableNetworkError("cannot connect to server " + base_url) from e
689696

690697
if error_retry(""):
691698
continue
@@ -695,6 +702,17 @@ def end_error_retry(succeeded: bool) -> None:
695702
# We'll split this out into more cases as we encounter new bugs.
696703
raise
697704

705+
# This is not a real object these endpoints would return. Redirect endpoints
706+
# have no JSON body; the resource URL lives in the "Location" header of the
707+
# 3xx response.
708+
if is_redirect_endpoint and res.is_redirect:
709+
end_error_retry(True)
710+
return {
711+
"result": "success",
712+
"msg": "",
713+
"url": res.headers.get("Location"),
714+
}
715+
698716
try:
699717
json_result = res.json()
700718
except Exception:
@@ -716,21 +734,23 @@ def call_endpoint(
716734
longpolling: bool = False,
717735
files: Optional[List[IO[Any]]] = None,
718736
timeout: Optional[float] = None,
737+
non_api_or_json_url: bool = False,
719738
) -> Dict[str, Any]:
720739
if request is None:
721740
request = dict()
722741
marshalled_request = {}
723742
for k, v in request.items():
724743
if v is not None:
725744
marshalled_request[k] = v
726-
versioned_url = API_VERSTRING + (url if url is not None else "")
745+
url = url or ""
727746
return self.do_api_query(
728747
marshalled_request,
729-
versioned_url,
748+
url=url if non_api_or_json_url else API_VERSTRING + url,
730749
method=method,
731750
longpolling=longpolling,
732751
files=files,
733752
timeout=timeout,
753+
non_api_or_json_url=non_api_or_json_url,
734754
)
735755

736756
def call_on_each_event(
@@ -1771,6 +1791,38 @@ def move_topic(
17711791
request=request,
17721792
)
17731793

1794+
def get_avatar_url_by_id(self, user_id: int, medium: bool = False) -> str:
1795+
"""
1796+
If `medium=False`, the default size will be returned. Avatar sizes:
1797+
- default `100x100` px
1798+
- medium `500x500` px
1799+
1800+
Example usage:
1801+
1802+
>>> client.get_avatar_by_id(user_id=8, medium=True)
1803+
"""
1804+
url = f"avatar/{user_id}"
1805+
if medium:
1806+
url += "/medium"
1807+
response = self.call_endpoint(url=url, method="GET", non_api_or_json_url=True)
1808+
return response["url"]
1809+
1810+
def get_avatar_url_by_email(self, email: str, medium: bool = False) -> str:
1811+
"""
1812+
If `medium=False`, the default size will be returned. Avatar sizes:
1813+
- default `100x100` px
1814+
- medium `500x500` px
1815+
1816+
Example usage:
1817+
1818+
>>> client.get_avatar_by_email(email="hamlet@zulip.com", medium=True)
1819+
"""
1820+
url = f"avatar/{email}"
1821+
if medium:
1822+
url += "/medium"
1823+
response = self.call_endpoint(url=url, method="GET", non_api_or_json_url=True)
1824+
return response["url"]
1825+
17741826

17751827
class ZulipStream:
17761828
"""
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
5+
usage = """get-user-avatar --user_id=<user_id> --email=<email address> [options]
6+
7+
Get the avatar URL for a user.
8+
"""
9+
10+
import zulip
11+
12+
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
13+
parser.add_argument("--user_id", required=True)
14+
parser.add_argument("--email", required=True)
15+
options = parser.parse_args()
16+
17+
client = zulip.init_from_options(options)
18+
19+
print("Get user avatar by ID:")
20+
print(client.get_avatar_url_by_id(user_id=options.user_id))
21+
print(client.get_avatar_url_by_id(user_id=options.user_id, medium=True))
22+
print("Get user avatar by email:")
23+
print(client.get_avatar_url_by_email(email=options.email))
24+
print(client.get_avatar_url_by_email(email=options.email, medium=True))

0 commit comments

Comments
 (0)