@openclaw/nextcloud-talk 2026.9.1-beta.1 → 2026.9.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-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-D2RVrR4p.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -7,31 +7,32 @@ import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from
|
|
|
7
7
|
import { createAllowlistProviderRouteAllowlistWarningCollector, createConditionalWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
|
|
8
8
|
import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
9
9
|
import { FormatCapabilityProfile, convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
10
|
-
import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
11
|
-
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
10
|
import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
|
|
13
11
|
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
|
|
14
12
|
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
15
|
-
import { buildSecretInputSchema, hasConfiguredSecretInput,
|
|
13
|
+
import { buildSecretInputSchema, hasConfiguredSecretInput, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
14
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
15
|
+
import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
16
16
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
17
17
|
import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
18
|
-
import { readProviderJsonResponse,
|
|
18
|
+
import { readProviderJsonResponse, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
|
|
19
19
|
import { resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
20
|
import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
|
|
21
21
|
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
22
|
+
import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
|
|
22
23
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
23
24
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
24
25
|
import { createHmac, randomBytes } from "node:crypto";
|
|
25
|
-
import {
|
|
26
|
-
import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
26
|
+
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
27
27
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
|
|
28
28
|
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
29
29
|
import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
|
|
30
|
-
import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
30
|
+
import { adaptScopedAccountAccessor, clearAccountFieldsFromConfigSection, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
31
31
|
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
32
32
|
import { z } from "zod";
|
|
33
33
|
import os from "node:os";
|
|
34
34
|
import path from "node:path";
|
|
35
|
+
import { fileExists } from "openclaw/plugin-sdk/file-access-runtime";
|
|
35
36
|
import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
36
37
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
37
38
|
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -41,6 +42,7 @@ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
|
41
42
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
42
43
|
import { createServer } from "node:http";
|
|
43
44
|
import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
|
|
45
|
+
import { sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
44
46
|
import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
45
47
|
import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
|
|
46
48
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
|
|
@@ -53,17 +55,19 @@ import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-to
|
|
|
53
55
|
function resolveNextcloudTalkApiCredentialsResult(params) {
|
|
54
56
|
const apiUser = params.apiUser?.trim();
|
|
55
57
|
if (!apiUser) return { status: "missing" };
|
|
56
|
-
const inlinePassword =
|
|
58
|
+
const inlinePassword = resolveSecretInputString({
|
|
57
59
|
value: params.apiPassword,
|
|
58
|
-
path: "channels.nextcloud-talk.apiPassword"
|
|
60
|
+
path: "channels.nextcloud-talk.apiPassword",
|
|
61
|
+
mode: params.mode
|
|
59
62
|
});
|
|
60
|
-
if (inlinePassword) return {
|
|
63
|
+
if (inlinePassword.status === "available") return {
|
|
61
64
|
status: "available",
|
|
62
65
|
value: {
|
|
63
66
|
apiUser,
|
|
64
|
-
apiPassword: inlinePassword
|
|
67
|
+
apiPassword: inlinePassword.value
|
|
65
68
|
}
|
|
66
69
|
};
|
|
70
|
+
if (inlinePassword.status === "configured_unavailable") return { status: "configured_unavailable" };
|
|
67
71
|
if (!params.apiPasswordFile?.trim()) return { status: "missing" };
|
|
68
72
|
const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
|
|
69
73
|
if (result.status === "available") return result.value ? {
|
|
@@ -180,15 +184,21 @@ function inspectNextcloudTalkAccount(params) {
|
|
|
180
184
|
apiUser: account.config.apiUser,
|
|
181
185
|
apiPassword: account.config.apiPassword,
|
|
182
186
|
apiPasswordFile: account.config.apiPasswordFile,
|
|
183
|
-
configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile
|
|
187
|
+
configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`,
|
|
188
|
+
mode: "inspect"
|
|
184
189
|
});
|
|
185
|
-
const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" ? [apiCredentialResolution.diagnostic] : []];
|
|
190
|
+
const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" && apiCredentialResolution.diagnostic ? [apiCredentialResolution.diagnostic] : []];
|
|
186
191
|
return {
|
|
187
192
|
...account,
|
|
193
|
+
configured: isNextcloudTalkAccountConfigured(account),
|
|
194
|
+
mode: "webhook",
|
|
188
195
|
apiCredentialStatus: apiCredentialResolution.status,
|
|
189
196
|
...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
|
|
190
197
|
};
|
|
191
198
|
}
|
|
199
|
+
function isNextcloudTalkAccountConfigured(account) {
|
|
200
|
+
return Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim());
|
|
201
|
+
}
|
|
192
202
|
//#endregion
|
|
193
203
|
//#region extensions/nextcloud-talk/src/approval-auth.ts
|
|
194
204
|
function normalizeNextcloudTalkApproverId(value) {
|
|
@@ -210,6 +220,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
|
|
|
210
220
|
if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
|
|
211
221
|
await params.release();
|
|
212
222
|
}
|
|
223
|
+
async function readNextcloudTalkErrorBody(response, ...credentials) {
|
|
224
|
+
try {
|
|
225
|
+
let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
|
|
226
|
+
maxBytes: 8192,
|
|
227
|
+
chunkTimeoutMs: 1e4
|
|
228
|
+
});
|
|
229
|
+
for (const credential of credentials) body = body.replaceAll(credential, "***");
|
|
230
|
+
return redactToolPayloadText(body);
|
|
231
|
+
} catch {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
213
235
|
//#endregion
|
|
214
236
|
//#region extensions/nextcloud-talk/src/signature.ts
|
|
215
237
|
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
|
|
@@ -222,7 +244,8 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
|
|
|
222
244
|
function verifyNextcloudTalkSignature(params) {
|
|
223
245
|
const { signature, random, body, secret } = params;
|
|
224
246
|
if (!signature || !random || !secret) return false;
|
|
225
|
-
|
|
247
|
+
const expected = createHmac("sha256", secret).update(random + body).digest("hex");
|
|
248
|
+
return safeEqualSecret(signature, expected);
|
|
226
249
|
}
|
|
227
250
|
/**
|
|
228
251
|
* Extract webhook headers from an incoming request.
|
|
@@ -256,7 +279,6 @@ function generateNextcloudTalkSignature(params) {
|
|
|
256
279
|
//#endregion
|
|
257
280
|
//#region extensions/nextcloud-talk/src/bot-preflight.ts
|
|
258
281
|
const BOT_FEATURE_RESPONSE = 2;
|
|
259
|
-
const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
|
260
282
|
function normalizeUrlForMatch(value) {
|
|
261
283
|
if (!value?.trim()) return "";
|
|
262
284
|
try {
|
|
@@ -321,7 +343,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
|
|
|
321
343
|
});
|
|
322
344
|
try {
|
|
323
345
|
if (!response.ok) {
|
|
324
|
-
const body = await
|
|
346
|
+
const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
|
|
325
347
|
return {
|
|
326
348
|
ok: false,
|
|
327
349
|
code: "api_error",
|
|
@@ -373,7 +395,16 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
|
|
|
373
395
|
sectionKey: "nextcloud-talk",
|
|
374
396
|
listAccountIds: listNextcloudTalkAccountIds,
|
|
375
397
|
resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
|
|
376
|
-
inspectAccount:
|
|
398
|
+
inspectAccount: (cfg, accountId) => {
|
|
399
|
+
const account = inspectNextcloudTalkAccount({
|
|
400
|
+
cfg,
|
|
401
|
+
accountId
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
...account,
|
|
405
|
+
baseUrl: account.baseUrl ? "[set]" : "[missing]"
|
|
406
|
+
};
|
|
407
|
+
},
|
|
377
408
|
defaultAccountId: resolveDefaultNextcloudTalkAccountId,
|
|
378
409
|
clearBaseFields: [
|
|
379
410
|
"botSecret",
|
|
@@ -405,7 +436,7 @@ const NextcloudTalkRoomSchema = buildGroupEntrySchema({ allowFrom: z.array(z.str
|
|
|
405
436
|
const NextcloudTalkNetworkSchema = z.object({
|
|
406
437
|
/** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
|
|
407
438
|
dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
|
|
408
|
-
const
|
|
439
|
+
const NextcloudTalkAccountSchemaBase = z.object({
|
|
409
440
|
name: z.string().optional(),
|
|
410
441
|
enabled: z.boolean().optional(),
|
|
411
442
|
configWrites: z.boolean().optional(),
|
|
@@ -429,7 +460,8 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
429
460
|
/** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
|
|
430
461
|
network: NextcloudTalkNetworkSchema,
|
|
431
462
|
...ReplyRuntimeConfigSchemaShape
|
|
432
|
-
}).strict()
|
|
463
|
+
}).strict();
|
|
464
|
+
const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(NextcloudTalkAccountSchemaBase, {
|
|
433
465
|
optionalAccount: true,
|
|
434
466
|
refine: (value, ctx) => {
|
|
435
467
|
requireChannelOpenAllowFrom({
|
|
@@ -445,7 +477,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
445
477
|
//#region extensions/nextcloud-talk/src/replay-migration-contract.ts
|
|
446
478
|
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
|
|
447
479
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
|
|
448
|
-
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS =
|
|
480
|
+
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 864e5;
|
|
449
481
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
450
482
|
//#endregion
|
|
451
483
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
@@ -525,10 +557,12 @@ function normalizeNextcloudTalkAllowlist(values) {
|
|
|
525
557
|
return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
|
|
526
558
|
}
|
|
527
559
|
function resolveNextcloudTalkAllowlistMatch(params) {
|
|
560
|
+
const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
|
|
561
|
+
const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
|
|
528
562
|
return resolveAllowlistMatchByCandidates({
|
|
529
|
-
allowList:
|
|
563
|
+
allowList: allowFrom,
|
|
530
564
|
candidates: [{
|
|
531
|
-
value:
|
|
565
|
+
value: senderId,
|
|
532
566
|
source: "id"
|
|
533
567
|
}]
|
|
534
568
|
});
|
|
@@ -596,8 +630,8 @@ function resolveNextcloudTalkGroupRequireMention(params) {
|
|
|
596
630
|
}
|
|
597
631
|
//#endregion
|
|
598
632
|
//#region extensions/nextcloud-talk/src/room-info.ts
|
|
599
|
-
const ROOM_CACHE_TTL_MS =
|
|
600
|
-
const ROOM_CACHE_ERROR_TTL_MS =
|
|
633
|
+
const ROOM_CACHE_TTL_MS = 3e5;
|
|
634
|
+
const ROOM_CACHE_ERROR_TTL_MS = 3e4;
|
|
601
635
|
const ROOM_CACHE_MAX_ENTRIES = 1e3;
|
|
602
636
|
const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
|
|
603
637
|
const roomCache = /* @__PURE__ */ new Map();
|
|
@@ -659,7 +693,8 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
659
693
|
runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
|
|
660
694
|
return;
|
|
661
695
|
}
|
|
662
|
-
const
|
|
696
|
+
const payload = await readProviderJsonResponse(response, "Nextcloud Talk room info failed");
|
|
697
|
+
const kind = resolveRoomKindFromType(parseStrictPositiveInteger(payload.ocs?.data?.type));
|
|
663
698
|
cacheRoomInfo(key, {
|
|
664
699
|
fetchedAt: Date.now(),
|
|
665
700
|
kind
|
|
@@ -705,7 +740,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
|
|
|
705
740
|
}
|
|
706
741
|
//#endregion
|
|
707
742
|
//#region extensions/nextcloud-talk/src/send.ts
|
|
708
|
-
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
|
|
709
743
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
|
|
710
744
|
const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
|
|
711
745
|
const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
|
|
@@ -726,20 +760,12 @@ function renderNextcloudTalkMarkdown(markdown) {
|
|
|
726
760
|
escapeText: (text) => text
|
|
727
761
|
}, NEXTCLOUD_TALK_FORMAT_PROFILE);
|
|
728
762
|
}
|
|
729
|
-
/** Collapses
|
|
763
|
+
/** Collapses and caps an already-redacted error body for display. */
|
|
730
764
|
function collapseErrorSnippet(text) {
|
|
731
765
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
732
766
|
if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
|
|
733
767
|
return collapsed;
|
|
734
768
|
}
|
|
735
|
-
/** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
|
|
736
|
-
async function readNextcloudTalkErrorSnippet(response) {
|
|
737
|
-
try {
|
|
738
|
-
return collapseErrorSnippet(await readResponseTextLimited(response, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES));
|
|
739
|
-
} catch {
|
|
740
|
-
return "";
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
769
|
function resolveCredentials(explicit, account) {
|
|
744
770
|
const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
|
|
745
771
|
const secret = explicit.secret?.trim() ?? account.secret;
|
|
@@ -812,8 +838,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
812
838
|
body: message,
|
|
813
839
|
secret
|
|
814
840
|
});
|
|
841
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
|
|
815
842
|
const { response, release } = await fetchWithSsrFGuard({
|
|
816
|
-
url
|
|
843
|
+
url,
|
|
817
844
|
init: {
|
|
818
845
|
method: "POST",
|
|
819
846
|
headers: {
|
|
@@ -830,7 +857,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
830
857
|
});
|
|
831
858
|
try {
|
|
832
859
|
if (!response.ok) {
|
|
833
|
-
const errorBody = await
|
|
860
|
+
const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
|
|
834
861
|
const status = response.status;
|
|
835
862
|
let errorMsg = `Nextcloud Talk send failed (${status})`;
|
|
836
863
|
if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
|
|
@@ -874,8 +901,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
874
901
|
body: reaction,
|
|
875
902
|
secret
|
|
876
903
|
});
|
|
904
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
|
|
877
905
|
const { response, release } = await fetchWithSsrFGuard({
|
|
878
|
-
url
|
|
906
|
+
url,
|
|
879
907
|
init: {
|
|
880
908
|
method: "POST",
|
|
881
909
|
headers: {
|
|
@@ -892,7 +920,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
892
920
|
});
|
|
893
921
|
try {
|
|
894
922
|
if (!response.ok) {
|
|
895
|
-
const errorBody = await
|
|
923
|
+
const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
|
|
896
924
|
throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
|
|
897
925
|
}
|
|
898
926
|
return { ok: true };
|
|
@@ -963,7 +991,15 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
963
991
|
accountId: account.accountId
|
|
964
992
|
});
|
|
965
993
|
const rawBody = message.text?.trim() ?? "";
|
|
966
|
-
if (!rawBody)
|
|
994
|
+
if (!rawBody) {
|
|
995
|
+
logInboundDrop({
|
|
996
|
+
log: (messageLocal) => runtime.log?.(messageLocal),
|
|
997
|
+
channel: CHANNEL_ID,
|
|
998
|
+
reason: `empty message body (mediaType=${message.mediaType})`,
|
|
999
|
+
target: message.senderId
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
967
1003
|
const roomKind = await resolveNextcloudTalkRoomKind({
|
|
968
1004
|
account,
|
|
969
1005
|
roomToken: message.roomToken,
|
|
@@ -1279,8 +1315,8 @@ async function migrateNextcloudTalkLegacyReplayState(params) {
|
|
|
1279
1315
|
}
|
|
1280
1316
|
//#endregion
|
|
1281
1317
|
//#region extensions/nextcloud-talk/src/monitor.ts
|
|
1282
|
-
const DEFAULT_WEBHOOK_MAX_BODY_BYTES =
|
|
1283
|
-
const PREAUTH_WEBHOOK_MAX_BODY_BYTES =
|
|
1318
|
+
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1319
|
+
const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
|
|
1284
1320
|
const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
|
|
1285
1321
|
const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
|
|
1286
1322
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
@@ -1336,9 +1372,14 @@ function verifyWebhookSignature(params) {
|
|
|
1336
1372
|
function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
1337
1373
|
return readRequestBodyWithLimit(req, {
|
|
1338
1374
|
maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
|
|
1339
|
-
timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS
|
|
1375
|
+
timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS,
|
|
1376
|
+
destroyOnLimit: false
|
|
1340
1377
|
});
|
|
1341
1378
|
}
|
|
1379
|
+
async function rejectWebhookRequest(req, res, status, error) {
|
|
1380
|
+
if (res.headersSent) return;
|
|
1381
|
+
await sendHttpRequestRejection(req, res, status, JSON.stringify({ error }), "application/json");
|
|
1382
|
+
}
|
|
1342
1383
|
function createNextcloudTalkWebhookServer(opts) {
|
|
1343
1384
|
const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
|
|
1344
1385
|
const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
|
|
@@ -1391,11 +1432,11 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1391
1432
|
writeJsonResponse(res, 200);
|
|
1392
1433
|
} catch (err) {
|
|
1393
1434
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1394
|
-
|
|
1435
|
+
await rejectWebhookRequest(req, res, 413, WEBHOOK_ERRORS.payloadTooLarge);
|
|
1395
1436
|
return;
|
|
1396
1437
|
}
|
|
1397
1438
|
if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
|
|
1398
|
-
|
|
1439
|
+
await rejectWebhookRequest(req, res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
|
|
1399
1440
|
return;
|
|
1400
1441
|
}
|
|
1401
1442
|
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
@@ -1439,8 +1480,10 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1439
1480
|
});
|
|
1440
1481
|
});
|
|
1441
1482
|
};
|
|
1442
|
-
if (abortSignal)
|
|
1443
|
-
|
|
1483
|
+
if (abortSignal) {
|
|
1484
|
+
if (abortSignal.aborted) stop();
|
|
1485
|
+
else abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1486
|
+
}
|
|
1444
1487
|
return {
|
|
1445
1488
|
server,
|
|
1446
1489
|
start,
|
|
@@ -1450,6 +1493,12 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1450
1493
|
//#endregion
|
|
1451
1494
|
//#region extensions/nextcloud-talk/src/webhook-spool.ts
|
|
1452
1495
|
const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
|
|
1496
|
+
function describeIgnoredWebhookEvent(rawEvent) {
|
|
1497
|
+
const envelope = parseRawObject(rawEvent);
|
|
1498
|
+
const type = typeof envelope.type === "string" ? envelope.type : "unknown";
|
|
1499
|
+
const object = isRecord(envelope.object) ? envelope.object : null;
|
|
1500
|
+
return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
|
|
1501
|
+
}
|
|
1453
1502
|
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1454
1503
|
type: z.enum([
|
|
1455
1504
|
"Create",
|
|
@@ -1576,7 +1625,11 @@ function createNextcloudTalkWebhookSpool(options) {
|
|
|
1576
1625
|
if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
|
|
1577
1626
|
const receiveTask = (async () => {
|
|
1578
1627
|
await startAfterMigration;
|
|
1579
|
-
|
|
1628
|
+
if ((await monitor.admit(rawEvent)).kind === "ignored") {
|
|
1629
|
+
options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
|
|
1630
|
+
return "ignored";
|
|
1631
|
+
}
|
|
1632
|
+
return "accepted";
|
|
1580
1633
|
})();
|
|
1581
1634
|
inFlightReceives.add(receiveTask);
|
|
1582
1635
|
receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
|
|
@@ -1720,44 +1773,18 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1720
1773
|
});
|
|
1721
1774
|
},
|
|
1722
1775
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
delete nextSection.botSecret;
|
|
1730
|
-
cleared = true;
|
|
1731
|
-
changed = true;
|
|
1732
|
-
}
|
|
1733
|
-
const accountCleanup = clearAccountEntryFields({
|
|
1734
|
-
accounts: nextSection.accounts,
|
|
1735
|
-
accountId,
|
|
1736
|
-
fields: ["botSecret"]
|
|
1737
|
-
});
|
|
1738
|
-
if (accountCleanup.changed) {
|
|
1739
|
-
changed = true;
|
|
1740
|
-
if (accountCleanup.cleared) cleared = true;
|
|
1741
|
-
if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
|
|
1742
|
-
else delete nextSection.accounts;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
|
|
1746
|
-
...nextCfg.channels,
|
|
1747
|
-
"nextcloud-talk": nextSection
|
|
1748
|
-
};
|
|
1749
|
-
else {
|
|
1750
|
-
const nextChannels = { ...nextCfg.channels };
|
|
1751
|
-
delete nextChannels["nextcloud-talk"];
|
|
1752
|
-
if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
|
|
1753
|
-
else delete nextCfg.channels;
|
|
1754
|
-
}
|
|
1776
|
+
const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
|
|
1777
|
+
cfg,
|
|
1778
|
+
sectionKey: "nextcloud-talk",
|
|
1779
|
+
accountId,
|
|
1780
|
+
fields: ["botSecret"]
|
|
1781
|
+
});
|
|
1755
1782
|
const loggedOut = resolveNextcloudTalkAccount({
|
|
1756
|
-
cfg:
|
|
1783
|
+
cfg: nextConfig,
|
|
1757
1784
|
accountId
|
|
1758
1785
|
}).secretSource === "none";
|
|
1759
1786
|
if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
|
|
1760
|
-
nextConfig
|
|
1787
|
+
nextConfig,
|
|
1761
1788
|
afterWrite: { mode: "auto" }
|
|
1762
1789
|
});
|
|
1763
1790
|
return {
|
|
@@ -1939,6 +1966,49 @@ const nextcloudTalkDmPolicy = createChannelDmPolicy({
|
|
|
1939
1966
|
applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
|
|
1940
1967
|
promptAllowFrom: promptNextcloudTalkAllowFromForAccount
|
|
1941
1968
|
});
|
|
1969
|
+
const nextcloudTalkSetupAdapter = {
|
|
1970
|
+
singleAccountKeysToMove: ["rooms"],
|
|
1971
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
1972
|
+
prepareAccountConfigInput: ({ input }) => {
|
|
1973
|
+
const setupInput = input;
|
|
1974
|
+
return {
|
|
1975
|
+
...setupInput,
|
|
1976
|
+
baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
|
|
1977
|
+
secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
|
|
1978
|
+
secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
|
|
1979
|
+
};
|
|
1980
|
+
},
|
|
1981
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
1982
|
+
cfg,
|
|
1983
|
+
channelKey: channel$1,
|
|
1984
|
+
accountId,
|
|
1985
|
+
name
|
|
1986
|
+
}),
|
|
1987
|
+
validateInput: createSetupInputPresenceValidator({
|
|
1988
|
+
defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
|
|
1989
|
+
validate: ({ input }) => {
|
|
1990
|
+
const setupInput = input;
|
|
1991
|
+
if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
|
|
1992
|
+
const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
|
|
1993
|
+
if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
|
|
1994
|
+
const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
|
|
1995
|
+
if (baseUrlError) return baseUrlError;
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
}),
|
|
1999
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
2000
|
+
const setupInput = input;
|
|
2001
|
+
return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
|
|
2002
|
+
cfg,
|
|
2003
|
+
channelKey: channel$1,
|
|
2004
|
+
accountId,
|
|
2005
|
+
name: setupInput.name
|
|
2006
|
+
}), accountId, {
|
|
2007
|
+
baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
|
|
2008
|
+
...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
|
|
2009
|
+
}, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
1942
2012
|
const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
1943
2013
|
fields: {
|
|
1944
2014
|
baseUrl: {
|
|
@@ -2004,49 +2074,7 @@ const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
|
2004
2074
|
envVars: ["NEXTCLOUD_TALK_BOT_SECRET"]
|
|
2005
2075
|
}
|
|
2006
2076
|
},
|
|
2007
|
-
legacyAdapter:
|
|
2008
|
-
singleAccountKeysToMove: ["rooms"],
|
|
2009
|
-
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
2010
|
-
prepareAccountConfigInput: ({ input }) => {
|
|
2011
|
-
const setupInput = input;
|
|
2012
|
-
return {
|
|
2013
|
-
...setupInput,
|
|
2014
|
-
baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
|
|
2015
|
-
secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
|
|
2016
|
-
secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
|
|
2017
|
-
};
|
|
2018
|
-
},
|
|
2019
|
-
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
2020
|
-
cfg,
|
|
2021
|
-
channelKey: channel$1,
|
|
2022
|
-
accountId,
|
|
2023
|
-
name
|
|
2024
|
-
}),
|
|
2025
|
-
validateInput: createSetupInputPresenceValidator({
|
|
2026
|
-
defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
|
|
2027
|
-
validate: ({ input }) => {
|
|
2028
|
-
const setupInput = input;
|
|
2029
|
-
if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
|
|
2030
|
-
const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
|
|
2031
|
-
if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
|
|
2032
|
-
const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
|
|
2033
|
-
if (baseUrlError) return baseUrlError;
|
|
2034
|
-
return null;
|
|
2035
|
-
}
|
|
2036
|
-
}),
|
|
2037
|
-
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
2038
|
-
const setupInput = input;
|
|
2039
|
-
return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
|
|
2040
|
-
cfg,
|
|
2041
|
-
channelKey: channel$1,
|
|
2042
|
-
accountId,
|
|
2043
|
-
name: setupInput.name
|
|
2044
|
-
}), accountId, {
|
|
2045
|
-
baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
|
|
2046
|
-
...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
|
|
2047
|
-
}, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2077
|
+
legacyAdapter: nextcloudTalkSetupAdapter
|
|
2050
2078
|
});
|
|
2051
2079
|
//#endregion
|
|
2052
2080
|
//#region extensions/nextcloud-talk/src/setup-surface.ts
|
|
@@ -2231,10 +2259,10 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2231
2259
|
configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
|
|
2232
2260
|
config: {
|
|
2233
2261
|
...nextcloudTalkConfigAdapter,
|
|
2234
|
-
isConfigured:
|
|
2262
|
+
isConfigured: isNextcloudTalkAccountConfigured,
|
|
2235
2263
|
describeAccount: (account) => describeWebhookAccountSnapshot({
|
|
2236
2264
|
account,
|
|
2237
|
-
configured:
|
|
2265
|
+
configured: isNextcloudTalkAccountConfigured(account),
|
|
2238
2266
|
extra: {
|
|
2239
2267
|
secretSource: account.secretSource,
|
|
2240
2268
|
tokenStatus: account.tokenStatus,
|
|
@@ -2290,7 +2318,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2290
2318
|
accountId: account.accountId,
|
|
2291
2319
|
name: account.name,
|
|
2292
2320
|
enabled: account.enabled,
|
|
2293
|
-
configured:
|
|
2321
|
+
configured: isNextcloudTalkAccountConfigured(account),
|
|
2294
2322
|
extra: {
|
|
2295
2323
|
secretSource: account.secretSource,
|
|
2296
2324
|
tokenStatus: account.tokenStatus,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-D2RVrR4p.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.9.1
|
|
3
|
+
"version": "2026.9.1",
|
|
4
4
|
"description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"openclaw": ">=2026.9.1
|
|
11
|
+
"openclaw": ">=2026.9.1"
|
|
12
12
|
},
|
|
13
13
|
"peerDependenciesMeta": {
|
|
14
14
|
"openclaw": {
|
|
@@ -129,10 +129,10 @@
|
|
|
129
129
|
"minHostVersion": ">=2026.4.10"
|
|
130
130
|
},
|
|
131
131
|
"compat": {
|
|
132
|
-
"pluginApi": ">=2026.9.1
|
|
132
|
+
"pluginApi": ">=2026.9.1"
|
|
133
133
|
},
|
|
134
134
|
"build": {
|
|
135
|
-
"openclawVersion": "2026.9.1
|
|
135
|
+
"openclawVersion": "2026.9.1"
|
|
136
136
|
},
|
|
137
137
|
"release": {
|
|
138
138
|
"publishToClawHub": true,
|