refactor: dedupe messaging lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 15:20:23 +01:00
parent d43cc470c6
commit 4bcbb22678
15 changed files with 58 additions and 45 deletions

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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:") ||

View File

@@ -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;
}

View File

@@ -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)) {

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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" ||

View File

@@ -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";
}

View File

@@ -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<ResolvedNextcloudTalkAccount> =
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`,

View File

@@ -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<number | undefined> {
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;
}