@openclaw/nextcloud-talk 2026.7.2-beta.2 → 2026.7.2-beta.4
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 +1 -1
- package/dist/{channel-BSnehBf6.js → channel-DtnECxVM.js} +583 -275
- package/dist/channel-plugin-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +0 -6
- package/package.json +89 -5
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-DtnECxVM.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -6,7 +6,7 @@ import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
|
6
6
|
import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
|
7
7
|
import { createAllowlistProviderRouteAllowlistWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
|
|
8
8
|
import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
9
|
-
import { convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
9
|
+
import { convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
10
10
|
import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
11
11
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
12
|
import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
|
|
@@ -15,7 +15,8 @@ import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecr
|
|
|
15
15
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
16
16
|
import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
17
17
|
import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
18
|
-
import {
|
|
18
|
+
import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
19
|
+
import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
|
|
19
20
|
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
20
21
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
21
22
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
@@ -24,16 +25,15 @@ import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtim
|
|
|
24
25
|
import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
25
26
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
|
|
26
27
|
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
27
|
-
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
|
28
|
+
import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
|
|
28
29
|
import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
29
|
-
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime
|
|
30
|
+
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
30
31
|
import { z } from "zod";
|
|
31
32
|
import os from "node:os";
|
|
32
33
|
import path from "node:path";
|
|
33
|
-
import {
|
|
34
|
+
import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
34
35
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
35
36
|
import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
36
|
-
import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
|
37
37
|
import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback } from "openclaw/plugin-sdk/channel-targets";
|
|
38
38
|
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
39
39
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
@@ -41,9 +41,41 @@ import { createServer } from "node:http";
|
|
|
41
41
|
import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
|
|
42
42
|
import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
|
|
43
43
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
|
|
44
|
+
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
44
45
|
import { applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
45
46
|
import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries, promptParsedAllowFromForAccount, resolveSetupAccountId } from "openclaw/plugin-sdk/setup-runtime";
|
|
46
47
|
import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
|
|
48
|
+
//#region extensions/nextcloud-talk/src/api-credentials.ts
|
|
49
|
+
function resolveNextcloudTalkApiCredentialsResult(params) {
|
|
50
|
+
const apiUser = params.apiUser?.trim();
|
|
51
|
+
if (!apiUser) return { status: "missing" };
|
|
52
|
+
const inlinePassword = normalizeResolvedSecretInputString({
|
|
53
|
+
value: params.apiPassword,
|
|
54
|
+
path: "channels.nextcloud-talk.apiPassword"
|
|
55
|
+
});
|
|
56
|
+
if (inlinePassword) return {
|
|
57
|
+
status: "available",
|
|
58
|
+
value: {
|
|
59
|
+
apiUser,
|
|
60
|
+
apiPassword: inlinePassword
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (!params.apiPasswordFile?.trim()) return { status: "missing" };
|
|
64
|
+
const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
|
|
65
|
+
if (result.status === "available") return result.value ? {
|
|
66
|
+
status: "available",
|
|
67
|
+
value: {
|
|
68
|
+
apiUser,
|
|
69
|
+
apiPassword: result.value
|
|
70
|
+
}
|
|
71
|
+
} : { status: "missing" };
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function resolveNextcloudTalkApiCredentials(params) {
|
|
75
|
+
const result = resolveNextcloudTalkApiCredentialsResult(params);
|
|
76
|
+
return result.status === "available" ? result.value : void 0;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
47
79
|
//#region extensions/nextcloud-talk/src/accounts.ts
|
|
48
80
|
function isTruthyEnvValue(value) {
|
|
49
81
|
const normalized = normalizeLowercaseStringOrEmpty(value);
|
|
@@ -79,13 +111,21 @@ function resolveNextcloudTalkSecret(cfg, opts) {
|
|
|
79
111
|
const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
|
|
80
112
|
if (envSecret && resolvedAccountId === DEFAULT_ACCOUNT_ID) return {
|
|
81
113
|
secret: envSecret,
|
|
82
|
-
source: "env"
|
|
114
|
+
source: "env",
|
|
115
|
+
status: "available"
|
|
83
116
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
117
|
+
const botSecretFile = normalizeOptionalString(merged.botSecretFile);
|
|
118
|
+
if (botSecretFile) {
|
|
119
|
+
const result = tryReadSecretFileSync(botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true }, { configPath: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecretFile` });
|
|
120
|
+
return result.status === "available" ? {
|
|
121
|
+
secret: result.value,
|
|
122
|
+
source: "secretFile",
|
|
123
|
+
status: "available"
|
|
124
|
+
} : {
|
|
125
|
+
secret: "",
|
|
126
|
+
source: "secretFile",
|
|
127
|
+
status: "configured_unavailable",
|
|
128
|
+
diagnostic: result.diagnostic
|
|
89
129
|
};
|
|
90
130
|
}
|
|
91
131
|
const inlineSecret = normalizeResolvedSecretInputString({
|
|
@@ -94,11 +134,13 @@ function resolveNextcloudTalkSecret(cfg, opts) {
|
|
|
94
134
|
});
|
|
95
135
|
if (inlineSecret) return {
|
|
96
136
|
secret: inlineSecret,
|
|
97
|
-
source: "config"
|
|
137
|
+
source: "config",
|
|
138
|
+
status: "available"
|
|
98
139
|
};
|
|
99
140
|
return {
|
|
100
141
|
secret: "",
|
|
101
|
-
source: "none"
|
|
142
|
+
source: "none",
|
|
143
|
+
status: "missing"
|
|
102
144
|
};
|
|
103
145
|
}
|
|
104
146
|
function resolveNextcloudTalkAccount(params) {
|
|
@@ -109,6 +151,13 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
109
151
|
const accountEnabled = merged.enabled !== false;
|
|
110
152
|
const enabled = baseEnabled && accountEnabled;
|
|
111
153
|
const secretResolution = resolveNextcloudTalkSecret(params.cfg, { accountId });
|
|
154
|
+
const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
|
|
155
|
+
apiUser: merged.apiUser,
|
|
156
|
+
apiPassword: merged.apiPassword,
|
|
157
|
+
apiPasswordFile: merged.apiPasswordFile,
|
|
158
|
+
configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`
|
|
159
|
+
});
|
|
160
|
+
const diagnostics = [secretResolution.diagnostic, apiCredentialResolution.status === "configured_unavailable" ? apiCredentialResolution.diagnostic : void 0].filter((diagnostic) => Boolean(diagnostic));
|
|
112
161
|
const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
|
|
113
162
|
debugAccounts("resolve", {
|
|
114
163
|
accountId,
|
|
@@ -123,6 +172,9 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
123
172
|
baseUrl,
|
|
124
173
|
secret: secretResolution.secret,
|
|
125
174
|
secretSource: secretResolution.source,
|
|
175
|
+
tokenStatus: secretResolution.status,
|
|
176
|
+
apiCredentialStatus: apiCredentialResolution.status,
|
|
177
|
+
...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
|
|
126
178
|
config: merged
|
|
127
179
|
};
|
|
128
180
|
};
|
|
@@ -130,7 +182,7 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
130
182
|
accountId: resolvedAccountId,
|
|
131
183
|
normalizeAccountId,
|
|
132
184
|
resolvePrimary: resolve,
|
|
133
|
-
hasCredential: (account) => account.
|
|
185
|
+
hasCredential: (account) => account.tokenStatus !== "missing",
|
|
134
186
|
resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
|
|
135
187
|
});
|
|
136
188
|
}
|
|
@@ -150,28 +202,10 @@ const nextcloudTalkApprovalAuth = createChannelApprovalAuth({
|
|
|
150
202
|
normalizeApprover: normalizeNextcloudTalkApproverId
|
|
151
203
|
}).approvalAuth;
|
|
152
204
|
//#endregion
|
|
153
|
-
//#region extensions/nextcloud-talk/src/
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const inlinePassword = normalizeResolvedSecretInputString({
|
|
158
|
-
value: params.apiPassword,
|
|
159
|
-
path: "channels.nextcloud-talk.apiPassword"
|
|
160
|
-
});
|
|
161
|
-
if (inlinePassword) return {
|
|
162
|
-
apiUser,
|
|
163
|
-
apiPassword: inlinePassword
|
|
164
|
-
};
|
|
165
|
-
if (!params.apiPasswordFile) return;
|
|
166
|
-
try {
|
|
167
|
-
const fileValue = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false });
|
|
168
|
-
return fileValue ? {
|
|
169
|
-
apiUser,
|
|
170
|
-
apiPassword: fileValue
|
|
171
|
-
} : void 0;
|
|
172
|
-
} catch {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
205
|
+
//#region extensions/nextcloud-talk/src/guarded-response.ts
|
|
206
|
+
async function releaseNextcloudTalkGuardedResponse(params) {
|
|
207
|
+
if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
|
|
208
|
+
await params.release();
|
|
175
209
|
}
|
|
176
210
|
//#endregion
|
|
177
211
|
//#region extensions/nextcloud-talk/src/signature.ts
|
|
@@ -317,7 +351,10 @@ async function probeNextcloudTalkBotResponseFeature(params) {
|
|
|
317
351
|
message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
|
|
318
352
|
};
|
|
319
353
|
} finally {
|
|
320
|
-
await
|
|
354
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
355
|
+
response,
|
|
356
|
+
release
|
|
357
|
+
});
|
|
321
358
|
}
|
|
322
359
|
} catch (error) {
|
|
323
360
|
return {
|
|
@@ -399,44 +436,13 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
399
436
|
}
|
|
400
437
|
});
|
|
401
438
|
//#endregion
|
|
402
|
-
//#region extensions/nextcloud-talk/src/replay-
|
|
439
|
+
//#region extensions/nextcloud-talk/src/replay-migration-contract.ts
|
|
403
440
|
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
|
|
404
441
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
const DEFAULT_STATE_MAX_ENTRIES = 1e4;
|
|
408
|
-
function buildNextcloudTalkReplayKey(params) {
|
|
409
|
-
const roomToken = params.roomToken.trim();
|
|
410
|
-
const messageId = params.messageId.trim();
|
|
411
|
-
if (!roomToken || !messageId) return null;
|
|
412
|
-
return `${roomToken}:${messageId}`;
|
|
413
|
-
}
|
|
414
|
-
function createNextcloudTalkReplayGuard(options) {
|
|
415
|
-
const stateDir = options.stateDir?.trim();
|
|
416
|
-
const baseOptions = {
|
|
417
|
-
ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
|
|
418
|
-
memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE
|
|
419
|
-
};
|
|
420
|
-
return createChannelReplayGuard({
|
|
421
|
-
dedupe: stateDir ? {
|
|
422
|
-
...baseOptions,
|
|
423
|
-
pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
|
|
424
|
-
namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
|
|
425
|
-
stateMaxEntries: options.stateMaxEntries ?? options.fileMaxEntries ?? DEFAULT_STATE_MAX_ENTRIES,
|
|
426
|
-
env: {
|
|
427
|
-
...process.env,
|
|
428
|
-
OPENCLAW_STATE_DIR: stateDir
|
|
429
|
-
},
|
|
430
|
-
onDiskError: options.onDiskError
|
|
431
|
-
} : baseOptions,
|
|
432
|
-
buildReplayKey: buildNextcloudTalkReplayKey,
|
|
433
|
-
namespace: (event) => event.accountId
|
|
434
|
-
});
|
|
435
|
-
}
|
|
442
|
+
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
|
|
443
|
+
const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
436
444
|
//#endregion
|
|
437
445
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
438
|
-
const REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
|
|
439
|
-
const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
440
446
|
function sanitizeLegacyReplaySegment(value) {
|
|
441
447
|
const trimmed = value.trim();
|
|
442
448
|
if (!trimmed) return "default";
|
|
@@ -471,11 +477,11 @@ async function repairNextcloudTalkReplayDedupeState(params) {
|
|
|
471
477
|
const result = await migratePersistentDedupeLegacyJsonFile({
|
|
472
478
|
filePath: legacyPath,
|
|
473
479
|
namespace: accountId,
|
|
474
|
-
ttlMs:
|
|
480
|
+
ttlMs: NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS,
|
|
475
481
|
memoryMaxSize: 0,
|
|
476
482
|
pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
|
|
477
483
|
namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
|
|
478
|
-
stateMaxEntries:
|
|
484
|
+
stateMaxEntries: NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES,
|
|
479
485
|
env
|
|
480
486
|
});
|
|
481
487
|
changes.push(`Migrated Nextcloud Talk replay dedupe cache for account "${accountId}" to SQLite (${result.imported} imported, ${result.skippedExpired} expired, ${result.skippedExisting} already current).`);
|
|
@@ -513,19 +519,13 @@ function normalizeNextcloudTalkAllowlist(values) {
|
|
|
513
519
|
return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
|
|
514
520
|
}
|
|
515
521
|
function resolveNextcloudTalkAllowlistMatch(params) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
};
|
|
523
|
-
const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
|
|
524
|
-
return allowFrom.includes(senderId) ? {
|
|
525
|
-
allowed: true,
|
|
526
|
-
matchKey: senderId,
|
|
527
|
-
matchSource: "id"
|
|
528
|
-
} : { allowed: false };
|
|
522
|
+
return resolveAllowlistMatchByCandidates({
|
|
523
|
+
allowList: normalizeNextcloudTalkAllowlist(params.allowFrom),
|
|
524
|
+
candidates: [{
|
|
525
|
+
value: normalizeNextcloudTalkAllowEntry(params.senderId),
|
|
526
|
+
source: "id"
|
|
527
|
+
}]
|
|
528
|
+
});
|
|
529
529
|
}
|
|
530
530
|
function resolveNextcloudTalkRoomMatch(params) {
|
|
531
531
|
const rooms = params.rooms ?? {};
|
|
@@ -664,7 +664,10 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
664
664
|
});
|
|
665
665
|
return kind;
|
|
666
666
|
} finally {
|
|
667
|
-
await
|
|
667
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
668
|
+
response,
|
|
669
|
+
release
|
|
670
|
+
});
|
|
668
671
|
}
|
|
669
672
|
} catch (err) {
|
|
670
673
|
cacheRoomInfo(key, {
|
|
@@ -703,6 +706,43 @@ function looksLikeNextcloudTalkTargetId(raw) {
|
|
|
703
706
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
|
|
704
707
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
|
|
705
708
|
const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
|
|
709
|
+
const NEXTCLOUD_TALK_FORMAT_PROFILE = {
|
|
710
|
+
mechanism: "markdown",
|
|
711
|
+
constructs: {
|
|
712
|
+
bold: "native",
|
|
713
|
+
italic: "native",
|
|
714
|
+
underline: "native",
|
|
715
|
+
strikethrough: "native",
|
|
716
|
+
spoiler: "native",
|
|
717
|
+
codeInline: "native",
|
|
718
|
+
codeBlock: "native",
|
|
719
|
+
codeLanguage: "native",
|
|
720
|
+
linkLabel: "native",
|
|
721
|
+
heading: "native",
|
|
722
|
+
bulletList: "native",
|
|
723
|
+
orderedList: "native",
|
|
724
|
+
taskList: "native",
|
|
725
|
+
table: "native",
|
|
726
|
+
blockquote: "native",
|
|
727
|
+
image: "native",
|
|
728
|
+
mention: "native"
|
|
729
|
+
},
|
|
730
|
+
chunk: {
|
|
731
|
+
limit: 4e3,
|
|
732
|
+
unit: "chars",
|
|
733
|
+
hardCap: 32e3
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
function renderNextcloudTalkMarkdown(markdown) {
|
|
737
|
+
return renderMarkdownWithMarkers({
|
|
738
|
+
text: markdown,
|
|
739
|
+
styles: [],
|
|
740
|
+
links: []
|
|
741
|
+
}, {
|
|
742
|
+
styleMarkers: {},
|
|
743
|
+
escapeText: (text) => text
|
|
744
|
+
}, NEXTCLOUD_TALK_FORMAT_PROFILE);
|
|
745
|
+
}
|
|
706
746
|
/** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
|
|
707
747
|
function collapseErrorSnippet(text) {
|
|
708
748
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
@@ -781,7 +821,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
781
821
|
channel: "nextcloud-talk",
|
|
782
822
|
accountId: account.accountId
|
|
783
823
|
});
|
|
784
|
-
const message = convertMarkdownTables(text.trim(), tableMode);
|
|
824
|
+
const message = convertMarkdownTables(renderNextcloudTalkMarkdown(text.trim()), tableMode);
|
|
785
825
|
const body = { message };
|
|
786
826
|
if (opts.replyTo) body.replyTo = opts.replyTo;
|
|
787
827
|
const bodyStr = JSON.stringify(body);
|
|
@@ -837,7 +877,10 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
837
877
|
timestamp
|
|
838
878
|
};
|
|
839
879
|
} finally {
|
|
840
|
-
await
|
|
880
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
881
|
+
response,
|
|
882
|
+
release
|
|
883
|
+
});
|
|
841
884
|
}
|
|
842
885
|
}
|
|
843
886
|
async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
@@ -871,7 +914,10 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
871
914
|
}
|
|
872
915
|
return { ok: true };
|
|
873
916
|
} finally {
|
|
874
|
-
await
|
|
917
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
918
|
+
response,
|
|
919
|
+
release
|
|
920
|
+
});
|
|
875
921
|
}
|
|
876
922
|
}
|
|
877
923
|
//#endregion
|
|
@@ -1077,19 +1123,17 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1077
1123
|
runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`);
|
|
1078
1124
|
return;
|
|
1079
1125
|
}
|
|
1080
|
-
const { route, buildEnvelope } =
|
|
1126
|
+
const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
|
|
1081
1127
|
cfg: config,
|
|
1082
1128
|
channel: CHANNEL_ID,
|
|
1083
1129
|
accountId: account.accountId,
|
|
1084
1130
|
peer: {
|
|
1085
1131
|
kind: isGroup ? "group" : "direct",
|
|
1086
1132
|
id: isGroup ? roomToken : senderId
|
|
1087
|
-
}
|
|
1088
|
-
runtime: core.channel,
|
|
1089
|
-
sessionStore: config.session?.store
|
|
1133
|
+
}
|
|
1090
1134
|
});
|
|
1091
1135
|
const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
|
|
1092
|
-
const
|
|
1136
|
+
const body = buildEnvelope({
|
|
1093
1137
|
channel: "Nextcloud Talk",
|
|
1094
1138
|
from: fromLabel,
|
|
1095
1139
|
timestamp: message.timestamp,
|
|
@@ -1097,40 +1141,58 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1097
1141
|
});
|
|
1098
1142
|
const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
|
|
1099
1143
|
const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
|
|
1100
|
-
const ctxPayload =
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1144
|
+
const ctxPayload = buildChannelInboundEventContext({
|
|
1145
|
+
channel: CHANNEL_ID,
|
|
1146
|
+
accountId: route.accountId,
|
|
1147
|
+
messageId: message.messageId,
|
|
1148
|
+
timestamp: message.timestamp,
|
|
1149
|
+
from: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`,
|
|
1150
|
+
sender: {
|
|
1151
|
+
id: senderId,
|
|
1152
|
+
name: senderName || void 0
|
|
1153
|
+
},
|
|
1154
|
+
conversation: {
|
|
1155
|
+
kind: isGroup ? "group" : "direct",
|
|
1156
|
+
id: roomToken,
|
|
1157
|
+
label: fromLabel
|
|
1158
|
+
},
|
|
1159
|
+
route: {
|
|
1160
|
+
agentId: route.agentId,
|
|
1161
|
+
dmScope: route.dmScope,
|
|
1162
|
+
accountId: route.accountId,
|
|
1163
|
+
routeSessionKey: route.sessionKey
|
|
1164
|
+
},
|
|
1165
|
+
reply: {
|
|
1166
|
+
to: `nextcloud-talk:${roomToken}`,
|
|
1167
|
+
originatingTo: `nextcloud-talk:${roomToken}`
|
|
1168
|
+
},
|
|
1169
|
+
message: {
|
|
1170
|
+
body,
|
|
1171
|
+
bodyForAgent: rawBody,
|
|
1172
|
+
rawBody,
|
|
1173
|
+
commandBody: rawBody
|
|
1174
|
+
},
|
|
1175
|
+
access: {
|
|
1176
|
+
commands: { authorized: commandAuthorized },
|
|
1177
|
+
mentions: {
|
|
1178
|
+
canDetectMention: isGroup,
|
|
1179
|
+
wasMentioned: isGroup && wasMentioned
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
extra: {
|
|
1183
|
+
GroupSubject: isGroup ? roomName || roomToken : void 0,
|
|
1184
|
+
GroupSystemPrompt: isGroup ? groupSystemPrompt : void 0
|
|
1185
|
+
}
|
|
1123
1186
|
});
|
|
1124
|
-
await core.channel.inbound.
|
|
1187
|
+
await core.channel.inbound.dispatch({
|
|
1125
1188
|
cfg: config,
|
|
1126
1189
|
channel: CHANNEL_ID,
|
|
1127
1190
|
accountId: account.accountId,
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1191
|
+
route: {
|
|
1192
|
+
agentId: route.agentId,
|
|
1193
|
+
sessionKey: route.sessionKey
|
|
1194
|
+
},
|
|
1131
1195
|
ctxPayload,
|
|
1132
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1133
|
-
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
1134
1196
|
delivery: {
|
|
1135
1197
|
preparePayload: (payload) => payload.text === void 0 ? payload : {
|
|
1136
1198
|
...payload,
|
|
@@ -1151,6 +1213,7 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1151
1213
|
},
|
|
1152
1214
|
replyPipeline: {},
|
|
1153
1215
|
replyOptions: {
|
|
1216
|
+
...params.turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) : {},
|
|
1154
1217
|
skillFilter: roomConfig?.skills,
|
|
1155
1218
|
disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
|
|
1156
1219
|
},
|
|
@@ -1159,6 +1222,74 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1159
1222
|
} }
|
|
1160
1223
|
});
|
|
1161
1224
|
}
|
|
1225
|
+
var NextcloudTalkWebhookPayloadError = class extends Error {
|
|
1226
|
+
constructor(message, options) {
|
|
1227
|
+
super(message, options);
|
|
1228
|
+
this.name = "NextcloudTalkWebhookPayloadError";
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
function isRecord(value) {
|
|
1232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1233
|
+
}
|
|
1234
|
+
function parseRawObject(rawEvent) {
|
|
1235
|
+
let parsed;
|
|
1236
|
+
try {
|
|
1237
|
+
parsed = JSON.parse(rawEvent);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook contains invalid JSON.", { cause: error });
|
|
1240
|
+
}
|
|
1241
|
+
if (!isRecord(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
|
|
1242
|
+
return parsed;
|
|
1243
|
+
}
|
|
1244
|
+
function requiredString(value, field) {
|
|
1245
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1246
|
+
throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk webhook is missing ${field}.`);
|
|
1247
|
+
}
|
|
1248
|
+
function inspectNextcloudTalkWebhookEnvelope(rawEvent) {
|
|
1249
|
+
const envelope = parseRawObject(rawEvent);
|
|
1250
|
+
if (envelope.type !== "Create") return null;
|
|
1251
|
+
const object = isRecord(envelope.object) ? envelope.object : null;
|
|
1252
|
+
if (object?.type !== void 0 && object.type !== "Note") return null;
|
|
1253
|
+
if (!object) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook is missing object.");
|
|
1254
|
+
const target = isRecord(envelope.target) ? envelope.target : null;
|
|
1255
|
+
return {
|
|
1256
|
+
eventId: requiredString(object.id, "object.id"),
|
|
1257
|
+
laneKey: `room:${requiredString(target?.id, "target.id")}`
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function parseLegacyReplayKey(key) {
|
|
1261
|
+
const separator = key.lastIndexOf(":");
|
|
1262
|
+
const roomId = key.slice(0, separator).trim();
|
|
1263
|
+
const messageId = key.slice(separator + 1).trim();
|
|
1264
|
+
return separator > 0 && roomId && messageId ? {
|
|
1265
|
+
messageId,
|
|
1266
|
+
roomId
|
|
1267
|
+
} : null;
|
|
1268
|
+
}
|
|
1269
|
+
/** Convert the shipped replay guard's live window into durable completion tombstones. */
|
|
1270
|
+
async function migrateNextcloudTalkLegacyReplayState(params) {
|
|
1271
|
+
const entries = await params.store.entries();
|
|
1272
|
+
let migrated = 0;
|
|
1273
|
+
for (const entry of entries) {
|
|
1274
|
+
const identity = parseLegacyReplayKey(entry.value.key);
|
|
1275
|
+
if (!identity || !Number.isFinite(entry.value.seenAt)) continue;
|
|
1276
|
+
const marker = {
|
|
1277
|
+
version: 1,
|
|
1278
|
+
receivedAt: entry.value.seenAt,
|
|
1279
|
+
rawEvent: ""
|
|
1280
|
+
};
|
|
1281
|
+
const result = await params.queue.enqueue(identity.messageId, marker, {
|
|
1282
|
+
receivedAt: entry.value.seenAt,
|
|
1283
|
+
laneKey: `room:${identity.roomId}`
|
|
1284
|
+
});
|
|
1285
|
+
if (result.kind === "accepted" || result.kind === "pending" && result.record.payload.rawEvent === "") {
|
|
1286
|
+
if (!await params.queue.complete(identity.messageId, { completedAt: entry.value.seenAt })) throw new Error(`Failed to migrate Nextcloud Talk replay key ${entry.value.key}.`);
|
|
1287
|
+
}
|
|
1288
|
+
migrated += 1;
|
|
1289
|
+
}
|
|
1290
|
+
await params.store.clear();
|
|
1291
|
+
return migrated;
|
|
1292
|
+
}
|
|
1162
1293
|
//#endregion
|
|
1163
1294
|
//#region extensions/nextcloud-talk/src/monitor.ts
|
|
1164
1295
|
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
@@ -1166,34 +1297,6 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
|
|
|
1166
1297
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
1167
1298
|
const HEALTH_PATH = "/healthz";
|
|
1168
1299
|
const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
|
|
1169
|
-
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1170
|
-
type: z.enum([
|
|
1171
|
-
"Create",
|
|
1172
|
-
"Update",
|
|
1173
|
-
"Delete"
|
|
1174
|
-
]),
|
|
1175
|
-
actor: z.object({
|
|
1176
|
-
type: z.literal("Person"),
|
|
1177
|
-
id: z.string().min(1),
|
|
1178
|
-
name: z.string()
|
|
1179
|
-
}),
|
|
1180
|
-
object: z.object({
|
|
1181
|
-
type: z.literal("Note"),
|
|
1182
|
-
id: z.string().min(1),
|
|
1183
|
-
name: z.string(),
|
|
1184
|
-
content: z.string(),
|
|
1185
|
-
mediaType: z.string()
|
|
1186
|
-
}),
|
|
1187
|
-
target: z.object({
|
|
1188
|
-
type: z.literal("Collection"),
|
|
1189
|
-
id: z.string().min(1),
|
|
1190
|
-
name: z.string()
|
|
1191
|
-
})
|
|
1192
|
-
});
|
|
1193
|
-
const NextcloudTalkWebhookEnvelopeSchema = z.object({
|
|
1194
|
-
type: z.string().min(1),
|
|
1195
|
-
object: z.object({ type: z.string().min(1).optional() }).passthrough().optional()
|
|
1196
|
-
});
|
|
1197
1300
|
const WEBHOOK_ERRORS = {
|
|
1198
1301
|
missingSignatureHeaders: "Missing signature headers",
|
|
1199
1302
|
invalidBackend: "Invalid backend",
|
|
@@ -1202,20 +1305,10 @@ const WEBHOOK_ERRORS = {
|
|
|
1202
1305
|
payloadTooLarge: "Payload too large",
|
|
1203
1306
|
internalServerError: "Internal server error"
|
|
1204
1307
|
};
|
|
1205
|
-
async function processNextcloudTalkReplayGuardedMessage(params) {
|
|
1206
|
-
return (await params.replayGuard.processGuarded({
|
|
1207
|
-
accountId: params.accountId,
|
|
1208
|
-
roomToken: params.message.roomToken,
|
|
1209
|
-
messageId: params.message.messageId
|
|
1210
|
-
}, params.handleMessage, { onError: "commit" })).kind === "processed" ? "processed" : "duplicate";
|
|
1211
|
-
}
|
|
1212
1308
|
function formatError(err) {
|
|
1213
1309
|
if (err instanceof Error) return err.message;
|
|
1214
1310
|
return typeof err === "string" ? err : JSON.stringify(err);
|
|
1215
1311
|
}
|
|
1216
|
-
function parseWebhookPayload(body) {
|
|
1217
|
-
return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
|
|
1218
|
-
}
|
|
1219
1312
|
function writeJsonResponse(res, status, body) {
|
|
1220
1313
|
if (body) {
|
|
1221
1314
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -1255,37 +1348,6 @@ function verifyWebhookSignature(params) {
|
|
|
1255
1348
|
params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1256
1349
|
return true;
|
|
1257
1350
|
}
|
|
1258
|
-
function decodeWebhookCreateMessage(params) {
|
|
1259
|
-
const envelope = safeParseJsonWithSchema(NextcloudTalkWebhookEnvelopeSchema, params.body);
|
|
1260
|
-
if (!envelope) {
|
|
1261
|
-
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1262
|
-
return { kind: "invalid" };
|
|
1263
|
-
}
|
|
1264
|
-
if (envelope.type !== "Create") return { kind: "ignore" };
|
|
1265
|
-
if (envelope.object?.type && envelope.object.type !== "Note") return { kind: "ignore" };
|
|
1266
|
-
const payload = parseWebhookPayload(params.body);
|
|
1267
|
-
if (!payload) {
|
|
1268
|
-
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1269
|
-
return { kind: "invalid" };
|
|
1270
|
-
}
|
|
1271
|
-
return {
|
|
1272
|
-
kind: "message",
|
|
1273
|
-
message: payloadToInboundMessage(payload)
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
function payloadToInboundMessage(payload) {
|
|
1277
|
-
return {
|
|
1278
|
-
messageId: payload.object.id,
|
|
1279
|
-
roomToken: payload.target.id,
|
|
1280
|
-
roomName: payload.target.name,
|
|
1281
|
-
senderId: payload.actor.id,
|
|
1282
|
-
senderName: payload.actor.name ?? "",
|
|
1283
|
-
text: payload.object.content || payload.object.name || "",
|
|
1284
|
-
mediaType: payload.object.mediaType || "text/plain",
|
|
1285
|
-
timestamp: Date.now(),
|
|
1286
|
-
isGroupChat: true
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
1351
|
function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
1290
1352
|
return readRequestBodyWithLimit(req, {
|
|
1291
1353
|
maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
|
|
@@ -1293,12 +1355,10 @@ function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
|
1293
1355
|
});
|
|
1294
1356
|
}
|
|
1295
1357
|
function createNextcloudTalkWebhookServer(opts) {
|
|
1296
|
-
const { port, host, path, secret,
|
|
1358
|
+
const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
|
|
1297
1359
|
const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
|
|
1298
1360
|
const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
|
|
1299
1361
|
const isBackendAllowed = opts.isBackendAllowed;
|
|
1300
|
-
const shouldProcessMessage = opts.shouldProcessMessage;
|
|
1301
|
-
const processMessage = opts.processMessage;
|
|
1302
1362
|
const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
|
|
1303
1363
|
const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
|
|
1304
1364
|
const webhookAuthRateLimiter = createAuthRateLimiter({
|
|
@@ -1342,37 +1402,8 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1342
1402
|
clientIp,
|
|
1343
1403
|
authRateLimiter: webhookAuthRateLimiter
|
|
1344
1404
|
})) return;
|
|
1345
|
-
|
|
1346
|
-
body,
|
|
1347
|
-
res
|
|
1348
|
-
});
|
|
1349
|
-
if (decoded.kind === "invalid") return;
|
|
1350
|
-
if (decoded.kind === "ignore") {
|
|
1351
|
-
writeJsonResponse(res, 200);
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
const message = decoded.message;
|
|
1355
|
-
if (processMessage) {
|
|
1356
|
-
writeJsonResponse(res, 200);
|
|
1357
|
-
try {
|
|
1358
|
-
await processMessage(message);
|
|
1359
|
-
} catch (err) {
|
|
1360
|
-
onError?.(err instanceof Error ? err : new Error(formatError(err)));
|
|
1361
|
-
}
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
if (shouldProcessMessage) {
|
|
1365
|
-
if (!await shouldProcessMessage(message)) {
|
|
1366
|
-
writeJsonResponse(res, 200);
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1405
|
+
await onWebhook(body);
|
|
1370
1406
|
writeJsonResponse(res, 200);
|
|
1371
|
-
try {
|
|
1372
|
-
await onMessage(message);
|
|
1373
|
-
} catch (err) {
|
|
1374
|
-
onError?.(err instanceof Error ? err : new Error(formatError(err)));
|
|
1375
|
-
}
|
|
1376
1407
|
} catch (err) {
|
|
1377
1408
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1378
1409
|
writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
|
|
@@ -1382,27 +1413,48 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1382
1413
|
writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
|
|
1383
1414
|
return;
|
|
1384
1415
|
}
|
|
1416
|
+
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
1417
|
+
writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1385
1420
|
const error = err instanceof Error ? err : new Error(formatError(err));
|
|
1386
1421
|
onError?.(error);
|
|
1387
1422
|
writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
|
|
1388
1423
|
}
|
|
1389
1424
|
})();
|
|
1390
1425
|
});
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1426
|
+
let stopRequested = false;
|
|
1427
|
+
let closePromise;
|
|
1428
|
+
const closeIfListening = () => {
|
|
1429
|
+
if (closePromise) return closePromise;
|
|
1430
|
+
if (!server.listening) return Promise.resolve();
|
|
1431
|
+
closePromise = new Promise((resolve) => {
|
|
1432
|
+
server.close(() => resolve());
|
|
1433
|
+
}).finally(() => {
|
|
1434
|
+
closePromise = void 0;
|
|
1394
1435
|
});
|
|
1436
|
+
return closePromise;
|
|
1395
1437
|
};
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1438
|
+
const stop = async () => {
|
|
1439
|
+
stopRequested = true;
|
|
1440
|
+
await closeIfListening();
|
|
1441
|
+
};
|
|
1442
|
+
const start = () => {
|
|
1443
|
+
if (stopRequested) return Promise.resolve();
|
|
1444
|
+
return new Promise((resolve, reject) => {
|
|
1445
|
+
const onListenError = (error) => reject(error);
|
|
1446
|
+
server.once("error", onListenError);
|
|
1447
|
+
server.listen(port, host, () => {
|
|
1448
|
+
server.off("error", onListenError);
|
|
1449
|
+
(async () => {
|
|
1450
|
+
if (stopRequested) await closeIfListening();
|
|
1451
|
+
resolve();
|
|
1452
|
+
})().catch(reject);
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1403
1455
|
};
|
|
1404
1456
|
if (abortSignal) if (abortSignal.aborted) stop();
|
|
1405
|
-
else abortSignal.addEventListener("abort", stop, { once: true });
|
|
1457
|
+
else abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1406
1458
|
return {
|
|
1407
1459
|
server,
|
|
1408
1460
|
start,
|
|
@@ -1410,6 +1462,164 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1410
1462
|
};
|
|
1411
1463
|
}
|
|
1412
1464
|
//#endregion
|
|
1465
|
+
//#region extensions/nextcloud-talk/src/webhook-spool.ts
|
|
1466
|
+
const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
|
|
1467
|
+
const NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
|
|
1468
|
+
const NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
1469
|
+
const NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES = 1e4;
|
|
1470
|
+
const NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
1471
|
+
const NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES = 1e4;
|
|
1472
|
+
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1473
|
+
type: z.enum([
|
|
1474
|
+
"Create",
|
|
1475
|
+
"Update",
|
|
1476
|
+
"Delete"
|
|
1477
|
+
]),
|
|
1478
|
+
actor: z.object({
|
|
1479
|
+
type: z.literal("Person"),
|
|
1480
|
+
id: z.string().min(1),
|
|
1481
|
+
name: z.string()
|
|
1482
|
+
}),
|
|
1483
|
+
object: z.object({
|
|
1484
|
+
type: z.literal("Note"),
|
|
1485
|
+
id: z.string().min(1),
|
|
1486
|
+
name: z.string(),
|
|
1487
|
+
content: z.string(),
|
|
1488
|
+
mediaType: z.string()
|
|
1489
|
+
}),
|
|
1490
|
+
target: z.object({
|
|
1491
|
+
type: z.literal("Collection"),
|
|
1492
|
+
id: z.string().min(1),
|
|
1493
|
+
name: z.string()
|
|
1494
|
+
})
|
|
1495
|
+
});
|
|
1496
|
+
function parseClaimedMessage(payload, claimedId, claimedLaneKey) {
|
|
1497
|
+
if (payload.version !== 1) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has an unsupported version.`);
|
|
1498
|
+
const result = NextcloudTalkWebhookPayloadSchema.safeParse(parseRawObject(payload.rawEvent));
|
|
1499
|
+
if (!result.success || result.data.type !== "Create" || result.data.object.id !== claimedId) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has invalid message identity.`);
|
|
1500
|
+
const webhook = result.data;
|
|
1501
|
+
const roomId = requiredString(webhook.target.id, "target.id");
|
|
1502
|
+
if (claimedLaneKey !== `room:${roomId}`) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} changed room identity.`);
|
|
1503
|
+
return {
|
|
1504
|
+
messageId: webhook.object.id,
|
|
1505
|
+
roomToken: roomId,
|
|
1506
|
+
roomName: webhook.target.name,
|
|
1507
|
+
senderId: webhook.actor.id,
|
|
1508
|
+
senderName: webhook.actor.name,
|
|
1509
|
+
text: webhook.object.content || webhook.object.name,
|
|
1510
|
+
mediaType: webhook.object.mediaType || "text/plain",
|
|
1511
|
+
timestamp: payload.receivedAt,
|
|
1512
|
+
isGroupChat: true
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function resolveNonRetryableFailure(error) {
|
|
1516
|
+
if (error instanceof NextcloudTalkWebhookPayloadError) return {
|
|
1517
|
+
reason: "invalid-event",
|
|
1518
|
+
message: error.message
|
|
1519
|
+
};
|
|
1520
|
+
const message = formatErrorMessage(error);
|
|
1521
|
+
if (message.includes("Nextcloud Talk: bot send was rejected") || message.includes("Nextcloud Talk: forbidden")) return {
|
|
1522
|
+
reason: "nextcloud-talk-auth",
|
|
1523
|
+
message
|
|
1524
|
+
};
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
function createNextcloudTalkWebhookSpool(options) {
|
|
1528
|
+
let queue = options.queue;
|
|
1529
|
+
const getQueue = () => {
|
|
1530
|
+
queue ??= getNextcloudTalkRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1531
|
+
return queue;
|
|
1532
|
+
};
|
|
1533
|
+
const legacyReplayStore = options.legacyReplayStore === null ? null : options.legacyReplayStore ?? getNextcloudTalkRuntime().state.openKeyedStore({
|
|
1534
|
+
namespace: resolvePersistentDedupePluginStateNamespace({
|
|
1535
|
+
namespace: options.accountId,
|
|
1536
|
+
namespacePrefix: "replay-dedupe"
|
|
1537
|
+
}),
|
|
1538
|
+
maxEntries: 1e4,
|
|
1539
|
+
defaultTtlMs: 864e5
|
|
1540
|
+
});
|
|
1541
|
+
const legacyMigration = legacyReplayStore ? migrateNextcloudTalkLegacyReplayState({
|
|
1542
|
+
queue: getQueue(),
|
|
1543
|
+
store: legacyReplayStore
|
|
1544
|
+
}) : Promise.resolve();
|
|
1545
|
+
const monitor = createChannelIngressMonitor({
|
|
1546
|
+
queue: getQueue,
|
|
1547
|
+
inspect: (rawEvent) => inspectNextcloudTalkWebhookEnvelope(rawEvent),
|
|
1548
|
+
payload: {
|
|
1549
|
+
version: 1,
|
|
1550
|
+
serialize: (rawEvent, { receivedAt }) => ({
|
|
1551
|
+
receivedAt,
|
|
1552
|
+
rawEvent
|
|
1553
|
+
}),
|
|
1554
|
+
deserialize: (body) => body.rawEvent,
|
|
1555
|
+
encode: ({ body }) => ({
|
|
1556
|
+
version: 1,
|
|
1557
|
+
...body
|
|
1558
|
+
}),
|
|
1559
|
+
decode: (payload) => ({
|
|
1560
|
+
version: payload.version,
|
|
1561
|
+
body: {
|
|
1562
|
+
receivedAt: payload.receivedAt,
|
|
1563
|
+
rawEvent: payload.rawEvent
|
|
1564
|
+
}
|
|
1565
|
+
}),
|
|
1566
|
+
createClaimError: (kind, claim) => new NextcloudTalkWebhookPayloadError(kind === "invalid-version" ? `Nextcloud Talk ingress row ${claim.id} has an unsupported version.` : `Nextcloud Talk ingress row ${claim.id} has invalid message identity.`)
|
|
1567
|
+
},
|
|
1568
|
+
deliver: async (_rawEvent, lifecycle, claim) => {
|
|
1569
|
+
const message = parseClaimedMessage(claim.payload, claim.id, claim.laneKey);
|
|
1570
|
+
await options.deliver(message, lifecycle);
|
|
1571
|
+
},
|
|
1572
|
+
pollIntervalMs: options.pollIntervalMs ?? NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS,
|
|
1573
|
+
waitForDeliveryIdleBeforeRepump: true,
|
|
1574
|
+
retention: {
|
|
1575
|
+
pruneIntervalMs: NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS,
|
|
1576
|
+
completedTtlMs: NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS,
|
|
1577
|
+
completedMaxEntries: NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES,
|
|
1578
|
+
failedTtlMs: NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS,
|
|
1579
|
+
failedMaxEntries: NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES
|
|
1580
|
+
},
|
|
1581
|
+
drain: {
|
|
1582
|
+
resolveNonRetryableFailure,
|
|
1583
|
+
...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
|
|
1584
|
+
onLog: (message) => options.runtime.log?.(`nextcloud-talk ${message}`)
|
|
1585
|
+
},
|
|
1586
|
+
...options.abortSignal ? { abortSignal: options.abortSignal } : {},
|
|
1587
|
+
createStoppedError: () => /* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"),
|
|
1588
|
+
onError: (error) => options.runtime.error?.(`nextcloud-talk ingress drain failed: ${formatErrorMessage(error)}`)
|
|
1589
|
+
});
|
|
1590
|
+
let stopping = false;
|
|
1591
|
+
const inFlightReceives = /* @__PURE__ */ new Set();
|
|
1592
|
+
const startAfterMigration = legacyMigration.then(() => {
|
|
1593
|
+
if (!stopping) monitor.start();
|
|
1594
|
+
});
|
|
1595
|
+
return {
|
|
1596
|
+
ready: async () => await startAfterMigration,
|
|
1597
|
+
receive: (rawEvent) => {
|
|
1598
|
+
if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
|
|
1599
|
+
const receiveTask = (async () => {
|
|
1600
|
+
await startAfterMigration;
|
|
1601
|
+
return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
|
|
1602
|
+
})();
|
|
1603
|
+
inFlightReceives.add(receiveTask);
|
|
1604
|
+
receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
|
|
1605
|
+
return receiveTask;
|
|
1606
|
+
},
|
|
1607
|
+
stop: async () => {
|
|
1608
|
+
stopping = true;
|
|
1609
|
+
const pendingReceives = [...inFlightReceives];
|
|
1610
|
+
const pauseTask = monitor.pause();
|
|
1611
|
+
await startAfterMigration.catch(() => void 0);
|
|
1612
|
+
await Promise.allSettled(pendingReceives);
|
|
1613
|
+
await monitor.stop();
|
|
1614
|
+
await pauseTask;
|
|
1615
|
+
},
|
|
1616
|
+
waitForIdle: async () => {
|
|
1617
|
+
await startAfterMigration.catch(() => void 0);
|
|
1618
|
+
await monitor.waitForIdle();
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
//#endregion
|
|
1413
1623
|
//#region extensions/nextcloud-talk/src/monitor-runtime.ts
|
|
1414
1624
|
const DEFAULT_WEBHOOK_PORT = 8788;
|
|
1415
1625
|
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
@@ -1438,13 +1648,29 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1438
1648
|
accountId: account.accountId
|
|
1439
1649
|
});
|
|
1440
1650
|
const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1651
|
+
const spool = (opts.createSpool ?? createNextcloudTalkWebhookSpool)({
|
|
1652
|
+
accountId: account.accountId,
|
|
1653
|
+
runtime,
|
|
1654
|
+
abortSignal: opts.abortSignal,
|
|
1655
|
+
deliver: async (message, lifecycle) => {
|
|
1656
|
+
core.channel.activity.record({
|
|
1657
|
+
channel: "nextcloud-talk",
|
|
1658
|
+
accountId: account.accountId,
|
|
1659
|
+
direction: "inbound",
|
|
1660
|
+
at: message.timestamp
|
|
1661
|
+
});
|
|
1662
|
+
if (opts.onMessage) await opts.onMessage(message, lifecycle);
|
|
1663
|
+
else await handleNextcloudTalkInbound({
|
|
1664
|
+
message,
|
|
1665
|
+
account,
|
|
1666
|
+
config: cfg,
|
|
1667
|
+
runtime,
|
|
1668
|
+
statusSink: opts.statusSink,
|
|
1669
|
+
turnAdoptionLifecycle: lifecycle
|
|
1670
|
+
});
|
|
1445
1671
|
}
|
|
1446
1672
|
});
|
|
1447
|
-
const
|
|
1673
|
+
const server = (opts.createServer ?? createNextcloudTalkWebhookServer)({
|
|
1448
1674
|
port,
|
|
1449
1675
|
host,
|
|
1450
1676
|
path,
|
|
@@ -1453,39 +1679,34 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1453
1679
|
if (!expectedBackendOrigin) return true;
|
|
1454
1680
|
return normalizeOrigin(backend) === expectedBackendOrigin;
|
|
1455
1681
|
},
|
|
1456
|
-
|
|
1457
|
-
if (await processNextcloudTalkReplayGuardedMessage({
|
|
1458
|
-
replayGuard,
|
|
1459
|
-
accountId: account.accountId,
|
|
1460
|
-
message,
|
|
1461
|
-
handleMessage: async () => {
|
|
1462
|
-
core.channel.activity.record({
|
|
1463
|
-
channel: "nextcloud-talk",
|
|
1464
|
-
accountId: account.accountId,
|
|
1465
|
-
direction: "inbound",
|
|
1466
|
-
at: message.timestamp
|
|
1467
|
-
});
|
|
1468
|
-
if (opts.onMessage) await opts.onMessage(message);
|
|
1469
|
-
else await handleNextcloudTalkInbound({
|
|
1470
|
-
message,
|
|
1471
|
-
account,
|
|
1472
|
-
config: cfg,
|
|
1473
|
-
runtime,
|
|
1474
|
-
statusSink: opts.statusSink
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
}) === "duplicate") logger.warn(`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`);
|
|
1478
|
-
},
|
|
1479
|
-
onMessage: async () => {},
|
|
1682
|
+
onWebhook: spool.receive,
|
|
1480
1683
|
onError: (error) => {
|
|
1481
1684
|
logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
|
|
1482
1685
|
},
|
|
1483
1686
|
abortSignal: opts.abortSignal
|
|
1484
1687
|
});
|
|
1485
|
-
|
|
1486
|
-
|
|
1688
|
+
let stopPromise;
|
|
1689
|
+
const stop = () => {
|
|
1690
|
+
stopPromise ??= (async () => {
|
|
1691
|
+
await server.stop();
|
|
1692
|
+
await spool.stop();
|
|
1693
|
+
})();
|
|
1694
|
+
return stopPromise;
|
|
1695
|
+
};
|
|
1696
|
+
if (opts.abortSignal && !opts.abortSignal.aborted) opts.abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1487
1697
|
if (opts.abortSignal?.aborted) {
|
|
1488
|
-
stop();
|
|
1698
|
+
await stop();
|
|
1699
|
+
return { stop };
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
await spool.ready();
|
|
1703
|
+
await server.start();
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
await stop();
|
|
1706
|
+
throw error;
|
|
1707
|
+
}
|
|
1708
|
+
if (opts.abortSignal?.aborted) {
|
|
1709
|
+
await stop();
|
|
1489
1710
|
return { stop };
|
|
1490
1711
|
}
|
|
1491
1712
|
const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
|
@@ -1503,7 +1724,7 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1503
1724
|
accountId: ctx.accountId,
|
|
1504
1725
|
setStatus: ctx.setStatus
|
|
1505
1726
|
});
|
|
1506
|
-
await
|
|
1727
|
+
await runPassiveAccountLifecycle({
|
|
1507
1728
|
abortSignal: ctx.abortSignal,
|
|
1508
1729
|
start: async () => await monitorNextcloudTalkProvider({
|
|
1509
1730
|
accountId: account.accountId,
|
|
@@ -1511,7 +1732,10 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1511
1732
|
runtime: ctx.runtime,
|
|
1512
1733
|
abortSignal: ctx.abortSignal,
|
|
1513
1734
|
statusSink
|
|
1514
|
-
})
|
|
1735
|
+
}),
|
|
1736
|
+
stop: async (monitor) => {
|
|
1737
|
+
await monitor.stop();
|
|
1738
|
+
}
|
|
1515
1739
|
});
|
|
1516
1740
|
},
|
|
1517
1741
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
@@ -1665,6 +1889,9 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
|
|
|
1665
1889
|
//#region extensions/nextcloud-talk/src/setup-core.ts
|
|
1666
1890
|
const t$1 = createSetupTranslator$1();
|
|
1667
1891
|
const channel$1 = "nextcloud-talk";
|
|
1892
|
+
function readOptionalString(value) {
|
|
1893
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1894
|
+
}
|
|
1668
1895
|
function addWildcardAllowFrom(allowFrom) {
|
|
1669
1896
|
return mergeAllowFromEntries(allowFrom, ["*"]);
|
|
1670
1897
|
}
|
|
@@ -1783,7 +2010,17 @@ const nextcloudTalkDmPolicy = {
|
|
|
1783
2010
|
promptAllowFrom: promptNextcloudTalkAllowFromForAccount
|
|
1784
2011
|
};
|
|
1785
2012
|
const nextcloudTalkSetupAdapter = {
|
|
2013
|
+
singleAccountKeysToMove: ["rooms"],
|
|
1786
2014
|
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
2015
|
+
prepareAccountConfigInput: ({ input }) => {
|
|
2016
|
+
const setupInput = input;
|
|
2017
|
+
return {
|
|
2018
|
+
...setupInput,
|
|
2019
|
+
baseUrl: setupInput.baseUrl ?? readOptionalString(setupInput.url),
|
|
2020
|
+
secret: setupInput.secret ?? readOptionalString(setupInput.token) ?? readOptionalString(setupInput.password),
|
|
2021
|
+
secretFile: setupInput.secretFile ?? readOptionalString(setupInput.tokenFile)
|
|
2022
|
+
};
|
|
2023
|
+
},
|
|
1787
2024
|
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
1788
2025
|
cfg,
|
|
1789
2026
|
channelKey: channel$1,
|
|
@@ -1813,6 +2050,72 @@ const nextcloudTalkSetupAdapter = {
|
|
|
1813
2050
|
});
|
|
1814
2051
|
}
|
|
1815
2052
|
};
|
|
2053
|
+
const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
2054
|
+
fields: {
|
|
2055
|
+
baseUrl: {
|
|
2056
|
+
kind: "string",
|
|
2057
|
+
cli: {
|
|
2058
|
+
flags: "--base-url <url>",
|
|
2059
|
+
description: "Nextcloud base URL"
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
url: {
|
|
2063
|
+
kind: "string",
|
|
2064
|
+
cli: {
|
|
2065
|
+
flags: "--url <url>",
|
|
2066
|
+
description: "Legacy Nextcloud base URL alias"
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
secret: {
|
|
2070
|
+
kind: "string",
|
|
2071
|
+
sensitive: true,
|
|
2072
|
+
cli: {
|
|
2073
|
+
flags: "--secret <secret>",
|
|
2074
|
+
description: "Nextcloud Talk bot secret"
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
token: {
|
|
2078
|
+
kind: "string",
|
|
2079
|
+
sensitive: true,
|
|
2080
|
+
cli: {
|
|
2081
|
+
flags: "--token <secret>",
|
|
2082
|
+
description: "Legacy Nextcloud bot secret alias"
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
password: {
|
|
2086
|
+
kind: "string",
|
|
2087
|
+
sensitive: true,
|
|
2088
|
+
cli: {
|
|
2089
|
+
flags: "--password <secret>",
|
|
2090
|
+
description: "Legacy Nextcloud bot secret alias"
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
secretFile: {
|
|
2094
|
+
kind: "string",
|
|
2095
|
+
sensitive: true,
|
|
2096
|
+
cli: {
|
|
2097
|
+
flags: "--secret-file <path>",
|
|
2098
|
+
description: "Nextcloud Talk bot secret file"
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
tokenFile: {
|
|
2102
|
+
kind: "string",
|
|
2103
|
+
sensitive: true,
|
|
2104
|
+
cli: {
|
|
2105
|
+
flags: "--token-file <path>",
|
|
2106
|
+
description: "Legacy Nextcloud bot secret file alias"
|
|
2107
|
+
}
|
|
2108
|
+
},
|
|
2109
|
+
useEnv: {
|
|
2110
|
+
kind: "boolean",
|
|
2111
|
+
cli: {
|
|
2112
|
+
flags: "--use-env",
|
|
2113
|
+
description: "Use Nextcloud Talk environment credentials"
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
legacyAdapter: nextcloudTalkSetupAdapter
|
|
2118
|
+
});
|
|
1816
2119
|
//#endregion
|
|
1817
2120
|
//#region extensions/nextcloud-talk/src/setup-surface.ts
|
|
1818
2121
|
const t = createSetupTranslator();
|
|
@@ -1992,12 +2295,14 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
1992
2295
|
configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
|
|
1993
2296
|
config: {
|
|
1994
2297
|
...nextcloudTalkConfigAdapter,
|
|
1995
|
-
isConfigured: (account) => Boolean(account.
|
|
2298
|
+
isConfigured: (account) => Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
|
|
1996
2299
|
describeAccount: (account) => describeWebhookAccountSnapshot({
|
|
1997
2300
|
account,
|
|
1998
|
-
configured: Boolean(account.
|
|
2301
|
+
configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
|
|
1999
2302
|
extra: {
|
|
2000
2303
|
secretSource: account.secretSource,
|
|
2304
|
+
tokenStatus: account.tokenStatus,
|
|
2305
|
+
apiCredentialStatus: account.apiCredentialStatus,
|
|
2001
2306
|
baseUrl: account.baseUrl ? "[set]" : "[missing]"
|
|
2002
2307
|
}
|
|
2003
2308
|
})
|
|
@@ -2026,6 +2331,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2026
2331
|
collectRuntimeConfigAssignments
|
|
2027
2332
|
},
|
|
2028
2333
|
setup: nextcloudTalkSetupAdapter,
|
|
2334
|
+
setupContract: nextcloudTalkSetupContract,
|
|
2029
2335
|
status: createComputedAccountStatusAdapter({
|
|
2030
2336
|
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
|
|
2031
2337
|
buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
|
|
@@ -2048,9 +2354,11 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2048
2354
|
accountId: account.accountId,
|
|
2049
2355
|
name: account.name,
|
|
2050
2356
|
enabled: account.enabled,
|
|
2051
|
-
configured: Boolean(account.
|
|
2357
|
+
configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
|
|
2052
2358
|
extra: {
|
|
2053
2359
|
secretSource: account.secretSource,
|
|
2360
|
+
tokenStatus: account.tokenStatus,
|
|
2361
|
+
apiCredentialStatus: account.apiCredentialStatus,
|
|
2054
2362
|
baseUrl: account.baseUrl ? "[set]" : "[missing]",
|
|
2055
2363
|
mode: "webhook"
|
|
2056
2364
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-DtnECxVM.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/nextcloud-talk",
|
|
9
|
-
"version": "2026.7.2-beta.
|
|
9
|
+
"version": "2026.7.2-beta.4",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"zod": "4.4.3"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"openclaw": ">=2026.7.2-beta.
|
|
14
|
+
"openclaw": ">=2026.7.2-beta.4"
|
|
15
15
|
},
|
|
16
16
|
"peerDependenciesMeta": {
|
|
17
17
|
"openclaw": {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.4",
|
|
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.7.2-beta.
|
|
11
|
+
"openclaw": ">=2026.7.2-beta.4"
|
|
12
12
|
},
|
|
13
13
|
"peerDependenciesMeta": {
|
|
14
14
|
"openclaw": {
|
|
@@ -20,8 +20,19 @@
|
|
|
20
20
|
"./index.ts"
|
|
21
21
|
],
|
|
22
22
|
"setupEntry": "./dist/setup-entry.js",
|
|
23
|
+
"setupFeatures": {
|
|
24
|
+
"configPromotion": true
|
|
25
|
+
},
|
|
23
26
|
"channel": {
|
|
24
27
|
"id": "nextcloud-talk",
|
|
28
|
+
"configuredState": {
|
|
29
|
+
"env": {
|
|
30
|
+
"anyOf": [
|
|
31
|
+
"NEXTCLOUD_TALK_BOT_SECRET",
|
|
32
|
+
"NEXTCLOUD_TALK_API_PASSWORD"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
25
36
|
"label": "Nextcloud Talk",
|
|
26
37
|
"selectionLabel": "Nextcloud Talk (self-hosted)",
|
|
27
38
|
"docsPath": "/channels/nextcloud-talk",
|
|
@@ -32,7 +43,80 @@
|
|
|
32
43
|
"nc"
|
|
33
44
|
],
|
|
34
45
|
"order": 65,
|
|
35
|
-
"quickstartAllowFrom": true
|
|
46
|
+
"quickstartAllowFrom": true,
|
|
47
|
+
"setup": {
|
|
48
|
+
"fields": [
|
|
49
|
+
{
|
|
50
|
+
"key": "baseUrl",
|
|
51
|
+
"kind": "string",
|
|
52
|
+
"cli": {
|
|
53
|
+
"flags": "--base-url <url>",
|
|
54
|
+
"description": "Nextcloud base URL"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"key": "url",
|
|
59
|
+
"kind": "string",
|
|
60
|
+
"cli": {
|
|
61
|
+
"flags": "--url <url>",
|
|
62
|
+
"description": "Legacy Nextcloud base URL alias"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"key": "secret",
|
|
67
|
+
"kind": "string",
|
|
68
|
+
"sensitive": true,
|
|
69
|
+
"cli": {
|
|
70
|
+
"flags": "--secret <secret>",
|
|
71
|
+
"description": "Nextcloud Talk bot secret"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"key": "token",
|
|
76
|
+
"kind": "string",
|
|
77
|
+
"sensitive": true,
|
|
78
|
+
"cli": {
|
|
79
|
+
"flags": "--token <secret>",
|
|
80
|
+
"description": "Legacy Nextcloud bot secret alias"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"key": "password",
|
|
85
|
+
"kind": "string",
|
|
86
|
+
"sensitive": true,
|
|
87
|
+
"cli": {
|
|
88
|
+
"flags": "--password <secret>",
|
|
89
|
+
"description": "Legacy Nextcloud bot secret alias"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"key": "secretFile",
|
|
94
|
+
"kind": "string",
|
|
95
|
+
"sensitive": true,
|
|
96
|
+
"cli": {
|
|
97
|
+
"flags": "--secret-file <path>",
|
|
98
|
+
"description": "Nextcloud Talk bot secret file"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"key": "tokenFile",
|
|
103
|
+
"kind": "string",
|
|
104
|
+
"sensitive": true,
|
|
105
|
+
"cli": {
|
|
106
|
+
"flags": "--token-file <path>",
|
|
107
|
+
"description": "Legacy Nextcloud bot secret file alias"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"key": "useEnv",
|
|
112
|
+
"kind": "boolean",
|
|
113
|
+
"cli": {
|
|
114
|
+
"flags": "--use-env",
|
|
115
|
+
"description": "Use Nextcloud Talk environment credentials"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
36
120
|
},
|
|
37
121
|
"install": {
|
|
38
122
|
"npmSpec": "@openclaw/nextcloud-talk",
|
|
@@ -40,10 +124,10 @@
|
|
|
40
124
|
"minHostVersion": ">=2026.4.10"
|
|
41
125
|
},
|
|
42
126
|
"compat": {
|
|
43
|
-
"pluginApi": ">=2026.7.2-beta.
|
|
127
|
+
"pluginApi": ">=2026.7.2-beta.4"
|
|
44
128
|
},
|
|
45
129
|
"build": {
|
|
46
|
-
"openclawVersion": "2026.7.2-beta.
|
|
130
|
+
"openclawVersion": "2026.7.2-beta.4"
|
|
47
131
|
},
|
|
48
132
|
"release": {
|
|
49
133
|
"publishToClawHub": true,
|