@openclaw/nextcloud-talk 2026.5.7 → 2026.5.9-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +1 -1
- package/dist/{channel-BVVRsVr5.js → channel-DOIJGKcg.js} +341 -127
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +1 -1
- package/dist/{runtime-api-BcCzeRN9.js → runtime-api-Cy4XTpP2.js} +3 -3
- package/dist/runtime-api.js +2 -2
- package/package.json +4 -7
- /package/dist/{doctor-contract-CYlB-4Bf.js → doctor-contract-DIa4Qbi_.js} +0 -0
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-DOIJGKcg.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import {
|
|
1
|
+
import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, f as getNextcloudTalkRuntime, i as deliverFormattedTextWithAttachments, l as resolveDefaultGroupPolicy, n as createChannelMessageReplyPipeline, o as logInboundDrop, r as createChannelPairingController, s as readStoreAllowFromForDmPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as resolveDmGroupAccessWithCommandGate } from "./runtime-api-Cy4XTpP2.js";
|
|
2
|
+
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-DIa4Qbi_.js";
|
|
3
3
|
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-bczDw2-2.js";
|
|
4
4
|
import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
5
5
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
@@ -11,6 +11,13 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
|
|
|
11
11
|
import { convertMarkdownTables, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
12
12
|
import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
13
13
|
import { createResolvedApproverActionAuthAdapter, resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
|
|
14
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
15
|
+
import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
|
|
16
|
+
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
19
|
+
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
20
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
14
21
|
import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
15
22
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
|
|
16
23
|
import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ToolPolicySchema, buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
@@ -18,19 +25,12 @@ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
|
|
|
18
25
|
import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
19
26
|
import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime, runStoppablePassiveMonitor, safeParseJsonWithSchema } from "openclaw/plugin-sdk/extension-shared";
|
|
20
27
|
import { z } from "openclaw/plugin-sdk/zod";
|
|
21
|
-
import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
22
28
|
import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
23
29
|
import os from "node:os";
|
|
24
30
|
import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision } from "openclaw/plugin-sdk/channel-targets";
|
|
25
31
|
import { evaluateMatchedGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
|
|
26
|
-
import { readFileSync } from "node:fs";
|
|
27
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
28
|
-
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
29
|
-
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
30
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
31
32
|
import { createServer } from "node:http";
|
|
32
33
|
import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
|
|
33
|
-
import { z as z$1 } from "zod";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
36
36
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
|
|
@@ -140,6 +140,195 @@ const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
|
|
|
140
140
|
normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value)
|
|
141
141
|
});
|
|
142
142
|
//#endregion
|
|
143
|
+
//#region extensions/nextcloud-talk/src/api-credentials.ts
|
|
144
|
+
function resolveNextcloudTalkApiCredentials(params) {
|
|
145
|
+
const apiUser = params.apiUser?.trim();
|
|
146
|
+
if (!apiUser) return;
|
|
147
|
+
const inlinePassword = normalizeResolvedSecretInputString({
|
|
148
|
+
value: params.apiPassword,
|
|
149
|
+
path: "channels.nextcloud-talk.apiPassword"
|
|
150
|
+
});
|
|
151
|
+
if (inlinePassword) return {
|
|
152
|
+
apiUser,
|
|
153
|
+
apiPassword: inlinePassword
|
|
154
|
+
};
|
|
155
|
+
if (!params.apiPasswordFile) return;
|
|
156
|
+
try {
|
|
157
|
+
const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
|
|
158
|
+
return filePassword ? {
|
|
159
|
+
apiUser,
|
|
160
|
+
apiPassword: filePassword
|
|
161
|
+
} : void 0;
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region extensions/nextcloud-talk/src/signature.ts
|
|
168
|
+
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
|
|
169
|
+
const RANDOM_HEADER = "x-nextcloud-talk-random";
|
|
170
|
+
const BACKEND_HEADER = "x-nextcloud-talk-backend";
|
|
171
|
+
/**
|
|
172
|
+
* Verify the HMAC-SHA256 signature of an incoming webhook request.
|
|
173
|
+
* Signature is calculated as: HMAC-SHA256(random + body, secret)
|
|
174
|
+
*/
|
|
175
|
+
function verifyNextcloudTalkSignature(params) {
|
|
176
|
+
const { signature, random, body, secret } = params;
|
|
177
|
+
if (!signature || !random || !secret) return false;
|
|
178
|
+
const expected = createHmac("sha256", secret).update(random + body).digest("hex");
|
|
179
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
180
|
+
const signatureBuf = Buffer.from(signature, "utf8");
|
|
181
|
+
const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
|
|
182
|
+
const paddedExpected = Buffer.alloc(maxLen);
|
|
183
|
+
const paddedSignature = Buffer.alloc(maxLen);
|
|
184
|
+
expectedBuf.copy(paddedExpected);
|
|
185
|
+
signatureBuf.copy(paddedSignature);
|
|
186
|
+
const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
|
|
187
|
+
return expectedBuf.length === signatureBuf.length && timingResult;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Extract webhook headers from an incoming request.
|
|
191
|
+
*/
|
|
192
|
+
function extractNextcloudTalkHeaders(headers) {
|
|
193
|
+
const getHeader = (name) => {
|
|
194
|
+
const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
|
|
195
|
+
return Array.isArray(value) ? value[0] : value;
|
|
196
|
+
};
|
|
197
|
+
const signature = getHeader(SIGNATURE_HEADER);
|
|
198
|
+
const random = getHeader(RANDOM_HEADER);
|
|
199
|
+
const backend = getHeader(BACKEND_HEADER);
|
|
200
|
+
if (!signature || !random || !backend) return null;
|
|
201
|
+
return {
|
|
202
|
+
signature,
|
|
203
|
+
random,
|
|
204
|
+
backend
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Generate signature headers for an outbound request to Nextcloud Talk.
|
|
209
|
+
*/
|
|
210
|
+
function generateNextcloudTalkSignature(params) {
|
|
211
|
+
const { body, secret } = params;
|
|
212
|
+
const random = randomBytes(32).toString("hex");
|
|
213
|
+
return {
|
|
214
|
+
random,
|
|
215
|
+
signature: createHmac("sha256", secret).update(random + body).digest("hex")
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region extensions/nextcloud-talk/src/bot-preflight.ts
|
|
220
|
+
const BOT_FEATURE_RESPONSE = 2;
|
|
221
|
+
function normalizeUrlForMatch(value) {
|
|
222
|
+
if (!value?.trim()) return "";
|
|
223
|
+
try {
|
|
224
|
+
const url = new URL(value.trim());
|
|
225
|
+
url.hash = "";
|
|
226
|
+
return url.toString().replace(/\/$/, "");
|
|
227
|
+
} catch {
|
|
228
|
+
return value.trim().replace(/\/$/, "");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function coerceFeatureMask(value) {
|
|
232
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
233
|
+
if (typeof value === "string" && value.trim()) {
|
|
234
|
+
const parsed = Number.parseInt(value, 10);
|
|
235
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function formatMissingResponseFeatureMessage(bot, features) {
|
|
239
|
+
const id = bot.id == null ? "unknown" : String(bot.id);
|
|
240
|
+
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.`;
|
|
241
|
+
}
|
|
242
|
+
async function probeNextcloudTalkBotResponseFeature(params) {
|
|
243
|
+
const { account, timeoutMs } = params;
|
|
244
|
+
const baseUrl = account.baseUrl?.trim();
|
|
245
|
+
if (!baseUrl) return {
|
|
246
|
+
ok: true,
|
|
247
|
+
skipped: true,
|
|
248
|
+
code: "missing_base_url",
|
|
249
|
+
message: "Nextcloud Talk bot response feature probe skipped: baseUrl is not configured."
|
|
250
|
+
};
|
|
251
|
+
const webhookUrl = normalizeUrlForMatch(account.config.webhookPublicUrl);
|
|
252
|
+
if (!webhookUrl) return {
|
|
253
|
+
ok: true,
|
|
254
|
+
skipped: true,
|
|
255
|
+
code: "missing_webhook_url",
|
|
256
|
+
message: "Nextcloud Talk bot response feature probe skipped: webhookPublicUrl is not configured."
|
|
257
|
+
};
|
|
258
|
+
const credentials = resolveNextcloudTalkApiCredentials({
|
|
259
|
+
apiUser: account.config.apiUser,
|
|
260
|
+
apiPassword: account.config.apiPassword,
|
|
261
|
+
apiPasswordFile: account.config.apiPasswordFile
|
|
262
|
+
});
|
|
263
|
+
if (!credentials) return {
|
|
264
|
+
ok: true,
|
|
265
|
+
skipped: true,
|
|
266
|
+
code: "missing_api_credentials",
|
|
267
|
+
message: "Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured."
|
|
268
|
+
};
|
|
269
|
+
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/admin`;
|
|
270
|
+
const auth = Buffer.from(`${credentials.apiUser}:${credentials.apiPassword}`, "utf-8").toString("base64");
|
|
271
|
+
try {
|
|
272
|
+
const { response, release } = await fetchWithSsrFGuard({
|
|
273
|
+
url,
|
|
274
|
+
init: {
|
|
275
|
+
method: "GET",
|
|
276
|
+
headers: {
|
|
277
|
+
Authorization: `Basic ${auth}`,
|
|
278
|
+
"OCS-APIRequest": "true",
|
|
279
|
+
Accept: "application/json"
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
auditContext: "nextcloud-talk.bot-response-preflight",
|
|
283
|
+
policy: ssrfPolicyFromPrivateNetworkOptIn$1(account.config),
|
|
284
|
+
timeoutMs
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
const body = await response.text().catch(() => "");
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
code: "api_error",
|
|
292
|
+
status: response.status,
|
|
293
|
+
message: `Nextcloud Talk bot response feature probe failed (${response.status})${body ? `: ${body}` : ""}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const payload = await response.json();
|
|
297
|
+
const bot = (Array.isArray(payload.ocs?.data) ? payload.ocs.data : []).find((entry) => normalizeUrlForMatch(entry.url) === webhookUrl);
|
|
298
|
+
if (!bot) return {
|
|
299
|
+
ok: false,
|
|
300
|
+
code: "bot_not_found",
|
|
301
|
+
message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`
|
|
302
|
+
};
|
|
303
|
+
const features = coerceFeatureMask(bot.features);
|
|
304
|
+
if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) return {
|
|
305
|
+
ok: false,
|
|
306
|
+
code: "missing_response_feature",
|
|
307
|
+
botId: bot.id == null ? void 0 : String(bot.id),
|
|
308
|
+
botName: bot.name,
|
|
309
|
+
features,
|
|
310
|
+
message: formatMissingResponseFeatureMessage(bot, features)
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
code: "ok",
|
|
315
|
+
botId: bot.id == null ? void 0 : String(bot.id),
|
|
316
|
+
botName: bot.name,
|
|
317
|
+
features,
|
|
318
|
+
message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`
|
|
319
|
+
};
|
|
320
|
+
} finally {
|
|
321
|
+
await release();
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
code: "request_failed",
|
|
327
|
+
message: `Nextcloud Talk bot response feature probe failed: ${formatErrorMessage(error)}`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
143
332
|
//#region extensions/nextcloud-talk/src/channel.adapters.ts
|
|
144
333
|
const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
|
|
145
334
|
sectionKey: "nextcloud-talk",
|
|
@@ -229,9 +418,26 @@ const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
|
|
|
229
418
|
});
|
|
230
419
|
//#endregion
|
|
231
420
|
//#region extensions/nextcloud-talk/src/doctor.ts
|
|
421
|
+
async function collectNextcloudTalkBotResponseWarnings(params) {
|
|
422
|
+
const warnings = [];
|
|
423
|
+
for (const accountId of listNextcloudTalkAccountIds(params.cfg)) {
|
|
424
|
+
const account = resolveNextcloudTalkAccount({
|
|
425
|
+
cfg: params.cfg,
|
|
426
|
+
accountId
|
|
427
|
+
});
|
|
428
|
+
if (!account.enabled || !account.secret || !account.baseUrl) continue;
|
|
429
|
+
const result = await probeNextcloudTalkBotResponseFeature({
|
|
430
|
+
account,
|
|
431
|
+
timeoutMs: 5e3
|
|
432
|
+
});
|
|
433
|
+
if (result.code === "missing_response_feature" || result.code === "bot_not_found" || result.code === "api_error" || result.code === "request_failed") warnings.push(`- channels.nextcloud-talk.${account.accountId}: ${result.message}`);
|
|
434
|
+
}
|
|
435
|
+
return warnings;
|
|
436
|
+
}
|
|
232
437
|
const nextcloudTalkDoctor = {
|
|
233
438
|
legacyConfigRules,
|
|
234
|
-
normalizeCompatibilityConfig
|
|
439
|
+
normalizeCompatibilityConfig,
|
|
440
|
+
collectPreviewWarnings: async ({ cfg }) => await collectNextcloudTalkBotResponseWarnings({ cfg })
|
|
235
441
|
};
|
|
236
442
|
//#endregion
|
|
237
443
|
//#region extensions/nextcloud-talk/src/policy.ts
|
|
@@ -338,19 +544,6 @@ const roomCache = /* @__PURE__ */ new Map();
|
|
|
338
544
|
function resolveRoomCacheKey(params) {
|
|
339
545
|
return `${params.accountId}:${params.roomToken}`;
|
|
340
546
|
}
|
|
341
|
-
function readApiPassword(params) {
|
|
342
|
-
const inlinePassword = normalizeResolvedSecretInputString({
|
|
343
|
-
value: params.apiPassword,
|
|
344
|
-
path: "channels.nextcloud-talk.apiPassword"
|
|
345
|
-
});
|
|
346
|
-
if (inlinePassword) return inlinePassword;
|
|
347
|
-
if (!params.apiPasswordFile) return;
|
|
348
|
-
try {
|
|
349
|
-
return readFileSync(params.apiPasswordFile, "utf-8").trim() || void 0;
|
|
350
|
-
} catch {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
547
|
function coerceRoomType(value) {
|
|
355
548
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
356
549
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -375,16 +568,16 @@ async function resolveNextcloudTalkRoomKind(params) {
|
|
|
375
568
|
if (cached.kind && age < ROOM_CACHE_TTL_MS) return cached.kind;
|
|
376
569
|
if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) return;
|
|
377
570
|
}
|
|
378
|
-
const
|
|
379
|
-
|
|
571
|
+
const apiCredentials = resolveNextcloudTalkApiCredentials({
|
|
572
|
+
apiUser: account.config.apiUser,
|
|
380
573
|
apiPassword: account.config.apiPassword,
|
|
381
574
|
apiPasswordFile: account.config.apiPasswordFile
|
|
382
575
|
});
|
|
383
|
-
if (!
|
|
576
|
+
if (!apiCredentials) return;
|
|
384
577
|
const baseUrl = account.baseUrl?.trim();
|
|
385
578
|
if (!baseUrl) return;
|
|
386
579
|
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
|
|
387
|
-
const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
|
|
580
|
+
const auth = Buffer.from(`${apiCredentials.apiUser}:${apiCredentials.apiPassword}`, "utf-8").toString("base64");
|
|
388
581
|
try {
|
|
389
582
|
const { response, release } = await fetchWithSsrFGuard({
|
|
390
583
|
url,
|
|
@@ -450,58 +643,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
|
|
|
450
643
|
return /^[a-z0-9]{8,}$/i.test(trimmed);
|
|
451
644
|
}
|
|
452
645
|
//#endregion
|
|
453
|
-
//#region extensions/nextcloud-talk/src/signature.ts
|
|
454
|
-
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
|
|
455
|
-
const RANDOM_HEADER = "x-nextcloud-talk-random";
|
|
456
|
-
const BACKEND_HEADER = "x-nextcloud-talk-backend";
|
|
457
|
-
/**
|
|
458
|
-
* Verify the HMAC-SHA256 signature of an incoming webhook request.
|
|
459
|
-
* Signature is calculated as: HMAC-SHA256(random + body, secret)
|
|
460
|
-
*/
|
|
461
|
-
function verifyNextcloudTalkSignature(params) {
|
|
462
|
-
const { signature, random, body, secret } = params;
|
|
463
|
-
if (!signature || !random || !secret) return false;
|
|
464
|
-
const expected = createHmac("sha256", secret).update(random + body).digest("hex");
|
|
465
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
466
|
-
const signatureBuf = Buffer.from(signature, "utf8");
|
|
467
|
-
const maxLen = Math.max(expectedBuf.length, signatureBuf.length);
|
|
468
|
-
const paddedExpected = Buffer.alloc(maxLen);
|
|
469
|
-
const paddedSignature = Buffer.alloc(maxLen);
|
|
470
|
-
expectedBuf.copy(paddedExpected);
|
|
471
|
-
signatureBuf.copy(paddedSignature);
|
|
472
|
-
const timingResult = timingSafeEqual(paddedExpected, paddedSignature);
|
|
473
|
-
return expectedBuf.length === signatureBuf.length && timingResult;
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Extract webhook headers from an incoming request.
|
|
477
|
-
*/
|
|
478
|
-
function extractNextcloudTalkHeaders(headers) {
|
|
479
|
-
const getHeader = (name) => {
|
|
480
|
-
const value = headers[name] ?? headers[normalizeLowercaseStringOrEmpty(name)];
|
|
481
|
-
return Array.isArray(value) ? value[0] : value;
|
|
482
|
-
};
|
|
483
|
-
const signature = getHeader(SIGNATURE_HEADER);
|
|
484
|
-
const random = getHeader(RANDOM_HEADER);
|
|
485
|
-
const backend = getHeader(BACKEND_HEADER);
|
|
486
|
-
if (!signature || !random || !backend) return null;
|
|
487
|
-
return {
|
|
488
|
-
signature,
|
|
489
|
-
random,
|
|
490
|
-
backend
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Generate signature headers for an outbound request to Nextcloud Talk.
|
|
495
|
-
*/
|
|
496
|
-
function generateNextcloudTalkSignature(params) {
|
|
497
|
-
const { body, secret } = params;
|
|
498
|
-
const random = randomBytes(32).toString("hex");
|
|
499
|
-
return {
|
|
500
|
-
random,
|
|
501
|
-
signature: createHmac("sha256", secret).update(random + body).digest("hex")
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
//#endregion
|
|
505
646
|
//#region extensions/nextcloud-talk/src/send.ts
|
|
506
647
|
function resolveCredentials(explicit, account) {
|
|
507
648
|
const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
|
|
@@ -546,6 +687,18 @@ function recordNextcloudTalkOutboundActivity(accountId) {
|
|
|
546
687
|
if (!(error instanceof Error) || error.message !== "Nextcloud Talk runtime not initialized") throw error;
|
|
547
688
|
}
|
|
548
689
|
}
|
|
690
|
+
function createNextcloudTalkSendReceipt(params) {
|
|
691
|
+
const messageId = params.messageId.trim();
|
|
692
|
+
return createMessageReceiptFromOutboundResults({
|
|
693
|
+
results: messageId && messageId !== "unknown" ? [{
|
|
694
|
+
channel: "nextcloud-talk",
|
|
695
|
+
messageId,
|
|
696
|
+
conversationId: params.roomToken
|
|
697
|
+
}] : [],
|
|
698
|
+
kind: "text",
|
|
699
|
+
...params.replyTo ? { replyToId: params.replyTo } : {}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
549
702
|
async function sendMessageNextcloudTalk(to, text, opts) {
|
|
550
703
|
const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
|
|
551
704
|
const roomToken = normalizeRoomToken(to);
|
|
@@ -584,7 +737,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
584
737
|
const status = response.status;
|
|
585
738
|
let errorMsg = `Nextcloud Talk send failed (${status})`;
|
|
586
739
|
if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
|
|
587
|
-
else if (status === 401) errorMsg = "Nextcloud Talk:
|
|
740
|
+
else if (status === 401) errorMsg = "Nextcloud Talk: bot send was rejected - check the bot secret and ensure the bot was installed with --feature response";
|
|
588
741
|
else if (status === 403) errorMsg = "Nextcloud Talk: forbidden - bot may not have permission in this room";
|
|
589
742
|
else if (status === 404) errorMsg = `Nextcloud Talk: room not found (token=${roomToken})`;
|
|
590
743
|
else if (errorBody) errorMsg = `Nextcloud Talk send failed: ${errorBody}`;
|
|
@@ -602,6 +755,11 @@ async function sendMessageNextcloudTalk(to, text, opts) {
|
|
|
602
755
|
return {
|
|
603
756
|
messageId,
|
|
604
757
|
roomToken,
|
|
758
|
+
receipt: createNextcloudTalkSendReceipt({
|
|
759
|
+
messageId,
|
|
760
|
+
roomToken,
|
|
761
|
+
...opts.replyTo ? { replyTo: opts.replyTo } : {}
|
|
762
|
+
}),
|
|
605
763
|
timestamp
|
|
606
764
|
};
|
|
607
765
|
} finally {
|
|
@@ -815,33 +973,46 @@ async function handleNextcloudTalkInbound(params) {
|
|
|
815
973
|
OriginatingTo: `nextcloud-talk:${roomToken}`,
|
|
816
974
|
CommandAuthorized: commandAuthorized
|
|
817
975
|
});
|
|
818
|
-
|
|
976
|
+
const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
|
|
819
977
|
cfg: config,
|
|
978
|
+
agentId: route.agentId,
|
|
979
|
+
channel: CHANNEL_ID,
|
|
980
|
+
accountId: account.accountId
|
|
981
|
+
});
|
|
982
|
+
await core.channel.turn.runPrepared({
|
|
820
983
|
channel: CHANNEL_ID,
|
|
821
984
|
accountId: account.accountId,
|
|
822
|
-
route,
|
|
985
|
+
routeSessionKey: route.sessionKey,
|
|
823
986
|
storePath,
|
|
824
987
|
ctxPayload,
|
|
825
|
-
core,
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
988
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
989
|
+
runDispatch: async () => await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
990
|
+
ctx: ctxPayload,
|
|
991
|
+
cfg: config,
|
|
992
|
+
dispatcherOptions: {
|
|
993
|
+
...replyPipeline,
|
|
994
|
+
deliver: async (payload) => {
|
|
995
|
+
await deliverNextcloudTalkReply({
|
|
996
|
+
cfg: config,
|
|
997
|
+
payload,
|
|
998
|
+
roomToken,
|
|
999
|
+
accountId: account.accountId,
|
|
1000
|
+
statusSink
|
|
1001
|
+
});
|
|
1002
|
+
},
|
|
1003
|
+
onError: (err, info) => {
|
|
1004
|
+
runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
replyOptions: {
|
|
1008
|
+
onModelSelected,
|
|
1009
|
+
skillFilter: roomConfig?.skills,
|
|
1010
|
+
disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
|
|
1011
|
+
}
|
|
1012
|
+
}),
|
|
1013
|
+
record: { onRecordError: (err) => {
|
|
836
1014
|
runtime.error?.(`nextcloud-talk: failed updating session meta: ${String(err)}`);
|
|
837
|
-
}
|
|
838
|
-
onDispatchError: (err, info) => {
|
|
839
|
-
runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
|
|
840
|
-
},
|
|
841
|
-
replyOptions: {
|
|
842
|
-
skillFilter: roomConfig?.skills,
|
|
843
|
-
disableBlockStreaming: typeof account.config.blockStreaming === "boolean" ? !account.config.blockStreaming : void 0
|
|
844
|
-
}
|
|
1015
|
+
} }
|
|
845
1016
|
});
|
|
846
1017
|
}
|
|
847
1018
|
//#endregion
|
|
@@ -851,28 +1022,28 @@ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
|
|
|
851
1022
|
const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
852
1023
|
const HEALTH_PATH = "/healthz";
|
|
853
1024
|
const WEBHOOK_AUTH_RATE_LIMIT_SCOPE = "nextcloud-talk-webhook-auth";
|
|
854
|
-
const NextcloudTalkWebhookPayloadSchema = z
|
|
855
|
-
type: z
|
|
1025
|
+
const NextcloudTalkWebhookPayloadSchema = z.object({
|
|
1026
|
+
type: z.enum([
|
|
856
1027
|
"Create",
|
|
857
1028
|
"Update",
|
|
858
1029
|
"Delete"
|
|
859
1030
|
]),
|
|
860
|
-
actor: z
|
|
861
|
-
type: z
|
|
862
|
-
id: z
|
|
863
|
-
name: z
|
|
1031
|
+
actor: z.object({
|
|
1032
|
+
type: z.literal("Person"),
|
|
1033
|
+
id: z.string().min(1),
|
|
1034
|
+
name: z.string()
|
|
864
1035
|
}),
|
|
865
|
-
object: z
|
|
866
|
-
type: z
|
|
867
|
-
id: z
|
|
868
|
-
name: z
|
|
869
|
-
content: z
|
|
870
|
-
mediaType: z
|
|
1036
|
+
object: z.object({
|
|
1037
|
+
type: z.literal("Note"),
|
|
1038
|
+
id: z.string().min(1),
|
|
1039
|
+
name: z.string(),
|
|
1040
|
+
content: z.string(),
|
|
1041
|
+
mediaType: z.string()
|
|
871
1042
|
}),
|
|
872
|
-
target: z
|
|
873
|
-
type: z
|
|
874
|
-
id: z
|
|
875
|
-
name: z
|
|
1043
|
+
target: z.object({
|
|
1044
|
+
type: z.literal("Collection"),
|
|
1045
|
+
id: z.string().min(1),
|
|
1046
|
+
name: z.string()
|
|
876
1047
|
})
|
|
877
1048
|
});
|
|
878
1049
|
const WEBHOOK_ERRORS = {
|
|
@@ -1334,6 +1505,28 @@ const nextcloudTalkGatewayAdapter = {
|
|
|
1334
1505
|
}
|
|
1335
1506
|
};
|
|
1336
1507
|
//#endregion
|
|
1508
|
+
//#region extensions/nextcloud-talk/src/message-adapter.ts
|
|
1509
|
+
const nextcloudTalkMessageAdapter = defineChannelMessageAdapter({
|
|
1510
|
+
id: "nextcloud-talk",
|
|
1511
|
+
durableFinal: { capabilities: {
|
|
1512
|
+
text: true,
|
|
1513
|
+
media: true,
|
|
1514
|
+
replyTo: true
|
|
1515
|
+
} },
|
|
1516
|
+
send: {
|
|
1517
|
+
text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageNextcloudTalk(to, text, {
|
|
1518
|
+
accountId: accountId ?? void 0,
|
|
1519
|
+
replyTo: replyToId ?? void 0,
|
|
1520
|
+
cfg
|
|
1521
|
+
}),
|
|
1522
|
+
media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageNextcloudTalk(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
|
|
1523
|
+
accountId: accountId ?? void 0,
|
|
1524
|
+
replyTo: replyToId ?? void 0,
|
|
1525
|
+
cfg
|
|
1526
|
+
})
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
//#endregion
|
|
1337
1530
|
//#region extensions/nextcloud-talk/src/session-route.ts
|
|
1338
1531
|
function resolveNextcloudTalkOutboundSessionRoute(params) {
|
|
1339
1532
|
const roomId = stripNextcloudTalkTargetPrefix(params.target);
|
|
@@ -1538,7 +1731,7 @@ const nextcloudTalkSetupWizard = {
|
|
|
1538
1731
|
title: "Nextcloud Talk bot setup",
|
|
1539
1732
|
lines: [
|
|
1540
1733
|
"1) SSH into your Nextcloud server",
|
|
1541
|
-
"2) Run: ./occ talk:bot:install \"OpenClaw\" \"<shared-secret>\" \"<webhook-url>\" --feature reaction",
|
|
1734
|
+
"2) Run: ./occ talk:bot:install \"OpenClaw\" \"<shared-secret>\" \"<webhook-url>\" --feature webhook --feature response --feature reaction",
|
|
1542
1735
|
"3) Copy the shared secret you used in the command",
|
|
1543
1736
|
"4) Enable the bot in your Nextcloud Talk room settings",
|
|
1544
1737
|
"Tip: you can also set NEXTCLOUD_TALK_BOT_SECRET in your env.",
|
|
@@ -1740,6 +1933,21 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
1740
1933
|
status: createComputedAccountStatusAdapter({
|
|
1741
1934
|
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$2),
|
|
1742
1935
|
buildChannelSummary: ({ snapshot }) => buildWebhookChannelStatusSummary(snapshot, { secretSource: snapshot.secretSource ?? "none" }),
|
|
1936
|
+
collectStatusIssues: (accounts) => accounts.flatMap((account) => {
|
|
1937
|
+
const probe = account.probe;
|
|
1938
|
+
if (!probe || probe.ok !== false || probe.code !== "missing_response_feature" || !probe.message) return [];
|
|
1939
|
+
return [{
|
|
1940
|
+
channel: "nextcloud-talk",
|
|
1941
|
+
accountId: account.accountId ?? DEFAULT_ACCOUNT_ID$2,
|
|
1942
|
+
kind: "config",
|
|
1943
|
+
message: probe.message,
|
|
1944
|
+
fix: "Add --feature response to the Talk bot."
|
|
1945
|
+
}];
|
|
1946
|
+
}),
|
|
1947
|
+
probeAccount: async ({ account, timeoutMs }) => await probeNextcloudTalkBotResponseFeature({
|
|
1948
|
+
account,
|
|
1949
|
+
timeoutMs
|
|
1950
|
+
}),
|
|
1743
1951
|
resolveAccountSnapshot: ({ account }) => ({
|
|
1744
1952
|
accountId: account.accountId,
|
|
1745
1953
|
name: account.name,
|
|
@@ -1752,7 +1960,8 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
1752
1960
|
}
|
|
1753
1961
|
})
|
|
1754
1962
|
}),
|
|
1755
|
-
gateway: nextcloudTalkGatewayAdapter
|
|
1963
|
+
gateway: nextcloudTalkGatewayAdapter,
|
|
1964
|
+
message: nextcloudTalkMessageAdapter
|
|
1756
1965
|
},
|
|
1757
1966
|
pairing: { text: {
|
|
1758
1967
|
...nextcloudTalkPairingTextAdapter,
|
|
@@ -1771,15 +1980,20 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
|
|
|
1771
1980
|
},
|
|
1772
1981
|
attachedResults: {
|
|
1773
1982
|
channel: "nextcloud-talk",
|
|
1774
|
-
sendText: async ({ cfg, to, text, accountId, replyToId }) => await
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1983
|
+
sendText: async ({ cfg, to, text, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.text({
|
|
1984
|
+
cfg,
|
|
1985
|
+
to,
|
|
1986
|
+
text,
|
|
1987
|
+
accountId,
|
|
1988
|
+
replyToId
|
|
1778
1989
|
}),
|
|
1779
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1990
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await nextcloudTalkMessageAdapter.send.media({
|
|
1991
|
+
cfg,
|
|
1992
|
+
to,
|
|
1993
|
+
text,
|
|
1994
|
+
mediaUrl: mediaUrl ?? "",
|
|
1995
|
+
accountId,
|
|
1996
|
+
replyToId
|
|
1783
1997
|
})
|
|
1784
1998
|
}
|
|
1785
1999
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as nextcloudTalkPlugin } from "./channel-
|
|
1
|
+
import { t as nextcloudTalkPlugin } from "./channel-DOIJGKcg.js";
|
|
2
2
|
export { nextcloudTalkPlugin };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-
|
|
1
|
+
import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract-DIa4Qbi_.js";
|
|
2
2
|
export { legacyConfigRules, normalizeCompatibilityConfig };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
2
2
|
import { readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithCommandGate } from "openclaw/plugin-sdk/channel-policy";
|
|
3
|
-
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
4
3
|
import { logInboundDrop } from "openclaw/plugin-sdk/channel-logging";
|
|
5
4
|
import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
|
|
6
|
-
import {
|
|
5
|
+
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";
|
|
7
6
|
import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
|
|
7
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
8
8
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
9
9
|
//#region extensions/nextcloud-talk/src/runtime.ts
|
|
10
10
|
const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime } = createPluginRuntimeStore({
|
|
@@ -12,4 +12,4 @@ const { setRuntime: setNextcloudTalkRuntime, getRuntime: getNextcloudTalkRuntime
|
|
|
12
12
|
errorMessage: "Nextcloud Talk runtime not initialized"
|
|
13
13
|
});
|
|
14
14
|
//#endregion
|
|
15
|
-
export { fetchWithSsrFGuard as a, resolveAllowlistProviderRuntimeGroupPolicy as c, warnMissingProviderGroupPolicyFallbackOnce as d, getNextcloudTalkRuntime as f,
|
|
15
|
+
export { fetchWithSsrFGuard as a, resolveAllowlistProviderRuntimeGroupPolicy as c, warnMissingProviderGroupPolicyFallbackOnce as d, getNextcloudTalkRuntime as f, deliverFormattedTextWithAttachments as i, resolveDefaultGroupPolicy as l, createChannelMessageReplyPipeline as n, logInboundDrop as o, setNextcloudTalkRuntime as p, createChannelPairingController as r, readStoreAllowFromForDmPolicy as s, GROUP_POLICY_BLOCKED_LABEL as t, resolveDmGroupAccessWithCommandGate as u };
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, i as
|
|
2
|
-
export { GROUP_POLICY_BLOCKED_LABEL, createChannelPairingController, deliverFormattedTextWithAttachments,
|
|
1
|
+
import { a as fetchWithSsrFGuard, c as resolveAllowlistProviderRuntimeGroupPolicy, d as warnMissingProviderGroupPolicyFallbackOnce, i as deliverFormattedTextWithAttachments, l as resolveDefaultGroupPolicy, n as createChannelMessageReplyPipeline, o as logInboundDrop, p as setNextcloudTalkRuntime, r as createChannelPairingController, s as readStoreAllowFromForDmPolicy, t as GROUP_POLICY_BLOCKED_LABEL, u as resolveDmGroupAccessWithCommandGate } from "./runtime-api-Cy4XTpP2.js";
|
|
2
|
+
export { GROUP_POLICY_BLOCKED_LABEL, createChannelMessageReplyPipeline, createChannelPairingController, deliverFormattedTextWithAttachments, fetchWithSsrFGuard, logInboundDrop, readStoreAllowFromForDmPolicy, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, resolveDmGroupAccessWithCommandGate, setNextcloudTalkRuntime, warnMissingProviderGroupPolicyFallbackOnce };
|
package/package.json
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.9-beta.1",
|
|
4
4
|
"description": "OpenClaw Nextcloud Talk channel plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/openclaw/openclaw"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"zod": "^4.4.3"
|
|
12
|
-
},
|
|
13
10
|
"devDependencies": {
|
|
14
11
|
"@openclaw/plugin-sdk": "workspace:*",
|
|
15
12
|
"openclaw": "workspace:*"
|
|
16
13
|
},
|
|
17
14
|
"peerDependencies": {
|
|
18
|
-
"openclaw": ">=2026.5.
|
|
15
|
+
"openclaw": ">=2026.5.9-beta.1"
|
|
19
16
|
},
|
|
20
17
|
"peerDependenciesMeta": {
|
|
21
18
|
"openclaw": {
|
|
@@ -47,10 +44,10 @@
|
|
|
47
44
|
"minHostVersion": ">=2026.4.10"
|
|
48
45
|
},
|
|
49
46
|
"compat": {
|
|
50
|
-
"pluginApi": ">=2026.5.
|
|
47
|
+
"pluginApi": ">=2026.5.9-beta.1"
|
|
51
48
|
},
|
|
52
49
|
"build": {
|
|
53
|
-
"openclawVersion": "2026.5.
|
|
50
|
+
"openclawVersion": "2026.5.9-beta.1"
|
|
54
51
|
},
|
|
55
52
|
"release": {
|
|
56
53
|
"publishToClawHub": true,
|
|
File without changes
|