@openclaw/nextcloud-talk 2026.2.2 → 2026.2.9
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/config-schema.ts +1 -0
- package/src/inbound.ts +26 -11
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +7 -2
- package/src/policy.test.ts +33 -0
- package/src/policy.ts +1 -9
- package/src/send.ts +7 -2
- package/src/types.ts +4 -0
package/package.json
CHANGED
package/src/config-schema.ts
CHANGED
|
@@ -47,6 +47,7 @@ export const NextcloudTalkAccountSchemaBase = z
|
|
|
47
47
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
48
48
|
blockStreaming: z.boolean().optional(),
|
|
49
49
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
50
|
+
responsePrefix: z.string().optional(),
|
|
50
51
|
mediaMaxMb: z.number().positive().optional(),
|
|
51
52
|
})
|
|
52
53
|
.strict();
|
package/src/inbound.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createReplyPrefixOptions,
|
|
2
3
|
logInboundDrop,
|
|
3
4
|
resolveControlCommandGate,
|
|
4
5
|
type OpenClawConfig,
|
|
5
6
|
type RuntimeEnv,
|
|
6
7
|
} from "openclaw/plugin-sdk";
|
|
7
8
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
8
|
-
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
|
|
9
|
+
import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js";
|
|
9
10
|
import {
|
|
10
11
|
normalizeNextcloudTalkAllowlist,
|
|
11
12
|
resolveNextcloudTalkAllowlistMatch,
|
|
@@ -83,8 +84,12 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
83
84
|
statusSink?.({ lastInboundAt: message.timestamp });
|
|
84
85
|
|
|
85
86
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
86
|
-
const defaultGroupPolicy = config.channels?.defaults
|
|
87
|
-
|
|
87
|
+
const defaultGroupPolicy = (config.channels as Record<string, unknown> | undefined)?.defaults as
|
|
88
|
+
| { groupPolicy?: string }
|
|
89
|
+
| undefined;
|
|
90
|
+
const groupPolicy = (account.config.groupPolicy ??
|
|
91
|
+
defaultGroupPolicy?.groupPolicy ??
|
|
92
|
+
"allowlist") as GroupPolicy;
|
|
88
93
|
|
|
89
94
|
const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
|
|
90
95
|
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
|
|
@@ -117,11 +122,11 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
117
122
|
cfg: config as OpenClawConfig,
|
|
118
123
|
surface: CHANNEL_ID,
|
|
119
124
|
});
|
|
120
|
-
const useAccessGroups =
|
|
125
|
+
const useAccessGroups =
|
|
126
|
+
(config.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
|
|
121
127
|
const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({
|
|
122
128
|
allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom,
|
|
123
129
|
senderId,
|
|
124
|
-
senderName,
|
|
125
130
|
}).allowed;
|
|
126
131
|
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig);
|
|
127
132
|
const commandGate = resolveControlCommandGate({
|
|
@@ -143,7 +148,6 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
143
148
|
outerAllowFrom: effectiveGroupAllowFrom,
|
|
144
149
|
innerAllowFrom: roomAllowFrom,
|
|
145
150
|
senderId,
|
|
146
|
-
senderName,
|
|
147
151
|
});
|
|
148
152
|
if (!groupAllow.allowed) {
|
|
149
153
|
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`);
|
|
@@ -158,7 +162,6 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
158
162
|
const dmAllowed = resolveNextcloudTalkAllowlistMatch({
|
|
159
163
|
allowFrom: effectiveAllowFrom,
|
|
160
164
|
senderId,
|
|
161
|
-
senderName,
|
|
162
165
|
}).allowed;
|
|
163
166
|
if (!dmAllowed) {
|
|
164
167
|
if (dmPolicy === "pairing") {
|
|
@@ -230,15 +233,18 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
230
233
|
channel: CHANNEL_ID,
|
|
231
234
|
accountId: account.accountId,
|
|
232
235
|
peer: {
|
|
233
|
-
kind: isGroup ? "group" : "
|
|
236
|
+
kind: isGroup ? "group" : "direct",
|
|
234
237
|
id: isGroup ? roomToken : senderId,
|
|
235
238
|
},
|
|
236
239
|
});
|
|
237
240
|
|
|
238
241
|
const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
|
|
239
|
-
const storePath = core.channel.session.resolveStorePath(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
const storePath = core.channel.session.resolveStorePath(
|
|
243
|
+
(config.session as Record<string, unknown> | undefined)?.store as string | undefined,
|
|
244
|
+
{
|
|
245
|
+
agentId: route.agentId,
|
|
246
|
+
},
|
|
247
|
+
);
|
|
242
248
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config as OpenClawConfig);
|
|
243
249
|
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
244
250
|
storePath,
|
|
@@ -288,10 +294,18 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
288
294
|
},
|
|
289
295
|
});
|
|
290
296
|
|
|
297
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
298
|
+
cfg: config as OpenClawConfig,
|
|
299
|
+
agentId: route.agentId,
|
|
300
|
+
channel: CHANNEL_ID,
|
|
301
|
+
accountId: account.accountId,
|
|
302
|
+
});
|
|
303
|
+
|
|
291
304
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
292
305
|
ctx: ctxPayload,
|
|
293
306
|
cfg: config as OpenClawConfig,
|
|
294
307
|
dispatcherOptions: {
|
|
308
|
+
...prefixOptions,
|
|
295
309
|
deliver: async (payload) => {
|
|
296
310
|
await deliverNextcloudTalkReply({
|
|
297
311
|
payload: payload as {
|
|
@@ -311,6 +325,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
311
325
|
},
|
|
312
326
|
replyOptions: {
|
|
313
327
|
skillFilter: roomConfig?.skills,
|
|
328
|
+
onModelSelected,
|
|
314
329
|
disableBlockStreaming:
|
|
315
330
|
typeof account.config.blockStreaming === "boolean"
|
|
316
331
|
? !account.config.blockStreaming
|
package/src/monitor.ts
CHANGED
|
@@ -54,7 +54,7 @@ function payloadToInboundMessage(
|
|
|
54
54
|
roomToken: payload.target.id,
|
|
55
55
|
roomName: payload.target.name,
|
|
56
56
|
senderId: payload.actor.id,
|
|
57
|
-
senderName: payload.actor.name,
|
|
57
|
+
senderName: payload.actor.name ?? "",
|
|
58
58
|
text: payload.object.content || payload.object.name || "",
|
|
59
59
|
mediaType: payload.object.mediaType || "text/plain",
|
|
60
60
|
timestamp: Date.now(),
|
package/src/onboarding.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
normalizeAccountId,
|
|
7
7
|
type ChannelOnboardingAdapter,
|
|
8
8
|
type ChannelOnboardingDmPolicy,
|
|
9
|
+
type OpenClawConfig,
|
|
9
10
|
type WizardPrompter,
|
|
10
11
|
} from "openclaw/plugin-sdk";
|
|
11
12
|
import type { CoreConfig, DmPolicy } from "./types.js";
|
|
@@ -159,7 +160,11 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
159
160
|
allowFromKey: "channels.nextcloud-talk.allowFrom",
|
|
160
161
|
getCurrent: (cfg) => cfg.channels?.["nextcloud-talk"]?.dmPolicy ?? "pairing",
|
|
161
162
|
setPolicy: (cfg, policy) => setNextcloudTalkDmPolicy(cfg as CoreConfig, policy as DmPolicy),
|
|
162
|
-
promptAllowFrom: promptNextcloudTalkAllowFromForAccount
|
|
163
|
+
promptAllowFrom: promptNextcloudTalkAllowFromForAccount as (params: {
|
|
164
|
+
cfg: OpenClawConfig;
|
|
165
|
+
prompter: WizardPrompter;
|
|
166
|
+
accountId?: string | undefined;
|
|
167
|
+
}) => Promise<OpenClawConfig>,
|
|
163
168
|
};
|
|
164
169
|
|
|
165
170
|
export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
@@ -196,7 +201,7 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
196
201
|
prompter,
|
|
197
202
|
label: "Nextcloud Talk",
|
|
198
203
|
currentId: accountId,
|
|
199
|
-
listAccountIds: listNextcloudTalkAccountIds,
|
|
204
|
+
listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
|
|
200
205
|
defaultAccountId,
|
|
201
206
|
});
|
|
202
207
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveNextcloudTalkAllowlistMatch } from "./policy.js";
|
|
3
|
+
|
|
4
|
+
describe("nextcloud-talk policy", () => {
|
|
5
|
+
describe("resolveNextcloudTalkAllowlistMatch", () => {
|
|
6
|
+
it("allows wildcard", () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveNextcloudTalkAllowlistMatch({
|
|
9
|
+
allowFrom: ["*"],
|
|
10
|
+
senderId: "user-id",
|
|
11
|
+
}).allowed,
|
|
12
|
+
).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("allows sender id match with normalization", () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveNextcloudTalkAllowlistMatch({
|
|
18
|
+
allowFrom: ["nc:User-Id"],
|
|
19
|
+
senderId: "user-id",
|
|
20
|
+
}),
|
|
21
|
+
).toEqual({ allowed: true, matchKey: "user-id", matchSource: "id" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("blocks when sender id does not match", () => {
|
|
25
|
+
expect(
|
|
26
|
+
resolveNextcloudTalkAllowlistMatch({
|
|
27
|
+
allowFrom: ["allowed"],
|
|
28
|
+
senderId: "other",
|
|
29
|
+
}).allowed,
|
|
30
|
+
).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/policy.ts
CHANGED
|
@@ -29,8 +29,7 @@ export function normalizeNextcloudTalkAllowlist(
|
|
|
29
29
|
export function resolveNextcloudTalkAllowlistMatch(params: {
|
|
30
30
|
allowFrom: Array<string | number> | undefined;
|
|
31
31
|
senderId: string;
|
|
32
|
-
|
|
33
|
-
}): AllowlistMatch<"wildcard" | "id" | "name"> {
|
|
32
|
+
}): AllowlistMatch<"wildcard" | "id"> {
|
|
34
33
|
const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
|
|
35
34
|
if (allowFrom.length === 0) {
|
|
36
35
|
return { allowed: false };
|
|
@@ -42,10 +41,6 @@ export function resolveNextcloudTalkAllowlistMatch(params: {
|
|
|
42
41
|
if (allowFrom.includes(senderId)) {
|
|
43
42
|
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
44
43
|
}
|
|
45
|
-
const senderName = params.senderName ? normalizeAllowEntry(params.senderName) : "";
|
|
46
|
-
if (senderName && allowFrom.includes(senderName)) {
|
|
47
|
-
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
48
|
-
}
|
|
49
44
|
return { allowed: false };
|
|
50
45
|
}
|
|
51
46
|
|
|
@@ -132,7 +127,6 @@ export function resolveNextcloudTalkGroupAllow(params: {
|
|
|
132
127
|
outerAllowFrom: Array<string | number> | undefined;
|
|
133
128
|
innerAllowFrom: Array<string | number> | undefined;
|
|
134
129
|
senderId: string;
|
|
135
|
-
senderName?: string | null;
|
|
136
130
|
}): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } {
|
|
137
131
|
if (params.groupPolicy === "disabled") {
|
|
138
132
|
return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
|
|
@@ -150,12 +144,10 @@ export function resolveNextcloudTalkGroupAllow(params: {
|
|
|
150
144
|
const outerMatch = resolveNextcloudTalkAllowlistMatch({
|
|
151
145
|
allowFrom: params.outerAllowFrom,
|
|
152
146
|
senderId: params.senderId,
|
|
153
|
-
senderName: params.senderName,
|
|
154
147
|
});
|
|
155
148
|
const innerMatch = resolveNextcloudTalkAllowlistMatch({
|
|
156
149
|
allowFrom: params.innerAllowFrom,
|
|
157
150
|
senderId: params.senderId,
|
|
158
|
-
senderName: params.senderName,
|
|
159
151
|
});
|
|
160
152
|
const allowed = resolveNestedAllowlistDecision({
|
|
161
153
|
outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,
|
package/src/send.ts
CHANGED
|
@@ -93,8 +93,12 @@ export async function sendMessageNextcloudTalk(
|
|
|
93
93
|
}
|
|
94
94
|
const bodyStr = JSON.stringify(body);
|
|
95
95
|
|
|
96
|
+
// Nextcloud Talk verifies signature against the extracted message text,
|
|
97
|
+
// not the full JSON body. See ChecksumVerificationService.php:
|
|
98
|
+
// hash_hmac('sha256', $random . $data, $secret)
|
|
99
|
+
// where $data is the "message" parameter, not the raw request body.
|
|
96
100
|
const { random, signature } = generateNextcloudTalkSignature({
|
|
97
|
-
body:
|
|
101
|
+
body: message,
|
|
98
102
|
secret,
|
|
99
103
|
});
|
|
100
104
|
|
|
@@ -183,8 +187,9 @@ export async function sendReactionNextcloudTalk(
|
|
|
183
187
|
const normalizedToken = normalizeRoomToken(roomToken);
|
|
184
188
|
|
|
185
189
|
const body = JSON.stringify({ reaction });
|
|
190
|
+
// Sign only the reaction string, not the full JSON body
|
|
186
191
|
const { random, signature } = generateNextcloudTalkSignature({
|
|
187
|
-
body,
|
|
192
|
+
body: reaction,
|
|
188
193
|
secret,
|
|
189
194
|
});
|
|
190
195
|
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
GroupPolicy,
|
|
6
6
|
} from "openclaw/plugin-sdk";
|
|
7
7
|
|
|
8
|
+
export type { DmPolicy, GroupPolicy };
|
|
9
|
+
|
|
8
10
|
export type NextcloudTalkRoomConfig = {
|
|
9
11
|
requireMention?: boolean;
|
|
10
12
|
/** Optional tool policy overrides for this room. */
|
|
@@ -68,6 +70,8 @@ export type NextcloudTalkAccountConfig = {
|
|
|
68
70
|
blockStreaming?: boolean;
|
|
69
71
|
/** Merge streamed block replies before sending. */
|
|
70
72
|
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
|
73
|
+
/** Outbound response prefix override for this channel/account. */
|
|
74
|
+
responsePrefix?: string;
|
|
71
75
|
/** Media upload max size in MB. */
|
|
72
76
|
mediaMaxMb?: number;
|
|
73
77
|
};
|