@openclaw/nextcloud-talk 2026.9.6 → 2026.9.8
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/assets/icon.png +0 -0
- package/dist/.setup/api-hlcmleZc.mjs +3 -0
- package/dist/.setup/{channel-B_MT8EAm.mjs → channel-CeJpOTiY.mjs} +484 -352
- package/dist/.setup/doctor-contract-BVJcDIeU.mjs +213 -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/setup-entry.js +1 -1
- package/openclaw.plugin.json +48 -14
- package/package.json +4 -4
- package/dist/.setup/api-CLvR4rnO.mjs +0 -3
- package/dist/.setup/doctor-contract-Bda_xc6V.mjs +0 -21
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { i as __reExport, n as normalizeCompatibilityConfig, r as __exportAll, t as legacyConfigRules } from "./doctor-contract-BVJcDIeU.mjs";
|
|
1
2
|
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-Bda_xc6V.mjs";
|
|
3
3
|
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-Bvm0PmP3.mjs";
|
|
4
4
|
import { createAccountListHelpers, describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
5
|
-
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
5
|
+
import { buildChannelOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
6
6
|
import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
|
7
7
|
import { createAllowlistProviderRouteAllowlistWarningCollector, createConditionalWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
|
|
8
8
|
import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
@@ -19,6 +19,7 @@ import { readProviderJsonResponse, readProviderTextResponse } from "openclaw/plu
|
|
|
19
19
|
import { resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
20
20
|
import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
|
|
21
21
|
import { SsrFBlockedError, ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
22
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
22
23
|
import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
|
|
23
24
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
24
25
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
@@ -28,7 +29,7 @@ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/
|
|
|
28
29
|
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
29
30
|
import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
|
|
30
31
|
import { adaptScopedAccountAccessor, clearAccountFieldsFromConfigSection, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
31
|
-
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
32
|
+
import { createDeferred, requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
32
33
|
import { z } from "zod";
|
|
33
34
|
import os from "node:os";
|
|
34
35
|
import path from "node:path";
|
|
@@ -40,16 +41,16 @@ import { channelIngressRoutes } from "openclaw/plugin-sdk/channel-ingress-runtim
|
|
|
40
41
|
import { safeParseJson, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
41
42
|
import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback } from "openclaw/plugin-sdk/channel-targets";
|
|
42
43
|
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
44
|
+
import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, createWebhookInFlightLimiter, getWebhookLegacyListener, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
|
|
45
|
+
import { sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
46
|
+
import { canonicalizeWebhookRouteKey, registerPluginHttpRoute, registerWebhookTarget, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-targets";
|
|
46
47
|
import { isRecord as isRecord$1 } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
47
48
|
import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
|
|
48
|
-
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
|
|
49
49
|
import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
|
|
50
50
|
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
51
|
+
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
|
|
52
|
+
import { applyAccountNameToChannelSection, baseUrlTextInput, createPromptParsedAllowFromForAccount, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
53
|
+
import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries } from "openclaw/plugin-sdk/setup-runtime";
|
|
53
54
|
import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
|
|
54
55
|
//#region extensions/nextcloud-talk/src/api-credentials.ts
|
|
55
56
|
function resolveNextcloudTalkApiCredentialsResult(params) {
|
|
@@ -243,12 +244,9 @@ function verifyNextcloudTalkSignature(params) {
|
|
|
243
244
|
const expected = createHmac("sha256", secret).update(random + body).digest("hex");
|
|
244
245
|
return safeEqualSecret(signature, expected);
|
|
245
246
|
}
|
|
246
|
-
/**
|
|
247
|
-
* Extract webhook headers from an incoming request.
|
|
248
|
-
*/
|
|
249
247
|
function extractNextcloudTalkHeaders(headers) {
|
|
250
248
|
const getHeader = (name) => {
|
|
251
|
-
const value = headers[name]
|
|
249
|
+
const value = headers[name];
|
|
252
250
|
return Array.isArray(value) ? value[0] : value;
|
|
253
251
|
};
|
|
254
252
|
const signature = getHeader(SIGNATURE_HEADER);
|
|
@@ -285,10 +283,6 @@ function normalizeUrlForMatch(value) {
|
|
|
285
283
|
return value.trim().replace(/\/$/, "");
|
|
286
284
|
}
|
|
287
285
|
}
|
|
288
|
-
function coerceFeatureMask(value) {
|
|
289
|
-
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
|
|
290
|
-
return parseStrictNonNegativeInteger(value);
|
|
291
|
-
}
|
|
292
286
|
function formatMissingResponseFeatureMessage(bot, features) {
|
|
293
287
|
const id = bot.id == null ? "unknown" : String(bot.id);
|
|
294
288
|
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.`;
|
|
@@ -354,7 +348,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
|
|
|
354
348
|
code: "bot_not_found",
|
|
355
349
|
message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`
|
|
356
350
|
};
|
|
357
|
-
const features =
|
|
351
|
+
const features = parseStrictNonNegativeInteger(bot.features);
|
|
358
352
|
if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) return {
|
|
359
353
|
ok: false,
|
|
360
354
|
code: "missing_response_feature",
|
|
@@ -442,8 +436,10 @@ const NextcloudTalkAccountSchemaBase = z.object({
|
|
|
442
436
|
apiPassword: buildSecretInputSchema().optional(),
|
|
443
437
|
apiPasswordFile: z.string().optional(),
|
|
444
438
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
445
|
-
|
|
446
|
-
|
|
439
|
+
legacyWebhook: z.union([z.literal(false), z.object({
|
|
440
|
+
port: z.number().int().min(1).max(65535),
|
|
441
|
+
host: z.string().optional()
|
|
442
|
+
}).strict()]).optional(),
|
|
447
443
|
webhookPath: z.string().optional(),
|
|
448
444
|
webhookPublicUrl: z.string().optional(),
|
|
449
445
|
allowFrom: z.array(z.string()).optional(),
|
|
@@ -467,18 +463,199 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(NextcloudTalkAc
|
|
|
467
463
|
}
|
|
468
464
|
});
|
|
469
465
|
//#endregion
|
|
466
|
+
//#region src/gateway/gateway-http-route-contracts.ts
|
|
467
|
+
const GATEWAY_PROBE_ROUTES = /* @__PURE__ */ new Map([
|
|
468
|
+
["/health", "live"],
|
|
469
|
+
["/healthz", "live"],
|
|
470
|
+
["/ready", "ready"],
|
|
471
|
+
["/readyz", "ready"],
|
|
472
|
+
["/startup", "startup"],
|
|
473
|
+
["/startupz", "startup"]
|
|
474
|
+
]);
|
|
475
|
+
function classifyGatewayProbePath(pathname) {
|
|
476
|
+
for (const [root, status] of GATEWAY_PROBE_ROUTES) {
|
|
477
|
+
if (pathname === root) return status;
|
|
478
|
+
if (pathname.startsWith(`${root}/`)) return "namespace";
|
|
479
|
+
}
|
|
480
|
+
return "outside";
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region packages/normalization-core/src/string-coerce.ts
|
|
484
|
+
/** Trims string input and returns null for non-strings or empty strings. */
|
|
485
|
+
function normalizeNullableString(value) {
|
|
486
|
+
if (typeof value !== "string") return null;
|
|
487
|
+
const trimmed = value.trim();
|
|
488
|
+
return trimmed ? trimmed : null;
|
|
489
|
+
}
|
|
490
|
+
/** Trims string input and returns undefined for non-strings or empty strings. */
|
|
491
|
+
function normalizeOptionalString$1(value) {
|
|
492
|
+
return normalizeNullableString(value) ?? void 0;
|
|
493
|
+
}
|
|
494
|
+
/** Lowercases a normalized optional string. */
|
|
495
|
+
function normalizeOptionalLowercaseString$1(value) {
|
|
496
|
+
return normalizeOptionalString$1(value)?.toLowerCase();
|
|
497
|
+
}
|
|
498
|
+
/** Lowercases a normalized string or returns an empty string when absent. */
|
|
499
|
+
function normalizeLowercaseStringOrEmpty$1(value) {
|
|
500
|
+
return normalizeOptionalLowercaseString$1(value) ?? "";
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/plugins/http-path-prefix.ts
|
|
504
|
+
function prefixMatchPath(pathname, prefix) {
|
|
505
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`) || pathname.startsWith(`${prefix}%`);
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/gateway/security-path.ts
|
|
509
|
+
const MAX_PATH_DECODE_PASSES = 32;
|
|
510
|
+
function normalizePathSeparators(pathname) {
|
|
511
|
+
const collapsed = pathname.replace(/\/{2,}/g, "/");
|
|
512
|
+
if (collapsed.length <= 1) return collapsed;
|
|
513
|
+
return collapsed.replace(/\/+$/, "");
|
|
514
|
+
}
|
|
515
|
+
function resolveDotSegments(pathname) {
|
|
516
|
+
try {
|
|
517
|
+
return new URL(pathname, "http://localhost").pathname;
|
|
518
|
+
} catch {
|
|
519
|
+
return pathname;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function normalizePathForSecurity(pathname) {
|
|
523
|
+
return normalizePathSeparators(normalizeLowercaseStringOrEmpty$1(resolveDotSegments(pathname))) || "/";
|
|
524
|
+
}
|
|
525
|
+
function buildCanonicalPathCandidates(pathname, maxDecodePasses = MAX_PATH_DECODE_PASSES) {
|
|
526
|
+
const candidates = /* @__PURE__ */ new Set([normalizePathForSecurity(pathname)]);
|
|
527
|
+
let decoded = pathname;
|
|
528
|
+
let malformedEncoding = false;
|
|
529
|
+
let decodePasses = 0;
|
|
530
|
+
for (let pass = 0; pass < maxDecodePasses; pass++) {
|
|
531
|
+
let nextDecoded;
|
|
532
|
+
try {
|
|
533
|
+
nextDecoded = decodeURIComponent(decoded);
|
|
534
|
+
} catch {
|
|
535
|
+
malformedEncoding = true;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
if (nextDecoded === decoded) break;
|
|
539
|
+
decodePasses += 1;
|
|
540
|
+
decoded = nextDecoded;
|
|
541
|
+
candidates.add(normalizePathForSecurity(decoded));
|
|
542
|
+
}
|
|
543
|
+
let decodePassLimitReached = false;
|
|
544
|
+
if (!malformedEncoding) try {
|
|
545
|
+
decodePassLimitReached = decodeURIComponent(decoded) !== decoded;
|
|
546
|
+
} catch {
|
|
547
|
+
malformedEncoding = true;
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
candidates: [...candidates],
|
|
551
|
+
decodePasses,
|
|
552
|
+
decodePassLimitReached,
|
|
553
|
+
malformedEncoding
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function canonicalizePathForSecurity(pathname) {
|
|
557
|
+
const { candidates, decodePasses, decodePassLimitReached, malformedEncoding } = buildCanonicalPathCandidates(pathname);
|
|
558
|
+
return {
|
|
559
|
+
canonicalPath: candidates[candidates.length - 1] ?? "/",
|
|
560
|
+
candidates,
|
|
561
|
+
decodePasses,
|
|
562
|
+
decodePassLimitReached,
|
|
563
|
+
malformedEncoding,
|
|
564
|
+
rawNormalizedPath: normalizePathSeparators(normalizeLowercaseStringOrEmpty$1(pathname)) || "/"
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const PROTECTED_PLUGIN_ROUTE_PREFIXES = ["/api/channels"];
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/gateway/server/plugins-http/path-context.ts
|
|
570
|
+
function normalizeProtectedPrefix(prefix) {
|
|
571
|
+
const collapsed = normalizeLowercaseStringOrEmpty$1(prefix).replace(/\/{2,}/g, "/");
|
|
572
|
+
if (collapsed.length <= 1) return collapsed || "/";
|
|
573
|
+
return collapsed.replace(/\/+$/, "");
|
|
574
|
+
}
|
|
575
|
+
const NORMALIZED_PROTECTED_PLUGIN_ROUTE_PREFIXES = PROTECTED_PLUGIN_ROUTE_PREFIXES.map(normalizeProtectedPrefix);
|
|
576
|
+
/** Returns true when any decoded path candidate targets a protected route. */
|
|
577
|
+
function isProtectedPluginRoutePathFromContext(context) {
|
|
578
|
+
if (context.candidates.some((candidate) => NORMALIZED_PROTECTED_PLUGIN_ROUTE_PREFIXES.some((prefix) => prefixMatchPath(candidate, prefix)))) return true;
|
|
579
|
+
if (context.decodePassLimitReached) return true;
|
|
580
|
+
if (!context.malformedEncoding) return false;
|
|
581
|
+
return NORMALIZED_PROTECTED_PLUGIN_ROUTE_PREFIXES.some((prefix) => prefixMatchPath(context.rawNormalizedPath, prefix));
|
|
582
|
+
}
|
|
583
|
+
/** Builds all security-relevant decoded path candidates for a request path. */
|
|
584
|
+
function resolvePluginRoutePathContext(pathname) {
|
|
585
|
+
const canonical = canonicalizePathForSecurity(pathname);
|
|
586
|
+
return {
|
|
587
|
+
pathname,
|
|
588
|
+
canonicalPath: canonical.canonicalPath,
|
|
589
|
+
candidates: canonical.candidates,
|
|
590
|
+
malformedEncoding: canonical.malformedEncoding,
|
|
591
|
+
decodePassLimitReached: canonical.decodePassLimitReached,
|
|
592
|
+
rawNormalizedPath: canonical.rawNormalizedPath
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region \0openclaw:bundled-sdk:openclaw/plugin-sdk/gateway-config-runtime
|
|
597
|
+
var gateway_config_runtime_exports = /* @__PURE__ */ __exportAll({
|
|
598
|
+
classifyGatewayProbePath: () => classifyGatewayProbePath,
|
|
599
|
+
isProtectedPluginRoutePathFromContext: () => isProtectedPluginRoutePathFromContext,
|
|
600
|
+
resolvePluginRoutePathContext: () => resolvePluginRoutePathContext
|
|
601
|
+
});
|
|
602
|
+
import * as import_openclaw_plugin_sdk_gateway_config_runtime from "openclaw/plugin-sdk/gateway-config-runtime";
|
|
603
|
+
__reExport(gateway_config_runtime_exports, import_openclaw_plugin_sdk_gateway_config_runtime);
|
|
604
|
+
//#endregion
|
|
470
605
|
//#region extensions/nextcloud-talk/src/replay-migration-contract.ts
|
|
471
606
|
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
|
|
472
607
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
|
|
473
608
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 864e5;
|
|
474
609
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
475
610
|
//#endregion
|
|
611
|
+
//#region extensions/nextcloud-talk/src/webhook-route.ts
|
|
612
|
+
const DEFAULT_NEXTCLOUD_TALK_WEBHOOK_PATH = "/nextcloud-talk-webhook";
|
|
613
|
+
function resolveNextcloudTalkLegacyWebhook(config) {
|
|
614
|
+
const listener = config.legacyWebhook;
|
|
615
|
+
if (listener === false) return;
|
|
616
|
+
return {
|
|
617
|
+
port: listener?.port ?? 8788,
|
|
618
|
+
host: listener?.host ?? "0.0.0.0"
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function describeNextcloudTalkWebhookRouteConflict(path, gatewayPort) {
|
|
622
|
+
const pathname = URL.parse(path, "http://localhost")?.pathname ?? path;
|
|
623
|
+
const probe = classifyGatewayProbePath(pathname);
|
|
624
|
+
const reason = probe !== "outside" && probe !== "namespace" ? "is reserved for Gateway probes" : isProtectedPluginRoutePathFromContext(resolvePluginRoutePathContext(pathname)) ? "requires Gateway authentication" : void 0;
|
|
625
|
+
if (!reason) return;
|
|
626
|
+
return `Webhook path "${path}" ${reason} and cannot receive Nextcloud callbacks on the Gateway port. Set webhookPath to "${DEFAULT_NEXTCLOUD_TALK_WEBHOOK_PATH}" and update the Nextcloud bot callback and reverse-proxy upstream to Gateway port ${gatewayPort}${DEFAULT_NEXTCLOUD_TALK_WEBHOOK_PATH}.`;
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
476
629
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
477
630
|
function sanitizeLegacyReplaySegment(value) {
|
|
478
631
|
const trimmed = value.trim();
|
|
479
632
|
if (!trimmed) return "default";
|
|
480
633
|
return trimmed.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
481
634
|
}
|
|
635
|
+
function runNextcloudTalkDoctorSequence(params) {
|
|
636
|
+
const infoNotes = [];
|
|
637
|
+
const warningNotes = [];
|
|
638
|
+
for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
|
|
639
|
+
const account = resolveNextcloudTalkAccount({
|
|
640
|
+
cfg: params.cfg,
|
|
641
|
+
accountId
|
|
642
|
+
});
|
|
643
|
+
if (!account.enabled || !isNextcloudTalkAccountConfigured(account)) continue;
|
|
644
|
+
const gatewayPort = (0, gateway_config_runtime_exports.resolveGatewayPort)({ gateway: params.cfg.gateway }, params.env);
|
|
645
|
+
const webhookPath = account.config.webhookPath ?? "/nextcloud-talk-webhook";
|
|
646
|
+
const destination = `Gateway port ${gatewayPort}${webhookPath}`;
|
|
647
|
+
const legacyListener = resolveNextcloudTalkLegacyWebhook(account.config);
|
|
648
|
+
const routeConflict = describeNextcloudTalkWebhookRouteConflict(webhookPath, gatewayPort);
|
|
649
|
+
if (routeConflict) warningNotes.push(`- channels.nextcloud-talk.${account.accountId}: ${routeConflict}` + (legacyListener ? ` Legacy webhook listener ${legacyListener.host}:${legacyListener.port} remains available; verify the new route before setting legacyWebhook: false.` : " This account cannot start until the callback path is changed."));
|
|
650
|
+
else if (legacyListener) infoNotes.push(`- channels.nextcloud-talk.${account.accountId}: legacy webhook listener ${legacyListener.host}:${legacyListener.port} forwards to the Gateway route. Point the Nextcloud callback or reverse-proxy upstream to ${destination}, verify delivery, then set legacyWebhook: false to disable this account's legacy forwarding.`);
|
|
651
|
+
else infoNotes.push(`- channels.nextcloud-talk.${account.accountId}: legacyWebhook is false; use ${destination} for the Nextcloud callback or reverse-proxy upstream.`);
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
changeNotes: [],
|
|
655
|
+
warningNotes,
|
|
656
|
+
infoNotes
|
|
657
|
+
};
|
|
658
|
+
}
|
|
482
659
|
async function collectNextcloudTalkBotResponseWarnings(params) {
|
|
483
660
|
const warnings = [];
|
|
484
661
|
for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
|
|
@@ -528,7 +705,8 @@ async function repairNextcloudTalkReplayDedupeState(params) {
|
|
|
528
705
|
const nextcloudTalkDoctor = {
|
|
529
706
|
legacyConfigRules,
|
|
530
707
|
normalizeCompatibilityConfig,
|
|
531
|
-
|
|
708
|
+
runConfigSequence: runNextcloudTalkDoctorSequence,
|
|
709
|
+
collectPreviewWarnings: collectNextcloudTalkBotResponseWarnings,
|
|
532
710
|
repairConfig: async ({ cfg, env }) => {
|
|
533
711
|
const repair = await repairNextcloudTalkReplayDedupeState({
|
|
534
712
|
cfg,
|
|
@@ -703,15 +881,7 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
703
881
|
//#endregion
|
|
704
882
|
//#region extensions/nextcloud-talk/src/normalize.ts
|
|
705
883
|
function stripNextcloudTalkTargetPrefix(raw) {
|
|
706
|
-
|
|
707
|
-
if (!trimmed) return;
|
|
708
|
-
let normalized = trimmed;
|
|
709
|
-
if (/^nextcloud-talk:/i.test(normalized)) normalized = normalized.slice(15).trim();
|
|
710
|
-
else if (/^nc-talk:/i.test(normalized)) normalized = normalized.slice(8).trim();
|
|
711
|
-
else if (/^nc:/i.test(normalized)) normalized = normalized.slice(3).trim();
|
|
712
|
-
if (/^room:/i.test(normalized)) normalized = normalized.slice(5).trim();
|
|
713
|
-
if (!normalized) return;
|
|
714
|
-
return normalized;
|
|
884
|
+
return raw.trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, "").trim().replace(/^room:/i, "").trim() || void 0;
|
|
715
885
|
}
|
|
716
886
|
function normalizeNextcloudTalkMessagingTarget(raw) {
|
|
717
887
|
const normalized = stripNextcloudTalkTargetPrefix(raw);
|
|
@@ -751,31 +921,16 @@ function collapseErrorSnippet(text) {
|
|
|
751
921
|
if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
|
|
752
922
|
return collapsed;
|
|
753
923
|
}
|
|
754
|
-
function resolveCredentials(explicit, account) {
|
|
755
|
-
const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
|
|
756
|
-
const secret = explicit.secret?.trim() ?? account.secret;
|
|
757
|
-
if (!baseUrl) throw new Error(`Nextcloud Talk baseUrl missing for account "${account.accountId}" (set channels.nextcloud-talk.baseUrl).`);
|
|
758
|
-
if (!secret) throw new Error(account.tokenStatus === "configured_unavailable" ? `Nextcloud Talk bot secret is configured but unavailable for account "${account.accountId}" (check the configured channels.nextcloud-talk.botSecret/botSecretFile).` : `Nextcloud Talk bot secret missing for account "${account.accountId}" (set channels.nextcloud-talk.botSecret/botSecretFile or NEXTCLOUD_TALK_BOT_SECRET for default).`);
|
|
759
|
-
return {
|
|
760
|
-
baseUrl,
|
|
761
|
-
secret
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
function normalizeRoomToken(to) {
|
|
765
|
-
const normalized = stripNextcloudTalkTargetPrefix(to);
|
|
766
|
-
if (!normalized) throw new Error("Room token is required for Nextcloud Talk sends");
|
|
767
|
-
return normalized;
|
|
768
|
-
}
|
|
769
924
|
function resolveNextcloudTalkSendContext(opts) {
|
|
770
925
|
const cfg = requireRuntimeConfig(opts.cfg, "Nextcloud Talk send");
|
|
771
926
|
const account = resolveNextcloudTalkAccount({
|
|
772
927
|
cfg,
|
|
773
928
|
accountId: opts.accountId
|
|
774
929
|
});
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
930
|
+
const baseUrl = opts.baseUrl?.trim() ?? account.baseUrl;
|
|
931
|
+
const secret = opts.secret?.trim() ?? account.secret;
|
|
932
|
+
if (!baseUrl) throw new Error(`Nextcloud Talk baseUrl missing for account "${account.accountId}" (set channels.nextcloud-talk.baseUrl).`);
|
|
933
|
+
if (!secret) throw new Error(account.tokenStatus === "configured_unavailable" ? `Nextcloud Talk bot secret is configured but unavailable for account "${account.accountId}" (check the configured channels.nextcloud-talk.botSecret/botSecretFile).` : `Nextcloud Talk bot secret missing for account "${account.accountId}" (set channels.nextcloud-talk.botSecret/botSecretFile or NEXTCLOUD_TALK_BOT_SECRET for default).`);
|
|
779
934
|
return {
|
|
780
935
|
cfg,
|
|
781
936
|
account,
|
|
@@ -783,6 +938,11 @@ function resolveNextcloudTalkSendContext(opts) {
|
|
|
783
938
|
secret
|
|
784
939
|
};
|
|
785
940
|
}
|
|
941
|
+
function normalizeRoomToken(to) {
|
|
942
|
+
const normalized = stripNextcloudTalkTargetPrefix(to);
|
|
943
|
+
if (!normalized) throw new Error("Room token is required for Nextcloud Talk sends");
|
|
944
|
+
return normalized;
|
|
945
|
+
}
|
|
786
946
|
function recordNextcloudTalkOutboundActivity(accountId) {
|
|
787
947
|
try {
|
|
788
948
|
getNextcloudTalkRuntime().channel.activity.record({
|
|
@@ -949,20 +1109,6 @@ function roomRoutes(params) {
|
|
|
949
1109
|
}
|
|
950
1110
|
});
|
|
951
1111
|
}
|
|
952
|
-
async function deliverNextcloudTalkReply(params) {
|
|
953
|
-
const { cfg, payload, roomToken, accountId, statusSink } = params;
|
|
954
|
-
return { visibleReplySent: await deliverFormattedTextWithAttachments({
|
|
955
|
-
payload,
|
|
956
|
-
send: async ({ text, replyToId }) => {
|
|
957
|
-
await sendMessageNextcloudTalk(roomToken, text, {
|
|
958
|
-
cfg,
|
|
959
|
-
accountId,
|
|
960
|
-
replyTo: replyToId
|
|
961
|
-
});
|
|
962
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
963
|
-
}
|
|
964
|
-
}) };
|
|
965
|
-
}
|
|
966
1112
|
async function handleNextcloudTalkInbound(params) {
|
|
967
1113
|
const { message, account, config, runtime, statusSink } = params;
|
|
968
1114
|
const core = getNextcloudTalkRuntime();
|
|
@@ -1209,13 +1355,17 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1209
1355
|
text: sanitizeAssistantVisibleText(payload.text)
|
|
1210
1356
|
},
|
|
1211
1357
|
deliver: async (payload) => {
|
|
1212
|
-
return await
|
|
1213
|
-
cfg: config,
|
|
1358
|
+
return { visibleReplySent: await deliverFormattedTextWithAttachments({
|
|
1214
1359
|
payload,
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1360
|
+
send: async ({ text, replyToId }) => {
|
|
1361
|
+
await sendMessageNextcloudTalk(roomToken, text, {
|
|
1362
|
+
cfg: config,
|
|
1363
|
+
accountId: account.accountId,
|
|
1364
|
+
replyTo: replyToId
|
|
1365
|
+
});
|
|
1366
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
1367
|
+
}
|
|
1368
|
+
}) };
|
|
1219
1369
|
},
|
|
1220
1370
|
onError: (err, info) => {
|
|
1221
1371
|
runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
|
|
@@ -1299,211 +1449,226 @@ async function migrateNextcloudTalkLegacyReplayState(params) {
|
|
|
1299
1449
|
}
|
|
1300
1450
|
//#endregion
|
|
1301
1451
|
//#region extensions/nextcloud-talk/src/monitor.ts
|
|
1302
|
-
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
|
|
1303
1452
|
const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
|
|
1304
1453
|
const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
|
|
1305
1454
|
const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
|
|
1306
1455
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
1307
1456
|
const PREAUTH_WEBHOOK_MAX_IN_FLIGHT = 64;
|
|
1308
1457
|
const WEBHOOK_IN_FLIGHT_KEY = "nextcloud-talk-webhook";
|
|
1309
|
-
const HEALTH_PATH = "/healthz";
|
|
1310
1458
|
const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
|
|
1311
|
-
const WEBHOOK_ERRORS = {
|
|
1312
|
-
missingSignatureHeaders: "Missing signature headers",
|
|
1313
|
-
invalidBackend: "Invalid backend",
|
|
1314
|
-
invalidSignature: "Invalid signature",
|
|
1315
|
-
invalidPayloadFormat: "Invalid payload format",
|
|
1316
|
-
payloadTooLarge: "Payload too large",
|
|
1317
|
-
internalServerError: "Internal server error"
|
|
1318
|
-
};
|
|
1319
|
-
function writeJsonResponse(res, status, body) {
|
|
1320
|
-
if (body) {
|
|
1321
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1322
|
-
res.end(JSON.stringify(body));
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
res.writeHead(status);
|
|
1326
|
-
res.end();
|
|
1327
|
-
}
|
|
1328
1459
|
function writeWebhookError(res, status, error) {
|
|
1329
1460
|
if (res.headersSent) return;
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1332
|
-
function validateWebhookHeaders(params) {
|
|
1333
|
-
const headers = extractNextcloudTalkHeaders(params.req.headers);
|
|
1334
|
-
if (!headers) {
|
|
1335
|
-
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.missingSignatureHeaders);
|
|
1336
|
-
return null;
|
|
1337
|
-
}
|
|
1338
|
-
if (params.isBackendAllowed && !params.isBackendAllowed(headers.backend)) {
|
|
1339
|
-
writeWebhookError(params.res, 401, WEBHOOK_ERRORS.invalidBackend);
|
|
1340
|
-
return null;
|
|
1341
|
-
}
|
|
1342
|
-
return headers;
|
|
1343
|
-
}
|
|
1344
|
-
function verifyWebhookSignature(params) {
|
|
1345
|
-
if (!verifyNextcloudTalkSignature({
|
|
1346
|
-
signature: params.headers.signature,
|
|
1347
|
-
random: params.headers.random,
|
|
1348
|
-
body: params.body,
|
|
1349
|
-
secret: params.secret
|
|
1350
|
-
})) {
|
|
1351
|
-
params.authRateLimiter.recordFailure(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1352
|
-
writeWebhookError(params.res, 401, WEBHOOK_ERRORS.invalidSignature);
|
|
1353
|
-
return false;
|
|
1354
|
-
}
|
|
1355
|
-
params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1356
|
-
return true;
|
|
1357
|
-
}
|
|
1358
|
-
function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
1359
|
-
return readRequestBodyWithLimit(req, {
|
|
1360
|
-
maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
|
|
1361
|
-
timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS,
|
|
1362
|
-
destroyOnLimit: false
|
|
1363
|
-
});
|
|
1461
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1462
|
+
res.end(JSON.stringify({ error }));
|
|
1364
1463
|
}
|
|
1365
1464
|
async function rejectWebhookRequest(req, res, status, error) {
|
|
1366
1465
|
if (res.headersSent) return;
|
|
1367
1466
|
await sendHttpRequestRejection(req, res, status, JSON.stringify({ error }), "application/json");
|
|
1368
1467
|
}
|
|
1369
|
-
function
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1468
|
+
function createWebhookGuards() {
|
|
1469
|
+
return {
|
|
1470
|
+
authRateLimiter: createAuthRateLimiter({
|
|
1471
|
+
maxAttempts: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
|
|
1472
|
+
windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
|
|
1473
|
+
lockoutMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
|
|
1474
|
+
exemptLoopback: false,
|
|
1475
|
+
pruneIntervalMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs
|
|
1476
|
+
}),
|
|
1477
|
+
inFlightLimiter: createWebhookInFlightLimiter({
|
|
1478
|
+
maxInFlightPerKey: PREAUTH_WEBHOOK_MAX_IN_FLIGHT,
|
|
1479
|
+
maxTrackedKeys: 1
|
|
1480
|
+
})
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async function handleWebhookRequest(req, res, targetsByPath, path) {
|
|
1484
|
+
const legacyListener = getWebhookLegacyListener(req);
|
|
1485
|
+
const requestTargets = (targetsByPath.get(path) ?? []).filter((entry) => entry.rawPath === req.url && (!legacyListener || entry.legacyListener?.port === legacyListener.port && entry.legacyListener.host === legacyListener.host));
|
|
1486
|
+
if (req.method !== "POST" || requestTargets.length === 0) {
|
|
1487
|
+
res.writeHead(404);
|
|
1488
|
+
res.end();
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const servingTargets = requestTargets.filter((entry) => !entry.stopping);
|
|
1492
|
+
const firstTarget = servingTargets[0];
|
|
1493
|
+
if (!firstTarget) {
|
|
1494
|
+
res.writeHead(503, { "Retry-After": "1" });
|
|
1495
|
+
res.end();
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const responseOwners = /* @__PURE__ */ new Map();
|
|
1499
|
+
if (!res.destroyed && !res.writableFinished) {
|
|
1500
|
+
for (const entry of servingTargets) {
|
|
1501
|
+
const responseDone = createDeferred();
|
|
1502
|
+
entry.pendingResponses.add(responseDone.promise);
|
|
1503
|
+
responseOwners.set(entry, () => {
|
|
1504
|
+
entry.pendingResponses.delete(responseDone.promise);
|
|
1505
|
+
responseOwners.delete(entry);
|
|
1506
|
+
responseDone.resolve();
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
const complete = () => {
|
|
1510
|
+
res.off("finish", complete);
|
|
1511
|
+
res.off("close", complete);
|
|
1512
|
+
for (const release of responseOwners.values()) release();
|
|
1513
|
+
};
|
|
1514
|
+
res.once("finish", complete);
|
|
1515
|
+
res.once("close", complete);
|
|
1516
|
+
}
|
|
1517
|
+
const guards = legacyListener ? firstTarget.legacyGuards : firstTarget.gatewayGuards;
|
|
1518
|
+
if (!guards) {
|
|
1519
|
+
res.writeHead(503, { "Retry-After": "1" });
|
|
1520
|
+
res.end();
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const { authRateLimiter: webhookAuthRateLimiter, inFlightLimiter: webhookInFlightLimiter } = guards;
|
|
1524
|
+
const { onError, trustedProxies, allowRealIpFallback } = firstTarget;
|
|
1525
|
+
const clientIp = resolveRequestClientIp(req, trustedProxies, allowRealIpFallback) ?? req.socket.remoteAddress ?? "unknown";
|
|
1526
|
+
if (!webhookAuthRateLimiter.check(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE).allowed) {
|
|
1527
|
+
res.writeHead(429);
|
|
1528
|
+
res.end("Too Many Requests");
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (!webhookInFlightLimiter.tryAcquire(WEBHOOK_IN_FLIGHT_KEY)) {
|
|
1532
|
+
await sendHttpRequestRejection(req, res, 429, "Too Many Requests");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
let body;
|
|
1536
|
+
let target;
|
|
1537
|
+
try {
|
|
1538
|
+
const headers = extractNextcloudTalkHeaders(req.headers);
|
|
1539
|
+
if (!headers) {
|
|
1540
|
+
writeWebhookError(res, 400, "Missing signature headers");
|
|
1391
1541
|
return;
|
|
1392
1542
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1543
|
+
const targets = requestTargets.filter((entry) => !entry.isBackendAllowed || entry.isBackendAllowed(headers.backend));
|
|
1544
|
+
if (targets.length === 0) {
|
|
1545
|
+
writeWebhookError(res, 401, "Invalid backend");
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
body = await readRequestBodyWithLimit(req, {
|
|
1549
|
+
maxBytes: PREAUTH_WEBHOOK_MAX_BODY_BYTES,
|
|
1550
|
+
timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS,
|
|
1551
|
+
destroyOnLimit: false
|
|
1552
|
+
});
|
|
1553
|
+
const matchesSignature = (entry) => verifyNextcloudTalkSignature({
|
|
1554
|
+
...headers,
|
|
1555
|
+
body,
|
|
1556
|
+
secret: entry.secret
|
|
1557
|
+
});
|
|
1558
|
+
const match = resolveSingleWebhookTarget(targets, (entry) => servingTargets.includes(entry) && matchesSignature(entry));
|
|
1559
|
+
if (match.kind === "none" && targets.some((entry) => !servingTargets.includes(entry) && matchesSignature(entry))) {
|
|
1560
|
+
res.writeHead(503, { "Retry-After": "1" });
|
|
1395
1561
|
res.end();
|
|
1396
1562
|
return;
|
|
1397
1563
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
res
|
|
1401
|
-
res.end("Too Many Requests");
|
|
1564
|
+
if (match.kind !== "single") {
|
|
1565
|
+
webhookAuthRateLimiter.recordFailure(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1566
|
+
writeWebhookError(res, 401, "Invalid signature");
|
|
1402
1567
|
return;
|
|
1403
1568
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1569
|
+
target = match.target;
|
|
1570
|
+
for (const [entry, release] of responseOwners) if (entry !== target) release();
|
|
1571
|
+
if (!targetsByPath.get(path)?.includes(target)) {
|
|
1572
|
+
res.writeHead(503, { "Retry-After": "1" });
|
|
1573
|
+
res.end();
|
|
1406
1574
|
return;
|
|
1407
1575
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
res,
|
|
1413
|
-
isBackendAllowed
|
|
1414
|
-
});
|
|
1415
|
-
if (!headers) return;
|
|
1416
|
-
body = await readBody(req, maxBodyBytes);
|
|
1417
|
-
if (!verifyWebhookSignature({
|
|
1418
|
-
headers,
|
|
1419
|
-
body,
|
|
1420
|
-
secret,
|
|
1421
|
-
res,
|
|
1422
|
-
clientIp,
|
|
1423
|
-
authRateLimiter: webhookAuthRateLimiter
|
|
1424
|
-
})) return;
|
|
1425
|
-
} catch (err) {
|
|
1426
|
-
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1427
|
-
await rejectWebhookRequest(req, res, 413, WEBHOOK_ERRORS.payloadTooLarge);
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
|
|
1431
|
-
await rejectWebhookRequest(req, res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
1435
|
-
writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
|
|
1439
|
-
onError?.(error);
|
|
1440
|
-
writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
|
|
1576
|
+
webhookAuthRateLimiter.reset(clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1579
|
+
await rejectWebhookRequest(req, res, 413, "Payload too large");
|
|
1441
1580
|
return;
|
|
1442
|
-
} finally {
|
|
1443
|
-
webhookInFlightLimiter.release(WEBHOOK_IN_FLIGHT_KEY);
|
|
1444
1581
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
} catch (err) {
|
|
1449
|
-
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
1450
|
-
writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
|
|
1454
|
-
onError?.(error);
|
|
1455
|
-
writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
|
|
1582
|
+
if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
|
|
1583
|
+
await rejectWebhookRequest(req, res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
|
|
1584
|
+
return;
|
|
1456
1585
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1586
|
+
const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
|
|
1587
|
+
onError?.(error);
|
|
1588
|
+
writeWebhookError(res, 500, "Internal server error");
|
|
1589
|
+
return;
|
|
1590
|
+
} finally {
|
|
1591
|
+
webhookInFlightLimiter.release(WEBHOOK_IN_FLIGHT_KEY);
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
if (await target.onWebhook(body) === "accepted") res.setHeader(NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER, NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE);
|
|
1595
|
+
res.writeHead(200);
|
|
1596
|
+
res.end();
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
1599
|
+
writeWebhookError(res, 400, "Invalid payload format");
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const error = err instanceof Error ? err : new Error(formatErrorMessage(err));
|
|
1603
|
+
target.onError?.(error);
|
|
1604
|
+
writeWebhookError(res, 500, "Internal server error");
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const webhookState = createPluginRuntimeStore({
|
|
1608
|
+
key: "nextcloud-talk:webhook-routes",
|
|
1609
|
+
errorMessage: "Nextcloud Talk webhook routes are not registered"
|
|
1610
|
+
});
|
|
1611
|
+
function registerNextcloudTalkWebhook(target) {
|
|
1612
|
+
let targets = webhookState.tryGetRuntime();
|
|
1613
|
+
if (!targets) {
|
|
1614
|
+
targets = /* @__PURE__ */ new Map();
|
|
1615
|
+
webhookState.setRuntime(targets);
|
|
1616
|
+
}
|
|
1617
|
+
const path = canonicalizeWebhookRouteKey(target.path);
|
|
1618
|
+
const gatewayGuards = targets.get(path)?.[0]?.gatewayGuards ?? createWebhookGuards();
|
|
1619
|
+
const endpoint = target.legacyListener;
|
|
1620
|
+
const legacyGuards = endpoint ? [...targets.values()].flat().find((entry) => entry.legacyListener?.port === endpoint.port && entry.legacyListener.host === endpoint.host)?.legacyGuards ?? createWebhookGuards() : void 0;
|
|
1621
|
+
const registration = registerWebhookTarget(targets, {
|
|
1622
|
+
...target,
|
|
1623
|
+
path,
|
|
1624
|
+
rawPath: target.path,
|
|
1625
|
+
stopping: false,
|
|
1626
|
+
pendingResponses: /* @__PURE__ */ new Set(),
|
|
1627
|
+
gatewayGuards,
|
|
1628
|
+
legacyGuards
|
|
1463
1629
|
});
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
if (closePromise) return closePromise;
|
|
1469
|
-
if (!listenerActive) return Promise.resolve();
|
|
1470
|
-
closePromise = new Promise((resolve) => {
|
|
1471
|
-
server.close(() => resolve());
|
|
1472
|
-
}).finally(() => {
|
|
1473
|
-
listenerActive = false;
|
|
1474
|
-
closePromise = void 0;
|
|
1475
|
-
});
|
|
1476
|
-
return closePromise;
|
|
1477
|
-
};
|
|
1478
|
-
const stop = async () => {
|
|
1479
|
-
stopRequested = true;
|
|
1480
|
-
await closeIfListening();
|
|
1481
|
-
webhookAuthRateLimiter.dispose();
|
|
1630
|
+
const removeTarget = () => {
|
|
1631
|
+
registration.unregister();
|
|
1632
|
+
const remaining = [...targets.values()].flat();
|
|
1633
|
+
for (const guards of [gatewayGuards, legacyGuards]) if (guards && !remaining.some((entry) => entry.gatewayGuards === guards || entry.legacyGuards === guards)) guards.authRateLimiter.dispose();
|
|
1482
1634
|
};
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1635
|
+
try {
|
|
1636
|
+
const unregister = registerPluginHttpRoute({
|
|
1637
|
+
path,
|
|
1638
|
+
auth: "plugin",
|
|
1639
|
+
pluginId: "nextcloud-talk",
|
|
1640
|
+
source: "webhook",
|
|
1641
|
+
accountId: target.accountId,
|
|
1642
|
+
handler: (req, res) => handleWebhookRequest(req, res, targets, path),
|
|
1643
|
+
legacyListener: target.legacyListener ? {
|
|
1644
|
+
...target.legacyListener,
|
|
1645
|
+
health: {
|
|
1646
|
+
path: "/healthz",
|
|
1647
|
+
contentType: "text/plain"
|
|
1648
|
+
}
|
|
1649
|
+
} : void 0,
|
|
1650
|
+
reuseExistingSameOwner: true,
|
|
1651
|
+
throwOnFailure: true
|
|
1496
1652
|
});
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1653
|
+
let stopPromise;
|
|
1654
|
+
return () => {
|
|
1655
|
+
if (stopPromise) return stopPromise;
|
|
1656
|
+
registration.target.stopping = true;
|
|
1657
|
+
const release = () => {
|
|
1658
|
+
removeTarget();
|
|
1659
|
+
unregister();
|
|
1660
|
+
};
|
|
1661
|
+
const pending = registration.target.pendingResponses;
|
|
1662
|
+
if (pending.size === 0) {
|
|
1663
|
+
release();
|
|
1664
|
+
stopPromise = Promise.resolve();
|
|
1665
|
+
} else stopPromise = Promise.all(pending).then(release);
|
|
1666
|
+
return stopPromise;
|
|
1667
|
+
};
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
removeTarget();
|
|
1670
|
+
throw error;
|
|
1501
1671
|
}
|
|
1502
|
-
return {
|
|
1503
|
-
server,
|
|
1504
|
-
start,
|
|
1505
|
-
stop
|
|
1506
|
-
};
|
|
1507
1672
|
}
|
|
1508
1673
|
//#endregion
|
|
1509
1674
|
//#region extensions/nextcloud-talk/src/webhook-spool.ts
|
|
@@ -1667,9 +1832,6 @@ function createNextcloudTalkWebhookSpool(options) {
|
|
|
1667
1832
|
}
|
|
1668
1833
|
//#endregion
|
|
1669
1834
|
//#region extensions/nextcloud-talk/src/monitor-runtime.ts
|
|
1670
|
-
const DEFAULT_WEBHOOK_PORT = 8788;
|
|
1671
|
-
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
1672
|
-
const DEFAULT_WEBHOOK_PATH = "/nextcloud-talk-webhook";
|
|
1673
1835
|
function normalizeOrigin(value) {
|
|
1674
1836
|
try {
|
|
1675
1837
|
return normalizeLowercaseStringOrEmpty(new URL(value).origin);
|
|
@@ -1686,9 +1848,11 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1686
1848
|
});
|
|
1687
1849
|
const runtime = resolveLoggerBackedRuntime(opts.runtime, core.logging.getChildLogger());
|
|
1688
1850
|
if (!account.secret) throw new Error(`Nextcloud Talk bot secret not configured for account "${account.accountId}"`);
|
|
1689
|
-
const
|
|
1690
|
-
const
|
|
1691
|
-
const
|
|
1851
|
+
const path = account.config.webhookPath ?? "/nextcloud-talk-webhook";
|
|
1852
|
+
const gatewayPort = (0, gateway_config_runtime_exports.resolveGatewayPort)({ gateway: cfg.gateway });
|
|
1853
|
+
const legacyListener = resolveNextcloudTalkLegacyWebhook(account.config);
|
|
1854
|
+
const routeConflict = describeNextcloudTalkWebhookRouteConflict(path, gatewayPort);
|
|
1855
|
+
if (routeConflict && !legacyListener) throw new Error(`[nextcloud-talk:${account.accountId}] ${routeConflict}`);
|
|
1692
1856
|
const logger = core.logging.getChildLogger({
|
|
1693
1857
|
channel: "nextcloud-talk",
|
|
1694
1858
|
accountId: account.accountId
|
|
@@ -1716,27 +1880,12 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1716
1880
|
});
|
|
1717
1881
|
}
|
|
1718
1882
|
});
|
|
1719
|
-
|
|
1720
|
-
port,
|
|
1721
|
-
host,
|
|
1722
|
-
path,
|
|
1723
|
-
secret: account.secret,
|
|
1724
|
-
isBackendAllowed: (backend) => {
|
|
1725
|
-
if (!expectedBackendOrigin) return true;
|
|
1726
|
-
return normalizeOrigin(backend) === expectedBackendOrigin;
|
|
1727
|
-
},
|
|
1728
|
-
onWebhook: spool.receive,
|
|
1729
|
-
onError: (error) => {
|
|
1730
|
-
logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
|
|
1731
|
-
},
|
|
1732
|
-
trustedProxies: cfg.gateway?.trustedProxies,
|
|
1733
|
-
allowRealIpFallback: cfg.gateway?.allowRealIpFallback,
|
|
1734
|
-
abortSignal: opts.abortSignal
|
|
1735
|
-
});
|
|
1883
|
+
let unregister;
|
|
1736
1884
|
let stopPromise;
|
|
1737
1885
|
const stop = () => {
|
|
1738
1886
|
stopPromise ??= (async () => {
|
|
1739
|
-
await
|
|
1887
|
+
await unregister?.();
|
|
1888
|
+
unregister = void 0;
|
|
1740
1889
|
await spool.stop();
|
|
1741
1890
|
})();
|
|
1742
1891
|
return stopPromise;
|
|
@@ -1748,7 +1897,26 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1748
1897
|
}
|
|
1749
1898
|
try {
|
|
1750
1899
|
await spool.ready();
|
|
1751
|
-
|
|
1900
|
+
if (opts.abortSignal?.aborted) {
|
|
1901
|
+
await stop();
|
|
1902
|
+
return { stop };
|
|
1903
|
+
}
|
|
1904
|
+
unregister = registerNextcloudTalkWebhook({
|
|
1905
|
+
accountId: account.accountId,
|
|
1906
|
+
legacyListener,
|
|
1907
|
+
path,
|
|
1908
|
+
secret: account.secret,
|
|
1909
|
+
isBackendAllowed: (backend) => {
|
|
1910
|
+
if (!expectedBackendOrigin) return true;
|
|
1911
|
+
return normalizeOrigin(backend) === expectedBackendOrigin;
|
|
1912
|
+
},
|
|
1913
|
+
onWebhook: spool.receive,
|
|
1914
|
+
onError: (error) => {
|
|
1915
|
+
logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
|
|
1916
|
+
},
|
|
1917
|
+
trustedProxies: cfg.gateway?.trustedProxies,
|
|
1918
|
+
allowRealIpFallback: cfg.gateway?.allowRealIpFallback
|
|
1919
|
+
});
|
|
1752
1920
|
} catch (error) {
|
|
1753
1921
|
await stop();
|
|
1754
1922
|
throw error;
|
|
@@ -1758,8 +1926,12 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1758
1926
|
return { stop };
|
|
1759
1927
|
}
|
|
1760
1928
|
opts.statusSink?.(channelReadyPatch());
|
|
1761
|
-
|
|
1762
|
-
|
|
1929
|
+
if (routeConflict && legacyListener) {
|
|
1930
|
+
logger.warn(`[nextcloud-talk:${account.accountId}] ${routeConflict} Legacy webhook listener ${legacyListener.host}:${legacyListener.port} remains available; verify the new route before setting legacyWebhook: false.`);
|
|
1931
|
+
return { stop };
|
|
1932
|
+
}
|
|
1933
|
+
logger.info(`[nextcloud-talk:${account.accountId}] Gateway webhook route ready at port ${gatewayPort}${path}; point the Nextcloud bot callback or reverse-proxy upstream here.`);
|
|
1934
|
+
if (legacyListener) logger.info(`[nextcloud-talk:${account.accountId}] legacy webhook listener ${legacyListener.host}:${legacyListener.port} forwards to the Gateway route. After verifying the callback or proxy upstream uses the Gateway port, set legacyWebhook: false to disable this account's legacy forwarding.`);
|
|
1763
1935
|
return { stop };
|
|
1764
1936
|
}
|
|
1765
1937
|
//#endregion
|
|
@@ -1768,7 +1940,7 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1768
1940
|
startAccount: async (ctx) => {
|
|
1769
1941
|
const account = ctx.account;
|
|
1770
1942
|
if (!account.secret || !account.baseUrl) throw new Error(`Nextcloud Talk not configured for account "${account.accountId}" (missing secret or baseUrl)`);
|
|
1771
|
-
ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook
|
|
1943
|
+
ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk Gateway webhook route`);
|
|
1772
1944
|
const statusSink = createAccountStatusSink({
|
|
1773
1945
|
accountId: ctx.accountId,
|
|
1774
1946
|
setStatus: ctx.setStatus
|
|
@@ -1865,6 +2037,15 @@ const nextcloudTalkMessageActions = {
|
|
|
1865
2037
|
};
|
|
1866
2038
|
//#endregion
|
|
1867
2039
|
//#region extensions/nextcloud-talk/src/message-adapter.ts
|
|
2040
|
+
function sendNextcloudTalkMessage(ctx, text = ctx.text) {
|
|
2041
|
+
return sendMessageNextcloudTalk(ctx.to, text, {
|
|
2042
|
+
accountId: ctx.accountId ?? void 0,
|
|
2043
|
+
replyTo: ctx.replyToId ?? void 0,
|
|
2044
|
+
cfg: ctx.cfg,
|
|
2045
|
+
onPlatformSendDispatch: ctx.onPlatformSendDispatch,
|
|
2046
|
+
assertDirectAdapterHandoff: ctx.assertDirectAdapterHandoff
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
1868
2049
|
const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
|
|
1869
2050
|
id: "nextcloud-talk",
|
|
1870
2051
|
durableFinal: { capabilities: {
|
|
@@ -1873,20 +2054,8 @@ const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
|
|
|
1873
2054
|
replyTo: true
|
|
1874
2055
|
} },
|
|
1875
2056
|
send: {
|
|
1876
|
-
text:
|
|
1877
|
-
|
|
1878
|
-
replyTo: ctx.replyToId ?? void 0,
|
|
1879
|
-
cfg: ctx.cfg,
|
|
1880
|
-
onPlatformSendDispatch: ctx.onPlatformSendDispatch,
|
|
1881
|
-
assertDirectAdapterHandoff: ctx.assertDirectAdapterHandoff
|
|
1882
|
-
}),
|
|
1883
|
-
media: async (ctx) => await sendMessageNextcloudTalk(ctx.to, ctx.mediaUrl ? `${ctx.text}\n\nAttachment: ${ctx.mediaUrl}` : ctx.text, {
|
|
1884
|
-
accountId: ctx.accountId ?? void 0,
|
|
1885
|
-
replyTo: ctx.replyToId ?? void 0,
|
|
1886
|
-
cfg: ctx.cfg,
|
|
1887
|
-
onPlatformSendDispatch: ctx.onPlatformSendDispatch,
|
|
1888
|
-
assertDirectAdapterHandoff: ctx.assertDirectAdapterHandoff
|
|
1889
|
-
})
|
|
2057
|
+
text: sendNextcloudTalkMessage,
|
|
2058
|
+
media: (ctx) => sendNextcloudTalkMessage(ctx, ctx.mediaUrl ? `${ctx.text}\n\nAttachment: ${ctx.mediaUrl}` : ctx.text)
|
|
1890
2059
|
}
|
|
1891
2060
|
});
|
|
1892
2061
|
//#endregion
|
|
@@ -1894,19 +2063,11 @@ const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
|
|
|
1894
2063
|
function resolveNextcloudTalkOutboundSessionRoute(params) {
|
|
1895
2064
|
const roomId = stripNextcloudTalkTargetPrefix(params.target);
|
|
1896
2065
|
if (!roomId) return null;
|
|
1897
|
-
|
|
2066
|
+
return buildChannelOutboundSessionRoute({
|
|
1898
2067
|
cfg: params.cfg,
|
|
1899
2068
|
agentId: params.agentId,
|
|
1900
2069
|
channel: "nextcloud-talk",
|
|
1901
2070
|
accountId: params.accountId,
|
|
1902
|
-
peer: {
|
|
1903
|
-
kind: "group",
|
|
1904
|
-
id: roomId
|
|
1905
|
-
}
|
|
1906
|
-
});
|
|
1907
|
-
return {
|
|
1908
|
-
sessionKey: baseSessionKey,
|
|
1909
|
-
baseSessionKey,
|
|
1910
2071
|
recipientSessionExact: false,
|
|
1911
2072
|
peer: {
|
|
1912
2073
|
kind: "group",
|
|
@@ -1915,7 +2076,7 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
|
|
|
1915
2076
|
chatType: "group",
|
|
1916
2077
|
from: `nextcloud-talk:room:${roomId}`,
|
|
1917
2078
|
to: `nextcloud-talk:${roomId}`
|
|
1918
|
-
};
|
|
2079
|
+
});
|
|
1919
2080
|
}
|
|
1920
2081
|
//#endregion
|
|
1921
2082
|
//#region extensions/nextcloud-talk/src/setup-core.ts
|
|
@@ -1937,44 +2098,28 @@ function setNextcloudTalkAccountConfig(cfg, accountId, updates, clearFields) {
|
|
|
1937
2098
|
...clearFields ? { clearFields } : {}
|
|
1938
2099
|
});
|
|
1939
2100
|
}
|
|
1940
|
-
async
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
placeholder: "username",
|
|
1955
|
-
parseEntries: (raw) => ({ entries: raw.split(/[\n,;]+/g).map(normalizeLowercaseStringOrEmpty).filter(Boolean) }),
|
|
1956
|
-
getExistingAllowFrom: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
|
|
1957
|
-
cfg,
|
|
1958
|
-
accountId
|
|
1959
|
-
}).config.allowFrom ?? [],
|
|
1960
|
-
mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing.map((value) => normalizeLowercaseStringOrEmpty(String(value))), parsed),
|
|
1961
|
-
applyAllowFrom: ({ cfg, accountId, allowFrom }) => setNextcloudTalkAccountConfig(cfg, accountId, {
|
|
1962
|
-
dmPolicy: "allowlist",
|
|
1963
|
-
allowFrom
|
|
1964
|
-
})
|
|
1965
|
-
});
|
|
1966
|
-
}
|
|
1967
|
-
async function promptNextcloudTalkAllowFromForAccount(params) {
|
|
1968
|
-
const accountId = resolveSetupAccountId({
|
|
1969
|
-
accountId: params.accountId,
|
|
1970
|
-
defaultAccountId: resolveDefaultNextcloudTalkAccountId(params.cfg)
|
|
1971
|
-
});
|
|
1972
|
-
return await promptNextcloudTalkAllowFrom({
|
|
1973
|
-
cfg: params.cfg,
|
|
1974
|
-
prompter: params.prompter,
|
|
2101
|
+
const promptNextcloudTalkAllowFrom = async (params) => await createPromptParsedAllowFromForAccount({
|
|
2102
|
+
defaultAccountId: resolveDefaultNextcloudTalkAccountId,
|
|
2103
|
+
noteTitle: t$1("wizard.nextcloudTalk.userIdTitle"),
|
|
2104
|
+
noteLines: [
|
|
2105
|
+
t$1("wizard.nextcloudTalk.userIdHelpAdmin"),
|
|
2106
|
+
t$1("wizard.nextcloudTalk.userIdHelpLogs"),
|
|
2107
|
+
t$1("wizard.nextcloudTalk.userIdHelpLowercase"),
|
|
2108
|
+
t$1("wizard.channels.docs", { link: formatDocsLink$1("/channels/nextcloud-talk", "nextcloud-talk") })
|
|
2109
|
+
],
|
|
2110
|
+
message: t$1("wizard.nextcloudTalk.allowFromPrompt"),
|
|
2111
|
+
placeholder: "username",
|
|
2112
|
+
parseEntries: (raw) => ({ entries: raw.split(/[\n,;]+/g).map(normalizeLowercaseStringOrEmpty).filter(Boolean) }),
|
|
2113
|
+
getExistingAllowFrom: ({ cfg, accountId }) => resolveNextcloudTalkAccount({
|
|
2114
|
+
cfg,
|
|
1975
2115
|
accountId
|
|
1976
|
-
})
|
|
1977
|
-
}
|
|
2116
|
+
}).config.allowFrom ?? [],
|
|
2117
|
+
mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing.map((value) => normalizeLowercaseStringOrEmpty(String(value))), parsed),
|
|
2118
|
+
applyAllowFrom: ({ cfg, accountId, allowFrom }) => setNextcloudTalkAccountConfig(cfg, accountId, {
|
|
2119
|
+
dmPolicy: "allowlist",
|
|
2120
|
+
allowFrom
|
|
2121
|
+
})
|
|
2122
|
+
})(params);
|
|
1978
2123
|
const nextcloudTalkDmPolicy = createChannelDmPolicy({
|
|
1979
2124
|
label: "Nextcloud Talk",
|
|
1980
2125
|
channel: channel$1,
|
|
@@ -1983,7 +2128,7 @@ const nextcloudTalkDmPolicy = createChannelDmPolicy({
|
|
|
1983
2128
|
accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg)
|
|
1984
2129
|
}),
|
|
1985
2130
|
applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
|
|
1986
|
-
promptAllowFrom:
|
|
2131
|
+
promptAllowFrom: promptNextcloudTalkAllowFrom
|
|
1987
2132
|
});
|
|
1988
2133
|
const nextcloudTalkSetupAdapter = {
|
|
1989
2134
|
singleAccountKeysToMove: ["rooms"],
|
|
@@ -2304,7 +2449,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2304
2449
|
],
|
|
2305
2450
|
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
|
2306
2451
|
inferTargetChatType: ({ to }) => normalizeNextcloudTalkMessagingTarget(to) ? "group" : void 0,
|
|
2307
|
-
resolveOutboundSessionRoute:
|
|
2452
|
+
resolveOutboundSessionRoute: resolveNextcloudTalkOutboundSessionRoute,
|
|
2308
2453
|
targetResolver: {
|
|
2309
2454
|
looksLikeId: looksLikeNextcloudTalkTargetId,
|
|
2310
2455
|
hint: "<roomToken>"
|
|
@@ -2369,24 +2514,11 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2369
2514
|
},
|
|
2370
2515
|
attachedResults: {
|
|
2371
2516
|
channel: "nextcloud-talk",
|
|
2372
|
-
sendText:
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
text: ctx.text,
|
|
2376
|
-
accountId: ctx.accountId,
|
|
2377
|
-
replyToId: ctx.replyToId,
|
|
2378
|
-
onPlatformSendDispatch: ctx.onPlatformSendDispatch,
|
|
2379
|
-
assertDirectAdapterHandoff: ctx.assertDirectAdapterHandoff
|
|
2380
|
-
}),
|
|
2381
|
-
sendMedia: async (ctx) => await nextcloudTalkMessageAdapter.send.media({
|
|
2382
|
-
cfg: ctx.cfg,
|
|
2383
|
-
to: ctx.to,
|
|
2384
|
-
text: ctx.text,
|
|
2517
|
+
sendText: nextcloudTalkMessageAdapter.send.text,
|
|
2518
|
+
sendMedia: (ctx) => nextcloudTalkMessageAdapter.send.media({
|
|
2519
|
+
...ctx,
|
|
2385
2520
|
mediaUrl: ctx.mediaUrl ?? "",
|
|
2386
|
-
|
|
2387
|
-
replyToId: ctx.replyToId,
|
|
2388
|
-
onPlatformSendDispatch: ctx.onPlatformSendDispatch,
|
|
2389
|
-
assertDirectAdapterHandoff: ctx.assertDirectAdapterHandoff
|
|
2521
|
+
onDeliveryResult: void 0
|
|
2390
2522
|
})
|
|
2391
2523
|
}
|
|
2392
2524
|
}
|