From 4bcbb226783761e5e2ed8edcda6c179f87a2ed2c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 15:20:23 +0100 Subject: [PATCH] refactor: dedupe messaging lowercase helpers --- .../bluebubbles/src/monitor-normalize.ts | 2 +- extensions/bluebubbles/src/targets.ts | 16 ++++------------ extensions/feishu/src/session-route.ts | 5 +++-- extensions/feishu/src/targets.ts | 5 +++-- extensions/imessage/src/normalize.ts | 19 +++++++++---------- extensions/imessage/src/setup-core.ts | 3 ++- extensions/imessage/src/targets.ts | 11 ++++++----- extensions/imessage/src/test-plugin.ts | 5 +++-- .../mattermost/src/mattermost/send.test.ts | 6 ++++++ extensions/mattermost/src/mattermost/send.ts | 7 +++++-- extensions/mattermost/src/normalize.ts | 4 +++- extensions/mattermost/src/session-route.ts | 3 ++- extensions/nextcloud-talk/src/accounts.ts | 7 +++++-- extensions/nextcloud-talk/src/channel.ts | 3 ++- extensions/synology-chat/src/client.ts | 7 ++++--- 15 files changed, 58 insertions(+), 45 deletions(-) diff --git a/extensions/bluebubbles/src/monitor-normalize.ts b/extensions/bluebubbles/src/monitor-normalize.ts index 903289d46dc..463e1939b1b 100644 --- a/extensions/bluebubbles/src/monitor-normalize.ts +++ b/extensions/bluebubbles/src/monitor-normalize.ts @@ -589,7 +589,7 @@ export function parseTapbackText(params: { quotedText: string; } | null { const trimmed = params.text.trim(); - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); if (!trimmed) { return null; } diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index aee00274efc..83ae14eab50 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -6,6 +6,10 @@ import { resolveServicePrefixedAllowTarget, resolveServicePrefixedTarget, } from "openclaw/plugin-sdk/channel-targets"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; export type BlueBubblesService = "imessage" | "sms" | "auto"; @@ -28,18 +32,6 @@ const SERVICE_PREFIXES: Array<{ prefix: string; service: BlueBubblesService }> = const CHAT_IDENTIFIER_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i; -function normalizeOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - -function normalizeLowercaseStringOrEmpty(value: unknown): string { - return normalizeOptionalString(value)?.toLowerCase() ?? ""; -} - function parseRawChatGuid(value: string): string | null { const trimmed = normalizeOptionalString(value); if (!trimmed) { diff --git a/extensions/feishu/src/session-route.ts b/extensions/feishu/src/session-route.ts index 70220f9dcd1..9d8c68ae7b5 100644 --- a/extensions/feishu/src/session-route.ts +++ b/extensions/feishu/src/session-route.ts @@ -3,6 +3,7 @@ import { stripChannelTargetPrefix, type ChannelOutboundSessionRouteParams, } from "openclaw/plugin-sdk/channel-core"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; export function resolveFeishuOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) { let trimmed = stripChannelTargetPrefix(params.target, "feishu", "lark"); @@ -10,7 +11,7 @@ export function resolveFeishuOutboundSessionRoute(params: ChannelOutboundSession return null; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); let isGroup = false; let typeExplicit = false; @@ -25,7 +26,7 @@ export function resolveFeishuOutboundSessionRoute(params: ChannelOutboundSession } if (!typeExplicit) { - const idLower = trimmed.toLowerCase(); + const idLower = normalizeLowercaseStringOrEmpty(trimmed); if (idLower.startsWith("ou_") || idLower.startsWith("on_")) { isGroup = false; } diff --git a/extensions/feishu/src/targets.ts b/extensions/feishu/src/targets.ts index 1ec68e258cb..7f64ca1f1db 100644 --- a/extensions/feishu/src/targets.ts +++ b/extensions/feishu/src/targets.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { FeishuIdType } from "./types.js"; const CHAT_ID_PREFIX = "oc_"; @@ -29,7 +30,7 @@ export function normalizeFeishuTarget(raw: string): string | null { } const withoutProvider = stripProviderPrefix(trimmed); - const lowered = withoutProvider.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(withoutProvider); if (lowered.startsWith("chat:")) { return withoutProvider.slice("chat:".length).trim() || null; } @@ -65,7 +66,7 @@ export function formatFeishuTarget(id: string, type?: FeishuIdType): string { export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_id" { const trimmed = id.trim(); - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); if ( lowered.startsWith("chat:") || lowered.startsWith("group:") || diff --git a/extensions/imessage/src/normalize.ts b/extensions/imessage/src/normalize.ts index 2e97403fa28..a219aff1531 100644 --- a/extensions/imessage/src/normalize.ts +++ b/extensions/imessage/src/normalize.ts @@ -1,14 +1,13 @@ import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; const SERVICE_PREFIXES = ["imessage:", "sms:", "auto:"] as const; const CHAT_TARGET_PREFIX_RE = /^(chat_id:|chatid:|chat:|chat_guid:|chatguid:|guid:|chat_identifier:|chatidentifier:|chatident:)/i; -function trimMessagingTarget(raw: string): string | undefined { - const trimmed = raw.trim(); - return trimmed || undefined; -} - function looksLikeHandleOrPhoneTarget(params: { raw: string; prefixPattern: RegExp; @@ -32,7 +31,7 @@ export function normalizeIMessageHandle(raw: string): string { if (!trimmed) { return ""; } - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); if (lowered.startsWith("imessage:")) { return normalizeIMessageHandle(trimmed.slice("imessage:".length)); } @@ -51,7 +50,7 @@ export function normalizeIMessageHandle(raw: string): string { return `${prefix.toLowerCase()}${value}`; } if (trimmed.includes("@")) { - return trimmed.toLowerCase(); + return normalizeLowercaseStringOrEmpty(trimmed); } const normalized = normalizeE164(trimmed); if (normalized) { @@ -61,12 +60,12 @@ export function normalizeIMessageHandle(raw: string): string { } export function normalizeIMessageMessagingTarget(raw: string): string | undefined { - const trimmed = trimMessagingTarget(raw); + const trimmed = normalizeOptionalString(raw); if (!trimmed) { return undefined; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); for (const prefix of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = trimmed.slice(prefix.length).trim(); @@ -86,7 +85,7 @@ export function normalizeIMessageMessagingTarget(raw: string): string | undefine } export function looksLikeIMessageTargetId(raw: string): boolean { - const trimmed = trimMessagingTarget(raw); + const trimmed = normalizeOptionalString(raw); if (!trimmed) { return false; } diff --git a/extensions/imessage/src/setup-core.ts b/extensions/imessage/src/setup-core.ts index f48005e6a54..5e3cf98caf3 100644 --- a/extensions/imessage/src/setup-core.ts +++ b/extensions/imessage/src/setup-core.ts @@ -18,6 +18,7 @@ import { type WizardPrompter, } from "openclaw/plugin-sdk/setup-runtime"; import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { resolveDefaultIMessageAccountId, resolveIMessageAccount } from "./accounts.js"; import { normalizeIMessageHandle } from "./targets.js"; @@ -25,7 +26,7 @@ const channel = "imessage" as const; export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } { return parseSetupEntriesAllowingWildcard(raw, (entry) => { - const lower = entry.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(entry); if (lower.startsWith("chat_id:")) { const id = entry.slice("chat_id:".length).trim(); if (!/^\d+$/.test(id)) { diff --git a/extensions/imessage/src/targets.ts b/extensions/imessage/src/targets.ts index 8730c94b297..f7f24a569bd 100644 --- a/extensions/imessage/src/targets.ts +++ b/extensions/imessage/src/targets.ts @@ -1,4 +1,5 @@ import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { createAllowedChatSenderMatcher, type ChatSenderAllowParams, @@ -32,7 +33,7 @@ export function normalizeIMessageHandle(raw: string): string { if (!trimmed) { return ""; } - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); if (lowered.startsWith("imessage:")) { return normalizeIMessageHandle(trimmed.slice(9)); } @@ -64,7 +65,7 @@ export function normalizeIMessageHandle(raw: string): string { } if (trimmed.includes("@")) { - return trimmed.toLowerCase(); + return normalizeLowercaseStringOrEmpty(trimmed); } const normalized = normalizeE164(trimmed); if (normalized) { @@ -78,7 +79,7 @@ export function parseIMessageTarget(raw: string): IMessageTarget { if (!trimmed) { throw new Error("iMessage target is required"); } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const servicePrefixed = resolveServicePrefixedChatTarget({ trimmed, @@ -112,7 +113,7 @@ export function looksLikeIMessageExplicitTargetId(raw: string): boolean { if (!trimmed) { return false; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); if (/^(imessage:|sms:|auto:)/.test(lower)) { return true; } @@ -140,7 +141,7 @@ export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget { if (!trimmed) { return { kind: "handle", handle: "" }; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const servicePrefixed = resolveServicePrefixedOrChatAllowTarget({ trimmed, diff --git a/extensions/imessage/src/test-plugin.ts b/extensions/imessage/src/test-plugin.ts index 8f3ca664992..8d7c79f6e12 100644 --- a/extensions/imessage/src/test-plugin.ts +++ b/extensions/imessage/src/test-plugin.ts @@ -2,13 +2,14 @@ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-contrac import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime"; import { collectStatusIssuesFromLastError } from "openclaw/plugin-sdk/status-helpers"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; function normalizeIMessageTestHandle(raw: string): string { const trimmed = raw.trim(); if (!trimmed) { return ""; } - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); if (lowered.startsWith("imessage:")) { return normalizeIMessageTestHandle(trimmed.slice("imessage:".length)); } @@ -24,7 +25,7 @@ function normalizeIMessageTestHandle(raw: string): string { ); } if (trimmed.includes("@")) { - return trimmed.toLowerCase(); + return normalizeLowercaseStringOrEmpty(trimmed); } const digits = trimmed.replace(/[^\d+]/g, ""); if (digits) { diff --git a/extensions/mattermost/src/mattermost/send.test.ts b/extensions/mattermost/src/mattermost/send.test.ts index be3d0bad287..c3c7076702f 100644 --- a/extensions/mattermost/src/mattermost/send.test.ts +++ b/extensions/mattermost/src/mattermost/send.test.ts @@ -45,6 +45,12 @@ vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ vi.mock("openclaw/plugin-sdk/text-runtime", () => ({ convertMarkdownTables: vi.fn((text: string) => text), + normalizeLowercaseStringOrEmpty: vi.fn((value: string | null | undefined) => { + if (typeof value !== "string") { + return ""; + } + return value.trim().toLowerCase(); + }), normalizeOptionalString: vi.fn((value: string | null | undefined) => { if (typeof value !== "string") { return undefined; diff --git a/extensions/mattermost/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts index 60320c165f7..23d03c7d39e 100644 --- a/extensions/mattermost/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -1,6 +1,9 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; -import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime"; +import { + convertMarkdownTables, + normalizeLowercaseStringOrEmpty, +} from "openclaw/plugin-sdk/text-runtime"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { @@ -94,7 +97,7 @@ export function parseMattermostTarget(raw: string): MattermostTarget { if (!trimmed) { throw new Error("Recipient is required for Mattermost sends"); } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); if (lower.startsWith("channel:")) { const id = trimmed.slice("channel:".length).trim(); if (!id) { diff --git a/extensions/mattermost/src/normalize.ts b/extensions/mattermost/src/normalize.ts index 870b30594c4..419dd084ecb 100644 --- a/extensions/mattermost/src/normalize.ts +++ b/extensions/mattermost/src/normalize.ts @@ -1,9 +1,11 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; + export function normalizeMattermostMessagingTarget(raw: string): string | undefined { const trimmed = raw.trim(); if (!trimmed) { return undefined; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); if (lower.startsWith("channel:")) { const id = trimmed.slice("channel:".length).trim(); return id ? `channel:${id}` : undefined; diff --git a/extensions/mattermost/src/session-route.ts b/extensions/mattermost/src/session-route.ts index 39f12e37127..5bbd3e4da47 100644 --- a/extensions/mattermost/src/session-route.ts +++ b/extensions/mattermost/src/session-route.ts @@ -6,13 +6,14 @@ import { type ChannelOutboundSessionRouteParams, } from "openclaw/plugin-sdk/core"; import { normalizeOutboundThreadId } from "openclaw/plugin-sdk/routing"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; export function resolveMattermostOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) { let trimmed = stripChannelTargetPrefix(params.target, "mattermost"); if (!trimmed) { return null; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const resolvedKind = params.resolvedTarget?.kind; const isUser = resolvedKind === "user" || diff --git a/extensions/nextcloud-talk/src/accounts.ts b/extensions/nextcloud-talk/src/accounts.ts index a910196e2d1..f3fc90a0943 100644 --- a/extensions/nextcloud-talk/src/accounts.ts +++ b/extensions/nextcloud-talk/src/accounts.ts @@ -1,6 +1,9 @@ import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution"; import { tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { createAccountListHelpers, DEFAULT_ACCOUNT_ID, @@ -11,7 +14,7 @@ import { normalizeResolvedSecretInputString } from "./secret-input.js"; import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js"; function isTruthyEnvValue(value?: string): boolean { - const normalized = normalizeOptionalString(value)?.toLowerCase() ?? ""; + const normalized = normalizeLowercaseStringOrEmpty(value); return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; } diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index a66f1733740..5a676241ece 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -16,6 +16,7 @@ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { listNextcloudTalkAccountIds, resolveDefaultNextcloudTalkAccountId, @@ -197,7 +198,7 @@ export const nextcloudTalkPlugin: ChannelPlugin = message: "OpenClaw: your access has been approved.", normalizeAllowEntry: createPairingPrefixStripper( /^(nextcloud-talk|nc-talk|nc):/i, - (entry) => entry.toLowerCase(), + (entry) => normalizeLowercaseStringOrEmpty(entry), ), notify: createLoggedPairingApprovalNotifier( ({ id }) => `[nextcloud-talk] User ${id} approved for pairing`, diff --git a/extensions/synology-chat/src/client.ts b/extensions/synology-chat/src/client.ts index 17203274cfe..dc75ed29420 100644 --- a/extensions/synology-chat/src/client.ts +++ b/extensions/synology-chat/src/client.ts @@ -6,6 +6,7 @@ import * as http from "node:http"; import * as https from "node:https"; import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { z } from "zod"; const MIN_SEND_INTERVAL_MS = 500; @@ -221,16 +222,16 @@ export async function resolveLegacyWebhookNameToChatUserId(params: { log?: { warn: (...args: unknown[]) => void }; }): Promise { const users = await fetchChatUsers(params.incomingUrl, params.allowInsecureSsl, params.log); - const lower = params.mutableWebhookUsername.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(params.mutableWebhookUsername); // Match by nickname first (webhook "username" field = Chat "nickname") - const byNickname = users.find((u) => u.nickname.toLowerCase() === lower); + const byNickname = users.find((u) => normalizeLowercaseStringOrEmpty(u.nickname) === lower); if (byNickname) { return byNickname.user_id; } // Then by username - const byUsername = users.find((u) => u.username.toLowerCase() === lower); + const byUsername = users.find((u) => normalizeLowercaseStringOrEmpty(u.username) === lower); if (byUsername) { return byUsername.user_id; }