@openclaw/nextcloud-talk 2026.3.13 → 2026.5.2-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.
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 +158 -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/types.ts CHANGED
@@ -4,9 +4,7 @@ import type {
4
4
  DmPolicy,
5
5
  GroupPolicy,
6
6
  SecretInput,
7
- } from "openclaw/plugin-sdk/nextcloud-talk";
8
-
9
- export type { DmPolicy, GroupPolicy };
7
+ } from "../runtime-api.js";
10
8
 
11
9
  export type NextcloudTalkRoomConfig = {
12
10
  requireMention?: boolean;
@@ -22,6 +20,11 @@ export type NextcloudTalkRoomConfig = {
22
20
  systemPrompt?: string;
23
21
  };
24
22
 
23
+ type NextcloudTalkNetworkConfig = {
24
+ /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
25
+ dangerouslyAllowPrivateNetwork?: boolean;
26
+ };
27
+
25
28
  export type NextcloudTalkAccountConfig = {
26
29
  /** Optional display name for this account (used in CLI/UI lists). */
27
30
  name?: string;
@@ -75,9 +78,11 @@ export type NextcloudTalkAccountConfig = {
75
78
  responsePrefix?: string;
76
79
  /** Media upload max size in MB. */
77
80
  mediaMaxMb?: number;
81
+ /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
82
+ network?: NextcloudTalkNetworkConfig;
78
83
  };
79
84
 
80
- export type NextcloudTalkConfig = {
85
+ type NextcloudTalkConfig = {
81
86
  /** Optional per-account Nextcloud Talk configuration (multi-account). */
82
87
  accounts?: Record<string, NextcloudTalkAccountConfig>;
83
88
  /** Optional default account id when multiple accounts are configured. */
@@ -97,7 +102,7 @@ export type CoreConfig = {
97
102
  */
98
103
 
99
104
  /** Actor in the activity (the message sender). */
100
- export type NextcloudTalkActor = {
105
+ type NextcloudTalkActor = {
101
106
  type: "Person";
102
107
  /** User ID in Nextcloud. */
103
108
  id: string;
@@ -106,7 +111,7 @@ export type NextcloudTalkActor = {
106
111
  };
107
112
 
108
113
  /** The message object in the activity. */
109
- export type NextcloudTalkObject = {
114
+ type NextcloudTalkObject = {
110
115
  type: "Note";
111
116
  /** Message ID. */
112
117
  id: string;
@@ -119,7 +124,7 @@ export type NextcloudTalkObject = {
119
124
  };
120
125
 
121
126
  /** Target conversation/room. */
122
- export type NextcloudTalkTarget = {
127
+ type NextcloudTalkTarget = {
123
128
  type: "Collection";
124
129
  /** Room token. */
125
130
  id: string;
@@ -172,19 +177,17 @@ export type NextcloudTalkWebhookServerOptions = {
172
177
  path: string;
173
178
  secret: string;
174
179
  maxBodyBytes?: number;
180
+ authRateLimit?: {
181
+ maxRequests?: number;
182
+ windowMs?: number;
183
+ };
175
184
  readBody?: (req: import("node:http").IncomingMessage, maxBodyBytes: number) => Promise<string>;
176
185
  isBackendAllowed?: (backend: string) => boolean;
177
186
  shouldProcessMessage?: (message: NextcloudTalkInboundMessage) => boolean | Promise<boolean>;
187
+ processMessage?: (
188
+ message: NextcloudTalkInboundMessage,
189
+ ) => void | "processed" | "duplicate" | Promise<void | "processed" | "duplicate">;
178
190
  onMessage: (message: NextcloudTalkInboundMessage) => void | Promise<void>;
179
191
  onError?: (error: Error) => void;
180
192
  abortSignal?: AbortSignal;
181
193
  };
182
-
183
- /** Options for sending a message. */
184
- export type NextcloudTalkSendOptions = {
185
- baseUrl: string;
186
- secret: string;
187
- roomToken: string;
188
- message: string;
189
- replyTo?: string;
190
- };
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.package-boundary.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./*.ts", "./src/**/*.ts"],
7
+ "exclude": [
8
+ "./**/*.test.ts",
9
+ "./dist/**",
10
+ "./node_modules/**",
11
+ "./src/test-support/**",
12
+ "./src/**/*test-helpers.ts",
13
+ "./src/**/*test-harness.ts",
14
+ "./src/**/*test-support.ts"
15
+ ]
16
+ }
@@ -1,30 +0,0 @@
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
- });
@@ -1,36 +0,0 @@
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/format.ts DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * Format utilities for Nextcloud Talk messages.
3
- *
4
- * Nextcloud Talk supports markdown natively, so most formatting passes through.
5
- * This module handles any edge cases or transformations needed.
6
- */
7
-
8
- /**
9
- * Convert markdown to Nextcloud Talk compatible format.
10
- * Nextcloud Talk supports standard markdown, so minimal transformation needed.
11
- */
12
- export function markdownToNextcloudTalk(text: string): string {
13
- return text.trim();
14
- }
15
-
16
- /**
17
- * Escape special characters in text to prevent markdown interpretation.
18
- */
19
- export function escapeNextcloudTalkMarkdown(text: string): string {
20
- return text.replace(/([*_`~[\]()#>+\-=|{}!\\])/g, "\\$1");
21
- }
22
-
23
- /**
24
- * Format a mention for a Nextcloud user.
25
- * Nextcloud Talk uses @user format for mentions.
26
- */
27
- export function formatNextcloudTalkMention(userId: string): string {
28
- return `@${userId.replace(/^@/, "")}`;
29
- }
30
-
31
- /**
32
- * Format a code block for Nextcloud Talk.
33
- */
34
- export function formatNextcloudTalkCodeBlock(code: string, language?: string): string {
35
- const lang = language ?? "";
36
- return `\`\`\`${lang}\n${code}\n\`\`\``;
37
- }
38
-
39
- /**
40
- * Format inline code for Nextcloud Talk.
41
- */
42
- export function formatNextcloudTalkInlineCode(code: string): string {
43
- if (code.includes("`")) {
44
- return `\`\` ${code} \`\``;
45
- }
46
- return `\`${code}\``;
47
- }
48
-
49
- /**
50
- * Strip Nextcloud Talk specific formatting from text.
51
- * Useful for extracting plain text content.
52
- */
53
- export function stripNextcloudTalkFormatting(text: string): string {
54
- return text
55
- .replace(/```[\s\S]*?```/g, "")
56
- .replace(/`[^`]+`/g, "")
57
- .replace(/\*\*([^*]+)\*\*/g, "$1")
58
- .replace(/\*([^*]+)\*/g, "$1")
59
- .replace(/_([^_]+)_/g, "$1")
60
- .replace(/~~([^~]+)~~/g, "$1")
61
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
62
- .replace(/\s+/g, " ")
63
- .trim();
64
- }
65
-
66
- /**
67
- * Truncate text to a maximum length, preserving word boundaries.
68
- */
69
- export function truncateNextcloudTalkText(text: string, maxLength: number, suffix = "..."): string {
70
- if (text.length <= maxLength) {
71
- return text;
72
- }
73
- const truncated = text.slice(0, maxLength - suffix.length);
74
- const lastSpace = truncated.lastIndexOf(" ");
75
- if (lastSpace > maxLength * 0.7) {
76
- return truncated.slice(0, lastSpace) + suffix;
77
- }
78
- return truncated + suffix;
79
- }
@@ -1,28 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { startWebhookServer } from "./monitor.test-harness.js";
3
-
4
- describe("createNextcloudTalkWebhookServer auth order", () => {
5
- it("rejects missing signature headers before reading request body", async () => {
6
- const readBody = vi.fn(async () => {
7
- throw new Error("should not be called for missing signature headers");
8
- });
9
- const harness = await startWebhookServer({
10
- path: "/nextcloud-auth-order",
11
- maxBodyBytes: 128,
12
- readBody,
13
- onMessage: vi.fn(),
14
- });
15
-
16
- const response = await fetch(harness.webhookUrl, {
17
- method: "POST",
18
- headers: {
19
- "content-type": "application/json",
20
- },
21
- body: "{}",
22
- });
23
-
24
- expect(response.status).toBe(400);
25
- expect(await response.json()).toEqual({ error: "Missing signature headers" });
26
- expect(readBody).not.toHaveBeenCalled();
27
- });
28
- });
@@ -1,27 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
3
- import { startWebhookServer } from "./monitor.test-harness.js";
4
-
5
- describe("createNextcloudTalkWebhookServer backend allowlist", () => {
6
- it("rejects requests from unexpected backend origins", async () => {
7
- const onMessage = vi.fn(async () => {});
8
- const harness = await startWebhookServer({
9
- path: "/nextcloud-backend-check",
10
- isBackendAllowed: (backend) => backend === "https://nextcloud.expected",
11
- onMessage,
12
- });
13
-
14
- const { body, headers } = createSignedCreateMessageRequest({
15
- backend: "https://nextcloud.unexpected",
16
- });
17
- const response = await fetch(harness.webhookUrl, {
18
- method: "POST",
19
- headers,
20
- body,
21
- });
22
-
23
- expect(response.status).toBe(401);
24
- expect(await response.json()).toEqual({ error: "Invalid backend" });
25
- expect(onMessage).not.toHaveBeenCalled();
26
- });
27
- });
@@ -1,16 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createMockIncomingRequest } from "../../../test/helpers/mock-incoming-request.js";
3
- import { readNextcloudTalkWebhookBody } from "./monitor.js";
4
-
5
- describe("readNextcloudTalkWebhookBody", () => {
6
- it("reads valid body within max bytes", async () => {
7
- const req = createMockIncomingRequest(['{"type":"Create"}']);
8
- const body = await readNextcloudTalkWebhookBody(req, 1024);
9
- expect(body).toBe('{"type":"Create"}');
10
- });
11
-
12
- it("rejects when payload exceeds max bytes", async () => {
13
- const req = createMockIncomingRequest(["x".repeat(300)]);
14
- await expect(readNextcloudTalkWebhookBody(req, 128)).rejects.toThrow("PayloadTooLarge");
15
- });
16
- });
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- looksLikeNextcloudTalkTargetId,
4
- normalizeNextcloudTalkMessagingTarget,
5
- stripNextcloudTalkTargetPrefix,
6
- } from "./normalize.js";
7
-
8
- describe("nextcloud-talk target normalization", () => {
9
- it("strips supported prefixes to a room token", () => {
10
- expect(stripNextcloudTalkTargetPrefix(" room:abc123 ")).toBe("abc123");
11
- expect(stripNextcloudTalkTargetPrefix("nextcloud-talk:room:AbC123")).toBe("AbC123");
12
- expect(stripNextcloudTalkTargetPrefix("nc-talk:room:ops")).toBe("ops");
13
- expect(stripNextcloudTalkTargetPrefix("nc:room:ops")).toBe("ops");
14
- expect(stripNextcloudTalkTargetPrefix("room: ")).toBeUndefined();
15
- });
16
-
17
- it("normalizes messaging targets to lowercase channel ids", () => {
18
- expect(normalizeNextcloudTalkMessagingTarget("room:AbC123")).toBe("nextcloud-talk:abc123");
19
- expect(normalizeNextcloudTalkMessagingTarget("nc-talk:room:Ops")).toBe("nextcloud-talk:ops");
20
- });
21
-
22
- it("detects prefixed and bare room ids", () => {
23
- expect(looksLikeNextcloudTalkTargetId("nextcloud-talk:room:abc12345")).toBe(true);
24
- expect(looksLikeNextcloudTalkTargetId("nc:opsroom1")).toBe(true);
25
- expect(looksLikeNextcloudTalkTargetId("abc12345")).toBe(true);
26
- expect(looksLikeNextcloudTalkTargetId("")).toBe(false);
27
- });
28
- });
package/src/onboarding.ts DELETED
@@ -1,302 +0,0 @@
1
- import {
2
- formatDocsLink,
3
- hasConfiguredSecretInput,
4
- mapAllowFromEntries,
5
- mergeAllowFromEntries,
6
- patchScopedAccountConfig,
7
- runSingleChannelSecretStep,
8
- resolveAccountIdForConfigure,
9
- DEFAULT_ACCOUNT_ID,
10
- normalizeAccountId,
11
- setTopLevelChannelDmPolicyWithAllowFrom,
12
- type ChannelOnboardingAdapter,
13
- type ChannelOnboardingDmPolicy,
14
- type OpenClawConfig,
15
- type WizardPrompter,
16
- } from "openclaw/plugin-sdk/nextcloud-talk";
17
- import {
18
- listNextcloudTalkAccountIds,
19
- resolveDefaultNextcloudTalkAccountId,
20
- resolveNextcloudTalkAccount,
21
- } from "./accounts.js";
22
- import type { CoreConfig, DmPolicy } from "./types.js";
23
-
24
- const channel = "nextcloud-talk" as const;
25
-
26
- function setNextcloudTalkDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
27
- return setTopLevelChannelDmPolicyWithAllowFrom({
28
- cfg,
29
- channel: "nextcloud-talk",
30
- dmPolicy,
31
- getAllowFrom: (inputCfg) =>
32
- mapAllowFromEntries(inputCfg.channels?.["nextcloud-talk"]?.allowFrom),
33
- }) as CoreConfig;
34
- }
35
-
36
- function setNextcloudTalkAccountConfig(
37
- cfg: CoreConfig,
38
- accountId: string,
39
- updates: Record<string, unknown>,
40
- ): CoreConfig {
41
- return patchScopedAccountConfig({
42
- cfg,
43
- channelKey: channel,
44
- accountId,
45
- patch: updates,
46
- }) as CoreConfig;
47
- }
48
-
49
- async function noteNextcloudTalkSecretHelp(prompter: WizardPrompter): Promise<void> {
50
- await prompter.note(
51
- [
52
- "1) SSH into your Nextcloud server",
53
- '2) Run: ./occ talk:bot:install "OpenClaw" "<shared-secret>" "<webhook-url>" --feature reaction',
54
- "3) Copy the shared secret you used in the command",
55
- "4) Enable the bot in your Nextcloud Talk room settings",
56
- "Tip: you can also set NEXTCLOUD_TALK_BOT_SECRET in your env.",
57
- `Docs: ${formatDocsLink("/channels/nextcloud-talk", "channels/nextcloud-talk")}`,
58
- ].join("\n"),
59
- "Nextcloud Talk bot setup",
60
- );
61
- }
62
-
63
- async function noteNextcloudTalkUserIdHelp(prompter: WizardPrompter): Promise<void> {
64
- await prompter.note(
65
- [
66
- "1) Check the Nextcloud admin panel for user IDs",
67
- "2) Or look at the webhook payload logs when someone messages",
68
- "3) User IDs are typically lowercase usernames in Nextcloud",
69
- `Docs: ${formatDocsLink("/channels/nextcloud-talk", "channels/nextcloud-talk")}`,
70
- ].join("\n"),
71
- "Nextcloud Talk user id",
72
- );
73
- }
74
-
75
- async function promptNextcloudTalkAllowFrom(params: {
76
- cfg: CoreConfig;
77
- prompter: WizardPrompter;
78
- accountId: string;
79
- }): Promise<CoreConfig> {
80
- const { cfg, prompter, accountId } = params;
81
- const resolved = resolveNextcloudTalkAccount({ cfg, accountId });
82
- const existingAllowFrom = resolved.config.allowFrom ?? [];
83
- await noteNextcloudTalkUserIdHelp(prompter);
84
-
85
- const parseInput = (value: string) =>
86
- value
87
- .split(/[\n,;]+/g)
88
- .map((entry) => entry.trim().toLowerCase())
89
- .filter(Boolean);
90
-
91
- let resolvedIds: string[] = [];
92
- while (resolvedIds.length === 0) {
93
- const entry = await prompter.text({
94
- message: "Nextcloud Talk allowFrom (user id)",
95
- placeholder: "username",
96
- initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
97
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
98
- });
99
- resolvedIds = parseInput(String(entry));
100
- if (resolvedIds.length === 0) {
101
- await prompter.note("Please enter at least one valid user ID.", "Nextcloud Talk allowlist");
102
- }
103
- }
104
-
105
- const merged = [
106
- ...existingAllowFrom.map((item) => String(item).trim().toLowerCase()).filter(Boolean),
107
- ...resolvedIds,
108
- ];
109
- const unique = mergeAllowFromEntries(undefined, merged);
110
-
111
- return setNextcloudTalkAccountConfig(cfg, accountId, {
112
- dmPolicy: "allowlist",
113
- allowFrom: unique,
114
- });
115
- }
116
-
117
- async function promptNextcloudTalkAllowFromForAccount(params: {
118
- cfg: CoreConfig;
119
- prompter: WizardPrompter;
120
- accountId?: string;
121
- }): Promise<CoreConfig> {
122
- const accountId =
123
- params.accountId && normalizeAccountId(params.accountId)
124
- ? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
125
- : resolveDefaultNextcloudTalkAccountId(params.cfg);
126
- return promptNextcloudTalkAllowFrom({
127
- cfg: params.cfg,
128
- prompter: params.prompter,
129
- accountId,
130
- });
131
- }
132
-
133
- const dmPolicy: ChannelOnboardingDmPolicy = {
134
- label: "Nextcloud Talk",
135
- channel,
136
- policyKey: "channels.nextcloud-talk.dmPolicy",
137
- allowFromKey: "channels.nextcloud-talk.allowFrom",
138
- getCurrent: (cfg) => cfg.channels?.["nextcloud-talk"]?.dmPolicy ?? "pairing",
139
- setPolicy: (cfg, policy) => setNextcloudTalkDmPolicy(cfg as CoreConfig, policy as DmPolicy),
140
- promptAllowFrom: promptNextcloudTalkAllowFromForAccount as (params: {
141
- cfg: OpenClawConfig;
142
- prompter: WizardPrompter;
143
- accountId?: string | undefined;
144
- }) => Promise<OpenClawConfig>,
145
- };
146
-
147
- export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
148
- channel,
149
- getStatus: async ({ cfg }) => {
150
- const configured = listNextcloudTalkAccountIds(cfg as CoreConfig).some((accountId) => {
151
- const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
152
- return Boolean(account.secret && account.baseUrl);
153
- });
154
- return {
155
- channel,
156
- configured,
157
- statusLines: [`Nextcloud Talk: ${configured ? "configured" : "needs setup"}`],
158
- selectionHint: configured ? "configured" : "self-hosted chat",
159
- quickstartScore: configured ? 1 : 5,
160
- };
161
- },
162
- configure: async ({
163
- cfg,
164
- prompter,
165
- accountOverrides,
166
- shouldPromptAccountIds,
167
- forceAllowFrom,
168
- }) => {
169
- const defaultAccountId = resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig);
170
- const accountId = await resolveAccountIdForConfigure({
171
- cfg,
172
- prompter,
173
- label: "Nextcloud Talk",
174
- accountOverride: accountOverrides["nextcloud-talk"],
175
- shouldPromptAccountIds,
176
- listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
177
- defaultAccountId,
178
- });
179
-
180
- let next = cfg as CoreConfig;
181
- const resolvedAccount = resolveNextcloudTalkAccount({
182
- cfg: next,
183
- accountId,
184
- });
185
- const accountConfigured = Boolean(resolvedAccount.secret && resolvedAccount.baseUrl);
186
- const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
187
- const hasConfigSecret = Boolean(
188
- hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
189
- resolvedAccount.config.botSecretFile,
190
- );
191
-
192
- let baseUrl = resolvedAccount.baseUrl;
193
- if (!baseUrl) {
194
- baseUrl = String(
195
- await prompter.text({
196
- message: "Enter Nextcloud instance URL (e.g., https://cloud.example.com)",
197
- validate: (value) => {
198
- const v = String(value ?? "").trim();
199
- if (!v) {
200
- return "Required";
201
- }
202
- if (!v.startsWith("http://") && !v.startsWith("https://")) {
203
- return "URL must start with http:// or https://";
204
- }
205
- return undefined;
206
- },
207
- }),
208
- ).trim();
209
- }
210
-
211
- const secretStep = await runSingleChannelSecretStep({
212
- cfg: next,
213
- prompter,
214
- providerHint: "nextcloud-talk",
215
- credentialLabel: "bot secret",
216
- accountConfigured,
217
- hasConfigToken: hasConfigSecret,
218
- allowEnv,
219
- envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
220
- envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
221
- keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
222
- inputPrompt: "Enter Nextcloud Talk bot secret",
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
- }),
234
- });
235
- next = secretStep.cfg as CoreConfig;
236
-
237
- if (secretStep.action === "keep" && baseUrl !== resolvedAccount.baseUrl) {
238
- next = setNextcloudTalkAccountConfig(next, accountId, {
239
- baseUrl,
240
- });
241
- }
242
-
243
- const existingApiUser = resolvedAccount.config.apiUser?.trim();
244
- const existingApiPasswordConfigured = Boolean(
245
- hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
246
- resolvedAccount.config.apiPasswordFile,
247
- );
248
- const configureApiCredentials = await prompter.confirm({
249
- message: "Configure optional Nextcloud Talk API credentials for room lookups?",
250
- initialValue: Boolean(existingApiUser && existingApiPasswordConfigured),
251
- });
252
- if (configureApiCredentials) {
253
- const apiUser = String(
254
- await prompter.text({
255
- message: "Nextcloud Talk API user",
256
- initialValue: existingApiUser,
257
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
258
- }),
259
- ).trim();
260
- const apiPasswordStep = await runSingleChannelSecretStep({
261
- cfg: next,
262
- prompter,
263
- providerHint: "nextcloud-talk-api",
264
- credentialLabel: "API password",
265
- accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
266
- hasConfigToken: existingApiPasswordConfigured,
267
- allowEnv: false,
268
- envPrompt: "",
269
- keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
270
- inputPrompt: "Enter Nextcloud Talk API password",
271
- preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
272
- applySet: async (cfg, value) =>
273
- setNextcloudTalkAccountConfig(cfg as CoreConfig, accountId, {
274
- apiUser,
275
- apiPassword: value,
276
- }),
277
- });
278
- next =
279
- apiPasswordStep.action === "keep"
280
- ? setNextcloudTalkAccountConfig(next, accountId, { apiUser })
281
- : (apiPasswordStep.cfg as CoreConfig);
282
- }
283
-
284
- if (forceAllowFrom) {
285
- next = await promptNextcloudTalkAllowFrom({
286
- cfg: next,
287
- prompter,
288
- accountId,
289
- });
290
- }
291
-
292
- return { cfg: next, accountId };
293
- },
294
- dmPolicy,
295
- disable: (cfg) => ({
296
- ...cfg,
297
- channels: {
298
- ...cfg.channels,
299
- "nextcloud-talk": { ...cfg.channels?.["nextcloud-talk"], enabled: false },
300
- },
301
- }),
302
- };