@openclaw/nextcloud-talk 2026.3.2 → 2026.3.8-beta.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/index.ts +2 -2
- package/package.json +4 -1
- package/src/accounts.ts +12 -34
- package/src/channel.startup.test.ts +3 -3
- package/src/channel.ts +83 -86
- package/src/config-schema.ts +1 -1
- package/src/inbound.authz.test.ts +2 -2
- package/src/inbound.ts +31 -50
- package/src/monitor.test-fixtures.ts +1 -1
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +79 -140
- package/src/policy.test.ts +106 -1
- package/src/policy.ts +27 -19
- package/src/replay-guard.ts +1 -1
- package/src/room-info.ts +2 -2
- package/src/runtime.ts +5 -13
- package/src/secret-input.ts +8 -14
- package/src/send.test.ts +104 -0
- package/src/send.ts +3 -2
- package/src/types.ts +1 -1
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.
|
|
3
|
+
"version": "2026.3.8-beta.1",
|
|
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,13 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
resolveAccountWithDefaultFallback,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import {
|
|
3
|
+
createAccountListHelpers,
|
|
7
4
|
DEFAULT_ACCOUNT_ID,
|
|
8
5
|
normalizeAccountId,
|
|
9
|
-
|
|
10
|
-
} from "openclaw/plugin-sdk/
|
|
6
|
+
resolveAccountWithDefaultFallback,
|
|
7
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
11
8
|
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
12
9
|
import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
|
|
13
10
|
|
|
@@ -32,37 +29,18 @@ export type ResolvedNextcloudTalkAccount = {
|
|
|
32
29
|
config: NextcloudTalkAccountConfig;
|
|
33
30
|
};
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
32
|
+
const {
|
|
33
|
+
listAccountIds: listNextcloudTalkAccountIdsInternal,
|
|
34
|
+
resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId,
|
|
35
|
+
} = createAccountListHelpers("nextcloud-talk", {
|
|
36
|
+
normalizeAccountId,
|
|
37
|
+
});
|
|
38
|
+
export { resolveDefaultNextcloudTalkAccountId };
|
|
41
39
|
|
|
42
40
|
export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
|
|
43
|
-
const ids =
|
|
41
|
+
const ids = listNextcloudTalkAccountIdsInternal(cfg);
|
|
44
42
|
debugAccounts("listNextcloudTalkAccountIds", ids);
|
|
45
|
-
|
|
46
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
47
|
-
}
|
|
48
|
-
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function resolveDefaultNextcloudTalkAccountId(cfg: CoreConfig): string {
|
|
52
|
-
const preferred = normalizeOptionalAccountId(cfg.channels?.["nextcloud-talk"]?.defaultAccount);
|
|
53
|
-
if (
|
|
54
|
-
preferred &&
|
|
55
|
-
listNextcloudTalkAccountIds(cfg).some(
|
|
56
|
-
(accountId) => normalizeAccountId(accountId) === preferred,
|
|
57
|
-
)
|
|
58
|
-
) {
|
|
59
|
-
return preferred;
|
|
60
|
-
}
|
|
61
|
-
const ids = listNextcloudTalkAccountIds(cfg);
|
|
62
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
63
|
-
return DEFAULT_ACCOUNT_ID;
|
|
64
|
-
}
|
|
65
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
43
|
+
return ids;
|
|
66
44
|
}
|
|
67
45
|
|
|
68
46
|
function resolveAccountConfig(
|
|
@@ -21,11 +21,11 @@ function buildAccount(): ResolvedNextcloudTalkAccount {
|
|
|
21
21
|
accountId: "default",
|
|
22
22
|
enabled: true,
|
|
23
23
|
baseUrl: "https://nextcloud.example.com",
|
|
24
|
-
secret: "secret",
|
|
25
|
-
secretSource: "config",
|
|
24
|
+
secret: "secret", // pragma: allowlist secret
|
|
25
|
+
secretSource: "config", // pragma: allowlist secret
|
|
26
26
|
config: {
|
|
27
27
|
baseUrl: "https://nextcloud.example.com",
|
|
28
|
-
botSecret: "secret",
|
|
28
|
+
botSecret: "secret", // pragma: allowlist secret
|
|
29
29
|
webhookPath: "/nextcloud-talk-webhook",
|
|
30
30
|
webhookPort: 8788,
|
|
31
31
|
},
|
package/src/channel.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
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,
|
|
18
|
+
waitForAbortSignal,
|
|
11
19
|
type ChannelPlugin,
|
|
12
20
|
type OpenClawConfig,
|
|
13
21
|
type ChannelSetupInput,
|
|
14
|
-
} from "openclaw/plugin-sdk";
|
|
15
|
-
import { waitForAbortSignal } from "../../../src/infra/abort-signal.js";
|
|
22
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
16
23
|
import {
|
|
17
24
|
listNextcloudTalkAccountIds,
|
|
18
25
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -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) =>
|
|
112
|
+
mapAllowFromEntries(
|
|
113
|
+
resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom,
|
|
114
|
+
).map((entry) => entry.toLowerCase()),
|
|
108
115
|
formatAllowFrom: ({ allowFrom }) =>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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:
|
|
308
|
-
lastStartAt:
|
|
309
|
-
lastStopAt:
|
|
310
|
-
lastError:
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
|
380
|
+
delete nextSection.accounts;
|
|
384
381
|
}
|
|
385
382
|
}
|
|
386
383
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
177
|
+
await issuePairingChallenge({
|
|
178
|
+
channel: CHANNEL_ID,
|
|
179
|
+
senderId,
|
|
180
|
+
senderIdLine: `Your Nextcloud user id: ${senderId}`,
|
|
179
181
|
meta: { name: senderName || undefined },
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
}
|
|
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
|
|
288
|
+
await dispatchInboundReplyWithBase({
|
|
289
|
+
cfg: config as OpenClawConfig,
|
|
290
|
+
channel: CHANNEL_ID,
|
|
291
|
+
accountId: account.accountId,
|
|
292
|
+
route,
|
|
295
293
|
storePath,
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
@@ -16,7 +16,7 @@ export function createSignedCreateMessageRequest(params?: { backend?: string })
|
|
|
16
16
|
const body = JSON.stringify(payload);
|
|
17
17
|
const { random, signature } = generateNextcloudTalkSignature({
|
|
18
18
|
body,
|
|
19
|
-
secret: "nextcloud-secret",
|
|
19
|
+
secret: "nextcloud-secret", // pragma: allowlist secret
|
|
20
20
|
});
|
|
21
21
|
return {
|
|
22
22
|
body,
|
package/src/monitor.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isRequestBodyLimitError,
|
|
7
7
|
readRequestBodyWithLimit,
|
|
8
8
|
requestBodyErrorToText,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
9
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
10
10
|
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
11
11
|
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
12
12
|
import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
|
package/src/onboarding.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
buildSingleChannelSecretPromptState,
|
|
3
3
|
formatDocsLink,
|
|
4
4
|
hasConfiguredSecretInput,
|
|
5
|
+
mapAllowFromEntries,
|
|
5
6
|
mergeAllowFromEntries,
|
|
6
7
|
promptSingleChannelSecretInput,
|
|
7
|
-
|
|
8
|
+
resolveAccountIdForConfigure,
|
|
8
9
|
DEFAULT_ACCOUNT_ID,
|
|
9
10
|
normalizeAccountId,
|
|
11
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
10
12
|
type SecretInput,
|
|
11
13
|
type ChannelOnboardingAdapter,
|
|
12
14
|
type ChannelOnboardingDmPolicy,
|
|
13
15
|
type OpenClawConfig,
|
|
14
16
|
type WizardPrompter,
|
|
15
|
-
} from "openclaw/plugin-sdk";
|
|
17
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
16
18
|
import {
|
|
17
19
|
listNextcloudTalkAccountIds,
|
|
18
20
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -23,24 +25,52 @@ import type { CoreConfig, DmPolicy } from "./types.js";
|
|
|
23
25
|
const channel = "nextcloud-talk" as const;
|
|
24
26
|
|
|
25
27
|
function setNextcloudTalkDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
dmPolicy === "open" ? (addWildcardAllowFrom(existingAllowFrom) as string[]) : existingAllowFrom;
|
|
30
|
-
|
|
31
|
-
const newNextcloudTalkConfig = {
|
|
32
|
-
...existingConfig,
|
|
28
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
29
|
+
cfg,
|
|
30
|
+
channel: "nextcloud-talk",
|
|
33
31
|
dmPolicy,
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
getAllowFrom: (inputCfg) =>
|
|
33
|
+
mapAllowFromEntries(inputCfg.channels?.["nextcloud-talk"]?.allowFrom),
|
|
34
|
+
}) as CoreConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setNextcloudTalkAccountConfig(
|
|
38
|
+
cfg: CoreConfig,
|
|
39
|
+
accountId: string,
|
|
40
|
+
updates: Record<string, unknown>,
|
|
41
|
+
): CoreConfig {
|
|
42
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
43
|
+
return {
|
|
44
|
+
...cfg,
|
|
45
|
+
channels: {
|
|
46
|
+
...cfg.channels,
|
|
47
|
+
"nextcloud-talk": {
|
|
48
|
+
...cfg.channels?.["nextcloud-talk"],
|
|
49
|
+
enabled: true,
|
|
50
|
+
...updates,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
36
55
|
|
|
37
56
|
return {
|
|
38
57
|
...cfg,
|
|
39
58
|
channels: {
|
|
40
59
|
...cfg.channels,
|
|
41
|
-
"nextcloud-talk":
|
|
60
|
+
"nextcloud-talk": {
|
|
61
|
+
...cfg.channels?.["nextcloud-talk"],
|
|
62
|
+
enabled: true,
|
|
63
|
+
accounts: {
|
|
64
|
+
...cfg.channels?.["nextcloud-talk"]?.accounts,
|
|
65
|
+
[accountId]: {
|
|
66
|
+
...cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
67
|
+
enabled: cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
68
|
+
...updates,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
42
72
|
},
|
|
43
|
-
}
|
|
73
|
+
};
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
async function noteNextcloudTalkSecretHelp(prompter: WizardPrompter): Promise<void> {
|
|
@@ -105,40 +135,10 @@ async function promptNextcloudTalkAllowFrom(params: {
|
|
|
105
135
|
];
|
|
106
136
|
const unique = mergeAllowFromEntries(undefined, merged);
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
...cfg.channels,
|
|
113
|
-
"nextcloud-talk": {
|
|
114
|
-
...cfg.channels?.["nextcloud-talk"],
|
|
115
|
-
enabled: true,
|
|
116
|
-
dmPolicy: "allowlist",
|
|
117
|
-
allowFrom: unique,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
...cfg,
|
|
125
|
-
channels: {
|
|
126
|
-
...cfg.channels,
|
|
127
|
-
"nextcloud-talk": {
|
|
128
|
-
...cfg.channels?.["nextcloud-talk"],
|
|
129
|
-
enabled: true,
|
|
130
|
-
accounts: {
|
|
131
|
-
...cfg.channels?.["nextcloud-talk"]?.accounts,
|
|
132
|
-
[accountId]: {
|
|
133
|
-
...cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
134
|
-
enabled: cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
135
|
-
dmPolicy: "allowlist",
|
|
136
|
-
allowFrom: unique,
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
138
|
+
return setNextcloudTalkAccountConfig(cfg, accountId, {
|
|
139
|
+
dmPolicy: "allowlist",
|
|
140
|
+
allowFrom: unique,
|
|
141
|
+
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
async function promptNextcloudTalkAllowFromForAccount(params: {
|
|
@@ -193,22 +193,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
193
193
|
shouldPromptAccountIds,
|
|
194
194
|
forceAllowFrom,
|
|
195
195
|
}) => {
|
|
196
|
-
const nextcloudTalkOverride = accountOverrides["nextcloud-talk"]?.trim();
|
|
197
196
|
const defaultAccountId = resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
currentId: accountId,
|
|
208
|
-
listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
|
|
209
|
-
defaultAccountId,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
197
|
+
const accountId = await resolveAccountIdForConfigure({
|
|
198
|
+
cfg,
|
|
199
|
+
prompter,
|
|
200
|
+
label: "Nextcloud Talk",
|
|
201
|
+
accountOverride: accountOverrides["nextcloud-talk"],
|
|
202
|
+
shouldPromptAccountIds,
|
|
203
|
+
listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
|
|
204
|
+
defaultAccountId,
|
|
205
|
+
});
|
|
212
206
|
|
|
213
207
|
let next = cfg as CoreConfig;
|
|
214
208
|
const resolvedAccount = resolveNextcloudTalkAccount({
|
|
@@ -217,11 +211,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
217
211
|
});
|
|
218
212
|
const accountConfigured = Boolean(resolvedAccount.secret && resolvedAccount.baseUrl);
|
|
219
213
|
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
220
|
-
const canUseEnv = allowEnv && Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim());
|
|
221
214
|
const hasConfigSecret = Boolean(
|
|
222
215
|
hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
|
|
223
216
|
resolvedAccount.config.botSecretFile,
|
|
224
217
|
);
|
|
218
|
+
const secretPromptState = buildSingleChannelSecretPromptState({
|
|
219
|
+
accountConfigured,
|
|
220
|
+
hasConfigToken: hasConfigSecret,
|
|
221
|
+
allowEnv,
|
|
222
|
+
envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
|
|
223
|
+
});
|
|
225
224
|
|
|
226
225
|
let baseUrl = resolvedAccount.baseUrl;
|
|
227
226
|
if (!baseUrl) {
|
|
@@ -252,9 +251,9 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
252
251
|
prompter,
|
|
253
252
|
providerHint: "nextcloud-talk",
|
|
254
253
|
credentialLabel: "bot secret",
|
|
255
|
-
accountConfigured,
|
|
256
|
-
canUseEnv: canUseEnv
|
|
257
|
-
hasConfigToken:
|
|
254
|
+
accountConfigured: secretPromptState.accountConfigured,
|
|
255
|
+
canUseEnv: secretPromptState.canUseEnv,
|
|
256
|
+
hasConfigToken: secretPromptState.hasConfigToken,
|
|
258
257
|
envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
|
|
259
258
|
keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
|
|
260
259
|
inputPrompt: "Enter Nextcloud Talk bot secret",
|
|
@@ -265,41 +264,10 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
265
264
|
}
|
|
266
265
|
|
|
267
266
|
if (secretResult.action === "use-env" || secret || baseUrl !== resolvedAccount.baseUrl) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
...next.channels,
|
|
273
|
-
"nextcloud-talk": {
|
|
274
|
-
...next.channels?.["nextcloud-talk"],
|
|
275
|
-
enabled: true,
|
|
276
|
-
baseUrl,
|
|
277
|
-
...(secret ? { botSecret: secret } : {}),
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
};
|
|
281
|
-
} else {
|
|
282
|
-
next = {
|
|
283
|
-
...next,
|
|
284
|
-
channels: {
|
|
285
|
-
...next.channels,
|
|
286
|
-
"nextcloud-talk": {
|
|
287
|
-
...next.channels?.["nextcloud-talk"],
|
|
288
|
-
enabled: true,
|
|
289
|
-
accounts: {
|
|
290
|
-
...next.channels?.["nextcloud-talk"]?.accounts,
|
|
291
|
-
[accountId]: {
|
|
292
|
-
...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
293
|
-
enabled:
|
|
294
|
-
next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
295
|
-
baseUrl,
|
|
296
|
-
...(secret ? { botSecret: secret } : {}),
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
}
|
|
267
|
+
next = setNextcloudTalkAccountConfig(next, accountId, {
|
|
268
|
+
baseUrl,
|
|
269
|
+
...(secret ? { botSecret: secret } : {}),
|
|
270
|
+
});
|
|
303
271
|
}
|
|
304
272
|
|
|
305
273
|
const existingApiUser = resolvedAccount.config.apiUser?.trim();
|
|
@@ -324,50 +292,21 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
324
292
|
prompter,
|
|
325
293
|
providerHint: "nextcloud-talk-api",
|
|
326
294
|
credentialLabel: "API password",
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
295
|
+
...buildSingleChannelSecretPromptState({
|
|
296
|
+
accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
|
|
297
|
+
hasConfigToken: existingApiPasswordConfigured,
|
|
298
|
+
allowEnv: false,
|
|
299
|
+
}),
|
|
330
300
|
envPrompt: "",
|
|
331
301
|
keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
|
|
332
302
|
inputPrompt: "Enter Nextcloud Talk API password",
|
|
333
303
|
preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
|
|
334
304
|
});
|
|
335
305
|
const apiPassword = apiPasswordResult.action === "set" ? apiPasswordResult.value : undefined;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
...next.channels,
|
|
341
|
-
"nextcloud-talk": {
|
|
342
|
-
...next.channels?.["nextcloud-talk"],
|
|
343
|
-
enabled: true,
|
|
344
|
-
apiUser,
|
|
345
|
-
...(apiPassword ? { apiPassword } : {}),
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
} else {
|
|
350
|
-
next = {
|
|
351
|
-
...next,
|
|
352
|
-
channels: {
|
|
353
|
-
...next.channels,
|
|
354
|
-
"nextcloud-talk": {
|
|
355
|
-
...next.channels?.["nextcloud-talk"],
|
|
356
|
-
enabled: true,
|
|
357
|
-
accounts: {
|
|
358
|
-
...next.channels?.["nextcloud-talk"]?.accounts,
|
|
359
|
-
[accountId]: {
|
|
360
|
-
...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
361
|
-
enabled:
|
|
362
|
-
next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
363
|
-
apiUser,
|
|
364
|
-
...(apiPassword ? { apiPassword } : {}),
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
}
|
|
306
|
+
next = setNextcloudTalkAccountConfig(next, accountId, {
|
|
307
|
+
apiUser,
|
|
308
|
+
...(apiPassword ? { apiPassword } : {}),
|
|
309
|
+
});
|
|
371
310
|
}
|
|
372
311
|
|
|
373
312
|
if (forceAllowFrom) {
|
package/src/policy.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { resolveNextcloudTalkAllowlistMatch } from "./policy.js";
|
|
2
|
+
import { resolveNextcloudTalkAllowlistMatch, resolveNextcloudTalkGroupAllow } from "./policy.js";
|
|
3
3
|
|
|
4
4
|
describe("nextcloud-talk policy", () => {
|
|
5
5
|
describe("resolveNextcloudTalkAllowlistMatch", () => {
|
|
@@ -30,4 +30,109 @@ describe("nextcloud-talk policy", () => {
|
|
|
30
30
|
).toBe(false);
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
describe("resolveNextcloudTalkGroupAllow", () => {
|
|
35
|
+
it("blocks disabled policy", () => {
|
|
36
|
+
expect(
|
|
37
|
+
resolveNextcloudTalkGroupAllow({
|
|
38
|
+
groupPolicy: "disabled",
|
|
39
|
+
outerAllowFrom: ["owner"],
|
|
40
|
+
innerAllowFrom: ["room-user"],
|
|
41
|
+
senderId: "owner",
|
|
42
|
+
}),
|
|
43
|
+
).toEqual({
|
|
44
|
+
allowed: false,
|
|
45
|
+
outerMatch: { allowed: false },
|
|
46
|
+
innerMatch: { allowed: false },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("allows open policy", () => {
|
|
51
|
+
expect(
|
|
52
|
+
resolveNextcloudTalkGroupAllow({
|
|
53
|
+
groupPolicy: "open",
|
|
54
|
+
outerAllowFrom: [],
|
|
55
|
+
innerAllowFrom: [],
|
|
56
|
+
senderId: "owner",
|
|
57
|
+
}),
|
|
58
|
+
).toEqual({
|
|
59
|
+
allowed: true,
|
|
60
|
+
outerMatch: { allowed: true },
|
|
61
|
+
innerMatch: { allowed: true },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("blocks allowlist mode when both outer and inner allowlists are empty", () => {
|
|
66
|
+
expect(
|
|
67
|
+
resolveNextcloudTalkGroupAllow({
|
|
68
|
+
groupPolicy: "allowlist",
|
|
69
|
+
outerAllowFrom: [],
|
|
70
|
+
innerAllowFrom: [],
|
|
71
|
+
senderId: "owner",
|
|
72
|
+
}),
|
|
73
|
+
).toEqual({
|
|
74
|
+
allowed: false,
|
|
75
|
+
outerMatch: { allowed: false },
|
|
76
|
+
innerMatch: { allowed: false },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("requires inner match when only room-specific allowlist is configured", () => {
|
|
81
|
+
expect(
|
|
82
|
+
resolveNextcloudTalkGroupAllow({
|
|
83
|
+
groupPolicy: "allowlist",
|
|
84
|
+
outerAllowFrom: [],
|
|
85
|
+
innerAllowFrom: ["room-user"],
|
|
86
|
+
senderId: "room-user",
|
|
87
|
+
}),
|
|
88
|
+
).toEqual({
|
|
89
|
+
allowed: true,
|
|
90
|
+
outerMatch: { allowed: false },
|
|
91
|
+
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("blocks when outer allowlist misses even if inner allowlist matches", () => {
|
|
96
|
+
expect(
|
|
97
|
+
resolveNextcloudTalkGroupAllow({
|
|
98
|
+
groupPolicy: "allowlist",
|
|
99
|
+
outerAllowFrom: ["team-owner"],
|
|
100
|
+
innerAllowFrom: ["room-user"],
|
|
101
|
+
senderId: "room-user",
|
|
102
|
+
}),
|
|
103
|
+
).toEqual({
|
|
104
|
+
allowed: false,
|
|
105
|
+
outerMatch: { allowed: false },
|
|
106
|
+
innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("allows when both outer and inner allowlists match", () => {
|
|
111
|
+
expect(
|
|
112
|
+
resolveNextcloudTalkGroupAllow({
|
|
113
|
+
groupPolicy: "allowlist",
|
|
114
|
+
outerAllowFrom: ["team-owner"],
|
|
115
|
+
innerAllowFrom: ["room-user"],
|
|
116
|
+
senderId: "team-owner",
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
allowed: false,
|
|
120
|
+
outerMatch: { allowed: true, matchKey: "team-owner", matchSource: "id" },
|
|
121
|
+
innerMatch: { allowed: false },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
resolveNextcloudTalkGroupAllow({
|
|
126
|
+
groupPolicy: "allowlist",
|
|
127
|
+
outerAllowFrom: ["shared-user"],
|
|
128
|
+
innerAllowFrom: ["shared-user"],
|
|
129
|
+
senderId: "shared-user",
|
|
130
|
+
}),
|
|
131
|
+
).toEqual({
|
|
132
|
+
allowed: true,
|
|
133
|
+
outerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
|
|
134
|
+
innerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
33
138
|
});
|
package/src/policy.ts
CHANGED
|
@@ -3,14 +3,15 @@ import type {
|
|
|
3
3
|
ChannelGroupContext,
|
|
4
4
|
GroupPolicy,
|
|
5
5
|
GroupToolPolicyConfig,
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
7
7
|
import {
|
|
8
8
|
buildChannelKeyCandidates,
|
|
9
|
+
evaluateMatchedGroupAccessForPolicy,
|
|
9
10
|
normalizeChannelSlug,
|
|
10
11
|
resolveChannelEntryMatchWithFallback,
|
|
11
12
|
resolveMentionGatingWithBypass,
|
|
12
13
|
resolveNestedAllowlistDecision,
|
|
13
|
-
} from "openclaw/plugin-sdk";
|
|
14
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
14
15
|
import type { NextcloudTalkRoomConfig } from "./types.js";
|
|
15
16
|
|
|
16
17
|
function normalizeAllowEntry(raw: string): string {
|
|
@@ -128,19 +129,8 @@ export function resolveNextcloudTalkGroupAllow(params: {
|
|
|
128
129
|
innerAllowFrom: Array<string | number> | undefined;
|
|
129
130
|
senderId: string;
|
|
130
131
|
}): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } {
|
|
131
|
-
if (params.groupPolicy === "disabled") {
|
|
132
|
-
return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
|
|
133
|
-
}
|
|
134
|
-
if (params.groupPolicy === "open") {
|
|
135
|
-
return { allowed: true, outerMatch: { allowed: true }, innerMatch: { allowed: true } };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
132
|
const outerAllow = normalizeNextcloudTalkAllowlist(params.outerAllowFrom);
|
|
139
133
|
const innerAllow = normalizeNextcloudTalkAllowlist(params.innerAllowFrom);
|
|
140
|
-
if (outerAllow.length === 0 && innerAllow.length === 0) {
|
|
141
|
-
return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
const outerMatch = resolveNextcloudTalkAllowlistMatch({
|
|
145
135
|
allowFrom: params.outerAllowFrom,
|
|
146
136
|
senderId: params.senderId,
|
|
@@ -149,14 +139,32 @@ export function resolveNextcloudTalkGroupAllow(params: {
|
|
|
149
139
|
allowFrom: params.innerAllowFrom,
|
|
150
140
|
senderId: params.senderId,
|
|
151
141
|
});
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
const access = evaluateMatchedGroupAccessForPolicy({
|
|
143
|
+
groupPolicy: params.groupPolicy,
|
|
144
|
+
allowlistConfigured: outerAllow.length > 0 || innerAllow.length > 0,
|
|
145
|
+
allowlistMatched: resolveNestedAllowlistDecision({
|
|
146
|
+
outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,
|
|
147
|
+
outerMatched: outerAllow.length > 0 ? outerMatch.allowed : true,
|
|
148
|
+
innerConfigured: innerAllow.length > 0,
|
|
149
|
+
innerMatched: innerMatch.allowed,
|
|
150
|
+
}),
|
|
157
151
|
});
|
|
158
152
|
|
|
159
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
allowed: access.allowed,
|
|
155
|
+
outerMatch:
|
|
156
|
+
params.groupPolicy === "open"
|
|
157
|
+
? { allowed: true }
|
|
158
|
+
: params.groupPolicy === "disabled"
|
|
159
|
+
? { allowed: false }
|
|
160
|
+
: outerMatch,
|
|
161
|
+
innerMatch:
|
|
162
|
+
params.groupPolicy === "open"
|
|
163
|
+
? { allowed: true }
|
|
164
|
+
: params.groupPolicy === "disabled"
|
|
165
|
+
? { allowed: false }
|
|
166
|
+
: innerMatch,
|
|
167
|
+
};
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
export function resolveNextcloudTalkMentionGate(params: {
|
package/src/replay-guard.ts
CHANGED
package/src/room-info.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
|
|
3
|
-
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/nextcloud-talk";
|
|
3
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/nextcloud-talk";
|
|
4
4
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
5
5
|
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
6
6
|
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
2
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/nextcloud-talk";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
runtime = next;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getNextcloudTalkRuntime(): PluginRuntime {
|
|
10
|
-
if (!runtime) {
|
|
11
|
-
throw new Error("Nextcloud Talk runtime not initialized");
|
|
12
|
-
}
|
|
13
|
-
return runtime;
|
|
14
|
-
}
|
|
4
|
+
const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime } =
|
|
5
|
+
createPluginRuntimeStore<PluginRuntime>("Nextcloud Talk runtime not initialized");
|
|
6
|
+
export { getNextcloudTalkRuntime, setNextcloudTalkRuntime };
|
package/src/secret-input.ts
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildSecretInputSchema,
|
|
2
3
|
hasConfiguredSecretInput,
|
|
3
4
|
normalizeResolvedSecretInputString,
|
|
4
5
|
normalizeSecretInputString,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import { z } from "zod";
|
|
6
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
7
7
|
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
source: z.enum(["env", "file", "exec"]),
|
|
15
|
-
provider: z.string().min(1),
|
|
16
|
-
id: z.string().min(1),
|
|
17
|
-
}),
|
|
18
|
-
]);
|
|
19
|
-
}
|
|
8
|
+
export {
|
|
9
|
+
buildSecretInputSchema,
|
|
10
|
+
hasConfiguredSecretInput,
|
|
11
|
+
normalizeResolvedSecretInputString,
|
|
12
|
+
normalizeSecretInputString,
|
|
13
|
+
};
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const hoisted = vi.hoisted(() => ({
|
|
4
|
+
loadConfig: vi.fn(),
|
|
5
|
+
resolveMarkdownTableMode: vi.fn(() => "preserve"),
|
|
6
|
+
convertMarkdownTables: vi.fn((text: string) => text),
|
|
7
|
+
record: vi.fn(),
|
|
8
|
+
resolveNextcloudTalkAccount: vi.fn(() => ({
|
|
9
|
+
accountId: "default",
|
|
10
|
+
baseUrl: "https://nextcloud.example.com",
|
|
11
|
+
secret: "secret-value", // pragma: allowlist secret
|
|
12
|
+
})),
|
|
13
|
+
generateNextcloudTalkSignature: vi.fn(() => ({
|
|
14
|
+
random: "r",
|
|
15
|
+
signature: "s",
|
|
16
|
+
})),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("./runtime.js", () => ({
|
|
20
|
+
getNextcloudTalkRuntime: () => ({
|
|
21
|
+
config: {
|
|
22
|
+
loadConfig: hoisted.loadConfig,
|
|
23
|
+
},
|
|
24
|
+
channel: {
|
|
25
|
+
text: {
|
|
26
|
+
resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode,
|
|
27
|
+
convertMarkdownTables: hoisted.convertMarkdownTables,
|
|
28
|
+
},
|
|
29
|
+
activity: {
|
|
30
|
+
record: hoisted.record,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("./accounts.js", () => ({
|
|
37
|
+
resolveNextcloudTalkAccount: hoisted.resolveNextcloudTalkAccount,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock("./signature.js", () => ({
|
|
41
|
+
generateNextcloudTalkSignature: hoisted.generateNextcloudTalkSignature,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
import { sendMessageNextcloudTalk, sendReactionNextcloudTalk } from "./send.js";
|
|
45
|
+
|
|
46
|
+
describe("nextcloud-talk send cfg threading", () => {
|
|
47
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
fetchMock.mockReset();
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("uses provided cfg for sendMessage and skips runtime loadConfig", async () => {
|
|
60
|
+
const cfg = { source: "provided" } as const;
|
|
61
|
+
fetchMock.mockResolvedValueOnce(
|
|
62
|
+
new Response(
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
ocs: { data: { id: 12345, timestamp: 1_706_000_000 } },
|
|
65
|
+
}),
|
|
66
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await sendMessageNextcloudTalk("room:abc123", "hello", {
|
|
71
|
+
cfg,
|
|
72
|
+
accountId: "work",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(hoisted.loadConfig).not.toHaveBeenCalled();
|
|
76
|
+
expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
|
|
77
|
+
cfg,
|
|
78
|
+
accountId: "work",
|
|
79
|
+
});
|
|
80
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
messageId: "12345",
|
|
83
|
+
roomToken: "abc123",
|
|
84
|
+
timestamp: 1_706_000_000,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("falls back to runtime cfg for sendReaction when cfg is omitted", async () => {
|
|
89
|
+
const runtimeCfg = { source: "runtime" } as const;
|
|
90
|
+
hoisted.loadConfig.mockReturnValueOnce(runtimeCfg);
|
|
91
|
+
fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
|
|
92
|
+
|
|
93
|
+
const result = await sendReactionNextcloudTalk("room:ops", "m-1", "👍", {
|
|
94
|
+
accountId: "default",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({ ok: true });
|
|
98
|
+
expect(hoisted.loadConfig).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
|
|
100
|
+
cfg: runtimeCfg,
|
|
101
|
+
accountId: "default",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/send.ts
CHANGED
|
@@ -9,6 +9,7 @@ type NextcloudTalkSendOpts = {
|
|
|
9
9
|
accountId?: string;
|
|
10
10
|
replyTo?: string;
|
|
11
11
|
verbose?: boolean;
|
|
12
|
+
cfg?: CoreConfig;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
function resolveCredentials(
|
|
@@ -60,7 +61,7 @@ export async function sendMessageNextcloudTalk(
|
|
|
60
61
|
text: string,
|
|
61
62
|
opts: NextcloudTalkSendOpts = {},
|
|
62
63
|
): Promise<NextcloudTalkSendResult> {
|
|
63
|
-
const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig;
|
|
64
|
+
const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
|
|
64
65
|
const account = resolveNextcloudTalkAccount({
|
|
65
66
|
cfg,
|
|
66
67
|
accountId: opts.accountId,
|
|
@@ -175,7 +176,7 @@ export async function sendReactionNextcloudTalk(
|
|
|
175
176
|
reaction: string,
|
|
176
177
|
opts: Omit<NextcloudTalkSendOpts, "replyTo"> = {},
|
|
177
178
|
): Promise<{ ok: true }> {
|
|
178
|
-
const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig;
|
|
179
|
+
const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
|
|
179
180
|
const account = resolveNextcloudTalkAccount({
|
|
180
181
|
cfg,
|
|
181
182
|
accountId: opts.accountId,
|