@openclaw/nextcloud-talk 2026.7.2-beta.5 → 2026.7.33
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/README.md +1 -1
- package/dist/.setup/api-Debykz-a.mjs +3 -0
- package/dist/{channel-D6MTjGj3.js → .setup/channel-DzVvFd6m.mjs} +593 -723
- package/dist/.setup/doctor-contract-BY7I5-v9.mjs +7 -0
- package/dist/{secret-contract-DGgvfVqm.js → .setup/secret-contract-CsCJ-vOP.mjs} +47 -6
- package/dist/.setup/secret-contract-api-CcDcTyh9.mjs +3 -0
- package/dist/api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +1 -1
- package/dist/runtime-api.js +1 -1
- package/dist/secret-contract-api.js +1 -1
- package/dist/setup-entry.js +3 -2
- package/openclaw.plugin.json +54 -72
- package/package.json +5 -89
- package/dist/doctor-contract-fNhK1wag.js +0 -22
- /package/dist/{runtime-api-BKsxTw4J.js → .setup/runtime-api-BKsxTw4J.mjs} +0 -0
|
@@ -1,82 +1,49 @@
|
|
|
1
|
-
import { a as fetchWithSsrFGuard, c as resolveDefaultGroupPolicy, i as deliverFormattedTextWithAttachments, l as warnMissingProviderGroupPolicyFallbackOnce, o as logInboundDrop, r as createChannelPairingController, s as resolveAllowlistProviderRuntimeGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as getNextcloudTalkRuntime } from "./runtime-api-BKsxTw4J.
|
|
2
|
-
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-
|
|
3
|
-
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-
|
|
4
|
-
import {
|
|
1
|
+
import { a as fetchWithSsrFGuard, c as resolveDefaultGroupPolicy, i as deliverFormattedTextWithAttachments, l as warnMissingProviderGroupPolicyFallbackOnce, o as logInboundDrop, r as createChannelPairingController, s as resolveAllowlistProviderRuntimeGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as getNextcloudTalkRuntime } from "./runtime-api-BKsxTw4J.mjs";
|
|
2
|
+
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-BY7I5-v9.mjs";
|
|
3
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-CsCJ-vOP.mjs";
|
|
4
|
+
import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
5
5
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
6
6
|
import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
|
7
|
-
import { createAllowlistProviderRouteAllowlistWarningCollector
|
|
7
|
+
import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
|
8
8
|
import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
|
-
import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
|
|
9
|
+
import { convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
10
|
+
import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
|
|
13
11
|
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
12
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
14
13
|
import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
14
|
+
import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
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 {
|
|
19
|
-
import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
|
|
18
|
+
import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
|
|
20
19
|
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
21
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
22
22
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
23
|
-
import { createHmac, randomBytes } from "node:crypto";
|
|
24
|
-
import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
23
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
25
24
|
import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
26
25
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
|
|
27
|
-
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape,
|
|
28
|
-
import { formatAllowFromLowercase
|
|
26
|
+
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ToolPolicySchema, buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
27
|
+
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
|
29
28
|
import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
30
|
-
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
29
|
+
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime, runStoppablePassiveMonitor, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
|
|
31
30
|
import { z } from "zod";
|
|
31
|
+
import fs from "node:fs/promises";
|
|
32
32
|
import os from "node:os";
|
|
33
33
|
import path from "node:path";
|
|
34
|
-
import {
|
|
34
|
+
import { createClaimableDedupe, migratePersistentDedupeLegacyJsonFile } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
35
35
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
36
36
|
import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
37
|
-
import {
|
|
37
|
+
import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
|
38
|
+
import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision } from "openclaw/plugin-sdk/channel-targets";
|
|
38
39
|
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
39
|
-
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
40
40
|
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 {
|
|
45
|
-
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
46
|
-
import { applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
44
|
+
import { applyAccountNameToChannelSection, createSetupTranslator, createStandardChannelSetupStatus, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
47
45
|
import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries, promptParsedAllowFromForAccount, resolveSetupAccountId } from "openclaw/plugin-sdk/setup-runtime";
|
|
48
46
|
import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
|
|
49
|
-
//#region extensions/nextcloud-talk/src/api-credentials.ts
|
|
50
|
-
function resolveNextcloudTalkApiCredentialsResult(params) {
|
|
51
|
-
const apiUser = params.apiUser?.trim();
|
|
52
|
-
if (!apiUser) return { status: "missing" };
|
|
53
|
-
const inlinePassword = normalizeResolvedSecretInputString({
|
|
54
|
-
value: params.apiPassword,
|
|
55
|
-
path: "channels.nextcloud-talk.apiPassword"
|
|
56
|
-
});
|
|
57
|
-
if (inlinePassword) return {
|
|
58
|
-
status: "available",
|
|
59
|
-
value: {
|
|
60
|
-
apiUser,
|
|
61
|
-
apiPassword: inlinePassword
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
if (!params.apiPasswordFile?.trim()) return { status: "missing" };
|
|
65
|
-
const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
|
|
66
|
-
if (result.status === "available") return result.value ? {
|
|
67
|
-
status: "available",
|
|
68
|
-
value: {
|
|
69
|
-
apiUser,
|
|
70
|
-
apiPassword: result.value
|
|
71
|
-
}
|
|
72
|
-
} : { status: "missing" };
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
function resolveNextcloudTalkApiCredentials(params) {
|
|
76
|
-
const result = resolveNextcloudTalkApiCredentialsResult(params);
|
|
77
|
-
return result.status === "available" ? result.value : void 0;
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
47
|
//#region extensions/nextcloud-talk/src/accounts.ts
|
|
81
48
|
function isTruthyEnvValue(value) {
|
|
82
49
|
const normalized = normalizeLowercaseStringOrEmpty(value);
|
|
@@ -85,9 +52,8 @@ function isTruthyEnvValue(value) {
|
|
|
85
52
|
const debugAccounts = (...args) => {
|
|
86
53
|
if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_NEXTCLOUD_TALK_ACCOUNTS)) console.warn("[nextcloud-talk:accounts]", ...args);
|
|
87
54
|
};
|
|
88
|
-
const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId
|
|
55
|
+
const { listAccountIds: listNextcloudTalkAccountIdsInternal, resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId } = createAccountListHelpers("nextcloud-talk", {
|
|
89
56
|
normalizeAccountId,
|
|
90
|
-
omitKeys: ["defaultAccount"],
|
|
91
57
|
hasImplicitDefaultAccount: (cfg) => {
|
|
92
58
|
const channel = cfg.channels?.["nextcloud-talk"];
|
|
93
59
|
return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.botSecret) || channel.botSecretFile?.trim() || process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()));
|
|
@@ -98,27 +64,28 @@ function listNextcloudTalkAccountIds(cfg) {
|
|
|
98
64
|
debugAccounts("listNextcloudTalkAccountIds", ids);
|
|
99
65
|
return ids;
|
|
100
66
|
}
|
|
67
|
+
function mergeNextcloudTalkAccountConfig(cfg, accountId) {
|
|
68
|
+
return resolveMergedAccountConfig({
|
|
69
|
+
channelConfig: cfg.channels?.["nextcloud-talk"],
|
|
70
|
+
accounts: cfg.channels?.["nextcloud-talk"]?.accounts,
|
|
71
|
+
accountId,
|
|
72
|
+
omitKeys: ["defaultAccount"],
|
|
73
|
+
normalizeAccountId
|
|
74
|
+
});
|
|
75
|
+
}
|
|
101
76
|
function resolveNextcloudTalkSecret(cfg, opts) {
|
|
102
77
|
const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
|
|
103
78
|
const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
|
|
104
79
|
const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
|
|
105
80
|
if (envSecret && resolvedAccountId === DEFAULT_ACCOUNT_ID) return {
|
|
106
81
|
secret: envSecret,
|
|
107
|
-
source: "env"
|
|
108
|
-
status: "available"
|
|
82
|
+
source: "env"
|
|
109
83
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
source: "secretFile",
|
|
116
|
-
status: "available"
|
|
117
|
-
} : {
|
|
118
|
-
secret: "",
|
|
119
|
-
source: "secretFile",
|
|
120
|
-
status: "configured_unavailable",
|
|
121
|
-
diagnostic: result.diagnostic
|
|
84
|
+
if (merged.botSecretFile) {
|
|
85
|
+
const fileSecret = tryReadSecretFileSync(merged.botSecretFile, "Nextcloud Talk bot secret file", { rejectSymlink: true });
|
|
86
|
+
if (fileSecret) return {
|
|
87
|
+
secret: fileSecret,
|
|
88
|
+
source: "secretFile"
|
|
122
89
|
};
|
|
123
90
|
}
|
|
124
91
|
const inlineSecret = normalizeResolvedSecretInputString({
|
|
@@ -127,13 +94,11 @@ function resolveNextcloudTalkSecret(cfg, opts) {
|
|
|
127
94
|
});
|
|
128
95
|
if (inlineSecret) return {
|
|
129
96
|
secret: inlineSecret,
|
|
130
|
-
source: "config"
|
|
131
|
-
status: "available"
|
|
97
|
+
source: "config"
|
|
132
98
|
};
|
|
133
99
|
return {
|
|
134
100
|
secret: "",
|
|
135
|
-
source: "none"
|
|
136
|
-
status: "missing"
|
|
101
|
+
source: "none"
|
|
137
102
|
};
|
|
138
103
|
}
|
|
139
104
|
function resolveNextcloudTalkAccount(params) {
|
|
@@ -144,13 +109,6 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
144
109
|
const accountEnabled = merged.enabled !== false;
|
|
145
110
|
const enabled = baseEnabled && accountEnabled;
|
|
146
111
|
const secretResolution = resolveNextcloudTalkSecret(params.cfg, { accountId });
|
|
147
|
-
const apiCredentialResolution = resolveNextcloudTalkApiCredentialsResult({
|
|
148
|
-
apiUser: merged.apiUser,
|
|
149
|
-
apiPassword: merged.apiPassword,
|
|
150
|
-
apiPasswordFile: merged.apiPasswordFile,
|
|
151
|
-
configPath: `channels.nextcloud-talk.accounts.${accountId}.apiPasswordFile`
|
|
152
|
-
});
|
|
153
|
-
const diagnostics = [secretResolution.diagnostic, apiCredentialResolution.status === "configured_unavailable" ? apiCredentialResolution.diagnostic : void 0].filter((diagnostic) => Boolean(diagnostic));
|
|
154
112
|
const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
|
|
155
113
|
debugAccounts("resolve", {
|
|
156
114
|
accountId,
|
|
@@ -165,9 +123,6 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
165
123
|
baseUrl,
|
|
166
124
|
secret: secretResolution.secret,
|
|
167
125
|
secretSource: secretResolution.source,
|
|
168
|
-
tokenStatus: secretResolution.status,
|
|
169
|
-
apiCredentialStatus: apiCredentialResolution.status,
|
|
170
|
-
...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
|
|
171
126
|
config: merged
|
|
172
127
|
};
|
|
173
128
|
};
|
|
@@ -175,7 +130,7 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
175
130
|
accountId: resolvedAccountId,
|
|
176
131
|
normalizeAccountId,
|
|
177
132
|
resolvePrimary: resolve,
|
|
178
|
-
hasCredential: (account) => account.
|
|
133
|
+
hasCredential: (account) => account.secretSource !== "none",
|
|
179
134
|
resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg)
|
|
180
135
|
});
|
|
181
136
|
}
|
|
@@ -184,21 +139,43 @@ function resolveNextcloudTalkAccount(params) {
|
|
|
184
139
|
function normalizeNextcloudTalkApproverId(value) {
|
|
185
140
|
return normalizeOptionalLowercaseString(String(value).trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, ""));
|
|
186
141
|
}
|
|
187
|
-
const nextcloudTalkApprovalAuth =
|
|
142
|
+
const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
|
|
188
143
|
channelLabel: "Nextcloud Talk",
|
|
189
|
-
|
|
190
|
-
|
|
144
|
+
resolveApprovers: ({ cfg, accountId }) => {
|
|
145
|
+
const account = resolveNextcloudTalkAccount({
|
|
191
146
|
cfg,
|
|
192
147
|
accountId
|
|
193
|
-
})
|
|
148
|
+
});
|
|
149
|
+
return resolveApprovalApprovers({
|
|
150
|
+
allowFrom: account.config.allowFrom,
|
|
151
|
+
normalizeApprover: normalizeNextcloudTalkApproverId
|
|
152
|
+
});
|
|
194
153
|
},
|
|
195
|
-
|
|
196
|
-
})
|
|
154
|
+
normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value)
|
|
155
|
+
});
|
|
197
156
|
//#endregion
|
|
198
|
-
//#region extensions/nextcloud-talk/src/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
157
|
+
//#region extensions/nextcloud-talk/src/api-credentials.ts
|
|
158
|
+
function resolveNextcloudTalkApiCredentials(params) {
|
|
159
|
+
const apiUser = params.apiUser?.trim();
|
|
160
|
+
if (!apiUser) return;
|
|
161
|
+
const inlinePassword = normalizeResolvedSecretInputString({
|
|
162
|
+
value: params.apiPassword,
|
|
163
|
+
path: "channels.nextcloud-talk.apiPassword"
|
|
164
|
+
});
|
|
165
|
+
if (inlinePassword) return {
|
|
166
|
+
apiUser,
|
|
167
|
+
apiPassword: inlinePassword
|
|
168
|
+
};
|
|
169
|
+
if (!params.apiPasswordFile) return;
|
|
170
|
+
try {
|
|
171
|
+
const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
|
|
172
|
+
return filePassword ? {
|
|
173
|
+
apiUser,
|
|
174
|
+
apiPassword: filePassword
|
|
175
|
+
} : void 0;
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
202
179
|
}
|
|
203
180
|
//#endregion
|
|
204
181
|
//#region extensions/nextcloud-talk/src/signature.ts
|
|
@@ -212,7 +189,16 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
|
|
|
212
189
|
function verifyNextcloudTalkSignature(params) {
|
|
213
190
|
const { signature, random, body, secret } = params;
|
|
214
191
|
if (!signature || !random || !secret) return false;
|
|
215
|
-
|
|
192
|
+
const expected = createHmac("sha256", secret).update(random + body).digest("hex");
|
|
193
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
194
|
+
const signatureBuf = Buffer.from(signature, "utf8");
|
|
195
|
+
const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
|
|
196
|
+
const paddedExpected = Buffer.alloc(maxLen);
|
|
197
|
+
const paddedSignature = Buffer.alloc(maxLen);
|
|
198
|
+
expectedBuf.copy(paddedExpected);
|
|
199
|
+
signatureBuf.copy(paddedSignature);
|
|
200
|
+
const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
|
|
201
|
+
return expectedBuf.length === signatureBuf.length && timingResult;
|
|
216
202
|
}
|
|
217
203
|
/**
|
|
218
204
|
* Extract webhook headers from an incoming request.
|
|
@@ -246,7 +232,7 @@ function generateNextcloudTalkSignature(params) {
|
|
|
246
232
|
//#endregion
|
|
247
233
|
//#region extensions/nextcloud-talk/src/bot-preflight.ts
|
|
248
234
|
const BOT_FEATURE_RESPONSE = 2;
|
|
249
|
-
const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES =
|
|
235
|
+
const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8192;
|
|
250
236
|
function normalizeUrlForMatch(value) {
|
|
251
237
|
if (!value?.trim()) return "";
|
|
252
238
|
try {
|
|
@@ -344,10 +330,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
|
|
|
344
330
|
message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
|
|
345
331
|
};
|
|
346
332
|
} finally {
|
|
347
|
-
await
|
|
348
|
-
response,
|
|
349
|
-
release
|
|
350
|
-
});
|
|
333
|
+
await release();
|
|
351
334
|
}
|
|
352
335
|
} catch (error) {
|
|
353
336
|
return {
|
|
@@ -390,11 +373,18 @@ const nextcloudTalkPairingTextAdapter = {
|
|
|
390
373
|
};
|
|
391
374
|
//#endregion
|
|
392
375
|
//#region extensions/nextcloud-talk/src/config-schema.ts
|
|
393
|
-
const NextcloudTalkRoomSchema =
|
|
376
|
+
const NextcloudTalkRoomSchema = z.object({
|
|
377
|
+
requireMention: z.boolean().optional(),
|
|
378
|
+
tools: ToolPolicySchema,
|
|
379
|
+
skills: z.array(z.string()).optional(),
|
|
380
|
+
enabled: z.boolean().optional(),
|
|
381
|
+
allowFrom: z.array(z.string()).optional(),
|
|
382
|
+
systemPrompt: z.string().optional()
|
|
383
|
+
}).strict();
|
|
394
384
|
const NextcloudTalkNetworkSchema = z.object({
|
|
395
385
|
/** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
|
|
396
386
|
dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
|
|
397
|
-
const
|
|
387
|
+
const NextcloudTalkAccountSchemaBase = z.object({
|
|
398
388
|
name: z.string().optional(),
|
|
399
389
|
enabled: z.boolean().optional(),
|
|
400
390
|
markdown: MarkdownConfigSchema,
|
|
@@ -416,31 +406,114 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
416
406
|
/** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
|
|
417
407
|
network: NextcloudTalkNetworkSchema,
|
|
418
408
|
...ReplyRuntimeConfigSchemaShape
|
|
419
|
-
}).strict()
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
409
|
+
}).strict();
|
|
410
|
+
const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine((value, ctx) => {
|
|
411
|
+
requireChannelOpenAllowFrom({
|
|
412
|
+
channel: "nextcloud-talk",
|
|
413
|
+
policy: value.dmPolicy,
|
|
414
|
+
allowFrom: value.allowFrom,
|
|
415
|
+
ctx,
|
|
416
|
+
requireOpenAllowFrom
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
|
|
420
|
+
accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
|
|
421
|
+
defaultAccount: z.string().optional()
|
|
422
|
+
}).superRefine((value, ctx) => {
|
|
423
|
+
requireChannelOpenAllowFrom({
|
|
424
|
+
channel: "nextcloud-talk",
|
|
425
|
+
policy: value.dmPolicy,
|
|
426
|
+
allowFrom: value.allowFrom,
|
|
427
|
+
ctx,
|
|
428
|
+
requireOpenAllowFrom
|
|
429
|
+
});
|
|
430
430
|
});
|
|
431
431
|
//#endregion
|
|
432
|
-
//#region extensions/nextcloud-talk/src/replay-
|
|
432
|
+
//#region extensions/nextcloud-talk/src/replay-guard.ts
|
|
433
433
|
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
|
|
434
434
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
|
|
435
|
-
const
|
|
436
|
-
const
|
|
435
|
+
const DEFAULT_REPLAY_TTL_MS = 864e5;
|
|
436
|
+
const DEFAULT_MEMORY_MAX_SIZE = 1e3;
|
|
437
|
+
const DEFAULT_STATE_MAX_ENTRIES = 1e4;
|
|
438
|
+
function buildReplayKey(params) {
|
|
439
|
+
const roomToken = params.roomToken.trim();
|
|
440
|
+
const messageId = params.messageId.trim();
|
|
441
|
+
if (!roomToken || !messageId) return null;
|
|
442
|
+
return `${roomToken}:${messageId}`;
|
|
443
|
+
}
|
|
444
|
+
function createNextcloudTalkReplayGuard(options) {
|
|
445
|
+
const stateDir = options.stateDir?.trim();
|
|
446
|
+
const baseOptions = {
|
|
447
|
+
ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
|
|
448
|
+
memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE
|
|
449
|
+
};
|
|
450
|
+
const dedupe = createClaimableDedupe(stateDir ? {
|
|
451
|
+
...baseOptions,
|
|
452
|
+
pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
|
|
453
|
+
namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
|
|
454
|
+
stateMaxEntries: options.stateMaxEntries ?? options.fileMaxEntries ?? DEFAULT_STATE_MAX_ENTRIES,
|
|
455
|
+
env: {
|
|
456
|
+
...process.env,
|
|
457
|
+
OPENCLAW_STATE_DIR: stateDir
|
|
458
|
+
},
|
|
459
|
+
onDiskError: options.onDiskError
|
|
460
|
+
} : baseOptions);
|
|
461
|
+
return {
|
|
462
|
+
claimMessage: async ({ accountId, roomToken, messageId }) => {
|
|
463
|
+
const replayKey = buildReplayKey({
|
|
464
|
+
roomToken,
|
|
465
|
+
messageId
|
|
466
|
+
});
|
|
467
|
+
if (!replayKey) return "invalid";
|
|
468
|
+
return (await dedupe.claim(replayKey, { namespace: accountId })).kind;
|
|
469
|
+
},
|
|
470
|
+
commitMessage: async ({ accountId, roomToken, messageId }) => {
|
|
471
|
+
const replayKey = buildReplayKey({
|
|
472
|
+
roomToken,
|
|
473
|
+
messageId
|
|
474
|
+
});
|
|
475
|
+
if (!replayKey) return true;
|
|
476
|
+
return await dedupe.commit(replayKey, { namespace: accountId });
|
|
477
|
+
},
|
|
478
|
+
releaseMessage: ({ accountId, roomToken, messageId, error }) => {
|
|
479
|
+
const replayKey = buildReplayKey({
|
|
480
|
+
roomToken,
|
|
481
|
+
messageId
|
|
482
|
+
});
|
|
483
|
+
if (!replayKey) return;
|
|
484
|
+
dedupe.release(replayKey, {
|
|
485
|
+
namespace: accountId,
|
|
486
|
+
error
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
shouldProcessMessage: async ({ accountId, roomToken, messageId }) => {
|
|
490
|
+
const replayKey = buildReplayKey({
|
|
491
|
+
roomToken,
|
|
492
|
+
messageId
|
|
493
|
+
});
|
|
494
|
+
if (!replayKey) return true;
|
|
495
|
+
if ((await dedupe.claim(replayKey, { namespace: accountId })).kind !== "claimed") return false;
|
|
496
|
+
return await dedupe.commit(replayKey, { namespace: accountId });
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
437
500
|
//#endregion
|
|
438
501
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
502
|
+
const REPLAY_DEDUPE_TTL_MS = 864e5;
|
|
503
|
+
const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
439
504
|
function sanitizeLegacyReplaySegment(value) {
|
|
440
505
|
const trimmed = value.trim();
|
|
441
506
|
if (!trimmed) return "default";
|
|
442
507
|
return trimmed.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
443
508
|
}
|
|
509
|
+
async function fileExists(filePath) {
|
|
510
|
+
try {
|
|
511
|
+
await fs.access(filePath);
|
|
512
|
+
return true;
|
|
513
|
+
} catch {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
444
517
|
async function collectNextcloudTalkBotResponseWarnings(params) {
|
|
445
518
|
const warnings = [];
|
|
446
519
|
for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
|
|
@@ -465,16 +538,16 @@ async function repairNextcloudTalkReplayDedupeState(params) {
|
|
|
465
538
|
const replayDir = path.join(stateDir, "nextcloud-talk", "replay-dedupe");
|
|
466
539
|
for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
|
|
467
540
|
const legacyPath = path.join(replayDir, `${sanitizeLegacyReplaySegment(accountId)}.json`);
|
|
468
|
-
if (!fileExists(legacyPath)) continue;
|
|
541
|
+
if (!await fileExists(legacyPath)) continue;
|
|
469
542
|
try {
|
|
470
543
|
const result = await migratePersistentDedupeLegacyJsonFile({
|
|
471
544
|
filePath: legacyPath,
|
|
472
545
|
namespace: accountId,
|
|
473
|
-
ttlMs:
|
|
546
|
+
ttlMs: REPLAY_DEDUPE_TTL_MS,
|
|
474
547
|
memoryMaxSize: 0,
|
|
475
548
|
pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
|
|
476
549
|
namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
|
|
477
|
-
stateMaxEntries:
|
|
550
|
+
stateMaxEntries: REPLAY_DEDUPE_MAX_ENTRIES,
|
|
478
551
|
env
|
|
479
552
|
});
|
|
480
553
|
changes.push(`Migrated Nextcloud Talk replay dedupe cache for account "${accountId}" to SQLite (${result.imported} imported, ${result.skippedExpired} expired, ${result.skippedExisting} already current).`);
|
|
@@ -512,25 +585,38 @@ function normalizeNextcloudTalkAllowlist(values) {
|
|
|
512
585
|
return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
|
|
513
586
|
}
|
|
514
587
|
function resolveNextcloudTalkAllowlistMatch(params) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
588
|
+
const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
|
|
589
|
+
if (allowFrom.length === 0) return { allowed: false };
|
|
590
|
+
if (allowFrom.includes("*")) return {
|
|
591
|
+
allowed: true,
|
|
592
|
+
matchKey: "*",
|
|
593
|
+
matchSource: "wildcard"
|
|
594
|
+
};
|
|
595
|
+
const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
|
|
596
|
+
if (allowFrom.includes(senderId)) return {
|
|
597
|
+
allowed: true,
|
|
598
|
+
matchKey: senderId,
|
|
599
|
+
matchSource: "id"
|
|
600
|
+
};
|
|
601
|
+
return { allowed: false };
|
|
522
602
|
}
|
|
523
603
|
function resolveNextcloudTalkRoomMatch(params) {
|
|
524
604
|
const rooms = params.rooms ?? {};
|
|
525
605
|
const allowlistConfigured = Object.keys(rooms).length > 0;
|
|
606
|
+
const roomCandidates = buildChannelKeyCandidates(params.roomToken);
|
|
526
607
|
const match = resolveChannelEntryMatchWithFallback({
|
|
527
608
|
entries: rooms,
|
|
528
|
-
keys:
|
|
609
|
+
keys: roomCandidates,
|
|
529
610
|
wildcardKey: "*",
|
|
530
611
|
normalizeKey: normalizeChannelSlug
|
|
531
612
|
});
|
|
532
613
|
const roomConfig = match.entry;
|
|
533
|
-
const allowed =
|
|
614
|
+
const allowed = resolveNestedAllowlistDecision({
|
|
615
|
+
outerConfigured: allowlistConfigured,
|
|
616
|
+
outerMatched: Boolean(roomConfig),
|
|
617
|
+
innerConfigured: false,
|
|
618
|
+
innerMatched: false
|
|
619
|
+
});
|
|
534
620
|
return {
|
|
535
621
|
roomConfig,
|
|
536
622
|
wildcardConfig: match.wildcardEntry,
|
|
@@ -541,50 +627,24 @@ function resolveNextcloudTalkRoomMatch(params) {
|
|
|
541
627
|
};
|
|
542
628
|
}
|
|
543
629
|
function resolveNextcloudTalkGroupToolPolicy(params) {
|
|
630
|
+
const cfg = params.cfg;
|
|
544
631
|
const roomToken = params.groupId?.trim();
|
|
545
632
|
if (!roomToken) return;
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}).config.rooms, roomToken);
|
|
550
|
-
return resolveScopeToolsPolicy({
|
|
551
|
-
tree,
|
|
552
|
-
path: toolsPath
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
function buildNextcloudTalkRoomScope(rooms, roomToken) {
|
|
556
|
-
const { "*": defaults, ...scopes } = rooms ?? {};
|
|
557
|
-
const tree = {
|
|
558
|
-
defaults,
|
|
559
|
-
scopes
|
|
560
|
-
};
|
|
561
|
-
const exactPath = Object.hasOwn(scopes, roomToken) ? [roomToken] : [];
|
|
562
|
-
const toolsMatch = resolveChannelEntryMatchWithFallback({
|
|
563
|
-
entries: scopes,
|
|
564
|
-
keys: buildChannelKeyCandidates(roomToken),
|
|
565
|
-
normalizeKey: normalizeChannelSlug
|
|
633
|
+
const match = resolveNextcloudTalkRoomMatch({
|
|
634
|
+
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
|
635
|
+
roomToken
|
|
566
636
|
});
|
|
567
|
-
return
|
|
568
|
-
tree,
|
|
569
|
-
exactPath,
|
|
570
|
-
toolsPath: toolsMatch.matchKey ? [toolsMatch.matchKey] : []
|
|
571
|
-
};
|
|
637
|
+
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
|
|
572
638
|
}
|
|
573
|
-
function
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
accountId: params.accountId
|
|
578
|
-
}).config.rooms, params.groupId);
|
|
579
|
-
return resolveScopeRequireMention({
|
|
580
|
-
tree,
|
|
581
|
-
path: exactPath
|
|
582
|
-
});
|
|
639
|
+
function resolveNextcloudTalkRequireMention(params) {
|
|
640
|
+
if (typeof params.roomConfig?.requireMention === "boolean") return params.roomConfig.requireMention;
|
|
641
|
+
if (typeof params.wildcardConfig?.requireMention === "boolean") return params.wildcardConfig.requireMention;
|
|
642
|
+
return true;
|
|
583
643
|
}
|
|
584
644
|
//#endregion
|
|
585
645
|
//#region extensions/nextcloud-talk/src/room-info.ts
|
|
586
|
-
const ROOM_CACHE_TTL_MS =
|
|
587
|
-
const ROOM_CACHE_ERROR_TTL_MS =
|
|
646
|
+
const ROOM_CACHE_TTL_MS = 3e5;
|
|
647
|
+
const ROOM_CACHE_ERROR_TTL_MS = 3e4;
|
|
588
648
|
const ROOM_CACHE_MAX_ENTRIES = 1e3;
|
|
589
649
|
const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
|
|
590
650
|
const roomCache = /* @__PURE__ */ new Map();
|
|
@@ -657,10 +717,7 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
657
717
|
});
|
|
658
718
|
return kind;
|
|
659
719
|
} finally {
|
|
660
|
-
await
|
|
661
|
-
response,
|
|
662
|
-
release
|
|
663
|
-
});
|
|
720
|
+
await release();
|
|
664
721
|
}
|
|
665
722
|
} catch (err) {
|
|
666
723
|
cacheRoomInfo(key, {
|
|
@@ -677,10 +734,10 @@ function stripNextcloudTalkTargetPrefix(raw) {
|
|
|
677
734
|
const trimmed = raw.trim();
|
|
678
735
|
if (!trimmed) return;
|
|
679
736
|
let normalized = trimmed;
|
|
680
|
-
if (
|
|
681
|
-
else if (
|
|
682
|
-
else if (
|
|
683
|
-
if (
|
|
737
|
+
if (normalized.startsWith("nextcloud-talk:")) normalized = normalized.slice(15).trim();
|
|
738
|
+
else if (normalized.startsWith("nc-talk:")) normalized = normalized.slice(8).trim();
|
|
739
|
+
else if (normalized.startsWith("nc:")) normalized = normalized.slice(3).trim();
|
|
740
|
+
if (normalized.startsWith("room:")) normalized = normalized.slice(5).trim();
|
|
684
741
|
if (!normalized) return;
|
|
685
742
|
return normalized;
|
|
686
743
|
}
|
|
@@ -691,36 +748,17 @@ function normalizeNextcloudTalkMessagingTarget(raw) {
|
|
|
691
748
|
function looksLikeNextcloudTalkTargetId(raw) {
|
|
692
749
|
const trimmed = raw.trim();
|
|
693
750
|
if (!trimmed) return false;
|
|
694
|
-
if (/^(nextcloud-talk|nc-talk|nc
|
|
751
|
+
if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed)) return true;
|
|
695
752
|
return /^[a-z0-9]{8,}$/i.test(trimmed);
|
|
696
753
|
}
|
|
697
754
|
//#endregion
|
|
698
755
|
//#region extensions/nextcloud-talk/src/send.ts
|
|
699
|
-
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES =
|
|
756
|
+
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8192;
|
|
700
757
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
|
|
701
|
-
const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
|
|
702
|
-
const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
|
|
703
|
-
mechanism: "markdown",
|
|
704
|
-
chunk: {
|
|
705
|
-
limit: 4e3,
|
|
706
|
-
unit: "chars",
|
|
707
|
-
hardCap: 32e3
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
function renderNextcloudTalkMarkdown(markdown) {
|
|
711
|
-
return renderMarkdownWithMarkers({
|
|
712
|
-
text: markdown,
|
|
713
|
-
styles: [],
|
|
714
|
-
links: []
|
|
715
|
-
}, {
|
|
716
|
-
styleMarkers: {},
|
|
717
|
-
escapeText: (text) => text
|
|
718
|
-
}, NEXTCLOUD_TALK_FORMAT_PROFILE);
|
|
719
|
-
}
|
|
720
758
|
/** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
|
|
721
759
|
function collapseErrorSnippet(text) {
|
|
722
760
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
723
|
-
if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${
|
|
761
|
+
if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${collapsed.slice(0, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
|
|
724
762
|
return collapsed;
|
|
725
763
|
}
|
|
726
764
|
/** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
|
|
@@ -795,7 +833,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
795
833
|
channel: "nextcloud-talk",
|
|
796
834
|
accountId: account.accountId
|
|
797
835
|
});
|
|
798
|
-
const message = convertMarkdownTables(
|
|
836
|
+
const message = convertMarkdownTables(text.trim(), tableMode);
|
|
799
837
|
const body = { message };
|
|
800
838
|
if (opts.replyTo) body.replyTo = opts.replyTo;
|
|
801
839
|
const bodyStr = JSON.stringify(body);
|
|
@@ -803,8 +841,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
803
841
|
body: message,
|
|
804
842
|
secret
|
|
805
843
|
});
|
|
844
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
|
|
806
845
|
const { response, release } = await fetchWithSsrFGuard({
|
|
807
|
-
url
|
|
846
|
+
url,
|
|
808
847
|
init: {
|
|
809
848
|
method: "POST",
|
|
810
849
|
headers: {
|
|
@@ -816,8 +855,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
816
855
|
body: bodyStr
|
|
817
856
|
},
|
|
818
857
|
auditContext: "nextcloud-talk-send",
|
|
819
|
-
policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config)
|
|
820
|
-
timeoutMs: opts.timeoutMs ?? NEXTCLOUD_TALK_SEND_TIMEOUT_MS
|
|
858
|
+
policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config)
|
|
821
859
|
});
|
|
822
860
|
try {
|
|
823
861
|
if (!response.ok) {
|
|
@@ -851,10 +889,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
851
889
|
timestamp
|
|
852
890
|
};
|
|
853
891
|
} finally {
|
|
854
|
-
await
|
|
855
|
-
response,
|
|
856
|
-
release
|
|
857
|
-
});
|
|
892
|
+
await release();
|
|
858
893
|
}
|
|
859
894
|
}
|
|
860
895
|
async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
@@ -865,8 +900,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
865
900
|
body: reaction,
|
|
866
901
|
secret
|
|
867
902
|
});
|
|
903
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
|
|
868
904
|
const { response, release } = await fetchWithSsrFGuard({
|
|
869
|
-
url
|
|
905
|
+
url,
|
|
870
906
|
init: {
|
|
871
907
|
method: "POST",
|
|
872
908
|
headers: {
|
|
@@ -878,8 +914,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
878
914
|
body
|
|
879
915
|
},
|
|
880
916
|
auditContext: "nextcloud-talk-reaction",
|
|
881
|
-
policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config)
|
|
882
|
-
timeoutMs: opts.timeoutMs ?? NEXTCLOUD_TALK_SEND_TIMEOUT_MS
|
|
917
|
+
policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config)
|
|
883
918
|
});
|
|
884
919
|
try {
|
|
885
920
|
if (!response.ok) {
|
|
@@ -888,10 +923,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
888
923
|
}
|
|
889
924
|
return { ok: true };
|
|
890
925
|
} finally {
|
|
891
|
-
await
|
|
892
|
-
response,
|
|
893
|
-
release
|
|
894
|
-
});
|
|
926
|
+
await release();
|
|
895
927
|
}
|
|
896
928
|
}
|
|
897
929
|
//#endregion
|
|
@@ -976,10 +1008,9 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
976
1008
|
surface: CHANNEL_ID
|
|
977
1009
|
});
|
|
978
1010
|
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
|
|
979
|
-
const shouldRequireMention = isGroup ?
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
groupId: roomToken
|
|
1011
|
+
const shouldRequireMention = isGroup ? resolveNextcloudTalkRequireMention({
|
|
1012
|
+
roomConfig,
|
|
1013
|
+
wildcardConfig: roomMatch.wildcardConfig
|
|
983
1014
|
}) : false;
|
|
984
1015
|
const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
985
1016
|
providerConfigPresent: (config.channels?.[CHANNEL_ID] ?? void 0) !== void 0,
|
|
@@ -1097,76 +1128,59 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1097
1128
|
runtime.log?.(`nextcloud-talk: drop room ${roomToken} (no mention)`);
|
|
1098
1129
|
return;
|
|
1099
1130
|
}
|
|
1100
|
-
const { route, buildEnvelope } =
|
|
1131
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
1101
1132
|
cfg: config,
|
|
1102
1133
|
channel: CHANNEL_ID,
|
|
1103
1134
|
accountId: account.accountId,
|
|
1104
1135
|
peer: {
|
|
1105
1136
|
kind: isGroup ? "group" : "direct",
|
|
1106
1137
|
id: isGroup ? roomToken : senderId
|
|
1107
|
-
}
|
|
1138
|
+
},
|
|
1139
|
+
runtime: core.channel,
|
|
1140
|
+
sessionStore: config.session?.store
|
|
1108
1141
|
});
|
|
1109
1142
|
const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
|
|
1110
|
-
const body = buildEnvelope({
|
|
1143
|
+
const { storePath, body } = buildEnvelope({
|
|
1111
1144
|
channel: "Nextcloud Talk",
|
|
1112
1145
|
from: fromLabel,
|
|
1113
1146
|
timestamp: message.timestamp,
|
|
1114
1147
|
body: rawBody
|
|
1115
1148
|
});
|
|
1116
1149
|
const groupSystemPrompt = normalizeOptionalString(roomConfig?.systemPrompt);
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
to: `nextcloud-talk:${roomToken}`,
|
|
1141
|
-
originatingTo: `nextcloud-talk:${roomToken}`
|
|
1142
|
-
},
|
|
1143
|
-
message: {
|
|
1144
|
-
body,
|
|
1145
|
-
bodyForAgent: rawBody,
|
|
1146
|
-
rawBody,
|
|
1147
|
-
commandBody: rawBody
|
|
1148
|
-
},
|
|
1149
|
-
access: {
|
|
1150
|
-
commands: { authorized: commandAuthorized },
|
|
1151
|
-
mentions: {
|
|
1152
|
-
canDetectMention: isGroup,
|
|
1153
|
-
wasMentioned: isGroup && wasMentioned
|
|
1154
|
-
}
|
|
1155
|
-
},
|
|
1156
|
-
extra: {
|
|
1157
|
-
GroupSubject: isGroup ? roomName || roomToken : void 0,
|
|
1158
|
-
GroupSystemPrompt: isGroup ? groupSystemPrompt : void 0
|
|
1159
|
-
}
|
|
1150
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1151
|
+
Body: body,
|
|
1152
|
+
BodyForAgent: rawBody,
|
|
1153
|
+
RawBody: rawBody,
|
|
1154
|
+
CommandBody: rawBody,
|
|
1155
|
+
From: isGroup ? `nextcloud-talk:room:${roomToken}` : `nextcloud-talk:${senderId}`,
|
|
1156
|
+
To: `nextcloud-talk:${roomToken}`,
|
|
1157
|
+
SessionKey: route.sessionKey,
|
|
1158
|
+
AccountId: route.accountId,
|
|
1159
|
+
ChatType: isGroup ? "group" : "direct",
|
|
1160
|
+
ConversationLabel: fromLabel,
|
|
1161
|
+
SenderName: senderName || void 0,
|
|
1162
|
+
SenderId: senderId,
|
|
1163
|
+
GroupSubject: isGroup ? roomName || roomToken : void 0,
|
|
1164
|
+
GroupSystemPrompt: isGroup ? groupSystemPrompt : void 0,
|
|
1165
|
+
Provider: CHANNEL_ID,
|
|
1166
|
+
Surface: CHANNEL_ID,
|
|
1167
|
+
WasMentioned: isGroup ? wasMentioned : void 0,
|
|
1168
|
+
MessageSid: message.messageId,
|
|
1169
|
+
Timestamp: message.timestamp,
|
|
1170
|
+
OriginatingChannel: CHANNEL_ID,
|
|
1171
|
+
OriginatingTo: `nextcloud-talk:${roomToken}`,
|
|
1172
|
+
CommandAuthorized: commandAuthorized
|
|
1160
1173
|
});
|
|
1161
|
-
await core.channel.inbound.
|
|
1174
|
+
await core.channel.inbound.dispatchReply({
|
|
1162
1175
|
cfg: config,
|
|
1163
1176
|
channel: CHANNEL_ID,
|
|
1164
1177
|
accountId: account.accountId,
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
},
|
|
1178
|
+
agentId: route.agentId,
|
|
1179
|
+
routeSessionKey: route.sessionKey,
|
|
1180
|
+
storePath,
|
|
1169
1181
|
ctxPayload,
|
|
1182
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1183
|
+
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
1170
1184
|
delivery: {
|
|
1171
1185
|
preparePayload: (payload) => payload.text === void 0 ? payload : {
|
|
1172
1186
|
...payload,
|
|
@@ -1187,90 +1201,49 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1187
1201
|
},
|
|
1188
1202
|
replyPipeline: {},
|
|
1189
1203
|
replyOptions: {
|
|
1190
|
-
...params.turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) : {},
|
|
1191
1204
|
skillFilter: roomConfig?.skills,
|
|
1192
|
-
disableBlockStreaming: typeof
|
|
1205
|
+
disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
|
|
1193
1206
|
},
|
|
1194
1207
|
record: { onRecordError: (err) => {
|
|
1195
1208
|
runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
|
|
1196
1209
|
} }
|
|
1197
1210
|
});
|
|
1198
1211
|
}
|
|
1199
|
-
var NextcloudTalkWebhookPayloadError = class extends Error {
|
|
1200
|
-
constructor(message, options) {
|
|
1201
|
-
super(message, options);
|
|
1202
|
-
this.name = "NextcloudTalkWebhookPayloadError";
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
function isRecord(value) {
|
|
1206
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1207
|
-
}
|
|
1208
|
-
function parseRawObject(rawEvent) {
|
|
1209
|
-
let parsed;
|
|
1210
|
-
try {
|
|
1211
|
-
parsed = JSON.parse(rawEvent);
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook contains invalid JSON.", { cause: error });
|
|
1214
|
-
}
|
|
1215
|
-
if (!isRecord(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
|
|
1216
|
-
return parsed;
|
|
1217
|
-
}
|
|
1218
|
-
function requiredString(value, field) {
|
|
1219
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1220
|
-
throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk webhook is missing ${field}.`);
|
|
1221
|
-
}
|
|
1222
|
-
function inspectNextcloudTalkWebhookEnvelope(rawEvent) {
|
|
1223
|
-
const envelope = parseRawObject(rawEvent);
|
|
1224
|
-
if (envelope.type !== "Create") return null;
|
|
1225
|
-
const object = isRecord(envelope.object) ? envelope.object : null;
|
|
1226
|
-
if (object?.type !== void 0 && object.type !== "Note") return null;
|
|
1227
|
-
if (!object) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook is missing object.");
|
|
1228
|
-
const target = isRecord(envelope.target) ? envelope.target : null;
|
|
1229
|
-
return {
|
|
1230
|
-
eventId: requiredString(object.id, "object.id"),
|
|
1231
|
-
laneKey: `room:${requiredString(target?.id, "target.id")}`
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
function parseLegacyReplayKey(key) {
|
|
1235
|
-
const separator = key.lastIndexOf(":");
|
|
1236
|
-
const roomId = key.slice(0, separator).trim();
|
|
1237
|
-
const messageId = key.slice(separator + 1).trim();
|
|
1238
|
-
return separator > 0 && roomId && messageId ? {
|
|
1239
|
-
messageId,
|
|
1240
|
-
roomId
|
|
1241
|
-
} : null;
|
|
1242
|
-
}
|
|
1243
|
-
/** Convert the shipped replay guard's live window into durable completion tombstones. */
|
|
1244
|
-
async function migrateNextcloudTalkLegacyReplayState(params) {
|
|
1245
|
-
const entries = await params.store.entries();
|
|
1246
|
-
let migrated = 0;
|
|
1247
|
-
for (const entry of entries) {
|
|
1248
|
-
const identity = parseLegacyReplayKey(entry.value.key);
|
|
1249
|
-
if (!identity || !Number.isFinite(entry.value.seenAt)) continue;
|
|
1250
|
-
const marker = {
|
|
1251
|
-
version: 1,
|
|
1252
|
-
receivedAt: entry.value.seenAt,
|
|
1253
|
-
rawEvent: ""
|
|
1254
|
-
};
|
|
1255
|
-
const result = await params.queue.enqueue(identity.messageId, marker, {
|
|
1256
|
-
receivedAt: entry.value.seenAt,
|
|
1257
|
-
laneKey: `room:${identity.roomId}`
|
|
1258
|
-
});
|
|
1259
|
-
if (result.kind === "accepted" || result.kind === "pending" && result.record.payload.rawEvent === "") {
|
|
1260
|
-
if (!await params.queue.complete(identity.messageId, { completedAt: entry.value.seenAt })) throw new Error(`Failed to migrate Nextcloud Talk replay key ${entry.value.key}.`);
|
|
1261
|
-
}
|
|
1262
|
-
migrated += 1;
|
|
1263
|
-
}
|
|
1264
|
-
await params.store.clear();
|
|
1265
|
-
return migrated;
|
|
1266
|
-
}
|
|
1267
1212
|
//#endregion
|
|
1268
1213
|
//#region extensions/nextcloud-talk/src/monitor.ts
|
|
1269
|
-
const DEFAULT_WEBHOOK_MAX_BODY_BYTES =
|
|
1270
|
-
const PREAUTH_WEBHOOK_MAX_BODY_BYTES =
|
|
1214
|
+
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1215
|
+
const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
|
|
1271
1216
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
1272
1217
|
const HEALTH_PATH = "/healthz";
|
|
1273
1218
|
const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
|
|
1219
|
+
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1220
|
+
type: z.enum([
|
|
1221
|
+
"Create",
|
|
1222
|
+
"Update",
|
|
1223
|
+
"Delete"
|
|
1224
|
+
]),
|
|
1225
|
+
actor: z.object({
|
|
1226
|
+
type: z.literal("Person"),
|
|
1227
|
+
id: z.string().min(1),
|
|
1228
|
+
name: z.string()
|
|
1229
|
+
}),
|
|
1230
|
+
object: z.object({
|
|
1231
|
+
type: z.literal("Note"),
|
|
1232
|
+
id: z.string().min(1),
|
|
1233
|
+
name: z.string(),
|
|
1234
|
+
content: z.string(),
|
|
1235
|
+
mediaType: z.string()
|
|
1236
|
+
}),
|
|
1237
|
+
target: z.object({
|
|
1238
|
+
type: z.literal("Collection"),
|
|
1239
|
+
id: z.string().min(1),
|
|
1240
|
+
name: z.string()
|
|
1241
|
+
})
|
|
1242
|
+
});
|
|
1243
|
+
const NextcloudTalkWebhookEnvelopeSchema = z.object({
|
|
1244
|
+
type: z.string().min(1),
|
|
1245
|
+
object: z.object({ type: z.string().min(1).optional() }).passthrough().optional()
|
|
1246
|
+
});
|
|
1274
1247
|
const WEBHOOK_ERRORS = {
|
|
1275
1248
|
missingSignatureHeaders: "Missing signature headers",
|
|
1276
1249
|
invalidBackend: "Invalid backend",
|
|
@@ -1279,10 +1252,48 @@ const WEBHOOK_ERRORS = {
|
|
|
1279
1252
|
payloadTooLarge: "Payload too large",
|
|
1280
1253
|
internalServerError: "Internal server error"
|
|
1281
1254
|
};
|
|
1255
|
+
var NextcloudTalkRetryableWebhookError = class extends Error {
|
|
1256
|
+
constructor(message, options) {
|
|
1257
|
+
super(message, options);
|
|
1258
|
+
this.name = "NextcloudTalkRetryableWebhookError";
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
async function processNextcloudTalkReplayGuardedMessage(params) {
|
|
1262
|
+
if (await params.replayGuard.claimMessage({
|
|
1263
|
+
accountId: params.accountId,
|
|
1264
|
+
roomToken: params.message.roomToken,
|
|
1265
|
+
messageId: params.message.messageId
|
|
1266
|
+
}) !== "claimed") return "duplicate";
|
|
1267
|
+
try {
|
|
1268
|
+
await params.handleMessage();
|
|
1269
|
+
await params.replayGuard.commitMessage({
|
|
1270
|
+
accountId: params.accountId,
|
|
1271
|
+
roomToken: params.message.roomToken,
|
|
1272
|
+
messageId: params.message.messageId
|
|
1273
|
+
});
|
|
1274
|
+
return "processed";
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (error instanceof NextcloudTalkRetryableWebhookError) params.replayGuard.releaseMessage({
|
|
1277
|
+
accountId: params.accountId,
|
|
1278
|
+
roomToken: params.message.roomToken,
|
|
1279
|
+
messageId: params.message.messageId,
|
|
1280
|
+
error
|
|
1281
|
+
});
|
|
1282
|
+
else await params.replayGuard.commitMessage({
|
|
1283
|
+
accountId: params.accountId,
|
|
1284
|
+
roomToken: params.message.roomToken,
|
|
1285
|
+
messageId: params.message.messageId
|
|
1286
|
+
});
|
|
1287
|
+
throw error;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1282
1290
|
function formatError(err) {
|
|
1283
1291
|
if (err instanceof Error) return err.message;
|
|
1284
1292
|
return typeof err === "string" ? err : JSON.stringify(err);
|
|
1285
1293
|
}
|
|
1294
|
+
function parseWebhookPayload(body) {
|
|
1295
|
+
return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
|
|
1296
|
+
}
|
|
1286
1297
|
function writeJsonResponse(res, status, body) {
|
|
1287
1298
|
if (body) {
|
|
1288
1299
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -1322,6 +1333,37 @@ function verifyWebhookSignature(params) {
|
|
|
1322
1333
|
params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1323
1334
|
return true;
|
|
1324
1335
|
}
|
|
1336
|
+
function decodeWebhookCreateMessage(params) {
|
|
1337
|
+
const envelope = safeParseJsonWithSchema(NextcloudTalkWebhookEnvelopeSchema, params.body);
|
|
1338
|
+
if (!envelope) {
|
|
1339
|
+
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1340
|
+
return { kind: "invalid" };
|
|
1341
|
+
}
|
|
1342
|
+
if (envelope.type !== "Create") return { kind: "ignore" };
|
|
1343
|
+
if (envelope.object?.type && envelope.object.type !== "Note") return { kind: "ignore" };
|
|
1344
|
+
const payload = parseWebhookPayload(params.body);
|
|
1345
|
+
if (!payload) {
|
|
1346
|
+
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1347
|
+
return { kind: "invalid" };
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
kind: "message",
|
|
1351
|
+
message: payloadToInboundMessage(payload)
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function payloadToInboundMessage(payload) {
|
|
1355
|
+
return {
|
|
1356
|
+
messageId: payload.object.id,
|
|
1357
|
+
roomToken: payload.target.id,
|
|
1358
|
+
roomName: payload.target.name,
|
|
1359
|
+
senderId: payload.actor.id,
|
|
1360
|
+
senderName: payload.actor.name ?? "",
|
|
1361
|
+
text: payload.object.content || payload.object.name || "",
|
|
1362
|
+
mediaType: payload.object.mediaType || "text/plain",
|
|
1363
|
+
timestamp: Date.now(),
|
|
1364
|
+
isGroupChat: true
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1325
1367
|
function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
1326
1368
|
return readRequestBodyWithLimit(req, {
|
|
1327
1369
|
maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
|
|
@@ -1329,10 +1371,12 @@ function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
|
1329
1371
|
});
|
|
1330
1372
|
}
|
|
1331
1373
|
function createNextcloudTalkWebhookServer(opts) {
|
|
1332
|
-
const { port, host, path, secret,
|
|
1374
|
+
const { port, host, path, secret, onMessage, onError, abortSignal } = opts;
|
|
1333
1375
|
const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
|
|
1334
1376
|
const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
|
|
1335
1377
|
const isBackendAllowed = opts.isBackendAllowed;
|
|
1378
|
+
const shouldProcessMessage = opts.shouldProcessMessage;
|
|
1379
|
+
const processMessage = opts.processMessage;
|
|
1336
1380
|
const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
|
|
1337
1381
|
const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
|
|
1338
1382
|
const webhookAuthRateLimiter = createAuthRateLimiter({
|
|
@@ -1376,8 +1420,37 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1376
1420
|
clientIp,
|
|
1377
1421
|
authRateLimiter: webhookAuthRateLimiter
|
|
1378
1422
|
})) return;
|
|
1379
|
-
|
|
1423
|
+
const decoded = decodeWebhookCreateMessage({
|
|
1424
|
+
body,
|
|
1425
|
+
res
|
|
1426
|
+
});
|
|
1427
|
+
if (decoded.kind === "invalid") return;
|
|
1428
|
+
if (decoded.kind === "ignore") {
|
|
1429
|
+
writeJsonResponse(res, 200);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const message = decoded.message;
|
|
1433
|
+
if (processMessage) {
|
|
1434
|
+
writeJsonResponse(res, 200);
|
|
1435
|
+
try {
|
|
1436
|
+
await processMessage(message);
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
onError?.(err instanceof Error ? err : new Error(formatError(err)));
|
|
1439
|
+
}
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
if (shouldProcessMessage) {
|
|
1443
|
+
if (!await shouldProcessMessage(message)) {
|
|
1444
|
+
writeJsonResponse(res, 200);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1380
1448
|
writeJsonResponse(res, 200);
|
|
1449
|
+
try {
|
|
1450
|
+
await onMessage(message);
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
onError?.(err instanceof Error ? err : new Error(formatError(err)));
|
|
1453
|
+
}
|
|
1381
1454
|
} catch (err) {
|
|
1382
1455
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1383
1456
|
writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
|
|
@@ -1387,48 +1460,29 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1387
1460
|
writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
|
|
1388
1461
|
return;
|
|
1389
1462
|
}
|
|
1390
|
-
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
1391
|
-
writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
1463
|
const error = err instanceof Error ? err : new Error(formatError(err));
|
|
1395
1464
|
onError?.(error);
|
|
1396
1465
|
writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
|
|
1397
1466
|
}
|
|
1398
1467
|
})();
|
|
1399
1468
|
});
|
|
1400
|
-
let stopRequested = false;
|
|
1401
|
-
let closePromise;
|
|
1402
|
-
const closeIfListening = () => {
|
|
1403
|
-
if (closePromise) return closePromise;
|
|
1404
|
-
if (!server.listening) return Promise.resolve();
|
|
1405
|
-
closePromise = new Promise((resolve) => {
|
|
1406
|
-
server.close(() => resolve());
|
|
1407
|
-
}).finally(() => {
|
|
1408
|
-
closePromise = void 0;
|
|
1409
|
-
});
|
|
1410
|
-
return closePromise;
|
|
1411
|
-
};
|
|
1412
|
-
const stop = async () => {
|
|
1413
|
-
stopRequested = true;
|
|
1414
|
-
await closeIfListening();
|
|
1415
|
-
};
|
|
1416
1469
|
const start = () => {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const onListenError = (error) => reject(error);
|
|
1420
|
-
server.once("error", onListenError);
|
|
1421
|
-
server.listen(port, host, () => {
|
|
1422
|
-
server.off("error", onListenError);
|
|
1423
|
-
(async () => {
|
|
1424
|
-
if (stopRequested) await closeIfListening();
|
|
1425
|
-
resolve();
|
|
1426
|
-
})().catch(reject);
|
|
1427
|
-
});
|
|
1470
|
+
return new Promise((resolve) => {
|
|
1471
|
+
server.listen(port, host, () => resolve());
|
|
1428
1472
|
});
|
|
1429
1473
|
};
|
|
1430
|
-
|
|
1431
|
-
|
|
1474
|
+
let stopped = false;
|
|
1475
|
+
const stop = () => {
|
|
1476
|
+
if (stopped) return;
|
|
1477
|
+
stopped = true;
|
|
1478
|
+
try {
|
|
1479
|
+
server.close();
|
|
1480
|
+
} catch {}
|
|
1481
|
+
};
|
|
1482
|
+
if (abortSignal) {
|
|
1483
|
+
if (abortSignal.aborted) stop();
|
|
1484
|
+
else abortSignal.addEventListener("abort", stop, { once: true });
|
|
1485
|
+
}
|
|
1432
1486
|
return {
|
|
1433
1487
|
server,
|
|
1434
1488
|
start,
|
|
@@ -1436,164 +1490,6 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1436
1490
|
};
|
|
1437
1491
|
}
|
|
1438
1492
|
//#endregion
|
|
1439
|
-
//#region extensions/nextcloud-talk/src/webhook-spool.ts
|
|
1440
|
-
const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
|
|
1441
|
-
const NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
|
|
1442
|
-
const NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
1443
|
-
const NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES = 1e4;
|
|
1444
|
-
const NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
1445
|
-
const NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES = 1e4;
|
|
1446
|
-
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1447
|
-
type: z.enum([
|
|
1448
|
-
"Create",
|
|
1449
|
-
"Update",
|
|
1450
|
-
"Delete"
|
|
1451
|
-
]),
|
|
1452
|
-
actor: z.object({
|
|
1453
|
-
type: z.literal("Person"),
|
|
1454
|
-
id: z.string().min(1),
|
|
1455
|
-
name: z.string()
|
|
1456
|
-
}),
|
|
1457
|
-
object: z.object({
|
|
1458
|
-
type: z.literal("Note"),
|
|
1459
|
-
id: z.string().min(1),
|
|
1460
|
-
name: z.string(),
|
|
1461
|
-
content: z.string(),
|
|
1462
|
-
mediaType: z.string()
|
|
1463
|
-
}),
|
|
1464
|
-
target: z.object({
|
|
1465
|
-
type: z.literal("Collection"),
|
|
1466
|
-
id: z.string().min(1),
|
|
1467
|
-
name: z.string()
|
|
1468
|
-
})
|
|
1469
|
-
});
|
|
1470
|
-
function parseClaimedMessage(payload, claimedId, claimedLaneKey) {
|
|
1471
|
-
if (payload.version !== 1) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has an unsupported version.`);
|
|
1472
|
-
const result = NextcloudTalkWebhookPayloadSchema.safeParse(parseRawObject(payload.rawEvent));
|
|
1473
|
-
if (!result.success || result.data.type !== "Create" || result.data.object.id !== claimedId) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has invalid message identity.`);
|
|
1474
|
-
const webhook = result.data;
|
|
1475
|
-
const roomId = requiredString(webhook.target.id, "target.id");
|
|
1476
|
-
if (claimedLaneKey !== `room:${roomId}`) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} changed room identity.`);
|
|
1477
|
-
return {
|
|
1478
|
-
messageId: webhook.object.id,
|
|
1479
|
-
roomToken: roomId,
|
|
1480
|
-
roomName: webhook.target.name,
|
|
1481
|
-
senderId: webhook.actor.id,
|
|
1482
|
-
senderName: webhook.actor.name,
|
|
1483
|
-
text: webhook.object.content || webhook.object.name,
|
|
1484
|
-
mediaType: webhook.object.mediaType || "text/plain",
|
|
1485
|
-
timestamp: payload.receivedAt,
|
|
1486
|
-
isGroupChat: true
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
function resolveNonRetryableFailure(error) {
|
|
1490
|
-
if (error instanceof NextcloudTalkWebhookPayloadError) return {
|
|
1491
|
-
reason: "invalid-event",
|
|
1492
|
-
message: error.message
|
|
1493
|
-
};
|
|
1494
|
-
const message = formatErrorMessage(error);
|
|
1495
|
-
if (message.includes("Nextcloud Talk: bot send was rejected") || message.includes("Nextcloud Talk: forbidden")) return {
|
|
1496
|
-
reason: "nextcloud-talk-auth",
|
|
1497
|
-
message
|
|
1498
|
-
};
|
|
1499
|
-
return null;
|
|
1500
|
-
}
|
|
1501
|
-
function createNextcloudTalkWebhookSpool(options) {
|
|
1502
|
-
let queue = options.queue;
|
|
1503
|
-
const getQueue = () => {
|
|
1504
|
-
queue ??= getNextcloudTalkRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1505
|
-
return queue;
|
|
1506
|
-
};
|
|
1507
|
-
const legacyReplayStore = options.legacyReplayStore === null ? null : options.legacyReplayStore ?? getNextcloudTalkRuntime().state.openKeyedStore({
|
|
1508
|
-
namespace: resolvePersistentDedupePluginStateNamespace({
|
|
1509
|
-
namespace: options.accountId,
|
|
1510
|
-
namespacePrefix: "replay-dedupe"
|
|
1511
|
-
}),
|
|
1512
|
-
maxEntries: 1e4,
|
|
1513
|
-
defaultTtlMs: 864e5
|
|
1514
|
-
});
|
|
1515
|
-
const legacyMigration = legacyReplayStore ? migrateNextcloudTalkLegacyReplayState({
|
|
1516
|
-
queue: getQueue(),
|
|
1517
|
-
store: legacyReplayStore
|
|
1518
|
-
}) : Promise.resolve();
|
|
1519
|
-
const monitor = createChannelIngressMonitor({
|
|
1520
|
-
queue: getQueue,
|
|
1521
|
-
inspect: (rawEvent) => inspectNextcloudTalkWebhookEnvelope(rawEvent),
|
|
1522
|
-
payload: {
|
|
1523
|
-
version: 1,
|
|
1524
|
-
serialize: (rawEvent, { receivedAt }) => ({
|
|
1525
|
-
receivedAt,
|
|
1526
|
-
rawEvent
|
|
1527
|
-
}),
|
|
1528
|
-
deserialize: (body) => body.rawEvent,
|
|
1529
|
-
encode: ({ body }) => ({
|
|
1530
|
-
version: 1,
|
|
1531
|
-
...body
|
|
1532
|
-
}),
|
|
1533
|
-
decode: (payload) => ({
|
|
1534
|
-
version: payload.version,
|
|
1535
|
-
body: {
|
|
1536
|
-
receivedAt: payload.receivedAt,
|
|
1537
|
-
rawEvent: payload.rawEvent
|
|
1538
|
-
}
|
|
1539
|
-
}),
|
|
1540
|
-
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.`)
|
|
1541
|
-
},
|
|
1542
|
-
deliver: async (_rawEvent, lifecycle, claim) => {
|
|
1543
|
-
const message = parseClaimedMessage(claim.payload, claim.id, claim.laneKey);
|
|
1544
|
-
await options.deliver(message, lifecycle);
|
|
1545
|
-
},
|
|
1546
|
-
pollIntervalMs: options.pollIntervalMs ?? NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS,
|
|
1547
|
-
waitForDeliveryIdleBeforeRepump: true,
|
|
1548
|
-
retention: {
|
|
1549
|
-
pruneIntervalMs: NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS,
|
|
1550
|
-
completedTtlMs: NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS,
|
|
1551
|
-
completedMaxEntries: NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES,
|
|
1552
|
-
failedTtlMs: NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS,
|
|
1553
|
-
failedMaxEntries: NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES
|
|
1554
|
-
},
|
|
1555
|
-
drain: {
|
|
1556
|
-
resolveNonRetryableFailure,
|
|
1557
|
-
...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
|
|
1558
|
-
onLog: (message) => options.runtime.log?.(`nextcloud-talk ${message}`)
|
|
1559
|
-
},
|
|
1560
|
-
...options.abortSignal ? { abortSignal: options.abortSignal } : {},
|
|
1561
|
-
createStoppedError: () => /* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"),
|
|
1562
|
-
onError: (error) => options.runtime.error?.(`nextcloud-talk ingress drain failed: ${formatErrorMessage(error)}`)
|
|
1563
|
-
});
|
|
1564
|
-
let stopping = false;
|
|
1565
|
-
const inFlightReceives = /* @__PURE__ */ new Set();
|
|
1566
|
-
const startAfterMigration = legacyMigration.then(() => {
|
|
1567
|
-
if (!stopping) monitor.start();
|
|
1568
|
-
});
|
|
1569
|
-
return {
|
|
1570
|
-
ready: async () => await startAfterMigration,
|
|
1571
|
-
receive: (rawEvent) => {
|
|
1572
|
-
if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
|
|
1573
|
-
const receiveTask = (async () => {
|
|
1574
|
-
await startAfterMigration;
|
|
1575
|
-
return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
|
|
1576
|
-
})();
|
|
1577
|
-
inFlightReceives.add(receiveTask);
|
|
1578
|
-
receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
|
|
1579
|
-
return receiveTask;
|
|
1580
|
-
},
|
|
1581
|
-
stop: async () => {
|
|
1582
|
-
stopping = true;
|
|
1583
|
-
const pendingReceives = [...inFlightReceives];
|
|
1584
|
-
const pauseTask = monitor.pause();
|
|
1585
|
-
await startAfterMigration.catch(() => void 0);
|
|
1586
|
-
await Promise.allSettled(pendingReceives);
|
|
1587
|
-
await monitor.stop();
|
|
1588
|
-
await pauseTask;
|
|
1589
|
-
},
|
|
1590
|
-
waitForIdle: async () => {
|
|
1591
|
-
await startAfterMigration.catch(() => void 0);
|
|
1592
|
-
await monitor.waitForIdle();
|
|
1593
|
-
}
|
|
1594
|
-
};
|
|
1595
|
-
}
|
|
1596
|
-
//#endregion
|
|
1597
1493
|
//#region extensions/nextcloud-talk/src/monitor-runtime.ts
|
|
1598
1494
|
const DEFAULT_WEBHOOK_PORT = 8788;
|
|
1599
1495
|
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
@@ -1622,29 +1518,13 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1622
1518
|
accountId: account.accountId
|
|
1623
1519
|
});
|
|
1624
1520
|
const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
deliver: async (message, lifecycle) => {
|
|
1630
|
-
core.channel.activity.record({
|
|
1631
|
-
channel: "nextcloud-talk",
|
|
1632
|
-
accountId: account.accountId,
|
|
1633
|
-
direction: "inbound",
|
|
1634
|
-
at: message.timestamp
|
|
1635
|
-
});
|
|
1636
|
-
if (opts.onMessage) await opts.onMessage(message, lifecycle);
|
|
1637
|
-
else await handleNextcloudTalkInbound({
|
|
1638
|
-
message,
|
|
1639
|
-
account,
|
|
1640
|
-
config: cfg,
|
|
1641
|
-
runtime,
|
|
1642
|
-
statusSink: opts.statusSink,
|
|
1643
|
-
turnAdoptionLifecycle: lifecycle
|
|
1644
|
-
});
|
|
1521
|
+
const replayGuard = createNextcloudTalkReplayGuard({
|
|
1522
|
+
stateDir: core.state.resolveStateDir(process.env, os.homedir),
|
|
1523
|
+
onDiskError: (error) => {
|
|
1524
|
+
logger.warn(`[nextcloud-talk:${account.accountId}] replay guard disk error: ${String(error)}`);
|
|
1645
1525
|
}
|
|
1646
1526
|
});
|
|
1647
|
-
const
|
|
1527
|
+
const { start, stop } = createNextcloudTalkWebhookServer({
|
|
1648
1528
|
port,
|
|
1649
1529
|
host,
|
|
1650
1530
|
path,
|
|
@@ -1653,34 +1533,39 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1653
1533
|
if (!expectedBackendOrigin) return true;
|
|
1654
1534
|
return normalizeOrigin(backend) === expectedBackendOrigin;
|
|
1655
1535
|
},
|
|
1656
|
-
|
|
1536
|
+
processMessage: async (message) => {
|
|
1537
|
+
if (await processNextcloudTalkReplayGuardedMessage({
|
|
1538
|
+
replayGuard,
|
|
1539
|
+
accountId: account.accountId,
|
|
1540
|
+
message,
|
|
1541
|
+
handleMessage: async () => {
|
|
1542
|
+
core.channel.activity.record({
|
|
1543
|
+
channel: "nextcloud-talk",
|
|
1544
|
+
accountId: account.accountId,
|
|
1545
|
+
direction: "inbound",
|
|
1546
|
+
at: message.timestamp
|
|
1547
|
+
});
|
|
1548
|
+
if (opts.onMessage) await opts.onMessage(message);
|
|
1549
|
+
else await handleNextcloudTalkInbound({
|
|
1550
|
+
message,
|
|
1551
|
+
account,
|
|
1552
|
+
config: cfg,
|
|
1553
|
+
runtime,
|
|
1554
|
+
statusSink: opts.statusSink
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}) === "duplicate") logger.warn(`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`);
|
|
1558
|
+
},
|
|
1559
|
+
onMessage: async () => {},
|
|
1657
1560
|
onError: (error) => {
|
|
1658
1561
|
logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
|
|
1659
1562
|
},
|
|
1660
1563
|
abortSignal: opts.abortSignal
|
|
1661
1564
|
});
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
stopPromise ??= (async () => {
|
|
1665
|
-
await server.stop();
|
|
1666
|
-
await spool.stop();
|
|
1667
|
-
})();
|
|
1668
|
-
return stopPromise;
|
|
1669
|
-
};
|
|
1670
|
-
if (opts.abortSignal && !opts.abortSignal.aborted) opts.abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1671
|
-
if (opts.abortSignal?.aborted) {
|
|
1672
|
-
await stop();
|
|
1673
|
-
return { stop };
|
|
1674
|
-
}
|
|
1675
|
-
try {
|
|
1676
|
-
await spool.ready();
|
|
1677
|
-
await server.start();
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
await stop();
|
|
1680
|
-
throw error;
|
|
1681
|
-
}
|
|
1565
|
+
if (opts.abortSignal?.aborted) return { stop };
|
|
1566
|
+
await start();
|
|
1682
1567
|
if (opts.abortSignal?.aborted) {
|
|
1683
|
-
|
|
1568
|
+
stop();
|
|
1684
1569
|
return { stop };
|
|
1685
1570
|
}
|
|
1686
1571
|
const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
|
@@ -1698,7 +1583,7 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1698
1583
|
accountId: ctx.accountId,
|
|
1699
1584
|
setStatus: ctx.setStatus
|
|
1700
1585
|
});
|
|
1701
|
-
await
|
|
1586
|
+
await runStoppablePassiveMonitor({
|
|
1702
1587
|
abortSignal: ctx.abortSignal,
|
|
1703
1588
|
start: async () => await monitorNextcloudTalkProvider({
|
|
1704
1589
|
accountId: account.accountId,
|
|
@@ -1706,10 +1591,7 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1706
1591
|
runtime: ctx.runtime,
|
|
1707
1592
|
abortSignal: ctx.abortSignal,
|
|
1708
1593
|
statusSink
|
|
1709
|
-
})
|
|
1710
|
-
stop: async (monitor) => {
|
|
1711
|
-
await monitor.stop();
|
|
1712
|
-
}
|
|
1594
|
+
})
|
|
1713
1595
|
});
|
|
1714
1596
|
},
|
|
1715
1597
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
@@ -1735,15 +1617,17 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1735
1617
|
else delete nextSection.accounts;
|
|
1736
1618
|
}
|
|
1737
1619
|
}
|
|
1738
|
-
if (changed)
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1620
|
+
if (changed) {
|
|
1621
|
+
if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
|
|
1622
|
+
...nextCfg.channels,
|
|
1623
|
+
"nextcloud-talk": nextSection
|
|
1624
|
+
};
|
|
1625
|
+
else {
|
|
1626
|
+
const nextChannels = { ...nextCfg.channels };
|
|
1627
|
+
delete nextChannels["nextcloud-talk"];
|
|
1628
|
+
if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
|
|
1629
|
+
else delete nextCfg.channels;
|
|
1630
|
+
}
|
|
1747
1631
|
}
|
|
1748
1632
|
const loggedOut = resolveNextcloudTalkAccount({
|
|
1749
1633
|
cfg: changed ? nextCfg : cfg,
|
|
@@ -1781,7 +1665,7 @@ const nextcloudTalkMessageActions = {
|
|
|
1781
1665
|
if (!hasConfiguredAccount(cfg, accountId)) return null;
|
|
1782
1666
|
return { actions: ["send", "react"] };
|
|
1783
1667
|
},
|
|
1784
|
-
supportsAction: ({ action }) => action
|
|
1668
|
+
supportsAction: ({ action }) => action !== "send",
|
|
1785
1669
|
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
|
1786
1670
|
if (action === "send") throw new Error("Send should be handled by outbound, not actions handler.");
|
|
1787
1671
|
if (action === "react") {
|
|
@@ -1863,8 +1747,8 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
|
|
|
1863
1747
|
//#region extensions/nextcloud-talk/src/setup-core.ts
|
|
1864
1748
|
const t$1 = createSetupTranslator$1();
|
|
1865
1749
|
const channel$1 = "nextcloud-talk";
|
|
1866
|
-
function
|
|
1867
|
-
return
|
|
1750
|
+
function addWildcardAllowFrom(allowFrom) {
|
|
1751
|
+
return mergeAllowFromEntries(allowFrom, ["*"]);
|
|
1868
1752
|
}
|
|
1869
1753
|
function normalizeNextcloudTalkBaseUrl(value) {
|
|
1870
1754
|
return value?.trim().replace(/\/+$/, "") ?? "";
|
|
@@ -1873,15 +1757,46 @@ function validateNextcloudTalkBaseUrl(value) {
|
|
|
1873
1757
|
if (!value) return "Required";
|
|
1874
1758
|
if (!value.startsWith("http://") && !value.startsWith("https://")) return "URL must start with http:// or https://";
|
|
1875
1759
|
}
|
|
1876
|
-
function setNextcloudTalkAccountConfig(cfg, accountId, updates
|
|
1760
|
+
function setNextcloudTalkAccountConfig(cfg, accountId, updates) {
|
|
1877
1761
|
return patchScopedAccountConfig({
|
|
1878
1762
|
cfg,
|
|
1879
1763
|
channelKey: channel$1,
|
|
1880
1764
|
accountId,
|
|
1881
|
-
patch: updates
|
|
1882
|
-
...clearFields ? { clearFields } : {}
|
|
1765
|
+
patch: updates
|
|
1883
1766
|
});
|
|
1884
1767
|
}
|
|
1768
|
+
function clearNextcloudTalkAccountFields(cfg, accountId, fields) {
|
|
1769
|
+
const section = cfg.channels?.["nextcloud-talk"];
|
|
1770
|
+
if (!section) return cfg;
|
|
1771
|
+
if (accountId === DEFAULT_ACCOUNT_ID$1) {
|
|
1772
|
+
const nextSection = { ...section };
|
|
1773
|
+
for (const field of fields) delete nextSection[field];
|
|
1774
|
+
return {
|
|
1775
|
+
...cfg,
|
|
1776
|
+
channels: {
|
|
1777
|
+
...cfg.channels,
|
|
1778
|
+
"nextcloud-talk": nextSection
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
const currentAccount = section.accounts?.[accountId];
|
|
1783
|
+
if (!currentAccount) return cfg;
|
|
1784
|
+
const nextAccount = { ...currentAccount };
|
|
1785
|
+
for (const field of fields) delete nextAccount[field];
|
|
1786
|
+
return {
|
|
1787
|
+
...cfg,
|
|
1788
|
+
channels: {
|
|
1789
|
+
...cfg.channels,
|
|
1790
|
+
"nextcloud-talk": {
|
|
1791
|
+
...section,
|
|
1792
|
+
accounts: {
|
|
1793
|
+
...section.accounts,
|
|
1794
|
+
[accountId]: nextAccount
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1885
1800
|
async function promptNextcloudTalkAllowFrom(params) {
|
|
1886
1801
|
return await promptParsedAllowFromForAccount({
|
|
1887
1802
|
cfg: params.cfg,
|
|
@@ -1920,28 +1835,37 @@ async function promptNextcloudTalkAllowFromForAccount(params) {
|
|
|
1920
1835
|
accountId
|
|
1921
1836
|
});
|
|
1922
1837
|
}
|
|
1923
|
-
const nextcloudTalkDmPolicy =
|
|
1838
|
+
const nextcloudTalkDmPolicy = {
|
|
1924
1839
|
label: "Nextcloud Talk",
|
|
1925
1840
|
channel: channel$1,
|
|
1926
|
-
|
|
1841
|
+
policyKey: "channels.nextcloud-talk.dmPolicy",
|
|
1842
|
+
allowFromKey: "channels.nextcloud-talk.allowFrom",
|
|
1843
|
+
resolveConfigKeys: (cfg, accountId) => (accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)) !== DEFAULT_ACCOUNT_ID$1 ? {
|
|
1844
|
+
policyKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.dmPolicy`,
|
|
1845
|
+
allowFromKey: `channels.nextcloud-talk.accounts.${accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)}.allowFrom`
|
|
1846
|
+
} : {
|
|
1847
|
+
policyKey: "channels.nextcloud-talk.dmPolicy",
|
|
1848
|
+
allowFromKey: "channels.nextcloud-talk.allowFrom"
|
|
1849
|
+
},
|
|
1850
|
+
getCurrent: (cfg, accountId) => resolveNextcloudTalkAccount({
|
|
1927
1851
|
cfg,
|
|
1928
1852
|
accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)
|
|
1929
|
-
}),
|
|
1930
|
-
|
|
1853
|
+
}).config.dmPolicy ?? "pairing",
|
|
1854
|
+
setPolicy: (cfg, policy, accountId) => {
|
|
1855
|
+
const resolvedAccountId = accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
|
|
1856
|
+
const resolved = resolveNextcloudTalkAccount({
|
|
1857
|
+
cfg,
|
|
1858
|
+
accountId: resolvedAccountId
|
|
1859
|
+
});
|
|
1860
|
+
return setNextcloudTalkAccountConfig(cfg, resolvedAccountId, {
|
|
1861
|
+
dmPolicy: policy,
|
|
1862
|
+
...policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}
|
|
1863
|
+
});
|
|
1864
|
+
},
|
|
1931
1865
|
promptAllowFrom: promptNextcloudTalkAllowFromForAccount
|
|
1932
|
-
}
|
|
1866
|
+
};
|
|
1933
1867
|
const nextcloudTalkSetupAdapter = {
|
|
1934
|
-
singleAccountKeysToMove: ["rooms"],
|
|
1935
1868
|
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
1936
|
-
prepareAccountConfigInput: ({ input }) => {
|
|
1937
|
-
const setupInput = input;
|
|
1938
|
-
return {
|
|
1939
|
-
...setupInput,
|
|
1940
|
-
baseUrl: setupInput.baseUrl ?? readOptionalString(setupInput.url),
|
|
1941
|
-
secret: setupInput.secret ?? readOptionalString(setupInput.token) ?? readOptionalString(setupInput.password),
|
|
1942
|
-
secretFile: setupInput.secretFile ?? readOptionalString(setupInput.tokenFile)
|
|
1943
|
-
};
|
|
1944
|
-
},
|
|
1945
1869
|
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
1946
1870
|
cfg,
|
|
1947
1871
|
channelKey: channel$1,
|
|
@@ -1959,83 +1883,18 @@ const nextcloudTalkSetupAdapter = {
|
|
|
1959
1883
|
}),
|
|
1960
1884
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
1961
1885
|
const setupInput = input;
|
|
1962
|
-
|
|
1886
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
1963
1887
|
cfg,
|
|
1964
1888
|
channelKey: channel$1,
|
|
1965
1889
|
accountId,
|
|
1966
1890
|
name: setupInput.name
|
|
1967
|
-
})
|
|
1891
|
+
});
|
|
1892
|
+
return setNextcloudTalkAccountConfig(setupInput.useEnv ? clearNextcloudTalkAccountFields(namedConfig, accountId, ["botSecret", "botSecretFile"]) : namedConfig, accountId, {
|
|
1968
1893
|
baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
|
|
1969
1894
|
...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
|
|
1970
|
-
}
|
|
1895
|
+
});
|
|
1971
1896
|
}
|
|
1972
1897
|
};
|
|
1973
|
-
const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
1974
|
-
fields: {
|
|
1975
|
-
baseUrl: {
|
|
1976
|
-
kind: "string",
|
|
1977
|
-
cli: {
|
|
1978
|
-
flags: "--base-url <url>",
|
|
1979
|
-
description: "Nextcloud base URL"
|
|
1980
|
-
}
|
|
1981
|
-
},
|
|
1982
|
-
url: {
|
|
1983
|
-
kind: "string",
|
|
1984
|
-
cli: {
|
|
1985
|
-
flags: "--url <url>",
|
|
1986
|
-
description: "Legacy Nextcloud base URL alias"
|
|
1987
|
-
}
|
|
1988
|
-
},
|
|
1989
|
-
secret: {
|
|
1990
|
-
kind: "string",
|
|
1991
|
-
sensitive: true,
|
|
1992
|
-
cli: {
|
|
1993
|
-
flags: "--secret <secret>",
|
|
1994
|
-
description: "Nextcloud Talk bot secret"
|
|
1995
|
-
}
|
|
1996
|
-
},
|
|
1997
|
-
token: {
|
|
1998
|
-
kind: "string",
|
|
1999
|
-
sensitive: true,
|
|
2000
|
-
cli: {
|
|
2001
|
-
flags: "--token <secret>",
|
|
2002
|
-
description: "Legacy Nextcloud bot secret alias"
|
|
2003
|
-
}
|
|
2004
|
-
},
|
|
2005
|
-
password: {
|
|
2006
|
-
kind: "string",
|
|
2007
|
-
sensitive: true,
|
|
2008
|
-
cli: {
|
|
2009
|
-
flags: "--password <secret>",
|
|
2010
|
-
description: "Legacy Nextcloud bot secret alias"
|
|
2011
|
-
}
|
|
2012
|
-
},
|
|
2013
|
-
secretFile: {
|
|
2014
|
-
kind: "string",
|
|
2015
|
-
sensitive: true,
|
|
2016
|
-
cli: {
|
|
2017
|
-
flags: "--secret-file <path>",
|
|
2018
|
-
description: "Nextcloud Talk bot secret file"
|
|
2019
|
-
}
|
|
2020
|
-
},
|
|
2021
|
-
tokenFile: {
|
|
2022
|
-
kind: "string",
|
|
2023
|
-
sensitive: true,
|
|
2024
|
-
cli: {
|
|
2025
|
-
flags: "--token-file <path>",
|
|
2026
|
-
description: "Legacy Nextcloud bot secret file alias"
|
|
2027
|
-
}
|
|
2028
|
-
},
|
|
2029
|
-
useEnv: {
|
|
2030
|
-
kind: "boolean",
|
|
2031
|
-
cli: {
|
|
2032
|
-
flags: "--use-env",
|
|
2033
|
-
description: "Use Nextcloud Talk environment credentials"
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
},
|
|
2037
|
-
legacyAdapter: nextcloudTalkSetupAdapter
|
|
2038
|
-
});
|
|
2039
1898
|
//#endregion
|
|
2040
1899
|
//#region extensions/nextcloud-talk/src/setup-surface.ts
|
|
2041
1900
|
const t = createSetupTranslator();
|
|
@@ -2093,10 +1952,8 @@ const nextcloudTalkSetupWizard = {
|
|
|
2093
1952
|
[CONFIGURE_API_FLAG]: "1"
|
|
2094
1953
|
} };
|
|
2095
1954
|
},
|
|
2096
|
-
credentials: [
|
|
1955
|
+
credentials: [{
|
|
2097
1956
|
inputKey: "token",
|
|
2098
|
-
configKey: "botSecret",
|
|
2099
|
-
configuredFields: ["botSecret", "botSecretFile"],
|
|
2100
1957
|
providerHint: channel,
|
|
2101
1958
|
credentialLabel: t("wizard.nextcloudTalk.botSecret"),
|
|
2102
1959
|
preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
|
|
@@ -2104,54 +1961,61 @@ const nextcloudTalkSetupWizard = {
|
|
|
2104
1961
|
keepPrompt: t("wizard.nextcloudTalk.botSecretKeep"),
|
|
2105
1962
|
inputPrompt: t("wizard.nextcloudTalk.botSecretInput"),
|
|
2106
1963
|
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1,
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
1964
|
+
inspect: ({ cfg, accountId }) => {
|
|
1965
|
+
const resolvedAccount = resolveNextcloudTalkAccount({
|
|
1966
|
+
cfg,
|
|
1967
|
+
accountId
|
|
1968
|
+
});
|
|
1969
|
+
return {
|
|
1970
|
+
accountConfigured: Boolean(resolvedAccount.secret && resolvedAccount.baseUrl),
|
|
1971
|
+
hasConfiguredValue: Boolean(hasConfiguredSecretInput(resolvedAccount.config.botSecret) || resolvedAccount.config.botSecretFile),
|
|
1972
|
+
resolvedValue: resolvedAccount.secret || void 0,
|
|
1973
|
+
envValue: accountId === DEFAULT_ACCOUNT_ID$1 ? normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET) : void 0
|
|
1974
|
+
};
|
|
1975
|
+
},
|
|
1976
|
+
applyUseEnv: async (params) => {
|
|
1977
|
+
const resolvedAccount = resolveNextcloudTalkAccount({
|
|
1978
|
+
cfg: params.cfg,
|
|
1979
|
+
accountId: params.accountId
|
|
1980
|
+
});
|
|
1981
|
+
return setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["botSecret", "botSecretFile"]), params.accountId, { baseUrl: resolvedAccount.baseUrl });
|
|
2119
1982
|
},
|
|
2120
|
-
|
|
2121
|
-
}
|
|
1983
|
+
applySet: async (params) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["botSecret", "botSecretFile"]), params.accountId, { botSecret: params.value })
|
|
1984
|
+
}, {
|
|
2122
1985
|
inputKey: "password",
|
|
2123
|
-
configKey: "apiPassword",
|
|
2124
|
-
configuredFields: ["apiPassword", "apiPasswordFile"],
|
|
2125
1986
|
providerHint: "nextcloud-talk-api",
|
|
2126
1987
|
credentialLabel: t("wizard.nextcloudTalk.apiPassword"),
|
|
2127
1988
|
preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
|
|
2128
1989
|
envPrompt: "",
|
|
2129
1990
|
keepPrompt: t("wizard.nextcloudTalk.apiPasswordKeep"),
|
|
2130
1991
|
inputPrompt: t("wizard.nextcloudTalk.apiPasswordInput"),
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
1992
|
+
inspect: ({ cfg, accountId }) => {
|
|
1993
|
+
const resolvedAccount = resolveNextcloudTalkAccount({
|
|
1994
|
+
cfg,
|
|
1995
|
+
accountId
|
|
1996
|
+
});
|
|
1997
|
+
const apiUser = resolvedAccount.config.apiUser?.trim();
|
|
1998
|
+
const apiPasswordConfigured = Boolean(hasConfiguredSecretInput(resolvedAccount.config.apiPassword) || resolvedAccount.config.apiPasswordFile);
|
|
1999
|
+
return {
|
|
2000
|
+
accountConfigured: Boolean(apiUser && apiPasswordConfigured),
|
|
2001
|
+
hasConfiguredValue: apiPasswordConfigured
|
|
2002
|
+
};
|
|
2003
|
+
},
|
|
2137
2004
|
shouldPrompt: ({ credentialValues }) => credentialValues[CONFIGURE_API_FLAG] === "1",
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
textInputs: [baseUrlTextInput({
|
|
2005
|
+
applySet: async (params) => setNextcloudTalkAccountConfig(clearNextcloudTalkAccountFields(params.cfg, params.accountId, ["apiPassword", "apiPasswordFile"]), params.accountId, { apiPassword: params.value })
|
|
2006
|
+
}],
|
|
2007
|
+
textInputs: [{
|
|
2142
2008
|
inputKey: "httpUrl",
|
|
2143
|
-
configKey: "baseUrl",
|
|
2144
2009
|
message: t("wizard.nextcloudTalk.instanceUrlPrompt"),
|
|
2145
|
-
|
|
2010
|
+
currentValue: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
|
|
2146
2011
|
cfg,
|
|
2147
2012
|
accountId
|
|
2148
|
-
}),
|
|
2149
|
-
currentValue: (account) => account.baseUrl || void 0,
|
|
2013
|
+
}).baseUrl || void 0,
|
|
2150
2014
|
shouldPrompt: ({ currentValue }) => !currentValue,
|
|
2151
|
-
validate: validateNextcloudTalkBaseUrl,
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
}
|
|
2015
|
+
validate: ({ value }) => validateNextcloudTalkBaseUrl(value),
|
|
2016
|
+
normalizeValue: ({ value }) => normalizeNextcloudTalkBaseUrl(value),
|
|
2017
|
+
applySet: async (params) => setNextcloudTalkAccountConfig(params.cfg, params.accountId, { baseUrl: params.value })
|
|
2018
|
+
}, {
|
|
2155
2019
|
inputKey: "userId",
|
|
2156
2020
|
message: t("wizard.nextcloudTalk.apiUserPrompt"),
|
|
2157
2021
|
currentValue: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
|
|
@@ -2213,14 +2077,12 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2213
2077
|
configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
|
|
2214
2078
|
config: {
|
|
2215
2079
|
...nextcloudTalkConfigAdapter,
|
|
2216
|
-
isConfigured: (account) => Boolean(account.
|
|
2080
|
+
isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
|
|
2217
2081
|
describeAccount: (account) => describeWebhookAccountSnapshot({
|
|
2218
2082
|
account,
|
|
2219
|
-
configured: Boolean(account.
|
|
2083
|
+
configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
|
|
2220
2084
|
extra: {
|
|
2221
2085
|
secretSource: account.secretSource,
|
|
2222
|
-
tokenStatus: account.tokenStatus,
|
|
2223
|
-
apiCredentialStatus: account.apiCredentialStatus,
|
|
2224
2086
|
baseUrl: account.baseUrl ? "[set]" : "[missing]"
|
|
2225
2087
|
}
|
|
2226
2088
|
})
|
|
@@ -2228,7 +2090,18 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2228
2090
|
approvalCapability: nextcloudTalkApprovalAuth,
|
|
2229
2091
|
doctor: nextcloudTalkDoctor,
|
|
2230
2092
|
groups: {
|
|
2231
|
-
resolveRequireMention:
|
|
2093
|
+
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
2094
|
+
const rooms = resolveNextcloudTalkAccount({
|
|
2095
|
+
cfg,
|
|
2096
|
+
accountId
|
|
2097
|
+
}).config.rooms;
|
|
2098
|
+
if (!rooms || !groupId) return true;
|
|
2099
|
+
const roomConfig = rooms[groupId];
|
|
2100
|
+
if (roomConfig?.requireMention !== void 0) return roomConfig.requireMention;
|
|
2101
|
+
const wildcardConfig = rooms["*"];
|
|
2102
|
+
if (wildcardConfig?.requireMention !== void 0) return wildcardConfig.requireMention;
|
|
2103
|
+
return true;
|
|
2104
|
+
},
|
|
2232
2105
|
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy
|
|
2233
2106
|
},
|
|
2234
2107
|
messaging: {
|
|
@@ -2249,7 +2122,6 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2249
2122
|
collectRuntimeConfigAssignments
|
|
2250
2123
|
},
|
|
2251
2124
|
setup: nextcloudTalkSetupAdapter,
|
|
2252
|
-
setupContract: nextcloudTalkSetupContract,
|
|
2253
2125
|
status: createComputedAccountStatusAdapter({
|
|
2254
2126
|
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
|
|
2255
2127
|
buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
|
|
@@ -2272,11 +2144,9 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2272
2144
|
accountId: account.accountId,
|
|
2273
2145
|
name: account.name,
|
|
2274
2146
|
enabled: account.enabled,
|
|
2275
|
-
configured: Boolean(account.
|
|
2147
|
+
configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
|
|
2276
2148
|
extra: {
|
|
2277
2149
|
secretSource: account.secretSource,
|
|
2278
|
-
tokenStatus: account.tokenStatus,
|
|
2279
|
-
apiCredentialStatus: account.apiCredentialStatus,
|
|
2280
2150
|
baseUrl: account.baseUrl ? "[set]" : "[missing]",
|
|
2281
2151
|
mode: "webhook"
|
|
2282
2152
|
}
|