From ed59629ccd827fe5708fecae976224eb9a590dc7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 29 May 2026 06:45:24 -0400 Subject: [PATCH] fix(nextcloud-talk): centralize integer coercion --- .../nextcloud-talk/src/bot-preflight.test.ts | 13 ++++++++ .../nextcloud-talk/src/bot-preflight.ts | 7 ++--- .../nextcloud-talk/src/room-info.test.ts | 30 +++++++++++++++++++ extensions/nextcloud-talk/src/room-info.ts | 7 ++--- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/extensions/nextcloud-talk/src/bot-preflight.test.ts b/extensions/nextcloud-talk/src/bot-preflight.test.ts index 07bfa333dca..a21b81cabb1 100644 --- a/extensions/nextcloud-talk/src/bot-preflight.test.ts +++ b/extensions/nextcloud-talk/src/bot-preflight.test.ts @@ -80,6 +80,19 @@ describe("probeNextcloudTalkBotResponseFeature", () => { }); }); + it("normalizes signed decimal bot feature strings through the shared parser", async () => { + mockBotAdmin("+011"); + + await expect(probeNextcloudTalkBotResponseFeature({ account: account() })).resolves.toEqual({ + ok: true, + code: "ok", + botId: "7", + botName: "OpenClaw", + features: 11, + message: 'Nextcloud Talk bot "OpenClaw" has the response feature.', + }); + }); + it("reports missing response feature for the matching webhook bot", async () => { mockBotAdmin(1 | 8); diff --git a/extensions/nextcloud-talk/src/bot-preflight.ts b/extensions/nextcloud-talk/src/bot-preflight.ts index fa93e5deaf7..bec844691b7 100644 --- a/extensions/nextcloud-talk/src/bot-preflight.ts +++ b/extensions/nextcloud-talk/src/bot-preflight.ts @@ -1,4 +1,5 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime"; import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard } from "../runtime-api.js"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; @@ -50,11 +51,7 @@ function coerceFeatureMask(value: unknown): number | undefined { if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) { return value; } - if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) { - const parsed = Number(value.trim()); - return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined; - } - return undefined; + return parseStrictNonNegativeInteger(value); } function formatMissingResponseFeatureMessage(bot: NextcloudTalkBotAdminEntry, features?: number) { diff --git a/extensions/nextcloud-talk/src/room-info.test.ts b/extensions/nextcloud-talk/src/room-info.test.ts index 07beab642c1..5f358003a8e 100644 --- a/extensions/nextcloud-talk/src/room-info.test.ts +++ b/extensions/nextcloud-talk/src/room-info.test.ts @@ -73,6 +73,36 @@ describe("nextcloud talk room info", () => { expect(release).toHaveBeenCalledTimes(1); }); + it("normalizes signed decimal room type strings through the shared parser", async () => { + fetchWithSsrFGuard.mockResolvedValue({ + response: { + ok: true, + json: async () => ({ + ocs: { + data: { + type: "+01", + }, + }, + }), + }, + release: vi.fn(async () => {}), + }); + + await expect( + resolveNextcloudTalkRoomKind({ + account: { + accountId: "acct-direct-string", + baseUrl: "https://nc.example.com", + config: { + apiUser: "bot", + apiPassword: "secret", + }, + } as never, + roomToken: "room-direct-string", + }), + ).resolves.toBe("direct"); + }); + it("does not coerce partial room type strings", async () => { fetchWithSsrFGuard.mockResolvedValue({ response: { diff --git a/extensions/nextcloud-talk/src/room-info.ts b/extensions/nextcloud-talk/src/room-info.ts index 13c21a5b074..85d3b7c3bf4 100644 --- a/extensions/nextcloud-talk/src/room-info.ts +++ b/extensions/nextcloud-talk/src/room-info.ts @@ -1,4 +1,5 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import { ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime"; import { fetchWithSsrFGuard, type RuntimeEnv } from "../runtime-api.js"; @@ -27,11 +28,7 @@ function coerceRoomType(value: unknown): number | undefined { if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { return value; } - if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) { - const parsed = Number(value.trim()); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined; - } - return undefined; + return parseStrictPositiveInteger(value); } function resolveRoomKindFromType(type: number | undefined): "direct" | "group" | undefined {