@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 +2 -2
- package/package.json +4 -1
- package/src/accounts.ts +26 -59
- package/src/channel.startup.test.ts +9 -41
- package/src/channel.ts +82 -85
- package/src/config-schema.test.ts +36 -0
- package/src/config-schema.ts +4 -3
- package/src/inbound.authz.test.ts +2 -2
- package/src/inbound.ts +31 -50
- package/src/monitor.backend.test.ts +4 -23
- package/src/monitor.replay.test.ts +2 -28
- package/src/monitor.test-fixtures.ts +30 -0
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +126 -145
- 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 +40 -25
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send.test.ts +104 -0
- package/src/send.ts +3 -2
- package/src/types.ts +4 -3
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.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
|
-
|
|
6
|
-
} from "openclaw/plugin-sdk/
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 =
|
|
41
|
+
const ids = listNextcloudTalkAccountIdsInternal(cfg);
|
|
47
42
|
debugAccounts("listNextcloudTalkAccountIds", ids);
|
|
48
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
}
|
|
@@ -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
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -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:
|
|
31
|
+
botSecret: buildSecretInputSchema().optional(),
|
|
31
32
|
botSecretFile: z.string().optional(),
|
|
32
33
|
apiUser: z.string().optional(),
|
|
33
|
-
apiPassword:
|
|
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
|
-
|
|
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
|