@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/send.ts CHANGED
@@ -1,16 +1,23 @@
1
- import { resolveNextcloudTalkAccount } from "./accounts.js";
2
1
  import { stripNextcloudTalkTargetPrefix } from "./normalize.js";
3
- import { getNextcloudTalkRuntime } from "./runtime.js";
4
- import { generateNextcloudTalkSignature } from "./signature.js";
2
+ import {
3
+ convertMarkdownTables,
4
+ fetchWithSsrFGuard,
5
+ generateNextcloudTalkSignature,
6
+ getNextcloudTalkRuntime,
7
+ requireRuntimeConfig,
8
+ resolveMarkdownTableMode,
9
+ resolveNextcloudTalkAccount,
10
+ ssrfPolicyFromPrivateNetworkOptIn,
11
+ } from "./send.runtime.js";
5
12
  import type { CoreConfig, NextcloudTalkSendResult } from "./types.js";
6
13
 
7
14
  type NextcloudTalkSendOpts = {
15
+ cfg: CoreConfig;
8
16
  baseUrl?: string;
9
17
  secret?: string;
10
18
  accountId?: string;
11
19
  replyTo?: string;
12
20
  verbose?: boolean;
13
- cfg?: CoreConfig;
14
21
  };
15
22
 
16
23
  function resolveCredentials(
@@ -48,7 +55,7 @@ function resolveNextcloudTalkSendContext(opts: NextcloudTalkSendOpts): {
48
55
  baseUrl: string;
49
56
  secret: string;
50
57
  } {
51
- const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
58
+ const cfg = requireRuntimeConfig(opts.cfg, "Nextcloud Talk send") as CoreConfig;
52
59
  const account = resolveNextcloudTalkAccount({
53
60
  cfg,
54
61
  accountId: opts.accountId,
@@ -60,10 +67,24 @@ function resolveNextcloudTalkSendContext(opts: NextcloudTalkSendOpts): {
60
67
  return { cfg, account, baseUrl, secret };
61
68
  }
62
69
 
70
+ function recordNextcloudTalkOutboundActivity(accountId: string): void {
71
+ try {
72
+ getNextcloudTalkRuntime().channel.activity.record({
73
+ channel: "nextcloud-talk",
74
+ accountId,
75
+ direction: "outbound",
76
+ });
77
+ } catch (error) {
78
+ if (!(error instanceof Error) || error.message !== "Nextcloud Talk runtime not initialized") {
79
+ throw error;
80
+ }
81
+ }
82
+ }
83
+
63
84
  export async function sendMessageNextcloudTalk(
64
85
  to: string,
65
86
  text: string,
66
- opts: NextcloudTalkSendOpts = {},
87
+ opts: NextcloudTalkSendOpts,
67
88
  ): Promise<NextcloudTalkSendResult> {
68
89
  const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
69
90
  const roomToken = normalizeRoomToken(to);
@@ -72,15 +93,12 @@ export async function sendMessageNextcloudTalk(
72
93
  throw new Error("Message must be non-empty for Nextcloud Talk sends");
73
94
  }
74
95
 
75
- const tableMode = getNextcloudTalkRuntime().channel.text.resolveMarkdownTableMode({
96
+ const tableMode = resolveMarkdownTableMode({
76
97
  cfg,
77
98
  channel: "nextcloud-talk",
78
99
  accountId: account.accountId,
79
100
  });
80
- const message = getNextcloudTalkRuntime().channel.text.convertMarkdownTables(
81
- text.trim(),
82
- tableMode,
83
- );
101
+ const message = convertMarkdownTables(text.trim(), tableMode);
84
102
 
85
103
  const body: Record<string, unknown> = {
86
104
  message,
@@ -101,76 +119,81 @@ export async function sendMessageNextcloudTalk(
101
119
 
102
120
  const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
103
121
 
104
- const response = await fetch(url, {
105
- method: "POST",
106
- headers: {
107
- "Content-Type": "application/json",
108
- "OCS-APIRequest": "true",
109
- "X-Nextcloud-Talk-Bot-Random": random,
110
- "X-Nextcloud-Talk-Bot-Signature": signature,
122
+ const { response, release } = await fetchWithSsrFGuard({
123
+ url,
124
+ init: {
125
+ method: "POST",
126
+ headers: {
127
+ "Content-Type": "application/json",
128
+ "OCS-APIRequest": "true",
129
+ "X-Nextcloud-Talk-Bot-Random": random,
130
+ "X-Nextcloud-Talk-Bot-Signature": signature,
131
+ },
132
+ body: bodyStr,
111
133
  },
112
- body: bodyStr,
134
+ auditContext: "nextcloud-talk-send",
135
+ policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
113
136
  });
114
137
 
115
- if (!response.ok) {
116
- const errorBody = await response.text().catch(() => "");
117
- const status = response.status;
118
- let errorMsg = `Nextcloud Talk send failed (${status})`;
119
-
120
- if (status === 400) {
121
- errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
122
- } else if (status === 401) {
123
- errorMsg = "Nextcloud Talk: authentication failed - check bot secret";
124
- } else if (status === 403) {
125
- errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room";
126
- } else if (status === 404) {
127
- errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`;
128
- } else if (errorBody) {
129
- errorMsg = `Nextcloud Talk send failed: ${errorBody}`;
138
+ try {
139
+ if (!response.ok) {
140
+ const errorBody = await response.text().catch(() => "");
141
+ const status = response.status;
142
+ let errorMsg = `Nextcloud Talk send failed (${status})`;
143
+
144
+ if (status === 400) {
145
+ errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
146
+ } else if (status === 401) {
147
+ errorMsg = "Nextcloud Talk: authentication failed - check bot secret";
148
+ } else if (status === 403) {
149
+ errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room";
150
+ } else if (status === 404) {
151
+ errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`;
152
+ } else if (errorBody) {
153
+ errorMsg = `Nextcloud Talk send failed: ${errorBody}`;
154
+ }
155
+
156
+ throw new Error(errorMsg);
130
157
  }
131
158
 
132
- throw new Error(errorMsg);
133
- }
134
-
135
- let messageId = "unknown";
136
- let timestamp: number | undefined;
137
- try {
138
- const data = (await response.json()) as {
139
- ocs?: {
140
- data?: {
141
- id?: number | string;
142
- timestamp?: number;
159
+ let messageId = "unknown";
160
+ let timestamp: number | undefined;
161
+ try {
162
+ const data = (await response.json()) as {
163
+ ocs?: {
164
+ data?: {
165
+ id?: number | string;
166
+ timestamp?: number;
167
+ };
143
168
  };
144
169
  };
145
- };
146
- if (data.ocs?.data?.id != null) {
147
- messageId = String(data.ocs.data.id);
148
- }
149
- if (typeof data.ocs?.data?.timestamp === "number") {
150
- timestamp = data.ocs.data.timestamp;
170
+ if (data.ocs?.data?.id != null) {
171
+ messageId = String(data.ocs.data.id);
172
+ }
173
+ if (typeof data.ocs?.data?.timestamp === "number") {
174
+ timestamp = data.ocs.data.timestamp;
175
+ }
176
+ } catch {
177
+ // Response parsing failed, but message was sent.
151
178
  }
152
- } catch {
153
- // Response parsing failed, but message was sent.
154
- }
155
179
 
156
- if (opts.verbose) {
157
- console.log(`[nextcloud-talk] Sent message ${messageId} to room ${roomToken}`);
158
- }
180
+ if (opts.verbose) {
181
+ console.log(`[nextcloud-talk] Sent message ${messageId} to room ${roomToken}`);
182
+ }
159
183
 
160
- getNextcloudTalkRuntime().channel.activity.record({
161
- channel: "nextcloud-talk",
162
- accountId: account.accountId,
163
- direction: "outbound",
164
- });
184
+ recordNextcloudTalkOutboundActivity(account.accountId);
165
185
 
166
- return { messageId, roomToken, timestamp };
186
+ return { messageId, roomToken, timestamp };
187
+ } finally {
188
+ await release();
189
+ }
167
190
  }
168
191
 
169
192
  export async function sendReactionNextcloudTalk(
170
193
  roomToken: string,
171
194
  messageId: string,
172
195
  reaction: string,
173
- opts: Omit<NextcloudTalkSendOpts, "replyTo"> = {},
196
+ opts: Omit<NextcloudTalkSendOpts, "replyTo">,
174
197
  ): Promise<{ ok: true }> {
175
198
  const { account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
176
199
  const normalizedToken = normalizeRoomToken(roomToken);
@@ -184,21 +207,30 @@ export async function sendReactionNextcloudTalk(
184
207
 
185
208
  const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
186
209
 
187
- const response = await fetch(url, {
188
- method: "POST",
189
- headers: {
190
- "Content-Type": "application/json",
191
- "OCS-APIRequest": "true",
192
- "X-Nextcloud-Talk-Bot-Random": random,
193
- "X-Nextcloud-Talk-Bot-Signature": signature,
210
+ const { response, release } = await fetchWithSsrFGuard({
211
+ url,
212
+ init: {
213
+ method: "POST",
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ "OCS-APIRequest": "true",
217
+ "X-Nextcloud-Talk-Bot-Random": random,
218
+ "X-Nextcloud-Talk-Bot-Signature": signature,
219
+ },
220
+ body,
194
221
  },
195
- body,
222
+ auditContext: "nextcloud-talk-reaction",
223
+ policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
196
224
  });
197
225
 
198
- if (!response.ok) {
199
- const errorBody = await response.text().catch(() => "");
200
- throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
201
- }
226
+ try {
227
+ if (!response.ok) {
228
+ const errorBody = await response.text().catch(() => "");
229
+ throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
230
+ }
202
231
 
203
- return { ok: true };
232
+ return { ok: true };
233
+ } finally {
234
+ await release();
235
+ }
204
236
  }
@@ -0,0 +1,40 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
+ import { buildOutboundBaseSessionKey } from "openclaw/plugin-sdk/routing";
3
+ import { stripNextcloudTalkTargetPrefix } from "./normalize.js";
4
+
5
+ type NextcloudTalkOutboundSessionRouteParams = {
6
+ cfg: OpenClawConfig;
7
+ agentId: string;
8
+ accountId?: string | null;
9
+ target: string;
10
+ };
11
+
12
+ export function resolveNextcloudTalkOutboundSessionRoute(
13
+ params: NextcloudTalkOutboundSessionRouteParams,
14
+ ) {
15
+ const roomId = stripNextcloudTalkTargetPrefix(params.target);
16
+ if (!roomId) {
17
+ return null;
18
+ }
19
+ const baseSessionKey = buildOutboundBaseSessionKey({
20
+ cfg: params.cfg,
21
+ agentId: params.agentId,
22
+ channel: "nextcloud-talk",
23
+ accountId: params.accountId,
24
+ peer: {
25
+ kind: "group",
26
+ id: roomId,
27
+ },
28
+ });
29
+ return {
30
+ sessionKey: baseSessionKey,
31
+ baseSessionKey,
32
+ peer: {
33
+ kind: "group" as const,
34
+ id: roomId,
35
+ },
36
+ chatType: "group" as const,
37
+ from: `nextcloud-talk:room:${roomId}`,
38
+ to: `nextcloud-talk:${roomId}`,
39
+ };
40
+ }
@@ -0,0 +1,248 @@
1
+ import type { ChannelSetupAdapter, ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
3
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
4
+ import {
5
+ applyAccountNameToChannelSection,
6
+ patchScopedAccountConfig,
7
+ } from "openclaw/plugin-sdk/setup";
8
+ import {
9
+ createSetupInputPresenceValidator,
10
+ mergeAllowFromEntries,
11
+ promptParsedAllowFromForAccount,
12
+ resolveSetupAccountId,
13
+ type ChannelSetupDmPolicy,
14
+ type WizardPrompter,
15
+ } from "openclaw/plugin-sdk/setup-runtime";
16
+ import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
17
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
18
+ import { resolveDefaultNextcloudTalkAccountId, resolveNextcloudTalkAccount } from "./accounts.js";
19
+ import type { CoreConfig } from "./types.js";
20
+
21
+ const channel = "nextcloud-talk" as const;
22
+
23
+ type NextcloudSetupInput = ChannelSetupInput & {
24
+ baseUrl?: string;
25
+ secret?: string;
26
+ secretFile?: string;
27
+ };
28
+ type NextcloudTalkSection = NonNullable<CoreConfig["channels"]>["nextcloud-talk"];
29
+
30
+ function addWildcardAllowFrom(allowFrom?: Array<string | number> | null): string[] {
31
+ return mergeAllowFromEntries(allowFrom, ["*"]);
32
+ }
33
+
34
+ export function normalizeNextcloudTalkBaseUrl(value: string | undefined): string {
35
+ return value?.trim().replace(/\/+$/, "") ?? "";
36
+ }
37
+
38
+ export function validateNextcloudTalkBaseUrl(value: string): string | undefined {
39
+ if (!value) {
40
+ return "Required";
41
+ }
42
+ if (!value.startsWith("http://") && !value.startsWith("https://")) {
43
+ return "URL must start with http:// or https://";
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ export function setNextcloudTalkAccountConfig(
49
+ cfg: CoreConfig,
50
+ accountId: string,
51
+ updates: Record<string, unknown>,
52
+ ): CoreConfig {
53
+ return patchScopedAccountConfig({
54
+ cfg,
55
+ channelKey: channel,
56
+ accountId,
57
+ patch: updates,
58
+ }) as CoreConfig;
59
+ }
60
+
61
+ export function clearNextcloudTalkAccountFields(
62
+ cfg: CoreConfig,
63
+ accountId: string,
64
+ fields: string[],
65
+ ): CoreConfig {
66
+ const section = cfg.channels?.["nextcloud-talk"];
67
+ if (!section) {
68
+ return cfg;
69
+ }
70
+
71
+ if (accountId === DEFAULT_ACCOUNT_ID) {
72
+ const nextSection = { ...section } as Record<string, unknown>;
73
+ for (const field of fields) {
74
+ delete nextSection[field];
75
+ }
76
+ return {
77
+ ...cfg,
78
+ channels: {
79
+ ...cfg.channels,
80
+ "nextcloud-talk": nextSection as NextcloudTalkSection,
81
+ },
82
+ } as CoreConfig;
83
+ }
84
+
85
+ const currentAccount = section.accounts?.[accountId];
86
+ if (!currentAccount) {
87
+ return cfg;
88
+ }
89
+
90
+ const nextAccount = { ...currentAccount } as Record<string, unknown>;
91
+ for (const field of fields) {
92
+ delete nextAccount[field];
93
+ }
94
+ return {
95
+ ...cfg,
96
+ channels: {
97
+ ...cfg.channels,
98
+ "nextcloud-talk": {
99
+ ...section,
100
+ accounts: {
101
+ ...section.accounts,
102
+ [accountId]: nextAccount as NonNullable<typeof section.accounts>[string],
103
+ },
104
+ },
105
+ },
106
+ } as CoreConfig;
107
+ }
108
+
109
+ async function promptNextcloudTalkAllowFrom(params: {
110
+ cfg: CoreConfig;
111
+ prompter: WizardPrompter;
112
+ accountId: string;
113
+ }): Promise<CoreConfig> {
114
+ return await promptParsedAllowFromForAccount({
115
+ cfg: params.cfg,
116
+ accountId: params.accountId,
117
+ defaultAccountId: params.accountId,
118
+ prompter: params.prompter,
119
+ noteTitle: "Nextcloud Talk user id",
120
+ noteLines: [
121
+ "1) Check the Nextcloud admin panel for user IDs",
122
+ "2) Or look at the webhook payload logs when someone messages",
123
+ "3) User IDs are typically lowercase usernames in Nextcloud",
124
+ `Docs: ${formatDocsLink("/channels/nextcloud-talk", "nextcloud-talk")}`,
125
+ ],
126
+ message: "Nextcloud Talk allowFrom (user id)",
127
+ placeholder: "username",
128
+ parseEntries: (raw) => ({
129
+ entries: raw
130
+ .split(/[\n,;]+/g)
131
+ .map(normalizeLowercaseStringOrEmpty)
132
+ .filter(Boolean),
133
+ }),
134
+ getExistingAllowFrom: ({ cfg, accountId }) =>
135
+ resolveNextcloudTalkAccount({ cfg, accountId }).config.allowFrom ?? [],
136
+ mergeEntries: ({ existing, parsed }) =>
137
+ mergeAllowFromEntries(
138
+ existing.map((value) => normalizeLowercaseStringOrEmpty(String(value))),
139
+ parsed,
140
+ ),
141
+ applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
142
+ setNextcloudTalkAccountConfig(cfg, accountId, {
143
+ dmPolicy: "allowlist",
144
+ allowFrom,
145
+ }),
146
+ });
147
+ }
148
+
149
+ async function promptNextcloudTalkAllowFromForAccount(params: {
150
+ cfg: OpenClawConfig;
151
+ prompter: WizardPrompter;
152
+ accountId?: string;
153
+ }): Promise<OpenClawConfig> {
154
+ const accountId = resolveSetupAccountId({
155
+ accountId: params.accountId,
156
+ defaultAccountId: resolveDefaultNextcloudTalkAccountId(params.cfg as CoreConfig),
157
+ });
158
+ return await promptNextcloudTalkAllowFrom({
159
+ cfg: params.cfg as CoreConfig,
160
+ prompter: params.prompter,
161
+ accountId,
162
+ });
163
+ }
164
+
165
+ export const nextcloudTalkDmPolicy: ChannelSetupDmPolicy = {
166
+ label: "Nextcloud Talk",
167
+ channel,
168
+ policyKey: "channels.nextcloud-talk.dmPolicy",
169
+ allowFromKey: "channels.nextcloud-talk.allowFrom",
170
+ resolveConfigKeys: (cfg, accountId) =>
171
+ (accountId ?? resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig)) !== DEFAULT_ACCOUNT_ID
172
+ ? {
173
+ policyKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig)}.dmPolicy`,
174
+ allowFromKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig)}.allowFrom`,
175
+ }
176
+ : {
177
+ policyKey: "channels.nextcloud-talk.dmPolicy",
178
+ allowFromKey: "channels.nextcloud-talk.allowFrom",
179
+ },
180
+ getCurrent: (cfg, accountId) =>
181
+ resolveNextcloudTalkAccount({
182
+ cfg: cfg as CoreConfig,
183
+ accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig),
184
+ }).config.dmPolicy ?? "pairing",
185
+ setPolicy: (cfg, policy, accountId) => {
186
+ const resolvedAccountId = accountId ?? resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig);
187
+ const resolved = resolveNextcloudTalkAccount({
188
+ cfg: cfg as CoreConfig,
189
+ accountId: resolvedAccountId,
190
+ });
191
+ return setNextcloudTalkAccountConfig(cfg as CoreConfig, resolvedAccountId, {
192
+ dmPolicy: policy,
193
+ ...(policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}),
194
+ });
195
+ },
196
+ promptAllowFrom: promptNextcloudTalkAllowFromForAccount,
197
+ };
198
+
199
+ export const nextcloudTalkSetupAdapter: ChannelSetupAdapter = {
200
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
201
+ applyAccountName: ({ cfg, accountId, name }) =>
202
+ applyAccountNameToChannelSection({
203
+ cfg,
204
+ channelKey: channel,
205
+ accountId,
206
+ name,
207
+ }),
208
+ validateInput: createSetupInputPresenceValidator({
209
+ defaultAccountOnlyEnvError:
210
+ "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
211
+ validate: ({ input }) => {
212
+ const setupInput = input as NextcloudSetupInput;
213
+ if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) {
214
+ return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
215
+ }
216
+ if (!setupInput.baseUrl) {
217
+ return "Nextcloud Talk requires --base-url.";
218
+ }
219
+ return null;
220
+ },
221
+ }),
222
+ applyAccountConfig: ({ cfg, accountId, input }) => {
223
+ const setupInput = input as NextcloudSetupInput;
224
+ const namedConfig = applyAccountNameToChannelSection({
225
+ cfg,
226
+ channelKey: channel,
227
+ accountId,
228
+ name: setupInput.name,
229
+ });
230
+ const next = setupInput.useEnv
231
+ ? clearNextcloudTalkAccountFields(namedConfig as CoreConfig, accountId, [
232
+ "botSecret",
233
+ "botSecretFile",
234
+ ])
235
+ : namedConfig;
236
+ const patch = {
237
+ baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
238
+ ...(setupInput.useEnv
239
+ ? {}
240
+ : setupInput.secretFile
241
+ ? { botSecretFile: setupInput.secretFile }
242
+ : setupInput.secret
243
+ ? { botSecret: setupInput.secret }
244
+ : {}),
245
+ };
246
+ return setNextcloudTalkAccountConfig(next as CoreConfig, accountId, patch);
247
+ },
248
+ };