@openclaw/nextcloud-talk 2026.3.13 → 2026.5.1-beta.2

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.
Files changed (59) hide show
  1. package/api.ts +1 -0
  2. package/channel-plugin-api.ts +1 -0
  3. package/contract-api.ts +4 -0
  4. package/doctor-contract-api.ts +1 -0
  5. package/index.ts +15 -12
  6. package/openclaw.plugin.json +804 -1
  7. package/package.json +31 -4
  8. package/runtime-api.ts +33 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/setup-entry.ts +13 -0
  11. package/src/accounts.ts +25 -42
  12. package/src/approval-auth.test.ts +17 -0
  13. package/src/approval-auth.ts +27 -0
  14. package/src/channel-api.ts +5 -0
  15. package/src/channel.adapters.ts +52 -0
  16. package/src/channel.core.test.ts +75 -0
  17. package/src/{channel.startup.test.ts → channel.lifecycle.test.ts} +29 -27
  18. package/src/channel.ts +157 -385
  19. package/src/config-schema.ts +24 -18
  20. package/src/core.test.ts +397 -0
  21. package/src/doctor-contract.ts +9 -0
  22. package/src/doctor.test.ts +40 -0
  23. package/src/doctor.ts +10 -0
  24. package/src/gateway.ts +109 -0
  25. package/src/inbound.authz.test.ts +87 -22
  26. package/src/inbound.behavior.test.ts +202 -0
  27. package/src/inbound.ts +28 -26
  28. package/src/monitor-runtime.ts +138 -0
  29. package/src/monitor.replay.test.ts +238 -0
  30. package/src/monitor.test-harness.ts +2 -2
  31. package/src/monitor.ts +125 -153
  32. package/src/policy.ts +23 -31
  33. package/src/replay-guard.ts +74 -11
  34. package/src/room-info.test.ts +116 -0
  35. package/src/room-info.ts +11 -3
  36. package/src/runtime.ts +6 -3
  37. package/src/secret-contract.ts +103 -0
  38. package/src/secret-input.ts +1 -10
  39. package/src/send.cfg-threading.test.ts +153 -0
  40. package/src/send.runtime.ts +8 -0
  41. package/src/send.ts +109 -77
  42. package/src/session-route.ts +40 -0
  43. package/src/setup-core.ts +248 -0
  44. package/src/setup-surface.ts +190 -0
  45. package/src/setup.test.ts +422 -0
  46. package/src/signature.ts +20 -10
  47. package/src/types.ts +19 -16
  48. package/tsconfig.json +16 -0
  49. package/src/accounts.test.ts +0 -30
  50. package/src/config-schema.test.ts +0 -36
  51. package/src/format.ts +0 -79
  52. package/src/monitor.auth-order.test.ts +0 -28
  53. package/src/monitor.backend.test.ts +0 -27
  54. package/src/monitor.read-body.test.ts +0 -16
  55. package/src/normalize.test.ts +0 -28
  56. package/src/onboarding.ts +0 -302
  57. package/src/policy.test.ts +0 -138
  58. package/src/replay-guard.test.ts +0 -70
  59. package/src/send.test.ts +0 -98
package/src/channel.ts CHANGED
@@ -1,42 +1,34 @@
1
+ import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
2
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
3
+ import { createLoggedPairingApprovalNotifier } from "openclaw/plugin-sdk/channel-pairing";
4
+ import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy";
1
5
  import {
2
- buildAccountScopedDmSecurityPolicy,
3
- collectAllowlistProviderGroupPolicyWarnings,
4
- collectOpenGroupPolicyRouteAllowlistWarnings,
5
- createAccountStatusSink,
6
- formatAllowFromLowercase,
7
- mapAllowFromEntries,
8
- } from "openclaw/plugin-sdk/compat";
6
+ buildWebhookChannelStatusSummary,
7
+ createComputedAccountStatusAdapter,
8
+ createDefaultChannelRuntimeState,
9
+ } from "openclaw/plugin-sdk/status-helpers";
10
+ import { resolveNextcloudTalkAccount, type ResolvedNextcloudTalkAccount } from "./accounts.js";
11
+ import { nextcloudTalkApprovalAuth } from "./approval-auth.js";
12
+ import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, type ChannelPlugin } from "./channel-api.js";
9
13
  import {
10
- applyAccountNameToChannelSection,
11
- buildBaseChannelStatusSummary,
12
- buildChannelConfigSchema,
13
- buildRuntimeAccountStatusSnapshot,
14
- clearAccountEntryFields,
15
- DEFAULT_ACCOUNT_ID,
16
- deleteAccountFromConfigSection,
17
- normalizeAccountId,
18
- setAccountEnabledInConfigSection,
19
- type ChannelPlugin,
20
- type OpenClawConfig,
21
- type ChannelSetupInput,
22
- } from "openclaw/plugin-sdk/nextcloud-talk";
23
- import { runStoppablePassiveMonitor } from "../../shared/passive-monitor.js";
24
- import {
25
- listNextcloudTalkAccountIds,
26
- resolveDefaultNextcloudTalkAccountId,
27
- resolveNextcloudTalkAccount,
28
- type ResolvedNextcloudTalkAccount,
29
- } from "./accounts.js";
14
+ nextcloudTalkConfigAdapter,
15
+ nextcloudTalkPairingTextAdapter,
16
+ nextcloudTalkSecurityAdapter,
17
+ } from "./channel.adapters.js";
30
18
  import { NextcloudTalkConfigSchema } from "./config-schema.js";
31
- import { monitorNextcloudTalkProvider } from "./monitor.js";
19
+ import { nextcloudTalkDoctor } from "./doctor.js";
20
+ import { nextcloudTalkGatewayAdapter } from "./gateway.js";
32
21
  import {
33
22
  looksLikeNextcloudTalkTargetId,
34
23
  normalizeNextcloudTalkMessagingTarget,
35
24
  } from "./normalize.js";
36
- import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
37
25
  import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
38
26
  import { getNextcloudTalkRuntime } from "./runtime.js";
27
+ import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
39
28
  import { sendMessageNextcloudTalk } from "./send.js";
29
+ import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
30
+ import { nextcloudTalkSetupAdapter } from "./setup-core.js";
31
+ import { nextcloudTalkSetupWizard } from "./setup-surface.js";
40
32
  import type { CoreConfig } from "./types.js";
41
33
 
42
34
  const meta = {
@@ -51,372 +43,152 @@ const meta = {
51
43
  quickstartAllowFrom: true,
52
44
  };
53
45
 
54
- type NextcloudSetupInput = ChannelSetupInput & {
55
- baseUrl?: string;
56
- secret?: string;
57
- secretFile?: string;
58
- useEnv?: boolean;
59
- };
60
-
61
- export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> = {
62
- id: "nextcloud-talk",
63
- meta,
64
- onboarding: nextcloudTalkOnboardingAdapter,
65
- pairing: {
66
- idLabel: "nextcloudUserId",
67
- normalizeAllowEntry: (entry) =>
68
- entry.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
69
- notifyApproval: async ({ id }) => {
70
- console.log(`[nextcloud-talk] User ${id} approved for pairing`);
46
+ const collectNextcloudTalkSecurityWarnings =
47
+ createAllowlistProviderRouteAllowlistWarningCollector<ResolvedNextcloudTalkAccount>({
48
+ providerConfigPresent: (cfg) =>
49
+ (cfg.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] !== undefined,
50
+ resolveGroupPolicy: (account) => account.config.groupPolicy,
51
+ resolveRouteAllowlistConfigured: (account) =>
52
+ Boolean(account.config.rooms) && Object.keys(account.config.rooms ?? {}).length > 0,
53
+ restrictSenders: {
54
+ surface: "Nextcloud Talk rooms",
55
+ openScope: "any member in allowed rooms",
56
+ groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
57
+ groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
71
58
  },
72
- },
73
- capabilities: {
74
- chatTypes: ["direct", "group"],
75
- reactions: true,
76
- threads: false,
77
- media: true,
78
- nativeCommands: false,
79
- blockStreaming: true,
80
- },
81
- reload: { configPrefixes: ["channels.nextcloud-talk"] },
82
- configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
83
- config: {
84
- listAccountIds: (cfg) => listNextcloudTalkAccountIds(cfg as CoreConfig),
85
- resolveAccount: (cfg, accountId) =>
86
- resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }),
87
- defaultAccountId: (cfg) => resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig),
88
- setAccountEnabled: ({ cfg, accountId, enabled }) =>
89
- setAccountEnabledInConfigSection({
90
- cfg,
91
- sectionKey: "nextcloud-talk",
92
- accountId,
93
- enabled,
94
- allowTopLevel: true,
95
- }),
96
- deleteAccount: ({ cfg, accountId }) =>
97
- deleteAccountFromConfigSection({
98
- cfg,
99
- sectionKey: "nextcloud-talk",
100
- accountId,
101
- clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"],
102
- }),
103
- isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
104
- describeAccount: (account) => ({
105
- accountId: account.accountId,
106
- name: account.name,
107
- enabled: account.enabled,
108
- configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
109
- secretSource: account.secretSource,
110
- baseUrl: account.baseUrl ? "[set]" : "[missing]",
111
- }),
112
- resolveAllowFrom: ({ cfg, accountId }) =>
113
- mapAllowFromEntries(
114
- resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom,
115
- ).map((entry) => entry.toLowerCase()),
116
- formatAllowFrom: ({ allowFrom }) =>
117
- formatAllowFromLowercase({
118
- allowFrom,
119
- stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
120
- }),
121
- },
122
- security: {
123
- resolveDmPolicy: ({ cfg, accountId, account }) => {
124
- return buildAccountScopedDmSecurityPolicy({
125
- cfg,
126
- channelKey: "nextcloud-talk",
127
- accountId,
128
- fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
129
- policy: account.config.dmPolicy,
130
- allowFrom: account.config.allowFrom ?? [],
131
- policyPathSuffix: "dmPolicy",
132
- normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
133
- });
59
+ noRouteAllowlist: {
60
+ surface: "Nextcloud Talk rooms",
61
+ routeAllowlistPath: "channels.nextcloud-talk.rooms",
62
+ routeScope: "room",
63
+ groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
64
+ groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
134
65
  },
135
- collectWarnings: ({ account, cfg }) => {
136
- const roomAllowlistConfigured =
137
- account.config.rooms && Object.keys(account.config.rooms).length > 0;
138
- return collectAllowlistProviderGroupPolicyWarnings({
139
- cfg,
140
- providerConfigPresent:
141
- (cfg.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] !== undefined,
142
- configuredGroupPolicy: account.config.groupPolicy,
143
- collect: (groupPolicy) =>
144
- collectOpenGroupPolicyRouteAllowlistWarnings({
145
- groupPolicy,
146
- routeAllowlistConfigured: Boolean(roomAllowlistConfigured),
147
- restrictSenders: {
148
- surface: "Nextcloud Talk rooms",
149
- openScope: "any member in allowed rooms",
150
- groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
151
- groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
152
- },
153
- noRouteAllowlist: {
154
- surface: "Nextcloud Talk rooms",
155
- routeAllowlistPath: "channels.nextcloud-talk.rooms",
156
- routeScope: "room",
157
- groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
158
- groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
66
+ });
67
+
68
+ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
69
+ createChatChannelPlugin({
70
+ base: {
71
+ id: "nextcloud-talk",
72
+ meta,
73
+ setupWizard: nextcloudTalkSetupWizard,
74
+ capabilities: {
75
+ chatTypes: ["direct", "group"],
76
+ reactions: true,
77
+ threads: false,
78
+ media: true,
79
+ nativeCommands: false,
80
+ blockStreaming: true,
81
+ },
82
+ reload: { configPrefixes: ["channels.nextcloud-talk"] },
83
+ configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
84
+ config: {
85
+ ...nextcloudTalkConfigAdapter,
86
+ isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
87
+ describeAccount: (account) =>
88
+ describeWebhookAccountSnapshot({
89
+ account,
90
+ configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
91
+ extra: {
92
+ secretSource: account.secretSource,
93
+ baseUrl: account.baseUrl ? "[set]" : "[missing]",
159
94
  },
160
95
  }),
161
- });
162
- },
163
- },
164
- groups: {
165
- resolveRequireMention: ({ cfg, accountId, groupId }) => {
166
- const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
167
- const rooms = account.config.rooms;
168
- if (!rooms || !groupId) {
169
- return true;
170
- }
96
+ },
97
+ approvalCapability: nextcloudTalkApprovalAuth,
98
+ doctor: nextcloudTalkDoctor,
99
+ groups: {
100
+ resolveRequireMention: ({ cfg, accountId, groupId }) => {
101
+ const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
102
+ const rooms = account.config.rooms;
103
+ if (!rooms || !groupId) {
104
+ return true;
105
+ }
171
106
 
172
- const roomConfig = rooms[groupId];
173
- if (roomConfig?.requireMention !== undefined) {
174
- return roomConfig.requireMention;
175
- }
107
+ const roomConfig = rooms[groupId];
108
+ if (roomConfig?.requireMention !== undefined) {
109
+ return roomConfig.requireMention;
110
+ }
176
111
 
177
- const wildcardConfig = rooms["*"];
178
- if (wildcardConfig?.requireMention !== undefined) {
179
- return wildcardConfig.requireMention;
180
- }
112
+ const wildcardConfig = rooms["*"];
113
+ if (wildcardConfig?.requireMention !== undefined) {
114
+ return wildcardConfig.requireMention;
115
+ }
181
116
 
182
- return true;
183
- },
184
- resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
185
- },
186
- messaging: {
187
- normalizeTarget: normalizeNextcloudTalkMessagingTarget,
188
- targetResolver: {
189
- looksLikeId: looksLikeNextcloudTalkTargetId,
190
- hint: "<roomToken>",
191
- },
192
- },
193
- setup: {
194
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
195
- applyAccountName: ({ cfg, accountId, name }) =>
196
- applyAccountNameToChannelSection({
197
- cfg: cfg,
198
- channelKey: "nextcloud-talk",
199
- accountId,
200
- name,
201
- }),
202
- validateInput: ({ accountId, input }) => {
203
- const setupInput = input as NextcloudSetupInput;
204
- if (setupInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
205
- return "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.";
206
- }
207
- if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) {
208
- return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
209
- }
210
- if (!setupInput.baseUrl) {
211
- return "Nextcloud Talk requires --base-url.";
212
- }
213
- return null;
214
- },
215
- applyAccountConfig: ({ cfg, accountId, input }) => {
216
- const setupInput = input as NextcloudSetupInput;
217
- const namedConfig = applyAccountNameToChannelSection({
218
- cfg: cfg,
219
- channelKey: "nextcloud-talk",
220
- accountId,
221
- name: setupInput.name,
222
- });
223
- if (accountId === DEFAULT_ACCOUNT_ID) {
224
- return {
225
- ...namedConfig,
226
- channels: {
227
- ...namedConfig.channels,
228
- "nextcloud-talk": {
229
- ...namedConfig.channels?.["nextcloud-talk"],
230
- enabled: true,
231
- baseUrl: setupInput.baseUrl,
232
- ...(setupInput.useEnv
233
- ? {}
234
- : setupInput.secretFile
235
- ? { botSecretFile: setupInput.secretFile }
236
- : setupInput.secret
237
- ? { botSecret: setupInput.secret }
238
- : {}),
239
- },
240
- },
241
- } as OpenClawConfig;
242
- }
243
- return {
244
- ...namedConfig,
245
- channels: {
246
- ...namedConfig.channels,
247
- "nextcloud-talk": {
248
- ...namedConfig.channels?.["nextcloud-talk"],
249
- enabled: true,
250
- accounts: {
251
- ...namedConfig.channels?.["nextcloud-talk"]?.accounts,
252
- [accountId]: {
253
- ...namedConfig.channels?.["nextcloud-talk"]?.accounts?.[accountId],
254
- enabled: true,
255
- baseUrl: setupInput.baseUrl,
256
- ...(setupInput.secretFile
257
- ? { botSecretFile: setupInput.secretFile }
258
- : setupInput.secret
259
- ? { botSecret: setupInput.secret }
260
- : {}),
261
- },
262
- },
263
- },
117
+ return true;
264
118
  },
265
- } as OpenClawConfig;
266
- },
267
- },
268
- outbound: {
269
- deliveryMode: "direct",
270
- chunker: (text, limit) => getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit),
271
- chunkerMode: "markdown",
272
- textChunkLimit: 4000,
273
- sendText: async ({ cfg, to, text, accountId, replyToId }) => {
274
- const result = await sendMessageNextcloudTalk(to, text, {
275
- accountId: accountId ?? undefined,
276
- replyTo: replyToId ?? undefined,
277
- cfg: cfg as CoreConfig,
278
- });
279
- return { channel: "nextcloud-talk", ...result };
280
- },
281
- sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => {
282
- const messageWithMedia = mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text;
283
- const result = await sendMessageNextcloudTalk(to, messageWithMedia, {
284
- accountId: accountId ?? undefined,
285
- replyTo: replyToId ?? undefined,
286
- cfg: cfg as CoreConfig,
287
- });
288
- return { channel: "nextcloud-talk", ...result };
289
- },
290
- },
291
- status: {
292
- defaultRuntime: {
293
- accountId: DEFAULT_ACCOUNT_ID,
294
- running: false,
295
- lastStartAt: null,
296
- lastStopAt: null,
297
- lastError: null,
119
+ resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
120
+ },
121
+ messaging: {
122
+ normalizeTarget: normalizeNextcloudTalkMessagingTarget,
123
+ resolveOutboundSessionRoute: (params) => resolveNextcloudTalkOutboundSessionRoute(params),
124
+ targetResolver: {
125
+ looksLikeId: looksLikeNextcloudTalkTargetId,
126
+ hint: "<roomToken>",
127
+ },
128
+ },
129
+ secrets: {
130
+ secretTargetRegistryEntries,
131
+ collectRuntimeConfigAssignments,
132
+ },
133
+ setup: nextcloudTalkSetupAdapter,
134
+ status: createComputedAccountStatusAdapter<ResolvedNextcloudTalkAccount>({
135
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
136
+ buildChannelSummary: ({ snapshot }) =>
137
+ buildWebhookChannelStatusSummary(snapshot, {
138
+ secretSource: snapshot.secretSource ?? "none",
139
+ }),
140
+ resolveAccountSnapshot: ({ account }) => ({
141
+ accountId: account.accountId,
142
+ name: account.name,
143
+ enabled: account.enabled,
144
+ configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
145
+ extra: {
146
+ secretSource: account.secretSource,
147
+ baseUrl: account.baseUrl ? "[set]" : "[missing]",
148
+ mode: "webhook",
149
+ },
150
+ }),
151
+ }),
152
+ gateway: nextcloudTalkGatewayAdapter,
298
153
  },
299
- buildChannelSummary: ({ snapshot }) => {
300
- const base = buildBaseChannelStatusSummary(snapshot);
301
- return {
302
- configured: base.configured,
303
- secretSource: snapshot.secretSource ?? "none",
304
- running: base.running,
305
- mode: "webhook",
306
- lastStartAt: base.lastStartAt,
307
- lastStopAt: base.lastStopAt,
308
- lastError: base.lastError,
309
- };
154
+ pairing: {
155
+ text: {
156
+ ...nextcloudTalkPairingTextAdapter,
157
+ notify: createLoggedPairingApprovalNotifier(
158
+ ({ id }) => `[nextcloud-talk] User ${id} approved for pairing`,
159
+ ),
160
+ },
310
161
  },
311
- buildAccountSnapshot: ({ account, runtime }) => {
312
- const configured = Boolean(account.secret?.trim() && account.baseUrl?.trim());
313
- const runtimeSnapshot = buildRuntimeAccountStatusSnapshot({ runtime });
314
- return {
315
- accountId: account.accountId,
316
- name: account.name,
317
- enabled: account.enabled,
318
- configured,
319
- secretSource: account.secretSource,
320
- baseUrl: account.baseUrl ? "[set]" : "[missing]",
321
- running: runtimeSnapshot.running,
322
- lastStartAt: runtimeSnapshot.lastStartAt,
323
- lastStopAt: runtimeSnapshot.lastStopAt,
324
- lastError: runtimeSnapshot.lastError,
325
- mode: "webhook",
326
- lastInboundAt: runtime?.lastInboundAt ?? null,
327
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
328
- };
162
+ security: {
163
+ ...nextcloudTalkSecurityAdapter,
164
+ collectWarnings: collectNextcloudTalkSecurityWarnings,
329
165
  },
330
- },
331
- gateway: {
332
- startAccount: async (ctx) => {
333
- const account = ctx.account;
334
- if (!account.secret || !account.baseUrl) {
335
- throw new Error(
336
- `Nextcloud Talk not configured for account "${account.accountId}" (missing secret or baseUrl)`,
337
- );
338
- }
339
-
340
- ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook server`);
341
-
342
- const statusSink = createAccountStatusSink({
343
- accountId: ctx.accountId,
344
- setStatus: ctx.setStatus,
345
- });
346
-
347
- await runStoppablePassiveMonitor({
348
- abortSignal: ctx.abortSignal,
349
- start: async () =>
350
- await monitorNextcloudTalkProvider({
351
- accountId: account.accountId,
352
- config: ctx.cfg as CoreConfig,
353
- runtime: ctx.runtime,
354
- abortSignal: ctx.abortSignal,
355
- statusSink,
166
+ outbound: {
167
+ base: {
168
+ deliveryMode: "direct",
169
+ chunker: (text, limit) =>
170
+ getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit),
171
+ chunkerMode: "markdown",
172
+ textChunkLimit: 4000,
173
+ },
174
+ attachedResults: {
175
+ channel: "nextcloud-talk",
176
+ sendText: async ({ cfg, to, text, accountId, replyToId }) =>
177
+ await sendMessageNextcloudTalk(to, text, {
178
+ accountId: accountId ?? undefined,
179
+ replyTo: replyToId ?? undefined,
180
+ cfg: cfg as CoreConfig,
356
181
  }),
357
- });
358
- },
359
- logoutAccount: async ({ accountId, cfg }) => {
360
- const nextCfg = { ...cfg } as OpenClawConfig;
361
- const nextSection = cfg.channels?.["nextcloud-talk"]
362
- ? { ...cfg.channels["nextcloud-talk"] }
363
- : undefined;
364
- let cleared = false;
365
- let changed = false;
366
-
367
- if (nextSection) {
368
- if (accountId === DEFAULT_ACCOUNT_ID && nextSection.botSecret) {
369
- delete nextSection.botSecret;
370
- cleared = true;
371
- changed = true;
372
- }
373
- const accountCleanup = clearAccountEntryFields({
374
- accounts: nextSection.accounts,
375
- accountId,
376
- fields: ["botSecret"],
377
- });
378
- if (accountCleanup.changed) {
379
- changed = true;
380
- if (accountCleanup.cleared) {
381
- cleared = true;
382
- }
383
- if (accountCleanup.nextAccounts) {
384
- nextSection.accounts = accountCleanup.nextAccounts;
385
- } else {
386
- delete nextSection.accounts;
387
- }
388
- }
389
- }
390
-
391
- if (changed) {
392
- if (nextSection && Object.keys(nextSection).length > 0) {
393
- nextCfg.channels = { ...nextCfg.channels, "nextcloud-talk": nextSection };
394
- } else {
395
- const nextChannels = { ...nextCfg.channels } as Record<string, unknown>;
396
- delete nextChannels["nextcloud-talk"];
397
- if (Object.keys(nextChannels).length > 0) {
398
- nextCfg.channels = nextChannels as OpenClawConfig["channels"];
399
- } else {
400
- delete nextCfg.channels;
401
- }
402
- }
403
- }
404
-
405
- const resolved = resolveNextcloudTalkAccount({
406
- cfg: changed ? (nextCfg as CoreConfig) : (cfg as CoreConfig),
407
- accountId,
408
- });
409
- const loggedOut = resolved.secretSource === "none";
410
-
411
- if (changed) {
412
- await getNextcloudTalkRuntime().config.writeConfigFile(nextCfg);
413
- }
414
-
415
- return {
416
- cleared,
417
- envSecret: Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()),
418
- loggedOut,
419
- };
182
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) =>
183
+ await sendMessageNextcloudTalk(
184
+ to,
185
+ mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text,
186
+ {
187
+ accountId: accountId ?? undefined,
188
+ replyTo: replyToId ?? undefined,
189
+ cfg: cfg as CoreConfig,
190
+ },
191
+ ),
192
+ },
420
193
  },
421
- },
422
- };
194
+ });
@@ -1,18 +1,16 @@
1
1
  import {
2
- BlockStreamingCoalesceSchema,
3
- DmConfigSchema,
4
2
  DmPolicySchema,
5
3
  GroupPolicySchema,
6
4
  MarkdownConfigSchema,
7
5
  ReplyRuntimeConfigSchemaShape,
8
6
  ToolPolicySchema,
9
7
  requireOpenAllowFrom,
10
- } from "openclaw/plugin-sdk/nextcloud-talk";
11
- import { z } from "zod";
12
- import { requireChannelOpenAllowFrom } from "../../shared/config-schema-helpers.js";
8
+ } from "openclaw/plugin-sdk/channel-config-schema";
9
+ import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
10
+ import { z } from "openclaw/plugin-sdk/zod";
13
11
  import { buildSecretInputSchema } from "./secret-input.js";
14
12
 
15
- export const NextcloudTalkRoomSchema = z
13
+ const NextcloudTalkRoomSchema = z
16
14
  .object({
17
15
  requireMention: z.boolean().optional(),
18
16
  tools: ToolPolicySchema,
@@ -23,7 +21,15 @@ export const NextcloudTalkRoomSchema = z
23
21
  })
24
22
  .strict();
25
23
 
26
- export const NextcloudTalkAccountSchemaBase = z
24
+ const NextcloudTalkNetworkSchema = z
25
+ .object({
26
+ /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
27
+ dangerouslyAllowPrivateNetwork: z.boolean().optional(),
28
+ })
29
+ .strict()
30
+ .optional();
31
+
32
+ const NextcloudTalkAccountSchemaBase = z
27
33
  .object({
28
34
  name: z.string().optional(),
29
35
  enabled: z.boolean().optional(),
@@ -43,21 +49,21 @@ export const NextcloudTalkAccountSchemaBase = z
43
49
  groupAllowFrom: z.array(z.string()).optional(),
44
50
  groupPolicy: GroupPolicySchema.optional().default("allowlist"),
45
51
  rooms: z.record(z.string(), NextcloudTalkRoomSchema.optional()).optional(),
52
+ /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
53
+ network: NextcloudTalkNetworkSchema,
46
54
  ...ReplyRuntimeConfigSchemaShape,
47
55
  })
48
56
  .strict();
49
57
 
50
- export const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine(
51
- (value, ctx) => {
52
- requireChannelOpenAllowFrom({
53
- channel: "nextcloud-talk",
54
- policy: value.dmPolicy,
55
- allowFrom: value.allowFrom,
56
- ctx,
57
- requireOpenAllowFrom,
58
- });
59
- },
60
- );
58
+ const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine((value, ctx) => {
59
+ requireChannelOpenAllowFrom({
60
+ channel: "nextcloud-talk",
61
+ policy: value.dmPolicy,
62
+ allowFrom: value.allowFrom,
63
+ ctx,
64
+ requireOpenAllowFrom,
65
+ });
66
+ });
61
67
 
62
68
  export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
63
69
  accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),