@openclaw/nextcloud-talk 2026.3.7 → 2026.3.10
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/package.json +1 -1
- package/src/accounts.test.ts +30 -0
- package/src/accounts.ts +8 -8
- package/src/channel.ts +19 -10
- package/src/onboarding.ts +38 -66
- package/src/runtime.ts +4 -12
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
6
|
+
import type { CoreConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
describe("resolveNextcloudTalkAccount", () => {
|
|
9
|
+
it.runIf(process.platform !== "win32")("rejects symlinked botSecretFile paths", () => {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-nextcloud-talk-"));
|
|
11
|
+
const secretFile = path.join(dir, "secret.txt");
|
|
12
|
+
const secretLink = path.join(dir, "secret-link.txt");
|
|
13
|
+
fs.writeFileSync(secretFile, "bot-secret\n", "utf8");
|
|
14
|
+
fs.symlinkSync(secretFile, secretLink);
|
|
15
|
+
|
|
16
|
+
const cfg = {
|
|
17
|
+
channels: {
|
|
18
|
+
"nextcloud-talk": {
|
|
19
|
+
baseUrl: "https://cloud.example.com",
|
|
20
|
+
botSecretFile: secretLink,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as CoreConfig;
|
|
24
|
+
|
|
25
|
+
const account = resolveNextcloudTalkAccount({ cfg });
|
|
26
|
+
expect(account.secret).toBe("");
|
|
27
|
+
expect(account.secretSource).toBe("none");
|
|
28
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/accounts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import {
|
|
3
3
|
createAccountListHelpers,
|
|
4
4
|
DEFAULT_ACCOUNT_ID,
|
|
@@ -88,13 +88,13 @@ function resolveNextcloudTalkSecret(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
if (merged.botSecretFile) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
const fileSecret = tryReadSecretFileSync(
|
|
92
|
+
merged.botSecretFile,
|
|
93
|
+
"Nextcloud Talk bot secret file",
|
|
94
|
+
{ rejectSymlink: true },
|
|
95
|
+
);
|
|
96
|
+
if (fileSecret) {
|
|
97
|
+
return { secret: fileSecret, source: "secretFile" };
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
package/src/channel.ts
CHANGED
|
@@ -2,8 +2,10 @@ import {
|
|
|
2
2
|
buildAccountScopedDmSecurityPolicy,
|
|
3
3
|
collectAllowlistProviderGroupPolicyWarnings,
|
|
4
4
|
collectOpenGroupPolicyRouteAllowlistWarnings,
|
|
5
|
+
createAccountStatusSink,
|
|
5
6
|
formatAllowFromLowercase,
|
|
6
7
|
mapAllowFromEntries,
|
|
8
|
+
runPassiveAccountLifecycle,
|
|
7
9
|
} from "openclaw/plugin-sdk/compat";
|
|
8
10
|
import {
|
|
9
11
|
applyAccountNameToChannelSection,
|
|
@@ -19,7 +21,6 @@ import {
|
|
|
19
21
|
type OpenClawConfig,
|
|
20
22
|
type ChannelSetupInput,
|
|
21
23
|
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
22
|
-
import { waitForAbortSignal } from "../../../src/infra/abort-signal.js";
|
|
23
24
|
import {
|
|
24
25
|
listNextcloudTalkAccountIds,
|
|
25
26
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -338,17 +339,25 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
338
339
|
|
|
339
340
|
ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook server`);
|
|
340
341
|
|
|
341
|
-
const
|
|
342
|
-
accountId:
|
|
343
|
-
|
|
344
|
-
runtime: ctx.runtime,
|
|
345
|
-
abortSignal: ctx.abortSignal,
|
|
346
|
-
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
342
|
+
const statusSink = createAccountStatusSink({
|
|
343
|
+
accountId: ctx.accountId,
|
|
344
|
+
setStatus: ctx.setStatus,
|
|
347
345
|
});
|
|
348
346
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
await runPassiveAccountLifecycle({
|
|
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,
|
|
356
|
+
}),
|
|
357
|
+
stop: async (monitor) => {
|
|
358
|
+
monitor.stop();
|
|
359
|
+
},
|
|
360
|
+
});
|
|
352
361
|
},
|
|
353
362
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
354
363
|
const nextCfg = { ...cfg } as OpenClawConfig;
|
package/src/onboarding.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
buildSingleChannelSecretPromptState,
|
|
3
2
|
formatDocsLink,
|
|
4
3
|
hasConfiguredSecretInput,
|
|
5
4
|
mapAllowFromEntries,
|
|
6
5
|
mergeAllowFromEntries,
|
|
7
|
-
|
|
6
|
+
patchScopedAccountConfig,
|
|
7
|
+
runSingleChannelSecretStep,
|
|
8
8
|
resolveAccountIdForConfigure,
|
|
9
9
|
DEFAULT_ACCOUNT_ID,
|
|
10
10
|
normalizeAccountId,
|
|
11
11
|
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
12
|
-
type SecretInput,
|
|
13
12
|
type ChannelOnboardingAdapter,
|
|
14
13
|
type ChannelOnboardingDmPolicy,
|
|
15
14
|
type OpenClawConfig,
|
|
@@ -39,38 +38,12 @@ function setNextcloudTalkAccountConfig(
|
|
|
39
38
|
accountId: string,
|
|
40
39
|
updates: Record<string, unknown>,
|
|
41
40
|
): CoreConfig {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
...cfg.channels?.["nextcloud-talk"],
|
|
49
|
-
enabled: true,
|
|
50
|
-
...updates,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
...cfg,
|
|
58
|
-
channels: {
|
|
59
|
-
...cfg.channels,
|
|
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
|
-
},
|
|
72
|
-
},
|
|
73
|
-
};
|
|
41
|
+
return patchScopedAccountConfig({
|
|
42
|
+
cfg,
|
|
43
|
+
channelKey: channel,
|
|
44
|
+
accountId,
|
|
45
|
+
patch: updates,
|
|
46
|
+
}) as CoreConfig;
|
|
74
47
|
}
|
|
75
48
|
|
|
76
49
|
async function noteNextcloudTalkSecretHelp(prompter: WizardPrompter): Promise<void> {
|
|
@@ -215,12 +188,6 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
215
188
|
hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
|
|
216
189
|
resolvedAccount.config.botSecretFile,
|
|
217
190
|
);
|
|
218
|
-
const secretPromptState = buildSingleChannelSecretPromptState({
|
|
219
|
-
accountConfigured,
|
|
220
|
-
hasConfigToken: hasConfigSecret,
|
|
221
|
-
allowEnv,
|
|
222
|
-
envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
|
|
223
|
-
});
|
|
224
191
|
|
|
225
192
|
let baseUrl = resolvedAccount.baseUrl;
|
|
226
193
|
if (!baseUrl) {
|
|
@@ -241,32 +208,35 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
241
208
|
).trim();
|
|
242
209
|
}
|
|
243
210
|
|
|
244
|
-
|
|
245
|
-
if (!accountConfigured) {
|
|
246
|
-
await noteNextcloudTalkSecretHelp(prompter);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const secretResult = await promptSingleChannelSecretInput({
|
|
211
|
+
const secretStep = await runSingleChannelSecretStep({
|
|
250
212
|
cfg: next,
|
|
251
213
|
prompter,
|
|
252
214
|
providerHint: "nextcloud-talk",
|
|
253
215
|
credentialLabel: "bot secret",
|
|
254
|
-
accountConfigured
|
|
255
|
-
|
|
256
|
-
|
|
216
|
+
accountConfigured,
|
|
217
|
+
hasConfigToken: hasConfigSecret,
|
|
218
|
+
allowEnv,
|
|
219
|
+
envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
|
|
257
220
|
envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
|
|
258
221
|
keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
|
|
259
222
|
inputPrompt: "Enter Nextcloud Talk bot secret",
|
|
260
223
|
preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
|
|
224
|
+
onMissingConfigured: async () => await noteNextcloudTalkSecretHelp(prompter),
|
|
225
|
+
applyUseEnv: async (cfg) =>
|
|
226
|
+
setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
|
|
227
|
+
baseUrl,
|
|
228
|
+
}),
|
|
229
|
+
applySet: async (cfg, value) =>
|
|
230
|
+
setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
|
|
231
|
+
baseUrl,
|
|
232
|
+
botSecret: value,
|
|
233
|
+
}),
|
|
261
234
|
});
|
|
262
|
-
|
|
263
|
-
secret = secretResult.value;
|
|
264
|
-
}
|
|
235
|
+
next = secretStep.cfg as CoreConfig;
|
|
265
236
|
|
|
266
|
-
if (
|
|
237
|
+
if (secretStep.action === "keep" && baseUrl !== resolvedAccount.baseUrl) {
|
|
267
238
|
next = setNextcloudTalkAccountConfig(next, accountId, {
|
|
268
239
|
baseUrl,
|
|
269
|
-
...(secret ? { botSecret: secret } : {}),
|
|
270
240
|
});
|
|
271
241
|
}
|
|
272
242
|
|
|
@@ -287,26 +257,28 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
287
257
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
288
258
|
}),
|
|
289
259
|
).trim();
|
|
290
|
-
const
|
|
260
|
+
const apiPasswordStep = await runSingleChannelSecretStep({
|
|
291
261
|
cfg: next,
|
|
292
262
|
prompter,
|
|
293
263
|
providerHint: "nextcloud-talk-api",
|
|
294
264
|
credentialLabel: "API password",
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
allowEnv: false,
|
|
299
|
-
}),
|
|
265
|
+
accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
|
|
266
|
+
hasConfigToken: existingApiPasswordConfigured,
|
|
267
|
+
allowEnv: false,
|
|
300
268
|
envPrompt: "",
|
|
301
269
|
keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
|
|
302
270
|
inputPrompt: "Enter Nextcloud Talk API password",
|
|
303
271
|
preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
|
|
272
|
+
applySet: async (cfg, value) =>
|
|
273
|
+
setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
|
|
274
|
+
apiUser,
|
|
275
|
+
apiPassword: value,
|
|
276
|
+
}),
|
|
304
277
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
});
|
|
278
|
+
next =
|
|
279
|
+
apiPasswordStep.action === "keep"
|
|
280
|
+
? setNextcloudTalkAccountConfig(next, accountId, { apiUser })
|
|
281
|
+
: (apiPasswordStep.cfg as CoreConfig);
|
|
310
282
|
}
|
|
311
283
|
|
|
312
284
|
if (forceAllowFrom) {
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
1
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 };
|