fix(slack): route file downloads to the owning workspace via URL-embedded team id

Salvaged from #59742 (downloads half only). Main already resolves the
event-level team id from the channel→workspace cache before downloads
(the file-EVENT half was covered by #30456), but when neither the event
nor the cache knows the workspace, both download helpers silently fell
back to the PRIMARY workspace token — Slack then returns an HTML login
page instead of file bytes for any non-primary workspace file.

Slack private file URLs embed the owning workspace id
(files-pri/<TEAM_ID>-<FILE_ID>/...), so _resolve_download_token now
prefers: explicit team_id -> URL-embedded team id -> primary token.
Both _download_slack_file and _download_slack_file_bytes route through
it.

Deferred from #59742 (out of this correctness cluster's scope): the
channel→team disk persistence, the pre-send conversations.info probe
loop, and the thread-file ingestion feature.

Reapplied from #59742 by @benjamin2026-dot onto the current adapter
(original targeted the pre-#30456 download sites).
This commit is contained in:
benjamin2026-dot 2026-07-23 08:17:07 -07:00 committed by Teknium
parent c1c991ae9e
commit ca8aee87e8

View file

@ -7879,6 +7879,31 @@ class SlackAdapter(BasePlatformAdapter):
cls._SLACK_CDN_HOST_SUFFIXES
)
def _resolve_download_token(self, url: str, team_id: str = "") -> str:
"""Pick the correct bot token for a Slack file download.
Order of preference:
1. Explicit team_id that maps to a known workspace client.
2. team_id parsed from the file URL itself Slack private file URLs
embed the workspace id as ``files-pri/<TEAM_ID>-<FILE_ID>/...`` so
we can route to the right workspace even when the triggering event
carried no team info (thread replies / mentions in multi-workspace
installs). This prevents defaulting to the primary workspace token,
which makes Slack return an HTML login page instead of file bytes.
3. Primary workspace token as a last resort.
"""
if team_id and team_id in self._team_clients:
return self._team_clients[team_id].token
try:
m = re.search(r"/files-pri/(T[A-Z0-9]+)-", url or "")
if m:
url_team = m.group(1)
if url_team in self._team_clients:
return self._team_clients[url_team].token
except Exception: # pragma: no cover - defensive
pass
return self.config.token or ""
async def _download_slack_file(
self, url: str, ext: str, audio: bool = False, team_id: str = ""
) -> str:
@ -7909,11 +7934,7 @@ class SlackAdapter(BasePlatformAdapter):
f"{safe_url_for_log(url)}"
)
bot_token = (
self._team_clients[team_id].token
if team_id and team_id in self._team_clients
else self.config.token
)
bot_token = self._resolve_download_token(url, team_id)
# DNS-pinned client: resolve + validate once, dial the vetted IP
# (closes the DNS-rebinding TOCTOU window between is_safe_url and
@ -7989,11 +8010,7 @@ class SlackAdapter(BasePlatformAdapter):
f"{safe_url_for_log(url)}"
)
bot_token = (
self._team_clients[team_id].token
if team_id and team_id in self._team_clients
else self.config.token
)
bot_token = self._resolve_download_token(url, team_id)
# DNS-pinned client: resolve + validate once, dial the vetted IP
# (closes the DNS-rebinding TOCTOU window between is_safe_url and