From 323a0efdd078d6599cf5c95aace1dacddcacd3ba Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 23 Aug 2026 13:07:45 -0700 Subject: [PATCH] improve(nextcloud-talk): avoid room credential reads on sends (#128344) * perf(nextcloud-talk): skip room credentials on sends * fix(nextcloud-talk): use account inspection hook --------- Co-authored-by: Amp --- extensions/nextcloud-talk/src/accounts.ts | 43 ++++++++++++------- .../nextcloud-talk/src/channel.adapters.ts | 2 + .../nextcloud-talk/src/channel.status.test.ts | 37 ++++++++++++++++ 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/extensions/nextcloud-talk/src/accounts.ts b/extensions/nextcloud-talk/src/accounts.ts index cfbeff186cbf..efa8847a53aa 100644 --- a/extensions/nextcloud-talk/src/accounts.ts +++ b/extensions/nextcloud-talk/src/accounts.ts @@ -120,20 +120,6 @@ export function resolveNextcloudTalkAccount(params: { const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const secretResolution = resolveNextcloudTalkSecret(accountId, merged); - const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({ - apiUser: merged.apiUser, - apiPassword: merged.apiPassword, - apiPasswordFile: merged.apiPasswordFile, - configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`, - }); - const diagnostics = [ - secretResolution.diagnostic, - apiCredentialResolution.status === "configured_unavailable" - ? apiCredentialResolution.diagnostic - : undefined, - ].filter((diagnostic): diagnostic is NextcloudTalkCredentialUnavailableDiagnostic => - Boolean(diagnostic), - ); const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? ""; debugAccounts("resolve", { @@ -151,8 +137,9 @@ export function resolveNextcloudTalkAccount(params: { secret: secretResolution.secret, secretSource: secretResolution.source, tokenStatus: secretResolution.status, - apiCredentialStatus: apiCredentialResolution.status, - ...(diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {}), + ...(secretResolution.diagnostic + ? { credentialDiagnostics: [secretResolution.diagnostic] } + : {}), config: merged, } satisfies ResolvedNextcloudTalkAccount; }; @@ -165,3 +152,27 @@ export function resolveNextcloudTalkAccount(params: { resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg), }); } + +export function inspectNextcloudTalkAccount(params: { + cfg: CoreConfig; + accountId?: string | null; +}): ResolvedNextcloudTalkAccount { + const account = resolveNextcloudTalkAccount(params); + const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({ + apiUser: account.config.apiUser, + apiPassword: account.config.apiPassword, + apiPasswordFile: account.config.apiPasswordFile, + configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`, + }); + const credentialDiagnostics = [ + ...(account.credentialDiagnostics ?? []), + ...(apiCredentialResolution.status === "configured_unavailable" + ? [apiCredentialResolution.diagnostic] + : []), + ]; + return { + ...account, + apiCredentialStatus: apiCredentialResolution.status, + ...(credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}), + }; +} diff --git a/extensions/nextcloud-talk/src/channel.adapters.ts b/extensions/nextcloud-talk/src/channel.adapters.ts index daa99f2480f8..2573cbd332be 100644 --- a/extensions/nextcloud-talk/src/channel.adapters.ts +++ b/extensions/nextcloud-talk/src/channel.adapters.ts @@ -8,6 +8,7 @@ import { import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { + inspectNextcloudTalkAccount, listNextcloudTalkAccountIds, resolveDefaultNextcloudTalkAccountId, resolveNextcloudTalkAccount, @@ -23,6 +24,7 @@ export const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter< sectionKey: "nextcloud-talk", listAccountIds: listNextcloudTalkAccountIds, resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount), + inspectAccount: adaptScopedAccountAccessor(inspectNextcloudTalkAccount), defaultAccountId: resolveDefaultNextcloudTalkAccountId, clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"], resolveAllowFrom: (account) => account.config.allowFrom, diff --git a/extensions/nextcloud-talk/src/channel.status.test.ts b/extensions/nextcloud-talk/src/channel.status.test.ts index ca142f67d09a..eb62855b8979 100644 --- a/extensions/nextcloud-talk/src/channel.status.test.ts +++ b/extensions/nextcloud-talk/src/channel.status.test.ts @@ -1,6 +1,10 @@ // Nextcloud Talk tests cover channel.status plugin behavior. +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { nextcloudTalkPlugin } from "./channel.js"; +import type { CoreConfig } from "./types.js"; describe("nextcloud-talk channel status", () => { it("classifies room tokens as groups", () => { @@ -32,4 +36,37 @@ describe("nextcloud-talk channel status", () => { }, ]); }); + + it("keeps API credential inspection off runtime resolution", async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nextcloud-talk-status-")); + const apiPasswordFile = path.join(directory, "api-password"); + const cfg = { + channels: { + "nextcloud-talk": { + baseUrl: "https://cloud.example.com", + botSecret: "bot-secret", + apiUser: "bot", + apiPasswordFile, + }, + }, + } satisfies CoreConfig; + + try { + const account = nextcloudTalkPlugin.config.resolveAccount(cfg, "default"); + expect(account.apiCredentialStatus).toBeUndefined(); + expect(account.credentialDiagnostics).toBeUndefined(); + + fs.writeFileSync(apiPasswordFile, "api-password\n", "utf8"); + const inspected = (await nextcloudTalkPlugin.config.inspectAccount?.( + cfg, + "default", + )) as typeof account; + expect(nextcloudTalkPlugin.config.describeAccount?.(inspected, cfg)).toMatchObject({ + configured: true, + apiCredentialStatus: "available", + }); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); });