@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.3.12",
3
+ "version": "2026.3.13",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -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 = new AbortController();
44
-
45
- const task = nextcloudTalkPlugin.gateway!.startAccount!(
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 vi.waitFor(() => {
56
- expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
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 runPassiveAccountLifecycle({
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 }) => {
@@ -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
- requireOpenAllowFrom({
52
+ requireChannelOpenAllowFrom({
53
+ channel: "nextcloud-talk",
52
54
  policy: value.dmPolicy,
53
55
  allowFrom: value.allowFrom,
54
56
  ctx,
55
- path: ["allowFrom"],
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
- requireOpenAllowFrom({
66
+ requireChannelOpenAllowFrom({
67
+ channel: "nextcloud-talk",
67
68
  policy: value.dmPolicy,
68
69
  allowFrom: value.allowFrom,
69
70
  ctx,
70
- path: ["allowFrom"],
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
- createLoggerBackedRuntime({
324
- logger: core.logging.getChildLogger(),
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 normalizeNextcloudTalkMessagingTarget(raw: string): string | undefined {
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 `nextcloud-talk:${normalized}`.toLowerCase();
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
- expect(hoisted.loadConfig).not.toHaveBeenCalled();
76
- expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
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
- expect(hoisted.loadConfig).toHaveBeenCalledTimes(1);
99
- expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
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 trimmed = to.trim();
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
- export async function sendMessageNextcloudTalk(
60
- to: string,
61
- text: string,
62
- opts: NextcloudTalkSendOpts = {},
63
- ): Promise<NextcloudTalkSendResult> {
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 cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
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 });