Skip to content

Commit a061f72

Browse files
authored
feat: support http proxies (#1398)
1 parent fdd70ae commit a061f72

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

interactions/api/http/http_client.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import aiohttp
1212
import discord_typings
13-
from aiohttp import BaseConnector, ClientSession, ClientWebSocketResponse, FormData
13+
from aiohttp import BaseConnector, ClientSession, ClientWebSocketResponse, FormData, BasicAuth
1414
from multidict import CIMultiDictProxy
1515

1616
import interactions.client.const as constants
@@ -214,7 +214,11 @@ class HTTPClient(
214214
"""A http client for sending requests to the Discord API."""
215215

216216
def __init__(
217-
self, connector: BaseConnector | None = None, logger: Logger = MISSING, show_ratelimit_tracebacks: bool = False
217+
self,
218+
connector: BaseConnector | None = None,
219+
logger: Logger = MISSING,
220+
show_ratelimit_tracebacks: bool = False,
221+
proxy: tuple[str, BasicAuth] | None = None,
218222
) -> None:
219223
self.connector: BaseConnector | None = connector
220224
self.__session: ClientSession | None = None
@@ -229,6 +233,8 @@ def __init__(
229233
self.user_agent: str = (
230234
f"DiscordBot ({__repo_url__} {__version__} Python/{__py_version__}) aiohttp/{aiohttp.__version__}"
231235
)
236+
self.proxy: tuple[str, BasicAuth] | None = proxy
237+
self.__proxy_validated: bool = False
232238

233239
if logger is MISSING:
234240
logger = constants.get_logger()
@@ -384,6 +390,10 @@ async def request( # noqa: C901
384390
kwargs["json"] = processed_data # pyright: ignore
385391
await self.global_lock.wait()
386392

393+
if self.proxy:
394+
kwargs["proxy"] = self.proxy[0]
395+
kwargs["proxy_auth"] = self.proxy[1]
396+
387397
async with self.__session.request(route.method, route.url, **kwargs) as response:
388398
result = await response_decode(response)
389399
self.ingest_ratelimit(route, response.headers, lock)
@@ -505,6 +515,19 @@ async def login(self, token: str) -> dict[str, Any]:
505515
connector=self.connector or aiohttp.TCPConnector(limit=self.global_lock.max_requests),
506516
json_serialize=FastJson.dumps,
507517
)
518+
if not self.__proxy_validated and self.proxy:
519+
try:
520+
self.logger.info(f"Validating Proxy @ {self.proxy[0]}")
521+
async with self.__session.get(
522+
"http://icanhazip.com/", proxy=self.proxy[0], proxy_auth=self.proxy[1]
523+
) as response:
524+
if response.status != 200:
525+
raise RuntimeError("Proxy configuration is invalid")
526+
self.logger.info(f"Proxy Connected @ {(await response.text()).strip()}")
527+
self.__proxy_validated = True
528+
except Exception as e:
529+
raise RuntimeError("Proxy configuration is invalid") from e
530+
508531
self.token = token
509532
try:
510533
result = await self.request(Route("GET", "/users/@me"))
@@ -556,4 +579,6 @@ async def websocket_connect(self, url: str) -> ClientWebSocketResponse:
556579
autoclose=False,
557580
headers={"User-Agent": self.user_agent},
558581
compress=0,
582+
proxy=self.proxy[0] if self.proxy else None,
583+
proxy_auth=self.proxy[1] if self.proxy else None,
559584
)

interactions/client/client.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
Tuple,
2727
)
2828

29+
from aiohttp import BasicAuth
30+
2931
import interactions.api.events as events
3032
import interactions.client.const as constants
3133
from interactions.api.events import BaseEvent, RawGatewayEvent, processors
@@ -241,6 +243,9 @@ class Client(
241243
logging_level: The level of logging to use for basic_logging. Do not use in combination with `Client.logger`
242244
logger: The logger interactions.py should use. Do not use in combination with `Client.basic_logging` and `Client.logging_level`. Note: Different loggers with multiple clients are not supported
243245
246+
proxy: A http/https proxy to use for all requests
247+
proxy_auth: The auth to use for the proxy - must be either a tuple of (username, password) or aiohttp.BasicAuth
248+
244249
Optionally, you can configure the caches here, by specifying the name of the cache, followed by a dict-style object to use.
245250
It is recommended to use `smart_cache.create_cache` to configure the cache here.
246251
as an example, this is a recommended attribute `message_cache=create_cache(250, 50)`,
@@ -283,6 +288,8 @@ def __init__(
283288
status: Status = Status.ONLINE,
284289
sync_ext: bool = True,
285290
sync_interactions: bool = True,
291+
proxy_url: str | None = None,
292+
proxy_auth: BasicAuth | tuple[str, str] | None = None,
286293
token: str | None = None,
287294
total_shards: int = 1,
288295
**kwargs,
@@ -321,8 +328,12 @@ def __init__(
321328
self.intents = intents if isinstance(intents, Intents) else Intents(intents)
322329

323330
# resources
331+
if isinstance(proxy_auth, tuple):
332+
proxy_auth = BasicAuth(*proxy_auth)
324333

325-
self.http: HTTPClient = HTTPClient(logger=self.logger, show_ratelimit_tracebacks=show_ratelimit_tracebacks)
334+
self.http: HTTPClient = HTTPClient(
335+
logger=self.logger, show_ratelimit_tracebacks=show_ratelimit_tracebacks, proxy=(proxy_url, proxy_auth)
336+
)
326337
"""The HTTP client to use when interacting with discord endpoints"""
327338

328339
# context factories

0 commit comments

Comments
 (0)