@openclaw/nextcloud-talk 2026.8.1-beta.2 → 2026.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-CojgyJSq.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-C7Xe0Nkb.js";
2
2
  export { nextcloudTalkPlugin };
@@ -4,7 +4,7 @@ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries
4
4
  import { createAccountListHelpers, describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
5
5
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
6
6
  import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
7
- import { createAllowlistProviderRouteAllowlistWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
7
+ import { createAllowlistProviderRouteAllowlistWarningCollector, createConditionalWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
8
8
  import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
9
9
  import { FormatCapabilityProfile, convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
10
10
  import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
@@ -12,22 +12,22 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, norm
12
12
  import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
13
13
  import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
14
14
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
15
- import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
15
+ import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
16
16
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
17
17
  import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
18
- import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
+ import { readProviderJsonResponse, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
19
19
  import { resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
20
20
  import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
21
21
  import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
22
+ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
22
23
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
23
24
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
24
25
  import { createHmac, randomBytes } from "node:crypto";
25
26
  import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
26
- import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
27
27
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
28
- import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
28
+ import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
29
29
  import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
30
- import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
30
+ import { adaptScopedAccountAccessor, clearAccountFieldsFromConfigSection, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
31
31
  import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
32
32
  import { z } from "zod";
33
33
  import os from "node:os";
@@ -40,7 +40,7 @@ import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMat
40
40
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
41
41
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
42
42
  import { createServer } from "node:http";
43
- import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
43
+ import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
44
44
  import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
45
45
  import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
46
46
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
@@ -97,18 +97,26 @@ function listNextcloudTalkAccountIds(cfg) {
97
97
  debugAccounts("listNextcloudTalkAccountIds", ids);
98
98
  return ids;
99
99
  }
100
- function resolveNextcloudTalkSecret(cfg, opts) {
101
- const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
102
- const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
100
+ function resolveNextcloudTalkSecret(accountId, merged) {
101
+ const configuredSecret = resolveSecretInputString({
102
+ value: merged.botSecret,
103
+ path: `channels.nextcloud-talk.accounts.${accountId}.botSecret`,
104
+ mode: "inspect"
105
+ });
106
+ if (configuredSecret.status === "configured_unavailable") return {
107
+ secret: "",
108
+ source: "config",
109
+ status: "configured_unavailable"
110
+ };
103
111
  const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
104
- if (envSecret && resolvedAccountId === DEFAULT_ACCOUNT_ID) return {
112
+ if (envSecret && accountId === DEFAULT_ACCOUNT_ID) return {
105
113
  secret: envSecret,
106
114
  source: "env",
107
115
  status: "available"
108
116
  };
109
117
  const botSecretFile = normalizeOptionalString(merged.botSecretFile);
110
118
  if (botSecretFile) {
111
- const result = tryReadSecretFileSync(botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true }, { configPath: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecretFile` });
119
+ const result = tryReadSecretFileSync(botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true }, { configPath: `channels.nextcloud-talk.accounts.${accountId}.botSecretFile` });
112
120
  return result.status === "available" ? {
113
121
  secret: result.value,
114
122
  source: "secretFile",
@@ -120,12 +128,8 @@ function resolveNextcloudTalkSecret(cfg, opts) {
120
128
  diagnostic: result.diagnostic
121
129
  };
122
130
  }
123
- const inlineSecret = normalizeResolvedSecretInputString({
124
- value: merged.botSecret,
125
- path: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecret`
126
- });
127
- if (inlineSecret) return {
128
- secret: inlineSecret,
131
+ if (configuredSecret.status === "available") return {
132
+ secret: configuredSecret.value,
129
133
  source: "config",
130
134
  status: "available"
131
135
  };
@@ -142,14 +146,7 @@ function resolveNextcloudTalkAccount(params) {
142
146
  const merged = mergeNextcloudTalkAccountConfig(params.cfg, accountId);
143
147
  const accountEnabled = merged.enabled !== false;
144
148
  const enabled = baseEnabled && accountEnabled;
145
- const secretResolution = resolveNextcloudTalkSecret(params.cfg, { accountId });
146
- const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
147
- apiUser: merged.apiUser,
148
- apiPassword: merged.apiPassword,
149
- apiPasswordFile: merged.apiPasswordFile,
150
- configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`
151
- });
152
- const diagnostics = [secretResolution.diagnostic, apiCredentialResolution.status === "configured_unavailable" ? apiCredentialResolution.diagnostic : void 0].filter((diagnostic) => Boolean(diagnostic));
149
+ const secretResolution = resolveNextcloudTalkSecret(accountId, merged);
153
150
  const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
154
151
  debugAccounts("resolve", {
155
152
  accountId,
@@ -165,8 +162,7 @@ function resolveNextcloudTalkAccount(params) {
165
162
  secret: secretResolution.secret,
166
163
  secretSource: secretResolution.source,
167
164
  tokenStatus: secretResolution.status,
168
- apiCredentialStatus: apiCredentialResolution.status,
169
- ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
165
+ ...secretResolution.diagnostic ? { credentialDiagnostics: [secretResolution.diagnostic] } : {},
170
166
  config: merged
171
167
  };
172
168
  };
@@ -178,6 +174,21 @@ function resolveNextcloudTalkAccount(params) {
178
174
  resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
179
175
  });
180
176
  }
177
+ function inspectNextcloudTalkAccount(params) {
178
+ const account = resolveNextcloudTalkAccount(params);
179
+ const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
180
+ apiUser: account.config.apiUser,
181
+ apiPassword: account.config.apiPassword,
182
+ apiPasswordFile: account.config.apiPasswordFile,
183
+ configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`
184
+ });
185
+ const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" ? [apiCredentialResolution.diagnostic] : []];
186
+ return {
187
+ ...account,
188
+ apiCredentialStatus: apiCredentialResolution.status,
189
+ ...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
190
+ };
191
+ }
181
192
  //#endregion
182
193
  //#region extensions/nextcloud-talk/src/approval-auth.ts
183
194
  function normalizeNextcloudTalkApproverId(value) {
@@ -199,6 +210,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
199
210
  if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
200
211
  await params.release();
201
212
  }
213
+ async function readNextcloudTalkErrorBody(response, ...credentials) {
214
+ try {
215
+ let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
216
+ maxBytes: 8 * 1024,
217
+ chunkTimeoutMs: 1e4
218
+ });
219
+ for (const credential of credentials) body = body.replaceAll(credential, "***");
220
+ return redactToolPayloadText(body);
221
+ } catch {
222
+ return "";
223
+ }
224
+ }
202
225
  //#endregion
203
226
  //#region extensions/nextcloud-talk/src/signature.ts
204
227
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
@@ -245,7 +268,6 @@ function generateNextcloudTalkSignature(params) {
245
268
  //#endregion
246
269
  //#region extensions/nextcloud-talk/src/bot-preflight.ts
247
270
  const BOT_FEATURE_RESPONSE = 2;
248
- const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
249
271
  function normalizeUrlForMatch(value) {
250
272
  if (!value?.trim()) return "";
251
273
  try {
@@ -310,7 +332,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
310
332
  });
311
333
  try {
312
334
  if (!response.ok) {
313
- const body = await readResponseTextLimited(response, BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES).catch(() => "");
335
+ const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
314
336
  return {
315
337
  ok: false,
316
338
  code: "api_error",
@@ -362,6 +384,7 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
362
384
  sectionKey: "nextcloud-talk",
363
385
  listAccountIds: listNextcloudTalkAccountIds,
364
386
  resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
387
+ inspectAccount: adaptScopedAccountAccessor(inspectNextcloudTalkAccount),
365
388
  defaultAccountId: resolveDefaultNextcloudTalkAccountId,
366
389
  clearBaseFields: [
367
390
  "botSecret",
@@ -397,6 +420,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
397
420
  name: z.string().optional(),
398
421
  enabled: z.boolean().optional(),
399
422
  configWrites: z.boolean().optional(),
423
+ replyToMode: ReplyToModeSchema.optional(),
400
424
  markdown: MarkdownConfigSchema,
401
425
  baseUrl: z.string().optional(),
402
426
  botSecret: buildSecretInputSchema().optional(),
@@ -692,7 +716,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
692
716
  }
693
717
  //#endregion
694
718
  //#region extensions/nextcloud-talk/src/send.ts
695
- const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
696
719
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
697
720
  const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
698
721
  const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
@@ -713,20 +736,12 @@ function renderNextcloudTalkMarkdown(markdown) {
713
736
  escapeText: (text) => text
714
737
  }, NEXTCLOUD_TALK_FORMAT_PROFILE);
715
738
  }
716
- /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
739
+ /** Collapses and caps an already-redacted error body for display. */
717
740
  function collapseErrorSnippet(text) {
718
741
  const collapsed = text.replace(/\s+/g, " ").trim();
719
742
  if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
720
743
  return collapsed;
721
744
  }
722
- /** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
723
- async function readNextcloudTalkErrorSnippet(response) {
724
- try {
725
- return collapseErrorSnippet(await readResponseTextLimited(response, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES));
726
- } catch {
727
- return "";
728
- }
729
- }
730
745
  function resolveCredentials(explicit, account) {
731
746
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
732
747
  const secret = explicit.secret?.trim() ?? account.secret;
@@ -817,7 +832,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
817
832
  });
818
833
  try {
819
834
  if (!response.ok) {
820
- const errorBody = await readNextcloudTalkErrorSnippet(response);
835
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
821
836
  const status = response.status;
822
837
  let errorMsg = `Nextcloud Talk send failed (${status})`;
823
838
  if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
@@ -879,7 +894,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
879
894
  });
880
895
  try {
881
896
  if (!response.ok) {
882
- const errorBody = await readNextcloudTalkErrorSnippet(response);
897
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
883
898
  throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
884
899
  }
885
900
  return { ok: true };
@@ -950,7 +965,15 @@ async function handleNextcloudTalkInbound(params) {
950
965
  accountId: account.accountId
951
966
  });
952
967
  const rawBody = message.text?.trim() ?? "";
953
- if (!rawBody) return;
968
+ if (!rawBody) {
969
+ logInboundDrop({
970
+ log: (messageLocal) => runtime.log?.(messageLocal),
971
+ channel: CHANNEL_ID,
972
+ reason: `empty message body (mediaType=${message.mediaType})`,
973
+ target: message.senderId
974
+ });
975
+ return;
976
+ }
954
977
  const roomKind = await resolveNextcloudTalkRoomKind({
955
978
  account,
956
979
  roomToken: message.roomToken,
@@ -1352,7 +1375,7 @@ function createNextcloudTalkWebhookServer(opts) {
1352
1375
  res.end();
1353
1376
  return;
1354
1377
  }
1355
- const clientIp = req.socket.remoteAddress ?? "unknown";
1378
+ const clientIp = resolveRequestClientIp(req, opts.trustedProxies, opts.allowRealIpFallback) ?? req.socket.remoteAddress ?? "unknown";
1356
1379
  if (!webhookAuthRateLimiter.check(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE).allowed) {
1357
1380
  res.writeHead(429);
1358
1381
  res.end("Too Many Requests");
@@ -1410,6 +1433,7 @@ function createNextcloudTalkWebhookServer(opts) {
1410
1433
  const stop = async () => {
1411
1434
  stopRequested = true;
1412
1435
  await closeIfListening();
1436
+ webhookAuthRateLimiter.dispose();
1413
1437
  };
1414
1438
  const start = () => {
1415
1439
  if (stopRequested) return Promise.resolve();
@@ -1436,6 +1460,12 @@ function createNextcloudTalkWebhookServer(opts) {
1436
1460
  //#endregion
1437
1461
  //#region extensions/nextcloud-talk/src/webhook-spool.ts
1438
1462
  const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1463
+ function describeIgnoredWebhookEvent(rawEvent) {
1464
+ const envelope = parseRawObject(rawEvent);
1465
+ const type = typeof envelope.type === "string" ? envelope.type : "unknown";
1466
+ const object = isRecord(envelope.object) ? envelope.object : null;
1467
+ return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
1468
+ }
1439
1469
  const NextcloudTalkWebhookPayloadSchema = z.object({
1440
1470
  type: z.enum([
1441
1471
  "Create",
@@ -1562,7 +1592,11 @@ function createNextcloudTalkWebhookSpool(options) {
1562
1592
  if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1563
1593
  const receiveTask = (async () => {
1564
1594
  await startAfterMigration;
1565
- return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1595
+ if ((await monitor.admit(rawEvent)).kind === "ignored") {
1596
+ options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
1597
+ return "ignored";
1598
+ }
1599
+ return "accepted";
1566
1600
  })();
1567
1601
  inFlightReceives.add(receiveTask);
1568
1602
  receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
@@ -1647,6 +1681,8 @@ async function monitorNextcloudTalkProvider(opts) {
1647
1681
  onError: (error) => {
1648
1682
  logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
1649
1683
  },
1684
+ trustedProxies: cfg.gateway?.trustedProxies,
1685
+ allowRealIpFallback: cfg.gateway?.allowRealIpFallback,
1650
1686
  abortSignal: opts.abortSignal
1651
1687
  });
1652
1688
  let stopPromise;
@@ -1704,44 +1740,18 @@ const nextcloudTalkGatewayAdapter = {
1704
1740
  });
1705
1741
  },
1706
1742
  logoutAccount: async ({ accountId, cfg }) => {
1707
- const nextCfg = { ...cfg };
1708
- const nextSection = cfg.channels?.["nextcloud-talk"] ? { ...cfg.channels["nextcloud-talk"] } : void 0;
1709
- let cleared = false;
1710
- let changed = false;
1711
- if (nextSection) {
1712
- if (accountId === DEFAULT_ACCOUNT_ID$2 && nextSection.botSecret) {
1713
- delete nextSection.botSecret;
1714
- cleared = true;
1715
- changed = true;
1716
- }
1717
- const accountCleanup = clearAccountEntryFields({
1718
- accounts: nextSection.accounts,
1719
- accountId,
1720
- fields: ["botSecret"]
1721
- });
1722
- if (accountCleanup.changed) {
1723
- changed = true;
1724
- if (accountCleanup.cleared) cleared = true;
1725
- if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
1726
- else delete nextSection.accounts;
1727
- }
1728
- }
1729
- if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
1730
- ...nextCfg.channels,
1731
- "nextcloud-talk": nextSection
1732
- };
1733
- else {
1734
- const nextChannels = { ...nextCfg.channels };
1735
- delete nextChannels["nextcloud-talk"];
1736
- if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
1737
- else delete nextCfg.channels;
1738
- }
1743
+ const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
1744
+ cfg,
1745
+ sectionKey: "nextcloud-talk",
1746
+ accountId,
1747
+ fields: ["botSecret"]
1748
+ });
1739
1749
  const loggedOut = resolveNextcloudTalkAccount({
1740
- cfg: changed ? nextCfg : cfg,
1750
+ cfg: nextConfig,
1741
1751
  accountId
1742
1752
  }).secretSource === "none";
1743
1753
  if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
1744
- nextConfig: nextCfg,
1754
+ nextConfig,
1745
1755
  afterWrite: { mode: "auto" }
1746
1756
  });
1747
1757
  return {
@@ -2192,6 +2202,12 @@ const collectNextcloudTalkSecurityWarnings = createAllowlistProviderRouteAllowli
2192
2202
  groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom"
2193
2203
  }
2194
2204
  });
2205
+ const collectNextcloudTalkOpenGroupFindings = createConditionalWarningCollector.findings({
2206
+ collectWarnings: collectNextcloudTalkSecurityWarnings,
2207
+ checkId: "channels.nextcloud-talk.groups.open",
2208
+ severity: "critical",
2209
+ title: "Nextcloud Talk security warning"
2210
+ });
2195
2211
  const nextcloudTalkPlugin = createChatChannelPlugin({
2196
2212
  base: {
2197
2213
  id: "nextcloud-talk",
@@ -2288,7 +2304,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2288
2304
  } },
2289
2305
  security: {
2290
2306
  ...nextcloudTalkSecurityAdapter,
2291
- collectWarnings: collectNextcloudTalkSecurityWarnings
2307
+ collectWarnings: collectNextcloudTalkOpenGroupFindings
2292
2308
  },
2293
2309
  outbound: {
2294
2310
  base: {
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-CojgyJSq.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-C7Xe0Nkb.js";
2
2
  export { nextcloudTalkPlugin };
@@ -0,0 +1,25 @@
1
+ import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, mergeAccountConfig } from "openclaw/plugin-sdk/account-core";
2
+ //#region extensions/nextcloud-talk/configured-state.ts
3
+ function hasConfiguredNextcloudAccount(account, env) {
4
+ return Boolean(account?.baseUrl?.trim() && (hasConfiguredAccountValue(account.botSecret) || hasConfiguredAccountValue(account.botSecretFile) || hasConfiguredAccountValue(env.NEXTCLOUD_TALK_BOT_SECRET)));
5
+ }
6
+ /** Require a Nextcloud server plus its account-owned bot credential. */
7
+ function hasConfiguredNextcloudTalkChannelState(params) {
8
+ const channel = params.cfg.channels?.["nextcloud-talk"];
9
+ if (channel?.enabled === false) return false;
10
+ const defaultAccount = channel?.accounts?.[DEFAULT_ACCOUNT_ID];
11
+ if (defaultAccount?.enabled !== false) {
12
+ if (hasConfiguredNextcloudAccount(defaultAccount ? mergeAccountConfig({
13
+ channelConfig: channel,
14
+ accountConfig: defaultAccount,
15
+ omitKeys: ["defaultAccount"]
16
+ }) : channel, params.env ?? process.env)) return true;
17
+ }
18
+ return Object.entries(channel?.accounts ?? {}).some(([accountId, account]) => accountId !== DEFAULT_ACCOUNT_ID && account.enabled !== false && hasConfiguredNextcloudAccount(mergeAccountConfig({
19
+ channelConfig: channel,
20
+ accountConfig: account,
21
+ omitKeys: ["defaultAccount"]
22
+ }), {}));
23
+ }
24
+ //#endregion
25
+ export { hasConfiguredNextcloudTalkChannelState };
@@ -32,6 +32,26 @@
32
32
  "configWrites": {
33
33
  "type": "boolean"
34
34
  },
35
+ "replyToMode": {
36
+ "anyOf": [
37
+ {
38
+ "type": "string",
39
+ "const": "off"
40
+ },
41
+ {
42
+ "type": "string",
43
+ "const": "first"
44
+ },
45
+ {
46
+ "type": "string",
47
+ "const": "all"
48
+ },
49
+ {
50
+ "type": "string",
51
+ "const": "batched"
52
+ }
53
+ ]
54
+ },
35
55
  "markdown": {
36
56
  "type": "object",
37
57
  "properties": {
@@ -479,6 +499,26 @@
479
499
  "configWrites": {
480
500
  "type": "boolean"
481
501
  },
502
+ "replyToMode": {
503
+ "anyOf": [
504
+ {
505
+ "type": "string",
506
+ "const": "off"
507
+ },
508
+ {
509
+ "type": "string",
510
+ "const": "first"
511
+ },
512
+ {
513
+ "type": "string",
514
+ "const": "all"
515
+ },
516
+ {
517
+ "type": "string",
518
+ "const": "batched"
519
+ }
520
+ ]
521
+ },
482
522
  "markdown": {
483
523
  "type": "object",
484
524
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.8.1-beta.2",
3
+ "version": "2026.8.1",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "peerDependencies": {
11
- "openclaw": ">=2026.8.1-beta.2"
11
+ "openclaw": ">=2026.8.1"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -31,7 +31,9 @@
31
31
  "NEXTCLOUD_TALK_BOT_SECRET",
32
32
  "NEXTCLOUD_TALK_API_PASSWORD"
33
33
  ]
34
- }
34
+ },
35
+ "specifier": "./dist/configured-state.js",
36
+ "exportName": "hasConfiguredNextcloudTalkChannelState"
35
37
  },
36
38
  "label": "Nextcloud Talk",
37
39
  "selectionLabel": "Nextcloud Talk (self-hosted)",
@@ -127,10 +129,10 @@
127
129
  "minHostVersion": ">=2026.4.10"
128
130
  },
129
131
  "compat": {
130
- "pluginApi": ">=2026.8.1-beta.2"
132
+ "pluginApi": ">=2026.8.1"
131
133
  },
132
134
  "build": {
133
- "openclawVersion": "2026.8.1-beta.2"
135
+ "openclawVersion": "2026.8.1"
134
136
  },
135
137
  "release": {
136
138
  "publishToClawHub": true,