refactor: unify target resolver metadata
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
listDiscordDirectoryGroupsFromConfig,
|
||||
listDiscordDirectoryPeersFromConfig,
|
||||
listSlackDirectoryGroupsFromConfig,
|
||||
listSlackDirectoryPeersFromConfig,
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
|
||||
describe("directory (config-backed)", () => {
|
||||
it("lists Slack peers/groups from config", async () => {
|
||||
const { slackPlugin } = await import("./slack.js");
|
||||
const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any;
|
||||
const cfg = {
|
||||
channels: {
|
||||
slack: {
|
||||
@@ -16,12 +24,11 @@ describe("directory (config-backed)", () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const peers = await slackPlugin.directory?.listPeers?.({
|
||||
const peers = await listSlackDirectoryPeersFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(peers?.map((e) => e.id).sort()).toEqual([
|
||||
"user:u123",
|
||||
@@ -30,19 +37,16 @@ describe("directory (config-backed)", () => {
|
||||
"user:u999",
|
||||
]);
|
||||
|
||||
const groups = await slackPlugin.directory?.listGroups?.({
|
||||
const groups = await listSlackDirectoryGroupsFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(groups?.map((e) => e.id)).toEqual(["channel:c111"]);
|
||||
});
|
||||
|
||||
it("lists Discord peers/groups from config (numeric ids only)", async () => {
|
||||
const { discordPlugin } = await import("./discord.js");
|
||||
const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any;
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -63,28 +67,24 @@ describe("directory (config-backed)", () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const peers = await discordPlugin.directory?.listPeers?.({
|
||||
const peers = await listDiscordDirectoryPeersFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(peers?.map((e) => e.id).sort()).toEqual(["user:111", "user:12345", "user:222"]);
|
||||
|
||||
const groups = await discordPlugin.directory?.listGroups?.({
|
||||
const groups = await listDiscordDirectoryGroupsFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(groups?.map((e) => e.id).sort()).toEqual(["channel:555", "channel:666"]);
|
||||
});
|
||||
|
||||
it("lists Telegram peers/groups from config", async () => {
|
||||
const { telegramPlugin } = await import("./telegram.js");
|
||||
const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any;
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
@@ -96,28 +96,24 @@ describe("directory (config-backed)", () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const peers = await telegramPlugin.directory?.listPeers?.({
|
||||
const peers = await listTelegramDirectoryPeersFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(peers?.map((e) => e.id).sort()).toEqual(["123", "456", "@alice", "@bob"]);
|
||||
|
||||
const groups = await telegramPlugin.directory?.listGroups?.({
|
||||
const groups = await listTelegramDirectoryGroupsFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(groups?.map((e) => e.id)).toEqual(["-1001"]);
|
||||
});
|
||||
|
||||
it("lists WhatsApp peers/groups from config", async () => {
|
||||
const { whatsappPlugin } = await import("./whatsapp.js");
|
||||
const runtime = { log: () => {}, error: () => {}, exit: () => {} } as any;
|
||||
const cfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
@@ -127,21 +123,19 @@ describe("directory (config-backed)", () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const peers = await whatsappPlugin.directory?.listPeers?.({
|
||||
const peers = await listWhatsAppDirectoryPeersFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(peers?.map((e) => e.id)).toEqual(["+15550000000"]);
|
||||
|
||||
const groups = await whatsappPlugin.directory?.listGroups?.({
|
||||
const groups = await listWhatsAppDirectoryGroupsFromConfig({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
query: null,
|
||||
limit: null,
|
||||
runtime,
|
||||
});
|
||||
expect(groups?.map((e) => e.id)).toEqual(["999@g.us"]);
|
||||
});
|
||||
|
||||
214
src/channels/plugins/directory-config.ts
Normal file
214
src/channels/plugins/directory-config.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { ClawdbotConfig } from "../../config/types.js";
|
||||
import type { ChannelDirectoryEntry } from "./types.js";
|
||||
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 { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||
|
||||
export type DirectoryConfigParams = {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
query?: string | null;
|
||||
limit?: number | null;
|
||||
};
|
||||
|
||||
export async function listSlackDirectoryPeersFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const entry of account.dm?.allowFrom ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (!raw || raw === "*") continue;
|
||||
ids.add(raw);
|
||||
}
|
||||
for (const id of Object.keys(account.config.dms ?? {})) {
|
||||
const trimmed = id.trim();
|
||||
if (trimmed) ids.add(trimmed);
|
||||
}
|
||||
for (const channel of Object.values(account.config.channels ?? {})) {
|
||||
for (const user of channel.users ?? []) {
|
||||
const raw = String(user).trim();
|
||||
if (raw) ids.add(raw);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<@([A-Z0-9]+)>$/i);
|
||||
const normalizedUserId = (mention?.[1] ?? raw).replace(/^(slack|user):/i, "").trim();
|
||||
if (!normalizedUserId) return null;
|
||||
const target = `user:${normalizedUserId}`;
|
||||
return normalizeSlackMessagingTarget(target) ?? target.toLowerCase();
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => id.startsWith("user:"))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
}
|
||||
|
||||
export async function listSlackDirectoryGroupsFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
return Object.keys(account.config.channels ?? {})
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => normalizeSlackMessagingTarget(raw) ?? raw.toLowerCase())
|
||||
.filter((id) => id.startsWith("channel:"))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
}
|
||||
|
||||
export async function listDiscordDirectoryPeersFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const entry of account.config.dm?.allowFrom ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (!raw || raw === "*") continue;
|
||||
ids.add(raw);
|
||||
}
|
||||
for (const id of Object.keys(account.config.dms ?? {})) {
|
||||
const trimmed = id.trim();
|
||||
if (trimmed) ids.add(trimmed);
|
||||
}
|
||||
for (const guild of Object.values(account.config.guilds ?? {})) {
|
||||
for (const entry of guild.users ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (raw) ids.add(raw);
|
||||
}
|
||||
for (const channel of Object.values(guild.channels ?? {})) {
|
||||
for (const user of channel.users ?? []) {
|
||||
const raw = String(user).trim();
|
||||
if (raw) ids.add(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<@!?(\d+)>$/);
|
||||
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim();
|
||||
if (!/^\d+$/.test(cleaned)) return null;
|
||||
return `user:${cleaned}`;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
}
|
||||
|
||||
export async function listDiscordDirectoryGroupsFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
for (const guild of Object.values(account.config.guilds ?? {})) {
|
||||
for (const channelId of Object.keys(guild.channels ?? {})) {
|
||||
const trimmed = channelId.trim();
|
||||
if (trimmed) ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<#(\d+)>$/);
|
||||
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim();
|
||||
if (!/^\d+$/.test(cleaned)) return null;
|
||||
return `channel:${cleaned}`;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
}
|
||||
|
||||
export async function listTelegramDirectoryPeersFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const raw = [
|
||||
...(account.config.allowFrom ?? []).map((entry) => String(entry)),
|
||||
...Object.keys(account.config.dms ?? {}),
|
||||
];
|
||||
return Array.from(
|
||||
new Set(
|
||||
raw
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => entry.replace(/^(telegram|tg):/i, "")),
|
||||
),
|
||||
)
|
||||
.map((entry) => {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return null;
|
||||
if (/^-?\d+$/.test(trimmed)) return trimmed;
|
||||
const withAt = trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
|
||||
return withAt;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
}
|
||||
|
||||
export async function listTelegramDirectoryGroupsFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveTelegramAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
return Object.keys(account.config.groups ?? {})
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => Boolean(id) && id !== "*")
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
}
|
||||
|
||||
export async function listWhatsAppDirectoryPeersFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
return (account.allowFrom ?? [])
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*")
|
||||
.map((entry) => normalizeWhatsAppTarget(entry) ?? "")
|
||||
.filter(Boolean)
|
||||
.filter((id) => !isWhatsAppGroupJid(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
}
|
||||
|
||||
export async function listWhatsAppDirectoryGroupsFromConfig(
|
||||
params: DirectoryConfigParams,
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
return Object.keys(account.groups ?? {})
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => Boolean(id) && id !== "*")
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
}
|
||||
@@ -38,6 +38,10 @@ import {
|
||||
} from "./setup-helpers.js";
|
||||
import { collectDiscordStatusIssues } from "./status-issues/discord.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import {
|
||||
listDiscordDirectoryGroupsFromConfig,
|
||||
listDiscordDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
|
||||
const meta = getChatChannelMeta("discord");
|
||||
|
||||
@@ -152,79 +156,15 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeDiscordMessagingTarget,
|
||||
looksLikeTargetId: looksLikeDiscordTargetId,
|
||||
targetHint: "<channelId|user:ID|channel:ID>",
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeDiscordTargetId,
|
||||
hint: "<channelId|user:ID|channel:ID>",
|
||||
},
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
listPeers: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveDiscordAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const entry of account.config.dm?.allowFrom ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (!raw || raw === "*") continue;
|
||||
ids.add(raw);
|
||||
}
|
||||
for (const id of Object.keys(account.config.dms ?? {})) {
|
||||
const trimmed = id.trim();
|
||||
if (trimmed) ids.add(trimmed);
|
||||
}
|
||||
for (const guild of Object.values(account.config.guilds ?? {})) {
|
||||
for (const entry of guild.users ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (raw) ids.add(raw);
|
||||
}
|
||||
for (const channel of Object.values(guild.channels ?? {})) {
|
||||
for (const user of channel.users ?? []) {
|
||||
const raw = String(user).trim();
|
||||
if (raw) ids.add(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const peers = Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<@!?(\d+)>$/);
|
||||
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim();
|
||||
if (!/^\d+$/.test(cleaned)) return null;
|
||||
return `user:${cleaned}`;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
return peers;
|
||||
},
|
||||
listGroups: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveDiscordAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
for (const guild of Object.values(account.config.guilds ?? {})) {
|
||||
for (const channelId of Object.keys(guild.channels ?? {})) {
|
||||
const trimmed = channelId.trim();
|
||||
if (trimmed) ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
const groups = Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<#(\d+)>$/);
|
||||
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim();
|
||||
if (!/^\d+$/.test(cleaned)) return null;
|
||||
return `channel:${cleaned}`;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
return groups;
|
||||
},
|
||||
listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params),
|
||||
listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params),
|
||||
listGroupsLive: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveDiscordAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
|
||||
@@ -107,14 +107,16 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
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);
|
||||
targetResolver: {
|
||||
looksLikeId: (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);
|
||||
},
|
||||
hint: "<handle|chat_id:ID>",
|
||||
},
|
||||
targetHint: "<handle|chat_id:ID>",
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
|
||||
@@ -118,8 +118,10 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSignalMessagingTarget,
|
||||
looksLikeTargetId: looksLikeSignalTargetId,
|
||||
targetHint: "<E.164|group:ID|signal:group:ID|signal:+E.164>",
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeSignalTargetId,
|
||||
hint: "<E.164|group:ID|signal:group:ID|signal:+E.164>",
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
migrateBaseNameToDefaultAccount,
|
||||
} from "./setup-helpers.js";
|
||||
import type { ChannelMessageActionName, ChannelPlugin } from "./types.js";
|
||||
import {
|
||||
listSlackDirectoryGroupsFromConfig,
|
||||
listSlackDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
|
||||
const meta = getChatChannelMeta("slack");
|
||||
|
||||
@@ -177,62 +181,15 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSlackMessagingTarget,
|
||||
looksLikeTargetId: looksLikeSlackTargetId,
|
||||
targetHint: "<channelId|user:ID|channel:ID>",
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeSlackTargetId,
|
||||
hint: "<channelId|user:ID|channel:ID>",
|
||||
},
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
listPeers: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const entry of account.dm?.allowFrom ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (!raw || raw === "*") continue;
|
||||
ids.add(raw);
|
||||
}
|
||||
for (const id of Object.keys(account.config.dms ?? {})) {
|
||||
const trimmed = id.trim();
|
||||
if (trimmed) ids.add(trimmed);
|
||||
}
|
||||
for (const channel of Object.values(account.config.channels ?? {})) {
|
||||
for (const user of channel.users ?? []) {
|
||||
const raw = String(user).trim();
|
||||
if (raw) ids.add(raw);
|
||||
}
|
||||
}
|
||||
|
||||
const peers = Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<@([A-Z0-9]+)>$/i);
|
||||
const normalizedUserId = (mention?.[1] ?? raw).replace(/^(slack|user):/i, "").trim();
|
||||
if (!normalizedUserId) return null;
|
||||
const target = `user:${normalizedUserId}`;
|
||||
return normalizeSlackMessagingTarget(target) ?? target.toLowerCase();
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => id.startsWith("user:"))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
return peers;
|
||||
},
|
||||
listGroups: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveSlackAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const groups = Object.keys(account.config.channels ?? {})
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => normalizeSlackMessagingTarget(raw) ?? raw.toLowerCase())
|
||||
.filter((id) => id.startsWith("channel:"))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
return groups;
|
||||
},
|
||||
listPeers: async (params) => listSlackDirectoryPeersFromConfig(params),
|
||||
listGroups: async (params) => listSlackDirectoryGroupsFromConfig(params),
|
||||
},
|
||||
actions: {
|
||||
listActions: ({ cfg }) => {
|
||||
|
||||
@@ -38,6 +38,10 @@ import {
|
||||
} from "./setup-helpers.js";
|
||||
import { collectTelegramStatusIssues } from "./status-issues/telegram.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
import {
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
|
||||
const meta = getChatChannelMeta("telegram");
|
||||
|
||||
@@ -160,50 +164,15 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeTelegramMessagingTarget,
|
||||
looksLikeTargetId: looksLikeTelegramTargetId,
|
||||
targetHint: "<chatId>",
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeTelegramTargetId,
|
||||
hint: "<chatId>",
|
||||
},
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
listPeers: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveTelegramAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const raw = [
|
||||
...(account.config.allowFrom ?? []).map((entry) => String(entry)),
|
||||
...Object.keys(account.config.dms ?? {}),
|
||||
];
|
||||
const peers = Array.from(
|
||||
new Set(
|
||||
raw
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => entry.replace(/^(telegram|tg):/i, "")),
|
||||
),
|
||||
)
|
||||
.map((entry) => {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return null;
|
||||
if (/^-?\d+$/.test(trimmed)) return trimmed;
|
||||
const withAt = trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
|
||||
return withAt;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
return peers;
|
||||
},
|
||||
listGroups: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveTelegramAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const groups = Object.keys(account.config.groups ?? {})
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => Boolean(id) && id !== "*")
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
return groups;
|
||||
},
|
||||
listPeers: async (params) => listTelegramDirectoryPeersFromConfig(params),
|
||||
listGroups: async (params) => listTelegramDirectoryGroupsFromConfig(params),
|
||||
},
|
||||
actions: telegramMessageActions,
|
||||
setup: {
|
||||
|
||||
@@ -216,13 +216,15 @@ export type ChannelThreadingToolContext = {
|
||||
|
||||
export type ChannelMessagingAdapter = {
|
||||
normalizeTarget?: (raw: string) => string | undefined;
|
||||
looksLikeTargetId?: (raw: string, normalized?: string) => boolean;
|
||||
targetResolver?: {
|
||||
looksLikeId?: (raw: string, normalized?: string) => boolean;
|
||||
hint?: string;
|
||||
};
|
||||
formatTargetDisplay?: (params: {
|
||||
target: string;
|
||||
display?: string;
|
||||
kind?: ChannelDirectoryEntryKind;
|
||||
}) => string;
|
||||
targetHint?: string;
|
||||
};
|
||||
|
||||
export type ChannelDirectoryEntryKind = "user" | "group" | "channel";
|
||||
|
||||
@@ -39,6 +39,10 @@ import { collectWhatsAppStatusIssues } from "./status-issues/whatsapp.js";
|
||||
import type { ChannelMessageActionName, ChannelPlugin } from "./types.js";
|
||||
import { resolveWhatsAppHeartbeatRecipients } from "./whatsapp-heartbeat.js";
|
||||
import { missingTargetError } from "../../infra/outbound/target-errors.js";
|
||||
import {
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
} from "./directory-config.js";
|
||||
|
||||
const meta = getChatChannelMeta("whatsapp");
|
||||
|
||||
@@ -222,8 +226,10 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeWhatsAppMessagingTarget,
|
||||
looksLikeTargetId: looksLikeWhatsAppTargetId,
|
||||
targetHint: "<E.164|group JID>",
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeWhatsAppTargetId,
|
||||
hint: "<E.164|group JID>",
|
||||
},
|
||||
},
|
||||
directory: {
|
||||
self: async ({ cfg, accountId }) => {
|
||||
@@ -238,31 +244,8 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
raw: { e164, jid },
|
||||
};
|
||||
},
|
||||
listPeers: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveWhatsAppAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const peers = (account.allowFrom ?? [])
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*")
|
||||
.map((entry) => normalizeWhatsAppTarget(entry) ?? "")
|
||||
.filter(Boolean)
|
||||
.filter((id) => !isWhatsAppGroupJid(id))
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "user", id }) as const);
|
||||
return peers;
|
||||
},
|
||||
listGroups: async ({ cfg, accountId, query, limit }) => {
|
||||
const account = resolveWhatsAppAccount({ cfg, accountId });
|
||||
const q = query?.trim().toLowerCase() || "";
|
||||
const groups = Object.keys(account.groups ?? {})
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => Boolean(id) && id !== "*")
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, limit && limit > 0 ? limit : undefined)
|
||||
.map((id) => ({ kind: "group", id }) as const);
|
||||
return groups;
|
||||
},
|
||||
listPeers: async (params) => listWhatsAppDirectoryPeersFromConfig(params),
|
||||
listGroups: async (params) => listWhatsAppDirectoryGroupsFromConfig(params),
|
||||
},
|
||||
actions: {
|
||||
listActions: ({ cfg }) => {
|
||||
|
||||
@@ -16,8 +16,9 @@ export function normalizeTargetForProvider(provider: string, raw?: string): stri
|
||||
|
||||
export function buildTargetResolverSignature(channel: ChannelId): string {
|
||||
const plugin = getChannelPlugin(channel);
|
||||
const hint = plugin?.messaging?.targetHint ?? "";
|
||||
const looksLike = plugin?.messaging?.looksLikeTargetId;
|
||||
const resolver = plugin?.messaging?.targetResolver;
|
||||
const hint = resolver?.hint ?? "";
|
||||
const looksLike = resolver?.looksLikeId;
|
||||
const source = looksLike ? looksLike.toString() : "";
|
||||
return hashSignature(`${hint}|${source}`);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ function looksLikeTargetId(params: {
|
||||
const raw = params.raw.trim();
|
||||
if (!raw) return false;
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const lookup = plugin?.messaging?.looksLikeTargetId;
|
||||
const lookup = plugin?.messaging?.targetResolver?.looksLikeId;
|
||||
if (lookup) return lookup(raw, params.normalized);
|
||||
if (/^(channel|group|user):/i.test(raw)) return true;
|
||||
if (/^[@#]/.test(raw)) return true;
|
||||
@@ -285,7 +285,7 @@ export async function resolveMessagingTarget(params: {
|
||||
}
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const providerLabel = plugin?.meta?.label ?? params.channel;
|
||||
const hint = plugin?.messaging?.targetHint;
|
||||
const hint = plugin?.messaging?.targetResolver?.hint;
|
||||
const kind = detectTargetKind(raw, params.preferredKind);
|
||||
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
|
||||
if (looksLikeTargetId({ channel: params.channel, raw, normalized })) {
|
||||
|
||||
@@ -146,7 +146,7 @@ export function resolveOutboundTarget(params: {
|
||||
if (trimmed) {
|
||||
return { ok: true, to: trimmed };
|
||||
}
|
||||
const hint = plugin.messaging?.targetHint;
|
||||
const hint = plugin.messaging?.targetResolver?.hint;
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError(plugin.meta.label ?? params.channel, hint),
|
||||
|
||||
Reference in New Issue
Block a user