@openclaw/nextcloud-talk 2026.3.1 → 2026.3.7

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/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/nextcloud-talk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/nextcloud-talk";
3
3
  import { nextcloudTalkPlugin } from "./src/channel.js";
4
4
  import { setNextcloudTalkRuntime } from "./src/runtime.js";
5
5
 
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.3.1",
3
+ "version": "2026.3.7",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
5
  "type": "module",
6
+ "dependencies": {
7
+ "zod": "^4.3.6"
8
+ },
6
9
  "openclaw": {
7
10
  "extensions": [
8
11
  "./index.ts"
package/src/accounts.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import {
3
+ createAccountListHelpers,
3
4
  DEFAULT_ACCOUNT_ID,
4
5
  normalizeAccountId,
5
- normalizeOptionalAccountId,
6
- } from "openclaw/plugin-sdk/account-id";
6
+ resolveAccountWithDefaultFallback,
7
+ } from "openclaw/plugin-sdk/nextcloud-talk";
8
+ import { normalizeResolvedSecretInputString } from "./secret-input.js";
7
9
  import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
8
10
 
9
11
  function isTruthyEnvValue(value?: string): boolean {
@@ -27,45 +29,18 @@ export type ResolvedNextcloudTalkAccount = {
27
29
  config: NextcloudTalkAccountConfig;
28
30
  };
29
31
 
30
- function listConfiguredAccountIds(cfg: CoreConfig): string[] {
31
- const accounts = cfg.channels?.["nextcloud-talk"]?.accounts;
32
- if (!accounts || typeof accounts !== "object") {
33
- return [];
34
- }
35
- const ids = new Set<string>();
36
- for (const key of Object.keys(accounts)) {
37
- if (!key) {
38
- continue;
39
- }
40
- ids.add(normalizeAccountId(key));
41
- }
42
- return [...ids];
43
- }
32
+ const {
33
+ listAccountIds: listNextcloudTalkAccountIdsInternal,
34
+ resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId,
35
+ } = createAccountListHelpers("nextcloud-talk", {
36
+ normalizeAccountId,
37
+ });
38
+ export { resolveDefaultNextcloudTalkAccountId };
44
39
 
45
40
  export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
46
- const ids = listConfiguredAccountIds(cfg);
41
+ const ids = listNextcloudTalkAccountIdsInternal(cfg);
47
42
  debugAccounts("listNextcloudTalkAccountIds", ids);
48
- if (ids.length === 0) {
49
- return [DEFAULT_ACCOUNT_ID];
50
- }
51
- return ids.toSorted((a, b) => a.localeCompare(b));
52
- }
53
-
54
- export function resolveDefaultNextcloudTalkAccountId(cfg: CoreConfig): string {
55
- const preferred = normalizeOptionalAccountId(cfg.channels?.["nextcloud-talk"]?.defaultAccount);
56
- if (
57
- preferred &&
58
- listNextcloudTalkAccountIds(cfg).some(
59
- (accountId) => normalizeAccountId(accountId) === preferred,
60
- )
61
- ) {
62
- return preferred;
63
- }
64
- const ids = listNextcloudTalkAccountIds(cfg);
65
- if (ids.includes(DEFAULT_ACCOUNT_ID)) {
66
- return DEFAULT_ACCOUNT_ID;
67
- }
68
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
43
+ return ids;
69
44
  }
70
45
 
71
46
  function resolveAccountConfig(
@@ -123,8 +98,12 @@ function resolveNextcloudTalkSecret(
123
98
  }
124
99
  }
125
100
 
126
- if (merged.botSecret?.trim()) {
127
- return { secret: merged.botSecret.trim(), source: "config" };
101
+ const inlineSecret = normalizeResolvedSecretInputString({
102
+ value: merged.botSecret,
103
+ path: `channels.nextcloud-talk.accounts.${opts.accountId ?? DEFAULT_ACCOUNT_ID}.botSecret`,
104
+ });
105
+ if (inlineSecret) {
106
+ return { secret: inlineSecret, source: "config" };
128
107
  }
129
108
 
130
109
  return { secret: "", source: "none" };
@@ -134,7 +113,6 @@ export function resolveNextcloudTalkAccount(params: {
134
113
  cfg: CoreConfig;
135
114
  accountId?: string | null;
136
115
  }): ResolvedNextcloudTalkAccount {
137
- const hasExplicitAccountId = Boolean(params.accountId?.trim());
138
116
  const baseEnabled = params.cfg.channels?.["nextcloud-talk"]?.enabled !== false;
139
117
 
140
118
  const resolve = (accountId: string) => {
@@ -162,24 +140,13 @@ export function resolveNextcloudTalkAccount(params: {
162
140
  } satisfies ResolvedNextcloudTalkAccount;
163
141
  };
164
142
 
165
- const normalized = normalizeAccountId(params.accountId);
166
- const primary = resolve(normalized);
167
- if (hasExplicitAccountId) {
168
- return primary;
169
- }
170
- if (primary.secretSource !== "none") {
171
- return primary;
172
- }
173
-
174
- const fallbackId = resolveDefaultNextcloudTalkAccountId(params.cfg);
175
- if (fallbackId === primary.accountId) {
176
- return primary;
177
- }
178
- const fallback = resolve(fallbackId);
179
- if (fallback.secretSource === "none") {
180
- return primary;
181
- }
182
- return fallback;
143
+ return resolveAccountWithDefaultFallback({
144
+ accountId: params.accountId,
145
+ normalizeAccountId,
146
+ resolvePrimary: resolve,
147
+ hasCredential: (account) => account.secretSource !== "none",
148
+ resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg),
149
+ });
183
150
  }
184
151
 
185
152
  export function listEnabledNextcloudTalkAccounts(cfg: CoreConfig): ResolvedNextcloudTalkAccount[] {
@@ -1,10 +1,5 @@
1
- import type {
2
- ChannelAccountSnapshot,
3
- ChannelGatewayContext,
4
- OpenClawConfig,
5
- } from "openclaw/plugin-sdk";
6
1
  import { afterEach, describe, expect, it, vi } from "vitest";
7
- import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
2
+ import { createStartAccountContext } from "../../test-utils/start-account-context.js";
8
3
  import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
9
4
 
10
5
  const hoisted = vi.hoisted(() => ({
@@ -21,40 +16,16 @@ vi.mock("./monitor.js", async () => {
21
16
 
22
17
  import { nextcloudTalkPlugin } from "./channel.js";
23
18
 
24
- function createStartAccountCtx(params: {
25
- account: ResolvedNextcloudTalkAccount;
26
- abortSignal: AbortSignal;
27
- }): ChannelGatewayContext<ResolvedNextcloudTalkAccount> {
28
- const snapshot: ChannelAccountSnapshot = {
29
- accountId: params.account.accountId,
30
- configured: true,
31
- enabled: true,
32
- running: false,
33
- };
34
- return {
35
- accountId: params.account.accountId,
36
- account: params.account,
37
- cfg: {} as OpenClawConfig,
38
- runtime: createRuntimeEnv(),
39
- abortSignal: params.abortSignal,
40
- log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
41
- getStatus: () => snapshot,
42
- setStatus: (next) => {
43
- Object.assign(snapshot, next);
44
- },
45
- };
46
- }
47
-
48
19
  function buildAccount(): ResolvedNextcloudTalkAccount {
49
20
  return {
50
21
  accountId: "default",
51
22
  enabled: true,
52
23
  baseUrl: "https://nextcloud.example.com",
53
- secret: "secret",
54
- secretSource: "config",
24
+ secret: "secret", // pragma: allowlist secret
25
+ secretSource: "config", // pragma: allowlist secret
55
26
  config: {
56
27
  baseUrl: "https://nextcloud.example.com",
57
- botSecret: "secret",
28
+ botSecret: "secret", // pragma: allowlist secret
58
29
  webhookPath: "/nextcloud-talk-webhook",
59
30
  webhookPort: 8788,
60
31
  },
@@ -72,22 +43,19 @@ describe("nextcloudTalkPlugin gateway.startAccount", () => {
72
43
  const abort = new AbortController();
73
44
 
74
45
  const task = nextcloudTalkPlugin.gateway!.startAccount!(
75
- createStartAccountCtx({
46
+ createStartAccountContext({
76
47
  account: buildAccount(),
77
48
  abortSignal: abort.signal,
78
49
  }),
79
50
  );
80
-
81
- await new Promise((resolve) => setTimeout(resolve, 20));
82
-
83
51
  let settled = false;
84
52
  void task.then(() => {
85
53
  settled = true;
86
54
  });
87
-
88
- await new Promise((resolve) => setTimeout(resolve, 20));
55
+ await vi.waitFor(() => {
56
+ expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
57
+ });
89
58
  expect(settled).toBe(false);
90
- expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
91
59
  expect(stop).not.toHaveBeenCalled();
92
60
 
93
61
  abort.abort();
@@ -103,7 +71,7 @@ describe("nextcloudTalkPlugin gateway.startAccount", () => {
103
71
  abort.abort();
104
72
 
105
73
  await nextcloudTalkPlugin.gateway!.startAccount!(
106
- createStartAccountCtx({
74
+ createStartAccountContext({
107
75
  account: buildAccount(),
108
76
  abortSignal: abort.signal,
109
77
  }),
package/src/channel.ts CHANGED
@@ -1,17 +1,24 @@
1
+ import {
2
+ buildAccountScopedDmSecurityPolicy,
3
+ collectAllowlistProviderGroupPolicyWarnings,
4
+ collectOpenGroupPolicyRouteAllowlistWarnings,
5
+ formatAllowFromLowercase,
6
+ mapAllowFromEntries,
7
+ } from "openclaw/plugin-sdk/compat";
1
8
  import {
2
9
  applyAccountNameToChannelSection,
10
+ buildBaseChannelStatusSummary,
3
11
  buildChannelConfigSchema,
12
+ buildRuntimeAccountStatusSnapshot,
13
+ clearAccountEntryFields,
4
14
  DEFAULT_ACCOUNT_ID,
5
15
  deleteAccountFromConfigSection,
6
- formatPairingApproveHint,
7
16
  normalizeAccountId,
8
- resolveAllowlistProviderRuntimeGroupPolicy,
9
- resolveDefaultGroupPolicy,
10
17
  setAccountEnabledInConfigSection,
11
18
  type ChannelPlugin,
12
19
  type OpenClawConfig,
13
20
  type ChannelSetupInput,
14
- } from "openclaw/plugin-sdk";
21
+ } from "openclaw/plugin-sdk/nextcloud-talk";
15
22
  import { waitForAbortSignal } from "../../../src/infra/abort-signal.js";
16
23
  import {
17
24
  listNextcloudTalkAccountIds,
@@ -102,55 +109,55 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
102
109
  baseUrl: account.baseUrl ? "[set]" : "[missing]",
103
110
  }),
104
111
  resolveAllowFrom: ({ cfg, accountId }) =>
105
- (
106
- resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []
107
- ).map((entry) => String(entry).toLowerCase()),
112
+ mapAllowFromEntries(
113
+ resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom,
114
+ ).map((entry) => entry.toLowerCase()),
108
115
  formatAllowFrom: ({ allowFrom }) =>
109
- allowFrom
110
- .map((entry) => String(entry).trim())
111
- .filter(Boolean)
112
- .map((entry) => entry.replace(/^(nextcloud-talk|nc-talk|nc):/i, ""))
113
- .map((entry) => entry.toLowerCase()),
116
+ formatAllowFromLowercase({
117
+ allowFrom,
118
+ stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
119
+ }),
114
120
  },
115
121
  security: {
116
122
  resolveDmPolicy: ({ cfg, accountId, account }) => {
117
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
118
- const useAccountPath = Boolean(
119
- cfg.channels?.["nextcloud-talk"]?.accounts?.[resolvedAccountId],
120
- );
121
- const basePath = useAccountPath
122
- ? `channels.nextcloud-talk.accounts.${resolvedAccountId}.`
123
- : "channels.nextcloud-talk.";
124
- return {
125
- policy: account.config.dmPolicy ?? "pairing",
123
+ return buildAccountScopedDmSecurityPolicy({
124
+ cfg,
125
+ channelKey: "nextcloud-talk",
126
+ accountId,
127
+ fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
128
+ policy: account.config.dmPolicy,
126
129
  allowFrom: account.config.allowFrom ?? [],
127
- policyPath: `${basePath}dmPolicy`,
128
- allowFromPath: basePath,
129
- approveHint: formatPairingApproveHint("nextcloud-talk"),
130
+ policyPathSuffix: "dmPolicy",
130
131
  normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
131
- };
132
+ });
132
133
  },
133
134
  collectWarnings: ({ account, cfg }) => {
134
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
135
- const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
135
+ const roomAllowlistConfigured =
136
+ account.config.rooms && Object.keys(account.config.rooms).length > 0;
137
+ return collectAllowlistProviderGroupPolicyWarnings({
138
+ cfg,
136
139
  providerConfigPresent:
137
140
  (cfg.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] !== undefined,
138
- groupPolicy: account.config.groupPolicy,
139
- defaultGroupPolicy,
141
+ configuredGroupPolicy: account.config.groupPolicy,
142
+ collect: (groupPolicy) =>
143
+ collectOpenGroupPolicyRouteAllowlistWarnings({
144
+ groupPolicy,
145
+ routeAllowlistConfigured: Boolean(roomAllowlistConfigured),
146
+ restrictSenders: {
147
+ surface: "Nextcloud Talk rooms",
148
+ openScope: "any member in allowed rooms",
149
+ groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
150
+ groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
151
+ },
152
+ noRouteAllowlist: {
153
+ surface: "Nextcloud Talk rooms",
154
+ routeAllowlistPath: "channels.nextcloud-talk.rooms",
155
+ routeScope: "room",
156
+ groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
157
+ groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
158
+ },
159
+ }),
140
160
  });
141
- if (groupPolicy !== "open") {
142
- return [];
143
- }
144
- const roomAllowlistConfigured =
145
- account.config.rooms && Object.keys(account.config.rooms).length > 0;
146
- if (roomAllowlistConfigured) {
147
- return [
148
- `- Nextcloud Talk rooms: groupPolicy="open" allows any member in allowed rooms to trigger (mention-gated). Set channels.nextcloud-talk.groupPolicy="allowlist" + channels.nextcloud-talk.groupAllowFrom to restrict senders.`,
149
- ];
150
- }
151
- return [
152
- `- Nextcloud Talk rooms: groupPolicy="open" with no channels.nextcloud-talk.rooms allowlist; any room can add + ping (mention-gated). Set channels.nextcloud-talk.groupPolicy="allowlist" + channels.nextcloud-talk.groupAllowFrom or configure channels.nextcloud-talk.rooms.`,
153
- ];
154
161
  },
155
162
  },
156
163
  groups: {
@@ -262,18 +269,20 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
262
269
  chunker: (text, limit) => getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit),
263
270
  chunkerMode: "markdown",
264
271
  textChunkLimit: 4000,
265
- sendText: async ({ to, text, accountId, replyToId }) => {
272
+ sendText: async ({ cfg, to, text, accountId, replyToId }) => {
266
273
  const result = await sendMessageNextcloudTalk(to, text, {
267
274
  accountId: accountId ?? undefined,
268
275
  replyTo: replyToId ?? undefined,
276
+ cfg: cfg as CoreConfig,
269
277
  });
270
278
  return { channel: "nextcloud-talk", ...result };
271
279
  },
272
- sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => {
280
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => {
273
281
  const messageWithMedia = mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text;
274
282
  const result = await sendMessageNextcloudTalk(to, messageWithMedia, {
275
283
  accountId: accountId ?? undefined,
276
284
  replyTo: replyToId ?? undefined,
285
+ cfg: cfg as CoreConfig,
277
286
  });
278
287
  return { channel: "nextcloud-talk", ...result };
279
288
  },
@@ -286,17 +295,21 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
286
295
  lastStopAt: null,
287
296
  lastError: null,
288
297
  },
289
- buildChannelSummary: ({ snapshot }) => ({
290
- configured: snapshot.configured ?? false,
291
- secretSource: snapshot.secretSource ?? "none",
292
- running: snapshot.running ?? false,
293
- mode: "webhook",
294
- lastStartAt: snapshot.lastStartAt ?? null,
295
- lastStopAt: snapshot.lastStopAt ?? null,
296
- lastError: snapshot.lastError ?? null,
297
- }),
298
+ buildChannelSummary: ({ snapshot }) => {
299
+ const base = buildBaseChannelStatusSummary(snapshot);
300
+ return {
301
+ configured: base.configured,
302
+ secretSource: snapshot.secretSource ?? "none",
303
+ running: base.running,
304
+ mode: "webhook",
305
+ lastStartAt: base.lastStartAt,
306
+ lastStopAt: base.lastStopAt,
307
+ lastError: base.lastError,
308
+ };
309
+ },
298
310
  buildAccountSnapshot: ({ account, runtime }) => {
299
311
  const configured = Boolean(account.secret?.trim() && account.baseUrl?.trim());
312
+ const runtimeSnapshot = buildRuntimeAccountStatusSnapshot({ runtime });
300
313
  return {
301
314
  accountId: account.accountId,
302
315
  name: account.name,
@@ -304,10 +317,10 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
304
317
  configured,
305
318
  secretSource: account.secretSource,
306
319
  baseUrl: account.baseUrl ? "[set]" : "[missing]",
307
- running: runtime?.running ?? false,
308
- lastStartAt: runtime?.lastStartAt ?? null,
309
- lastStopAt: runtime?.lastStopAt ?? null,
310
- lastError: runtime?.lastError ?? null,
320
+ running: runtimeSnapshot.running,
321
+ lastStartAt: runtimeSnapshot.lastStartAt,
322
+ lastStopAt: runtimeSnapshot.lastStopAt,
323
+ lastError: runtimeSnapshot.lastError,
311
324
  mode: "webhook",
312
325
  lastInboundAt: runtime?.lastInboundAt ?? null,
313
326
  lastOutboundAt: runtime?.lastOutboundAt ?? null,
@@ -351,36 +364,20 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
351
364
  cleared = true;
352
365
  changed = true;
353
366
  }
354
- const accounts =
355
- nextSection.accounts && typeof nextSection.accounts === "object"
356
- ? { ...nextSection.accounts }
357
- : undefined;
358
- if (accounts && accountId in accounts) {
359
- const entry = accounts[accountId];
360
- if (entry && typeof entry === "object") {
361
- const nextEntry = { ...entry } as Record<string, unknown>;
362
- if ("botSecret" in nextEntry) {
363
- const secret = nextEntry.botSecret;
364
- if (typeof secret === "string" ? secret.trim() : secret) {
365
- cleared = true;
366
- }
367
- delete nextEntry.botSecret;
368
- changed = true;
369
- }
370
- if (Object.keys(nextEntry).length === 0) {
371
- delete accounts[accountId];
372
- changed = true;
373
- } else {
374
- accounts[accountId] = nextEntry as typeof entry;
375
- }
367
+ const accountCleanup = clearAccountEntryFields({
368
+ accounts: nextSection.accounts,
369
+ accountId,
370
+ fields: ["botSecret"],
371
+ });
372
+ if (accountCleanup.changed) {
373
+ changed = true;
374
+ if (accountCleanup.cleared) {
375
+ cleared = true;
376
376
  }
377
- }
378
- if (accounts) {
379
- if (Object.keys(accounts).length === 0) {
380
- delete nextSection.accounts;
381
- changed = true;
377
+ if (accountCleanup.nextAccounts) {
378
+ nextSection.accounts = accountCleanup.nextAccounts;
382
379
  } else {
383
- nextSection.accounts = accounts;
380
+ delete nextSection.accounts;
384
381
  }
385
382
  }
386
383
  }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { NextcloudTalkConfigSchema } from "./config-schema.js";
3
+
4
+ describe("NextcloudTalkConfigSchema SecretInput", () => {
5
+ it("accepts SecretRef botSecret and apiPassword at top-level", () => {
6
+ const result = NextcloudTalkConfigSchema.safeParse({
7
+ baseUrl: "https://cloud.example.com",
8
+ botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
9
+ apiUser: "bot",
10
+ apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
11
+ });
12
+ expect(result.success).toBe(true);
13
+ });
14
+
15
+ it("accepts SecretRef botSecret and apiPassword on account", () => {
16
+ const result = NextcloudTalkConfigSchema.safeParse({
17
+ accounts: {
18
+ main: {
19
+ baseUrl: "https://cloud.example.com",
20
+ botSecret: {
21
+ source: "env",
22
+ provider: "default",
23
+ id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
24
+ },
25
+ apiUser: "bot",
26
+ apiPassword: {
27
+ source: "env",
28
+ provider: "default",
29
+ id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
30
+ },
31
+ },
32
+ },
33
+ });
34
+ expect(result.success).toBe(true);
35
+ });
36
+ });
@@ -7,8 +7,9 @@ import {
7
7
  ReplyRuntimeConfigSchemaShape,
8
8
  ToolPolicySchema,
9
9
  requireOpenAllowFrom,
10
- } from "openclaw/plugin-sdk";
10
+ } from "openclaw/plugin-sdk/nextcloud-talk";
11
11
  import { z } from "zod";
12
+ import { buildSecretInputSchema } from "./secret-input.js";
12
13
 
13
14
  export const NextcloudTalkRoomSchema = z
14
15
  .object({
@@ -27,10 +28,10 @@ export const NextcloudTalkAccountSchemaBase = z
27
28
  enabled: z.boolean().optional(),
28
29
  markdown: MarkdownConfigSchema,
29
30
  baseUrl: z.string().optional(),
30
- botSecret: z.string().optional(),
31
+ botSecret: buildSecretInputSchema().optional(),
31
32
  botSecretFile: z.string().optional(),
32
33
  apiUser: z.string().optional(),
33
- apiPassword: z.string().optional(),
34
+ apiPassword: buildSecretInputSchema().optional(),
34
35
  apiPasswordFile: z.string().optional(),
35
36
  dmPolicy: DmPolicySchema.optional().default("pairing"),
36
37
  webhookPort: z.number().int().positive().optional(),
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/nextcloud-talk";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
4
4
  import { handleNextcloudTalkInbound } from "./inbound.js";
@@ -45,7 +45,7 @@ describe("nextcloud-talk inbound authz", () => {
45
45
  enabled: true,
46
46
  baseUrl: "",
47
47
  secret: "",
48
- secretSource: "none",
48
+ secretSource: "none", // pragma: allowlist secret
49
49
  config: {
50
50
  dmPolicy: "pairing",
51
51
  allowFrom: [],
package/src/inbound.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  GROUP_POLICY_BLOCKED_LABEL,
3
3
  createScopedPairingAccess,
4
- createNormalizedOutboundDeliverer,
5
- createReplyPrefixOptions,
4
+ dispatchInboundReplyWithBase,
6
5
  formatTextWithAttachmentLinks,
6
+ issuePairingChallenge,
7
7
  logInboundDrop,
8
8
  readStoreAllowFromForDmPolicy,
9
9
  resolveDmGroupAccessWithCommandGate,
@@ -14,7 +14,7 @@ import {
14
14
  type OutboundReplyPayload,
15
15
  type OpenClawConfig,
16
16
  type RuntimeEnv,
17
- } from "openclaw/plugin-sdk";
17
+ } from "openclaw/plugin-sdk/nextcloud-talk";
18
18
  import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
19
19
  import {
20
20
  normalizeNextcloudTalkAllowlist,
@@ -174,26 +174,20 @@ export async function handleNextcloudTalkInbound(params: {
174
174
  } else {
175
175
  if (access.decision !== "allow") {
176
176
  if (access.decision === "pairing") {
177
- const { code, created } = await pairing.upsertPairingRequest({
178
- id: senderId,
177
+ await issuePairingChallenge({
178
+ channel: CHANNEL_ID,
179
+ senderId,
180
+ senderIdLine: `Your Nextcloud user id: ${senderId}`,
179
181
  meta: { name: senderName || undefined },
180
- });
181
- if (created) {
182
- try {
183
- await sendMessageNextcloudTalk(
184
- roomToken,
185
- core.channel.pairing.buildPairingReply({
186
- channel: CHANNEL_ID,
187
- idLine: `Your Nextcloud user id: ${senderId}`,
188
- code,
189
- }),
190
- { accountId: account.accountId },
191
- );
182
+ upsertPairingRequest: pairing.upsertPairingRequest,
183
+ sendPairingReply: async (text) => {
184
+ await sendMessageNextcloudTalk(roomToken, text, { accountId: account.accountId });
192
185
  statusSink?.({ lastOutboundAt: Date.now() });
193
- } catch (err) {
186
+ },
187
+ onReplyError: (err) => {
194
188
  runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
195
- }
196
- }
189
+ },
190
+ });
197
191
  }
198
192
  runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
199
193
  return;
@@ -291,43 +285,30 @@ export async function handleNextcloudTalkInbound(params: {
291
285
  CommandAuthorized: commandAuthorized,
292
286
  });
293
287
 
294
- await core.channel.session.recordInboundSession({
288
+ await dispatchInboundReplyWithBase({
289
+ cfg: config as OpenClawConfig,
290
+ channel: CHANNEL_ID,
291
+ accountId: account.accountId,
292
+ route,
295
293
  storePath,
296
- sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
297
- ctx: ctxPayload,
294
+ ctxPayload,
295
+ core,
296
+ deliver: async (payload) => {
297
+ await deliverNextcloudTalkReply({
298
+ payload,
299
+ roomToken,
300
+ accountId: account.accountId,
301
+ statusSink,
302
+ });
303
+ },
298
304
  onRecordError: (err) => {
299
305
  runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
300
306
  },
301
- });
302
-
303
- const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
304
- cfg: config as OpenClawConfig,
305
- agentId: route.agentId,
306
- channel: CHANNEL_ID,
307
- accountId: account.accountId,
308
- });
309
- const deliverReply = createNormalizedOutboundDeliverer(async (payload) => {
310
- await deliverNextcloudTalkReply({
311
- payload,
312
- roomToken,
313
- accountId: account.accountId,
314
- statusSink,
315
- });
316
- });
317
-
318
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
319
- ctx: ctxPayload,
320
- cfg: config as OpenClawConfig,
321
- dispatcherOptions: {
322
- ...prefixOptions,
323
- deliver: deliverReply,
324
- onError: (err, info) => {
325
- runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
326
- },
307
+ onDispatchError: (err, info) => {
308
+ runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
327
309
  },
328
310
  replyOptions: {
329
311
  skillFilter: roomConfig?.skills,
330
- onModelSelected,
331
312
  disableBlockStreaming:
332
313
  typeof account.config.blockStreaming === "boolean"
333
314
  ? !account.config.blockStreaming