refactor(src): split oversized modules
This commit is contained in:
157
src/slack/monitor/replies.ts
Normal file
157
src/slack/monitor/replies.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { chunkMarkdownText } from "../../auto-reply/chunk.js";
|
||||
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 { sendMessageSlack } from "../send.js";
|
||||
|
||||
export async function deliverReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
target: string;
|
||||
token: string;
|
||||
accountId?: string;
|
||||
runtime: RuntimeEnv;
|
||||
textLimit: number;
|
||||
replyThreadTs?: string;
|
||||
}) {
|
||||
const chunkLimit = Math.min(params.textLimit, 4000);
|
||||
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) {
|
||||
for (const chunk of chunkMarkdownText(text, chunkLimit)) {
|
||||
const trimmed = chunk.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 chunkMarkdownText(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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user