From ae1cc2d6df3342232ad5c577bfd35cc68bd941e9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 00:23:22 +0100 Subject: [PATCH] refactor: dedupe matrix trimmed readers --- extensions/matrix/src/account-selection.ts | 6 +- extensions/matrix/src/matrix/accounts.ts | 2 +- .../matrix/src/matrix/direct-management.ts | 8 +- .../matrix/src/matrix/monitor/auto-join.ts | 5 +- .../matrix/src/matrix/monitor/events.ts | 10 +- .../matrix/src/matrix/monitor/location.ts | 9 +- .../src/matrix/monitor/verification-utils.ts | 4 +- extensions/matrix/src/matrix/poll-types.ts | 5 +- .../matrix/src/matrix/reaction-common.ts | 16 +- extensions/matrix/src/migration-config.ts | 3 +- extensions/matrix/src/onboarding.ts | 153 ++++++++++-------- extensions/matrix/src/plugin-entry.runtime.ts | 14 +- 12 files changed, 134 insertions(+), 101 deletions(-) diff --git a/extensions/matrix/src/account-selection.ts b/extensions/matrix/src/account-selection.ts index 22676b9fb5c..acde1d72a36 100644 --- a/extensions/matrix/src/account-selection.ts +++ b/extensions/matrix/src/account-selection.ts @@ -11,6 +11,7 @@ import { } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveMatrixAccountStringValues, type MatrixResolvedStringField, @@ -21,7 +22,7 @@ import { isRecord } from "./record-shared.js"; type MatrixTopologyStringSources = Partial>; function readConfiguredMatrixString(value: unknown): string { - return typeof value === "string" ? value.trim() : ""; + return normalizeOptionalString(value) ?? ""; } function readConfiguredMatrixSecretSource(value: unknown): string { @@ -45,8 +46,7 @@ function resolveMatrixChannelStringSources( } function readEnvMatrixString(env: NodeJS.ProcessEnv, key: string): string { - const value = env[key]; - return typeof value === "string" ? value.trim() : ""; + return normalizeOptionalString(env[key]) ?? ""; } function resolveScopedMatrixEnvStringSources( diff --git a/extensions/matrix/src/matrix/accounts.ts b/extensions/matrix/src/matrix/accounts.ts index 876ea1a8bce..343e13e6bd1 100644 --- a/extensions/matrix/src/matrix/accounts.ts +++ b/extensions/matrix/src/matrix/accounts.ts @@ -26,7 +26,7 @@ export type ResolvedMatrixAccount = { }; function clean(value: unknown): string { - return typeof value === "string" ? value.trim() : ""; + return normalizeOptionalString(value) ?? ""; } function resolveMatrixAccountAuthView(params: { diff --git a/extensions/matrix/src/matrix/direct-management.ts b/extensions/matrix/src/matrix/direct-management.ts index fc0de7a5dee..b02d7dbbddf 100644 --- a/extensions/matrix/src/matrix/direct-management.ts +++ b/extensions/matrix/src/matrix/direct-management.ts @@ -1,4 +1,5 @@ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { inspectMatrixDirectRoomEvidence } from "./direct-room.js"; import type { MatrixClient } from "./sdk.js"; import { EventType, type MatrixDirectAccountData } from "./send/types.js"; @@ -60,7 +61,7 @@ async function readMatrixDirectAccountData(client: MatrixClient): Promise(); const normalized: string[] = []; for (const value of current) { - const roomId = typeof value === "string" ? value.trim() : ""; + const roomId = normalizeOptionalString(value) ?? ""; if (!roomId || seen.has(roomId)) { continue; } @@ -257,7 +258,8 @@ export async function inspectMatrixDirectRooms(params: { remoteUserId: string; }): Promise { const remoteUserId = normalizeRemoteUserId(params.remoteUserId); - const selfUserId = (await params.client.getUserId().catch(() => null))?.trim() || null; + const selfUserId = + normalizeOptionalString(await params.client.getUserId().catch(() => null)) ?? null; const directContent = await readMatrixDirectAccountData(params.client); const mappedRoomIds = normalizeMappedRoomIds(directContent, remoteUserId); const mappedRooms = await Promise.all( diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index e8096a1bbdb..3c48c58a646 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -1,3 +1,4 @@ +import { normalizeStringifiedOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { getMatrixRuntime } from "../../runtime.js"; import type { MatrixConfig } from "../../types.js"; import type { MatrixClient } from "../sdk.js"; @@ -18,8 +19,8 @@ export function registerMatrixAutoJoin(params: { }; const autoJoin = accountConfig.autoJoin ?? "off"; const rawAllowlist = (accountConfig.autoJoinAllowlist ?? []) - .map((entry) => String(entry).trim()) - .filter(Boolean); + .map((entry) => normalizeStringifiedOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)); const autoJoinAllowlist = new Set(rawAllowlist); const allowedRoomIds = new Set(rawAllowlist.filter((entry) => entry.startsWith("!"))); const allowedAliases = rawAllowlist.filter((entry) => entry.startsWith("#")); diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 1d69053d786..258c4ebcfb0 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { PluginRuntime, RuntimeLogger } from "../../runtime-api.js"; import type { CoreConfig } from "../../types.js"; import type { MatrixAuth } from "../client.js"; @@ -127,12 +128,13 @@ export function registerMatrixMonitorEvents(params: { directTracker?.invalidateRoom(roomId); const eventId = event?.event_id ?? "unknown"; const sender = event?.sender ?? "unknown"; - const invitee = typeof event?.state_key === "string" ? event.state_key.trim() : ""; + const invitee = normalizeOptionalString(event?.state_key) ?? ""; const senderIsInvitee = - typeof event?.sender === "string" && invitee && event.sender.trim() === invitee; + Boolean(invitee) && (normalizeOptionalString(event?.sender) ?? "") === invitee; const isDirect = (event?.content as { is_direct?: boolean } | undefined)?.is_direct === true; - if (typeof event?.sender === "string" && event.sender.trim() && !senderIsInvitee) { - directTracker?.rememberInvite?.(roomId, event.sender); + const rememberedSender = normalizeOptionalString(event?.sender); + if (rememberedSender && !senderIsInvitee) { + directTracker?.rememberInvite?.(roomId, rememberedSender); } logVerboseMessage( `matrix: invite room=${roomId} sender=${sender} direct=${String(isDirect)} id=${eventId}`, diff --git a/extensions/matrix/src/matrix/monitor/location.ts b/extensions/matrix/src/matrix/monitor/location.ts index 59f3e05124c..46057e8ff98 100644 --- a/extensions/matrix/src/matrix/monitor/location.ts +++ b/extensions/matrix/src/matrix/monitor/location.ts @@ -1,4 +1,7 @@ -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import type { LocationMessageEventContent } from "../sdk.js"; import { formatLocationText, toLocationContext, type NormalizedLocation } from "./runtime-api.js"; import { EventType } from "./types.js"; @@ -72,7 +75,7 @@ export function resolveMatrixLocation(params: { if (!isLocation) { return null; } - const geoUri = typeof content.geo_uri === "string" ? content.geo_uri.trim() : ""; + const geoUri = normalizeOptionalString(content.geo_uri) ?? ""; if (!geoUri) { return null; } @@ -80,7 +83,7 @@ export function resolveMatrixLocation(params: { if (!parsed) { return null; } - const caption = typeof content.body === "string" ? content.body.trim() : ""; + const caption = normalizeOptionalString(content.body) ?? ""; const location: NormalizedLocation = { latitude: parsed.latitude, longitude: parsed.longitude, diff --git a/extensions/matrix/src/matrix/monitor/verification-utils.ts b/extensions/matrix/src/matrix/monitor/verification-utils.ts index d777167c4ff..2044fe259cf 100644 --- a/extensions/matrix/src/matrix/monitor/verification-utils.ts +++ b/extensions/matrix/src/matrix/monitor/verification-utils.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; + const VERIFICATION_EVENT_PREFIX = "m.key.verification."; const VERIFICATION_REQUEST_MSGTYPE = "m.key.verification.request"; @@ -11,7 +13,7 @@ const VERIFICATION_NOTICE_PREFIXES = [ ]; function trimMaybeString(input: unknown): string { - return typeof input === "string" ? input.trim() : ""; + return normalizeOptionalString(input) ?? ""; } export function isMatrixVerificationEventType(type: unknown): boolean { diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index 196f3bf8cc9..cdf3c172eb8 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -7,6 +7,7 @@ * - m.poll.end - Closes a poll */ +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { normalizePollInput, type PollInput } from "../runtime-api.js"; export const M_POLL_START = "m.poll.start" as const; @@ -295,7 +296,7 @@ export function buildPollResultsSummary(params: { if (!isPollResponseType(typeof event.type === "string" ? event.type : "")) { continue; } - const senderId = typeof event.sender === "string" ? event.sender.trim() : ""; + const senderId = normalizeOptionalString(event.sender) ?? ""; if (!senderId) { continue; } @@ -310,7 +311,7 @@ export function buildPollResultsSummary(params: { const normalizedAnswers = Array.from( new Set( rawAnswers - .map((answerId) => answerId.trim()) + .map((answerId) => normalizeOptionalString(answerId) ?? "") .filter((answerId) => answerIds.has(answerId)) .slice(0, parsed.maxSelections), ), diff --git a/extensions/matrix/src/matrix/reaction-common.ts b/extensions/matrix/src/matrix/reaction-common.ts index 797e5392dfd..e80ab2aef38 100644 --- a/extensions/matrix/src/matrix/reaction-common.ts +++ b/extensions/matrix/src/matrix/reaction-common.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; + export const MATRIX_ANNOTATION_RELATION_TYPE = "m.annotation"; export const MATRIX_REACTION_EVENT_TYPE = "m.reaction"; @@ -83,11 +85,11 @@ export function extractMatrixReactionAnnotation( ) { return undefined; } - const key = typeof relatesTo.key === "string" ? relatesTo.key.trim() : ""; + const key = normalizeOptionalString(relatesTo.key) ?? ""; if (!key) { return undefined; } - const eventId = typeof relatesTo.event_id === "string" ? relatesTo.event_id.trim() : ""; + const eventId = normalizeOptionalString(relatesTo.event_id) ?? ""; return { key, eventId: eventId || undefined, @@ -107,7 +109,7 @@ export function summarizeMatrixReactionEvents( if (!key) { continue; } - const sender = event.sender?.trim() ?? ""; + const sender = normalizeOptionalString(event.sender) ?? ""; const entry = summaries.get(key) ?? { key, count: 0, users: [] }; entry.count += 1; if (sender && !entry.users.includes(sender)) { @@ -123,20 +125,20 @@ export function selectOwnMatrixReactionEventIds( userId: string, emoji?: string, ): string[] { - const senderId = userId.trim(); + const senderId = normalizeOptionalString(userId) ?? ""; if (!senderId) { return []; } - const targetEmoji = emoji?.trim(); + const targetEmoji = normalizeOptionalString(emoji); const ids: string[] = []; for (const event of events) { - if ((event.sender?.trim() ?? "") !== senderId) { + if ((normalizeOptionalString(event.sender) ?? "") !== senderId) { continue; } if (targetEmoji && extractMatrixReactionKey(event.content) !== targetEmoji) { continue; } - const eventId = event.event_id?.trim(); + const eventId = normalizeOptionalString(event.event_id); if (eventId) { ids.push(eventId); } diff --git a/extensions/matrix/src/migration-config.ts b/extensions/matrix/src/migration-config.ts index 39fd7d8abd1..70aae97cd0e 100644 --- a/extensions/matrix/src/migration-config.ts +++ b/extensions/matrix/src/migration-config.ts @@ -3,6 +3,7 @@ import os from "node:os"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { findMatrixAccountEntry, requiresExplicitMatrixDefaultAccount, @@ -40,7 +41,7 @@ export type MatrixLegacyFlatStoreTarget = MatrixMigrationAccountTarget & { type MatrixLegacyFlatStoreKind = "state" | "encrypted state"; function clean(value: unknown): string { - return typeof value === "string" ? value.trim() : ""; + return normalizeOptionalString(value) ?? ""; } function resolveMatrixAccountConfigEntry( diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 3d1d49ea3c7..cdbf89d5f78 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -4,7 +4,11 @@ import { type ChannelSetupWizardAdapter, } from "openclaw/plugin-sdk/setup"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { requiresExplicitMatrixDefaultAccount } from "./account-selection.js"; import { listMatrixDirectoryGroupsLive } from "./directory-live.js"; import { @@ -63,12 +67,18 @@ function isMatrixInviteAutoJoinTarget(entry: string): boolean { } function normalizeMatrixInviteAutoJoinTargets(entries: string[]): string[] { - return [...new Set(entries.map((entry) => entry.trim()).filter(Boolean))]; + return [ + ...new Set( + entries + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)), + ), + ]; } function resolveMatrixOnboardingAccountId(cfg: CoreConfig, accountId?: string): string { return normalizeAccountId( - accountId?.trim() || resolveDefaultMatrixAccountId(cfg) || DEFAULT_ACCOUNT_ID, + normalizeOptionalString(accountId) || resolveDefaultMatrixAccountId(cfg) || DEFAULT_ACCOUNT_ID, ); } @@ -130,7 +140,7 @@ async function promptMatrixAllowFrom(params: { message: "Matrix allowFrom (full @user:server; display name only if unique)", placeholder: "@user:server", initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"), }); const parts = splitSetupEntries(String(entry)); const resolvedIds: string[] = []; @@ -345,7 +355,7 @@ async function configureMatrixAccessPrompts(params: { const resolvedIds: string[] = []; const unresolved: string[] = []; for (const entry of accessConfig.entries) { - const trimmed = entry.trim(); + const trimmed = normalizeOptionalString(entry) ?? ""; if (!trimmed) { continue; } @@ -372,7 +382,12 @@ async function configureMatrixAccessPrompts(params: { unresolved.push(entry); } } - roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)]; + roomKeys = [ + ...resolvedIds, + ...unresolved + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)), + ]; if (resolvedIds.length > 0 || unresolved.length > 0) { await params.prompter.note( [ @@ -453,12 +468,13 @@ async function runMatrixConfigure(params: { const defaultAccountId = resolveDefaultMatrixAccountId(next); let accountId = defaultAccountId || DEFAULT_ACCOUNT_ID; if (params.intent === "add-account") { - const enteredName = String( - await params.prompter.text({ - message: "Matrix account name", - validate: (value) => (value?.trim() ? undefined : "Required"), - }), - ).trim(); + const enteredName = + normalizeStringifiedOptionalString( + await params.prompter.text({ + message: "Matrix account name", + validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"), + }), + ) ?? ""; accountId = normalizeAccountId(enteredName); if (enteredName !== accountId) { await params.prompter.note(`Account id will be "${accountId}".`, "Matrix account"); @@ -468,7 +484,7 @@ async function runMatrixConfigure(params: { } next = updateMatrixAccountConfig(next, accountId, { name: enteredName, enabled: true }); } else { - const override = params.accountOverrides?.[channel]?.trim(); + const override = normalizeOptionalString(params.accountOverrides?.[channel]); if (override) { accountId = normalizeAccountId(override); } else if (params.shouldPromptAccountIds) { @@ -517,22 +533,23 @@ async function runMatrixConfigure(params: { } } - const homeserver = String( - await params.prompter.text({ - message: "Matrix homeserver URL", - initialValue: existing.homeserver ?? envHomeserver, - validate: (value) => { - try { - validateMatrixHomeserverUrl(String(value ?? ""), { - allowPrivateNetwork: true, - }); - return undefined; - } catch (error) { - return error instanceof Error ? error.message : "Invalid Matrix homeserver URL"; - } - }, - }), - ).trim(); + const homeserver = + normalizeStringifiedOptionalString( + await params.prompter.text({ + message: "Matrix homeserver URL", + initialValue: existing.homeserver ?? envHomeserver, + validate: (value) => { + try { + validateMatrixHomeserverUrl(String(value ?? ""), { + allowPrivateNetwork: true, + }); + return undefined; + } catch (error) { + return error instanceof Error ? error.message : "Invalid Matrix homeserver URL"; + } + }, + }), + ) ?? ""; const requiresAllowPrivateNetwork = requiresMatrixPrivateNetworkOptIn(homeserver); const shouldPromptAllowPrivateNetwork = requiresAllowPrivateNetwork || isPrivateNetworkOptInEnabled(existing); @@ -575,50 +592,54 @@ async function runMatrixConfigure(params: { }); if (authMode === "token") { - accessToken = String( - await params.prompter.text({ - message: "Matrix access token", - validate: (value) => (value?.trim() ? undefined : "Required"), - }), - ).trim(); + accessToken = + normalizeStringifiedOptionalString( + await params.prompter.text({ + message: "Matrix access token", + validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"), + }), + ) ?? ""; password = undefined; userId = ""; } else { - userId = String( - await params.prompter.text({ - message: "Matrix user ID", - initialValue: existing.userId ?? envUserId, - validate: (value) => { - const raw = String(value ?? "").trim(); - if (!raw) { - return "Required"; - } - if (!raw.startsWith("@")) { - return "Matrix user IDs should start with @"; - } - if (!raw.includes(":")) { - return "Matrix user IDs should include a server (:server)"; - } - return undefined; - }, - }), - ).trim(); - password = String( - await params.prompter.text({ - message: "Matrix password", - validate: (value) => (value?.trim() ? undefined : "Required"), - }), - ).trim(); + userId = + normalizeStringifiedOptionalString( + await params.prompter.text({ + message: "Matrix user ID", + initialValue: existing.userId ?? envUserId, + validate: (value) => { + const raw = normalizeOptionalString(value) ?? ""; + if (!raw) { + return "Required"; + } + if (!raw.startsWith("@")) { + return "Matrix user IDs should start with @"; + } + if (!raw.includes(":")) { + return "Matrix user IDs should include a server (:server)"; + } + return undefined; + }, + }), + ) ?? ""; + password = + normalizeStringifiedOptionalString( + await params.prompter.text({ + message: "Matrix password", + validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"), + }), + ) ?? ""; accessToken = undefined; } } - const deviceName = String( - await params.prompter.text({ - message: "Matrix device name (optional)", - initialValue: existing.deviceName ?? "OpenClaw Gateway", - }), - ).trim(); + const deviceName = + normalizeStringifiedOptionalString( + await params.prompter.text({ + message: "Matrix device name (optional)", + initialValue: existing.deviceName ?? "OpenClaw Gateway", + }), + ) ?? ""; const enableEncryption = await params.prompter.confirm({ message: "Enable end-to-end encryption (E2EE)?", diff --git a/extensions/matrix/src/plugin-entry.runtime.ts b/extensions/matrix/src/plugin-entry.runtime.ts index c43ee0628b6..220e90abe0e 100644 --- a/extensions/matrix/src/plugin-entry.runtime.ts +++ b/extensions/matrix/src/plugin-entry.runtime.ts @@ -1,4 +1,5 @@ import type { GatewayRequestHandlerOptions } from "openclaw/plugin-sdk/core"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { formatMatrixErrorMessage } from "./matrix/errors.js"; function sendError(respond: (ok: boolean, payload?: unknown) => void, err: unknown) { @@ -18,13 +19,12 @@ export async function handleVerifyRecoveryKey({ }: GatewayRequestHandlerOptions): Promise { try { const { verifyMatrixRecoveryKey } = await import("./matrix/actions/verification.js"); - const key = typeof params?.key === "string" ? params.key : ""; - if (!key.trim()) { + const key = normalizeOptionalString(params?.key); + if (!key) { respond(false, { error: "key required" }); return; } - const accountId = - typeof params?.accountId === "string" ? params.accountId.trim() || undefined : undefined; + const accountId = normalizeOptionalString(params?.accountId); const result = await verifyMatrixRecoveryKey(key, { accountId }); respond(result.success, result); } catch (err) { @@ -38,8 +38,7 @@ export async function handleVerificationBootstrap({ }: GatewayRequestHandlerOptions): Promise { try { const { bootstrapMatrixVerification } = await import("./matrix/actions/verification.js"); - const accountId = - typeof params?.accountId === "string" ? params.accountId.trim() || undefined : undefined; + const accountId = normalizeOptionalString(params?.accountId); const recoveryKey = typeof params?.recoveryKey === "string" ? params.recoveryKey : undefined; const forceResetCrossSigning = params?.forceResetCrossSigning === true; const result = await bootstrapMatrixVerification({ @@ -59,8 +58,7 @@ export async function handleVerificationStatus({ }: GatewayRequestHandlerOptions): Promise { try { const { getMatrixVerificationStatus } = await import("./matrix/actions/verification.js"); - const accountId = - typeof params?.accountId === "string" ? params.accountId.trim() || undefined : undefined; + const accountId = normalizeOptionalString(params?.accountId); const includeRecoveryKey = params?.includeRecoveryKey === true; const status = await getMatrixVerificationStatus({ accountId, includeRecoveryKey }); respond(true, status);