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