refactor(channels): unify target parsing
This commit is contained in:
@@ -4,7 +4,7 @@ import { resolveSlackAccount } from "../../slack/accounts.js";
|
|||||||
import { resolveDiscordAccount } from "../../discord/accounts.js";
|
import { resolveDiscordAccount } from "../../discord/accounts.js";
|
||||||
import { resolveTelegramAccount } from "../../telegram/accounts.js";
|
import { resolveTelegramAccount } from "../../telegram/accounts.js";
|
||||||
import { resolveWhatsAppAccount } from "../../web/accounts.js";
|
import { resolveWhatsAppAccount } from "../../web/accounts.js";
|
||||||
import { normalizeSlackMessagingTarget } from "./normalize-target.js";
|
import { normalizeSlackMessagingTarget } from "./normalize/slack.js";
|
||||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||||
|
|
||||||
export type DirectoryConfigParams = {
|
export type DirectoryConfigParams = {
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import {
|
|||||||
} from "./config-helpers.js";
|
} from "./config-helpers.js";
|
||||||
import { resolveDiscordGroupRequireMention } from "./group-mentions.js";
|
import { resolveDiscordGroupRequireMention } from "./group-mentions.js";
|
||||||
import { formatPairingApproveHint } from "./helpers.js";
|
import { formatPairingApproveHint } from "./helpers.js";
|
||||||
import { looksLikeDiscordTargetId, normalizeDiscordMessagingTarget } from "./normalize-target.js";
|
import {
|
||||||
|
looksLikeDiscordTargetId,
|
||||||
|
normalizeDiscordMessagingTarget,
|
||||||
|
} from "./normalize/discord.js";
|
||||||
import { discordOnboardingAdapter } from "./onboarding/discord.js";
|
import { discordOnboardingAdapter } from "./onboarding/discord.js";
|
||||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { parseDiscordTarget } from "../../discord/targets.js";
|
|
||||||
import { parseSlackTarget } from "../../slack/targets.js";
|
|
||||||
import { normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
|
||||||
|
|
||||||
export function normalizeSlackMessagingTarget(raw: string): string | undefined {
|
|
||||||
const target = parseSlackTarget(raw, { defaultKind: "channel" });
|
|
||||||
return target?.normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
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):/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 {
|
|
||||||
// Default bare IDs to channels so routing is stable across tool actions.
|
|
||||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
|
||||||
return target?.normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function looksLikeDiscordTargetId(raw: string): boolean {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) return false;
|
|
||||||
if (/^<@!?\d+>$/.test(trimmed)) return true;
|
|
||||||
if (/^(user|channel|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;
|
|
||||||
let normalized = trimmed;
|
|
||||||
if (normalized.startsWith("telegram:")) {
|
|
||||||
normalized = normalized.slice("telegram:".length).trim();
|
|
||||||
} else if (normalized.startsWith("tg:")) {
|
|
||||||
normalized = normalized.slice("tg:".length).trim();
|
|
||||||
}
|
|
||||||
if (!normalized) return undefined;
|
|
||||||
const tmeMatch =
|
|
||||||
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
|
|
||||||
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
|
|
||||||
if (tmeMatch?.[1]) normalized = `@${tmeMatch[1]}`;
|
|
||||||
if (!normalized) return undefined;
|
|
||||||
return `telegram:${normalized}`.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function looksLikeTelegramTargetId(raw: string): boolean {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) return false;
|
|
||||||
if (/^(telegram|tg):/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;
|
|
||||||
let normalized = trimmed;
|
|
||||||
if (normalized.toLowerCase().startsWith("signal:")) {
|
|
||||||
normalized = normalized.slice("signal:".length).trim();
|
|
||||||
}
|
|
||||||
if (!normalized) return undefined;
|
|
||||||
const lower = normalized.toLowerCase();
|
|
||||||
if (lower.startsWith("group:")) {
|
|
||||||
const id = normalized.slice("group:".length).trim();
|
|
||||||
return id ? `group:${id}`.toLowerCase() : undefined;
|
|
||||||
}
|
|
||||||
if (lower.startsWith("username:")) {
|
|
||||||
const id = normalized.slice("username:".length).trim();
|
|
||||||
return id ? `username:${id}`.toLowerCase() : undefined;
|
|
||||||
}
|
|
||||||
if (lower.startsWith("u:")) {
|
|
||||||
const id = normalized.slice("u:".length).trim();
|
|
||||||
return id ? `username:${id}`.toLowerCase() : 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);
|
|
||||||
}
|
|
||||||
16
src/channels/plugins/normalize/discord.ts
Normal file
16
src/channels/plugins/normalize/discord.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { parseDiscordTarget } from "../../../discord/targets.js";
|
||||||
|
|
||||||
|
export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
|
||||||
|
// Default bare IDs to channels so routing is stable across tool actions.
|
||||||
|
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||||
|
return target?.normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function looksLikeDiscordTargetId(raw: string): boolean {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (/^<@!?\d+>$/.test(trimmed)) return true;
|
||||||
|
if (/^(user|channel|discord):/i.test(trimmed)) return true;
|
||||||
|
if (/^\d{6,}$/.test(trimmed)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
30
src/channels/plugins/normalize/signal.ts
Normal file
30
src/channels/plugins/normalize/signal.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function normalizeSignalMessagingTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
let normalized = trimmed;
|
||||||
|
if (normalized.toLowerCase().startsWith("signal:")) {
|
||||||
|
normalized = normalized.slice("signal:".length).trim();
|
||||||
|
}
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
const lower = normalized.toLowerCase();
|
||||||
|
if (lower.startsWith("group:")) {
|
||||||
|
const id = normalized.slice("group:".length).trim();
|
||||||
|
return id ? `group:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (lower.startsWith("username:")) {
|
||||||
|
const id = normalized.slice("username:".length).trim();
|
||||||
|
return id ? `username:${id}`.toLowerCase() : undefined;
|
||||||
|
}
|
||||||
|
if (lower.startsWith("u:")) {
|
||||||
|
const id = normalized.slice("u:".length).trim();
|
||||||
|
return id ? `username:${id}`.toLowerCase() : 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);
|
||||||
|
}
|
||||||
16
src/channels/plugins/normalize/slack.ts
Normal file
16
src/channels/plugins/normalize/slack.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { parseSlackTarget } from "../../../slack/targets.js";
|
||||||
|
|
||||||
|
export function normalizeSlackMessagingTarget(raw: string): string | undefined {
|
||||||
|
const target = parseSlackTarget(raw, { defaultKind: "channel" });
|
||||||
|
return target?.normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
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):/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);
|
||||||
|
}
|
||||||
25
src/channels/plugins/normalize/telegram.ts
Normal file
25
src/channels/plugins/normalize/telegram.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export function normalizeTelegramMessagingTarget(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
let normalized = trimmed;
|
||||||
|
if (normalized.startsWith("telegram:")) {
|
||||||
|
normalized = normalized.slice("telegram:".length).trim();
|
||||||
|
} else if (normalized.startsWith("tg:")) {
|
||||||
|
normalized = normalized.slice("tg:".length).trim();
|
||||||
|
}
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
const tmeMatch =
|
||||||
|
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
|
||||||
|
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
|
||||||
|
if (tmeMatch?.[1]) normalized = `@${tmeMatch[1]}`;
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
return `telegram:${normalized}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function looksLikeTelegramTargetId(raw: string): boolean {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (/^(telegram|tg):/i.test(trimmed)) return true;
|
||||||
|
if (trimmed.startsWith("@")) return true;
|
||||||
|
return /^-?\d{6,}$/.test(trimmed);
|
||||||
|
}
|
||||||
15
src/channels/plugins/normalize/whatsapp.ts
Normal file
15
src/channels/plugins/normalize/whatsapp.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -18,7 +18,10 @@ import {
|
|||||||
} from "./config-helpers.js";
|
} from "./config-helpers.js";
|
||||||
import { formatPairingApproveHint } from "./helpers.js";
|
import { formatPairingApproveHint } from "./helpers.js";
|
||||||
import { resolveChannelMediaMaxBytes } from "./media-limits.js";
|
import { resolveChannelMediaMaxBytes } from "./media-limits.js";
|
||||||
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize-target.js";
|
import {
|
||||||
|
looksLikeSignalTargetId,
|
||||||
|
normalizeSignalMessagingTarget,
|
||||||
|
} from "./normalize/signal.js";
|
||||||
import { signalOnboardingAdapter } from "./onboarding/signal.js";
|
import { signalOnboardingAdapter } from "./onboarding/signal.js";
|
||||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "./config-helpers.js";
|
} from "./config-helpers.js";
|
||||||
import { resolveSlackGroupRequireMention } from "./group-mentions.js";
|
import { resolveSlackGroupRequireMention } from "./group-mentions.js";
|
||||||
import { formatPairingApproveHint } from "./helpers.js";
|
import { formatPairingApproveHint } from "./helpers.js";
|
||||||
import { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./normalize-target.js";
|
import { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./normalize/slack.js";
|
||||||
import { slackOnboardingAdapter } from "./onboarding/slack.js";
|
import { slackOnboardingAdapter } from "./onboarding/slack.js";
|
||||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import {
|
|||||||
} from "./config-helpers.js";
|
} from "./config-helpers.js";
|
||||||
import { resolveTelegramGroupRequireMention } from "./group-mentions.js";
|
import { resolveTelegramGroupRequireMention } from "./group-mentions.js";
|
||||||
import { formatPairingApproveHint } from "./helpers.js";
|
import { formatPairingApproveHint } from "./helpers.js";
|
||||||
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize-target.js";
|
import {
|
||||||
|
looksLikeTelegramTargetId,
|
||||||
|
normalizeTelegramMessagingTarget,
|
||||||
|
} from "./normalize/telegram.js";
|
||||||
import { telegramOnboardingAdapter } from "./onboarding/telegram.js";
|
import { telegramOnboardingAdapter } from "./onboarding/telegram.js";
|
||||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import { buildChannelConfigSchema } from "./config-schema.js";
|
|||||||
import { createWhatsAppLoginTool } from "./agent-tools/whatsapp-login.js";
|
import { createWhatsAppLoginTool } from "./agent-tools/whatsapp-login.js";
|
||||||
import { resolveWhatsAppGroupRequireMention } from "./group-mentions.js";
|
import { resolveWhatsAppGroupRequireMention } from "./group-mentions.js";
|
||||||
import { formatPairingApproveHint } from "./helpers.js";
|
import { formatPairingApproveHint } from "./helpers.js";
|
||||||
import { looksLikeWhatsAppTargetId, normalizeWhatsAppMessagingTarget } from "./normalize-target.js";
|
import {
|
||||||
|
looksLikeWhatsAppTargetId,
|
||||||
|
normalizeWhatsAppMessagingTarget,
|
||||||
|
} from "./normalize/whatsapp.js";
|
||||||
import { whatsappOnboardingAdapter } from "./onboarding/whatsapp.js";
|
import { whatsappOnboardingAdapter } from "./onboarding/whatsapp.js";
|
||||||
import {
|
import {
|
||||||
applyAccountNameToChannelSection,
|
applyAccountNameToChannelSection,
|
||||||
|
|||||||
42
src/channels/targets.test.ts
Normal file
42
src/channels/targets.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { buildMessagingTarget, ensureTargetId, requireTargetKind } from "./targets.js";
|
||||||
|
|
||||||
|
describe("ensureTargetId", () => {
|
||||||
|
it("returns the candidate when it matches", () => {
|
||||||
|
expect(
|
||||||
|
ensureTargetId({
|
||||||
|
candidate: "U123",
|
||||||
|
pattern: /^[A-Z0-9]+$/i,
|
||||||
|
errorMessage: "bad",
|
||||||
|
}),
|
||||||
|
).toBe("U123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws with the provided message on mismatch", () => {
|
||||||
|
expect(() =>
|
||||||
|
ensureTargetId({
|
||||||
|
candidate: "not-ok",
|
||||||
|
pattern: /^[A-Z0-9]+$/i,
|
||||||
|
errorMessage: "Bad target",
|
||||||
|
}),
|
||||||
|
).toThrow(/Bad target/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireTargetKind", () => {
|
||||||
|
it("returns the target id when the kind matches", () => {
|
||||||
|
const target = buildMessagingTarget("channel", "C123", "C123");
|
||||||
|
expect(requireTargetKind({ platform: "Slack", target, kind: "channel" })).toBe("C123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when the kind is missing or mismatched", () => {
|
||||||
|
expect(() => requireTargetKind({ platform: "Slack", target: undefined, kind: "channel" })).toThrow(
|
||||||
|
/Slack channel id is required/,
|
||||||
|
);
|
||||||
|
const target = buildMessagingTarget("user", "U123", "U123");
|
||||||
|
expect(() => requireTargetKind({ platform: "Slack", target, kind: "channel" })).toThrow(
|
||||||
|
/Slack channel id is required/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
56
src/channels/targets.ts
Normal file
56
src/channels/targets.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
export type MessagingTargetKind = "user" | "channel";
|
||||||
|
|
||||||
|
export type MessagingTarget = {
|
||||||
|
kind: MessagingTargetKind;
|
||||||
|
id: string;
|
||||||
|
raw: string;
|
||||||
|
normalized: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessagingTargetParseOptions = {
|
||||||
|
defaultKind?: MessagingTargetKind;
|
||||||
|
ambiguousMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeTargetId(kind: MessagingTargetKind, id: string): string {
|
||||||
|
return `${kind}:${id}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMessagingTarget(
|
||||||
|
kind: MessagingTargetKind,
|
||||||
|
id: string,
|
||||||
|
raw: string,
|
||||||
|
): MessagingTarget {
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
id,
|
||||||
|
raw,
|
||||||
|
normalized: normalizeTargetId(kind, id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureTargetId(params: {
|
||||||
|
candidate: string;
|
||||||
|
pattern: RegExp;
|
||||||
|
errorMessage: string;
|
||||||
|
}): string {
|
||||||
|
if (!params.pattern.test(params.candidate)) {
|
||||||
|
throw new Error(params.errorMessage);
|
||||||
|
}
|
||||||
|
return params.candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireTargetKind(params: {
|
||||||
|
platform: string;
|
||||||
|
target: MessagingTarget | undefined;
|
||||||
|
kind: MessagingTargetKind;
|
||||||
|
}): string {
|
||||||
|
const kindLabel = params.kind;
|
||||||
|
if (!params.target) {
|
||||||
|
throw new Error(`${params.platform} ${kindLabel} id is required.`);
|
||||||
|
}
|
||||||
|
if (params.target.kind !== params.kind) {
|
||||||
|
throw new Error(`${params.platform} ${kindLabel} id is required (use ${kindLabel}:<id>).`);
|
||||||
|
}
|
||||||
|
return params.target.id;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { normalizeDiscordMessagingTarget } from "../channels/plugins/normalize-target.js";
|
import { normalizeDiscordMessagingTarget } from "../channels/plugins/normalize/discord.js";
|
||||||
import { parseDiscordTarget, resolveDiscordChannelId } from "./targets.js";
|
import { parseDiscordTarget, resolveDiscordChannelId } from "./targets.js";
|
||||||
|
|
||||||
describe("parseDiscordTarget", () => {
|
describe("parseDiscordTarget", () => {
|
||||||
|
|||||||
@@ -1,29 +1,17 @@
|
|||||||
export type DiscordTargetKind = "user" | "channel";
|
import {
|
||||||
|
buildMessagingTarget,
|
||||||
|
ensureTargetId,
|
||||||
|
requireTargetKind,
|
||||||
|
type MessagingTarget,
|
||||||
|
type MessagingTargetKind,
|
||||||
|
type MessagingTargetParseOptions,
|
||||||
|
} from "../channels/targets.js";
|
||||||
|
|
||||||
export type DiscordTarget = {
|
export type DiscordTargetKind = MessagingTargetKind;
|
||||||
kind: DiscordTargetKind;
|
|
||||||
id: string;
|
|
||||||
raw: string;
|
|
||||||
normalized: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DiscordTargetParseOptions = {
|
export type DiscordTarget = MessagingTarget;
|
||||||
defaultKind?: DiscordTargetKind;
|
|
||||||
ambiguousMessage?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeTargetId(kind: DiscordTargetKind, id: string) {
|
type DiscordTargetParseOptions = MessagingTargetParseOptions;
|
||||||
return `${kind}:${id}`.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTarget(kind: DiscordTargetKind, id: string, raw: string): DiscordTarget {
|
|
||||||
return {
|
|
||||||
kind,
|
|
||||||
id,
|
|
||||||
raw,
|
|
||||||
normalized: normalizeTargetId(kind, id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseDiscordTarget(
|
export function parseDiscordTarget(
|
||||||
raw: string,
|
raw: string,
|
||||||
@@ -33,46 +21,42 @@ export function parseDiscordTarget(
|
|||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
|
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
|
||||||
if (mentionMatch) {
|
if (mentionMatch) {
|
||||||
return buildTarget("user", mentionMatch[1], trimmed);
|
return buildMessagingTarget("user", mentionMatch[1], trimmed);
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("user:")) {
|
if (trimmed.startsWith("user:")) {
|
||||||
const id = trimmed.slice("user:".length).trim();
|
const id = trimmed.slice("user:".length).trim();
|
||||||
return id ? buildTarget("user", id, trimmed) : undefined;
|
return id ? buildMessagingTarget("user", id, trimmed) : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("channel:")) {
|
if (trimmed.startsWith("channel:")) {
|
||||||
const id = trimmed.slice("channel:".length).trim();
|
const id = trimmed.slice("channel:".length).trim();
|
||||||
return id ? buildTarget("channel", id, trimmed) : undefined;
|
return id ? buildMessagingTarget("channel", id, trimmed) : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("discord:")) {
|
if (trimmed.startsWith("discord:")) {
|
||||||
const id = trimmed.slice("discord:".length).trim();
|
const id = trimmed.slice("discord:".length).trim();
|
||||||
return id ? buildTarget("user", id, trimmed) : undefined;
|
return id ? buildMessagingTarget("user", id, trimmed) : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("@")) {
|
if (trimmed.startsWith("@")) {
|
||||||
const candidate = trimmed.slice(1).trim();
|
const candidate = trimmed.slice(1).trim();
|
||||||
if (!/^\d+$/.test(candidate)) {
|
const id = ensureTargetId({
|
||||||
throw new Error("Discord DMs require a user id (use user:<id> or a <@id> mention)");
|
candidate,
|
||||||
}
|
pattern: /^\d+$/,
|
||||||
return buildTarget("user", candidate, trimmed);
|
errorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)",
|
||||||
|
});
|
||||||
|
return buildMessagingTarget("user", id, trimmed);
|
||||||
}
|
}
|
||||||
if (/^\d+$/.test(trimmed)) {
|
if (/^\d+$/.test(trimmed)) {
|
||||||
if (options.defaultKind) {
|
if (options.defaultKind) {
|
||||||
return buildTarget(options.defaultKind, trimmed, trimmed);
|
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
options.ambiguousMessage ??
|
options.ambiguousMessage ??
|
||||||
`Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
|
`Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return buildTarget("channel", trimmed, trimmed);
|
return buildMessagingTarget("channel", trimmed, trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDiscordChannelId(raw: string): string {
|
export function resolveDiscordChannelId(raw: string): string {
|
||||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||||
if (!target) {
|
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
|
||||||
throw new Error("Discord channel id is required.");
|
|
||||||
}
|
|
||||||
if (target.kind !== "channel") {
|
|
||||||
throw new Error("Discord channel id is required (use channel:<id>).");
|
|
||||||
}
|
|
||||||
return target.id;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { normalizeSlackMessagingTarget } from "../channels/plugins/normalize-target.js";
|
import { normalizeSlackMessagingTarget } from "../channels/plugins/normalize/slack.js";
|
||||||
import { parseSlackTarget, resolveSlackChannelId } from "./targets.js";
|
import { parseSlackTarget, resolveSlackChannelId } from "./targets.js";
|
||||||
|
|
||||||
describe("parseSlackTarget", () => {
|
describe("parseSlackTarget", () => {
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
export type SlackTargetKind = "user" | "channel";
|
import {
|
||||||
|
buildMessagingTarget,
|
||||||
|
ensureTargetId,
|
||||||
|
requireTargetKind,
|
||||||
|
type MessagingTarget,
|
||||||
|
type MessagingTargetKind,
|
||||||
|
type MessagingTargetParseOptions,
|
||||||
|
} from "../channels/targets.js";
|
||||||
|
|
||||||
export type SlackTarget = {
|
export type SlackTargetKind = MessagingTargetKind;
|
||||||
kind: SlackTargetKind;
|
|
||||||
id: string;
|
|
||||||
raw: string;
|
|
||||||
normalized: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SlackTargetParseOptions = {
|
export type SlackTarget = MessagingTarget;
|
||||||
defaultKind?: SlackTargetKind;
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeTargetId(kind: SlackTargetKind, id: string) {
|
type SlackTargetParseOptions = MessagingTargetParseOptions;
|
||||||
return `${kind}:${id}`.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTarget(kind: SlackTargetKind, id: string, raw: string): SlackTarget {
|
|
||||||
return {
|
|
||||||
kind,
|
|
||||||
id,
|
|
||||||
raw,
|
|
||||||
normalized: normalizeTargetId(kind, id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseSlackTarget(
|
export function parseSlackTarget(
|
||||||
raw: string,
|
raw: string,
|
||||||
@@ -32,47 +21,45 @@ export function parseSlackTarget(
|
|||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||||
if (mentionMatch) {
|
if (mentionMatch) {
|
||||||
return buildTarget("user", mentionMatch[1], trimmed);
|
return buildMessagingTarget("user", mentionMatch[1], trimmed);
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("user:")) {
|
if (trimmed.startsWith("user:")) {
|
||||||
const id = trimmed.slice("user:".length).trim();
|
const id = trimmed.slice("user:".length).trim();
|
||||||
return id ? buildTarget("user", id, trimmed) : undefined;
|
return id ? buildMessagingTarget("user", id, trimmed) : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("channel:")) {
|
if (trimmed.startsWith("channel:")) {
|
||||||
const id = trimmed.slice("channel:".length).trim();
|
const id = trimmed.slice("channel:".length).trim();
|
||||||
return id ? buildTarget("channel", id, trimmed) : undefined;
|
return id ? buildMessagingTarget("channel", id, trimmed) : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("slack:")) {
|
if (trimmed.startsWith("slack:")) {
|
||||||
const id = trimmed.slice("slack:".length).trim();
|
const id = trimmed.slice("slack:".length).trim();
|
||||||
return id ? buildTarget("user", id, trimmed) : undefined;
|
return id ? buildMessagingTarget("user", id, trimmed) : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("@")) {
|
if (trimmed.startsWith("@")) {
|
||||||
const candidate = trimmed.slice(1).trim();
|
const candidate = trimmed.slice(1).trim();
|
||||||
if (!/^[A-Z0-9]+$/i.test(candidate)) {
|
const id = ensureTargetId({
|
||||||
throw new Error("Slack DMs require a user id (use user:<id> or <@id>)");
|
candidate,
|
||||||
}
|
pattern: /^[A-Z0-9]+$/i,
|
||||||
return buildTarget("user", candidate, trimmed);
|
errorMessage: "Slack DMs require a user id (use user:<id> or <@id>)",
|
||||||
|
});
|
||||||
|
return buildMessagingTarget("user", id, trimmed);
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("#")) {
|
if (trimmed.startsWith("#")) {
|
||||||
const candidate = trimmed.slice(1).trim();
|
const candidate = trimmed.slice(1).trim();
|
||||||
if (!/^[A-Z0-9]+$/i.test(candidate)) {
|
const id = ensureTargetId({
|
||||||
throw new Error("Slack channels require a channel id (use channel:<id>)");
|
candidate,
|
||||||
}
|
pattern: /^[A-Z0-9]+$/i,
|
||||||
return buildTarget("channel", candidate, trimmed);
|
errorMessage: "Slack channels require a channel id (use channel:<id>)",
|
||||||
|
});
|
||||||
|
return buildMessagingTarget("channel", id, trimmed);
|
||||||
}
|
}
|
||||||
if (options.defaultKind) {
|
if (options.defaultKind) {
|
||||||
return buildTarget(options.defaultKind, trimmed, trimmed);
|
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
|
||||||
}
|
}
|
||||||
return buildTarget("channel", trimmed, trimmed);
|
return buildMessagingTarget("channel", trimmed, trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSlackChannelId(raw: string): string {
|
export function resolveSlackChannelId(raw: string): string {
|
||||||
const target = parseSlackTarget(raw, { defaultKind: "channel" });
|
const target = parseSlackTarget(raw, { defaultKind: "channel" });
|
||||||
if (!target) {
|
return requireTargetKind({ platform: "Slack", target, kind: "channel" });
|
||||||
throw new Error("Slack channel id is required.");
|
|
||||||
}
|
|
||||||
if (target.kind !== "channel") {
|
|
||||||
throw new Error("Slack channel id is required (use channel:<id>).");
|
|
||||||
}
|
|
||||||
return target.id;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user