|
18 | 18 | """ |
19 | 19 | from __future__ import annotations |
20 | 20 |
|
21 | | -import json |
22 | 21 | import threading |
23 | 22 | import urllib.error |
24 | 23 | import urllib.parse |
|
28 | 27 |
|
29 | 28 | from je_auto_control.utils.chatops.router import CommandResult, CommandRouter |
30 | 29 | from je_auto_control.utils.exception.exceptions import AutoControlException |
| 30 | +from je_auto_control.utils.http_client.http_client import build_call, perform_call |
31 | 31 | from je_auto_control.utils.logging.logging_instance import autocontrol_logger |
32 | 32 |
|
33 | 33 |
|
|
37 | 37 | _MAX_BACKOFF = 60.0 |
38 | 38 |
|
39 | 39 |
|
40 | | -class SlackError(RuntimeError): |
| 40 | +class SlackError(AutoControlException, RuntimeError): |
41 | 41 | """Raised when the Slack API returns ``ok: false`` or HTTP fails.""" |
42 | 42 |
|
43 | 43 |
|
@@ -151,7 +151,8 @@ def _fetch_messages(self) -> list: |
151 | 151 | if self.last_seen_ts: |
152 | 152 | params["oldest"] = self.last_seen_ts |
153 | 153 | body = self._api_get("conversations.history", params) |
154 | | - return list(body.get("messages") or []) |
| 154 | + messages = body.get("messages") or [] |
| 155 | + return [message for message in messages if isinstance(message, dict)] |
155 | 156 |
|
156 | 157 | def _is_self(self, message: Dict[str, Any]) -> bool: |
157 | 158 | if message.get("subtype") == "bot_message": |
@@ -188,23 +189,21 @@ def _request(self, url: str, *, method: str, |
188 | 189 | ) -> Dict[str, Any]: |
189 | 190 | if not url.startswith("https://slack.com/api/"): |
190 | 191 | raise SlackError(f"refusing to call non-Slack URL: {url}") |
191 | | - headers = {"Authorization": f"Bearer {self.token}"} |
192 | | - data: Optional[bytes] = None |
193 | | - if payload is not None: |
194 | | - data = json.dumps(payload).encode("utf-8") |
195 | | - headers["Content-Type"] = "application/json" |
196 | | - request = urllib.request.Request( # nosec B310 # reason: scheme allow-listed above |
197 | | - url, data=data, method=method, headers=headers, |
198 | | - ) |
| 192 | + # Through http_client, so the egress policy applies to Slack too; a |
| 193 | + # Slack API call never redirects, so a 3xx is an error, not followed. |
| 194 | + call = build_call(url, method, headers={"Authorization": f"Bearer {self.token}"}, |
| 195 | + json_body=payload, timeout=_HTTP_TIMEOUT) |
| 196 | + call["follow_redirects"] = False |
199 | 197 | try: |
200 | | - with urllib.request.urlopen( # nosec B310 |
201 | | - request, timeout=_HTTP_TIMEOUT, |
202 | | - ) as response: |
203 | | - body = json.loads(response.read().decode("utf-8")) |
204 | | - except urllib.error.URLError as error: |
| 198 | + response = perform_call(call) |
| 199 | + except (OSError, ValueError) as error: # URLError, EgressBlocked |
205 | 200 | raise SlackError(f"HTTP failure: {error}") from error |
206 | | - except ValueError as error: |
207 | | - raise SlackError(f"non-JSON response: {error}") from error |
| 201 | + body = response["json"] |
| 202 | + if not isinstance(body, dict): |
| 203 | + # A list or string body raised AttributeError, which run_forever |
| 204 | + # does not catch: one bad reply ended the poll loop for good. |
| 205 | + raise SlackError(f"Slack {url} returned HTTP {response['status']} " |
| 206 | + "without a JSON object") |
208 | 207 | if not body.get("ok"): |
209 | 208 | raise SlackError( |
210 | 209 | f"Slack {url} returned {body.get('error', 'unknown')}", |
|
0 commit comments