import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-reference.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import type { RuntimeEnv } from "../../runtime.js"; import { markdownToSlackMrkdwnChunks } from "../format.js"; import { sendMessageSlack } from "../send.js"; export async function deliverReplies(params: { replies: ReplyPayload[]; target: string; token: string; accountId?: string; runtime: RuntimeEnv; textLimit: number; replyThreadTs?: string; }) { for (const payload of params.replies) { const threadTs = payload.replyToId ?? params.replyThreadTs; const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const text = payload.text ?? ""; if (!text && mediaList.length === 0) continue; if (mediaList.length === 0) { const trimmed = text.trim(); if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) continue; await sendMessageSlack(params.target, trimmed, { token: params.token, threadTs, accountId: params.accountId, }); } else { let first = true; for (const mediaUrl of mediaList) { const caption = first ? text : ""; first = false; await sendMessageSlack(params.target, caption, { token: params.token, mediaUrl, threadTs, accountId: params.accountId, }); } } params.runtime.log?.(`delivered reply to ${params.target}`); } } export type SlackRespondFn = (payload: { text: string; response_type?: "ephemeral" | "in_channel"; }) => Promise; /** * Compute effective threadTs for a Slack reply based on replyToMode. * - "off": stay in thread if already in one, otherwise main channel * - "first": first reply goes to thread, subsequent replies to main channel * - "all": all replies go to thread */ export function resolveSlackThreadTs(params: { replyToMode: "off" | "first" | "all"; incomingThreadTs: string | undefined; messageTs: string | undefined; hasReplied: boolean; }): string | undefined { const planner = createSlackReplyReferencePlanner({ replyToMode: params.replyToMode, incomingThreadTs: params.incomingThreadTs, messageTs: params.messageTs, hasReplied: params.hasReplied, }); return planner.use(); } type SlackReplyDeliveryPlan = { nextThreadTs: () => string | undefined; markSent: () => void; }; function createSlackReplyReferencePlanner(params: { replyToMode: "off" | "first" | "all"; incomingThreadTs: string | undefined; messageTs: string | undefined; hasReplied?: boolean; }) { return createReplyReferencePlanner({ replyToMode: params.replyToMode, existingId: params.incomingThreadTs, startId: params.messageTs, hasReplied: params.hasReplied, }); } export function createSlackReplyDeliveryPlan(params: { replyToMode: "off" | "first" | "all"; incomingThreadTs: string | undefined; messageTs: string | undefined; hasRepliedRef: { value: boolean }; }): SlackReplyDeliveryPlan { const replyReference = createSlackReplyReferencePlanner({ replyToMode: params.replyToMode, incomingThreadTs: params.incomingThreadTs, messageTs: params.messageTs, hasReplied: params.hasRepliedRef.value, }); return { nextThreadTs: () => replyReference.use(), markSent: () => { replyReference.markSent(); params.hasRepliedRef.value = replyReference.hasReplied(); }, }; } export async function deliverSlackSlashReplies(params: { replies: ReplyPayload[]; respond: SlackRespondFn; ephemeral: boolean; textLimit: number; }) { const messages: string[] = []; const chunkLimit = Math.min(params.textLimit, 4000); for (const payload of params.replies) { const textRaw = payload.text?.trim() ?? ""; const text = textRaw && !isSilentReplyText(textRaw, SILENT_REPLY_TOKEN) ? textRaw : undefined; const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const combined = [text ?? "", ...mediaList.map((url) => url.trim()).filter(Boolean)] .filter(Boolean) .join("\n"); if (!combined) continue; for (const chunk of markdownToSlackMrkdwnChunks(combined, chunkLimit)) { messages.push(chunk); } } if (messages.length === 0) return; // Slack slash command responses can be multi-part by sending follow-ups via response_url. const responseType = params.ephemeral ? "ephemeral" : "in_channel"; for (const text of messages) { await params.respond({ text, response_type: responseType }); } }