mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 22:44:36 +00:00
fix(slack): scope download-file to channel and thread context
This commit is contained in:
@@ -280,6 +280,129 @@ export async function listSlackPins(
|
||||
return (result.items ?? []) as SlackPin[];
|
||||
}
|
||||
|
||||
type SlackFileInfoSummary = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
mimetype?: string;
|
||||
url_private?: string;
|
||||
url_private_download?: string;
|
||||
channels?: unknown;
|
||||
groups?: unknown;
|
||||
ims?: unknown;
|
||||
shares?: unknown;
|
||||
};
|
||||
|
||||
type SlackFileThreadShare = {
|
||||
channelId: string;
|
||||
ts?: string;
|
||||
threadTs?: string;
|
||||
};
|
||||
|
||||
function normalizeSlackScopeValue(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function collectSlackDirectShareChannelIds(file: SlackFileInfoSummary): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const group of [file.channels, file.groups, file.ims]) {
|
||||
if (!Array.isArray(group)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of group) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeSlackScopeValue(entry);
|
||||
if (normalized) {
|
||||
ids.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function collectSlackShareMaps(file: SlackFileInfoSummary): Array<Record<string, unknown>> {
|
||||
if (!file.shares || typeof file.shares !== "object" || Array.isArray(file.shares)) {
|
||||
return [];
|
||||
}
|
||||
const shares = file.shares as Record<string, unknown>;
|
||||
return [shares.public, shares.private].filter(
|
||||
(value): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === "object" && !Array.isArray(value),
|
||||
);
|
||||
}
|
||||
|
||||
function collectSlackSharedChannelIds(file: SlackFileInfoSummary): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const shareMap of collectSlackShareMaps(file)) {
|
||||
for (const channelId of Object.keys(shareMap)) {
|
||||
const normalized = normalizeSlackScopeValue(channelId);
|
||||
if (normalized) {
|
||||
ids.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function collectSlackThreadShares(
|
||||
file: SlackFileInfoSummary,
|
||||
channelId: string,
|
||||
): SlackFileThreadShare[] {
|
||||
const matches: SlackFileThreadShare[] = [];
|
||||
for (const shareMap of collectSlackShareMaps(file)) {
|
||||
const rawEntries = shareMap[channelId];
|
||||
if (!Array.isArray(rawEntries)) {
|
||||
continue;
|
||||
}
|
||||
for (const rawEntry of rawEntries) {
|
||||
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
||||
continue;
|
||||
}
|
||||
const entry = rawEntry as Record<string, unknown>;
|
||||
const ts = typeof entry.ts === "string" ? normalizeSlackScopeValue(entry.ts) : undefined;
|
||||
const threadTs =
|
||||
typeof entry.thread_ts === "string" ? normalizeSlackScopeValue(entry.thread_ts) : undefined;
|
||||
matches.push({ channelId, ts, threadTs });
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function hasSlackScopeMismatch(params: {
|
||||
file: SlackFileInfoSummary;
|
||||
channelId?: string;
|
||||
threadId?: string;
|
||||
}): boolean {
|
||||
const channelId = normalizeSlackScopeValue(params.channelId);
|
||||
if (!channelId) {
|
||||
return false;
|
||||
}
|
||||
const threadId = normalizeSlackScopeValue(params.threadId);
|
||||
|
||||
const directIds = collectSlackDirectShareChannelIds(params.file);
|
||||
const sharedIds = collectSlackSharedChannelIds(params.file);
|
||||
const hasChannelEvidence = directIds.size > 0 || sharedIds.size > 0;
|
||||
const inChannel = directIds.has(channelId) || sharedIds.has(channelId);
|
||||
if (hasChannelEvidence && !inChannel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!threadId) {
|
||||
return false;
|
||||
}
|
||||
const threadShares = collectSlackThreadShares(params.file, channelId);
|
||||
if (threadShares.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const threadEvidence = threadShares.filter((entry) => entry.threadTs || entry.ts);
|
||||
if (threadEvidence.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return !threadEvidence.some((entry) => entry.threadTs === threadId || entry.ts === threadId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a Slack file by ID and saves it to the local media store.
|
||||
* Fetches a fresh download URL via files.info to avoid using stale private URLs.
|
||||
@@ -287,26 +410,21 @@ export async function listSlackPins(
|
||||
*/
|
||||
export async function downloadSlackFile(
|
||||
fileId: string,
|
||||
opts: SlackActionClientOpts & { maxBytes: number },
|
||||
opts: SlackActionClientOpts & { maxBytes: number; channelId?: string; threadId?: string },
|
||||
): Promise<SlackMediaResult | null> {
|
||||
const token = resolveToken(opts.token, opts.accountId);
|
||||
const client = await getClient(opts);
|
||||
|
||||
// Fetch fresh file metadata (includes a current url_private_download).
|
||||
const info = await client.files.info({ file: fileId });
|
||||
const file = info.file as
|
||||
| {
|
||||
id?: string;
|
||||
name?: string;
|
||||
mimetype?: string;
|
||||
url_private?: string;
|
||||
url_private_download?: string;
|
||||
}
|
||||
| undefined;
|
||||
const file = info.file as SlackFileInfoSummary | undefined;
|
||||
|
||||
if (!file?.url_private_download && !file?.url_private) {
|
||||
return null;
|
||||
}
|
||||
if (hasSlackScopeMismatch({ file, channelId: opts.channelId, threadId: opts.threadId })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = await resolveSlackMedia({
|
||||
files: [
|
||||
|
||||
Reference in New Issue
Block a user