@openclaw/nextcloud-talk 2026.5.7-beta.1 → 2026.5.9-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.
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-BVVRsVr5.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-DOIJGKcg.js";
2
2
  export { nextcloudTalkPlugin };
@@ -1,5 +1,5 @@
1
- import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-CYlB-4Bf.js";
2
- import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, f as getNextcloudTalkRuntime, i as dispatchInboundReplyWithBase, l as resolveDefaultGroupPolicy, n as createChannelPairingController, o as logInboundDrop, r as deliverFormattedTextWithAttachments, s as readStoreAllowFromForDmPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as resolveDmGroupAccessWithCommandGate } from "./runtime-api-BcCzeRN9.js";
1
+ import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, f as getNextcloudTalkRuntime, i as deliverFormattedTextWithAttachments, l as resolveDefaultGroupPolicy, n as createChannelMessageReplyPipeline, o as logInboundDrop, r as createChannelPairingController, s as readStoreAllowFromForDmPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as resolveDmGroupAccessWithCommandGate } from "./runtime-api-Cy4XTpP2.js";
2
+ import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-DIa4Qbi_.js";
3
3
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-bczDw2-2.js";
4
4
  import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
5
5
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
@@ -11,6 +11,13 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
11
11
  import { convertMarkdownTables, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
12
12
  import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
13
13
  import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
14
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
15
+ import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
16
+ import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
17
+ import { readFileSync } from "node:fs";
18
+ import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
19
+ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
20
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
14
21
  import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
15
22
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
16
23
  import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ToolPolicySchema, buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
@@ -18,19 +25,12 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
18
25
  import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
19
26
  import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime, runStoppablePassiveMonitor, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
20
27
  import { z } from "openclaw/plugin-sdk/zod";
21
- import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
22
28
  import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
23
29
  import os from "node:os";
24
30
  import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision } from "openclaw/plugin-sdk/channel-targets";
25
31
  import { evaluateMatchedGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
26
- import { readFileSync } from "node:fs";
27
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
28
- import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
29
- import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
30
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
31
32
  import { createServer } from "node:http";
32
33
  import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
33
- import { z as z$1 } from "zod";
34
34
  import path from "node:path";
35
35
  import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
36
36
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
@@ -140,6 +140,195 @@ const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
140
140
  normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value)
141
141
  });
142
142
  //#endregion
143
+ //#region extensions/nextcloud-talk/src/api-credentials.ts
144
+ function resolveNextcloudTalkApiCredentials(params) {
145
+ const apiUser = params.apiUser?.trim();
146
+ if (!apiUser) return;
147
+ const inlinePassword = normalizeResolvedSecretInputString({
148
+ value: params.apiPassword,
149
+ path: "channels.nextcloud-talk.apiPassword"
150
+ });
151
+ if (inlinePassword) return {
152
+ apiUser,
153
+ apiPassword: inlinePassword
154
+ };
155
+ if (!params.apiPasswordFile) return;
156
+ try {
157
+ const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
158
+ return filePassword ? {
159
+ apiUser,
160
+ apiPassword: filePassword
161
+ } : void 0;
162
+ } catch {
163
+ return;
164
+ }
165
+ }
166
+ //#endregion
167
+ //#region extensions/nextcloud-talk/src/signature.ts
168
+ const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
169
+ const RANDOM_HEADER = "x-nextcloud-talk-random";
170
+ const BACKEND_HEADER = "x-nextcloud-talk-backend";
171
+ /**
172
+ * Verify the HMAC-SHA256 signature of an incoming webhook request.
173
+ * Signature is calculated as: HMAC-SHA256(random + body, secret)
174
+ */
175
+ function verifyNextcloudTalkSignature(params) {
176
+ const { signature, random, body, secret } = params;
177
+ if (!signature || !random || !secret) return false;
178
+ const expected = createHmac("sha256", secret).update(random + body).digest("hex");
179
+ const expectedBuf = Buffer.from(expected, "utf8");
180
+ const signatureBuf = Buffer.from(signature, "utf8");
181
+ const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
182
+ const paddedExpected = Buffer.alloc(maxLen);
183
+ const paddedSignature = Buffer.alloc(maxLen);
184
+ expectedBuf.copy(paddedExpected);
185
+ signatureBuf.copy(paddedSignature);
186
+ const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
187
+ return expectedBuf.length === signatureBuf.length && timingResult;
188
+ }
189
+ /**
190
+ * Extract webhook headers from an incoming request.
191
+ */
192
+ function extractNextcloudTalkHeaders(headers) {
193
+ const getHeader = (name) => {
194
+ const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
195
+ return Array.isArray(value) ? value[0] : value;
196
+ };
197
+ const signature = getHeader(SIGNATURE_HEADER);
198
+ const random = getHeader(RANDOM_HEADER);
199
+ const backend = getHeader(BACKEND_HEADER);
200
+ if (!signature || !random || !backend) return null;
201
+ return {
202
+ signature,
203
+ random,
204
+ backend
205
+ };
206
+ }
207
+ /**
208
+ * Generate signature headers for an outbound request to Nextcloud Talk.
209
+ */
210
+ function generateNextcloudTalkSignature(params) {
211
+ const { body, secret } = params;
212
+ const random = randomBytes(32).toString("hex");
213
+ return {
214
+ random,
215
+ signature: createHmac("sha256", secret).update(random + body).digest("hex")
216
+ };
217
+ }
218
+ //#endregion
219
+ //#region extensions/nextcloud-talk/src/bot-preflight.ts
220
+ const BOT_FEATURE_RESPONSE = 2;
221
+ function normalizeUrlForMatch(value) {
222
+ if (!value?.trim()) return "";
223
+ try {
224
+ const url = new URL(value.trim());
225
+ url.hash = "";
226
+ return url.toString().replace(/\/$/, "");
227
+ } catch {
228
+ return value.trim().replace(/\/$/, "");
229
+ }
230
+ }
231
+ function coerceFeatureMask(value) {
232
+ if (typeof value === "number" && Number.isFinite(value)) return value;
233
+ if (typeof value === "string" && value.trim()) {
234
+ const parsed = Number.parseInt(value, 10);
235
+ return Number.isFinite(parsed) ? parsed : void 0;
236
+ }
237
+ }
238
+ function formatMissingResponseFeatureMessage(bot, features) {
239
+ const id = bot.id == null ? "unknown" : String(bot.id);
240
+ return `Nextcloud Talk bot "${bot.name?.trim() || "matching bot"}" (${id}) is missing the response feature${typeof features === "number" ? ` (features=${features})` : ""}; outbound replies will fail. Run ./occ talk:bot:state --feature webhook --feature response --feature reaction ${id} 1 or reinstall the bot with --feature response.`;
241
+ }
242
+ async function probeNextcloudTalkBotResponseFeature(params) {
243
+ const { account, timeoutMs } = params;
244
+ const baseUrl = account.baseUrl?.trim();
245
+ if (!baseUrl) return {
246
+ ok: true,
247
+ skipped: true,
248
+ code: "missing_base_url",
249
+ message: "Nextcloud Talk bot response feature probe skipped: baseUrl is not configured."
250
+ };
251
+ const webhookUrl = normalizeUrlForMatch(account.config.webhookPublicUrl);
252
+ if (!webhookUrl) return {
253
+ ok: true,
254
+ skipped: true,
255
+ code: "missing_webhook_url",
256
+ message: "Nextcloud Talk bot response feature probe skipped: webhookPublicUrl is not configured."
257
+ };
258
+ const credentials = resolveNextcloudTalkApiCredentials({
259
+ apiUser: account.config.apiUser,
260
+ apiPassword: account.config.apiPassword,
261
+ apiPasswordFile: account.config.apiPasswordFile
262
+ });
263
+ if (!credentials) return {
264
+ ok: true,
265
+ skipped: true,
266
+ code: "missing_api_credentials",
267
+ message: "Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured."
268
+ };
269
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/admin`;
270
+ const auth = Buffer.from(`${credentials.apiUser}:${credentials.apiPassword}`, "utf-8").toString("base64");
271
+ try {
272
+ const { response, release } = await fetchWithSsrFGuard({
273
+ url,
274
+ init: {
275
+ method: "GET",
276
+ headers: {
277
+ Authorization: `Basic ${auth}`,
278
+ "OCS-APIRequest": "true",
279
+ Accept: "application/json"
280
+ }
281
+ },
282
+ auditContext: "nextcloud-talk.bot-response-preflight",
283
+ policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config),
284
+ timeoutMs
285
+ });
286
+ try {
287
+ if (!response.ok) {
288
+ const body = await response.text().catch(() => "");
289
+ return {
290
+ ok: false,
291
+ code: "api_error",
292
+ status: response.status,
293
+ message: `Nextcloud Talk bot response feature probe failed (${response.status})${body ? `: ${body}` : ""}`
294
+ };
295
+ }
296
+ const payload = await response.json();
297
+ const bot = (Array.isArray(payload.ocs?.data) ? payload.ocs.data : []).find((entry) => normalizeUrlForMatch(entry.url) === webhookUrl);
298
+ if (!bot) return {
299
+ ok: false,
300
+ code: "bot_not_found",
301
+ message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`
302
+ };
303
+ const features = coerceFeatureMask(bot.features);
304
+ if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) return {
305
+ ok: false,
306
+ code: "missing_response_feature",
307
+ botId: bot.id == null ? void 0 : String(bot.id),
308
+ botName: bot.name,
309
+ features,
310
+ message: formatMissingResponseFeatureMessage(bot, features)
311
+ };
312
+ return {
313
+ ok: true,
314
+ code: "ok",
315
+ botId: bot.id == null ? void 0 : String(bot.id),
316
+ botName: bot.name,
317
+ features,
318
+ message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
319
+ };
320
+ } finally {
321
+ await release();
322
+ }
323
+ } catch (error) {
324
+ return {
325
+ ok: false,
326
+ code: "request_failed",
327
+ message: `Nextcloud Talk bot response feature probe failed: ${formatErrorMessage(error)}`
328
+ };
329
+ }
330
+ }
331
+ //#endregion
143
332
  //#region extensions/nextcloud-talk/src/channel.adapters.ts
144
333
  const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
145
334
  sectionKey: "nextcloud-talk",
@@ -229,9 +418,26 @@ const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
229
418
  });
230
419
  //#endregion
231
420
  //#region extensions/nextcloud-talk/src/doctor.ts
421
+ async function collectNextcloudTalkBotResponseWarnings(params) {
422
+ const warnings = [];
423
+ for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
424
+ const account = resolveNextcloudTalkAccount({
425
+ cfg: params.cfg,
426
+ accountId
427
+ });
428
+ if (!account.enabled || !account.secret || !account.baseUrl) continue;
429
+ const result = await probeNextcloudTalkBotResponseFeature({
430
+ account,
431
+ timeoutMs: 5e3
432
+ });
433
+ if (result.code === "missing_response_feature" || result.code === "bot_not_found" || result.code === "api_error" || result.code === "request_failed") warnings.push(`- channels.nextcloud-talk.${account.accountId}: ${result.message}`);
434
+ }
435
+ return warnings;
436
+ }
232
437
  const nextcloudTalkDoctor = {
233
438
  legacyConfigRules,
234
- normalizeCompatibilityConfig
439
+ normalizeCompatibilityConfig,
440
+ collectPreviewWarnings: async ({ cfg }) => await collectNextcloudTalkBotResponseWarnings({ cfg })
235
441
  };
236
442
  //#endregion
237
443
  //#region extensions/nextcloud-talk/src/policy.ts
@@ -338,19 +544,6 @@ const roomCache = /* @__PURE__ */ new Map();
338
544
  function resolveRoomCacheKey(params) {
339
545
  return `${params.accountId}:${params.roomToken}`;
340
546
  }
341
- function readApiPassword(params) {
342
- const inlinePassword = normalizeResolvedSecretInputString({
343
- value: params.apiPassword,
344
- path: "channels.nextcloud-talk.apiPassword"
345
- });
346
- if (inlinePassword) return inlinePassword;
347
- if (!params.apiPasswordFile) return;
348
- try {
349
- return readFileSync(params.apiPasswordFile, "utf-8").trim() || void 0;
350
- } catch {
351
- return;
352
- }
353
- }
354
547
  function coerceRoomType(value) {
355
548
  if (typeof value === "number" && Number.isFinite(value)) return value;
356
549
  if (typeof value === "string" && value.trim()) {
@@ -375,16 +568,16 @@ async function resolveNextcloudTalkRoomKind(params) {
375
568
  if (cached.kind && age < ROOM_CACHE_TTL_MS) return cached.kind;
376
569
  if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) return;
377
570
  }
378
- const apiUser = account.config.apiUser?.trim();
379
- const apiPassword = readApiPassword({
571
+ const apiCredentials = resolveNextcloudTalkApiCredentials({
572
+ apiUser: account.config.apiUser,
380
573
  apiPassword: account.config.apiPassword,
381
574
  apiPasswordFile: account.config.apiPasswordFile
382
575
  });
383
- if (!apiUser || !apiPassword) return;
576
+ if (!apiCredentials) return;
384
577
  const baseUrl = account.baseUrl?.trim();
385
578
  if (!baseUrl) return;
386
579
  const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
387
- const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
580
+ const auth = Buffer.from(`${apiCredentials.apiUser}:${apiCredentials.apiPassword}`, "utf-8").toString("base64");
388
581
  try {
389
582
  const { response, release } = await fetchWithSsrFGuard({
390
583
  url,
@@ -450,58 +643,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
450
643
  return /^[a-z0-9]{8,}$/i.test(trimmed);
451
644
  }
452
645
  //#endregion
453
- //#region extensions/nextcloud-talk/src/signature.ts
454
- const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
455
- const RANDOM_HEADER = "x-nextcloud-talk-random";
456
- const BACKEND_HEADER = "x-nextcloud-talk-backend";
457
- /**
458
- * Verify the HMAC-SHA256 signature of an incoming webhook request.
459
- * Signature is calculated as: HMAC-SHA256(random + body, secret)
460
- */
461
- function verifyNextcloudTalkSignature(params) {
462
- const { signature, random, body, secret } = params;
463
- if (!signature || !random || !secret) return false;
464
- const expected = createHmac("sha256", secret).update(random + body).digest("hex");
465
- const expectedBuf = Buffer.from(expected, "utf8");
466
- const signatureBuf = Buffer.from(signature, "utf8");
467
- const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
468
- const paddedExpected = Buffer.alloc(maxLen);
469
- const paddedSignature = Buffer.alloc(maxLen);
470
- expectedBuf.copy(paddedExpected);
471
- signatureBuf.copy(paddedSignature);
472
- const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
473
- return expectedBuf.length === signatureBuf.length && timingResult;
474
- }
475
- /**
476
- * Extract webhook headers from an incoming request.
477
- */
478
- function extractNextcloudTalkHeaders(headers) {
479
- const getHeader = (name) => {
480
- const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
481
- return Array.isArray(value) ? value[0] : value;
482
- };
483
- const signature = getHeader(SIGNATURE_HEADER);
484
- const random = getHeader(RANDOM_HEADER);
485
- const backend = getHeader(BACKEND_HEADER);
486
- if (!signature || !random || !backend) return null;
487
- return {
488
- signature,
489
- random,
490
- backend
491
- };
492
- }
493
- /**
494
- * Generate signature headers for an outbound request to Nextcloud Talk.
495
- */
496
- function generateNextcloudTalkSignature(params) {
497
- const { body, secret } = params;
498
- const random = randomBytes(32).toString("hex");
499
- return {
500
- random,
501
- signature: createHmac("sha256", secret).update(random + body).digest("hex")
502
- };
503
- }
504
- //#endregion
505
646
  //#region extensions/nextcloud-talk/src/send.ts
506
647
  function resolveCredentials(explicit, account) {
507
648
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
@@ -546,6 +687,18 @@ function recordNextcloudTalkOutboundActivity(accountId) {
546
687
  if (!(error instanceof Error) || error.message !== "Nextcloud Talk runtime not initialized") throw error;
547
688
  }
548
689
  }
690
+ function createNextcloudTalkSendReceipt(params) {
691
+ const messageId = params.messageId.trim();
692
+ return createMessageReceiptFromOutboundResults({
693
+ results: messageId && messageId !== "unknown" ? [{
694
+ channel: "nextcloud-talk",
695
+ messageId,
696
+ conversationId: params.roomToken
697
+ }] : [],
698
+ kind: "text",
699
+ ...params.replyTo ? { replyToId: params.replyTo } : {}
700
+ });
701
+ }
549
702
  async function sendMessageNextcloudTalk(to, text, opts) {
550
703
  const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
551
704
  const roomToken = normalizeRoomToken(to);
@@ -584,7 +737,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
584
737
  const status = response.status;
585
738
  let errorMsg = `Nextcloud Talk send failed (${status})`;
586
739
  if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
587
- else if (status === 401) errorMsg = "Nextcloud Talk: authentication failed - check bot secret";
740
+ else if (status === 401) errorMsg = "Nextcloud Talk: bot send was rejected - check the bot secret and ensure the bot was installed with --feature response";
588
741
  else if (status === 403) errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room";
589
742
  else if (status === 404) errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`;
590
743
  else if (errorBody) errorMsg = `Nextcloud Talk send failed: ${errorBody}`;
@@ -602,6 +755,11 @@ async function sendMessageNextcloudTalk(to, text, opts) {
602
755
  return {
603
756
  messageId,
604
757
  roomToken,
758
+ receipt: createNextcloudTalkSendReceipt({
759
+ messageId,
760
+ roomToken,
761
+ ...opts.replyTo ? { replyTo: opts.replyTo } : {}
762
+ }),
605
763
  timestamp
606
764
  };
607
765
  } finally {
@@ -815,33 +973,46 @@ async function handleNextcloudTalkInbound(params) {
815
973
  OriginatingTo: `nextcloud-talk:${roomToken}`,
816
974
  CommandAuthorized: commandAuthorized
817
975
  });
818
- await dispatchInboundReplyWithBase({
976
+ const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
819
977
  cfg: config,
978
+ agentId: route.agentId,
979
+ channel: CHANNEL_ID,
980
+ accountId: account.accountId
981
+ });
982
+ await core.channel.turn.runPrepared({
820
983
  channel: CHANNEL_ID,
821
984
  accountId: account.accountId,
822
- route,
985
+ routeSessionKey: route.sessionKey,
823
986
  storePath,
824
987
  ctxPayload,
825
- core,
826
- deliver: async (payload) => {
827
- await deliverNextcloudTalkReply({
828
- cfg: config,
829
- payload,
830
- roomToken,
831
- accountId: account.accountId,
832
- statusSink
833
- });
834
- },
835
- onRecordError: (err) => {
988
+ recordInboundSession: core.channel.session.recordInboundSession,
989
+ runDispatch: async () => await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
990
+ ctx: ctxPayload,
991
+ cfg: config,
992
+ dispatcherOptions: {
993
+ ...replyPipeline,
994
+ deliver: async (payload) => {
995
+ await deliverNextcloudTalkReply({
996
+ cfg: config,
997
+ payload,
998
+ roomToken,
999
+ accountId: account.accountId,
1000
+ statusSink
1001
+ });
1002
+ },
1003
+ onError: (err, info) => {
1004
+ runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
1005
+ }
1006
+ },
1007
+ replyOptions: {
1008
+ onModelSelected,
1009
+ skillFilter: roomConfig?.skills,
1010
+ disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
1011
+ }
1012
+ }),
1013
+ record: { onRecordError: (err) => {
836
1014
  runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
837
- },
838
- onDispatchError: (err, info) => {
839
- runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
840
- },
841
- replyOptions: {
842
- skillFilter: roomConfig?.skills,
843
- disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
844
- }
1015
+ } }
845
1016
  });
846
1017
  }
847
1018
  //#endregion
@@ -851,28 +1022,28 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
851
1022
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
852
1023
  const HEALTH_PATH = "/healthz";
853
1024
  const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
854
- const NextcloudTalkWebhookPayloadSchema = z$1.object({
855
- type: z$1.enum([
1025
+ const NextcloudTalkWebhookPayloadSchema = z.object({
1026
+ type: z.enum([
856
1027
  "Create",
857
1028
  "Update",
858
1029
  "Delete"
859
1030
  ]),
860
- actor: z$1.object({
861
- type: z$1.literal("Person"),
862
- id: z$1.string().min(1),
863
- name: z$1.string()
1031
+ actor: z.object({
1032
+ type: z.literal("Person"),
1033
+ id: z.string().min(1),
1034
+ name: z.string()
864
1035
  }),
865
- object: z$1.object({
866
- type: z$1.literal("Note"),
867
- id: z$1.string().min(1),
868
- name: z$1.string(),
869
- content: z$1.string(),
870
- mediaType: z$1.string()
1036
+ object: z.object({
1037
+ type: z.literal("Note"),
1038
+ id: z.string().min(1),
1039
+ name: z.string(),
1040
+ content: z.string(),
1041
+ mediaType: z.string()
871
1042
  }),
872
- target: z$1.object({
873
- type: z$1.literal("Collection"),
874
- id: z$1.string().min(1),
875
- name: z$1.string()
1043
+ target: z.object({
1044
+ type: z.literal("Collection"),
1045
+ id: z.string().min(1),
1046
+ name: z.string()
876
1047
  })
877
1048
  });
878
1049
  const WEBHOOK_ERRORS = {
@@ -1334,6 +1505,28 @@ const nextcloudTalkGatewayAdapter = {
1334
1505
  }
1335
1506
  };
1336
1507
  //#endregion
1508
+ //#region extensions/nextcloud-talk/src/message-adapter.ts
1509
+ const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
1510
+ id: "nextcloud-talk",
1511
+ durableFinal: { capabilities: {
1512
+ text: true,
1513
+ media: true,
1514
+ replyTo: true
1515
+ } },
1516
+ send: {
1517
+ text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageNextcloudTalk(to, text, {
1518
+ accountId: accountId ?? void 0,
1519
+ replyTo: replyToId ?? void 0,
1520
+ cfg
1521
+ }),
1522
+ media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageNextcloudTalk(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
1523
+ accountId: accountId ?? void 0,
1524
+ replyTo: replyToId ?? void 0,
1525
+ cfg
1526
+ })
1527
+ }
1528
+ });
1529
+ //#endregion
1337
1530
  //#region extensions/nextcloud-talk/src/session-route.ts
1338
1531
  function resolveNextcloudTalkOutboundSessionRoute(params) {
1339
1532
  const roomId = stripNextcloudTalkTargetPrefix(params.target);
@@ -1538,7 +1731,7 @@ const nextcloudTalkSetupWizard = {
1538
1731
  title: "Nextcloud Talk bot setup",
1539
1732
  lines: [
1540
1733
  "1) SSH into your Nextcloud server",
1541
- "2) Run: ./occ talk:bot:install \"OpenClaw\" \"<shared-secret>\" \"<webhook-url>\" --feature reaction",
1734
+ "2) Run: ./occ talk:bot:install \"OpenClaw\" \"<shared-secret>\" \"<webhook-url>\" --feature webhook --feature response --feature reaction",
1542
1735
  "3) Copy the shared secret you used in the command",
1543
1736
  "4) Enable the bot in your Nextcloud Talk room settings",
1544
1737
  "Tip: you can also set NEXTCLOUD_TALK_BOT_SECRET in your env.",
@@ -1740,6 +1933,21 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
1740
1933
  status: createComputedAccountStatusAdapter({
1741
1934
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
1742
1935
  buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
1936
+ collectStatusIssues: (accounts) => accounts.flatMap((account) => {
1937
+ const probe = account.probe;
1938
+ if (!probe || probe.ok !== false || probe.code !== "missing_response_feature" || !probe.message) return [];
1939
+ return [{
1940
+ channel: "nextcloud-talk",
1941
+ accountId: account.accountId ?? DEFAULT_ACCOUNT_ID$2,
1942
+ kind: "config",
1943
+ message: probe.message,
1944
+ fix: "Add --feature response to the Talk bot."
1945
+ }];
1946
+ }),
1947
+ probeAccount: async ({ account, timeoutMs }) => await probeNextcloudTalkBotResponseFeature({
1948
+ account,
1949
+ timeoutMs
1950
+ }),
1743
1951
  resolveAccountSnapshot: ({ account }) => ({
1744
1952
  accountId: account.accountId,
1745
1953
  name: account.name,
@@ -1752,7 +1960,8 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
1752
1960
  }
1753
1961
  })
1754
1962
  }),
1755
- gateway: nextcloudTalkGatewayAdapter
1963
+ gateway: nextcloudTalkGatewayAdapter,
1964
+ message: nextcloudTalkMessageAdapter
1756
1965
  },
1757
1966
  pairing: { text: {
1758
1967
  ...nextcloudTalkPairingTextAdapter,
@@ -1771,15 +1980,20 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
1771
1980
  },
1772
1981
  attachedResults: {
1773
1982
  channel: "nextcloud-talk",
1774
- sendText: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageNextcloudTalk(to, text, {
1775
- accountId: accountId ?? void 0,
1776
- replyTo: replyToId ?? void 0,
1777
- cfg
1983
+ sendText: async ({ cfg, to, text, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.text({
1984
+ cfg,
1985
+ to,
1986
+ text,
1987
+ accountId,
1988
+ replyToId
1778
1989
  }),
1779
- sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageNextcloudTalk(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
1780
- accountId: accountId ?? void 0,
1781
- replyTo: replyToId ?? void 0,
1782
- cfg
1990
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.media({
1991
+ cfg,
1992
+ to,
1993
+ text,
1994
+ mediaUrl: mediaUrl ?? "",
1995
+ accountId,
1996
+ replyToId
1783
1997
  })
1784
1998
  }
1785
1999
  }
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-BVVRsVr5.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-DOIJGKcg.js";
2
2
  export { nextcloudTalkPlugin };
@@ -1,2 +1,2 @@
1
- import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-CYlB-4Bf.js";
1
+ import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-DIa4Qbi_.js";
2
2
  export { legacyConfigRules, normalizeCompatibilityConfig };
@@ -1,10 +1,10 @@
1
1
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
2
2
  import { readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithCommandGate } from "openclaw/plugin-sdk/channel-policy";
3
- import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
4
3
  import { logInboundDrop } from "openclaw/plugin-sdk/channel-logging";
5
4
  import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
6
- import { dispatchInboundReplyWithBase } from "openclaw/plugin-sdk/inbound-reply-dispatch";
5
+ import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
7
6
  import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
7
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
8
8
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
9
9
  //#region extensions/nextcloud-talk/src/runtime.ts
10
10
  const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime } = createPluginRuntimeStore({
@@ -12,4 +12,4 @@ const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime
12
12
  errorMessage: "Nextcloud Talk runtime not initialized"
13
13
  });
14
14
  //#endregion
15
- export { fetchWithSsrFGuard as a, resolveAllowlistProviderRuntimeGroupPolicy as c, warnMissingProviderGroupPolicyFallbackOnce as d, getNextcloudTalkRuntime as f, dispatchInboundReplyWithBase as i, resolveDefaultGroupPolicy as l, createChannelPairingController as n, logInboundDrop as o, setNextcloudTalkRuntime as p, deliverFormattedTextWithAttachments as r, readStoreAllowFromForDmPolicy as s, GROUP_POLICY_BLOCKED_LABEL as t, resolveDmGroupAccessWithCommandGate as u };
15
+ export { fetchWithSsrFGuard as a, resolveAllowlistProviderRuntimeGroupPolicy as c, warnMissingProviderGroupPolicyFallbackOnce as d, getNextcloudTalkRuntime as f, deliverFormattedTextWithAttachments as i, resolveDefaultGroupPolicy as l, createChannelMessageReplyPipeline as n, logInboundDrop as o, setNextcloudTalkRuntime as p, createChannelPairingController as r, readStoreAllowFromForDmPolicy as s, GROUP_POLICY_BLOCKED_LABEL as t, resolveDmGroupAccessWithCommandGate as u };
@@ -1,2 +1,2 @@
1
- import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, i as dispatchInboundReplyWithBase, l as resolveDefaultGroupPolicy, n as createChannelPairingController, o as logInboundDrop, p as setNextcloudTalkRuntime, r as deliverFormattedTextWithAttachments, s as readStoreAllowFromForDmPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as resolveDmGroupAccessWithCommandGate } from "./runtime-api-BcCzeRN9.js";
2
- export { GROUP_POLICY_BLOCKED_LABEL, createChannelPairingController, deliverFormattedTextWithAttachments, dispatchInboundReplyWithBase, fetchWithSsrFGuard, logInboundDrop, readStoreAllowFromForDmPolicy, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, resolveDmGroupAccessWithCommandGate, setNextcloudTalkRuntime, warnMissingProviderGroupPolicyFallbackOnce };
1
+ import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, i as deliverFormattedTextWithAttachments, l as resolveDefaultGroupPolicy, n as createChannelMessageReplyPipeline, o as logInboundDrop, p as setNextcloudTalkRuntime, r as createChannelPairingController, s as readStoreAllowFromForDmPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as resolveDmGroupAccessWithCommandGate } from "./runtime-api-Cy4XTpP2.js";
2
+ export { GROUP_POLICY_BLOCKED_LABEL, createChannelMessageReplyPipeline, createChannelPairingController, deliverFormattedTextWithAttachments, fetchWithSsrFGuard, logInboundDrop, readStoreAllowFromForDmPolicy, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, resolveDmGroupAccessWithCommandGate, setNextcloudTalkRuntime, warnMissingProviderGroupPolicyFallbackOnce };
package/package.json CHANGED
@@ -1,21 +1,18 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.5.7-beta.1",
3
+ "version": "2026.5.9-beta.1",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/openclaw/openclaw"
8
8
  },
9
9
  "type": "module",
10
- "dependencies": {
11
- "zod": "^4.4.3"
12
- },
13
10
  "devDependencies": {
14
11
  "@openclaw/plugin-sdk": "workspace:*",
15
12
  "openclaw": "workspace:*"
16
13
  },
17
14
  "peerDependencies": {
18
- "openclaw": ">=2026.5.7-beta.1"
15
+ "openclaw": ">=2026.5.9-beta.1"
19
16
  },
20
17
  "peerDependenciesMeta": {
21
18
  "openclaw": {
@@ -47,10 +44,10 @@
47
44
  "minHostVersion": ">=2026.4.10"
48
45
  },
49
46
  "compat": {
50
- "pluginApi": ">=2026.5.7-beta.1"
47
+ "pluginApi": ">=2026.5.9-beta.1"
51
48
  },
52
49
  "build": {
53
- "openclawVersion": "2026.5.7-beta.1"
50
+ "openclawVersion": "2026.5.9-beta.1"
54
51
  },
55
52
  "release": {
56
53
  "publishToClawHub": true,