Skip to content

Commit f109a2f

Browse files
authored
Fix #769 by supporting prefix/suffix for User-Agent (#773)
1 parent 0a3eeb8 commit f109a2f

13 files changed

Lines changed: 134 additions & 14 deletions

slack/web/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import platform
22
import sys
3-
from typing import Dict
3+
from typing import Dict, Optional
44

55
import slack.version as slack_version
66

@@ -26,7 +26,7 @@ def convert_bool_to_0_or_1(params: Dict[str, any]) -> Dict[str, any]:
2626
return None
2727

2828

29-
def get_user_agent():
29+
def get_user_agent(prefix: Optional[str] = None, suffix: Optional[str] = None):
3030
"""Construct the user-agent header with the package info,
3131
Python version and OS version.
3232
@@ -39,4 +39,6 @@ def get_user_agent():
3939
python_version = "Python/{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info)
4040
system_info = "{0}/{1}".format(platform.system(), platform.release())
4141
user_agent_string = " ".join([python_version, client, system_info])
42-
return user_agent_string
42+
prefix = f"{prefix} " if prefix else ""
43+
suffix = f" {suffix}" if suffix else ""
44+
return prefix + user_agent_string + suffix

slack/web/async_base_client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import aiohttp
66
from aiohttp import FormData
77

8-
from slack.web import convert_bool_to_0_or_1
8+
from slack.web import convert_bool_to_0_or_1, get_user_agent
99
from slack.web.async_internal_utils import (
1010
_build_req_args,
1111
_get_url,
@@ -29,6 +29,8 @@ def __init__(
2929
session: Optional[aiohttp.ClientSession] = None,
3030
trust_env_in_session: bool = False,
3131
headers: Optional[dict] = None,
32+
user_agent_prefix: Optional[str] = None,
33+
user_agent_suffix: Optional[str] = None,
3234
):
3335
self.token = None if token is None else token.strip()
3436
self.base_url = base_url
@@ -39,6 +41,9 @@ def __init__(
3941
# https://github.com/slackapi/python-slackclient/issues/738
4042
self.trust_env_in_session = trust_env_in_session
4143
self.headers = headers or {}
44+
self.headers["User-Agent"] = get_user_agent(
45+
user_agent_prefix, user_agent_suffix
46+
)
4247
self._logger = logging.getLogger(__name__)
4348

4449
async def api_call( # skipcq: PYL-R1710
@@ -87,6 +92,9 @@ async def api_call( # skipcq: PYL-R1710
8792
"""
8893

8994
api_url = _get_url(self.base_url, api_method)
95+
headers = headers or {}
96+
headers.update(self.headers)
97+
9098
req_args = _build_req_args(
9199
token=self.token,
92100
http_verb=http_verb,

slack/web/async_internal_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ def _get_headers(
6060
}
6161
"""
6262
final_headers = {
63-
"User-Agent": get_user_agent(),
6463
"Content-Type": "application/x-www-form-urlencoded",
6564
}
65+
if headers is None or "User-Agent" not in headers:
66+
final_headers["User-Agent"] = get_user_agent()
6667

6768
if token:
6869
final_headers.update({"Authorization": "Bearer {}".format(token)})

slack/web/base_client.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ def __init__(
5151
use_sync_aiohttp: bool = False,
5252
session: Optional[aiohttp.ClientSession] = None,
5353
headers: Optional[dict] = None,
54+
user_agent_prefix: Optional[str] = None,
55+
user_agent_suffix: Optional[str] = None,
5456
):
5557
self.token = None if token is None else token.strip()
5658
self.base_url = base_url
@@ -61,6 +63,9 @@ def __init__(
6163
self.use_sync_aiohttp = use_sync_aiohttp
6264
self.session = session
6365
self.headers = headers or {}
66+
self.headers["User-Agent"] = get_user_agent(
67+
user_agent_prefix, user_agent_suffix
68+
)
6469
self._logger = logging.getLogger(__name__)
6570
self._event_loop = loop
6671

@@ -110,6 +115,9 @@ def api_call( # skipcq: PYL-R1710
110115
"""
111116

112117
api_url = _get_url(self.base_url, api_method)
118+
headers = headers or {}
119+
headers.update(self.headers)
120+
113121
req_args = _build_req_args(
114122
token=self.token,
115123
http_verb=http_verb,
@@ -483,10 +491,7 @@ def _perform_urllib_http_request(
483491
def _build_urllib_request_headers(
484492
self, token: str, has_json: bool, has_files: bool, additional_headers: dict
485493
) -> Dict[str, str]:
486-
headers = {
487-
"User-Agent": get_user_agent(),
488-
"Content-Type": "application/x-www-form-urlencoded",
489-
}
494+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
490495
headers.update(self.headers)
491496
if token:
492497
headers.update({"Authorization": "Bearer {}".format(token)})

slack/webhook/async_client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from slack.errors import SlackApiError
1010
from .internal_utils import _debug_log_response, _build_request_headers, _build_body
1111
from .webhook_response import WebhookResponse
12+
from ..web import get_user_agent
1213
from ..web.classes.attachments import Attachment
1314
from ..web.classes.blocks import Block
1415

@@ -26,6 +27,8 @@ def __init__(
2627
trust_env_in_session: bool = False,
2728
auth: Optional[BasicAuth] = None,
2829
default_headers: Optional[Dict[str, str]] = None,
30+
user_agent_prefix: Optional[str] = None,
31+
user_agent_suffix: Optional[str] = None,
2932
):
3033
"""API client for Incoming Webhooks and response_url
3134
:param url: a complete URL to send data (e.g., https://hooks.slack.com/XXX)
@@ -36,6 +39,8 @@ def __init__(
3639
:param trust_env_in_session: True/False for aiohttp.ClientSession
3740
:param auth: Basic auth info for aiohttp.ClientSession
3841
:param default_headers: request headers to add to all requests
42+
:param user_agent_prefix: prefix for User-Agent header value
43+
:param user_agent_suffix: suffix for User-Agent header value
3944
"""
4045
self.url = url
4146
self.timeout = timeout
@@ -45,6 +50,9 @@ def __init__(
4550
self.session = session
4651
self.auth = auth
4752
self.default_headers = default_headers if default_headers else {}
53+
self.default_headers["User-Agent"] = get_user_agent(
54+
user_agent_prefix, user_agent_suffix
55+
)
4856

4957
async def send(
5058
self,

slack/webhook/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from slack.errors import SlackRequestError
1111
from .internal_utils import _build_body, _build_request_headers, _debug_log_response
1212
from .webhook_response import WebhookResponse
13+
from ..web import get_user_agent
1314
from ..web.classes.attachments import Attachment
1415
from ..web.classes.blocks import Block
1516

@@ -24,19 +25,26 @@ def __init__(
2425
ssl: Optional[SSLContext] = None,
2526
proxy: Optional[str] = None,
2627
default_headers: Optional[Dict[str, str]] = None,
28+
user_agent_prefix: Optional[str] = None,
29+
user_agent_suffix: Optional[str] = None,
2730
):
2831
"""API client for Incoming Webhooks and response_url
2932
:param url: a complete URL to send data (e.g., https://hooks.slack.com/XXX)
3033
:param timeout: request timeout (in seconds)
3134
:param ssl: ssl.SSLContext to use for requests
3235
:param proxy: proxy URL (e.g., localhost:9000, http://localhost:9000)
3336
:param default_headers: request headers to add to all requests
37+
:param user_agent_prefix: prefix for User-Agent header value
38+
:param user_agent_suffix: suffix for User-Agent header value
3439
"""
3540
self.url = url
3641
self.timeout = timeout
3742
self.ssl = ssl
3843
self.proxy = proxy
3944
self.default_headers = default_headers if default_headers else {}
45+
self.default_headers["User-Agent"] = get_user_agent(
46+
user_agent_prefix, user_agent_suffix
47+
)
4048

4149
def send(
4250
self,

slack/webhook/internal_utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ def _build_body(original_body: Dict[str, any]) -> Dict[str, any]:
1414

1515

1616
def _build_request_headers(
17-
default_headers, additional_headers: Optional[Dict[str, str]],
17+
default_headers: Dict[str, str], additional_headers: Optional[Dict[str, str]],
1818
) -> Dict[str, str]:
19-
if additional_headers is None:
19+
if default_headers is None and additional_headers is None:
2020
return {}
2121

2222
request_headers = {
23-
"User-Agent": get_user_agent(),
2423
"Content-Type": "application/json;charset=utf-8",
2524
}
25+
if default_headers is None or "User-Agent" not in default_headers:
26+
request_headers["User-Agent"] = get_user_agent()
27+
2628
request_headers.update(default_headers)
2729
if additional_headers:
2830
request_headers.update(additional_headers)

tests/web/mock_web_api_server.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ def _handle(self):
107107
self.wfile.close()
108108
return
109109

110+
if pattern.startswith("user-agent"):
111+
elements = pattern.split(" ")
112+
prefix, suffix = elements[1], elements[-1]
113+
ua: str = self.headers["User-Agent"]
114+
if ua.startswith(prefix) and ua.endswith(suffix):
115+
self.send_response(200)
116+
self.set_common_headers()
117+
self.wfile.write("""{"ok":true}""".encode("utf-8"))
118+
self.wfile.close()
119+
return
120+
else:
121+
self.send_response(400)
122+
self.set_common_headers()
123+
self.wfile.write("""{"ok":false, "error":"invalid_user_agent"}""".encode("utf-8"))
124+
self.wfile.close()
125+
return
126+
110127
if request_body and "cursor" in request_body:
111128
page = request_body["cursor"]
112129
pattern = f"{pattern}_{page}"

tests/web/test_async_web_client.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import re
22
import unittest
33

4-
import aiohttp
5-
64
import slack.errors as err
75
from slack import AsyncWebClient
86
from tests.helpers import async_test
@@ -130,3 +128,14 @@ async def test_html_response_body_issue_718_async(self):
130128
except err.SlackApiError as e:
131129
self.assertTrue(
132130
str(e).startswith("Failed to parse the response body: Expecting value: line 1 column 1 (char 0)"), e)
131+
132+
@async_test
133+
async def test_user_agent_customization_issue_769_async(self):
134+
client = AsyncWebClient(
135+
token="xoxb-user-agent this_is test",
136+
base_url="http://localhost:8888",
137+
user_agent_prefix="this_is",
138+
user_agent_suffix="test",
139+
)
140+
resp = await client.api_test()
141+
self.assertTrue(resp["ok"])

tests/web/test_web_client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,25 @@ async def test_html_response_body_issue_718_async(self):
263263
except err.SlackApiError as e:
264264
self.assertTrue(
265265
str(e).startswith("Failed to parse the response body: Expecting value: line 1 column 1 (char 0)"), e)
266+
267+
def test_user_agent_customization_issue_769(self):
268+
client = WebClient(
269+
base_url="http://localhost:8888",
270+
token="xoxb-user-agent this_is test",
271+
user_agent_prefix="this_is",
272+
user_agent_suffix="test",
273+
)
274+
resp = client.api_test()
275+
self.assertTrue(resp["ok"])
276+
277+
@async_test
278+
async def test_user_agent_customization_issue_769_async(self):
279+
client = WebClient(
280+
run_async=True,
281+
base_url="http://localhost:8888",
282+
token="xoxb-user-agent this_is test",
283+
user_agent_prefix="this_is",
284+
user_agent_suffix="test",
285+
)
286+
resp = await client.api_test()
287+
self.assertTrue(resp["ok"])

0 commit comments

Comments
 (0)