143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
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<unknown>;
|
|
|
|
/**
|
|
* 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 });
|
|
}
|
|
}
|