@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.2.2",
3
+ "version": "2026.2.9",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -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?.groupPolicy;
87
- const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
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 = config.commands?.useAccessGroups !== false;
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" : "dm",
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(config.session?.store, {
240
- agentId: route.agentId,
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
- senderName?: string | null;
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: bodyStr,
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
  };