@openclaw/nextcloud-talk 2026.1.29 → 2026.2.2
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/index.ts +0 -1
- package/openclaw.plugin.json +1 -3
- package/package.json +5 -2
- package/src/accounts.ts +38 -18
- package/src/channel.ts +19 -14
- package/src/format.ts +13 -13
- package/src/inbound.ts +18 -34
- package/src/monitor.ts +8 -8
- package/src/normalize.ts +12 -4
- package/src/onboarding.ts +6 -4
- package/src/policy.ts +19 -6
- package/src/room-info.ts +29 -15
- package/src/send.ts +7 -3
- package/src/signature.ts +9 -4
package/index.ts
CHANGED
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nextcloud-talk",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2026.2.2",
|
|
5
4
|
"description": "OpenClaw Nextcloud Talk channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"openclaw": "workspace:*"
|
|
8
|
+
},
|
|
6
9
|
"openclaw": {
|
|
7
10
|
"extensions": [
|
|
8
11
|
"./index.ts"
|
package/src/accounts.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
4
|
-
|
|
5
3
|
import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
|
|
6
4
|
|
|
7
5
|
const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]);
|
|
8
6
|
|
|
9
7
|
function isTruthyEnvValue(value?: string): boolean {
|
|
10
|
-
if (!value)
|
|
8
|
+
if (!value) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
11
|
return TRUTHY_ENV.has(value.trim().toLowerCase());
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -29,10 +29,14 @@ export type ResolvedNextcloudTalkAccount = {
|
|
|
29
29
|
|
|
30
30
|
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
|
31
31
|
const accounts = cfg.channels?.["nextcloud-talk"]?.accounts;
|
|
32
|
-
if (!accounts || typeof accounts !== "object")
|
|
32
|
+
if (!accounts || typeof accounts !== "object") {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
33
35
|
const ids = new Set<string>();
|
|
34
36
|
for (const key of Object.keys(accounts)) {
|
|
35
|
-
if (!key)
|
|
37
|
+
if (!key) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
36
40
|
ids.add(normalizeAccountId(key));
|
|
37
41
|
}
|
|
38
42
|
return [...ids];
|
|
@@ -41,13 +45,17 @@ function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
|
|
41
45
|
export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
|
|
42
46
|
const ids = listConfiguredAccountIds(cfg);
|
|
43
47
|
debugAccounts("listNextcloudTalkAccountIds", ids);
|
|
44
|
-
if (ids.length === 0)
|
|
45
|
-
|
|
48
|
+
if (ids.length === 0) {
|
|
49
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
50
|
+
}
|
|
51
|
+
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export function resolveDefaultNextcloudTalkAccountId(cfg: CoreConfig): string {
|
|
49
55
|
const ids = listNextcloudTalkAccountIds(cfg);
|
|
50
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID))
|
|
56
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
57
|
+
return DEFAULT_ACCOUNT_ID;
|
|
58
|
+
}
|
|
51
59
|
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -56,9 +64,13 @@ function resolveAccountConfig(
|
|
|
56
64
|
accountId: string,
|
|
57
65
|
): NextcloudTalkAccountConfig | undefined {
|
|
58
66
|
const accounts = cfg.channels?.["nextcloud-talk"]?.accounts;
|
|
59
|
-
if (!accounts || typeof accounts !== "object")
|
|
67
|
+
if (!accounts || typeof accounts !== "object") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
60
70
|
const direct = accounts[accountId] as NextcloudTalkAccountConfig | undefined;
|
|
61
|
-
if (direct)
|
|
71
|
+
if (direct) {
|
|
72
|
+
return direct;
|
|
73
|
+
}
|
|
62
74
|
const normalized = normalizeAccountId(accountId);
|
|
63
75
|
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
|
64
76
|
return matchKey ? (accounts[matchKey] as NextcloudTalkAccountConfig | undefined) : undefined;
|
|
@@ -88,7 +100,9 @@ function resolveNextcloudTalkSecret(
|
|
|
88
100
|
if (merged.botSecretFile) {
|
|
89
101
|
try {
|
|
90
102
|
const fileSecret = readFileSync(merged.botSecretFile, "utf-8").trim();
|
|
91
|
-
if (fileSecret)
|
|
103
|
+
if (fileSecret) {
|
|
104
|
+
return { secret: fileSecret, source: "secretFile" };
|
|
105
|
+
}
|
|
92
106
|
} catch {
|
|
93
107
|
// File not found or unreadable, fall through.
|
|
94
108
|
}
|
|
@@ -135,19 +149,25 @@ export function resolveNextcloudTalkAccount(params: {
|
|
|
135
149
|
|
|
136
150
|
const normalized = normalizeAccountId(params.accountId);
|
|
137
151
|
const primary = resolve(normalized);
|
|
138
|
-
if (hasExplicitAccountId)
|
|
139
|
-
|
|
152
|
+
if (hasExplicitAccountId) {
|
|
153
|
+
return primary;
|
|
154
|
+
}
|
|
155
|
+
if (primary.secretSource !== "none") {
|
|
156
|
+
return primary;
|
|
157
|
+
}
|
|
140
158
|
|
|
141
159
|
const fallbackId = resolveDefaultNextcloudTalkAccountId(params.cfg);
|
|
142
|
-
if (fallbackId === primary.accountId)
|
|
160
|
+
if (fallbackId === primary.accountId) {
|
|
161
|
+
return primary;
|
|
162
|
+
}
|
|
143
163
|
const fallback = resolve(fallbackId);
|
|
144
|
-
if (fallback.secretSource === "none")
|
|
164
|
+
if (fallback.secretSource === "none") {
|
|
165
|
+
return primary;
|
|
166
|
+
}
|
|
145
167
|
return fallback;
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
export function listEnabledNextcloudTalkAccounts(
|
|
149
|
-
cfg: CoreConfig,
|
|
150
|
-
): ResolvedNextcloudTalkAccount[] {
|
|
170
|
+
export function listEnabledNextcloudTalkAccounts(cfg: CoreConfig): ResolvedNextcloudTalkAccount[] {
|
|
151
171
|
return listNextcloudTalkAccountIds(cfg)
|
|
152
172
|
.map((accountId) => resolveNextcloudTalkAccount({ cfg, accountId }))
|
|
153
173
|
.filter((account) => account.enabled);
|
package/src/channel.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type OpenClawConfig,
|
|
11
11
|
type ChannelSetupInput,
|
|
12
12
|
} from "openclaw/plugin-sdk";
|
|
13
|
-
|
|
13
|
+
import type { CoreConfig } from "./types.js";
|
|
14
14
|
import {
|
|
15
15
|
listNextcloudTalkAccountIds,
|
|
16
16
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -19,12 +19,14 @@ import {
|
|
|
19
19
|
} from "./accounts.js";
|
|
20
20
|
import { NextcloudTalkConfigSchema } from "./config-schema.js";
|
|
21
21
|
import { monitorNextcloudTalkProvider } from "./monitor.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
looksLikeNextcloudTalkTargetId,
|
|
24
|
+
normalizeNextcloudTalkMessagingTarget,
|
|
25
|
+
} from "./normalize.js";
|
|
23
26
|
import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
|
|
27
|
+
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
|
24
28
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
25
29
|
import { sendMessageNextcloudTalk } from "./send.js";
|
|
26
|
-
import type { CoreConfig } from "./types.js";
|
|
27
|
-
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
|
28
30
|
|
|
29
31
|
const meta = {
|
|
30
32
|
id: "nextcloud-talk",
|
|
@@ -97,9 +99,9 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
97
99
|
baseUrl: account.baseUrl ? "[set]" : "[missing]",
|
|
98
100
|
}),
|
|
99
101
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
100
|
-
(
|
|
101
|
-
(
|
|
102
|
-
),
|
|
102
|
+
(
|
|
103
|
+
resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []
|
|
104
|
+
).map((entry) => String(entry).toLowerCase()),
|
|
103
105
|
formatAllowFrom: ({ allowFrom }) =>
|
|
104
106
|
allowFrom
|
|
105
107
|
.map((entry) => String(entry).trim())
|
|
@@ -122,14 +124,15 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
122
124
|
policyPath: `${basePath}dmPolicy`,
|
|
123
125
|
allowFromPath: basePath,
|
|
124
126
|
approveHint: formatPairingApproveHint("nextcloud-talk"),
|
|
125
|
-
normalizeEntry: (raw) =>
|
|
126
|
-
raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
|
|
127
|
+
normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
|
|
127
128
|
};
|
|
128
129
|
},
|
|
129
130
|
collectWarnings: ({ account, cfg }) => {
|
|
130
131
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
131
132
|
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
132
|
-
if (groupPolicy !== "open")
|
|
133
|
+
if (groupPolicy !== "open") {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
133
136
|
const roomAllowlistConfigured =
|
|
134
137
|
account.config.rooms && Object.keys(account.config.rooms).length > 0;
|
|
135
138
|
if (roomAllowlistConfigured) {
|
|
@@ -146,7 +149,9 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
146
149
|
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
|
147
150
|
const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
|
|
148
151
|
const rooms = account.config.rooms;
|
|
149
|
-
if (!rooms || !groupId)
|
|
152
|
+
if (!rooms || !groupId) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
150
155
|
|
|
151
156
|
const roomConfig = rooms[groupId];
|
|
152
157
|
if (roomConfig?.requireMention !== undefined) {
|
|
@@ -173,7 +178,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
173
178
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
174
179
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
175
180
|
applyAccountNameToChannelSection({
|
|
176
|
-
cfg: cfg
|
|
181
|
+
cfg: cfg,
|
|
177
182
|
channelKey: "nextcloud-talk",
|
|
178
183
|
accountId,
|
|
179
184
|
name,
|
|
@@ -194,7 +199,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
194
199
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
195
200
|
const setupInput = input as NextcloudSetupInput;
|
|
196
201
|
const namedConfig = applyAccountNameToChannelSection({
|
|
197
|
-
cfg: cfg
|
|
202
|
+
cfg: cfg,
|
|
198
203
|
channelKey: "nextcloud-talk",
|
|
199
204
|
accountId,
|
|
200
205
|
name: setupInput.name,
|
|
@@ -385,7 +390,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
const resolved = resolveNextcloudTalkAccount({
|
|
388
|
-
cfg:
|
|
393
|
+
cfg: changed ? (nextCfg as CoreConfig) : (cfg as CoreConfig),
|
|
389
394
|
accountId,
|
|
390
395
|
});
|
|
391
396
|
const loggedOut = resolved.secretSource === "none";
|
package/src/format.ts
CHANGED
|
@@ -51,25 +51,25 @@ export function formatNextcloudTalkInlineCode(code: string): string {
|
|
|
51
51
|
* Useful for extracting plain text content.
|
|
52
52
|
*/
|
|
53
53
|
export function stripNextcloudTalkFormatting(text: string): string {
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.trim()
|
|
65
|
-
);
|
|
54
|
+
return text
|
|
55
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
56
|
+
.replace(/`[^`]+`/g, "")
|
|
57
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
58
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
59
|
+
.replace(/_([^_]+)_/g, "$1")
|
|
60
|
+
.replace(/~~([^~]+)~~/g, "$1")
|
|
61
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
62
|
+
.replace(/\s+/g, " ")
|
|
63
|
+
.trim();
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
/**
|
|
69
67
|
* Truncate text to a maximum length, preserving word boundaries.
|
|
70
68
|
*/
|
|
71
69
|
export function truncateNextcloudTalkText(text: string, maxLength: number, suffix = "..."): string {
|
|
72
|
-
if (text.length <= maxLength)
|
|
70
|
+
if (text.length <= maxLength) {
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
73
|
const truncated = text.slice(0, maxLength - suffix.length);
|
|
74
74
|
const lastSpace = truncated.lastIndexOf(" ");
|
|
75
75
|
if (lastSpace > maxLength * 0.7) {
|
package/src/inbound.ts
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
type OpenClawConfig,
|
|
5
5
|
type RuntimeEnv,
|
|
6
6
|
} from "openclaw/plugin-sdk";
|
|
7
|
-
|
|
8
7
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
8
|
+
import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js";
|
|
9
9
|
import {
|
|
10
10
|
normalizeNextcloudTalkAllowlist,
|
|
11
11
|
resolveNextcloudTalkAllowlistMatch,
|
|
@@ -15,9 +15,8 @@ import {
|
|
|
15
15
|
resolveNextcloudTalkRoomMatch,
|
|
16
16
|
} from "./policy.js";
|
|
17
17
|
import { resolveNextcloudTalkRoomKind } from "./room-info.js";
|
|
18
|
-
import { sendMessageNextcloudTalk } from "./send.js";
|
|
19
18
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
20
|
-
import
|
|
19
|
+
import { sendMessageNextcloudTalk } from "./send.js";
|
|
21
20
|
|
|
22
21
|
const CHANNEL_ID = "nextcloud-talk" as const;
|
|
23
22
|
|
|
@@ -35,7 +34,9 @@ async function deliverNextcloudTalkReply(params: {
|
|
|
35
34
|
? [payload.mediaUrl]
|
|
36
35
|
: [];
|
|
37
36
|
|
|
38
|
-
if (!text.trim() && mediaList.length === 0)
|
|
37
|
+
if (!text.trim() && mediaList.length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
39
40
|
|
|
40
41
|
const mediaBlock = mediaList.length
|
|
41
42
|
? mediaList.map((url) => `Attachment: ${url}`).join("\n")
|
|
@@ -64,15 +65,16 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
64
65
|
const core = getNextcloudTalkRuntime();
|
|
65
66
|
|
|
66
67
|
const rawBody = message.text?.trim() ?? "";
|
|
67
|
-
if (!rawBody)
|
|
68
|
+
if (!rawBody) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
68
71
|
|
|
69
72
|
const roomKind = await resolveNextcloudTalkRoomKind({
|
|
70
73
|
account,
|
|
71
74
|
roomToken: message.roomToken,
|
|
72
75
|
runtime,
|
|
73
76
|
});
|
|
74
|
-
const isGroup =
|
|
75
|
-
roomKind === "direct" ? false : roomKind === "group" ? true : message.isGroupChat;
|
|
77
|
+
const isGroup = roomKind === "direct" ? false : roomKind === "group" ? true : message.isGroupChat;
|
|
76
78
|
const senderId = message.senderId;
|
|
77
79
|
const senderName = message.senderName;
|
|
78
80
|
const roomToken = message.roomToken;
|
|
@@ -86,9 +88,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
86
88
|
|
|
87
89
|
const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
|
|
88
90
|
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
|
|
89
|
-
const storeAllowFrom = await core.channel.pairing
|
|
90
|
-
.readAllowFromStore(CHANNEL_ID)
|
|
91
|
-
.catch(() => []);
|
|
91
|
+
const storeAllowFrom = await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []);
|
|
92
92
|
const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
|
|
93
93
|
|
|
94
94
|
const roomMatch = resolveNextcloudTalkRoomMatch({
|
|
@@ -123,16 +123,12 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
123
123
|
senderId,
|
|
124
124
|
senderName,
|
|
125
125
|
}).allowed;
|
|
126
|
-
const hasControlCommand = core.channel.text.hasControlCommand(
|
|
127
|
-
rawBody,
|
|
128
|
-
config as OpenClawConfig,
|
|
129
|
-
);
|
|
126
|
+
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig);
|
|
130
127
|
const commandGate = resolveControlCommandGate({
|
|
131
128
|
useAccessGroups,
|
|
132
129
|
authorizers: [
|
|
133
130
|
{
|
|
134
|
-
configured:
|
|
135
|
-
(isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom).length > 0,
|
|
131
|
+
configured: (isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom).length > 0,
|
|
136
132
|
allowed: senderAllowedForCommands,
|
|
137
133
|
},
|
|
138
134
|
],
|
|
@@ -150,9 +146,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
150
146
|
senderName,
|
|
151
147
|
});
|
|
152
148
|
if (!groupAllow.allowed) {
|
|
153
|
-
runtime.log?.(
|
|
154
|
-
`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`,
|
|
155
|
-
);
|
|
149
|
+
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`);
|
|
156
150
|
return;
|
|
157
151
|
}
|
|
158
152
|
} else {
|
|
@@ -192,9 +186,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
192
186
|
}
|
|
193
187
|
}
|
|
194
188
|
}
|
|
195
|
-
runtime.log?.(
|
|
196
|
-
`nextcloud-talk: drop DM sender ${senderId} (dmPolicy=${dmPolicy})`,
|
|
197
|
-
);
|
|
189
|
+
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (dmPolicy=${dmPolicy})`);
|
|
198
190
|
return;
|
|
199
191
|
}
|
|
200
192
|
}
|
|
@@ -210,9 +202,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
210
202
|
return;
|
|
211
203
|
}
|
|
212
204
|
|
|
213
|
-
const mentionRegexes = core.channel.mentions.buildMentionRegexes(
|
|
214
|
-
config as OpenClawConfig,
|
|
215
|
-
);
|
|
205
|
+
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config as OpenClawConfig);
|
|
216
206
|
const wasMentioned = mentionRegexes.length
|
|
217
207
|
? core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes)
|
|
218
208
|
: false;
|
|
@@ -245,15 +235,11 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
245
235
|
},
|
|
246
236
|
});
|
|
247
237
|
|
|
248
|
-
const fromLabel = isGroup
|
|
249
|
-
? `room:${roomName || roomToken}`
|
|
250
|
-
: senderName || `user:${senderId}`;
|
|
238
|
+
const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`;
|
|
251
239
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
252
240
|
agentId: route.agentId,
|
|
253
241
|
});
|
|
254
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(
|
|
255
|
-
config as OpenClawConfig,
|
|
256
|
-
);
|
|
242
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config as OpenClawConfig);
|
|
257
243
|
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
258
244
|
storePath,
|
|
259
245
|
sessionKey: route.sessionKey,
|
|
@@ -320,9 +306,7 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
320
306
|
});
|
|
321
307
|
},
|
|
322
308
|
onError: (err, info) => {
|
|
323
|
-
runtime.error?.(
|
|
324
|
-
`nextcloud-talk ${info.kind} reply failed: ${String(err)}`,
|
|
325
|
-
);
|
|
309
|
+
runtime.error?.(`nextcloud-talk ${info.kind} reply failed: ${String(err)}`);
|
|
326
310
|
},
|
|
327
311
|
},
|
|
328
312
|
replyOptions: {
|
package/src/monitor.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
|
-
|
|
3
1
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
|
-
|
|
5
|
-
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
6
|
-
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
7
|
-
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
8
|
-
import { extractNextcloudTalkHeaders, verifyNextcloudTalkSignature } from "./signature.js";
|
|
2
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
9
3
|
import type {
|
|
10
4
|
CoreConfig,
|
|
11
5
|
NextcloudTalkInboundMessage,
|
|
12
6
|
NextcloudTalkWebhookPayload,
|
|
13
7
|
NextcloudTalkWebhookServerOptions,
|
|
14
8
|
} from "./types.js";
|
|
9
|
+
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
10
|
+
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
11
|
+
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
12
|
+
import { extractNextcloudTalkHeaders, verifyNextcloudTalkSignature } from "./signature.js";
|
|
15
13
|
|
|
16
14
|
const DEFAULT_WEBHOOK_PORT = 8788;
|
|
17
15
|
const DEFAULT_WEBHOOK_HOST = "0.0.0.0";
|
|
@@ -19,7 +17,9 @@ const DEFAULT_WEBHOOK_PATH = "/nextcloud-talk-webhook";
|
|
|
19
17
|
const HEALTH_PATH = "/healthz";
|
|
20
18
|
|
|
21
19
|
function formatError(err: unknown): string {
|
|
22
|
-
if (err instanceof Error)
|
|
20
|
+
if (err instanceof Error) {
|
|
21
|
+
return err.message;
|
|
22
|
+
}
|
|
23
23
|
return typeof err === "string" ? err : JSON.stringify(err);
|
|
24
24
|
}
|
|
25
25
|
|
package/src/normalize.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export function normalizeNextcloudTalkMessagingTarget(raw: string): string | undefined {
|
|
2
2
|
const trimmed = raw.trim();
|
|
3
|
-
if (!trimmed)
|
|
3
|
+
if (!trimmed) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
4
6
|
|
|
5
7
|
let normalized = trimmed;
|
|
6
8
|
|
|
@@ -16,16 +18,22 @@ export function normalizeNextcloudTalkMessagingTarget(raw: string): string | und
|
|
|
16
18
|
normalized = normalized.slice("room:".length).trim();
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
if (!normalized)
|
|
21
|
+
if (!normalized) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
20
24
|
|
|
21
25
|
return `nextcloud-talk:${normalized}`.toLowerCase();
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function looksLikeNextcloudTalkTargetId(raw: string): boolean {
|
|
25
29
|
const trimmed = raw.trim();
|
|
26
|
-
if (!trimmed)
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
27
33
|
|
|
28
|
-
if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed))
|
|
34
|
+
if (/^(nextcloud-talk|nc-talk|nc):/i.test(trimmed)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
return /^[a-z0-9]{8,}$/i.test(trimmed);
|
|
31
39
|
}
|
package/src/onboarding.ts
CHANGED
|
@@ -8,13 +8,12 @@ import {
|
|
|
8
8
|
type ChannelOnboardingDmPolicy,
|
|
9
9
|
type WizardPrompter,
|
|
10
10
|
} from "openclaw/plugin-sdk";
|
|
11
|
-
|
|
11
|
+
import type { CoreConfig, DmPolicy } from "./types.js";
|
|
12
12
|
import {
|
|
13
13
|
listNextcloudTalkAccountIds,
|
|
14
14
|
resolveDefaultNextcloudTalkAccountId,
|
|
15
15
|
resolveNextcloudTalkAccount,
|
|
16
16
|
} from "./accounts.js";
|
|
17
|
-
import type { CoreConfig, DmPolicy } from "./types.js";
|
|
18
17
|
|
|
19
18
|
const channel = "nextcloud-talk" as const;
|
|
20
19
|
|
|
@@ -221,7 +220,9 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
221
220
|
message: "Enter Nextcloud instance URL (e.g., https://cloud.example.com)",
|
|
222
221
|
validate: (value) => {
|
|
223
222
|
const v = String(value ?? "").trim();
|
|
224
|
-
if (!v)
|
|
223
|
+
if (!v) {
|
|
224
|
+
return "Required";
|
|
225
|
+
}
|
|
225
226
|
if (!v.startsWith("http://") && !v.startsWith("https://")) {
|
|
226
227
|
return "URL must start with http:// or https://";
|
|
227
228
|
}
|
|
@@ -309,7 +310,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
309
310
|
...next.channels?.["nextcloud-talk"]?.accounts,
|
|
310
311
|
[accountId]: {
|
|
311
312
|
...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
312
|
-
enabled:
|
|
313
|
+
enabled:
|
|
314
|
+
next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
313
315
|
baseUrl,
|
|
314
316
|
...(secret ? { botSecret: secret } : {}),
|
|
315
317
|
},
|
package/src/policy.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AllowlistMatch,
|
|
3
|
+
ChannelGroupContext,
|
|
4
|
+
GroupPolicy,
|
|
5
|
+
GroupToolPolicyConfig,
|
|
6
|
+
} from "openclaw/plugin-sdk";
|
|
2
7
|
import {
|
|
3
8
|
buildChannelKeyCandidates,
|
|
4
9
|
normalizeChannelSlug,
|
|
@@ -6,11 +11,13 @@ import {
|
|
|
6
11
|
resolveMentionGatingWithBypass,
|
|
7
12
|
resolveNestedAllowlistDecision,
|
|
8
13
|
} from "openclaw/plugin-sdk";
|
|
9
|
-
|
|
10
14
|
import type { NextcloudTalkRoomConfig } from "./types.js";
|
|
11
15
|
|
|
12
16
|
function normalizeAllowEntry(raw: string): string {
|
|
13
|
-
return raw
|
|
17
|
+
return raw
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/^(nextcloud-talk|nc-talk|nc):/i, "");
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export function normalizeNextcloudTalkAllowlist(
|
|
@@ -25,7 +32,9 @@ export function resolveNextcloudTalkAllowlistMatch(params: {
|
|
|
25
32
|
senderName?: string | null;
|
|
26
33
|
}): AllowlistMatch<"wildcard" | "id" | "name"> {
|
|
27
34
|
const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
|
|
28
|
-
if (allowFrom.length === 0)
|
|
35
|
+
if (allowFrom.length === 0) {
|
|
36
|
+
return { allowed: false };
|
|
37
|
+
}
|
|
29
38
|
if (allowFrom.includes("*")) {
|
|
30
39
|
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
31
40
|
}
|
|
@@ -89,9 +98,13 @@ export function resolveNextcloudTalkRoomMatch(params: {
|
|
|
89
98
|
export function resolveNextcloudTalkGroupToolPolicy(
|
|
90
99
|
params: ChannelGroupContext,
|
|
91
100
|
): GroupToolPolicyConfig | undefined {
|
|
92
|
-
const cfg = params.cfg as {
|
|
101
|
+
const cfg = params.cfg as {
|
|
102
|
+
channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } };
|
|
103
|
+
};
|
|
93
104
|
const roomToken = params.groupId?.trim();
|
|
94
|
-
if (!roomToken)
|
|
105
|
+
if (!roomToken) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
95
108
|
const roomName = params.groupChannel?.trim() || undefined;
|
|
96
109
|
const match = resolveNextcloudTalkRoomMatch({
|
|
97
110
|
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
package/src/room-info.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
1
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
|
-
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
5
3
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
6
4
|
|
|
7
5
|
const ROOM_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -20,8 +18,12 @@ function readApiPassword(params: {
|
|
|
20
18
|
apiPassword?: string;
|
|
21
19
|
apiPasswordFile?: string;
|
|
22
20
|
}): string | undefined {
|
|
23
|
-
if (params.apiPassword?.trim())
|
|
24
|
-
|
|
21
|
+
if (params.apiPassword?.trim()) {
|
|
22
|
+
return params.apiPassword.trim();
|
|
23
|
+
}
|
|
24
|
+
if (!params.apiPasswordFile) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
25
27
|
try {
|
|
26
28
|
const value = readFileSync(params.apiPasswordFile, "utf-8").trim();
|
|
27
29
|
return value || undefined;
|
|
@@ -31,7 +33,9 @@ function readApiPassword(params: {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function coerceRoomType(value: unknown): number | undefined {
|
|
34
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
36
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
35
39
|
if (typeof value === "string" && value.trim()) {
|
|
36
40
|
const parsed = Number.parseInt(value, 10);
|
|
37
41
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
@@ -40,8 +44,12 @@ function coerceRoomType(value: unknown): number | undefined {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function resolveRoomKindFromType(type: number | undefined): "direct" | "group" | undefined {
|
|
43
|
-
if (!type)
|
|
44
|
-
|
|
47
|
+
if (!type) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (type === 1 || type === 5 || type === 6) {
|
|
51
|
+
return "direct";
|
|
52
|
+
}
|
|
45
53
|
return "group";
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -55,8 +63,12 @@ export async function resolveNextcloudTalkRoomKind(params: {
|
|
|
55
63
|
const cached = roomCache.get(key);
|
|
56
64
|
if (cached) {
|
|
57
65
|
const age = Date.now() - cached.fetchedAt;
|
|
58
|
-
if (cached.kind && age < ROOM_CACHE_TTL_MS)
|
|
59
|
-
|
|
66
|
+
if (cached.kind && age < ROOM_CACHE_TTL_MS) {
|
|
67
|
+
return cached.kind;
|
|
68
|
+
}
|
|
69
|
+
if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
const apiUser = account.config.apiUser?.trim();
|
|
@@ -64,10 +76,14 @@ export async function resolveNextcloudTalkRoomKind(params: {
|
|
|
64
76
|
apiPassword: account.config.apiPassword,
|
|
65
77
|
apiPasswordFile: account.config.apiPasswordFile,
|
|
66
78
|
});
|
|
67
|
-
if (!apiUser || !apiPassword)
|
|
79
|
+
if (!apiUser || !apiPassword) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
68
82
|
|
|
69
83
|
const baseUrl = account.baseUrl?.trim();
|
|
70
|
-
if (!baseUrl)
|
|
84
|
+
if (!baseUrl) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
71
87
|
|
|
72
88
|
const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
|
|
73
89
|
const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
|
|
@@ -87,9 +103,7 @@ export async function resolveNextcloudTalkRoomKind(params: {
|
|
|
87
103
|
fetchedAt: Date.now(),
|
|
88
104
|
error: `status:${response.status}`,
|
|
89
105
|
});
|
|
90
|
-
runtime?.log?.(
|
|
91
|
-
`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`,
|
|
92
|
-
);
|
|
106
|
+
runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
|
|
93
107
|
return undefined;
|
|
94
108
|
}
|
|
95
109
|
|
package/src/send.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { CoreConfig, NextcloudTalkSendResult } from "./types.js";
|
|
1
2
|
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
2
3
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
3
4
|
import { generateNextcloudTalkSignature } from "./signature.js";
|
|
4
|
-
import type { CoreConfig, NextcloudTalkSendResult } from "./types.js";
|
|
5
5
|
|
|
6
6
|
type NextcloudTalkSendOpts = {
|
|
7
7
|
baseUrl?: string;
|
|
@@ -34,7 +34,9 @@ function resolveCredentials(
|
|
|
34
34
|
|
|
35
35
|
function normalizeRoomToken(to: string): string {
|
|
36
36
|
const trimmed = to.trim();
|
|
37
|
-
if (!trimmed)
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
throw new Error("Room token is required for Nextcloud Talk sends");
|
|
39
|
+
}
|
|
38
40
|
|
|
39
41
|
let normalized = trimmed;
|
|
40
42
|
if (normalized.startsWith("nextcloud-talk:")) {
|
|
@@ -47,7 +49,9 @@ function normalizeRoomToken(to: string): string {
|
|
|
47
49
|
normalized = normalized.slice("room:".length).trim();
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
if (!normalized)
|
|
52
|
+
if (!normalized) {
|
|
53
|
+
throw new Error("Room token is required for Nextcloud Talk sends");
|
|
54
|
+
}
|
|
51
55
|
return normalized;
|
|
52
56
|
}
|
|
53
57
|
|
package/src/signature.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
-
|
|
3
2
|
import type { NextcloudTalkWebhookHeaders } from "./types.js";
|
|
4
3
|
|
|
5
4
|
const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
|
|
@@ -17,13 +16,17 @@ export function verifyNextcloudTalkSignature(params: {
|
|
|
17
16
|
secret: string;
|
|
18
17
|
}): boolean {
|
|
19
18
|
const { signature, random, body, secret } = params;
|
|
20
|
-
if (!signature || !random || !secret)
|
|
19
|
+
if (!signature || !random || !secret) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
21
22
|
|
|
22
23
|
const expected = createHmac("sha256", secret)
|
|
23
24
|
.update(random + body)
|
|
24
25
|
.digest("hex");
|
|
25
26
|
|
|
26
|
-
if (signature.length !== expected.length)
|
|
27
|
+
if (signature.length !== expected.length) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
27
30
|
let result = 0;
|
|
28
31
|
for (let i = 0; i < signature.length; i++) {
|
|
29
32
|
result |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
@@ -46,7 +49,9 @@ export function extractNextcloudTalkHeaders(
|
|
|
46
49
|
const random = getHeader(RANDOM_HEADER);
|
|
47
50
|
const backend = getHeader(BACKEND_HEADER);
|
|
48
51
|
|
|
49
|
-
if (!signature || !random || !backend)
|
|
52
|
+
if (!signature || !random || !backend) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
50
55
|
|
|
51
56
|
return { signature, random, backend };
|
|
52
57
|
}
|