@openclaw/nextcloud-talk 2026.5.7 → 2026.5.10-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-DSNSV1Y5.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 resolveDefaultGroupPolicy, i as deliverFormattedTextWithAttachments, l as warnMissingProviderGroupPolicyFallbackOnce, o as logInboundDrop, r as createChannelPairingController, s as resolveAllowlistProviderRuntimeGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as getNextcloudTalkRuntime } from "./runtime-api-D_apwxTv.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";
@@ -8,9 +8,16 @@ import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/
8
8
  import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
9
9
  import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
10
10
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
11
- import { convertMarkdownTables, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
11
+ import { convertMarkdownTables, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } 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,13 @@ 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";
30
+ import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
31
+ import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
24
32
  import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision } from "openclaw/plugin-sdk/channel-targets";
25
- 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
33
  import { createServer } from "node:http";
32
34
  import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
33
- import { z as z$1 } from "zod";
34
35
  import path from "node:path";
35
36
  import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
36
37
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
@@ -140,6 +141,195 @@ const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
140
141
  normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value)
141
142
  });
142
143
  //#endregion
144
+ //#region extensions/nextcloud-talk/src/api-credentials.ts
145
+ function resolveNextcloudTalkApiCredentials(params) {
146
+ const apiUser = params.apiUser?.trim();
147
+ if (!apiUser) return;
148
+ const inlinePassword = normalizeResolvedSecretInputString({
149
+ value: params.apiPassword,
150
+ path: "channels.nextcloud-talk.apiPassword"
151
+ });
152
+ if (inlinePassword) return {
153
+ apiUser,
154
+ apiPassword: inlinePassword
155
+ };
156
+ if (!params.apiPasswordFile) return;
157
+ try {
158
+ const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
159
+ return filePassword ? {
160
+ apiUser,
161
+ apiPassword: filePassword
162
+ } : void 0;
163
+ } catch {
164
+ return;
165
+ }
166
+ }
167
+ //#endregion
168
+ //#region extensions/nextcloud-talk/src/signature.ts
169
+ const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
170
+ const RANDOM_HEADER = "x-nextcloud-talk-random";
171
+ const BACKEND_HEADER = "x-nextcloud-talk-backend";
172
+ /**
173
+ * Verify the HMAC-SHA256 signature of an incoming webhook request.
174
+ * Signature is calculated as: HMAC-SHA256(random + body, secret)
175
+ */
176
+ function verifyNextcloudTalkSignature(params) {
177
+ const { signature, random, body, secret } = params;
178
+ if (!signature || !random || !secret) return false;
179
+ const expected = createHmac("sha256", secret).update(random + body).digest("hex");
180
+ const expectedBuf = Buffer.from(expected, "utf8");
181
+ const signatureBuf = Buffer.from(signature, "utf8");
182
+ const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
183
+ const paddedExpected = Buffer.alloc(maxLen);
184
+ const paddedSignature = Buffer.alloc(maxLen);
185
+ expectedBuf.copy(paddedExpected);
186
+ signatureBuf.copy(paddedSignature);
187
+ const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
188
+ return expectedBuf.length === signatureBuf.length && timingResult;
189
+ }
190
+ /**
191
+ * Extract webhook headers from an incoming request.
192
+ */
193
+ function extractNextcloudTalkHeaders(headers) {
194
+ const getHeader = (name) => {
195
+ const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
196
+ return Array.isArray(value) ? value[0] : value;
197
+ };
198
+ const signature = getHeader(SIGNATURE_HEADER);
199
+ const random = getHeader(RANDOM_HEADER);
200
+ const backend = getHeader(BACKEND_HEADER);
201
+ if (!signature || !random || !backend) return null;
202
+ return {
203
+ signature,
204
+ random,
205
+ backend
206
+ };
207
+ }
208
+ /**
209
+ * Generate signature headers for an outbound request to Nextcloud Talk.
210
+ */
211
+ function generateNextcloudTalkSignature(params) {
212
+ const { body, secret } = params;
213
+ const random = randomBytes(32).toString("hex");
214
+ return {
215
+ random,
216
+ signature: createHmac("sha256", secret).update(random + body).digest("hex")
217
+ };
218
+ }
219
+ //#endregion
220
+ //#region extensions/nextcloud-talk/src/bot-preflight.ts
221
+ const BOT_FEATURE_RESPONSE = 2;
222
+ function normalizeUrlForMatch(value) {
223
+ if (!value?.trim()) return "";
224
+ try {
225
+ const url = new URL(value.trim());
226
+ url.hash = "";
227
+ return url.toString().replace(/\/$/, "");
228
+ } catch {
229
+ return value.trim().replace(/\/$/, "");
230
+ }
231
+ }
232
+ function coerceFeatureMask(value) {
233
+ if (typeof value === "number" && Number.isFinite(value)) return value;
234
+ if (typeof value === "string" && value.trim()) {
235
+ const parsed = Number.parseInt(value, 10);
236
+ return Number.isFinite(parsed) ? parsed : void 0;
237
+ }
238
+ }
239
+ function formatMissingResponseFeatureMessage(bot, features) {
240
+ const id = bot.id == null ? "unknown" : String(bot.id);
241
+ 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.`;
242
+ }
243
+ async function probeNextcloudTalkBotResponseFeature(params) {
244
+ const { account, timeoutMs } = params;
245
+ const baseUrl = account.baseUrl?.trim();
246
+ if (!baseUrl) return {
247
+ ok: true,
248
+ skipped: true,
249
+ code: "missing_base_url",
250
+ message: "Nextcloud Talk bot response feature probe skipped: baseUrl is not configured."
251
+ };
252
+ const webhookUrl = normalizeUrlForMatch(account.config.webhookPublicUrl);
253
+ if (!webhookUrl) return {
254
+ ok: true,
255
+ skipped: true,
256
+ code: "missing_webhook_url",
257
+ message: "Nextcloud Talk bot response feature probe skipped: webhookPublicUrl is not configured."
258
+ };
259
+ const credentials = resolveNextcloudTalkApiCredentials({
260
+ apiUser: account.config.apiUser,
261
+ apiPassword: account.config.apiPassword,
262
+ apiPasswordFile: account.config.apiPasswordFile
263
+ });
264
+ if (!credentials) return {
265
+ ok: true,
266
+ skipped: true,
267
+ code: "missing_api_credentials",
268
+ message: "Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured."
269
+ };
270
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/admin`;
271
+ const auth = Buffer.from(`${credentials.apiUser}:${credentials.apiPassword}`, "utf-8").toString("base64");
272
+ try {
273
+ const { response, release } = await fetchWithSsrFGuard({
274
+ url,
275
+ init: {
276
+ method: "GET",
277
+ headers: {
278
+ Authorization: `Basic ${auth}`,
279
+ "OCS-APIRequest": "true",
280
+ Accept: "application/json"
281
+ }
282
+ },
283
+ auditContext: "nextcloud-talk.bot-response-preflight",
284
+ policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config),
285
+ timeoutMs
286
+ });
287
+ try {
288
+ if (!response.ok) {
289
+ const body = await response.text().catch(() => "");
290
+ return {
291
+ ok: false,
292
+ code: "api_error",
293
+ status: response.status,
294
+ message: `Nextcloud Talk bot response feature probe failed (${response.status})${body ? `: ${body}` : ""}`
295
+ };
296
+ }
297
+ const payload = await response.json();
298
+ const bot = (Array.isArray(payload.ocs?.data) ? payload.ocs.data : []).find((entry) => normalizeUrlForMatch(entry.url) === webhookUrl);
299
+ if (!bot) return {
300
+ ok: false,
301
+ code: "bot_not_found",
302
+ message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`
303
+ };
304
+ const features = coerceFeatureMask(bot.features);
305
+ if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) return {
306
+ ok: false,
307
+ code: "missing_response_feature",
308
+ botId: bot.id == null ? void 0 : String(bot.id),
309
+ botName: bot.name,
310
+ features,
311
+ message: formatMissingResponseFeatureMessage(bot, features)
312
+ };
313
+ return {
314
+ ok: true,
315
+ code: "ok",
316
+ botId: bot.id == null ? void 0 : String(bot.id),
317
+ botName: bot.name,
318
+ features,
319
+ message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
320
+ };
321
+ } finally {
322
+ await release();
323
+ }
324
+ } catch (error) {
325
+ return {
326
+ ok: false,
327
+ code: "request_failed",
328
+ message: `Nextcloud Talk bot response feature probe failed: ${formatErrorMessage(error)}`
329
+ };
330
+ }
331
+ }
332
+ //#endregion
143
333
  //#region extensions/nextcloud-talk/src/channel.adapters.ts
144
334
  const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
145
335
  sectionKey: "nextcloud-talk",
@@ -229,17 +419,34 @@ const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
229
419
  });
230
420
  //#endregion
231
421
  //#region extensions/nextcloud-talk/src/doctor.ts
422
+ async function collectNextcloudTalkBotResponseWarnings(params) {
423
+ const warnings = [];
424
+ for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
425
+ const account = resolveNextcloudTalkAccount({
426
+ cfg: params.cfg,
427
+ accountId
428
+ });
429
+ if (!account.enabled || !account.secret || !account.baseUrl) continue;
430
+ const result = await probeNextcloudTalkBotResponseFeature({
431
+ account,
432
+ timeoutMs: 5e3
433
+ });
434
+ 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}`);
435
+ }
436
+ return warnings;
437
+ }
232
438
  const nextcloudTalkDoctor = {
233
439
  legacyConfigRules,
234
- normalizeCompatibilityConfig
440
+ normalizeCompatibilityConfig,
441
+ collectPreviewWarnings: async ({ cfg }) => await collectNextcloudTalkBotResponseWarnings({ cfg })
235
442
  };
236
443
  //#endregion
237
444
  //#region extensions/nextcloud-talk/src/policy.ts
238
- function normalizeAllowEntry(raw) {
445
+ function normalizeNextcloudTalkAllowEntry(raw) {
239
446
  return raw.trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase();
240
447
  }
241
448
  function normalizeNextcloudTalkAllowlist(values) {
242
- return (values ?? []).map((value) => normalizeAllowEntry(String(value))).filter(Boolean);
449
+ return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
243
450
  }
244
451
  function resolveNextcloudTalkAllowlistMatch(params) {
245
452
  const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
@@ -249,7 +456,7 @@ function resolveNextcloudTalkAllowlistMatch(params) {
249
456
  matchKey: "*",
250
457
  matchSource: "wildcard"
251
458
  };
252
- const senderId = normalizeAllowEntry(params.senderId);
459
+ const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
253
460
  if (allowFrom.includes(senderId)) return {
254
461
  allowed: true,
255
462
  matchKey: senderId,
@@ -297,39 +504,6 @@ function resolveNextcloudTalkRequireMention(params) {
297
504
  if (typeof params.wildcardConfig?.requireMention === "boolean") return params.wildcardConfig.requireMention;
298
505
  return true;
299
506
  }
300
- function resolveNextcloudTalkGroupAllow(params) {
301
- const outerAllow = normalizeNextcloudTalkAllowlist(params.outerAllowFrom);
302
- const innerAllow = normalizeNextcloudTalkAllowlist(params.innerAllowFrom);
303
- const outerMatch = resolveNextcloudTalkAllowlistMatch({
304
- allowFrom: params.outerAllowFrom,
305
- senderId: params.senderId
306
- });
307
- const innerMatch = resolveNextcloudTalkAllowlistMatch({
308
- allowFrom: params.innerAllowFrom,
309
- senderId: params.senderId
310
- });
311
- return {
312
- allowed: evaluateMatchedGroupAccessForPolicy({
313
- groupPolicy: params.groupPolicy,
314
- allowlistConfigured: outerAllow.length > 0 || innerAllow.length > 0,
315
- allowlistMatched: resolveNestedAllowlistDecision({
316
- outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,
317
- outerMatched: outerAllow.length > 0 ? outerMatch.allowed : true,
318
- innerConfigured: innerAllow.length > 0,
319
- innerMatched: innerMatch.allowed
320
- })
321
- }).allowed,
322
- outerMatch: params.groupPolicy === "open" ? { allowed: true } : params.groupPolicy === "disabled" ? { allowed: false } : outerMatch,
323
- innerMatch: params.groupPolicy === "open" ? { allowed: true } : params.groupPolicy === "disabled" ? { allowed: false } : innerMatch
324
- };
325
- }
326
- function resolveNextcloudTalkMentionGate(params) {
327
- const shouldBypassMention = params.isGroup && params.requireMention && !params.wasMentioned && params.allowTextCommands && params.commandAuthorized && params.hasControlCommand;
328
- return {
329
- shouldBypassMention,
330
- shouldSkip: params.requireMention && !params.wasMentioned && !shouldBypassMention
331
- };
332
- }
333
507
  //#endregion
334
508
  //#region extensions/nextcloud-talk/src/room-info.ts
335
509
  const ROOM_CACHE_TTL_MS = 300 * 1e3;
@@ -338,19 +512,6 @@ const roomCache = /* @__PURE__ */ new Map();
338
512
  function resolveRoomCacheKey(params) {
339
513
  return `${params.accountId}:${params.roomToken}`;
340
514
  }
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
515
  function coerceRoomType(value) {
355
516
  if (typeof value === "number" && Number.isFinite(value)) return value;
356
517
  if (typeof value === "string" && value.trim()) {
@@ -375,16 +536,16 @@ async function resolveNextcloudTalkRoomKind(params) {
375
536
  if (cached.kind && age < ROOM_CACHE_TTL_MS) return cached.kind;
376
537
  if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) return;
377
538
  }
378
- const apiUser = account.config.apiUser?.trim();
379
- const apiPassword = readApiPassword({
539
+ const apiCredentials = resolveNextcloudTalkApiCredentials({
540
+ apiUser: account.config.apiUser,
380
541
  apiPassword: account.config.apiPassword,
381
542
  apiPasswordFile: account.config.apiPasswordFile
382
543
  });
383
- if (!apiUser || !apiPassword) return;
544
+ if (!apiCredentials) return;
384
545
  const baseUrl = account.baseUrl?.trim();
385
546
  if (!baseUrl) return;
386
547
  const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
387
- const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
548
+ const auth = Buffer.from(`${apiCredentials.apiUser}:${apiCredentials.apiPassword}`, "utf-8").toString("base64");
388
549
  try {
389
550
  const { response, release } = await fetchWithSsrFGuard({
390
551
  url,
@@ -450,58 +611,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
450
611
  return /^[a-z0-9]{8,}$/i.test(trimmed);
451
612
  }
452
613
  //#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
614
  //#region extensions/nextcloud-talk/src/send.ts
506
615
  function resolveCredentials(explicit, account) {
507
616
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
@@ -546,6 +655,18 @@ function recordNextcloudTalkOutboundActivity(accountId) {
546
655
  if (!(error instanceof Error) || error.message !== "Nextcloud Talk runtime not initialized") throw error;
547
656
  }
548
657
  }
658
+ function createNextcloudTalkSendReceipt(params) {
659
+ const messageId = params.messageId.trim();
660
+ return createMessageReceiptFromOutboundResults({
661
+ results: messageId && messageId !== "unknown" ? [{
662
+ channel: "nextcloud-talk",
663
+ messageId,
664
+ conversationId: params.roomToken
665
+ }] : [],
666
+ kind: "text",
667
+ ...params.replyTo ? { replyToId: params.replyTo } : {}
668
+ });
669
+ }
549
670
  async function sendMessageNextcloudTalk(to, text, opts) {
550
671
  const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
551
672
  const roomToken = normalizeRoomToken(to);
@@ -584,7 +705,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
584
705
  const status = response.status;
585
706
  let errorMsg = `Nextcloud Talk send failed (${status})`;
586
707
  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";
708
+ 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
709
  else if (status === 403) errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room";
589
710
  else if (status === 404) errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`;
590
711
  else if (errorBody) errorMsg = `Nextcloud Talk send failed: ${errorBody}`;
@@ -602,6 +723,11 @@ async function sendMessageNextcloudTalk(to, text, opts) {
602
723
  return {
603
724
  messageId,
604
725
  roomToken,
726
+ receipt: createNextcloudTalkSendReceipt({
727
+ messageId,
728
+ roomToken,
729
+ ...opts.replyTo ? { replyTo: opts.replyTo } : {}
730
+ }),
605
731
  timestamp
606
732
  };
607
733
  } finally {
@@ -611,6 +737,40 @@ async function sendMessageNextcloudTalk(to, text, opts) {
611
737
  //#endregion
612
738
  //#region extensions/nextcloud-talk/src/inbound.ts
613
739
  const CHANNEL_ID = "nextcloud-talk";
740
+ function hasAllowEntries(entries) {
741
+ return normalizeNextcloudTalkAllowlist(entries).length > 0;
742
+ }
743
+ function roomRoutes(params) {
744
+ if (!params.isGroup) return [];
745
+ const roomSenderConfigured = params.groupPolicy === "allowlist" && hasAllowEntries(params.roomAllowFrom);
746
+ return channelIngressRoutes(params.roomMatch.allowlistConfigured && {
747
+ id: "nextcloud-talk:room",
748
+ allowed: params.roomMatch.allowed,
749
+ precedence: 0,
750
+ matchId: "nextcloud-talk-room",
751
+ blockReason: "room_not_allowlisted"
752
+ }, params.roomConfig?.enabled === false && {
753
+ id: "nextcloud-talk:room-enabled",
754
+ enabled: false,
755
+ precedence: 10,
756
+ blockReason: "room_disabled"
757
+ }, roomSenderConfigured && {
758
+ id: "nextcloud-talk:room-sender",
759
+ kind: "nestedAllowlist",
760
+ precedence: 20,
761
+ blockReason: "room_sender_not_allowlisted",
762
+ ...!hasAllowEntries(params.outerGroupAllowFrom) ? {
763
+ senderPolicy: "replace",
764
+ senderAllowFrom: params.roomAllowFrom
765
+ } : {
766
+ allowed: resolveNextcloudTalkAllowlistMatch({
767
+ allowFrom: params.roomAllowFrom,
768
+ senderId: params.senderId
769
+ }).allowed,
770
+ matchId: "nextcloud-talk-room-sender"
771
+ }
772
+ });
773
+ }
614
774
  async function deliverNextcloudTalkReply(params) {
615
775
  const { cfg, payload, roomToken, accountId, statusSink } = params;
616
776
  await deliverFormattedTextWithAttachments({
@@ -646,83 +806,103 @@ async function handleNextcloudTalkInbound(params) {
646
806
  const roomToken = message.roomToken;
647
807
  const roomName = message.roomName;
648
808
  statusSink?.({ lastInboundAt: message.timestamp });
649
- const dmPolicy = account.config.dmPolicy ?? "pairing";
650
- const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
651
- const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
652
- providerConfigPresent: (config.channels?.["nextcloud-talk"] ?? void 0) !== void 0,
653
- groupPolicy: account.config.groupPolicy,
654
- defaultGroupPolicy
655
- });
656
- warnMissingProviderGroupPolicyFallbackOnce({
657
- providerMissingFallbackApplied,
658
- providerKey: "nextcloud-talk",
659
- accountId: account.accountId,
660
- blockedLabel: GROUP_POLICY_BLOCKED_LABEL.room,
661
- log: (message) => runtime.log?.(message)
662
- });
663
- const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
664
- const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
665
- const storeAllowList = normalizeNextcloudTalkAllowlist(await readStoreAllowFromForDmPolicy({
666
- provider: CHANNEL_ID,
667
- accountId: account.accountId,
668
- dmPolicy,
669
- readStore: pairing.readStoreForDmPolicy
670
- }));
671
809
  const roomMatch = resolveNextcloudTalkRoomMatch({
672
810
  rooms: account.config.rooms,
673
811
  roomToken
674
812
  });
675
813
  const roomConfig = roomMatch.roomConfig;
676
- if (isGroup && !roomMatch.allowed) {
677
- runtime.log?.(`nextcloud-talk: drop room ${roomToken} (not allowlisted)`);
678
- return;
679
- }
680
- if (roomConfig?.enabled === false) {
681
- runtime.log?.(`nextcloud-talk: drop room ${roomToken} (disabled)`);
682
- return;
683
- }
684
- const roomAllowFrom = normalizeNextcloudTalkAllowlist(roomConfig?.allowFrom);
685
814
  const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
686
815
  cfg: config,
687
816
  surface: CHANNEL_ID
688
817
  });
689
- const useAccessGroups = config.commands?.useAccessGroups !== false;
690
818
  const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
691
- const access = resolveDmGroupAccessWithCommandGate({
692
- isGroup,
693
- dmPolicy,
819
+ const shouldRequireMention = isGroup ? resolveNextcloudTalkRequireMention({
820
+ roomConfig,
821
+ wildcardConfig: roomMatch.wildcardConfig
822
+ }) : false;
823
+ const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
824
+ providerConfigPresent: (config.channels?.[CHANNEL_ID] ?? void 0) !== void 0,
825
+ groupPolicy: account.config.groupPolicy,
826
+ defaultGroupPolicy: resolveDefaultGroupPolicy(config)
827
+ });
828
+ const allowFrom = normalizeStringEntries(account.config.allowFrom);
829
+ const outerGroupAllowFrom = account.config.groupAllowFrom?.length ? normalizeStringEntries(account.config.groupAllowFrom) : allowFrom;
830
+ const roomAllowFrom = normalizeStringEntries(roomConfig?.allowFrom);
831
+ const resolveAccess = async (wasMentioned) => await resolveStableChannelMessageIngress({
832
+ channelId: CHANNEL_ID,
833
+ accountId: account.accountId,
834
+ identity: {
835
+ key: "nextcloud-talk-user-id",
836
+ normalize: (value) => normalizeNextcloudTalkAllowEntry(value) || null,
837
+ sensitivity: "pii",
838
+ entryIdPrefix: "nextcloud-talk-entry"
839
+ },
840
+ cfg: config,
841
+ readStoreAllowFrom: async () => await pairing.readStoreForDmPolicy(CHANNEL_ID, account.accountId),
842
+ subject: { stableId: senderId },
843
+ conversation: {
844
+ kind: isGroup ? "group" : "direct",
845
+ id: isGroup ? roomToken : senderId
846
+ },
847
+ route: roomRoutes({
848
+ isGroup,
849
+ groupPolicy,
850
+ roomMatch,
851
+ roomConfig,
852
+ senderId,
853
+ outerGroupAllowFrom,
854
+ roomAllowFrom
855
+ }),
856
+ dmPolicy: account.config.dmPolicy ?? "pairing",
694
857
  groupPolicy,
695
- allowFrom: configAllowFrom,
696
- groupAllowFrom: configGroupAllowFrom,
697
- storeAllowFrom: storeAllowList,
698
- isSenderAllowed: (allowFrom) => resolveNextcloudTalkAllowlistMatch({
699
- allowFrom,
700
- senderId
701
- }).allowed,
858
+ policy: {
859
+ groupAllowFromFallbackToAllowFrom: true,
860
+ activation: {
861
+ requireMention: isGroup && shouldRequireMention,
862
+ allowTextCommands
863
+ }
864
+ },
865
+ mentionFacts: isGroup && wasMentioned !== void 0 ? {
866
+ canDetectMention: true,
867
+ wasMentioned,
868
+ hasAnyMention: wasMentioned
869
+ } : void 0,
870
+ allowFrom,
871
+ groupAllowFrom: account.config.groupAllowFrom,
702
872
  command: {
703
- useAccessGroups,
704
873
  allowTextCommands,
705
874
  hasControlCommand
706
875
  }
707
876
  });
708
- const commandAuthorized = access.commandAuthorized;
709
- const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
877
+ let access = await resolveAccess();
878
+ warnMissingProviderGroupPolicyFallbackOnce({
879
+ providerMissingFallbackApplied,
880
+ providerKey: "nextcloud-talk",
881
+ accountId: account.accountId,
882
+ blockedLabel: GROUP_POLICY_BLOCKED_LABEL.room,
883
+ log: (message) => runtime.log?.(message)
884
+ });
885
+ const commandAuthorized = access.commandAccess.authorized;
886
+ const accessReason = access.ingress.reasonCode === "route_blocked" ? "route blocked" : access.senderAccess.reasonCode;
710
887
  if (isGroup) {
711
- if (access.decision !== "allow") {
712
- runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${access.reason})`);
888
+ if (access.routeAccess.reason === "room_not_allowlisted") {
889
+ runtime.log?.(`nextcloud-talk: drop room ${roomToken} (not allowlisted)`);
713
890
  return;
714
891
  }
715
- if (!resolveNextcloudTalkGroupAllow({
716
- groupPolicy,
717
- outerAllowFrom: effectiveGroupAllowFrom,
718
- innerAllowFrom: roomAllowFrom,
719
- senderId
720
- }).allowed) {
892
+ if (access.routeAccess.reason === "room_disabled") {
893
+ runtime.log?.(`nextcloud-talk: drop room ${roomToken} (disabled)`);
894
+ return;
895
+ }
896
+ if (access.routeAccess.reason === "room_sender_not_allowlisted") {
721
897
  runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`);
722
898
  return;
723
899
  }
724
- } else if (access.decision !== "allow") {
725
- if (access.decision === "pairing") await pairing.issueChallenge({
900
+ if (access.senderAccess.decision !== "allow") {
901
+ runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${accessReason})`);
902
+ return;
903
+ }
904
+ } else if (access.senderAccess.decision !== "allow") {
905
+ if (access.senderAccess.decision === "pairing") await pairing.issueChallenge({
726
906
  senderId,
727
907
  senderIdLine: `Your Nextcloud user id: ${senderId}`,
728
908
  meta: { name: senderName || void 0 },
@@ -737,10 +917,10 @@ async function handleNextcloudTalkInbound(params) {
737
917
  runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
738
918
  }
739
919
  });
740
- runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
920
+ runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${accessReason})`);
741
921
  return;
742
922
  }
743
- if (access.shouldBlockControlCommand) {
923
+ if (access.commandAccess.shouldBlockControlCommand) {
744
924
  logInboundDrop({
745
925
  log: (message) => runtime.log?.(message),
746
926
  channel: CHANNEL_ID,
@@ -751,43 +931,27 @@ async function handleNextcloudTalkInbound(params) {
751
931
  }
752
932
  const mentionRegexes = core.channel.mentions.buildMentionRegexes(config);
753
933
  const wasMentioned = mentionRegexes.length ? core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) : false;
754
- const mentionGate = resolveNextcloudTalkMentionGate({
755
- isGroup,
756
- requireMention: isGroup ? resolveNextcloudTalkRequireMention({
757
- roomConfig,
758
- wildcardConfig: roomMatch.wildcardConfig
759
- }) : false,
760
- wasMentioned,
761
- allowTextCommands,
762
- hasControlCommand,
763
- commandAuthorized
764
- });
765
- if (isGroup && mentionGate.shouldSkip) {
934
+ if (isGroup) access = await resolveAccess(wasMentioned);
935
+ if (isGroup && access.activationAccess.shouldSkip) {
766
936
  runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`);
767
937
  return;
768
938
  }
769
- const route = core.channel.routing.resolveAgentRoute({
939
+ const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
770
940
  cfg: config,
771
941
  channel: CHANNEL_ID,
772
942
  accountId: account.accountId,
773
943
  peer: {
774
944
  kind: isGroup ? "group" : "direct",
775
945
  id: isGroup ? roomToken : senderId
776
- }
946
+ },
947
+ runtime: core.channel,
948
+ sessionStore: config.session?.store
777
949
  });
778
950
  const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
779
- const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
780
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
781
- const previousTimestamp = core.channel.session.readSessionUpdatedAt({
782
- storePath,
783
- sessionKey: route.sessionKey
784
- });
785
- const body = core.channel.reply.formatAgentEnvelope({
951
+ const { storePath, body } = buildEnvelope({
786
952
  channel: "Nextcloud Talk",
787
953
  from: fromLabel,
788
954
  timestamp: message.timestamp,
789
- previousTimestamp,
790
- envelope: envelopeOptions,
791
955
  body: rawBody
792
956
  });
793
957
  const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
@@ -815,33 +979,38 @@ async function handleNextcloudTalkInbound(params) {
815
979
  OriginatingTo: `nextcloud-talk:${roomToken}`,
816
980
  CommandAuthorized: commandAuthorized
817
981
  });
818
- await dispatchInboundReplyWithBase({
982
+ await core.channel.turn.runAssembled({
819
983
  cfg: config,
820
984
  channel: CHANNEL_ID,
821
985
  accountId: account.accountId,
822
- route,
986
+ agentId: route.agentId,
987
+ routeSessionKey: route.sessionKey,
823
988
  storePath,
824
989
  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) => {
836
- 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)}`);
990
+ recordInboundSession: core.channel.session.recordInboundSession,
991
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
992
+ delivery: {
993
+ deliver: async (payload) => {
994
+ await deliverNextcloudTalkReply({
995
+ cfg: config,
996
+ payload,
997
+ roomToken,
998
+ accountId: account.accountId,
999
+ statusSink
1000
+ });
1001
+ },
1002
+ onError: (err, info) => {
1003
+ runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
1004
+ }
840
1005
  },
1006
+ replyPipeline: {},
841
1007
  replyOptions: {
842
1008
  skillFilter: roomConfig?.skills,
843
1009
  disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
844
- }
1010
+ },
1011
+ record: { onRecordError: (err) => {
1012
+ runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
1013
+ } }
845
1014
  });
846
1015
  }
847
1016
  //#endregion
@@ -851,28 +1020,28 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
851
1020
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
852
1021
  const HEALTH_PATH = "/healthz";
853
1022
  const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
854
- const NextcloudTalkWebhookPayloadSchema = z$1.object({
855
- type: z$1.enum([
1023
+ const NextcloudTalkWebhookPayloadSchema = z.object({
1024
+ type: z.enum([
856
1025
  "Create",
857
1026
  "Update",
858
1027
  "Delete"
859
1028
  ]),
860
- actor: z$1.object({
861
- type: z$1.literal("Person"),
862
- id: z$1.string().min(1),
863
- name: z$1.string()
1029
+ actor: z.object({
1030
+ type: z.literal("Person"),
1031
+ id: z.string().min(1),
1032
+ name: z.string()
864
1033
  }),
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()
1034
+ object: z.object({
1035
+ type: z.literal("Note"),
1036
+ id: z.string().min(1),
1037
+ name: z.string(),
1038
+ content: z.string(),
1039
+ mediaType: z.string()
871
1040
  }),
872
- target: z$1.object({
873
- type: z$1.literal("Collection"),
874
- id: z$1.string().min(1),
875
- name: z$1.string()
1041
+ target: z.object({
1042
+ type: z.literal("Collection"),
1043
+ id: z.string().min(1),
1044
+ name: z.string()
876
1045
  })
877
1046
  });
878
1047
  const WEBHOOK_ERRORS = {
@@ -1334,6 +1503,28 @@ const nextcloudTalkGatewayAdapter = {
1334
1503
  }
1335
1504
  };
1336
1505
  //#endregion
1506
+ //#region extensions/nextcloud-talk/src/message-adapter.ts
1507
+ const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
1508
+ id: "nextcloud-talk",
1509
+ durableFinal: { capabilities: {
1510
+ text: true,
1511
+ media: true,
1512
+ replyTo: true
1513
+ } },
1514
+ send: {
1515
+ text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageNextcloudTalk(to, text, {
1516
+ accountId: accountId ?? void 0,
1517
+ replyTo: replyToId ?? void 0,
1518
+ cfg
1519
+ }),
1520
+ media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageNextcloudTalk(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
1521
+ accountId: accountId ?? void 0,
1522
+ replyTo: replyToId ?? void 0,
1523
+ cfg
1524
+ })
1525
+ }
1526
+ });
1527
+ //#endregion
1337
1528
  //#region extensions/nextcloud-talk/src/session-route.ts
1338
1529
  function resolveNextcloudTalkOutboundSessionRoute(params) {
1339
1530
  const roomId = stripNextcloudTalkTargetPrefix(params.target);
@@ -1538,7 +1729,7 @@ const nextcloudTalkSetupWizard = {
1538
1729
  title: "Nextcloud Talk bot setup",
1539
1730
  lines: [
1540
1731
  "1) SSH into your Nextcloud server",
1541
- "2) Run: ./occ talk:bot:install \"OpenClaw\" \"<shared-secret>\" \"<webhook-url>\" --feature reaction",
1732
+ "2) Run: ./occ talk:bot:install \"OpenClaw\" \"<shared-secret>\" \"<webhook-url>\" --feature webhook --feature response --feature reaction",
1542
1733
  "3) Copy the shared secret you used in the command",
1543
1734
  "4) Enable the bot in your Nextcloud Talk room settings",
1544
1735
  "Tip: you can also set NEXTCLOUD_TALK_BOT_SECRET in your env.",
@@ -1740,6 +1931,21 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
1740
1931
  status: createComputedAccountStatusAdapter({
1741
1932
  defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
1742
1933
  buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
1934
+ collectStatusIssues: (accounts) => accounts.flatMap((account) => {
1935
+ const probe = account.probe;
1936
+ if (!probe || probe.ok !== false || probe.code !== "missing_response_feature" || !probe.message) return [];
1937
+ return [{
1938
+ channel: "nextcloud-talk",
1939
+ accountId: account.accountId ?? DEFAULT_ACCOUNT_ID$2,
1940
+ kind: "config",
1941
+ message: probe.message,
1942
+ fix: "Add --feature response to the Talk bot."
1943
+ }];
1944
+ }),
1945
+ probeAccount: async ({ account, timeoutMs }) => await probeNextcloudTalkBotResponseFeature({
1946
+ account,
1947
+ timeoutMs
1948
+ }),
1743
1949
  resolveAccountSnapshot: ({ account }) => ({
1744
1950
  accountId: account.accountId,
1745
1951
  name: account.name,
@@ -1752,7 +1958,8 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
1752
1958
  }
1753
1959
  })
1754
1960
  }),
1755
- gateway: nextcloudTalkGatewayAdapter
1961
+ gateway: nextcloudTalkGatewayAdapter,
1962
+ message: nextcloudTalkMessageAdapter
1756
1963
  },
1757
1964
  pairing: { text: {
1758
1965
  ...nextcloudTalkPairingTextAdapter,
@@ -1771,15 +1978,20 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
1771
1978
  },
1772
1979
  attachedResults: {
1773
1980
  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
1981
+ sendText: async ({ cfg, to, text, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.text({
1982
+ cfg,
1983
+ to,
1984
+ text,
1985
+ accountId,
1986
+ replyToId
1778
1987
  }),
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
1988
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.media({
1989
+ cfg,
1990
+ to,
1991
+ text,
1992
+ mediaUrl: mediaUrl ?? "",
1993
+ accountId,
1994
+ replyToId
1783
1995
  })
1784
1996
  }
1785
1997
  }
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-BVVRsVr5.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-DSNSV1Y5.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,9 @@
1
1
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
2
- import { readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithCommandGate } from "openclaw/plugin-sdk/channel-policy";
3
- import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
4
2
  import { logInboundDrop } from "openclaw/plugin-sdk/channel-logging";
5
3
  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";
4
+ import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
7
5
  import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
6
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
8
7
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
9
8
  //#region extensions/nextcloud-talk/src/runtime.ts
10
9
  const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime } = createPluginRuntimeStore({
@@ -12,4 +11,4 @@ const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime
12
11
  errorMessage: "Nextcloud Talk runtime not initialized"
13
12
  });
14
13
  //#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 };
14
+ export { fetchWithSsrFGuard as a, resolveDefaultGroupPolicy as c, setNextcloudTalkRuntime as d, deliverFormattedTextWithAttachments as i, warnMissingProviderGroupPolicyFallbackOnce as l, createChannelMessageReplyPipeline as n, logInboundDrop as o, createChannelPairingController as r, resolveAllowlistProviderRuntimeGroupPolicy as s, GROUP_POLICY_BLOCKED_LABEL as t, getNextcloudTalkRuntime 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 resolveDefaultGroupPolicy, d as setNextcloudTalkRuntime, i as deliverFormattedTextWithAttachments, l as warnMissingProviderGroupPolicyFallbackOnce, n as createChannelMessageReplyPipeline, o as logInboundDrop, r as createChannelPairingController, s as resolveAllowlistProviderRuntimeGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL } from "./runtime-api-D_apwxTv.js";
2
+ export { GROUP_POLICY_BLOCKED_LABEL, createChannelMessageReplyPipeline, createChannelPairingController, deliverFormattedTextWithAttachments, fetchWithSsrFGuard, logInboundDrop, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, setNextcloudTalkRuntime, warnMissingProviderGroupPolicyFallbackOnce };
package/package.json CHANGED
@@ -1,21 +1,18 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.5.7",
3
+ "version": "2026.5.10-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"
15
+ "openclaw": ">=2026.5.10-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"
47
+ "pluginApi": ">=2026.5.10-beta.1"
51
48
  },
52
49
  "build": {
53
- "openclawVersion": "2026.5.7"
50
+ "openclawVersion": "2026.5.10-beta.1"
54
51
  },
55
52
  "release": {
56
53
  "publishToClawHub": true,