@openclaw/nextcloud-talk 2026.7.2-beta.3 → 2026.7.2-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +1 -1
- package/dist/{channel-De2OKnzd.js → channel-DtnECxVM.js} +459 -192
- package/dist/channel-plugin-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +0 -6
- package/package.json +89 -5
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-DtnECxVM.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -6,7 +6,7 @@ import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
|
6
6
|
import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
|
7
7
|
import { createAllowlistProviderRouteAllowlistWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
|
|
8
8
|
import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
9
|
-
import { convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
9
|
+
import { convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
10
10
|
import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
11
11
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
12
|
import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-core";
|
|
@@ -16,7 +16,7 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
|
16
16
|
import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
17
17
|
import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
18
18
|
import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
19
|
-
import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
19
|
+
import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
|
|
20
20
|
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
21
21
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
22
22
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
@@ -27,11 +27,11 @@ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/
|
|
|
27
27
|
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
28
28
|
import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
|
|
29
29
|
import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
30
|
-
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime
|
|
30
|
+
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
|
|
31
31
|
import { z } from "zod";
|
|
32
32
|
import os from "node:os";
|
|
33
33
|
import path from "node:path";
|
|
34
|
-
import {
|
|
34
|
+
import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
35
35
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
36
36
|
import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
37
37
|
import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback } from "openclaw/plugin-sdk/channel-targets";
|
|
@@ -41,6 +41,7 @@ import { createServer } from "node:http";
|
|
|
41
41
|
import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
|
|
42
42
|
import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
|
|
43
43
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
|
|
44
|
+
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
44
45
|
import { applyAccountNameToChannelSection, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
|
|
45
46
|
import { createSetupInputPresenceValidator, createSetupTranslator as createSetupTranslator$1, mergeAllowFromEntries, promptParsedAllowFromForAccount, resolveSetupAccountId } from "openclaw/plugin-sdk/setup-runtime";
|
|
46
47
|
import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-tools";
|
|
@@ -201,6 +202,12 @@ const nextcloudTalkApprovalAuth = createChannelApprovalAuth({
|
|
|
201
202
|
normalizeApprover: normalizeNextcloudTalkApproverId
|
|
202
203
|
}).approvalAuth;
|
|
203
204
|
//#endregion
|
|
205
|
+
//#region extensions/nextcloud-talk/src/guarded-response.ts
|
|
206
|
+
async function releaseNextcloudTalkGuardedResponse(params) {
|
|
207
|
+
if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
|
|
208
|
+
await params.release();
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
204
211
|
//#region extensions/nextcloud-talk/src/signature.ts
|
|
205
212
|
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
|
|
206
213
|
const RANDOM_HEADER = "x-nextcloud-talk-random";
|
|
@@ -344,7 +351,10 @@ async function probeNextcloudTalkBotResponseFeature(params) {
|
|
|
344
351
|
message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
|
|
345
352
|
};
|
|
346
353
|
} finally {
|
|
347
|
-
await
|
|
354
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
355
|
+
response,
|
|
356
|
+
release
|
|
357
|
+
});
|
|
348
358
|
}
|
|
349
359
|
} catch (error) {
|
|
350
360
|
return {
|
|
@@ -426,44 +436,13 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
|
|
|
426
436
|
}
|
|
427
437
|
});
|
|
428
438
|
//#endregion
|
|
429
|
-
//#region extensions/nextcloud-talk/src/replay-
|
|
439
|
+
//#region extensions/nextcloud-talk/src/replay-migration-contract.ts
|
|
430
440
|
const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
|
|
431
441
|
const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const DEFAULT_STATE_MAX_ENTRIES = 1e4;
|
|
435
|
-
function buildNextcloudTalkReplayKey(params) {
|
|
436
|
-
const roomToken = params.roomToken.trim();
|
|
437
|
-
const messageId = params.messageId.trim();
|
|
438
|
-
if (!roomToken || !messageId) return null;
|
|
439
|
-
return `${roomToken}:${messageId}`;
|
|
440
|
-
}
|
|
441
|
-
function createNextcloudTalkReplayGuard(options) {
|
|
442
|
-
const stateDir = options.stateDir?.trim();
|
|
443
|
-
const baseOptions = {
|
|
444
|
-
ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
|
|
445
|
-
memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE
|
|
446
|
-
};
|
|
447
|
-
return createChannelReplayGuard({
|
|
448
|
-
dedupe: stateDir ? {
|
|
449
|
-
...baseOptions,
|
|
450
|
-
pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
|
|
451
|
-
namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
|
|
452
|
-
stateMaxEntries: options.stateMaxEntries ?? options.fileMaxEntries ?? DEFAULT_STATE_MAX_ENTRIES,
|
|
453
|
-
env: {
|
|
454
|
-
...process.env,
|
|
455
|
-
OPENCLAW_STATE_DIR: stateDir
|
|
456
|
-
},
|
|
457
|
-
onDiskError: options.onDiskError
|
|
458
|
-
} : baseOptions,
|
|
459
|
-
buildReplayKey: buildNextcloudTalkReplayKey,
|
|
460
|
-
namespace: (event) => event.accountId
|
|
461
|
-
});
|
|
462
|
-
}
|
|
442
|
+
const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
|
|
443
|
+
const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
463
444
|
//#endregion
|
|
464
445
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
465
|
-
const REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
|
|
466
|
-
const REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
|
|
467
446
|
function sanitizeLegacyReplaySegment(value) {
|
|
468
447
|
const trimmed = value.trim();
|
|
469
448
|
if (!trimmed) return "default";
|
|
@@ -498,11 +477,11 @@ async function repairNextcloudTalkReplayDedupeState(params) {
|
|
|
498
477
|
const result = await migratePersistentDedupeLegacyJsonFile({
|
|
499
478
|
filePath: legacyPath,
|
|
500
479
|
namespace: accountId,
|
|
501
|
-
ttlMs:
|
|
480
|
+
ttlMs: NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS,
|
|
502
481
|
memoryMaxSize: 0,
|
|
503
482
|
pluginId: NEXTCLOUD_TALK_PLUGIN_ID,
|
|
504
483
|
namespacePrefix: NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX,
|
|
505
|
-
stateMaxEntries:
|
|
484
|
+
stateMaxEntries: NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES,
|
|
506
485
|
env
|
|
507
486
|
});
|
|
508
487
|
changes.push(`Migrated Nextcloud Talk replay dedupe cache for account "${accountId}" to SQLite (${result.imported} imported, ${result.skippedExpired} expired, ${result.skippedExisting} already current).`);
|
|
@@ -685,7 +664,10 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
685
664
|
});
|
|
686
665
|
return kind;
|
|
687
666
|
} finally {
|
|
688
|
-
await
|
|
667
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
668
|
+
response,
|
|
669
|
+
release
|
|
670
|
+
});
|
|
689
671
|
}
|
|
690
672
|
} catch (err) {
|
|
691
673
|
cacheRoomInfo(key, {
|
|
@@ -724,6 +706,43 @@ function looksLikeNextcloudTalkTargetId(raw) {
|
|
|
724
706
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
|
|
725
707
|
const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
|
|
726
708
|
const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
|
|
709
|
+
const NEXTCLOUD_TALK_FORMAT_PROFILE = {
|
|
710
|
+
mechanism: "markdown",
|
|
711
|
+
constructs: {
|
|
712
|
+
bold: "native",
|
|
713
|
+
italic: "native",
|
|
714
|
+
underline: "native",
|
|
715
|
+
strikethrough: "native",
|
|
716
|
+
spoiler: "native",
|
|
717
|
+
codeInline: "native",
|
|
718
|
+
codeBlock: "native",
|
|
719
|
+
codeLanguage: "native",
|
|
720
|
+
linkLabel: "native",
|
|
721
|
+
heading: "native",
|
|
722
|
+
bulletList: "native",
|
|
723
|
+
orderedList: "native",
|
|
724
|
+
taskList: "native",
|
|
725
|
+
table: "native",
|
|
726
|
+
blockquote: "native",
|
|
727
|
+
image: "native",
|
|
728
|
+
mention: "native"
|
|
729
|
+
},
|
|
730
|
+
chunk: {
|
|
731
|
+
limit: 4e3,
|
|
732
|
+
unit: "chars",
|
|
733
|
+
hardCap: 32e3
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
function renderNextcloudTalkMarkdown(markdown) {
|
|
737
|
+
return renderMarkdownWithMarkers({
|
|
738
|
+
text: markdown,
|
|
739
|
+
styles: [],
|
|
740
|
+
links: []
|
|
741
|
+
}, {
|
|
742
|
+
styleMarkers: {},
|
|
743
|
+
escapeText: (text) => text
|
|
744
|
+
}, NEXTCLOUD_TALK_FORMAT_PROFILE);
|
|
745
|
+
}
|
|
727
746
|
/** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
|
|
728
747
|
function collapseErrorSnippet(text) {
|
|
729
748
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
@@ -802,7 +821,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
802
821
|
channel: "nextcloud-talk",
|
|
803
822
|
accountId: account.accountId
|
|
804
823
|
});
|
|
805
|
-
const message = convertMarkdownTables(text.trim(), tableMode);
|
|
824
|
+
const message = convertMarkdownTables(renderNextcloudTalkMarkdown(text.trim()), tableMode);
|
|
806
825
|
const body = { message };
|
|
807
826
|
if (opts.replyTo) body.replyTo = opts.replyTo;
|
|
808
827
|
const bodyStr = JSON.stringify(body);
|
|
@@ -858,7 +877,10 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
858
877
|
timestamp
|
|
859
878
|
};
|
|
860
879
|
} finally {
|
|
861
|
-
await
|
|
880
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
881
|
+
response,
|
|
882
|
+
release
|
|
883
|
+
});
|
|
862
884
|
}
|
|
863
885
|
}
|
|
864
886
|
async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
@@ -892,7 +914,10 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
|
|
|
892
914
|
}
|
|
893
915
|
return { ok: true };
|
|
894
916
|
} finally {
|
|
895
|
-
await
|
|
917
|
+
await releaseNextcloudTalkGuardedResponse({
|
|
918
|
+
response,
|
|
919
|
+
release
|
|
920
|
+
});
|
|
896
921
|
}
|
|
897
922
|
}
|
|
898
923
|
//#endregion
|
|
@@ -1188,6 +1213,7 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1188
1213
|
},
|
|
1189
1214
|
replyPipeline: {},
|
|
1190
1215
|
replyOptions: {
|
|
1216
|
+
...params.turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) : {},
|
|
1191
1217
|
skillFilter: roomConfig?.skills,
|
|
1192
1218
|
disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
|
|
1193
1219
|
},
|
|
@@ -1196,6 +1222,74 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
1196
1222
|
} }
|
|
1197
1223
|
});
|
|
1198
1224
|
}
|
|
1225
|
+
var NextcloudTalkWebhookPayloadError = class extends Error {
|
|
1226
|
+
constructor(message, options) {
|
|
1227
|
+
super(message, options);
|
|
1228
|
+
this.name = "NextcloudTalkWebhookPayloadError";
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
function isRecord(value) {
|
|
1232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1233
|
+
}
|
|
1234
|
+
function parseRawObject(rawEvent) {
|
|
1235
|
+
let parsed;
|
|
1236
|
+
try {
|
|
1237
|
+
parsed = JSON.parse(rawEvent);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook contains invalid JSON.", { cause: error });
|
|
1240
|
+
}
|
|
1241
|
+
if (!isRecord(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
|
|
1242
|
+
return parsed;
|
|
1243
|
+
}
|
|
1244
|
+
function requiredString(value, field) {
|
|
1245
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1246
|
+
throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk webhook is missing ${field}.`);
|
|
1247
|
+
}
|
|
1248
|
+
function inspectNextcloudTalkWebhookEnvelope(rawEvent) {
|
|
1249
|
+
const envelope = parseRawObject(rawEvent);
|
|
1250
|
+
if (envelope.type !== "Create") return null;
|
|
1251
|
+
const object = isRecord(envelope.object) ? envelope.object : null;
|
|
1252
|
+
if (object?.type !== void 0 && object.type !== "Note") return null;
|
|
1253
|
+
if (!object) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook is missing object.");
|
|
1254
|
+
const target = isRecord(envelope.target) ? envelope.target : null;
|
|
1255
|
+
return {
|
|
1256
|
+
eventId: requiredString(object.id, "object.id"),
|
|
1257
|
+
laneKey: `room:${requiredString(target?.id, "target.id")}`
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function parseLegacyReplayKey(key) {
|
|
1261
|
+
const separator = key.lastIndexOf(":");
|
|
1262
|
+
const roomId = key.slice(0, separator).trim();
|
|
1263
|
+
const messageId = key.slice(separator + 1).trim();
|
|
1264
|
+
return separator > 0 && roomId && messageId ? {
|
|
1265
|
+
messageId,
|
|
1266
|
+
roomId
|
|
1267
|
+
} : null;
|
|
1268
|
+
}
|
|
1269
|
+
/** Convert the shipped replay guard's live window into durable completion tombstones. */
|
|
1270
|
+
async function migrateNextcloudTalkLegacyReplayState(params) {
|
|
1271
|
+
const entries = await params.store.entries();
|
|
1272
|
+
let migrated = 0;
|
|
1273
|
+
for (const entry of entries) {
|
|
1274
|
+
const identity = parseLegacyReplayKey(entry.value.key);
|
|
1275
|
+
if (!identity || !Number.isFinite(entry.value.seenAt)) continue;
|
|
1276
|
+
const marker = {
|
|
1277
|
+
version: 1,
|
|
1278
|
+
receivedAt: entry.value.seenAt,
|
|
1279
|
+
rawEvent: ""
|
|
1280
|
+
};
|
|
1281
|
+
const result = await params.queue.enqueue(identity.messageId, marker, {
|
|
1282
|
+
receivedAt: entry.value.seenAt,
|
|
1283
|
+
laneKey: `room:${identity.roomId}`
|
|
1284
|
+
});
|
|
1285
|
+
if (result.kind === "accepted" || result.kind === "pending" && result.record.payload.rawEvent === "") {
|
|
1286
|
+
if (!await params.queue.complete(identity.messageId, { completedAt: entry.value.seenAt })) throw new Error(`Failed to migrate Nextcloud Talk replay key ${entry.value.key}.`);
|
|
1287
|
+
}
|
|
1288
|
+
migrated += 1;
|
|
1289
|
+
}
|
|
1290
|
+
await params.store.clear();
|
|
1291
|
+
return migrated;
|
|
1292
|
+
}
|
|
1199
1293
|
//#endregion
|
|
1200
1294
|
//#region extensions/nextcloud-talk/src/monitor.ts
|
|
1201
1295
|
const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
@@ -1203,34 +1297,6 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
|
|
|
1203
1297
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
1204
1298
|
const HEALTH_PATH = "/healthz";
|
|
1205
1299
|
const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
|
|
1206
|
-
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1207
|
-
type: z.enum([
|
|
1208
|
-
"Create",
|
|
1209
|
-
"Update",
|
|
1210
|
-
"Delete"
|
|
1211
|
-
]),
|
|
1212
|
-
actor: z.object({
|
|
1213
|
-
type: z.literal("Person"),
|
|
1214
|
-
id: z.string().min(1),
|
|
1215
|
-
name: z.string()
|
|
1216
|
-
}),
|
|
1217
|
-
object: z.object({
|
|
1218
|
-
type: z.literal("Note"),
|
|
1219
|
-
id: z.string().min(1),
|
|
1220
|
-
name: z.string(),
|
|
1221
|
-
content: z.string(),
|
|
1222
|
-
mediaType: z.string()
|
|
1223
|
-
}),
|
|
1224
|
-
target: z.object({
|
|
1225
|
-
type: z.literal("Collection"),
|
|
1226
|
-
id: z.string().min(1),
|
|
1227
|
-
name: z.string()
|
|
1228
|
-
})
|
|
1229
|
-
});
|
|
1230
|
-
const NextcloudTalkWebhookEnvelopeSchema = z.object({
|
|
1231
|
-
type: z.string().min(1),
|
|
1232
|
-
object: z.object({ type: z.string().min(1).optional() }).passthrough().optional()
|
|
1233
|
-
});
|
|
1234
1300
|
const WEBHOOK_ERRORS = {
|
|
1235
1301
|
missingSignatureHeaders: "Missing signature headers",
|
|
1236
1302
|
invalidBackend: "Invalid backend",
|
|
@@ -1239,20 +1305,10 @@ const WEBHOOK_ERRORS = {
|
|
|
1239
1305
|
payloadTooLarge: "Payload too large",
|
|
1240
1306
|
internalServerError: "Internal server error"
|
|
1241
1307
|
};
|
|
1242
|
-
async function processNextcloudTalkReplayGuardedMessage(params) {
|
|
1243
|
-
return (await params.replayGuard.processGuarded({
|
|
1244
|
-
accountId: params.accountId,
|
|
1245
|
-
roomToken: params.message.roomToken,
|
|
1246
|
-
messageId: params.message.messageId
|
|
1247
|
-
}, params.handleMessage, { onError: "commit" })).kind === "processed" ? "processed" : "duplicate";
|
|
1248
|
-
}
|
|
1249
1308
|
function formatError(err) {
|
|
1250
1309
|
if (err instanceof Error) return err.message;
|
|
1251
1310
|
return typeof err === "string" ? err : JSON.stringify(err);
|
|
1252
1311
|
}
|
|
1253
|
-
function parseWebhookPayload(body) {
|
|
1254
|
-
return safeParseJsonWithSchema(NextcloudTalkWebhookPayloadSchema, body);
|
|
1255
|
-
}
|
|
1256
1312
|
function writeJsonResponse(res, status, body) {
|
|
1257
1313
|
if (body) {
|
|
1258
1314
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -1292,37 +1348,6 @@ function verifyWebhookSignature(params) {
|
|
|
1292
1348
|
params.authRateLimiter.reset(params.clientIp, WEBHOOK_AUTH_RATE_LIMIT_SCOPE);
|
|
1293
1349
|
return true;
|
|
1294
1350
|
}
|
|
1295
|
-
function decodeWebhookCreateMessage(params) {
|
|
1296
|
-
const envelope = safeParseJsonWithSchema(NextcloudTalkWebhookEnvelopeSchema, params.body);
|
|
1297
|
-
if (!envelope) {
|
|
1298
|
-
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1299
|
-
return { kind: "invalid" };
|
|
1300
|
-
}
|
|
1301
|
-
if (envelope.type !== "Create") return { kind: "ignore" };
|
|
1302
|
-
if (envelope.object?.type && envelope.object.type !== "Note") return { kind: "ignore" };
|
|
1303
|
-
const payload = parseWebhookPayload(params.body);
|
|
1304
|
-
if (!payload) {
|
|
1305
|
-
writeWebhookError(params.res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1306
|
-
return { kind: "invalid" };
|
|
1307
|
-
}
|
|
1308
|
-
return {
|
|
1309
|
-
kind: "message",
|
|
1310
|
-
message: payloadToInboundMessage(payload)
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
function payloadToInboundMessage(payload) {
|
|
1314
|
-
return {
|
|
1315
|
-
messageId: payload.object.id,
|
|
1316
|
-
roomToken: payload.target.id,
|
|
1317
|
-
roomName: payload.target.name,
|
|
1318
|
-
senderId: payload.actor.id,
|
|
1319
|
-
senderName: payload.actor.name ?? "",
|
|
1320
|
-
text: payload.object.content || payload.object.name || "",
|
|
1321
|
-
mediaType: payload.object.mediaType || "text/plain",
|
|
1322
|
-
timestamp: Date.now(),
|
|
1323
|
-
isGroupChat: true
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
1351
|
function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
1327
1352
|
return readRequestBodyWithLimit(req, {
|
|
1328
1353
|
maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
|
|
@@ -1330,12 +1355,10 @@ function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
|
|
|
1330
1355
|
});
|
|
1331
1356
|
}
|
|
1332
1357
|
function createNextcloudTalkWebhookServer(opts) {
|
|
1333
|
-
const { port, host, path, secret,
|
|
1358
|
+
const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
|
|
1334
1359
|
const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
|
|
1335
1360
|
const readBody = opts.readBody ?? readNextcloudTalkWebhookBody;
|
|
1336
1361
|
const isBackendAllowed = opts.isBackendAllowed;
|
|
1337
|
-
const shouldProcessMessage = opts.shouldProcessMessage;
|
|
1338
|
-
const processMessage = opts.processMessage;
|
|
1339
1362
|
const authRateLimitMaxRequests = typeof opts.authRateLimit?.maxRequests === "number" ? opts.authRateLimit.maxRequests : WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests;
|
|
1340
1363
|
const authRateLimitWindowMs = typeof opts.authRateLimit?.windowMs === "number" ? opts.authRateLimit.windowMs : WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs;
|
|
1341
1364
|
const webhookAuthRateLimiter = createAuthRateLimiter({
|
|
@@ -1379,37 +1402,8 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1379
1402
|
clientIp,
|
|
1380
1403
|
authRateLimiter: webhookAuthRateLimiter
|
|
1381
1404
|
})) return;
|
|
1382
|
-
|
|
1383
|
-
body,
|
|
1384
|
-
res
|
|
1385
|
-
});
|
|
1386
|
-
if (decoded.kind === "invalid") return;
|
|
1387
|
-
if (decoded.kind === "ignore") {
|
|
1388
|
-
writeJsonResponse(res, 200);
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
const message = decoded.message;
|
|
1392
|
-
if (processMessage) {
|
|
1393
|
-
writeJsonResponse(res, 200);
|
|
1394
|
-
try {
|
|
1395
|
-
await processMessage(message);
|
|
1396
|
-
} catch (err) {
|
|
1397
|
-
onError?.(err instanceof Error ? err : new Error(formatError(err)));
|
|
1398
|
-
}
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
if (shouldProcessMessage) {
|
|
1402
|
-
if (!await shouldProcessMessage(message)) {
|
|
1403
|
-
writeJsonResponse(res, 200);
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1405
|
+
await onWebhook(body);
|
|
1407
1406
|
writeJsonResponse(res, 200);
|
|
1408
|
-
try {
|
|
1409
|
-
await onMessage(message);
|
|
1410
|
-
} catch (err) {
|
|
1411
|
-
onError?.(err instanceof Error ? err : new Error(formatError(err)));
|
|
1412
|
-
}
|
|
1413
1407
|
} catch (err) {
|
|
1414
1408
|
if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
|
|
1415
1409
|
writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
|
|
@@ -1419,27 +1413,48 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1419
1413
|
writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
|
|
1420
1414
|
return;
|
|
1421
1415
|
}
|
|
1416
|
+
if (err instanceof NextcloudTalkWebhookPayloadError) {
|
|
1417
|
+
writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1422
1420
|
const error = err instanceof Error ? err : new Error(formatError(err));
|
|
1423
1421
|
onError?.(error);
|
|
1424
1422
|
writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError);
|
|
1425
1423
|
}
|
|
1426
1424
|
})();
|
|
1427
1425
|
});
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1426
|
+
let stopRequested = false;
|
|
1427
|
+
let closePromise;
|
|
1428
|
+
const closeIfListening = () => {
|
|
1429
|
+
if (closePromise) return closePromise;
|
|
1430
|
+
if (!server.listening) return Promise.resolve();
|
|
1431
|
+
closePromise = new Promise((resolve) => {
|
|
1432
|
+
server.close(() => resolve());
|
|
1433
|
+
}).finally(() => {
|
|
1434
|
+
closePromise = void 0;
|
|
1431
1435
|
});
|
|
1436
|
+
return closePromise;
|
|
1432
1437
|
};
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1438
|
+
const stop = async () => {
|
|
1439
|
+
stopRequested = true;
|
|
1440
|
+
await closeIfListening();
|
|
1441
|
+
};
|
|
1442
|
+
const start = () => {
|
|
1443
|
+
if (stopRequested) return Promise.resolve();
|
|
1444
|
+
return new Promise((resolve, reject) => {
|
|
1445
|
+
const onListenError = (error) => reject(error);
|
|
1446
|
+
server.once("error", onListenError);
|
|
1447
|
+
server.listen(port, host, () => {
|
|
1448
|
+
server.off("error", onListenError);
|
|
1449
|
+
(async () => {
|
|
1450
|
+
if (stopRequested) await closeIfListening();
|
|
1451
|
+
resolve();
|
|
1452
|
+
})().catch(reject);
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1440
1455
|
};
|
|
1441
1456
|
if (abortSignal) if (abortSignal.aborted) stop();
|
|
1442
|
-
else abortSignal.addEventListener("abort", stop, { once: true });
|
|
1457
|
+
else abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1443
1458
|
return {
|
|
1444
1459
|
server,
|
|
1445
1460
|
start,
|
|
@@ -1447,6 +1462,164 @@ function createNextcloudTalkWebhookServer(opts) {
|
|
|
1447
1462
|
};
|
|
1448
1463
|
}
|
|
1449
1464
|
//#endregion
|
|
1465
|
+
//#region extensions/nextcloud-talk/src/webhook-spool.ts
|
|
1466
|
+
const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
|
|
1467
|
+
const NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
|
|
1468
|
+
const NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
1469
|
+
const NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES = 1e4;
|
|
1470
|
+
const NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
1471
|
+
const NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES = 1e4;
|
|
1472
|
+
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1473
|
+
type: z.enum([
|
|
1474
|
+
"Create",
|
|
1475
|
+
"Update",
|
|
1476
|
+
"Delete"
|
|
1477
|
+
]),
|
|
1478
|
+
actor: z.object({
|
|
1479
|
+
type: z.literal("Person"),
|
|
1480
|
+
id: z.string().min(1),
|
|
1481
|
+
name: z.string()
|
|
1482
|
+
}),
|
|
1483
|
+
object: z.object({
|
|
1484
|
+
type: z.literal("Note"),
|
|
1485
|
+
id: z.string().min(1),
|
|
1486
|
+
name: z.string(),
|
|
1487
|
+
content: z.string(),
|
|
1488
|
+
mediaType: z.string()
|
|
1489
|
+
}),
|
|
1490
|
+
target: z.object({
|
|
1491
|
+
type: z.literal("Collection"),
|
|
1492
|
+
id: z.string().min(1),
|
|
1493
|
+
name: z.string()
|
|
1494
|
+
})
|
|
1495
|
+
});
|
|
1496
|
+
function parseClaimedMessage(payload, claimedId, claimedLaneKey) {
|
|
1497
|
+
if (payload.version !== 1) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has an unsupported version.`);
|
|
1498
|
+
const result = NextcloudTalkWebhookPayloadSchema.safeParse(parseRawObject(payload.rawEvent));
|
|
1499
|
+
if (!result.success || result.data.type !== "Create" || result.data.object.id !== claimedId) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} has invalid message identity.`);
|
|
1500
|
+
const webhook = result.data;
|
|
1501
|
+
const roomId = requiredString(webhook.target.id, "target.id");
|
|
1502
|
+
if (claimedLaneKey !== `room:${roomId}`) throw new NextcloudTalkWebhookPayloadError(`Nextcloud Talk ingress row ${claimedId} changed room identity.`);
|
|
1503
|
+
return {
|
|
1504
|
+
messageId: webhook.object.id,
|
|
1505
|
+
roomToken: roomId,
|
|
1506
|
+
roomName: webhook.target.name,
|
|
1507
|
+
senderId: webhook.actor.id,
|
|
1508
|
+
senderName: webhook.actor.name,
|
|
1509
|
+
text: webhook.object.content || webhook.object.name,
|
|
1510
|
+
mediaType: webhook.object.mediaType || "text/plain",
|
|
1511
|
+
timestamp: payload.receivedAt,
|
|
1512
|
+
isGroupChat: true
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function resolveNonRetryableFailure(error) {
|
|
1516
|
+
if (error instanceof NextcloudTalkWebhookPayloadError) return {
|
|
1517
|
+
reason: "invalid-event",
|
|
1518
|
+
message: error.message
|
|
1519
|
+
};
|
|
1520
|
+
const message = formatErrorMessage(error);
|
|
1521
|
+
if (message.includes("Nextcloud Talk: bot send was rejected") || message.includes("Nextcloud Talk: forbidden")) return {
|
|
1522
|
+
reason: "nextcloud-talk-auth",
|
|
1523
|
+
message
|
|
1524
|
+
};
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
function createNextcloudTalkWebhookSpool(options) {
|
|
1528
|
+
let queue = options.queue;
|
|
1529
|
+
const getQueue = () => {
|
|
1530
|
+
queue ??= getNextcloudTalkRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
|
|
1531
|
+
return queue;
|
|
1532
|
+
};
|
|
1533
|
+
const legacyReplayStore = options.legacyReplayStore === null ? null : options.legacyReplayStore ?? getNextcloudTalkRuntime().state.openKeyedStore({
|
|
1534
|
+
namespace: resolvePersistentDedupePluginStateNamespace({
|
|
1535
|
+
namespace: options.accountId,
|
|
1536
|
+
namespacePrefix: "replay-dedupe"
|
|
1537
|
+
}),
|
|
1538
|
+
maxEntries: 1e4,
|
|
1539
|
+
defaultTtlMs: 864e5
|
|
1540
|
+
});
|
|
1541
|
+
const legacyMigration = legacyReplayStore ? migrateNextcloudTalkLegacyReplayState({
|
|
1542
|
+
queue: getQueue(),
|
|
1543
|
+
store: legacyReplayStore
|
|
1544
|
+
}) : Promise.resolve();
|
|
1545
|
+
const monitor = createChannelIngressMonitor({
|
|
1546
|
+
queue: getQueue,
|
|
1547
|
+
inspect: (rawEvent) => inspectNextcloudTalkWebhookEnvelope(rawEvent),
|
|
1548
|
+
payload: {
|
|
1549
|
+
version: 1,
|
|
1550
|
+
serialize: (rawEvent, { receivedAt }) => ({
|
|
1551
|
+
receivedAt,
|
|
1552
|
+
rawEvent
|
|
1553
|
+
}),
|
|
1554
|
+
deserialize: (body) => body.rawEvent,
|
|
1555
|
+
encode: ({ body }) => ({
|
|
1556
|
+
version: 1,
|
|
1557
|
+
...body
|
|
1558
|
+
}),
|
|
1559
|
+
decode: (payload) => ({
|
|
1560
|
+
version: payload.version,
|
|
1561
|
+
body: {
|
|
1562
|
+
receivedAt: payload.receivedAt,
|
|
1563
|
+
rawEvent: payload.rawEvent
|
|
1564
|
+
}
|
|
1565
|
+
}),
|
|
1566
|
+
createClaimError: (kind, claim) => new NextcloudTalkWebhookPayloadError(kind === "invalid-version" ? `Nextcloud Talk ingress row ${claim.id} has an unsupported version.` : `Nextcloud Talk ingress row ${claim.id} has invalid message identity.`)
|
|
1567
|
+
},
|
|
1568
|
+
deliver: async (_rawEvent, lifecycle, claim) => {
|
|
1569
|
+
const message = parseClaimedMessage(claim.payload, claim.id, claim.laneKey);
|
|
1570
|
+
await options.deliver(message, lifecycle);
|
|
1571
|
+
},
|
|
1572
|
+
pollIntervalMs: options.pollIntervalMs ?? NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS,
|
|
1573
|
+
waitForDeliveryIdleBeforeRepump: true,
|
|
1574
|
+
retention: {
|
|
1575
|
+
pruneIntervalMs: NEXTCLOUD_TALK_INGRESS_PRUNE_INTERVAL_MS,
|
|
1576
|
+
completedTtlMs: NEXTCLOUD_TALK_INGRESS_COMPLETED_TTL_MS,
|
|
1577
|
+
completedMaxEntries: NEXTCLOUD_TALK_INGRESS_COMPLETED_MAX_ENTRIES,
|
|
1578
|
+
failedTtlMs: NEXTCLOUD_TALK_INGRESS_FAILED_TTL_MS,
|
|
1579
|
+
failedMaxEntries: NEXTCLOUD_TALK_INGRESS_FAILED_MAX_ENTRIES
|
|
1580
|
+
},
|
|
1581
|
+
drain: {
|
|
1582
|
+
resolveNonRetryableFailure,
|
|
1583
|
+
...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
|
|
1584
|
+
onLog: (message) => options.runtime.log?.(`nextcloud-talk ${message}`)
|
|
1585
|
+
},
|
|
1586
|
+
...options.abortSignal ? { abortSignal: options.abortSignal } : {},
|
|
1587
|
+
createStoppedError: () => /* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"),
|
|
1588
|
+
onError: (error) => options.runtime.error?.(`nextcloud-talk ingress drain failed: ${formatErrorMessage(error)}`)
|
|
1589
|
+
});
|
|
1590
|
+
let stopping = false;
|
|
1591
|
+
const inFlightReceives = /* @__PURE__ */ new Set();
|
|
1592
|
+
const startAfterMigration = legacyMigration.then(() => {
|
|
1593
|
+
if (!stopping) monitor.start();
|
|
1594
|
+
});
|
|
1595
|
+
return {
|
|
1596
|
+
ready: async () => await startAfterMigration,
|
|
1597
|
+
receive: (rawEvent) => {
|
|
1598
|
+
if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
|
|
1599
|
+
const receiveTask = (async () => {
|
|
1600
|
+
await startAfterMigration;
|
|
1601
|
+
return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
|
|
1602
|
+
})();
|
|
1603
|
+
inFlightReceives.add(receiveTask);
|
|
1604
|
+
receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
|
|
1605
|
+
return receiveTask;
|
|
1606
|
+
},
|
|
1607
|
+
stop: async () => {
|
|
1608
|
+
stopping = true;
|
|
1609
|
+
const pendingReceives = [...inFlightReceives];
|
|
1610
|
+
const pauseTask = monitor.pause();
|
|
1611
|
+
await startAfterMigration.catch(() => void 0);
|
|
1612
|
+
await Promise.allSettled(pendingReceives);
|
|
1613
|
+
await monitor.stop();
|
|
1614
|
+
await pauseTask;
|
|
1615
|
+
},
|
|
1616
|
+
waitForIdle: async () => {
|
|
1617
|
+
await startAfterMigration.catch(() => void 0);
|
|
1618
|
+
await monitor.waitForIdle();
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
//#endregion
|
|
1450
1623
|
//#region extensions/nextcloud-talk/src/monitor-runtime.ts
|
|
1451
1624
|
const DEFAULT_WEBHOOK_PORT = 8788;
|
|
1452
1625
|
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
@@ -1475,13 +1648,29 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1475
1648
|
accountId: account.accountId
|
|
1476
1649
|
});
|
|
1477
1650
|
const expectedBackendOrigin = normalizeOrigin(account.baseUrl);
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1651
|
+
const spool = (opts.createSpool ?? createNextcloudTalkWebhookSpool)({
|
|
1652
|
+
accountId: account.accountId,
|
|
1653
|
+
runtime,
|
|
1654
|
+
abortSignal: opts.abortSignal,
|
|
1655
|
+
deliver: async (message, lifecycle) => {
|
|
1656
|
+
core.channel.activity.record({
|
|
1657
|
+
channel: "nextcloud-talk",
|
|
1658
|
+
accountId: account.accountId,
|
|
1659
|
+
direction: "inbound",
|
|
1660
|
+
at: message.timestamp
|
|
1661
|
+
});
|
|
1662
|
+
if (opts.onMessage) await opts.onMessage(message, lifecycle);
|
|
1663
|
+
else await handleNextcloudTalkInbound({
|
|
1664
|
+
message,
|
|
1665
|
+
account,
|
|
1666
|
+
config: cfg,
|
|
1667
|
+
runtime,
|
|
1668
|
+
statusSink: opts.statusSink,
|
|
1669
|
+
turnAdoptionLifecycle: lifecycle
|
|
1670
|
+
});
|
|
1482
1671
|
}
|
|
1483
1672
|
});
|
|
1484
|
-
const
|
|
1673
|
+
const server = (opts.createServer ?? createNextcloudTalkWebhookServer)({
|
|
1485
1674
|
port,
|
|
1486
1675
|
host,
|
|
1487
1676
|
path,
|
|
@@ -1490,39 +1679,34 @@ async function monitorNextcloudTalkProvider(opts) {
|
|
|
1490
1679
|
if (!expectedBackendOrigin) return true;
|
|
1491
1680
|
return normalizeOrigin(backend) === expectedBackendOrigin;
|
|
1492
1681
|
},
|
|
1493
|
-
|
|
1494
|
-
if (await processNextcloudTalkReplayGuardedMessage({
|
|
1495
|
-
replayGuard,
|
|
1496
|
-
accountId: account.accountId,
|
|
1497
|
-
message,
|
|
1498
|
-
handleMessage: async () => {
|
|
1499
|
-
core.channel.activity.record({
|
|
1500
|
-
channel: "nextcloud-talk",
|
|
1501
|
-
accountId: account.accountId,
|
|
1502
|
-
direction: "inbound",
|
|
1503
|
-
at: message.timestamp
|
|
1504
|
-
});
|
|
1505
|
-
if (opts.onMessage) await opts.onMessage(message);
|
|
1506
|
-
else await handleNextcloudTalkInbound({
|
|
1507
|
-
message,
|
|
1508
|
-
account,
|
|
1509
|
-
config: cfg,
|
|
1510
|
-
runtime,
|
|
1511
|
-
statusSink: opts.statusSink
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
}) === "duplicate") logger.warn(`[nextcloud-talk:${account.accountId}] replayed webhook ignored room=${message.roomToken} messageId=${message.messageId}`);
|
|
1515
|
-
},
|
|
1516
|
-
onMessage: async () => {},
|
|
1682
|
+
onWebhook: spool.receive,
|
|
1517
1683
|
onError: (error) => {
|
|
1518
1684
|
logger.error(`[nextcloud-talk:${account.accountId}] webhook error: ${error.message}`);
|
|
1519
1685
|
},
|
|
1520
1686
|
abortSignal: opts.abortSignal
|
|
1521
1687
|
});
|
|
1522
|
-
|
|
1523
|
-
|
|
1688
|
+
let stopPromise;
|
|
1689
|
+
const stop = () => {
|
|
1690
|
+
stopPromise ??= (async () => {
|
|
1691
|
+
await server.stop();
|
|
1692
|
+
await spool.stop();
|
|
1693
|
+
})();
|
|
1694
|
+
return stopPromise;
|
|
1695
|
+
};
|
|
1696
|
+
if (opts.abortSignal && !opts.abortSignal.aborted) opts.abortSignal.addEventListener("abort", () => void stop(), { once: true });
|
|
1524
1697
|
if (opts.abortSignal?.aborted) {
|
|
1525
|
-
stop();
|
|
1698
|
+
await stop();
|
|
1699
|
+
return { stop };
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
await spool.ready();
|
|
1703
|
+
await server.start();
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
await stop();
|
|
1706
|
+
throw error;
|
|
1707
|
+
}
|
|
1708
|
+
if (opts.abortSignal?.aborted) {
|
|
1709
|
+
await stop();
|
|
1526
1710
|
return { stop };
|
|
1527
1711
|
}
|
|
1528
1712
|
const publicUrl = account.config.webhookPublicUrl ?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${path}`;
|
|
@@ -1540,7 +1724,7 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1540
1724
|
accountId: ctx.accountId,
|
|
1541
1725
|
setStatus: ctx.setStatus
|
|
1542
1726
|
});
|
|
1543
|
-
await
|
|
1727
|
+
await runPassiveAccountLifecycle({
|
|
1544
1728
|
abortSignal: ctx.abortSignal,
|
|
1545
1729
|
start: async () => await monitorNextcloudTalkProvider({
|
|
1546
1730
|
accountId: account.accountId,
|
|
@@ -1548,7 +1732,10 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1548
1732
|
runtime: ctx.runtime,
|
|
1549
1733
|
abortSignal: ctx.abortSignal,
|
|
1550
1734
|
statusSink
|
|
1551
|
-
})
|
|
1735
|
+
}),
|
|
1736
|
+
stop: async (monitor) => {
|
|
1737
|
+
await monitor.stop();
|
|
1738
|
+
}
|
|
1552
1739
|
});
|
|
1553
1740
|
},
|
|
1554
1741
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
@@ -1702,6 +1889,9 @@ function resolveNextcloudTalkOutboundSessionRoute(params) {
|
|
|
1702
1889
|
//#region extensions/nextcloud-talk/src/setup-core.ts
|
|
1703
1890
|
const t$1 = createSetupTranslator$1();
|
|
1704
1891
|
const channel$1 = "nextcloud-talk";
|
|
1892
|
+
function readOptionalString(value) {
|
|
1893
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1894
|
+
}
|
|
1705
1895
|
function addWildcardAllowFrom(allowFrom) {
|
|
1706
1896
|
return mergeAllowFromEntries(allowFrom, ["*"]);
|
|
1707
1897
|
}
|
|
@@ -1820,7 +2010,17 @@ const nextcloudTalkDmPolicy = {
|
|
|
1820
2010
|
promptAllowFrom: promptNextcloudTalkAllowFromForAccount
|
|
1821
2011
|
};
|
|
1822
2012
|
const nextcloudTalkSetupAdapter = {
|
|
2013
|
+
singleAccountKeysToMove: ["rooms"],
|
|
1823
2014
|
resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
|
|
2015
|
+
prepareAccountConfigInput: ({ input }) => {
|
|
2016
|
+
const setupInput = input;
|
|
2017
|
+
return {
|
|
2018
|
+
...setupInput,
|
|
2019
|
+
baseUrl: setupInput.baseUrl ?? readOptionalString(setupInput.url),
|
|
2020
|
+
secret: setupInput.secret ?? readOptionalString(setupInput.token) ?? readOptionalString(setupInput.password),
|
|
2021
|
+
secretFile: setupInput.secretFile ?? readOptionalString(setupInput.tokenFile)
|
|
2022
|
+
};
|
|
2023
|
+
},
|
|
1824
2024
|
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
1825
2025
|
cfg,
|
|
1826
2026
|
channelKey: channel$1,
|
|
@@ -1850,6 +2050,72 @@ const nextcloudTalkSetupAdapter = {
|
|
|
1850
2050
|
});
|
|
1851
2051
|
}
|
|
1852
2052
|
};
|
|
2053
|
+
const nextcloudTalkSetupContract = defineChannelSetupContract({
|
|
2054
|
+
fields: {
|
|
2055
|
+
baseUrl: {
|
|
2056
|
+
kind: "string",
|
|
2057
|
+
cli: {
|
|
2058
|
+
flags: "--base-url <url>",
|
|
2059
|
+
description: "Nextcloud base URL"
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
url: {
|
|
2063
|
+
kind: "string",
|
|
2064
|
+
cli: {
|
|
2065
|
+
flags: "--url <url>",
|
|
2066
|
+
description: "Legacy Nextcloud base URL alias"
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
secret: {
|
|
2070
|
+
kind: "string",
|
|
2071
|
+
sensitive: true,
|
|
2072
|
+
cli: {
|
|
2073
|
+
flags: "--secret <secret>",
|
|
2074
|
+
description: "Nextcloud Talk bot secret"
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
token: {
|
|
2078
|
+
kind: "string",
|
|
2079
|
+
sensitive: true,
|
|
2080
|
+
cli: {
|
|
2081
|
+
flags: "--token <secret>",
|
|
2082
|
+
description: "Legacy Nextcloud bot secret alias"
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
password: {
|
|
2086
|
+
kind: "string",
|
|
2087
|
+
sensitive: true,
|
|
2088
|
+
cli: {
|
|
2089
|
+
flags: "--password <secret>",
|
|
2090
|
+
description: "Legacy Nextcloud bot secret alias"
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
secretFile: {
|
|
2094
|
+
kind: "string",
|
|
2095
|
+
sensitive: true,
|
|
2096
|
+
cli: {
|
|
2097
|
+
flags: "--secret-file <path>",
|
|
2098
|
+
description: "Nextcloud Talk bot secret file"
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
tokenFile: {
|
|
2102
|
+
kind: "string",
|
|
2103
|
+
sensitive: true,
|
|
2104
|
+
cli: {
|
|
2105
|
+
flags: "--token-file <path>",
|
|
2106
|
+
description: "Legacy Nextcloud bot secret file alias"
|
|
2107
|
+
}
|
|
2108
|
+
},
|
|
2109
|
+
useEnv: {
|
|
2110
|
+
kind: "boolean",
|
|
2111
|
+
cli: {
|
|
2112
|
+
flags: "--use-env",
|
|
2113
|
+
description: "Use Nextcloud Talk environment credentials"
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
legacyAdapter: nextcloudTalkSetupAdapter
|
|
2118
|
+
});
|
|
1853
2119
|
//#endregion
|
|
1854
2120
|
//#region extensions/nextcloud-talk/src/setup-surface.ts
|
|
1855
2121
|
const t = createSetupTranslator();
|
|
@@ -2065,6 +2331,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
2065
2331
|
collectRuntimeConfigAssignments
|
|
2066
2332
|
},
|
|
2067
2333
|
setup: nextcloudTalkSetupAdapter,
|
|
2334
|
+
setupContract: nextcloudTalkSetupContract,
|
|
2068
2335
|
status: createComputedAccountStatusAdapter({
|
|
2069
2336
|
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
|
|
2070
2337
|
buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-DtnECxVM.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/nextcloud-talk",
|
|
9
|
-
"version": "2026.7.2-beta.
|
|
9
|
+
"version": "2026.7.2-beta.4",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"zod": "4.4.3"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"openclaw": ">=2026.7.2-beta.
|
|
14
|
+
"openclaw": ">=2026.7.2-beta.4"
|
|
15
15
|
},
|
|
16
16
|
"peerDependenciesMeta": {
|
|
17
17
|
"openclaw": {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.4",
|
|
4
4
|
"description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"openclaw": ">=2026.7.2-beta.
|
|
11
|
+
"openclaw": ">=2026.7.2-beta.4"
|
|
12
12
|
},
|
|
13
13
|
"peerDependenciesMeta": {
|
|
14
14
|
"openclaw": {
|
|
@@ -20,8 +20,19 @@
|
|
|
20
20
|
"./index.ts"
|
|
21
21
|
],
|
|
22
22
|
"setupEntry": "./dist/setup-entry.js",
|
|
23
|
+
"setupFeatures": {
|
|
24
|
+
"configPromotion": true
|
|
25
|
+
},
|
|
23
26
|
"channel": {
|
|
24
27
|
"id": "nextcloud-talk",
|
|
28
|
+
"configuredState": {
|
|
29
|
+
"env": {
|
|
30
|
+
"anyOf": [
|
|
31
|
+
"NEXTCLOUD_TALK_BOT_SECRET",
|
|
32
|
+
"NEXTCLOUD_TALK_API_PASSWORD"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
25
36
|
"label": "Nextcloud Talk",
|
|
26
37
|
"selectionLabel": "Nextcloud Talk (self-hosted)",
|
|
27
38
|
"docsPath": "/channels/nextcloud-talk",
|
|
@@ -32,7 +43,80 @@
|
|
|
32
43
|
"nc"
|
|
33
44
|
],
|
|
34
45
|
"order": 65,
|
|
35
|
-
"quickstartAllowFrom": true
|
|
46
|
+
"quickstartAllowFrom": true,
|
|
47
|
+
"setup": {
|
|
48
|
+
"fields": [
|
|
49
|
+
{
|
|
50
|
+
"key": "baseUrl",
|
|
51
|
+
"kind": "string",
|
|
52
|
+
"cli": {
|
|
53
|
+
"flags": "--base-url <url>",
|
|
54
|
+
"description": "Nextcloud base URL"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"key": "url",
|
|
59
|
+
"kind": "string",
|
|
60
|
+
"cli": {
|
|
61
|
+
"flags": "--url <url>",
|
|
62
|
+
"description": "Legacy Nextcloud base URL alias"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"key": "secret",
|
|
67
|
+
"kind": "string",
|
|
68
|
+
"sensitive": true,
|
|
69
|
+
"cli": {
|
|
70
|
+
"flags": "--secret <secret>",
|
|
71
|
+
"description": "Nextcloud Talk bot secret"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"key": "token",
|
|
76
|
+
"kind": "string",
|
|
77
|
+
"sensitive": true,
|
|
78
|
+
"cli": {
|
|
79
|
+
"flags": "--token <secret>",
|
|
80
|
+
"description": "Legacy Nextcloud bot secret alias"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"key": "password",
|
|
85
|
+
"kind": "string",
|
|
86
|
+
"sensitive": true,
|
|
87
|
+
"cli": {
|
|
88
|
+
"flags": "--password <secret>",
|
|
89
|
+
"description": "Legacy Nextcloud bot secret alias"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"key": "secretFile",
|
|
94
|
+
"kind": "string",
|
|
95
|
+
"sensitive": true,
|
|
96
|
+
"cli": {
|
|
97
|
+
"flags": "--secret-file <path>",
|
|
98
|
+
"description": "Nextcloud Talk bot secret file"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"key": "tokenFile",
|
|
103
|
+
"kind": "string",
|
|
104
|
+
"sensitive": true,
|
|
105
|
+
"cli": {
|
|
106
|
+
"flags": "--token-file <path>",
|
|
107
|
+
"description": "Legacy Nextcloud bot secret file alias"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"key": "useEnv",
|
|
112
|
+
"kind": "boolean",
|
|
113
|
+
"cli": {
|
|
114
|
+
"flags": "--use-env",
|
|
115
|
+
"description": "Use Nextcloud Talk environment credentials"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
36
120
|
},
|
|
37
121
|
"install": {
|
|
38
122
|
"npmSpec": "@openclaw/nextcloud-talk",
|
|
@@ -40,10 +124,10 @@
|
|
|
40
124
|
"minHostVersion": ">=2026.4.10"
|
|
41
125
|
},
|
|
42
126
|
"compat": {
|
|
43
|
-
"pluginApi": ">=2026.7.2-beta.
|
|
127
|
+
"pluginApi": ">=2026.7.2-beta.4"
|
|
44
128
|
},
|
|
45
129
|
"build": {
|
|
46
|
-
"openclawVersion": "2026.7.2-beta.
|
|
130
|
+
"openclawVersion": "2026.7.2-beta.4"
|
|
47
131
|
},
|
|
48
132
|
"release": {
|
|
49
133
|
"publishToClawHub": true,
|