mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 22:51:23 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -15,14 +15,28 @@ export type AckReactionGateParams = {
|
||||
|
||||
export function shouldAckReaction(params: AckReactionGateParams): boolean {
|
||||
const scope = params.scope ?? "group-mentions";
|
||||
if (scope === "off" || scope === "none") return false;
|
||||
if (scope === "all") return true;
|
||||
if (scope === "direct") return params.isDirect;
|
||||
if (scope === "group-all") return params.isGroup;
|
||||
if (scope === "off" || scope === "none") {
|
||||
return false;
|
||||
}
|
||||
if (scope === "all") {
|
||||
return true;
|
||||
}
|
||||
if (scope === "direct") {
|
||||
return params.isDirect;
|
||||
}
|
||||
if (scope === "group-all") {
|
||||
return params.isGroup;
|
||||
}
|
||||
if (scope === "group-mentions") {
|
||||
if (!params.isMentionableGroup) return false;
|
||||
if (!params.requireMention) return false;
|
||||
if (!params.canDetectMention) return false;
|
||||
if (!params.isMentionableGroup) {
|
||||
return false;
|
||||
}
|
||||
if (!params.requireMention) {
|
||||
return false;
|
||||
}
|
||||
if (!params.canDetectMention) {
|
||||
return false;
|
||||
}
|
||||
return params.effectiveWasMentioned || params.shouldBypassMention === true;
|
||||
}
|
||||
return false;
|
||||
@@ -37,11 +51,21 @@ export function shouldAckReactionForWhatsApp(params: {
|
||||
wasMentioned: boolean;
|
||||
groupActivated: boolean;
|
||||
}): boolean {
|
||||
if (!params.emoji) return false;
|
||||
if (params.isDirect) return params.directEnabled;
|
||||
if (!params.isGroup) return false;
|
||||
if (params.groupMode === "never") return false;
|
||||
if (params.groupMode === "always") return true;
|
||||
if (!params.emoji) {
|
||||
return false;
|
||||
}
|
||||
if (params.isDirect) {
|
||||
return params.directEnabled;
|
||||
}
|
||||
if (!params.isGroup) {
|
||||
return false;
|
||||
}
|
||||
if (params.groupMode === "never") {
|
||||
return false;
|
||||
}
|
||||
if (params.groupMode === "always") {
|
||||
return true;
|
||||
}
|
||||
return shouldAckReaction({
|
||||
scope: "group-mentions",
|
||||
isDirect: false,
|
||||
@@ -61,11 +85,19 @@ export function removeAckReactionAfterReply(params: {
|
||||
remove: () => Promise<void>;
|
||||
onError?: (err: unknown) => void;
|
||||
}) {
|
||||
if (!params.removeAfterReply) return;
|
||||
if (!params.ackReactionPromise) return;
|
||||
if (!params.ackReactionValue) return;
|
||||
if (!params.removeAfterReply) {
|
||||
return;
|
||||
}
|
||||
if (!params.ackReactionPromise) {
|
||||
return;
|
||||
}
|
||||
if (!params.ackReactionValue) {
|
||||
return;
|
||||
}
|
||||
void params.ackReactionPromise.then((didAck) => {
|
||||
if (!didAck) return;
|
||||
if (!didAck) {
|
||||
return;
|
||||
}
|
||||
params.remove().catch((err) => params.onError?.(err));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ export function mergeAllowlist(params: {
|
||||
const merged: string[] = [];
|
||||
const push = (value: string) => {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return;
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const key = normalized.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
if (seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
merged.push(normalized);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,9 @@ export function resolveChannelMatchConfig<
|
||||
TEntry,
|
||||
TResult extends { matchKey?: string; matchSource?: ChannelMatchSource },
|
||||
>(match: ChannelEntryMatch<TEntry>, resolveEntry: (entry: TEntry) => TResult): TResult | null {
|
||||
if (!match.entry) return null;
|
||||
if (!match.entry) {
|
||||
return null;
|
||||
}
|
||||
return applyChannelMatchMeta(resolveEntry(match.entry), match);
|
||||
}
|
||||
|
||||
@@ -42,9 +44,13 @@ export function buildChannelKeyCandidates(...keys: Array<string | undefined | nu
|
||||
const seen = new Set<string>();
|
||||
const candidates: string[] = [];
|
||||
for (const key of keys) {
|
||||
if (typeof key !== "string") continue;
|
||||
if (typeof key !== "string") {
|
||||
continue;
|
||||
}
|
||||
const trimmed = key.trim();
|
||||
if (!trimmed || seen.has(trimmed)) continue;
|
||||
if (!trimmed || seen.has(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
candidates.push(trimmed);
|
||||
}
|
||||
@@ -59,7 +65,9 @@ export function resolveChannelEntryMatch<T>(params: {
|
||||
const entries = params.entries ?? {};
|
||||
const match: ChannelEntryMatch<T> = {};
|
||||
for (const key of params.keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
|
||||
if (!Object.prototype.hasOwnProperty.call(entries, key)) {
|
||||
continue;
|
||||
}
|
||||
match.entry = entries[key];
|
||||
match.key = key;
|
||||
break;
|
||||
@@ -161,8 +169,14 @@ export function resolveNestedAllowlistDecision(params: {
|
||||
innerConfigured: boolean;
|
||||
innerMatched: boolean;
|
||||
}): boolean {
|
||||
if (!params.outerConfigured) return true;
|
||||
if (!params.outerMatched) return false;
|
||||
if (!params.innerConfigured) return true;
|
||||
if (!params.outerConfigured) {
|
||||
return true;
|
||||
}
|
||||
if (!params.outerMatched) {
|
||||
return false;
|
||||
}
|
||||
if (!params.innerConfigured) {
|
||||
return true;
|
||||
}
|
||||
return params.innerMatched;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,17 @@ export type NormalizedChatType = "direct" | "group" | "channel";
|
||||
|
||||
export function normalizeChatType(raw?: string): NormalizedChatType | undefined {
|
||||
const value = raw?.trim().toLowerCase();
|
||||
if (!value) return undefined;
|
||||
if (value === "direct" || value === "dm") return "direct";
|
||||
if (value === "group") return "group";
|
||||
if (value === "channel") return "channel";
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "direct" || value === "dm") {
|
||||
return "direct";
|
||||
}
|
||||
if (value === "group") {
|
||||
return "group";
|
||||
}
|
||||
if (value === "channel") {
|
||||
return "channel";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,16 @@ export function resolveCommandAuthorizedFromAuthorizers(params: {
|
||||
const { useAccessGroups, authorizers } = params;
|
||||
const mode = params.modeWhenAccessGroupsOff ?? "allow";
|
||||
if (!useAccessGroups) {
|
||||
if (mode === "allow") return true;
|
||||
if (mode === "deny") return false;
|
||||
if (mode === "allow") {
|
||||
return true;
|
||||
}
|
||||
if (mode === "deny") {
|
||||
return false;
|
||||
}
|
||||
const anyConfigured = authorizers.some((entry) => entry.configured);
|
||||
if (!anyConfigured) return true;
|
||||
if (!anyConfigured) {
|
||||
return true;
|
||||
}
|
||||
return authorizers.some((entry) => entry.configured && entry.allowed);
|
||||
}
|
||||
return authorizers.some((entry) => entry.configured && entry.allowed);
|
||||
|
||||
@@ -3,23 +3,33 @@ import { normalizeChatType } from "./chat-type.js";
|
||||
|
||||
function extractConversationId(from?: string): string | undefined {
|
||||
const trimmed = from?.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = trimmed.split(":").filter(Boolean);
|
||||
return parts.length > 0 ? parts[parts.length - 1] : trimmed;
|
||||
}
|
||||
|
||||
function shouldAppendId(id: string): boolean {
|
||||
if (/^[0-9]+$/.test(id)) return true;
|
||||
if (id.includes("@g.us")) return true;
|
||||
if (/^[0-9]+$/.test(id)) {
|
||||
return true;
|
||||
}
|
||||
if (id.includes("@g.us")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function resolveConversationLabel(ctx: MsgContext): string | undefined {
|
||||
const explicit = ctx.ConversationLabel?.trim();
|
||||
if (explicit) return explicit;
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const threadLabel = ctx.ThreadLabel?.trim();
|
||||
if (threadLabel) return threadLabel;
|
||||
if (threadLabel) {
|
||||
return threadLabel;
|
||||
}
|
||||
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
if (chatType === "direct") {
|
||||
@@ -32,14 +42,28 @@ export function resolveConversationLabel(ctx: MsgContext): string | undefined {
|
||||
ctx.GroupSpace?.trim() ||
|
||||
ctx.From?.trim() ||
|
||||
"";
|
||||
if (!base) return undefined;
|
||||
if (!base) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const id = extractConversationId(ctx.From);
|
||||
if (!id) return base;
|
||||
if (!shouldAppendId(id)) return base;
|
||||
if (base === id) return base;
|
||||
if (base.includes(id)) return base;
|
||||
if (base.toLowerCase().includes(" id:")) return base;
|
||||
if (base.startsWith("#") || base.startsWith("@")) return base;
|
||||
if (!id) {
|
||||
return base;
|
||||
}
|
||||
if (!shouldAppendId(id)) {
|
||||
return base;
|
||||
}
|
||||
if (base === id) {
|
||||
return base;
|
||||
}
|
||||
if (base.includes(id)) {
|
||||
return base;
|
||||
}
|
||||
if (base.toLowerCase().includes(" id:")) {
|
||||
return base;
|
||||
}
|
||||
if (base.startsWith("#") || base.startsWith("@")) {
|
||||
return base;
|
||||
}
|
||||
return `${base} id:${id}`;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,9 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
||||
mentions: {
|
||||
stripPatterns: ({ ctx }) => {
|
||||
const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/, "");
|
||||
if (!selfE164) return [];
|
||||
if (!selfE164) {
|
||||
return [];
|
||||
}
|
||||
const escaped = escapeRegExp(selfE164);
|
||||
return [escaped, `@${escaped}`];
|
||||
},
|
||||
@@ -403,9 +405,13 @@ function listPluginDockEntries(): Array<{ id: ChannelId; dock: ChannelDock; orde
|
||||
for (const entry of registry.channels) {
|
||||
const plugin = entry.plugin;
|
||||
const id = String(plugin.id).trim();
|
||||
if (!id || seen.has(id)) continue;
|
||||
if (!id || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
if (CHAT_CHANNEL_ORDER.includes(plugin.id as ChatChannelId)) continue;
|
||||
if (CHAT_CHANNEL_ORDER.includes(plugin.id as ChatChannelId)) {
|
||||
continue;
|
||||
}
|
||||
const dock = entry.dock ?? buildDockFromPlugin(plugin);
|
||||
entries.push({ id: plugin.id, dock, order: plugin.meta.order });
|
||||
}
|
||||
@@ -425,7 +431,9 @@ export function listChannelDocks(): ChannelDock[] {
|
||||
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
||||
const orderA = a.order ?? (indexA === -1 ? 999 : indexA);
|
||||
const orderB = b.order ?? (indexB === -1 ? 999 : indexB);
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
return combined.map((entry) => entry.dock);
|
||||
@@ -433,9 +441,13 @@ export function listChannelDocks(): ChannelDock[] {
|
||||
|
||||
export function getChannelDock(id: ChannelId): ChannelDock | undefined {
|
||||
const core = DOCKS[id as ChatChannelId];
|
||||
if (core) return core;
|
||||
if (core) {
|
||||
return core;
|
||||
}
|
||||
const registry = requireActivePluginRegistry();
|
||||
const pluginEntry = registry.channels.find((entry) => entry.plugin.id === id);
|
||||
if (!pluginEntry) return undefined;
|
||||
if (!pluginEntry) {
|
||||
return undefined;
|
||||
}
|
||||
return pluginEntry.dock ?? buildDockFromPlugin(pluginEntry.plugin);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ function resolveLocation(location: NormalizedLocation): ResolvedLocation {
|
||||
}
|
||||
|
||||
function formatAccuracy(accuracy?: number): string {
|
||||
if (!Number.isFinite(accuracy)) return "";
|
||||
if (!Number.isFinite(accuracy)) {
|
||||
return "";
|
||||
}
|
||||
return ` ±${Math.round(accuracy ?? 0)}m`;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,14 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
const accounts = listEnabledDiscordAccounts(cfg).filter(
|
||||
(account) => account.tokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return [];
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const gate = createActionGate(cfg.channels?.discord?.actions);
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
if (gate("polls")) actions.add("poll");
|
||||
if (gate("polls")) {
|
||||
actions.add("poll");
|
||||
}
|
||||
if (gate("reactions")) {
|
||||
actions.add("react");
|
||||
actions.add("reactions");
|
||||
@@ -26,19 +30,35 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
actions.add("unpin");
|
||||
actions.add("list-pins");
|
||||
}
|
||||
if (gate("permissions")) actions.add("permissions");
|
||||
if (gate("permissions")) {
|
||||
actions.add("permissions");
|
||||
}
|
||||
if (gate("threads")) {
|
||||
actions.add("thread-create");
|
||||
actions.add("thread-list");
|
||||
actions.add("thread-reply");
|
||||
}
|
||||
if (gate("search")) actions.add("search");
|
||||
if (gate("stickers")) actions.add("sticker");
|
||||
if (gate("memberInfo")) actions.add("member-info");
|
||||
if (gate("roleInfo")) actions.add("role-info");
|
||||
if (gate("reactions")) actions.add("emoji-list");
|
||||
if (gate("emojiUploads")) actions.add("emoji-upload");
|
||||
if (gate("stickerUploads")) actions.add("sticker-upload");
|
||||
if (gate("search")) {
|
||||
actions.add("search");
|
||||
}
|
||||
if (gate("stickers")) {
|
||||
actions.add("sticker");
|
||||
}
|
||||
if (gate("memberInfo")) {
|
||||
actions.add("member-info");
|
||||
}
|
||||
if (gate("roleInfo")) {
|
||||
actions.add("role-info");
|
||||
}
|
||||
if (gate("reactions")) {
|
||||
actions.add("emoji-list");
|
||||
}
|
||||
if (gate("emojiUploads")) {
|
||||
actions.add("emoji-upload");
|
||||
}
|
||||
if (gate("stickerUploads")) {
|
||||
actions.add("sticker-upload");
|
||||
}
|
||||
if (gate("roles", false)) {
|
||||
actions.add("role-add");
|
||||
actions.add("role-remove");
|
||||
@@ -56,7 +76,9 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
|
||||
actions.add("category-edit");
|
||||
actions.add("category-delete");
|
||||
}
|
||||
if (gate("voiceStatus")) actions.add("voice-status");
|
||||
if (gate("voiceStatus")) {
|
||||
actions.add("voice-status");
|
||||
}
|
||||
if (gate("events")) {
|
||||
actions.add("event-list");
|
||||
actions.add("event-create");
|
||||
|
||||
@@ -12,8 +12,12 @@ import { resolveDiscordChannelId } from "../../../../discord/targets.js";
|
||||
const providerId = "discord";
|
||||
|
||||
function readParentIdParam(params: Record<string, unknown>): string | null | undefined {
|
||||
if (params.clearParent === true) return null;
|
||||
if (params.parentId === null) return null;
|
||||
if (params.clearParent === true) {
|
||||
return null;
|
||||
}
|
||||
if (params.parentId === null) {
|
||||
return null;
|
||||
}
|
||||
return readStringParam(params, "parentId");
|
||||
}
|
||||
|
||||
@@ -219,7 +223,9 @@ export async function handleDiscordMessageAction(
|
||||
resolveChannelId,
|
||||
readParentIdParam,
|
||||
});
|
||||
if (adminResult !== undefined) return adminResult;
|
||||
if (adminResult !== undefined) {
|
||||
return adminResult;
|
||||
}
|
||||
|
||||
throw new Error(`Action ${String(action)} is not supported for provider ${providerId}.`);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,13 @@ const GROUP_PREFIX = "group:";
|
||||
|
||||
function normalizeSignalReactionRecipient(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
const withoutSignal = trimmed.replace(/^signal:/i, "").trim();
|
||||
if (!withoutSignal) return withoutSignal;
|
||||
if (!withoutSignal) {
|
||||
return withoutSignal;
|
||||
}
|
||||
if (withoutSignal.toLowerCase().startsWith("uuid:")) {
|
||||
return withoutSignal.slice("uuid:".length).trim();
|
||||
}
|
||||
@@ -20,9 +24,13 @@ function normalizeSignalReactionRecipient(raw: string): string {
|
||||
|
||||
function resolveSignalReactionTarget(raw: string): { recipient?: string; groupId?: string } {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return {};
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
const withoutSignal = trimmed.replace(/^signal:/i, "").trim();
|
||||
if (!withoutSignal) return {};
|
||||
if (!withoutSignal) {
|
||||
return {};
|
||||
}
|
||||
if (withoutSignal.toLowerCase().startsWith(GROUP_PREFIX)) {
|
||||
const groupId = withoutSignal.slice(GROUP_PREFIX.length).trim();
|
||||
return groupId ? { groupId } : {};
|
||||
@@ -33,9 +41,13 @@ function resolveSignalReactionTarget(raw: string): { recipient?: string; groupId
|
||||
export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
listActions: ({ cfg }) => {
|
||||
const accounts = listEnabledSignalAccounts(cfg);
|
||||
if (accounts.length === 0) return [];
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const configuredAccounts = accounts.filter((account) => account.configured);
|
||||
if (configuredAccounts.length === 0) return [];
|
||||
if (configuredAccounts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
|
||||
@@ -105,7 +117,9 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
}
|
||||
|
||||
if (remove) {
|
||||
if (!emoji) throw new Error("Emoji required to remove reaction.");
|
||||
if (!emoji) {
|
||||
throw new Error("Emoji required to remove reaction.");
|
||||
}
|
||||
await removeReactionSignal(target.recipient ?? "", timestamp, emoji, {
|
||||
accountId: accountId ?? undefined,
|
||||
groupId: target.groupId,
|
||||
@@ -115,7 +129,9 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
|
||||
return jsonResult({ ok: true, removed: emoji });
|
||||
}
|
||||
|
||||
if (!emoji) throw new Error("Emoji required to add reaction.");
|
||||
if (!emoji) {
|
||||
throw new Error("Emoji required to add reaction.");
|
||||
}
|
||||
await sendReactionSignal(target.recipient ?? "", timestamp, emoji, {
|
||||
accountId: accountId ?? undefined,
|
||||
groupId: target.groupId,
|
||||
|
||||
@@ -42,12 +42,20 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
||||
(account) => account.tokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return [];
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const gate = createActionGate(cfg.channels?.telegram?.actions);
|
||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||
if (gate("reactions")) actions.add("react");
|
||||
if (gate("deleteMessage")) actions.add("delete");
|
||||
if (gate("editMessage")) actions.add("edit");
|
||||
if (gate("reactions")) {
|
||||
actions.add("react");
|
||||
}
|
||||
if (gate("deleteMessage")) {
|
||||
actions.add("delete");
|
||||
}
|
||||
if (gate("editMessage")) {
|
||||
actions.add("edit");
|
||||
}
|
||||
if (gate("sticker", false)) {
|
||||
actions.add("sticker");
|
||||
actions.add("sticker-search");
|
||||
@@ -58,16 +66,22 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
const accounts = listEnabledTelegramAccounts(cfg).filter(
|
||||
(account) => account.tokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return false;
|
||||
if (accounts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return accounts.some((account) =>
|
||||
isTelegramInlineButtonsEnabled({ cfg, accountId: account.accountId }),
|
||||
);
|
||||
},
|
||||
extractToolSend: ({ args }) => {
|
||||
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||
if (action !== "sendMessage") return null;
|
||||
if (action !== "sendMessage") {
|
||||
return null;
|
||||
}
|
||||
const to = typeof args.to === "string" ? args.to : undefined;
|
||||
if (!to) return null;
|
||||
if (!to) {
|
||||
return null;
|
||||
}
|
||||
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
||||
return { to, accountId };
|
||||
},
|
||||
|
||||
@@ -70,15 +70,21 @@ function parseCatalogEntries(raw: unknown): ExternalCatalogEntry[] {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.filter((entry): entry is ExternalCatalogEntry => isRecord(entry));
|
||||
}
|
||||
if (!isRecord(raw)) return [];
|
||||
if (!isRecord(raw)) {
|
||||
return [];
|
||||
}
|
||||
const list = raw.entries ?? raw.packages ?? raw.plugins;
|
||||
if (!Array.isArray(list)) return [];
|
||||
if (!Array.isArray(list)) {
|
||||
return [];
|
||||
}
|
||||
return list.filter((entry): entry is ExternalCatalogEntry => isRecord(entry));
|
||||
}
|
||||
|
||||
function splitEnvPaths(value: string): string[] {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return [];
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
return trimmed
|
||||
.split(/[;,]/g)
|
||||
.flatMap((chunk) => chunk.split(path.delimiter))
|
||||
@@ -104,7 +110,9 @@ function loadExternalCatalogEntries(options: CatalogOptions): ExternalCatalogEnt
|
||||
const entries: ExternalCatalogEntry[] = [];
|
||||
for (const rawPath of paths) {
|
||||
const resolved = resolveUserPath(rawPath);
|
||||
if (!fs.existsSync(resolved)) continue;
|
||||
if (!fs.existsSync(resolved)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(resolved, "utf-8")) as unknown;
|
||||
entries.push(...parseCatalogEntries(payload));
|
||||
@@ -120,7 +128,9 @@ function toChannelMeta(params: {
|
||||
id: string;
|
||||
}): ChannelMeta | null {
|
||||
const label = params.channel.label?.trim();
|
||||
if (!label) return null;
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
const selectionLabel = params.channel.selectionLabel?.trim() || label;
|
||||
const detailLabel = params.channel.detailLabel?.trim();
|
||||
const docsPath = params.channel.docsPath?.trim() || `/channels/${params.id}`;
|
||||
@@ -170,7 +180,9 @@ function resolveInstallInfo(params: {
|
||||
workspaceDir?: string;
|
||||
}): ChannelPluginCatalogEntry["install"] | null {
|
||||
const npmSpec = params.manifest.install?.npmSpec?.trim() ?? params.packageName?.trim();
|
||||
if (!npmSpec) return null;
|
||||
if (!npmSpec) {
|
||||
return null;
|
||||
}
|
||||
let localPath = params.manifest.install?.localPath?.trim() || undefined;
|
||||
if (!localPath && params.workspaceDir && params.packageDir) {
|
||||
localPath = path.relative(params.workspaceDir, params.packageDir) || undefined;
|
||||
@@ -190,18 +202,26 @@ function buildCatalogEntry(candidate: {
|
||||
packageManifest?: OpenClawPackageManifest;
|
||||
}): ChannelPluginCatalogEntry | null {
|
||||
const manifest = candidate.packageManifest;
|
||||
if (!manifest?.channel) return null;
|
||||
if (!manifest?.channel) {
|
||||
return null;
|
||||
}
|
||||
const id = manifest.channel.id?.trim();
|
||||
if (!id) return null;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const meta = toChannelMeta({ channel: manifest.channel, id });
|
||||
if (!meta) return null;
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
const install = resolveInstallInfo({
|
||||
manifest,
|
||||
packageName: candidate.packageName,
|
||||
packageDir: candidate.packageDir,
|
||||
workspaceDir: candidate.workspaceDir,
|
||||
});
|
||||
if (!install) return null;
|
||||
if (!install) {
|
||||
return null;
|
||||
}
|
||||
return { id, meta, install };
|
||||
}
|
||||
|
||||
@@ -249,7 +269,9 @@ export function listChannelPluginCatalogEntries(
|
||||
|
||||
for (const candidate of discovery.candidates) {
|
||||
const entry = buildCatalogEntry(candidate);
|
||||
if (!entry) continue;
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
const priority = ORIGIN_PRIORITY[candidate.origin] ?? 99;
|
||||
const existing = resolved.get(entry.id);
|
||||
if (!existing || priority < existing.priority) {
|
||||
@@ -271,7 +293,9 @@ export function listChannelPluginCatalogEntries(
|
||||
.toSorted((a, b) => {
|
||||
const orderA = a.meta.order ?? 999;
|
||||
const orderB = b.meta.order ?? 999;
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
return a.meta.label.localeCompare(b.meta.label);
|
||||
});
|
||||
}
|
||||
@@ -281,6 +305,8 @@ export function getChannelPluginCatalogEntry(
|
||||
options: CatalogOptions = {},
|
||||
): ChannelPluginCatalogEntry | undefined {
|
||||
const trimmed = id.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return listChannelPluginCatalogEntries(options).find((entry) => entry.id === trimmed);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,9 @@ export function deleteAccountFromConfigSection(params: {
|
||||
const accountKey = params.accountId || DEFAULT_ACCOUNT_ID;
|
||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
||||
const base = channels?.[params.sectionKey] as ChannelSection | undefined;
|
||||
if (!base) return params.cfg;
|
||||
if (!base) {
|
||||
return params.cfg;
|
||||
}
|
||||
|
||||
const baseAccounts =
|
||||
base.accounts && typeof base.accounts === "object" ? { ...base.accounts } : undefined;
|
||||
@@ -83,7 +85,9 @@ export function deleteAccountFromConfigSection(params: {
|
||||
delete baseAccounts[accountKey];
|
||||
const baseRecord = { ...(base as Record<string, unknown>) };
|
||||
for (const field of params.clearBaseFields ?? []) {
|
||||
if (field in baseRecord) baseRecord[field] = undefined;
|
||||
if (field in baseRecord) {
|
||||
baseRecord[field] = undefined;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...params.cfg,
|
||||
|
||||
@@ -8,8 +8,12 @@ type ChannelConfigWithAccounts = {
|
||||
};
|
||||
|
||||
function resolveAccountConfig(accounts: ChannelConfigWithAccounts["accounts"], accountId: string) {
|
||||
if (!accounts || typeof accounts !== "object") return undefined;
|
||||
if (accountId in accounts) return accounts[accountId];
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if (accountId in accounts) {
|
||||
return accounts[accountId];
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find(
|
||||
(key) => key.toLowerCase() === accountId.toLowerCase(),
|
||||
);
|
||||
@@ -21,10 +25,14 @@ export function resolveChannelConfigWrites(params: {
|
||||
channelId?: ChannelId | null;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
if (!params.channelId) return true;
|
||||
if (!params.channelId) {
|
||||
return true;
|
||||
}
|
||||
const channels = params.cfg.channels as Record<string, ChannelConfigWithAccounts> | undefined;
|
||||
const channelConfig = channels?.[params.channelId];
|
||||
if (!channelConfig) return true;
|
||||
if (!channelConfig) {
|
||||
return true;
|
||||
}
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const accountConfig = resolveAccountConfig(channelConfig.accounts, accountId);
|
||||
const value = accountConfig?.configWrites ?? channelConfig.configWrites;
|
||||
|
||||
@@ -23,17 +23,23 @@ export async function listSlackDirectoryPeersFromConfig(
|
||||
|
||||
for (const entry of account.dm?.allowFrom ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (!raw || raw === "*") continue;
|
||||
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);
|
||||
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);
|
||||
if (raw) {
|
||||
ids.add(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +49,9 @@ export async function listSlackDirectoryPeersFromConfig(
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<@([A-Z0-9]+)>$/i);
|
||||
const normalizedUserId = (mention?.[1] ?? raw).replace(/^(slack|user):/i, "").trim();
|
||||
if (!normalizedUserId) return null;
|
||||
if (!normalizedUserId) {
|
||||
return null;
|
||||
}
|
||||
const target = `user:${normalizedUserId}`;
|
||||
return normalizeSlackMessagingTarget(target) ?? target.toLowerCase();
|
||||
})
|
||||
@@ -78,22 +86,30 @@ export async function listDiscordDirectoryPeersFromConfig(
|
||||
|
||||
for (const entry of account.config.dm?.allowFrom ?? []) {
|
||||
const raw = String(entry).trim();
|
||||
if (!raw || raw === "*") continue;
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
if (raw) {
|
||||
ids.add(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +120,9 @@ export async function listDiscordDirectoryPeersFromConfig(
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<@!?(\d+)>$/);
|
||||
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim();
|
||||
if (!/^\d+$/.test(cleaned)) return null;
|
||||
if (!/^\d+$/.test(cleaned)) {
|
||||
return null;
|
||||
}
|
||||
return `user:${cleaned}`;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
@@ -122,7 +140,9 @@ export async function listDiscordDirectoryGroupsFromConfig(
|
||||
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);
|
||||
if (trimmed) {
|
||||
ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +152,9 @@ export async function listDiscordDirectoryGroupsFromConfig(
|
||||
.map((raw) => {
|
||||
const mention = raw.match(/^<#(\d+)>$/);
|
||||
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim();
|
||||
if (!/^\d+$/.test(cleaned)) return null;
|
||||
if (!/^\d+$/.test(cleaned)) {
|
||||
return null;
|
||||
}
|
||||
return `channel:${cleaned}`;
|
||||
})
|
||||
.filter((id): id is string => Boolean(id))
|
||||
@@ -160,8 +182,12 @@ export async function listTelegramDirectoryPeersFromConfig(
|
||||
)
|
||||
.map((entry) => {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return null;
|
||||
if (/^-?\d+$/.test(trimmed)) return trimmed;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (/^-?\d+$/.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
const withAt = trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
|
||||
return withAt;
|
||||
})
|
||||
|
||||
@@ -24,9 +24,13 @@ type GroupMentionParams = {
|
||||
};
|
||||
|
||||
function normalizeDiscordSlug(value?: string | null) {
|
||||
if (!value) return "";
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
let text = value.trim().toLowerCase();
|
||||
if (!text) return "";
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
text = text.replace(/^[@#]+/, "");
|
||||
text = text.replace(/[\s_]+/g, "-");
|
||||
text = text.replace(/[^a-z0-9-]+/g, "-");
|
||||
@@ -36,7 +40,9 @@ function normalizeDiscordSlug(value?: string | null) {
|
||||
|
||||
function normalizeSlackSlug(raw?: string | null) {
|
||||
const trimmed = raw?.trim().toLowerCase() ?? "";
|
||||
if (!trimmed) return "";
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
const dashed = trimmed.replace(/\s+/g, "-");
|
||||
const cleaned = dashed.replace(/[^a-z0-9#@._+-]+/g, "-");
|
||||
return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
||||
@@ -44,7 +50,9 @@ function normalizeSlackSlug(raw?: string | null) {
|
||||
|
||||
function parseTelegramGroupId(value?: string | null) {
|
||||
const raw = value?.trim() ?? "";
|
||||
if (!raw) return { chatId: undefined, topicId: undefined };
|
||||
if (!raw) {
|
||||
return { chatId: undefined, topicId: undefined };
|
||||
}
|
||||
const parts = raw.split(":").filter(Boolean);
|
||||
if (
|
||||
parts.length >= 3 &&
|
||||
@@ -66,7 +74,9 @@ function resolveTelegramRequireMention(params: {
|
||||
topicId?: string;
|
||||
}): boolean | undefined {
|
||||
const { cfg, chatId, topicId } = params;
|
||||
if (!chatId) return undefined;
|
||||
if (!chatId) {
|
||||
return undefined;
|
||||
}
|
||||
const groupConfig = cfg.channels?.telegram?.groups?.[chatId];
|
||||
const groupDefault = cfg.channels?.telegram?.groups?.["*"];
|
||||
const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
|
||||
@@ -88,16 +98,24 @@ function resolveTelegramRequireMention(params: {
|
||||
}
|
||||
|
||||
function resolveDiscordGuildEntry(guilds: DiscordConfig["guilds"], groupSpace?: string | null) {
|
||||
if (!guilds || Object.keys(guilds).length === 0) return null;
|
||||
if (!guilds || Object.keys(guilds).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const space = groupSpace?.trim() ?? "";
|
||||
if (space && guilds[space]) return guilds[space];
|
||||
if (space && guilds[space]) {
|
||||
return guilds[space];
|
||||
}
|
||||
const normalized = normalizeDiscordSlug(space);
|
||||
if (normalized && guilds[normalized]) return guilds[normalized];
|
||||
if (normalized && guilds[normalized]) {
|
||||
return guilds[normalized];
|
||||
}
|
||||
if (normalized) {
|
||||
const match = Object.values(guilds).find(
|
||||
(entry) => normalizeDiscordSlug(entry?.slug ?? undefined) === normalized,
|
||||
);
|
||||
if (match) return match;
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return guilds["*"] ?? null;
|
||||
}
|
||||
@@ -111,7 +129,9 @@ export function resolveTelegramGroupRequireMention(
|
||||
chatId,
|
||||
topicId,
|
||||
});
|
||||
if (typeof requireMention === "boolean") return requireMention;
|
||||
if (typeof requireMention === "boolean") {
|
||||
return requireMention;
|
||||
}
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
channel: "telegram",
|
||||
@@ -194,7 +214,9 @@ export function resolveSlackGroupRequireMention(params: GroupMentionParams): boo
|
||||
});
|
||||
const channels = account.channels ?? {};
|
||||
const keys = Object.keys(channels);
|
||||
if (keys.length === 0) return true;
|
||||
if (keys.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const channelId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel;
|
||||
const channelName = groupChannel?.replace(/^#/, "");
|
||||
@@ -299,8 +321,12 @@ export function resolveDiscordGroupToolPolicy(
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) return senderPolicy;
|
||||
if (entry?.tools) return entry.tools;
|
||||
if (senderPolicy) {
|
||||
return senderPolicy;
|
||||
}
|
||||
if (entry?.tools) {
|
||||
return entry.tools;
|
||||
}
|
||||
}
|
||||
const guildSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: guildEntry?.toolsBySender,
|
||||
@@ -309,8 +335,12 @@ export function resolveDiscordGroupToolPolicy(
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (guildSenderPolicy) return guildSenderPolicy;
|
||||
if (guildEntry?.tools) return guildEntry.tools;
|
||||
if (guildSenderPolicy) {
|
||||
return guildSenderPolicy;
|
||||
}
|
||||
if (guildEntry?.tools) {
|
||||
return guildEntry.tools;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -323,7 +353,9 @@ export function resolveSlackGroupToolPolicy(
|
||||
});
|
||||
const channels = account.channels ?? {};
|
||||
const keys = Object.keys(channels);
|
||||
if (keys.length === 0) return undefined;
|
||||
if (keys.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const channelId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel;
|
||||
const channelName = groupChannel?.replace(/^#/, "");
|
||||
@@ -351,8 +383,12 @@ export function resolveSlackGroupToolPolicy(
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) return senderPolicy;
|
||||
if (resolved?.tools) return resolved.tools;
|
||||
if (senderPolicy) {
|
||||
return senderPolicy;
|
||||
}
|
||||
if (resolved?.tools) {
|
||||
return resolved.tools;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] {
|
||||
const resolved: ChannelPlugin[] = [];
|
||||
for (const plugin of channels) {
|
||||
const id = String(plugin.id).trim();
|
||||
if (!id || seen.has(id)) continue;
|
||||
if (!id || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(id);
|
||||
resolved.push(plugin);
|
||||
}
|
||||
@@ -33,14 +35,18 @@ export function listChannelPlugins(): ChannelPlugin[] {
|
||||
const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId);
|
||||
const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA);
|
||||
const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB);
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
|
||||
const resolvedId = String(id).trim();
|
||||
if (!resolvedId) return undefined;
|
||||
if (!resolvedId) {
|
||||
return undefined;
|
||||
}
|
||||
return listChannelPlugins().find((plugin) => plugin.id === resolvedId);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ const cache = new Map<ChannelId, ChannelPlugin>();
|
||||
let lastRegistry: PluginRegistry | null = null;
|
||||
|
||||
function ensureCacheForRegistry(registry: PluginRegistry | null) {
|
||||
if (registry === lastRegistry) return;
|
||||
if (registry === lastRegistry) {
|
||||
return;
|
||||
}
|
||||
cache.clear();
|
||||
lastRegistry = registry;
|
||||
}
|
||||
@@ -15,7 +17,9 @@ export async function loadChannelPlugin(id: ChannelId): Promise<ChannelPlugin |
|
||||
const registry = getActivePluginRegistry();
|
||||
ensureCacheForRegistry(registry);
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||
if (pluginEntry) {
|
||||
cache.set(id, pluginEntry.plugin);
|
||||
|
||||
@@ -15,7 +15,9 @@ export function resolveChannelMediaMaxBytes(params: {
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
});
|
||||
if (channelLimit) return channelLimit * MB;
|
||||
if (channelLimit) {
|
||||
return channelLimit * MB;
|
||||
}
|
||||
if (params.cfg.agents?.defaults?.mediaMaxMb) {
|
||||
return params.cfg.agents.defaults.mediaMaxMb * MB;
|
||||
}
|
||||
|
||||
@@ -8,22 +8,30 @@ export function listChannelMessageActions(cfg: OpenClawConfig): ChannelMessageAc
|
||||
const actions = new Set<ChannelMessageActionName>(["send", "broadcast"]);
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const list = plugin.actions?.listActions?.({ cfg });
|
||||
if (!list) continue;
|
||||
for (const action of list) actions.add(action);
|
||||
if (!list) {
|
||||
continue;
|
||||
}
|
||||
for (const action of list) {
|
||||
actions.add(action);
|
||||
}
|
||||
}
|
||||
return Array.from(actions);
|
||||
}
|
||||
|
||||
export function supportsChannelMessageButtons(cfg: OpenClawConfig): boolean {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (plugin.actions?.supportsButtons?.({ cfg })) return true;
|
||||
if (plugin.actions?.supportsButtons?.({ cfg })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function supportsChannelMessageCards(cfg: OpenClawConfig): boolean {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (plugin.actions?.supportsCards?.({ cfg })) return true;
|
||||
if (plugin.actions?.supportsCards?.({ cfg })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -32,7 +40,9 @@ export async function dispatchChannelMessageAction(
|
||||
ctx: ChannelMessageActionContext,
|
||||
): Promise<AgentToolResult<unknown> | null> {
|
||||
const plugin = getChannelPlugin(ctx.channel);
|
||||
if (!plugin?.actions?.handleAction) return null;
|
||||
if (!plugin?.actions?.handleAction) {
|
||||
return null;
|
||||
}
|
||||
if (plugin.actions.supportsAction && !plugin.actions.supportsAction({ action: ctx.action })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,17 @@ export function normalizeDiscordMessagingTarget(raw: string): string | undefined
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ const CHAT_TARGET_PREFIX_RE =
|
||||
|
||||
export function normalizeIMessageMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Preserve service prefix if present (e.g., "sms:+1555" → "sms:+15551234567")
|
||||
const lower = trimmed.toLowerCase();
|
||||
@@ -15,8 +17,12 @@ export function normalizeIMessageMessagingTarget(raw: string): string | undefine
|
||||
if (lower.startsWith(prefix)) {
|
||||
const remainder = trimmed.slice(prefix.length).trim();
|
||||
const normalizedHandle = normalizeIMessageHandle(remainder);
|
||||
if (!normalizedHandle) return undefined;
|
||||
if (CHAT_TARGET_PREFIX_RE.test(normalizedHandle)) return normalizedHandle;
|
||||
if (!normalizedHandle) {
|
||||
return undefined;
|
||||
}
|
||||
if (CHAT_TARGET_PREFIX_RE.test(normalizedHandle)) {
|
||||
return normalizedHandle;
|
||||
}
|
||||
return `${prefix}${normalizedHandle}`;
|
||||
}
|
||||
}
|
||||
@@ -27,9 +33,17 @@ export function normalizeIMessageMessagingTarget(raw: string): string | undefine
|
||||
|
||||
export function looksLikeIMessageTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(imessage:|sms:|auto:)/i.test(trimmed)) return true;
|
||||
if (CHAT_TARGET_PREFIX_RE.test(trimmed)) return true;
|
||||
if (trimmed.includes("@")) return true;
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^(imessage:|sms:|auto:)/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (CHAT_TARGET_PREFIX_RE.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
export function normalizeSignalMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
let normalized = trimmed;
|
||||
if (normalized.toLowerCase().startsWith("signal:")) {
|
||||
normalized = normalized.slice("signal:".length).trim();
|
||||
}
|
||||
if (!normalized) return undefined;
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
const lower = normalized.toLowerCase();
|
||||
if (lower.startsWith("group:")) {
|
||||
const id = normalized.slice("group:".length).trim();
|
||||
@@ -32,17 +36,25 @@ const UUID_COMPACT_PATTERN = /^[0-9a-f]{32}$/i;
|
||||
|
||||
export function looksLikeSignalTargetId(raw: string): boolean {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (/^(signal:)?(group:|username:|u:)/i.test(trimmed)) return true;
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^(signal:)?(group:|username:|u:)/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(signal:)?uuid:/i.test(trimmed)) {
|
||||
const stripped = trimmed
|
||||
.replace(/^signal:/i, "")
|
||||
.replace(/^uuid:/i, "")
|
||||
.trim();
|
||||
if (!stripped) return false;
|
||||
if (!stripped) {
|
||||
return false;
|
||||
}
|
||||
return UUID_PATTERN.test(stripped) || UUID_COMPACT_PATTERN.test(stripped);
|
||||
}
|
||||
// Accept UUIDs (used by signal-cli for reactions)
|
||||
if (UUID_PATTERN.test(trimmed) || UUID_COMPACT_PATTERN.test(trimmed)) return true;
|
||||
if (UUID_PATTERN.test(trimmed) || UUID_COMPACT_PATTERN.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,20 @@ export function normalizeSlackMessagingTarget(raw: string): string | undefined {
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
export function normalizeTelegramMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^(telegram|tg):/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.startsWith("@")) {
|
||||
return true;
|
||||
}
|
||||
return /^-?\d{6,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,22 @@ import { normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
|
||||
|
||||
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
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;
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^whatsapp:/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ export async function promptChannelAccessConfig(params: {
|
||||
: `Configure ${params.label} access?`,
|
||||
initialValue: shouldPrompt,
|
||||
});
|
||||
if (!wants) return null;
|
||||
if (!wants) {
|
||||
return null;
|
||||
}
|
||||
const policy = await promptChannelAccessPolicy({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
@@ -85,7 +87,9 @@ export async function promptChannelAccessConfig(params: {
|
||||
allowOpen: params.allowOpen,
|
||||
allowDisabled: params.allowDisabled,
|
||||
});
|
||||
if (policy !== "allowlist") return { policy, entries: [] };
|
||||
if (policy !== "allowlist") {
|
||||
return { policy, entries: [] };
|
||||
}
|
||||
const entries = await promptChannelAllowlist({
|
||||
prompter: params.prompter,
|
||||
label: params.label,
|
||||
|
||||
@@ -201,11 +201,17 @@ async function promptDiscordAllowFrom(params: {
|
||||
const parseInputs = (value: string) => parseDiscordAllowFromInput(value);
|
||||
const parseId = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const mention = trimmed.match(/^<@!?(\d+)>$/);
|
||||
if (mention) return mention[1];
|
||||
if (mention) {
|
||||
return mention[1];
|
||||
}
|
||||
const prefixed = trimmed.replace(/^(user:|discord:)/i, "");
|
||||
if (/^\d+$/.test(prefixed)) return prefixed;
|
||||
if (/^\d+$/.test(prefixed)) {
|
||||
return prefixed;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -387,7 +393,9 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
([guildKey, value]) => {
|
||||
const channels = value?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels);
|
||||
if (channelKeys.length === 0) return [guildKey];
|
||||
if (channelKeys.length === 0) {
|
||||
return [guildKey];
|
||||
}
|
||||
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
|
||||
},
|
||||
);
|
||||
@@ -463,7 +471,9 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
const channelKey =
|
||||
entry.channelId ??
|
||||
(entry.channelName ? normalizeDiscordSlug(entry.channelName) : undefined);
|
||||
if (!channelKey && guildKey === "*") continue;
|
||||
if (!channelKey && guildKey === "*") {
|
||||
continue;
|
||||
}
|
||||
allowlistEntries.push({ guildKey, ...(channelKey ? { channelKey } : {}) });
|
||||
}
|
||||
next = setDiscordGroupPolicy(next, discordAccountId, "allowlist");
|
||||
|
||||
@@ -16,7 +16,9 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa
|
||||
initialValue: initial,
|
||||
});
|
||||
|
||||
if (choice !== "__new__") return normalizeAccountId(choice);
|
||||
if (choice !== "__new__") {
|
||||
return normalizeAccountId(choice);
|
||||
}
|
||||
|
||||
const entered = await params.prompter.text({
|
||||
message: `New ${params.label} account id`,
|
||||
@@ -36,6 +38,8 @@ export function addWildcardAllowFrom(
|
||||
allowFrom?: Array<string | number> | null,
|
||||
): Array<string | number> {
|
||||
const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
if (!next.includes("*")) next.push("*");
|
||||
if (!next.includes("*")) {
|
||||
next.push("*");
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -103,24 +103,36 @@ async function promptIMessageAllowFrom(params: {
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const parts = parseIMessageAllowFromInput(raw);
|
||||
for (const part of parts) {
|
||||
if (part === "*") continue;
|
||||
if (part === "*") {
|
||||
continue;
|
||||
}
|
||||
if (part.toLowerCase().startsWith("chat_id:")) {
|
||||
const id = part.slice("chat_id:".length).trim();
|
||||
if (!/^\d+$/.test(id)) return `Invalid chat_id: ${part}`;
|
||||
if (!/^\d+$/.test(id)) {
|
||||
return `Invalid chat_id: ${part}`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (part.toLowerCase().startsWith("chat_guid:")) {
|
||||
if (!part.slice("chat_guid:".length).trim()) return "Invalid chat_guid entry";
|
||||
if (!part.slice("chat_guid:".length).trim()) {
|
||||
return "Invalid chat_guid entry";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (part.toLowerCase().startsWith("chat_identifier:")) {
|
||||
if (!part.slice("chat_identifier:".length).trim()) return "Invalid chat_identifier entry";
|
||||
if (!part.slice("chat_identifier:".length).trim()) {
|
||||
return "Invalid chat_identifier entry";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!normalizeIMessageHandle(part)) return `Invalid handle: ${part}`;
|
||||
if (!normalizeIMessageHandle(part)) {
|
||||
return `Invalid handle: ${part}`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
@@ -107,16 +107,26 @@ async function promptSignalAllowFrom(params: {
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const parts = parseSignalAllowFromInput(raw);
|
||||
for (const part of parts) {
|
||||
if (part === "*") continue;
|
||||
if (part.toLowerCase().startsWith("uuid:")) {
|
||||
if (!part.slice("uuid:".length).trim()) return "Invalid uuid entry";
|
||||
if (part === "*") {
|
||||
continue;
|
||||
}
|
||||
if (isUuidLike(part)) continue;
|
||||
if (!normalizeE164(part)) return `Invalid entry: ${part}`;
|
||||
if (part.toLowerCase().startsWith("uuid:")) {
|
||||
if (!part.slice("uuid:".length).trim()) {
|
||||
return "Invalid uuid entry";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isUuidLike(part)) {
|
||||
continue;
|
||||
}
|
||||
if (!normalizeE164(part)) {
|
||||
return `Invalid entry: ${part}`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@@ -124,9 +134,15 @@ async function promptSignalAllowFrom(params: {
|
||||
const parts = parseSignalAllowFromInput(String(entry));
|
||||
const normalized = parts
|
||||
.map((part) => {
|
||||
if (part === "*") return "*";
|
||||
if (part.toLowerCase().startsWith("uuid:")) return `uuid:${part.slice(5).trim()}`;
|
||||
if (isUuidLike(part)) return `uuid:${part}`;
|
||||
if (part === "*") {
|
||||
return "*";
|
||||
}
|
||||
if (part.toLowerCase().startsWith("uuid:")) {
|
||||
return `uuid:${part.slice(5).trim()}`;
|
||||
}
|
||||
if (isUuidLike(part)) {
|
||||
return `uuid:${part}`;
|
||||
}
|
||||
return normalizeE164(part);
|
||||
})
|
||||
.filter(Boolean);
|
||||
@@ -231,7 +247,9 @@ export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
message: `Signal account set (${account}). Keep it?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) account = "";
|
||||
if (!keep) {
|
||||
account = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
|
||||
@@ -251,11 +251,17 @@ async function promptSlackAllowFrom(params: {
|
||||
const parseInputs = (value: string) => parseSlackAllowFromInput(value);
|
||||
const parseId = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i);
|
||||
if (mention) return mention[1]?.toUpperCase();
|
||||
if (mention) {
|
||||
return mention[1]?.toUpperCase();
|
||||
}
|
||||
const prefixed = trimmed.replace(/^(slack:|user:)/i, "");
|
||||
if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) return prefixed.toUpperCase();
|
||||
if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) {
|
||||
return prefixed.toUpperCase();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -74,21 +74,31 @@ async function promptTelegramAllowFrom(params: {
|
||||
|
||||
const resolveTelegramUserId = async (raw: string): Promise<string | null> => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const stripped = trimmed.replace(/^(telegram|tg):/i, "").trim();
|
||||
if (/^\d+$/.test(stripped)) return stripped;
|
||||
if (!token) return null;
|
||||
if (/^\d+$/.test(stripped)) {
|
||||
return stripped;
|
||||
}
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
const data = (await res.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
result?: { id?: number | string };
|
||||
} | null;
|
||||
const id = data?.ok ? data?.result?.id : undefined;
|
||||
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||
if (typeof id === "number" || typeof id === "string") {
|
||||
return String(id);
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
// Network error during username lookup - return null to prompt user for numeric ID
|
||||
|
||||
@@ -68,9 +68,13 @@ async function promptWhatsAppAllowFrom(
|
||||
initialValue: existingAllowFrom[0],
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const normalized = normalizeE164(raw);
|
||||
if (!normalized) return `Invalid number: ${raw}`;
|
||||
if (!normalized) {
|
||||
return `Invalid number: ${raw}`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
@@ -126,9 +130,13 @@ async function promptWhatsAppAllowFrom(
|
||||
initialValue: existingAllowFrom[0],
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const normalized = normalizeE164(raw);
|
||||
if (!normalized) return `Invalid number: ${raw}`;
|
||||
if (!normalized) {
|
||||
return `Invalid number: ${raw}`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
@@ -170,7 +178,9 @@ async function promptWhatsAppAllowFrom(
|
||||
if (policy === "open") {
|
||||
next = setWhatsAppAllowFrom(next, ["*"]);
|
||||
}
|
||||
if (policy === "disabled") return next;
|
||||
if (policy === "disabled") {
|
||||
return next;
|
||||
}
|
||||
|
||||
const allowOptions =
|
||||
existingAllowFrom.length > 0
|
||||
@@ -205,16 +215,24 @@ async function promptWhatsAppAllowFrom(
|
||||
placeholder: "+15555550123, +447700900123",
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "Required";
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const parts = raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) return "Required";
|
||||
if (parts.length === 0) {
|
||||
return "Required";
|
||||
}
|
||||
for (const part of parts) {
|
||||
if (part === "*") continue;
|
||||
if (part === "*") {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeE164(part);
|
||||
if (!normalized) return `Invalid number: ${part}`;
|
||||
if (!normalized) {
|
||||
return `Invalid number: ${part}`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
@@ -11,7 +11,9 @@ const cache = new Map<ChannelId, ChannelOutboundAdapter>();
|
||||
let lastRegistry: PluginRegistry | null = null;
|
||||
|
||||
function ensureCacheForRegistry(registry: PluginRegistry | null) {
|
||||
if (registry === lastRegistry) return;
|
||||
if (registry === lastRegistry) {
|
||||
return;
|
||||
}
|
||||
cache.clear();
|
||||
lastRegistry = registry;
|
||||
}
|
||||
@@ -22,7 +24,9 @@ export async function loadChannelOutboundAdapter(
|
||||
const registry = getActivePluginRegistry();
|
||||
ensureCacheForRegistry(registry);
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id);
|
||||
const outbound = pluginEntry?.plugin.outbound;
|
||||
if (outbound) {
|
||||
|
||||
@@ -3,18 +3,24 @@ import { sendMessageTelegram } from "../../../telegram/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
function parseReplyToMessageId(replyToId?: string | null) {
|
||||
if (!replyToId) return undefined;
|
||||
if (!replyToId) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number.parseInt(replyToId, 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseThreadId(threadId?: string | number | null) {
|
||||
if (threadId == null) return undefined;
|
||||
if (threadId == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof threadId === "number") {
|
||||
return Number.isFinite(threadId) ? Math.trunc(threadId) : undefined;
|
||||
}
|
||||
const trimmed = threadId.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ export async function notifyPairingApproved(params: {
|
||||
}): Promise<void> {
|
||||
// Extensions may provide adapter directly to bypass ESM module isolation
|
||||
const adapter = params.pairingAdapter ?? requirePairingAdapter(params.channelId);
|
||||
if (!adapter.notifyApproval) return;
|
||||
if (!adapter.notifyApproval) {
|
||||
return;
|
||||
}
|
||||
await adapter.notifyApproval({
|
||||
cfg: params.cfg,
|
||||
id: params.id,
|
||||
|
||||
@@ -18,8 +18,12 @@ function shouldStoreNameInAccounts(params: {
|
||||
accountId: string;
|
||||
alwaysUseAccounts?: boolean;
|
||||
}): boolean {
|
||||
if (params.alwaysUseAccounts) return true;
|
||||
if (params.accountId !== DEFAULT_ACCOUNT_ID) return true;
|
||||
if (params.alwaysUseAccounts) {
|
||||
return true;
|
||||
}
|
||||
if (params.accountId !== DEFAULT_ACCOUNT_ID) {
|
||||
return true;
|
||||
}
|
||||
return channelHasAccounts(params.cfg, params.channelKey);
|
||||
}
|
||||
|
||||
@@ -31,7 +35,9 @@ export function applyAccountNameToChannelSection(params: {
|
||||
alwaysUseAccounts?: boolean;
|
||||
}): OpenClawConfig {
|
||||
const trimmed = params.name?.trim();
|
||||
if (!trimmed) return params.cfg;
|
||||
if (!trimmed) {
|
||||
return params.cfg;
|
||||
}
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
||||
const baseConfig = channels?.[params.channelKey];
|
||||
@@ -85,11 +91,15 @@ export function migrateBaseNameToDefaultAccount(params: {
|
||||
channelKey: string;
|
||||
alwaysUseAccounts?: boolean;
|
||||
}): OpenClawConfig {
|
||||
if (params.alwaysUseAccounts) return params.cfg;
|
||||
if (params.alwaysUseAccounts) {
|
||||
return params.cfg;
|
||||
}
|
||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
||||
const base = channels?.[params.channelKey] as ChannelSectionBase | undefined;
|
||||
const baseName = base?.name?.trim();
|
||||
if (!baseName) return params.cfg;
|
||||
if (!baseName) {
|
||||
return params.cfg;
|
||||
}
|
||||
const accounts: Record<string, Record<string, unknown>> = {
|
||||
...base?.accounts,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,9 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap
|
||||
const accounts = listEnabledSlackAccounts(cfg).filter(
|
||||
(account) => account.botTokenSource !== "none",
|
||||
);
|
||||
if (accounts.length === 0) return [];
|
||||
if (accounts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const isActionEnabled = (key: string, defaultValue = true) => {
|
||||
for (const account of accounts) {
|
||||
const gate = createActionGate(
|
||||
@@ -24,7 +26,9 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap
|
||||
boolean | undefined
|
||||
>,
|
||||
);
|
||||
if (gate(key, defaultValue)) return true;
|
||||
if (gate(key, defaultValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -44,15 +48,23 @@ export function createSlackActions(providerId: string): ChannelMessageActionAdap
|
||||
actions.add("unpin");
|
||||
actions.add("list-pins");
|
||||
}
|
||||
if (isActionEnabled("memberInfo")) actions.add("member-info");
|
||||
if (isActionEnabled("emojiList")) actions.add("emoji-list");
|
||||
if (isActionEnabled("memberInfo")) {
|
||||
actions.add("member-info");
|
||||
}
|
||||
if (isActionEnabled("emojiList")) {
|
||||
actions.add("emoji-list");
|
||||
}
|
||||
return Array.from(actions);
|
||||
},
|
||||
extractToolSend: ({ args }): ChannelToolSend | null => {
|
||||
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||
if (action !== "sendMessage") return null;
|
||||
if (action !== "sendMessage") {
|
||||
return null;
|
||||
}
|
||||
const to = typeof args.to === "string" ? args.to : undefined;
|
||||
if (!to) return null;
|
||||
if (!to) {
|
||||
return null;
|
||||
}
|
||||
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
||||
return { to, accountId };
|
||||
},
|
||||
|
||||
@@ -20,7 +20,9 @@ type BlueBubblesProbeResult = {
|
||||
function readBlueBubblesAccountStatus(
|
||||
value: ChannelAccountSnapshot,
|
||||
): BlueBubblesAccountStatus | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
@@ -33,7 +35,9 @@ function readBlueBubblesAccountStatus(
|
||||
}
|
||||
|
||||
function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
||||
status: typeof value.status === "number" ? value.status : null,
|
||||
@@ -47,10 +51,14 @@ export function collectBlueBubblesStatusIssues(
|
||||
const issues: ChannelStatusIssue[] = [];
|
||||
for (const entry of accounts) {
|
||||
const account = readBlueBubblesAccountStatus(entry);
|
||||
if (!account) continue;
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountId = asString(account.accountId) ?? "default";
|
||||
const enabled = account.enabled !== false;
|
||||
if (!enabled) continue;
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const configured = account.configured === true;
|
||||
const running = account.running === true;
|
||||
|
||||
@@ -30,7 +30,9 @@ type DiscordPermissionsAuditSummary = {
|
||||
};
|
||||
|
||||
function readDiscordAccountStatus(value: ChannelAccountSnapshot): DiscordAccountStatus | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
@@ -41,9 +43,13 @@ function readDiscordAccountStatus(value: ChannelAccountSnapshot): DiscordAccount
|
||||
}
|
||||
|
||||
function readDiscordApplicationSummary(value: unknown): DiscordApplicationSummary {
|
||||
if (!isRecord(value)) return {};
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
}
|
||||
const intentsRaw = value.intents;
|
||||
if (!isRecord(intentsRaw)) return {};
|
||||
if (!isRecord(intentsRaw)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
intents: {
|
||||
messageContent:
|
||||
@@ -57,7 +63,9 @@ function readDiscordApplicationSummary(value: unknown): DiscordApplicationSummar
|
||||
}
|
||||
|
||||
function readDiscordPermissionsAuditSummary(value: unknown): DiscordPermissionsAuditSummary {
|
||||
if (!isRecord(value)) return {};
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
}
|
||||
const unresolvedChannels =
|
||||
typeof value.unresolvedChannels === "number" && Number.isFinite(value.unresolvedChannels)
|
||||
? value.unresolvedChannels
|
||||
@@ -66,9 +74,13 @@ function readDiscordPermissionsAuditSummary(value: unknown): DiscordPermissionsA
|
||||
const channels = Array.isArray(channelsRaw)
|
||||
? (channelsRaw
|
||||
.map((entry) => {
|
||||
if (!isRecord(entry)) return null;
|
||||
if (!isRecord(entry)) {
|
||||
return null;
|
||||
}
|
||||
const channelId = asString(entry.channelId);
|
||||
if (!channelId) return null;
|
||||
if (!channelId) {
|
||||
return null;
|
||||
}
|
||||
const ok = typeof entry.ok === "boolean" ? entry.ok : undefined;
|
||||
const missing = Array.isArray(entry.missing)
|
||||
? entry.missing.map((v) => asString(v)).filter(Boolean)
|
||||
@@ -96,11 +108,15 @@ export function collectDiscordStatusIssues(
|
||||
const issues: ChannelStatusIssue[] = [];
|
||||
for (const entry of accounts) {
|
||||
const account = readDiscordAccountStatus(entry);
|
||||
if (!account) continue;
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountId = asString(account.accountId) ?? "default";
|
||||
const enabled = account.enabled !== false;
|
||||
const configured = account.configured === true;
|
||||
if (!enabled || !configured) continue;
|
||||
if (!enabled || !configured) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const app = readDiscordApplicationSummary(account.application);
|
||||
const messageContent = app.intents?.messageContent;
|
||||
@@ -125,7 +141,9 @@ export function collectDiscordStatusIssues(
|
||||
});
|
||||
}
|
||||
for (const channel of audit.channels ?? []) {
|
||||
if (channel.ok === true) continue;
|
||||
if (channel.ok === true) {
|
||||
continue;
|
||||
}
|
||||
const missing = channel.missing?.length ? ` missing ${channel.missing.join(", ")}` : "";
|
||||
const error = channel.error ? `: ${channel.error}` : "";
|
||||
const baseMessage = `Channel ${channel.channelId} permission check failed.${missing}${error}`;
|
||||
|
||||
@@ -23,7 +23,9 @@ type TelegramGroupMembershipAuditSummary = {
|
||||
};
|
||||
|
||||
function readTelegramAccountStatus(value: ChannelAccountSnapshot): TelegramAccountStatus | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
@@ -36,7 +38,9 @@ function readTelegramAccountStatus(value: ChannelAccountSnapshot): TelegramAccou
|
||||
function readTelegramGroupMembershipAuditSummary(
|
||||
value: unknown,
|
||||
): TelegramGroupMembershipAuditSummary {
|
||||
if (!isRecord(value)) return {};
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
}
|
||||
const unresolvedGroups =
|
||||
typeof value.unresolvedGroups === "number" && Number.isFinite(value.unresolvedGroups)
|
||||
? value.unresolvedGroups
|
||||
@@ -49,9 +53,13 @@ function readTelegramGroupMembershipAuditSummary(
|
||||
const groups = Array.isArray(groupsRaw)
|
||||
? (groupsRaw
|
||||
.map((entry) => {
|
||||
if (!isRecord(entry)) return null;
|
||||
if (!isRecord(entry)) {
|
||||
return null;
|
||||
}
|
||||
const chatId = asString(entry.chatId);
|
||||
if (!chatId) return null;
|
||||
if (!chatId) {
|
||||
return null;
|
||||
}
|
||||
const ok = typeof entry.ok === "boolean" ? entry.ok : undefined;
|
||||
const status = asString(entry.status) ?? null;
|
||||
const error = asString(entry.error) ?? null;
|
||||
@@ -70,11 +78,15 @@ export function collectTelegramStatusIssues(
|
||||
const issues: ChannelStatusIssue[] = [];
|
||||
for (const entry of accounts) {
|
||||
const account = readTelegramAccountStatus(entry);
|
||||
if (!account) continue;
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountId = asString(account.accountId) ?? "default";
|
||||
const enabled = account.enabled !== false;
|
||||
const configured = account.configured === true;
|
||||
if (!enabled || !configured) continue;
|
||||
if (!enabled || !configured) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (account.allowUnmentionedGroups === true) {
|
||||
issues.push({
|
||||
@@ -108,7 +120,9 @@ export function collectTelegramStatusIssues(
|
||||
});
|
||||
}
|
||||
for (const group of audit.groups ?? []) {
|
||||
if (group.ok === true) continue;
|
||||
if (group.ok === true) {
|
||||
continue;
|
||||
}
|
||||
const status = group.status ? ` status=${group.status}` : "";
|
||||
const err = group.error ? `: ${group.error}` : "";
|
||||
const baseMessage = `Group ${group.chatId} not reachable by bot.${status}${err}`;
|
||||
|
||||
@@ -13,7 +13,9 @@ type WhatsAppAccountStatus = {
|
||||
};
|
||||
|
||||
function readWhatsAppAccountStatus(value: ChannelAccountSnapshot): WhatsAppAccountStatus | null {
|
||||
if (!isRecord(value)) return null;
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountId: value.accountId,
|
||||
enabled: value.enabled,
|
||||
@@ -31,10 +33,14 @@ export function collectWhatsAppStatusIssues(
|
||||
const issues: ChannelStatusIssue[] = [];
|
||||
for (const entry of accounts) {
|
||||
const account = readWhatsAppAccountStatus(entry);
|
||||
if (!account) continue;
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountId = asString(account.accountId) ?? "default";
|
||||
const enabled = account.enabled !== false;
|
||||
if (!enabled) continue;
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
const linked = account.linked === true;
|
||||
const running = account.running === true;
|
||||
const connected = account.connected === true;
|
||||
|
||||
@@ -9,7 +9,9 @@ type HeartbeatRecipientsOpts = { to?: string; all?: boolean };
|
||||
function getSessionRecipients(cfg: OpenClawConfig) {
|
||||
const sessionCfg = cfg.session;
|
||||
const scope = sessionCfg?.scope ?? "per-sender";
|
||||
if (scope === "global") return [];
|
||||
if (scope === "global") {
|
||||
return [];
|
||||
}
|
||||
const storePath = resolveStorePath(cfg.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
const isGroupKey = (key: string) =>
|
||||
@@ -32,7 +34,9 @@ function getSessionRecipients(cfg: OpenClawConfig) {
|
||||
// Dedupe while preserving recency ordering.
|
||||
const seen = new Set<string>();
|
||||
return recipients.filter((r) => {
|
||||
if (seen.has(r.to)) return false;
|
||||
if (seen.has(r.to)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(r.to);
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -27,7 +27,9 @@ describe("channel registry", () => {
|
||||
it("formats selection lines with docs labels", () => {
|
||||
const channels = listChatChannels();
|
||||
const first = channels[0];
|
||||
if (!first) throw new Error("Missing channel metadata.");
|
||||
if (!first) {
|
||||
throw new Error("Missing channel metadata.");
|
||||
}
|
||||
const line = formatChannelSelectionLine(first, (path, label) =>
|
||||
[label, path].filter(Boolean).join(":"),
|
||||
);
|
||||
|
||||
@@ -125,7 +125,9 @@ export function getChatChannelMeta(id: ChatChannelId): ChatChannelMeta {
|
||||
|
||||
export function normalizeChatChannelId(raw?: string | null): ChatChannelId | null {
|
||||
const normalized = normalizeChannelKey(raw);
|
||||
if (!normalized) return null;
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized;
|
||||
return CHAT_CHANNEL_ORDER.includes(resolved) ? resolved : null;
|
||||
}
|
||||
@@ -142,14 +144,18 @@ export function normalizeChannelId(raw?: string | null): ChatChannelId | null {
|
||||
// monitors, web login, etc). The plugin registry must be initialized first.
|
||||
export function normalizeAnyChannelId(raw?: string | null): ChannelId | null {
|
||||
const key = normalizeChannelKey(raw);
|
||||
if (!key) return null;
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registry = requireActivePluginRegistry();
|
||||
const hit = registry.channels.find((entry) => {
|
||||
const id = String(entry.plugin.id ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (id && id === key) return true;
|
||||
if (id && id === key) {
|
||||
return true;
|
||||
}
|
||||
return (entry.plugin.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === key);
|
||||
});
|
||||
return hit?.plugin.id ?? null;
|
||||
|
||||
@@ -25,10 +25,12 @@ export function validateSenderIdentity(ctx: MsgContext): string[] {
|
||||
}
|
||||
|
||||
if (senderUsername) {
|
||||
if (senderUsername.includes("@"))
|
||||
if (senderUsername.includes("@")) {
|
||||
issues.push(`SenderUsername should not include "@": ${senderUsername}`);
|
||||
if (/\s/.test(senderUsername))
|
||||
}
|
||||
if (/\s/.test(senderUsername)) {
|
||||
issues.push(`SenderUsername should not include whitespace: ${senderUsername}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.SenderId != null && !senderId) {
|
||||
|
||||
@@ -20,7 +20,9 @@ export function resolveSenderLabel(params: SenderLabelParams): string | null {
|
||||
|
||||
const display = name ?? username ?? tag ?? "";
|
||||
const idPart = e164 ?? id ?? "";
|
||||
if (display && idPart && display !== idPart) return `${display} (${idPart})`;
|
||||
if (display && idPart && display !== idPart) {
|
||||
return `${display} (${idPart})`;
|
||||
}
|
||||
return display || idPart || null;
|
||||
}
|
||||
|
||||
@@ -32,12 +34,24 @@ export function listSenderLabelCandidates(params: SenderLabelParams): string[] {
|
||||
const e164 = normalize(params.e164);
|
||||
const id = normalize(params.id);
|
||||
|
||||
if (name) candidates.add(name);
|
||||
if (username) candidates.add(username);
|
||||
if (tag) candidates.add(tag);
|
||||
if (e164) candidates.add(e164);
|
||||
if (id) candidates.add(id);
|
||||
if (name) {
|
||||
candidates.add(name);
|
||||
}
|
||||
if (username) {
|
||||
candidates.add(username);
|
||||
}
|
||||
if (tag) {
|
||||
candidates.add(tag);
|
||||
}
|
||||
if (e164) {
|
||||
candidates.add(e164);
|
||||
}
|
||||
if (id) {
|
||||
candidates.add(id);
|
||||
}
|
||||
const resolved = resolveSenderLabel(params);
|
||||
if (resolved) candidates.add(resolved);
|
||||
if (resolved) {
|
||||
candidates.add(resolved);
|
||||
}
|
||||
return Array.from(candidates);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ export async function recordInboundSession(params: {
|
||||
}).catch(params.onRecordError);
|
||||
|
||||
const update = params.updateLastRoute;
|
||||
if (!update) return;
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
await updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: update.sessionKey,
|
||||
|
||||
Reference in New Issue
Block a user