@openclaw/nextcloud-talk 2026.3.1 → 2026.3.7
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 +2 -2
- package/package.json +4 -1
- package/src/accounts.ts +26 -59
- package/src/channel.startup.test.ts +9 -41
- package/src/channel.ts +82 -85
- package/src/config-schema.test.ts +36 -0
- package/src/config-schema.ts +4 -3
- package/src/inbound.authz.test.ts +2 -2
- package/src/inbound.ts +31 -50
- package/src/monitor.backend.test.ts +4 -23
- package/src/monitor.replay.test.ts +2 -28
- package/src/monitor.test-fixtures.ts +30 -0
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +126 -145
- package/src/policy.test.ts +106 -1
- package/src/policy.ts +27 -19
- package/src/replay-guard.ts +1 -1
- package/src/room-info.ts +40 -25
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send.test.ts +104 -0
- package/src/send.ts +3 -2
- package/src/types.ts +4 -3
package/src/room-info.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import
|
|
2
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/nextcloud-talk";
|
|
3
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/nextcloud-talk";
|
|
3
4
|
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
5
|
+
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
4
6
|
|
|
5
7
|
const ROOM_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
8
|
const ROOM_CACHE_ERROR_TTL_MS = 30 * 1000;
|
|
@@ -15,11 +17,15 @@ function resolveRoomCacheKey(params: { accountId: string; roomToken: string }) {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function readApiPassword(params: {
|
|
18
|
-
apiPassword?:
|
|
20
|
+
apiPassword?: unknown;
|
|
19
21
|
apiPasswordFile?: string;
|
|
20
22
|
}): string | undefined {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const inlinePassword = normalizeResolvedSecretInputString({
|
|
24
|
+
value: params.apiPassword,
|
|
25
|
+
path: "channels.nextcloud-talk.apiPassword",
|
|
26
|
+
});
|
|
27
|
+
if (inlinePassword) {
|
|
28
|
+
return inlinePassword;
|
|
23
29
|
}
|
|
24
30
|
if (!params.apiPasswordFile) {
|
|
25
31
|
return undefined;
|
|
@@ -89,31 +95,40 @@ export async function resolveNextcloudTalkRoomKind(params: {
|
|
|
89
95
|
const auth = Buffer.from(`${apiUser}:${apiPassword}`, "utf-8").toString("base64");
|
|
90
96
|
|
|
91
97
|
try {
|
|
92
|
-
const response = await
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
const { response, release } = await fetchWithSsrFGuard({
|
|
99
|
+
url,
|
|
100
|
+
init: {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Basic ${auth}`,
|
|
104
|
+
"OCS-APIRequest": "true",
|
|
105
|
+
Accept: "application/json",
|
|
106
|
+
},
|
|
98
107
|
},
|
|
108
|
+
auditContext: "nextcloud-talk.room-info",
|
|
99
109
|
});
|
|
110
|
+
try {
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
roomCache.set(key, {
|
|
113
|
+
fetchedAt: Date.now(),
|
|
114
|
+
error: `status:${response.status}`,
|
|
115
|
+
});
|
|
116
|
+
runtime?.log?.(
|
|
117
|
+
`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`,
|
|
118
|
+
);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
122
|
+
const payload = (await response.json()) as {
|
|
123
|
+
ocs?: { data?: { type?: number | string } };
|
|
124
|
+
};
|
|
125
|
+
const type = coerceRoomType(payload.ocs?.data?.type);
|
|
126
|
+
const kind = resolveRoomKindFromType(type);
|
|
127
|
+
roomCache.set(key, { fetchedAt: Date.now(), kind });
|
|
128
|
+
return kind;
|
|
129
|
+
} finally {
|
|
130
|
+
await release();
|
|
108
131
|
}
|
|
109
|
-
|
|
110
|
-
const payload = (await response.json()) as {
|
|
111
|
-
ocs?: { data?: { type?: number | string } };
|
|
112
|
-
};
|
|
113
|
-
const type = coerceRoomType(payload.ocs?.data?.type);
|
|
114
|
-
const kind = resolveRoomKindFromType(type);
|
|
115
|
-
roomCache.set(key, { fetchedAt: Date.now(), kind });
|
|
116
|
-
return kind;
|
|
117
132
|
} catch (err) {
|
|
118
133
|
roomCache.set(key, {
|
|
119
134
|
fetchedAt: Date.now(),
|
package/src/runtime.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSecretInputSchema,
|
|
3
|
+
hasConfiguredSecretInput,
|
|
4
|
+
normalizeResolvedSecretInputString,
|
|
5
|
+
normalizeSecretInputString,
|
|
6
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
buildSecretInputSchema,
|
|
10
|
+
hasConfiguredSecretInput,
|
|
11
|
+
normalizeResolvedSecretInputString,
|
|
12
|
+
normalizeSecretInputString,
|
|
13
|
+
};
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const hoisted = vi.hoisted(() => ({
|
|
4
|
+
loadConfig: vi.fn(),
|
|
5
|
+
resolveMarkdownTableMode: vi.fn(() => "preserve"),
|
|
6
|
+
convertMarkdownTables: vi.fn((text: string) => text),
|
|
7
|
+
record: vi.fn(),
|
|
8
|
+
resolveNextcloudTalkAccount: vi.fn(() => ({
|
|
9
|
+
accountId: "default",
|
|
10
|
+
baseUrl: "https://nextcloud.example.com",
|
|
11
|
+
secret: "secret-value", // pragma: allowlist secret
|
|
12
|
+
})),
|
|
13
|
+
generateNextcloudTalkSignature: vi.fn(() => ({
|
|
14
|
+
random: "r",
|
|
15
|
+
signature: "s",
|
|
16
|
+
})),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
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
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("./accounts.js", () => ({
|
|
37
|
+
resolveNextcloudTalkAccount: hoisted.resolveNextcloudTalkAccount,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock("./signature.js", () => ({
|
|
41
|
+
generateNextcloudTalkSignature: hoisted.generateNextcloudTalkSignature,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
import { sendMessageNextcloudTalk, sendReactionNextcloudTalk } from "./send.js";
|
|
45
|
+
|
|
46
|
+
describe("nextcloud-talk send cfg threading", () => {
|
|
47
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
fetchMock.mockReset();
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("uses provided cfg for sendMessage and skips runtime loadConfig", async () => {
|
|
60
|
+
const cfg = { source: "provided" } as const;
|
|
61
|
+
fetchMock.mockResolvedValueOnce(
|
|
62
|
+
new Response(
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
ocs: { data: { id: 12345, timestamp: 1_706_000_000 } },
|
|
65
|
+
}),
|
|
66
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await sendMessageNextcloudTalk("room:abc123", "hello", {
|
|
71
|
+
cfg,
|
|
72
|
+
accountId: "work",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(hoisted.loadConfig).not.toHaveBeenCalled();
|
|
76
|
+
expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
|
|
77
|
+
cfg,
|
|
78
|
+
accountId: "work",
|
|
79
|
+
});
|
|
80
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
messageId: "12345",
|
|
83
|
+
roomToken: "abc123",
|
|
84
|
+
timestamp: 1_706_000_000,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("falls back to runtime cfg for sendReaction when cfg is omitted", async () => {
|
|
89
|
+
const runtimeCfg = { source: "runtime" } as const;
|
|
90
|
+
hoisted.loadConfig.mockReturnValueOnce(runtimeCfg);
|
|
91
|
+
fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 }));
|
|
92
|
+
|
|
93
|
+
const result = await sendReactionNextcloudTalk("room:ops", "m-1", "👍", {
|
|
94
|
+
accountId: "default",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({ ok: true });
|
|
98
|
+
expect(hoisted.loadConfig).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
|
|
100
|
+
cfg: runtimeCfg,
|
|
101
|
+
accountId: "default",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/send.ts
CHANGED
|
@@ -9,6 +9,7 @@ type NextcloudTalkSendOpts = {
|
|
|
9
9
|
accountId?: string;
|
|
10
10
|
replyTo?: string;
|
|
11
11
|
verbose?: boolean;
|
|
12
|
+
cfg?: CoreConfig;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
function resolveCredentials(
|
|
@@ -60,7 +61,7 @@ export async function sendMessageNextcloudTalk(
|
|
|
60
61
|
text: string,
|
|
61
62
|
opts: NextcloudTalkSendOpts = {},
|
|
62
63
|
): Promise<NextcloudTalkSendResult> {
|
|
63
|
-
const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig;
|
|
64
|
+
const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
|
|
64
65
|
const account = resolveNextcloudTalkAccount({
|
|
65
66
|
cfg,
|
|
66
67
|
accountId: opts.accountId,
|
|
@@ -175,7 +176,7 @@ export async function sendReactionNextcloudTalk(
|
|
|
175
176
|
reaction: string,
|
|
176
177
|
opts: Omit<NextcloudTalkSendOpts, "replyTo"> = {},
|
|
177
178
|
): Promise<{ ok: true }> {
|
|
178
|
-
const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig;
|
|
179
|
+
const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig;
|
|
179
180
|
const account = resolveNextcloudTalkAccount({
|
|
180
181
|
cfg,
|
|
181
182
|
accountId: opts.accountId,
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type {
|
|
|
3
3
|
DmConfig,
|
|
4
4
|
DmPolicy,
|
|
5
5
|
GroupPolicy,
|
|
6
|
-
|
|
6
|
+
SecretInput,
|
|
7
|
+
} from "openclaw/plugin-sdk/nextcloud-talk";
|
|
7
8
|
|
|
8
9
|
export type { DmPolicy, GroupPolicy };
|
|
9
10
|
|
|
@@ -29,13 +30,13 @@ export type NextcloudTalkAccountConfig = {
|
|
|
29
30
|
/** Base URL of the Nextcloud instance (e.g., "https://cloud.example.com"). */
|
|
30
31
|
baseUrl?: string;
|
|
31
32
|
/** Bot shared secret from occ talk:bot:install output. */
|
|
32
|
-
botSecret?:
|
|
33
|
+
botSecret?: SecretInput;
|
|
33
34
|
/** Path to file containing bot secret (for secret managers). */
|
|
34
35
|
botSecretFile?: string;
|
|
35
36
|
/** Optional API user for room lookups (DM detection). */
|
|
36
37
|
apiUser?: string;
|
|
37
38
|
/** Optional API password/app password for room lookups. */
|
|
38
|
-
apiPassword?:
|
|
39
|
+
apiPassword?: SecretInput;
|
|
39
40
|
/** Path to file containing API password/app password. */
|
|
40
41
|
apiPasswordFile?: string;
|
|
41
42
|
/** Direct message policy (default: pairing). */
|