@openclaw/nextcloud-talk 2026.3.12 → 2026.5.1-beta.2
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/api.ts +1 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +4 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +804 -1
- package/package.json +31 -4
- package/runtime-api.ts +33 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.ts +25 -42
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +27 -0
- package/src/channel-api.ts +5 -0
- package/src/channel.adapters.ts +52 -0
- package/src/channel.core.test.ts +75 -0
- package/src/channel.lifecycle.test.ts +81 -0
- package/src/channel.ts +157 -388
- package/src/config-schema.ts +27 -22
- package/src/core.test.ts +397 -0
- package/src/doctor-contract.ts +9 -0
- package/src/doctor.test.ts +40 -0
- package/src/doctor.ts +10 -0
- package/src/gateway.ts +109 -0
- package/src/inbound.authz.test.ts +87 -22
- package/src/inbound.behavior.test.ts +202 -0
- package/src/inbound.ts +28 -26
- package/src/monitor-runtime.ts +138 -0
- package/src/monitor.replay.test.ts +238 -0
- package/src/monitor.test-harness.ts +2 -2
- package/src/monitor.ts +125 -155
- package/src/normalize.ts +7 -2
- package/src/policy.ts +23 -31
- package/src/replay-guard.ts +74 -11
- package/src/room-info.test.ts +116 -0
- package/src/room-info.ts +11 -3
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +103 -0
- package/src/secret-input.ts +1 -10
- package/src/send.cfg-threading.test.ts +153 -0
- package/src/send.runtime.ts +8 -0
- package/src/send.ts +126 -106
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +248 -0
- package/src/setup-surface.ts +190 -0
- package/src/setup.test.ts +422 -0
- package/src/signature.ts +20 -10
- package/src/types.ts +19 -16
- package/tsconfig.json +16 -0
- package/src/accounts.test.ts +0 -30
- package/src/channel.startup.test.ts +0 -83
- package/src/config-schema.test.ts +0 -36
- package/src/format.ts +0 -79
- package/src/monitor.auth-order.test.ts +0 -28
- package/src/monitor.backend.test.ts +0 -27
- package/src/monitor.read-body.test.ts +0 -16
- package/src/onboarding.ts +0 -302
- package/src/policy.test.ts +0 -138
- package/src/replay-guard.test.ts +0 -70
- package/src/send.test.ts +0 -104
package/src/send.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { stripNextcloudTalkTargetPrefix } from "./normalize.js";
|
|
2
|
+
import {
|
|
3
|
+
convertMarkdownTables,
|
|
4
|
+
fetchWithSsrFGuard,
|
|
5
|
+
generateNextcloudTalkSignature,
|
|
6
|
+
getNextcloudTalkRuntime,
|
|
7
|
+
requireRuntimeConfig,
|
|
8
|
+
resolveMarkdownTableMode,
|
|
9
|
+
resolveNextcloudTalkAccount,
|
|
10
|
+
ssrfPolicyFromPrivateNetworkOptIn,
|
|
11
|
+
} from "./send.runtime.js";
|
|
4
12
|
import type { CoreConfig, NextcloudTalkSendResult } from "./types.js";
|
|
5
13
|
|
|
6
14
|
type NextcloudTalkSendOpts = {
|
|
15
|
+
cfg: CoreConfig;
|
|
7
16
|
baseUrl?: string;
|
|
8
17
|
secret?: string;
|
|
9
18
|
accountId?: string;
|
|
10
19
|
replyTo?: string;
|
|
11
20
|
verbose?: boolean;
|
|
12
|
-
cfg?: CoreConfig;
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
function resolveCredentials(
|
|
@@ -34,34 +42,20 @@ function resolveCredentials(
|
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
function normalizeRoomToken(to: string): string {
|
|
37
|
-
const
|
|
38
|
-
if (!trimmed) {
|
|
39
|
-
throw new Error("Room token is required for Nextcloud Talk sends");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let normalized = trimmed;
|
|
43
|
-
if (normalized.startsWith("nextcloud-talk:")) {
|
|
44
|
-
normalized = normalized.slice("nextcloud-talk:".length).trim();
|
|
45
|
-
} else if (normalized.startsWith("nc:")) {
|
|
46
|
-
normalized = normalized.slice("nc:".length).trim();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (normalized.startsWith("room:")) {
|
|
50
|
-
normalized = normalized.slice("room:".length).trim();
|
|
51
|
-
}
|
|
52
|
-
|
|
45
|
+
const normalized = stripNextcloudTalkTargetPrefix(to);
|
|
53
46
|
if (!normalized) {
|
|
54
47
|
throw new Error("Room token is required for Nextcloud Talk sends");
|
|
55
48
|
}
|
|
56
49
|
return normalized;
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
function resolveNextcloudTalkSendContext(opts: NextcloudTalkSendOpts): {
|
|
53
|
+
cfg: CoreConfig;
|
|
54
|
+
account: ReturnType<typeof resolveNextcloudTalkAccount>;
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
secret: string;
|
|
57
|
+
} {
|
|
58
|
+
const cfg = requireRuntimeConfig(opts.cfg, "Nextcloud Talk send") as CoreConfig;
|
|
65
59
|
const account = resolveNextcloudTalkAccount({
|
|
66
60
|
cfg,
|
|
67
61
|
accountId: opts.accountId,
|
|
@@ -70,21 +64,41 @@ export async function sendMessageNextcloudTalk(
|
|
|
70
64
|
{ baseUrl: opts.baseUrl, secret: opts.secret },
|
|
71
65
|
account,
|
|
72
66
|
);
|
|
67
|
+
return { cfg, account, baseUrl, secret };
|
|
68
|
+
}
|
|
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
|
+
|
|
84
|
+
export async function sendMessageNextcloudTalk(
|
|
85
|
+
to: string,
|
|
86
|
+
text: string,
|
|
87
|
+
opts: NextcloudTalkSendOpts,
|
|
88
|
+
): Promise<NextcloudTalkSendResult> {
|
|
89
|
+
const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
|
|
73
90
|
const roomToken = normalizeRoomToken(to);
|
|
74
91
|
|
|
75
92
|
if (!text?.trim()) {
|
|
76
93
|
throw new Error("Message must be non-empty for Nextcloud Talk sends");
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
const tableMode =
|
|
96
|
+
const tableMode = resolveMarkdownTableMode({
|
|
80
97
|
cfg,
|
|
81
98
|
channel: "nextcloud-talk",
|
|
82
99
|
accountId: account.accountId,
|
|
83
100
|
});
|
|
84
|
-
const message =
|
|
85
|
-
text.trim(),
|
|
86
|
-
tableMode,
|
|
87
|
-
);
|
|
101
|
+
const message = convertMarkdownTables(text.trim(), tableMode);
|
|
88
102
|
|
|
89
103
|
const body: Record<string, unknown> = {
|
|
90
104
|
message,
|
|
@@ -105,86 +119,83 @@ export async function sendMessageNextcloudTalk(
|
|
|
105
119
|
|
|
106
120
|
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
|
|
107
121
|
|
|
108
|
-
const response = await
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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,
|
|
115
133
|
},
|
|
116
|
-
|
|
134
|
+
auditContext: "nextcloud-talk-send",
|
|
135
|
+
policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
|
|
117
136
|
});
|
|
118
137
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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);
|
|
134
157
|
}
|
|
135
158
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
id?: number | string;
|
|
146
|
-
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
|
+
};
|
|
147
168
|
};
|
|
148
169
|
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
152
178
|
}
|
|
153
|
-
if (typeof data.ocs?.data?.timestamp === "number") {
|
|
154
|
-
timestamp = data.ocs.data.timestamp;
|
|
155
|
-
}
|
|
156
|
-
} catch {
|
|
157
|
-
// Response parsing failed, but message was sent.
|
|
158
|
-
}
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
if (opts.verbose) {
|
|
181
|
+
console.log(`[nextcloud-talk] Sent message ${messageId} to room ${roomToken}`);
|
|
182
|
+
}
|
|
163
183
|
|
|
164
|
-
|
|
165
|
-
channel: "nextcloud-talk",
|
|
166
|
-
accountId: account.accountId,
|
|
167
|
-
direction: "outbound",
|
|
168
|
-
});
|
|
184
|
+
recordNextcloudTalkOutboundActivity(account.accountId);
|
|
169
185
|
|
|
170
|
-
|
|
186
|
+
return { messageId, roomToken, timestamp };
|
|
187
|
+
} finally {
|
|
188
|
+
await release();
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
export async function sendReactionNextcloudTalk(
|
|
174
193
|
roomToken: string,
|
|
175
194
|
messageId: string,
|
|
176
195
|
reaction: string,
|
|
177
|
-
opts: Omit<NextcloudTalkSendOpts, "replyTo"
|
|
196
|
+
opts: Omit<NextcloudTalkSendOpts, "replyTo">,
|
|
178
197
|
): Promise<{ ok: true }> {
|
|
179
|
-
const
|
|
180
|
-
const account = resolveNextcloudTalkAccount({
|
|
181
|
-
cfg,
|
|
182
|
-
accountId: opts.accountId,
|
|
183
|
-
});
|
|
184
|
-
const { baseUrl, secret } = resolveCredentials(
|
|
185
|
-
{ baseUrl: opts.baseUrl, secret: opts.secret },
|
|
186
|
-
account,
|
|
187
|
-
);
|
|
198
|
+
const { account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
|
|
188
199
|
const normalizedToken = normalizeRoomToken(roomToken);
|
|
189
200
|
|
|
190
201
|
const body = JSON.stringify({ reaction });
|
|
@@ -196,21 +207,30 @@ export async function sendReactionNextcloudTalk(
|
|
|
196
207
|
|
|
197
208
|
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
|
|
198
209
|
|
|
199
|
-
const response = await
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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,
|
|
206
221
|
},
|
|
207
|
-
|
|
222
|
+
auditContext: "nextcloud-talk-reaction",
|
|
223
|
+
policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
|
|
208
224
|
});
|
|
209
225
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
}
|
|
214
231
|
|
|
215
|
-
|
|
232
|
+
return { ok: true };
|
|
233
|
+
} finally {
|
|
234
|
+
await release();
|
|
235
|
+
}
|
|
216
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
|
+
};
|