refactor: centralize message target resolution
Co-authored-by: Thinh Dinh <tobalsan@users.noreply.github.com>
This commit is contained in:
@@ -27,7 +27,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveDiscordGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { normalizeDiscordMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeDiscordTargetId,
|
||||
normalizeDiscordMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { discordOnboardingAdapter } from "./onboarding/discord.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
@@ -36,7 +39,6 @@ import {
|
||||
} from "./setup-helpers.js";
|
||||
import { collectDiscordStatusIssues } from "./status-issues/discord.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import { missingTargetError } from "../../infra/outbound/target-errors.js";
|
||||
|
||||
const meta = getChatChannelMeta("discord");
|
||||
|
||||
@@ -178,6 +180,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeDiscordMessagingTarget,
|
||||
looksLikeTargetId: looksLikeDiscordTargetId,
|
||||
targetHint: "<channelId|user:ID|channel:ID>",
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
@@ -345,16 +349,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
chunker: null,
|
||||
textChunkLimit: 2000,
|
||||
pollMaxOptions: 10,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Discord", "<channelId|user:ID|channel:ID>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, replyToId }) => {
|
||||
const send = deps?.sendDiscord ?? sendMessageDiscord;
|
||||
const result = await send(to, text, {
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "./setup-helpers.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import { missingTargetError } from "../../infra/outbound/target-errors.js";
|
||||
|
||||
const meta = getChatChannelMeta("imessage");
|
||||
|
||||
@@ -107,6 +106,16 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
groups: {
|
||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||
},
|
||||
messaging: {
|
||||
looksLikeTargetId: (raw) => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(imessage:|chat_id:)/i.test(trimmed)) return true;
|
||||
if (trimmed.includes("@")) return true;
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
},
|
||||
targetHint: "<handle|chat_id:ID>",
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
@@ -173,16 +182,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("iMessage", "<handle|chat_id:ID>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
|
||||
@@ -32,6 +32,16 @@ export function normalizeSlackMessagingTarget(raw: string): string | undefined {
|
||||
return `channel:${trimmed}`.toLowerCase();
|
||||
}
|
||||
|
||||
export function looksLikeSlackTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) return true;
|
||||
if (/^(user|channel|group):/i.test(trimmed)) return true;
|
||||
if (/^slack:/i.test(trimmed)) return true;
|
||||
if (/^[@#]/.test(trimmed)) return true;
|
||||
return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed);
|
||||
}
|
||||
|
||||
export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
@@ -60,6 +70,15 @@ export function normalizeDiscordMessagingTarget(raw: string): string | undefined
|
||||
return `channel:${trimmed}`.toLowerCase();
|
||||
}
|
||||
|
||||
export function looksLikeDiscordTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^<@!?\d+>$/.test(trimmed)) return true;
|
||||
if (/^(user|channel|group|discord):/i.test(trimmed)) return true;
|
||||
if (/^\d{6,}$/.test(trimmed)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function normalizeTelegramMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
@@ -80,6 +99,14 @@ export function normalizeTelegramMessagingTarget(raw: string): string | undefine
|
||||
return `telegram:${normalized}`.toLowerCase();
|
||||
}
|
||||
|
||||
export function looksLikeTelegramTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(telegram|tg|group):/i.test(trimmed)) return true;
|
||||
if (trimmed.startsWith("@")) return true;
|
||||
return /^-?\d{6,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
export function normalizeSignalMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
@@ -104,8 +131,23 @@ export function normalizeSignalMessagingTarget(raw: string): string | undefined
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
export function looksLikeSignalTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(signal:)?(group:|username:|u:)/i.test(trimmed)) return true;
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
return normalizeWhatsAppTarget(trimmed) ?? undefined;
|
||||
}
|
||||
|
||||
export function looksLikeWhatsAppTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^whatsapp:/i.test(trimmed)) return true;
|
||||
if (trimmed.includes("@")) return true;
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { sendMessageDiscord, sendPollDiscord } from "../../../discord/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
|
||||
export const discordOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: null,
|
||||
textChunkLimit: 2000,
|
||||
pollMaxOptions: 10,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Discord", "<channelId|user:ID|channel:ID>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, replyToId }) => {
|
||||
const send = deps?.sendDiscord ?? sendMessageDiscord;
|
||||
const result = await send(to, text, {
|
||||
|
||||
@@ -2,22 +2,11 @@ import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import { sendMessageIMessage } from "../../../imessage/send.js";
|
||||
import { resolveChannelMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
|
||||
export const imessageOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("iMessage", "<handle|chat_id:ID>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendIMessage ?? sendMessageIMessage;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
|
||||
@@ -2,22 +2,11 @@ import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import { sendMessageSignal } from "../../../signal/send.js";
|
||||
import { resolveChannelMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
|
||||
export const signalOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Signal", "<E.164|group:ID|signal:group:ID|signal:+E.164>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendSignal ?? sendMessageSignal;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { sendMessageSlack } from "../../../slack/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
|
||||
export const slackOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: null,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Slack", "<channelId|user:ID|channel:ID>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, replyToId }) => {
|
||||
const send = deps?.sendSlack ?? sendMessageSlack;
|
||||
const result = await send(to, text, {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { markdownToTelegramHtmlChunks } from "../../../telegram/format.js";
|
||||
import { sendMessageTelegram } from "../../../telegram/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { missingTargetError } from "../../../infra/outbound/target-errors.js";
|
||||
|
||||
function parseReplyToMessageId(replyToId?: string | null) {
|
||||
if (!replyToId) return undefined;
|
||||
@@ -23,16 +22,6 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: markdownToTelegramHtmlChunks,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Telegram", "<chatId>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
||||
const send = deps?.sendTelegram ?? sendMessageTelegram;
|
||||
const replyToMessageId = parseReplyToMessageId(replyToId);
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { resolveChannelMediaMaxBytes } from "./media-limits.js";
|
||||
import { normalizeSignalMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeSignalTargetId,
|
||||
normalizeSignalMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { signalOnboardingAdapter } from "./onboarding/signal.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
@@ -26,7 +29,6 @@ import {
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "./setup-helpers.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import { missingTargetError } from "../../infra/outbound/target-errors.js";
|
||||
|
||||
const meta = getChatChannelMeta("signal");
|
||||
|
||||
@@ -116,6 +118,8 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSignalMessagingTarget,
|
||||
looksLikeTargetId: looksLikeSignalTargetId,
|
||||
targetHint: "<E.164|group:ID|signal:group:ID|signal:+E.164>",
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
@@ -197,16 +201,6 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkText,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Signal", "<E.164|group:ID|signal:group:ID|signal:+E.164>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = deps?.sendSignal ?? sendMessageSignal;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveSlackGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { normalizeSlackMessagingTarget } from "./normalize-target.js";
|
||||
import { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./normalize-target.js";
|
||||
import { slackOnboardingAdapter } from "./onboarding/slack.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "./setup-helpers.js";
|
||||
import type { ChannelMessageActionName, ChannelPlugin } from "./types.js";
|
||||
import { missingTargetError } from "../../infra/outbound/target-errors.js";
|
||||
|
||||
const meta = getChatChannelMeta("slack");
|
||||
|
||||
@@ -205,6 +204,8 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSlackMessagingTarget,
|
||||
looksLikeTargetId: looksLikeSlackTargetId,
|
||||
targetHint: "<channelId|user:ID|channel:ID>",
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
@@ -526,16 +527,6 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
deliveryMode: "direct",
|
||||
chunker: null,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Slack", "<channelId|user:ID|channel:ID>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, cfg }) => {
|
||||
const send = deps?.sendSlack ?? sendMessageSlack;
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveTelegramGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { normalizeTelegramMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeTelegramTargetId,
|
||||
normalizeTelegramMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { telegramOnboardingAdapter } from "./onboarding/telegram.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
@@ -35,7 +38,6 @@ import {
|
||||
} from "./setup-helpers.js";
|
||||
import { collectTelegramStatusIssues } from "./status-issues/telegram.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import { missingTargetError } from "../../infra/outbound/target-errors.js";
|
||||
|
||||
const meta = getChatChannelMeta("telegram");
|
||||
|
||||
@@ -158,6 +160,8 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeTelegramMessagingTarget,
|
||||
looksLikeTargetId: looksLikeTelegramTargetId,
|
||||
targetHint: "<chatId>",
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
@@ -281,16 +285,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkMarkdownText,
|
||||
textChunkLimit: 4000,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Telegram", "<chatId>"),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
||||
const send = deps?.sendTelegram ?? sendMessageTelegram;
|
||||
const replyToMessageId = parseReplyToMessageId(replyToId);
|
||||
|
||||
@@ -215,6 +215,13 @@ export type ChannelThreadingToolContext = {
|
||||
|
||||
export type ChannelMessagingAdapter = {
|
||||
normalizeTarget?: (raw: string) => string | undefined;
|
||||
looksLikeTargetId?: (raw: string, normalized?: string) => boolean;
|
||||
formatTargetDisplay?: (params: {
|
||||
target: string;
|
||||
display?: string;
|
||||
kind?: ChannelDirectoryEntryKind;
|
||||
}) => string;
|
||||
targetHint?: string;
|
||||
};
|
||||
|
||||
export type ChannelDirectoryEntryKind = "user" | "group" | "channel";
|
||||
|
||||
@@ -26,7 +26,10 @@ import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import { createWhatsAppLoginTool } from "./agent-tools/whatsapp-login.js";
|
||||
import { resolveWhatsAppGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { normalizeWhatsAppMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { whatsappOnboardingAdapter } from "./onboarding/whatsapp.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
@@ -219,6 +222,8 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeWhatsAppMessagingTarget,
|
||||
looksLikeTargetId: looksLikeWhatsAppTargetId,
|
||||
targetHint: "<E.164|group JID>",
|
||||
},
|
||||
directory: {
|
||||
self: async ({ cfg, accountId }) => {
|
||||
|
||||
Reference in New Issue
Block a user