This commit is contained in:
MindTheBruno 2026-01-30 18:20:17 +02:00 committed by GitHub
commit 4cf05f67db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 22 deletions

View File

@ -37,16 +37,23 @@ export async function fetchWithSlackAuth(url: string, token: string): Promise<Re
return fetch(resolvedUrl, { redirect: "follow" });
}
export async function resolveSlackMedia(params: {
files?: SlackFile[];
token: string;
maxBytes: number;
}): Promise<{
export type SlackMediaInfo = {
path: string;
contentType?: string;
placeholder: string;
} | null> {
};
/**
* Resolves all Slack media files from a message.
* Returns an array of successfully downloaded files.
*/
export async function resolveSlackMediaList(params: {
files?: SlackFile[];
token: string;
maxBytes: number;
}): Promise<SlackMediaInfo[]> {
const files = params.files ?? [];
const out: SlackMediaInfo[] = [];
for (const file of files) {
const url = file.url_private_download ?? file.url_private;
if (!url) continue;
@ -71,16 +78,58 @@ export async function resolveSlackMedia(params: {
params.maxBytes,
);
const label = fetched.fileName ?? file.name;
return {
out.push({
path: saved.path,
contentType: saved.contentType,
placeholder: label ? `[Slack file: ${label}]` : "[Slack file]",
};
});
} catch {
// Ignore download failures and fall through to the next file.
// Ignore download failures and continue to the next file.
}
}
return null;
return out;
}
/**
* Legacy function for backwards compatibility.
* @deprecated Use resolveSlackMediaList instead.
*/
export async function resolveSlackMedia(params: {
files?: SlackFile[];
token: string;
maxBytes: number;
}): Promise<SlackMediaInfo | null> {
const list = await resolveSlackMediaList(params);
return list[0] ?? null;
}
/**
* Builds the media payload fields for the inbound context.
* Provides both singular (MediaPath) and plural (MediaPaths) fields for compatibility.
*/
export function buildSlackMediaPayload(mediaList: SlackMediaInfo[]): {
MediaPath?: string;
MediaType?: string;
MediaUrl?: string;
MediaPaths?: string[];
MediaUrls?: string[];
MediaTypes?: string[];
placeholder?: string;
} {
if (mediaList.length === 0) return {};
const first = mediaList[0];
const mediaPaths = mediaList.map((media) => media.path);
const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[];
const placeholders = mediaList.map((media) => media.placeholder);
return {
MediaPath: first?.path,
MediaType: first?.contentType,
MediaUrl: first?.path,
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
placeholder: placeholders.length > 0 ? placeholders.join(" ") : undefined,
};
}
export type SlackThreadStarter = {

View File

@ -44,7 +44,12 @@ import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-li
import { resolveSlackEffectiveAllowFrom } from "../auth.js";
import { resolveSlackChannelConfig } from "../channel-config.js";
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js";
import {
buildSlackMediaPayload,
resolveSlackMediaList,
resolveSlackThreadStarter,
type SlackMediaInfo,
} from "../media.js";
import type { PreparedSlackMessage } from "./types.js";
@ -331,12 +336,13 @@ export async function prepareSlackMessage(params: {
return null;
}
const media = await resolveSlackMedia({
const mediaList = await resolveSlackMediaList({
files: message.files,
token: ctx.botToken,
maxBytes: ctx.mediaMaxBytes,
});
const rawBody = (message.text ?? "").trim() || media?.placeholder || "";
const mediaPayload = buildSlackMediaPayload(mediaList);
const rawBody = (message.text ?? "").trim() || mediaPayload.placeholder || "";
if (!rawBody) return null;
const ackReaction = resolveAckReaction(cfg, route.agentId);
@ -453,7 +459,7 @@ export async function prepareSlackMessage(params: {
let threadStarterBody: string | undefined;
let threadLabel: string | undefined;
let threadStarterMedia: Awaited<ReturnType<typeof resolveSlackMedia>> = null;
let threadStarterMediaList: SlackMediaInfo[] = [];
if (isThreadReply && threadTs) {
const starter = await resolveSlackThreadStarter({
channelId: message.channel,
@ -474,15 +480,15 @@ export async function prepareSlackMessage(params: {
const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
threadLabel = `Slack thread ${roomLabel}${snippet ? `: ${snippet}` : ""}`;
// If current message has no files but thread starter does, fetch starter's files
if (!media && starter.files && starter.files.length > 0) {
threadStarterMedia = await resolveSlackMedia({
if (mediaList.length === 0 && starter.files && starter.files.length > 0) {
threadStarterMediaList = await resolveSlackMediaList({
files: starter.files,
token: ctx.botToken,
maxBytes: ctx.mediaMaxBytes,
});
if (threadStarterMedia) {
if (threadStarterMediaList.length > 0) {
logVerbose(
`slack: hydrated thread starter file ${threadStarterMedia.placeholder} from root message`,
`slack: hydrated ${threadStarterMediaList.length} thread starter file(s) from root message`,
);
}
}
@ -492,7 +498,8 @@ export async function prepareSlackMessage(params: {
}
// Use thread starter media if current message has none
const effectiveMedia = media ?? threadStarterMedia;
const effectiveMediaPayload =
mediaList.length > 0 ? mediaPayload : buildSlackMediaPayload(threadStarterMediaList);
const ctxPayload = finalizeInboundContext({
Body: combinedBody,
@ -519,9 +526,12 @@ export async function prepareSlackMessage(params: {
ThreadLabel: threadLabel,
Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
WasMentioned: isRoomish ? effectiveWasMentioned : undefined,
MediaPath: effectiveMedia?.path,
MediaType: effectiveMedia?.contentType,
MediaUrl: effectiveMedia?.path,
MediaPath: effectiveMediaPayload.MediaPath,
MediaType: effectiveMediaPayload.MediaType,
MediaUrl: effectiveMediaPayload.MediaUrl,
MediaPaths: effectiveMediaPayload.MediaPaths,
MediaUrls: effectiveMediaPayload.MediaUrls,
MediaTypes: effectiveMediaPayload.MediaTypes,
CommandAuthorized: commandAuthorized,
OriginatingChannel: "slack" as const,
OriginatingTo: slackTo,