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