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 { resolveTelegramAccount } from "../../telegram/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";
|
||||
|
||||
export type DirectoryConfigParams = {
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveDiscordGroupRequireMention } from "./group-mentions.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 { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
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";
|
||||
import { formatPairingApproveHint } from "./helpers.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 { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveSlackGroupRequireMention } from "./group-mentions.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 { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveTelegramGroupRequireMention } from "./group-mentions.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 { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
|
||||
@@ -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 { looksLikeWhatsAppTargetId, normalizeWhatsAppMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
} from "./normalize/whatsapp.js";
|
||||
import { whatsappOnboardingAdapter } from "./onboarding/whatsapp.js";
|
||||
import {
|
||||
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 { normalizeDiscordMessagingTarget } from "../channels/plugins/normalize-target.js";
|
||||
import { normalizeDiscordMessagingTarget } from "../channels/plugins/normalize/discord.js";
|
||||
import { parseDiscordTarget, resolveDiscordChannelId } from "./targets.js";
|
||||
|
||||
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 = {
|
||||
kind: DiscordTargetKind;
|
||||
id: string;
|
||||
raw: string;
|
||||
normalized: string;
|
||||
};
|
||||
export type DiscordTargetKind = MessagingTargetKind;
|
||||
|
||||
type DiscordTargetParseOptions = {
|
||||
defaultKind?: DiscordTargetKind;
|
||||
ambiguousMessage?: string;
|
||||
};
|
||||
export type DiscordTarget = MessagingTarget;
|
||||
|
||||
function normalizeTargetId(kind: DiscordTargetKind, id: string) {
|
||||
return `${kind}:${id}`.toLowerCase();
|
||||
}
|
||||
|
||||
function buildTarget(kind: DiscordTargetKind, id: string, raw: string): DiscordTarget {
|
||||
return {
|
||||
kind,
|
||||
id,
|
||||
raw,
|
||||
normalized: normalizeTargetId(kind, id),
|
||||
};
|
||||
}
|
||||
type DiscordTargetParseOptions = MessagingTargetParseOptions;
|
||||
|
||||
export function parseDiscordTarget(
|
||||
raw: string,
|
||||
@@ -33,46 +21,42 @@ export function parseDiscordTarget(
|
||||
if (!trimmed) return undefined;
|
||||
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
|
||||
if (mentionMatch) {
|
||||
return buildTarget("user", mentionMatch[1], trimmed);
|
||||
return buildMessagingTarget("user", mentionMatch[1], trimmed);
|
||||
}
|
||||
if (trimmed.startsWith("user:")) {
|
||||
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:")) {
|
||||
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:")) {
|
||||
const id = trimmed.slice("discord:".length).trim();
|
||||
return id ? buildTarget("user", id, trimmed) : undefined;
|
||||
return id ? buildMessagingTarget("user", id, trimmed) : undefined;
|
||||
}
|
||||
if (trimmed.startsWith("@")) {
|
||||
const candidate = trimmed.slice(1).trim();
|
||||
if (!/^\d+$/.test(candidate)) {
|
||||
throw new Error("Discord DMs require a user id (use user:<id> or a <@id> mention)");
|
||||
}
|
||||
return buildTarget("user", candidate, trimmed);
|
||||
const id = ensureTargetId({
|
||||
candidate,
|
||||
pattern: /^\d+$/,
|
||||
errorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)",
|
||||
});
|
||||
return buildMessagingTarget("user", id, trimmed);
|
||||
}
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
if (options.defaultKind) {
|
||||
return buildTarget(options.defaultKind, trimmed, trimmed);
|
||||
return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
|
||||
}
|
||||
throw new Error(
|
||||
options.ambiguousMessage ??
|
||||
`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 {
|
||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||
if (!target) {
|
||||
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;
|
||||
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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";
|
||||
|
||||
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 = {
|
||||
kind: SlackTargetKind;
|
||||
id: string;
|
||||
raw: string;
|
||||
normalized: string;
|
||||
};
|
||||
export type SlackTargetKind = MessagingTargetKind;
|
||||
|
||||
type SlackTargetParseOptions = {
|
||||
defaultKind?: SlackTargetKind;
|
||||
};
|
||||
export type SlackTarget = MessagingTarget;
|
||||
|
||||
function normalizeTargetId(kind: SlackTargetKind, id: string) {
|
||||
return `${kind}:${id}`.toLowerCase();
|
||||
}
|
||||
|
||||
function buildTarget(kind: SlackTargetKind, id: string, raw: string): SlackTarget {
|
||||
return {
|
||||
kind,
|
||||
id,
|
||||
raw,
|
||||
normalized: normalizeTargetId(kind, id),
|
||||
};
|
||||
}
|
||||
type SlackTargetParseOptions = MessagingTargetParseOptions;
|
||||
|
||||
export function parseSlackTarget(
|
||||
raw: string,
|
||||
@@ -32,47 +21,45 @@ export function parseSlackTarget(
|
||||
if (!trimmed) return undefined;
|
||||
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||
if (mentionMatch) {
|
||||
return buildTarget("user", mentionMatch[1], trimmed);
|
||||
return buildMessagingTarget("user", mentionMatch[1], trimmed);
|
||||
}
|
||||
if (trimmed.startsWith("user:")) {
|
||||
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:")) {
|
||||
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:")) {
|
||||
const id = trimmed.slice("slack:".length).trim();
|
||||
return id ? buildTarget("user", id, trimmed) : undefined;
|
||||
return id ? buildMessagingTarget("user", id, trimmed) : undefined;
|
||||
}
|
||||
if (trimmed.startsWith("@")) {
|
||||
const candidate = trimmed.slice(1).trim();
|
||||
if (!/^[A-Z0-9]+$/i.test(candidate)) {
|
||||
throw new Error("Slack DMs require a user id (use user:<id> or <@id>)");
|
||||
}
|
||||
return buildTarget("user", candidate, trimmed);
|
||||
const id = ensureTargetId({
|
||||
candidate,
|
||||
pattern: /^[A-Z0-9]+$/i,
|
||||
errorMessage: "Slack DMs require a user id (use user:<id> or <@id>)",
|
||||
});
|
||||
return buildMessagingTarget("user", id, trimmed);
|
||||
}
|
||||
if (trimmed.startsWith("#")) {
|
||||
const candidate = trimmed.slice(1).trim();
|
||||
if (!/^[A-Z0-9]+$/i.test(candidate)) {
|
||||
throw new Error("Slack channels require a channel id (use channel:<id>)");
|
||||
}
|
||||
return buildTarget("channel", candidate, trimmed);
|
||||
const id = ensureTargetId({
|
||||
candidate,
|
||||
pattern: /^[A-Z0-9]+$/i,
|
||||
errorMessage: "Slack channels require a channel id (use channel:<id>)",
|
||||
});
|
||||
return buildMessagingTarget("channel", id, trimmed);
|
||||
}
|
||||
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 {
|
||||
const target = parseSlackTarget(raw, { defaultKind: "channel" });
|
||||
if (!target) {
|
||||
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;
|
||||
return requireTargetKind({ platform: "Slack", target, kind: "channel" });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user