@openclaw/nextcloud-talk 2026.3.12 → 2026.3.13
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/package.json +1 -1
- package/src/channel.startup.test.ts +16 -20
- package/src/channel.ts +2 -5
- package/src/config-schema.ts +7 -8
- package/src/monitor.ts +5 -7
- package/src/normalize.test.ts +28 -0
- package/src/normalize.ts +7 -2
- package/src/send.test.ts +12 -18
- package/src/send.ts +18 -30
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createStartAccountContext } from "../../test-utils/start-account-context.js";
|
|
3
|
+
import {
|
|
4
|
+
expectStopPendingUntilAbort,
|
|
5
|
+
startAccountAndTrackLifecycle,
|
|
6
|
+
} from "../../test-utils/start-account-lifecycle.js";
|
|
3
7
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
4
8
|
|
|
5
9
|
const hoisted = vi.hoisted(() => ({
|
|
@@ -40,28 +44,20 @@ describe("nextcloudTalkPlugin gateway.startAccount", () => {
|
|
|
40
44
|
it("keeps startAccount pending until abort, then stops the monitor", async () => {
|
|
41
45
|
const stop = vi.fn();
|
|
42
46
|
hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
|
|
43
|
-
const abort =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
createStartAccountContext({
|
|
47
|
-
account: buildAccount(),
|
|
48
|
-
abortSignal: abort.signal,
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
let settled = false;
|
|
52
|
-
void task.then(() => {
|
|
53
|
-
settled = true;
|
|
47
|
+
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
|
|
48
|
+
startAccount: nextcloudTalkPlugin.gateway!.startAccount!,
|
|
49
|
+
account: buildAccount(),
|
|
54
50
|
});
|
|
55
|
-
await
|
|
56
|
-
|
|
51
|
+
await expectStopPendingUntilAbort({
|
|
52
|
+
waitForStarted: () =>
|
|
53
|
+
vi.waitFor(() => {
|
|
54
|
+
expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
|
|
55
|
+
}),
|
|
56
|
+
isSettled,
|
|
57
|
+
abort,
|
|
58
|
+
task,
|
|
59
|
+
stop,
|
|
57
60
|
});
|
|
58
|
-
expect(settled).toBe(false);
|
|
59
|
-
expect(stop).not.toHaveBeenCalled();
|
|
60
|
-
|
|
61
|
-
abort.abort();
|
|
62
|
-
await task;
|
|
63
|
-
|
|
64
|
-
expect(stop).toHaveBeenCalledOnce();
|
|
65
61
|
});
|
|
66
62
|
|
|
67
63
|
it("stops immediately when startAccount receives an already-aborted signal", async () => {
|
package/src/channel.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
createAccountStatusSink,
|
|
6
6
|
formatAllowFromLowercase,
|
|
7
7
|
mapAllowFromEntries,
|
|
8
|
-
runPassiveAccountLifecycle,
|
|
9
8
|
} from "openclaw/plugin-sdk/compat";
|
|
10
9
|
import {
|
|
11
10
|
applyAccountNameToChannelSection,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
type OpenClawConfig,
|
|
22
21
|
type ChannelSetupInput,
|
|
23
22
|
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
23
|
+
import { runStoppablePassiveMonitor } from "../../shared/passive-monitor.js";
|
|
24
24
|
import {
|
|
25
25
|
listNextcloudTalkAccountIds,
|
|
26
26
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -344,7 +344,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
344
344
|
setStatus: ctx.setStatus,
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
-
await
|
|
347
|
+
await runStoppablePassiveMonitor({
|
|
348
348
|
abortSignal: ctx.abortSignal,
|
|
349
349
|
start: async () =>
|
|
350
350
|
await monitorNextcloudTalkProvider({
|
|
@@ -354,9 +354,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
354
354
|
abortSignal: ctx.abortSignal,
|
|
355
355
|
statusSink,
|
|
356
356
|
}),
|
|
357
|
-
stop: async (monitor) => {
|
|
358
|
-
monitor.stop();
|
|
359
|
-
},
|
|
360
357
|
});
|
|
361
358
|
},
|
|
362
359
|
logoutAccount: async ({ accountId, cfg }) => {
|
package/src/config-schema.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
requireOpenAllowFrom,
|
|
10
10
|
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import { requireChannelOpenAllowFrom } from "../../shared/config-schema-helpers.js";
|
|
12
13
|
import { buildSecretInputSchema } from "./secret-input.js";
|
|
13
14
|
|
|
14
15
|
export const NextcloudTalkRoomSchema = z
|
|
@@ -48,13 +49,12 @@ export const NextcloudTalkAccountSchemaBase = z
|
|
|
48
49
|
|
|
49
50
|
export const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine(
|
|
50
51
|
(value, ctx) => {
|
|
51
|
-
|
|
52
|
+
requireChannelOpenAllowFrom({
|
|
53
|
+
channel: "nextcloud-talk",
|
|
52
54
|
policy: value.dmPolicy,
|
|
53
55
|
allowFrom: value.allowFrom,
|
|
54
56
|
ctx,
|
|
55
|
-
|
|
56
|
-
message:
|
|
57
|
-
'channels.nextcloud-talk.dmPolicy="open" requires channels.nextcloud-talk.allowFrom to include "*"',
|
|
57
|
+
requireOpenAllowFrom,
|
|
58
58
|
});
|
|
59
59
|
},
|
|
60
60
|
);
|
|
@@ -63,12 +63,11 @@ export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
|
|
|
63
63
|
accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
|
|
64
64
|
defaultAccount: z.string().optional(),
|
|
65
65
|
}).superRefine((value, ctx) => {
|
|
66
|
-
|
|
66
|
+
requireChannelOpenAllowFrom({
|
|
67
|
+
channel: "nextcloud-talk",
|
|
67
68
|
policy: value.dmPolicy,
|
|
68
69
|
allowFrom: value.allowFrom,
|
|
69
70
|
ctx,
|
|
70
|
-
|
|
71
|
-
message:
|
|
72
|
-
'channels.nextcloud-talk.dmPolicy="open" requires channels.nextcloud-talk.allowFrom to include "*"',
|
|
71
|
+
requireOpenAllowFrom,
|
|
73
72
|
});
|
|
74
73
|
});
|
package/src/monitor.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import {
|
|
4
|
-
createLoggerBackedRuntime,
|
|
5
4
|
type RuntimeEnv,
|
|
6
5
|
isRequestBodyLimitError,
|
|
7
6
|
readRequestBodyWithLimit,
|
|
8
7
|
requestBodyErrorToText,
|
|
9
8
|
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
9
|
+
import { resolveLoggerBackedRuntime } from "../../shared/runtime.js";
|
|
10
10
|
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
11
11
|
import { handleNextcloudTalkInbound } from "./inbound.js";
|
|
12
12
|
import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
|
|
@@ -318,12 +318,10 @@ export async function monitorNextcloudTalkProvider(
|
|
|
318
318
|
cfg,
|
|
319
319
|
accountId: opts.accountId,
|
|
320
320
|
});
|
|
321
|
-
const runtime: RuntimeEnv =
|
|
322
|
-
opts.runtime
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
exitError: () => new Error("Runtime exit not available"),
|
|
326
|
-
});
|
|
321
|
+
const runtime: RuntimeEnv = resolveLoggerBackedRuntime(
|
|
322
|
+
opts.runtime,
|
|
323
|
+
core.logging.getChildLogger(),
|
|
324
|
+
);
|
|
327
325
|
|
|
328
326
|
if (!account.secret) {
|
|
329
327
|
throw new Error(`Nextcloud Talk bot secret not configured for account "${account.accountId}"`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
looksLikeNextcloudTalkTargetId,
|
|
4
|
+
normalizeNextcloudTalkMessagingTarget,
|
|
5
|
+
stripNextcloudTalkTargetPrefix,
|
|
6
|
+
} from "./normalize.js";
|
|
7
|
+
|
|
8
|
+
describe("nextcloud-talk target normalization", () => {
|
|
9
|
+
it("strips supported prefixes to a room token", () => {
|
|
10
|
+
expect(stripNextcloudTalkTargetPrefix(" room:abc123 ")).toBe("abc123");
|
|
11
|
+
expect(stripNextcloudTalkTargetPrefix("nextcloud-talk:room:AbC123")).toBe("AbC123");
|
|
12
|
+
expect(stripNextcloudTalkTargetPrefix("nc-talk:room:ops")).toBe("ops");
|
|
13
|
+
expect(stripNextcloudTalkTargetPrefix("nc:room:ops")).toBe("ops");
|
|
14
|
+
expect(stripNextcloudTalkTargetPrefix("room: ")).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("normalizes messaging targets to lowercase channel ids", () => {
|
|
18
|
+
expect(normalizeNextcloudTalkMessagingTarget("room:AbC123")).toBe("nextcloud-talk:abc123");
|
|
19
|
+
expect(normalizeNextcloudTalkMessagingTarget("nc-talk:room:Ops")).toBe("nextcloud-talk:ops");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("detects prefixed and bare room ids", () => {
|
|
23
|
+
expect(looksLikeNextcloudTalkTargetId("nextcloud-talk:room:abc12345")).toBe(true);
|
|
24
|
+
expect(looksLikeNextcloudTalkTargetId("nc:opsroom1")).toBe(true);
|
|
25
|
+
expect(looksLikeNextcloudTalkTargetId("abc12345")).toBe(true);
|
|
26
|
+
expect(looksLikeNextcloudTalkTargetId("")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
package/src/normalize.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function stripNextcloudTalkTargetPrefix(raw: string): string | undefined {
|
|
2
2
|
const trimmed = raw.trim();
|
|
3
3
|
if (!trimmed) {
|
|
4
4
|
return undefined;
|
|
@@ -22,7 +22,12 @@ export function normalizeNextcloudTalkMessagingTarget(raw: string): string | und
|
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
return
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeNextcloudTalkMessagingTarget(raw: string): string | undefined {
|
|
29
|
+
const normalized = stripNextcloudTalkTargetPrefix(raw);
|
|
30
|
+
return normalized ? `nextcloud-talk:${normalized}`.toLowerCase() : undefined;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function looksLikeNextcloudTalkTargetId(raw: string): boolean {
|
package/src/send.test.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createSendCfgThreadingRuntime,
|
|
4
|
+
expectProvidedCfgSkipsRuntimeLoad,
|
|
5
|
+
expectRuntimeCfgFallback,
|
|
6
|
+
} from "../../test-utils/send-config.js";
|
|
2
7
|
|
|
3
8
|
const hoisted = vi.hoisted(() => ({
|
|
4
9
|
loadConfig: vi.fn(),
|
|
@@ -17,20 +22,7 @@ const hoisted = vi.hoisted(() => ({
|
|
|
17
22
|
}));
|
|
18
23
|
|
|
19
24
|
vi.mock("./runtime.js", () => ({
|
|
20
|
-
getNextcloudTalkRuntime: () => (
|
|
21
|
-
config: {
|
|
22
|
-
loadConfig: hoisted.loadConfig,
|
|
23
|
-
},
|
|
24
|
-
channel: {
|
|
25
|
-
text: {
|
|
26
|
-
resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode,
|
|
27
|
-
convertMarkdownTables: hoisted.convertMarkdownTables,
|
|
28
|
-
},
|
|
29
|
-
activity: {
|
|
30
|
-
record: hoisted.record,
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
25
|
+
getNextcloudTalkRuntime: () => createSendCfgThreadingRuntime(hoisted),
|
|
34
26
|
}));
|
|
35
27
|
|
|
36
28
|
vi.mock("./accounts.js", () => ({
|
|
@@ -72,8 +64,9 @@ describe("nextcloud-talk send cfg threading", () => {
|
|
|
72
64
|
accountId: "work",
|
|
73
65
|
});
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
expectProvidedCfgSkipsRuntimeLoad({
|
|
68
|
+
loadConfig: hoisted.loadConfig,
|
|
69
|
+
resolveAccount: hoisted.resolveNextcloudTalkAccount,
|
|
77
70
|
cfg,
|
|
78
71
|
accountId: "work",
|
|
79
72
|
});
|
|
@@ -95,8 +88,9 @@ describe("nextcloud-talk send cfg threading", () => {
|
|
|
95
88
|
});
|
|
96
89
|
|
|
97
90
|
expect(result).toEqual({ ok: true });
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
expectRuntimeCfgFallback({
|
|
92
|
+
loadConfig: hoisted.loadConfig,
|
|
93
|
+
resolveAccount: hoisted.resolveNextcloudTalkAccount,
|
|
100
94
|
cfg: runtimeCfg,
|
|
101
95
|
accountId: "default",
|
|
102
96
|
});
|
package/src/send.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
|
2
|
+
import { stripNextcloudTalkTargetPrefix } from "./normalize.js";
|
|
2
3
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
|
3
4
|
import { generateNextcloudTalkSignature } from "./signature.js";
|
|
4
5
|
import type { CoreConfig, NextcloudTalkSendResult } from "./types.js";
|
|
@@ -34,33 +35,19 @@ function resolveCredentials(
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function normalizeRoomToken(to: string): string {
|
|
37
|
-
const
|
|
38
|
-
if (!trimmed) {
|
|
39
|
-
throw new Error("Room token is required for Nextcloud Talk sends");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let normalized = trimmed;
|
|
43
|
-
if (normalized.startsWith("nextcloud-talk:")) {
|
|
44
|
-
normalized = normalized.slice("nextcloud-talk:".length).trim();
|
|
45
|
-
} else if (normalized.startsWith("nc:")) {
|
|
46
|
-
normalized = normalized.slice("nc:".length).trim();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (normalized.startsWith("room:")) {
|
|
50
|
-
normalized = normalized.slice("room:".length).trim();
|
|
51
|
-
}
|
|
52
|
-
|
|
38
|
+
const normalized = stripNextcloudTalkTargetPrefix(to);
|
|
53
39
|
if (!normalized) {
|
|
54
40
|
throw new Error("Room token is required for Nextcloud Talk sends");
|
|
55
41
|
}
|
|
56
42
|
return normalized;
|
|
57
43
|
}
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
function resolveNextcloudTalkSendContext(opts: NextcloudTalkSendOpts): {
|
|
46
|
+
cfg: CoreConfig;
|
|
47
|
+
account: ReturnType<typeof resolveNextcloudTalkAccount>;
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
secret: string;
|
|
50
|
+
} {
|
|
64
51
|
const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
|
|
65
52
|
const account = resolveNextcloudTalkAccount({
|
|
66
53
|
cfg,
|
|
@@ -70,6 +57,15 @@ export async function sendMessageNextcloudTalk(
|
|
|
70
57
|
{ baseUrl: opts.baseUrl, secret: opts.secret },
|
|
71
58
|
account,
|
|
72
59
|
);
|
|
60
|
+
return { cfg, account, baseUrl, secret };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function sendMessageNextcloudTalk(
|
|
64
|
+
to: string,
|
|
65
|
+
text: string,
|
|
66
|
+
opts: NextcloudTalkSendOpts = {},
|
|
67
|
+
): Promise<NextcloudTalkSendResult> {
|
|
68
|
+
const { cfg, account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
|
|
73
69
|
const roomToken = normalizeRoomToken(to);
|
|
74
70
|
|
|
75
71
|
if (!text?.trim()) {
|
|
@@ -176,15 +172,7 @@ export async function sendReactionNextcloudTalk(
|
|
|
176
172
|
reaction: string,
|
|
177
173
|
opts: Omit<NextcloudTalkSendOpts, "replyTo"> = {},
|
|
178
174
|
): Promise<{ ok: true }> {
|
|
179
|
-
const
|
|
180
|
-
const account = resolveNextcloudTalkAccount({
|
|
181
|
-
cfg,
|
|
182
|
-
accountId: opts.accountId,
|
|
183
|
-
});
|
|
184
|
-
const { baseUrl, secret } = resolveCredentials(
|
|
185
|
-
{ baseUrl: opts.baseUrl, secret: opts.secret },
|
|
186
|
-
account,
|
|
187
|
-
);
|
|
175
|
+
const { account, baseUrl, secret } = resolveNextcloudTalkSendContext(opts);
|
|
188
176
|
const normalizedToken = normalizeRoomToken(roomToken);
|
|
189
177
|
|
|
190
178
|
const body = JSON.stringify({ reaction });
|