@openclaw/nextcloud-talk 2026.2.25 → 2026.3.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/package.json +1 -1
- package/src/accounts.ts +44 -36
- package/src/channel.startup.test.ts +83 -0
- package/src/channel.ts +4 -1
- package/src/config-schema.test.ts +36 -0
- package/src/config-schema.ts +4 -2
- package/src/inbound.authz.test.ts +4 -1
- package/src/inbound.ts +60 -61
- 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 +22 -2
- package/src/onboarding.ts +80 -38
- package/src/room-info.ts +39 -24
- package/src/secret-input.ts +19 -0
- package/src/types.ts +5 -2
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
listConfiguredAccountIds as listConfiguredAccountIdsFromSection,
|
|
4
|
+
resolveAccountWithDefaultFallback,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ACCOUNT_ID,
|
|
8
|
+
normalizeAccountId,
|
|
9
|
+
normalizeOptionalAccountId,
|
|
10
|
+
} from "openclaw/plugin-sdk/account-id";
|
|
11
|
+
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
3
12
|
import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
|
|
4
13
|
|
|
5
14
|
function isTruthyEnvValue(value?: string): boolean {
|
|
@@ -24,18 +33,10 @@ export type ResolvedNextcloudTalkAccount = {
|
|
|
24
33
|
};
|
|
25
34
|
|
|
26
35
|
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
const ids = new Set<string>();
|
|
32
|
-
for (const key of Object.keys(accounts)) {
|
|
33
|
-
if (!key) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
ids.add(normalizeAccountId(key));
|
|
37
|
-
}
|
|
38
|
-
return [...ids];
|
|
36
|
+
return listConfiguredAccountIdsFromSection({
|
|
37
|
+
accounts: cfg.channels?.["nextcloud-talk"]?.accounts as Record<string, unknown> | undefined,
|
|
38
|
+
normalizeAccountId,
|
|
39
|
+
});
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
|
|
@@ -48,6 +49,15 @@ export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export function resolveDefaultNextcloudTalkAccountId(cfg: CoreConfig): string {
|
|
52
|
+
const preferred = normalizeOptionalAccountId(cfg.channels?.["nextcloud-talk"]?.defaultAccount);
|
|
53
|
+
if (
|
|
54
|
+
preferred &&
|
|
55
|
+
listNextcloudTalkAccountIds(cfg).some(
|
|
56
|
+
(accountId) => normalizeAccountId(accountId) === preferred,
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
return preferred;
|
|
60
|
+
}
|
|
51
61
|
const ids = listNextcloudTalkAccountIds(cfg);
|
|
52
62
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
53
63
|
return DEFAULT_ACCOUNT_ID;
|
|
@@ -76,8 +86,14 @@ function mergeNextcloudTalkAccountConfig(
|
|
|
76
86
|
cfg: CoreConfig,
|
|
77
87
|
accountId: string,
|
|
78
88
|
): NextcloudTalkAccountConfig {
|
|
79
|
-
const {
|
|
80
|
-
|
|
89
|
+
const {
|
|
90
|
+
accounts: _ignored,
|
|
91
|
+
defaultAccount: _ignoredDefaultAccount,
|
|
92
|
+
...base
|
|
93
|
+
} = (cfg.channels?.["nextcloud-talk"] ?? {}) as NextcloudTalkAccountConfig & {
|
|
94
|
+
accounts?: unknown;
|
|
95
|
+
defaultAccount?: unknown;
|
|
96
|
+
};
|
|
81
97
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
82
98
|
return { ...base, ...account };
|
|
83
99
|
}
|
|
@@ -104,8 +120,12 @@ function resolveNextcloudTalkSecret(
|
|
|
104
120
|
}
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
const inlineSecret = normalizeResolvedSecretInputString({
|
|
124
|
+
value: merged.botSecret,
|
|
125
|
+
path: `channels.nextcloud-talk.accounts.${opts.accountId ?? DEFAULT_ACCOUNT_ID}.botSecret`,
|
|
126
|
+
});
|
|
127
|
+
if (inlineSecret) {
|
|
128
|
+
return { secret: inlineSecret, source: "config" };
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
return { secret: "", source: "none" };
|
|
@@ -115,7 +135,6 @@ export function resolveNextcloudTalkAccount(params: {
|
|
|
115
135
|
cfg: CoreConfig;
|
|
116
136
|
accountId?: string | null;
|
|
117
137
|
}): ResolvedNextcloudTalkAccount {
|
|
118
|
-
const hasExplicitAccountId = Boolean(params.accountId?.trim());
|
|
119
138
|
const baseEnabled = params.cfg.channels?.["nextcloud-talk"]?.enabled !== false;
|
|
120
139
|
|
|
121
140
|
const resolve = (accountId: string) => {
|
|
@@ -143,24 +162,13 @@ export function resolveNextcloudTalkAccount(params: {
|
|
|
143
162
|
} satisfies ResolvedNextcloudTalkAccount;
|
|
144
163
|
};
|
|
145
164
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const fallbackId = resolveDefaultNextcloudTalkAccountId(params.cfg);
|
|
156
|
-
if (fallbackId === primary.accountId) {
|
|
157
|
-
return primary;
|
|
158
|
-
}
|
|
159
|
-
const fallback = resolve(fallbackId);
|
|
160
|
-
if (fallback.secretSource === "none") {
|
|
161
|
-
return primary;
|
|
162
|
-
}
|
|
163
|
-
return fallback;
|
|
165
|
+
return resolveAccountWithDefaultFallback({
|
|
166
|
+
accountId: params.accountId,
|
|
167
|
+
normalizeAccountId,
|
|
168
|
+
resolvePrimary: resolve,
|
|
169
|
+
hasCredential: (account) => account.secretSource !== "none",
|
|
170
|
+
resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg),
|
|
171
|
+
});
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
export function listEnabledNextcloudTalkAccounts(cfg: CoreConfig): ResolvedNextcloudTalkAccount[] {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createStartAccountContext } from "../../test-utils/start-account-context.js";
|
|
3
|
+
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
4
|
+
|
|
5
|
+
const hoisted = vi.hoisted(() => ({
|
|
6
|
+
monitorNextcloudTalkProvider: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("./monitor.js", async () => {
|
|
10
|
+
const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
monitorNextcloudTalkProvider: hoisted.monitorNextcloudTalkProvider,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
import { nextcloudTalkPlugin } from "./channel.js";
|
|
18
|
+
|
|
19
|
+
function buildAccount(): ResolvedNextcloudTalkAccount {
|
|
20
|
+
return {
|
|
21
|
+
accountId: "default",
|
|
22
|
+
enabled: true,
|
|
23
|
+
baseUrl: "https://nextcloud.example.com",
|
|
24
|
+
secret: "secret",
|
|
25
|
+
secretSource: "config",
|
|
26
|
+
config: {
|
|
27
|
+
baseUrl: "https://nextcloud.example.com",
|
|
28
|
+
botSecret: "secret",
|
|
29
|
+
webhookPath: "/nextcloud-talk-webhook",
|
|
30
|
+
webhookPort: 8788,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("nextcloudTalkPlugin gateway.startAccount", () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps startAccount pending until abort, then stops the monitor", async () => {
|
|
41
|
+
const stop = vi.fn();
|
|
42
|
+
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;
|
|
54
|
+
});
|
|
55
|
+
await vi.waitFor(() => {
|
|
56
|
+
expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
|
|
57
|
+
});
|
|
58
|
+
expect(settled).toBe(false);
|
|
59
|
+
expect(stop).not.toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
abort.abort();
|
|
62
|
+
await task;
|
|
63
|
+
|
|
64
|
+
expect(stop).toHaveBeenCalledOnce();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("stops immediately when startAccount receives an already-aborted signal", async () => {
|
|
68
|
+
const stop = vi.fn();
|
|
69
|
+
hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
|
|
70
|
+
const abort = new AbortController();
|
|
71
|
+
abort.abort();
|
|
72
|
+
|
|
73
|
+
await nextcloudTalkPlugin.gateway!.startAccount!(
|
|
74
|
+
createStartAccountContext({
|
|
75
|
+
account: buildAccount(),
|
|
76
|
+
abortSignal: abort.signal,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
|
|
81
|
+
expect(stop).toHaveBeenCalledOnce();
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/channel.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type OpenClawConfig,
|
|
13
13
|
type ChannelSetupInput,
|
|
14
14
|
} from "openclaw/plugin-sdk";
|
|
15
|
+
import { waitForAbortSignal } from "../../../src/infra/abort-signal.js";
|
|
15
16
|
import {
|
|
16
17
|
listNextcloudTalkAccountIds,
|
|
17
18
|
resolveDefaultNextcloudTalkAccountId,
|
|
@@ -332,7 +333,9 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|
|
332
333
|
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
333
334
|
});
|
|
334
335
|
|
|
335
|
-
|
|
336
|
+
// Keep webhook channels pending for the account lifecycle.
|
|
337
|
+
await waitForAbortSignal(ctx.abortSignal);
|
|
338
|
+
stop();
|
|
336
339
|
},
|
|
337
340
|
logoutAccount: async ({ accountId, cfg }) => {
|
|
338
341
|
const nextCfg = { ...cfg } as OpenClawConfig;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { NextcloudTalkConfigSchema } from "./config-schema.js";
|
|
3
|
+
|
|
4
|
+
describe("NextcloudTalkConfigSchema SecretInput", () => {
|
|
5
|
+
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
|
|
6
|
+
const result = NextcloudTalkConfigSchema.safeParse({
|
|
7
|
+
baseUrl: "https://cloud.example.com",
|
|
8
|
+
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
|
|
9
|
+
apiUser: "bot",
|
|
10
|
+
apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
|
|
11
|
+
});
|
|
12
|
+
expect(result.success).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("accepts SecretRef botSecret and apiPassword on account", () => {
|
|
16
|
+
const result = NextcloudTalkConfigSchema.safeParse({
|
|
17
|
+
accounts: {
|
|
18
|
+
main: {
|
|
19
|
+
baseUrl: "https://cloud.example.com",
|
|
20
|
+
botSecret: {
|
|
21
|
+
source: "env",
|
|
22
|
+
provider: "default",
|
|
23
|
+
id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
|
|
24
|
+
},
|
|
25
|
+
apiUser: "bot",
|
|
26
|
+
apiPassword: {
|
|
27
|
+
source: "env",
|
|
28
|
+
provider: "default",
|
|
29
|
+
id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
requireOpenAllowFrom,
|
|
10
10
|
} from "openclaw/plugin-sdk";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import { buildSecretInputSchema } from "./secret-input.js";
|
|
12
13
|
|
|
13
14
|
export const NextcloudTalkRoomSchema = z
|
|
14
15
|
.object({
|
|
@@ -27,10 +28,10 @@ export const NextcloudTalkAccountSchemaBase = z
|
|
|
27
28
|
enabled: z.boolean().optional(),
|
|
28
29
|
markdown: MarkdownConfigSchema,
|
|
29
30
|
baseUrl: z.string().optional(),
|
|
30
|
-
botSecret:
|
|
31
|
+
botSecret: buildSecretInputSchema().optional(),
|
|
31
32
|
botSecretFile: z.string().optional(),
|
|
32
33
|
apiUser: z.string().optional(),
|
|
33
|
-
apiPassword:
|
|
34
|
+
apiPassword: buildSecretInputSchema().optional(),
|
|
34
35
|
apiPasswordFile: z.string().optional(),
|
|
35
36
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
36
37
|
webhookPort: z.number().int().positive().optional(),
|
|
@@ -60,6 +61,7 @@ export const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRe
|
|
|
60
61
|
|
|
61
62
|
export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
|
|
62
63
|
accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
|
|
64
|
+
defaultAccount: z.string().optional(),
|
|
63
65
|
}).superRefine((value, ctx) => {
|
|
64
66
|
requireOpenAllowFrom({
|
|
65
67
|
policy: value.dmPolicy,
|
|
@@ -75,7 +75,10 @@ describe("nextcloud-talk inbound authz", () => {
|
|
|
75
75
|
} as unknown as RuntimeEnv,
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
expect(readAllowFromStore).toHaveBeenCalledWith(
|
|
78
|
+
expect(readAllowFromStore).toHaveBeenCalledWith({
|
|
79
|
+
channel: "nextcloud-talk",
|
|
80
|
+
accountId: "default",
|
|
81
|
+
});
|
|
79
82
|
expect(buildMentionRegexes).not.toHaveBeenCalled();
|
|
80
83
|
});
|
|
81
84
|
});
|
package/src/inbound.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GROUP_POLICY_BLOCKED_LABEL,
|
|
3
|
+
createScopedPairingAccess,
|
|
3
4
|
createNormalizedOutboundDeliverer,
|
|
4
5
|
createReplyPrefixOptions,
|
|
5
6
|
formatTextWithAttachmentLinks,
|
|
6
7
|
logInboundDrop,
|
|
7
|
-
|
|
8
|
+
readStoreAllowFromForDmPolicy,
|
|
9
|
+
resolveDmGroupAccessWithCommandGate,
|
|
8
10
|
resolveOutboundMediaUrls,
|
|
9
11
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
10
12
|
resolveDefaultGroupPolicy,
|
|
@@ -57,6 +59,11 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
57
59
|
}): Promise<void> {
|
|
58
60
|
const { message, account, config, runtime, statusSink } = params;
|
|
59
61
|
const core = getNextcloudTalkRuntime();
|
|
62
|
+
const pairing = createScopedPairingAccess({
|
|
63
|
+
core,
|
|
64
|
+
channel: CHANNEL_ID,
|
|
65
|
+
accountId: account.accountId,
|
|
66
|
+
});
|
|
60
67
|
|
|
61
68
|
const rawBody = message.text?.trim() ?? "";
|
|
62
69
|
if (!rawBody) {
|
|
@@ -96,10 +103,12 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
96
103
|
|
|
97
104
|
const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
|
|
98
105
|
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
|
|
99
|
-
const storeAllowFrom =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
107
|
+
provider: CHANNEL_ID,
|
|
108
|
+
accountId: account.accountId,
|
|
109
|
+
dmPolicy,
|
|
110
|
+
readStore: pairing.readStoreForDmPolicy,
|
|
111
|
+
});
|
|
103
112
|
const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
|
|
104
113
|
|
|
105
114
|
const roomMatch = resolveNextcloudTalkRoomMatch({
|
|
@@ -118,11 +127,6 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
const roomAllowFrom = normalizeNextcloudTalkAllowlist(roomConfig?.allowFrom);
|
|
121
|
-
const baseGroupAllowFrom =
|
|
122
|
-
configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
|
|
123
|
-
|
|
124
|
-
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowList].filter(Boolean);
|
|
125
|
-
const effectiveGroupAllowFrom = [...baseGroupAllowFrom].filter(Boolean);
|
|
126
130
|
|
|
127
131
|
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
128
132
|
cfg: config as OpenClawConfig,
|
|
@@ -130,25 +134,33 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
130
134
|
});
|
|
131
135
|
const useAccessGroups =
|
|
132
136
|
(config.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
|
|
133
|
-
const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({
|
|
134
|
-
allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom,
|
|
135
|
-
senderId,
|
|
136
|
-
}).allowed;
|
|
137
137
|
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
const access = resolveDmGroupAccessWithCommandGate({
|
|
139
|
+
isGroup,
|
|
140
|
+
dmPolicy,
|
|
141
|
+
groupPolicy,
|
|
142
|
+
allowFrom: configAllowFrom,
|
|
143
|
+
groupAllowFrom: configGroupAllowFrom,
|
|
144
|
+
storeAllowFrom: storeAllowList,
|
|
145
|
+
isSenderAllowed: (allowFrom) =>
|
|
146
|
+
resolveNextcloudTalkAllowlistMatch({
|
|
147
|
+
allowFrom,
|
|
148
|
+
senderId,
|
|
149
|
+
}).allowed,
|
|
150
|
+
command: {
|
|
151
|
+
useAccessGroups,
|
|
152
|
+
allowTextCommands,
|
|
153
|
+
hasControlCommand,
|
|
154
|
+
},
|
|
148
155
|
});
|
|
149
|
-
const commandAuthorized =
|
|
156
|
+
const commandAuthorized = access.commandAuthorized;
|
|
157
|
+
const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
|
|
150
158
|
|
|
151
159
|
if (isGroup) {
|
|
160
|
+
if (access.decision !== "allow") {
|
|
161
|
+
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${access.reason})`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
152
164
|
const groupAllow = resolveNextcloudTalkGroupAllow({
|
|
153
165
|
groupPolicy,
|
|
154
166
|
outerAllowFrom: effectiveGroupAllowFrom,
|
|
@@ -160,48 +172,35 @@ export async function handleNextcloudTalkInbound(params: {
|
|
|
160
172
|
return;
|
|
161
173
|
}
|
|
162
174
|
} else {
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
core.channel.pairing.buildPairingReply({
|
|
184
|
-
channel: CHANNEL_ID,
|
|
185
|
-
idLine: `Your Nextcloud user id: ${senderId}`,
|
|
186
|
-
code,
|
|
187
|
-
}),
|
|
188
|
-
{ accountId: account.accountId },
|
|
189
|
-
);
|
|
190
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
191
|
-
} catch (err) {
|
|
192
|
-
runtime.error?.(
|
|
193
|
-
`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
175
|
+
if (access.decision !== "allow") {
|
|
176
|
+
if (access.decision === "pairing") {
|
|
177
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
178
|
+
id: senderId,
|
|
179
|
+
meta: { name: senderName || undefined },
|
|
180
|
+
});
|
|
181
|
+
if (created) {
|
|
182
|
+
try {
|
|
183
|
+
await sendMessageNextcloudTalk(
|
|
184
|
+
roomToken,
|
|
185
|
+
core.channel.pairing.buildPairingReply({
|
|
186
|
+
channel: CHANNEL_ID,
|
|
187
|
+
idLine: `Your Nextcloud user id: ${senderId}`,
|
|
188
|
+
code,
|
|
189
|
+
}),
|
|
190
|
+
{ accountId: account.accountId },
|
|
191
|
+
);
|
|
192
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
|
|
196
195
|
}
|
|
197
196
|
}
|
|
198
|
-
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (dmPolicy=${dmPolicy})`);
|
|
199
|
-
return;
|
|
200
197
|
}
|
|
198
|
+
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
|
|
199
|
+
return;
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
if (
|
|
203
|
+
if (access.shouldBlockControlCommand) {
|
|
205
204
|
logInboundDrop({
|
|
206
205
|
log: (message) => runtime.log?.(message),
|
|
207
206
|
channel: CHANNEL_ID,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
|
|
2
3
|
import { startWebhookServer } from "./monitor.test-harness.js";
|
|
3
|
-
import { generateNextcloudTalkSignature } from "./signature.js";
|
|
4
4
|
|
|
5
5
|
describe("createNextcloudTalkWebhookServer backend allowlist", () => {
|
|
6
6
|
it("rejects requests from unexpected backend origins", async () => {
|
|
@@ -11,31 +11,12 @@ describe("createNextcloudTalkWebhookServer backend allowlist", () => {
|
|
|
11
11
|
onMessage,
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
actor: { type: "Person", id: "alice", name: "Alice" },
|
|
17
|
-
object: {
|
|
18
|
-
type: "Note",
|
|
19
|
-
id: "msg-1",
|
|
20
|
-
name: "hello",
|
|
21
|
-
content: "hello",
|
|
22
|
-
mediaType: "text/plain",
|
|
23
|
-
},
|
|
24
|
-
target: { type: "Collection", id: "room-1", name: "Room 1" },
|
|
25
|
-
};
|
|
26
|
-
const body = JSON.stringify(payload);
|
|
27
|
-
const { random, signature } = generateNextcloudTalkSignature({
|
|
28
|
-
body,
|
|
29
|
-
secret: "nextcloud-secret",
|
|
14
|
+
const { body, headers } = createSignedCreateMessageRequest({
|
|
15
|
+
backend: "https://nextcloud.unexpected",
|
|
30
16
|
});
|
|
31
17
|
const response = await fetch(harness.webhookUrl, {
|
|
32
18
|
method: "POST",
|
|
33
|
-
headers
|
|
34
|
-
"content-type": "application/json",
|
|
35
|
-
"x-nextcloud-talk-random": random,
|
|
36
|
-
"x-nextcloud-talk-signature": signature,
|
|
37
|
-
"x-nextcloud-talk-backend": "https://nextcloud.unexpected",
|
|
38
|
-
},
|
|
19
|
+
headers,
|
|
39
20
|
body,
|
|
40
21
|
});
|
|
41
22
|
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createSignedCreateMessageRequest } from "./monitor.test-fixtures.js";
|
|
2
3
|
import { startWebhookServer } from "./monitor.test-harness.js";
|
|
3
|
-
import { generateNextcloudTalkSignature } from "./signature.js";
|
|
4
4
|
import type { NextcloudTalkInboundMessage } from "./types.js";
|
|
5
5
|
|
|
6
|
-
function createSignedRequest(body: string): { random: string; signature: string } {
|
|
7
|
-
return generateNextcloudTalkSignature({
|
|
8
|
-
body,
|
|
9
|
-
secret: "nextcloud-secret",
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
6
|
describe("createNextcloudTalkWebhookServer replay handling", () => {
|
|
14
7
|
it("acknowledges replayed requests and skips onMessage side effects", async () => {
|
|
15
8
|
const seen = new Set<string>();
|
|
@@ -27,26 +20,7 @@ describe("createNextcloudTalkWebhookServer replay handling", () => {
|
|
|
27
20
|
onMessage,
|
|
28
21
|
});
|
|
29
22
|
|
|
30
|
-
const
|
|
31
|
-
type: "Create",
|
|
32
|
-
actor: { type: "Person", id: "alice", name: "Alice" },
|
|
33
|
-
object: {
|
|
34
|
-
type: "Note",
|
|
35
|
-
id: "msg-1",
|
|
36
|
-
name: "hello",
|
|
37
|
-
content: "hello",
|
|
38
|
-
mediaType: "text/plain",
|
|
39
|
-
},
|
|
40
|
-
target: { type: "Collection", id: "room-1", name: "Room 1" },
|
|
41
|
-
};
|
|
42
|
-
const body = JSON.stringify(payload);
|
|
43
|
-
const { random, signature } = createSignedRequest(body);
|
|
44
|
-
const headers = {
|
|
45
|
-
"content-type": "application/json",
|
|
46
|
-
"x-nextcloud-talk-random": random,
|
|
47
|
-
"x-nextcloud-talk-signature": signature,
|
|
48
|
-
"x-nextcloud-talk-backend": "https://nextcloud.example",
|
|
49
|
-
};
|
|
23
|
+
const { body, headers } = createSignedCreateMessageRequest();
|
|
50
24
|
|
|
51
25
|
const first = await fetch(harness.webhookUrl, {
|
|
52
26
|
method: "POST",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { generateNextcloudTalkSignature } from "./signature.js";
|
|
2
|
+
|
|
3
|
+
export function createSignedCreateMessageRequest(params?: { backend?: string }) {
|
|
4
|
+
const payload = {
|
|
5
|
+
type: "Create",
|
|
6
|
+
actor: { type: "Person", id: "alice", name: "Alice" },
|
|
7
|
+
object: {
|
|
8
|
+
type: "Note",
|
|
9
|
+
id: "msg-1",
|
|
10
|
+
name: "hello",
|
|
11
|
+
content: "hello",
|
|
12
|
+
mediaType: "text/plain",
|
|
13
|
+
},
|
|
14
|
+
target: { type: "Collection", id: "room-1", name: "Room 1" },
|
|
15
|
+
};
|
|
16
|
+
const body = JSON.stringify(payload);
|
|
17
|
+
const { random, signature } = generateNextcloudTalkSignature({
|
|
18
|
+
body,
|
|
19
|
+
secret: "nextcloud-secret",
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
body,
|
|
23
|
+
headers: {
|
|
24
|
+
"content-type": "application/json",
|
|
25
|
+
"x-nextcloud-talk-random": random,
|
|
26
|
+
"x-nextcloud-talk-signature": signature,
|
|
27
|
+
"x-nextcloud-talk-backend": params?.backend ?? "https://nextcloud.example",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -276,12 +276,25 @@ export function createNextcloudTalkWebhookServer(opts: NextcloudTalkWebhookServe
|
|
|
276
276
|
});
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
+
let stopped = false;
|
|
279
280
|
const stop = () => {
|
|
280
|
-
|
|
281
|
+
if (stopped) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
stopped = true;
|
|
285
|
+
try {
|
|
286
|
+
server.close();
|
|
287
|
+
} catch {
|
|
288
|
+
// ignore close races while shutting down
|
|
289
|
+
}
|
|
281
290
|
};
|
|
282
291
|
|
|
283
292
|
if (abortSignal) {
|
|
284
|
-
abortSignal.
|
|
293
|
+
if (abortSignal.aborted) {
|
|
294
|
+
stop();
|
|
295
|
+
} else {
|
|
296
|
+
abortSignal.addEventListener("abort", stop, { once: true });
|
|
297
|
+
}
|
|
285
298
|
}
|
|
286
299
|
|
|
287
300
|
return { server, start, stop };
|
|
@@ -384,7 +397,14 @@ export async function monitorNextcloudTalkProvider(
|
|
|
384
397
|
abortSignal: opts.abortSignal,
|
|
385
398
|
});
|
|
386
399
|
|
|
400
|
+
if (opts.abortSignal?.aborted) {
|
|
401
|
+
return { stop };
|
|
402
|
+
}
|
|
387
403
|
await start();
|
|
404
|
+
if (opts.abortSignal?.aborted) {
|
|
405
|
+
stop();
|
|
406
|
+
return { stop };
|
|
407
|
+
}
|
|
388
408
|
|
|
389
409
|
const publicUrl =
|
|
390
410
|
account.config.webhookPublicUrl ??
|
package/src/onboarding.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
addWildcardAllowFrom,
|
|
3
3
|
formatDocsLink,
|
|
4
|
+
hasConfiguredSecretInput,
|
|
4
5
|
mergeAllowFromEntries,
|
|
6
|
+
promptSingleChannelSecretInput,
|
|
5
7
|
promptAccountId,
|
|
6
8
|
DEFAULT_ACCOUNT_ID,
|
|
7
9
|
normalizeAccountId,
|
|
10
|
+
type SecretInput,
|
|
8
11
|
type ChannelOnboardingAdapter,
|
|
9
12
|
type ChannelOnboardingDmPolicy,
|
|
10
13
|
type OpenClawConfig,
|
|
@@ -216,7 +219,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
216
219
|
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
|
217
220
|
const canUseEnv = allowEnv && Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim());
|
|
218
221
|
const hasConfigSecret = Boolean(
|
|
219
|
-
resolvedAccount.config.botSecret ||
|
|
222
|
+
hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
|
|
223
|
+
resolvedAccount.config.botSecretFile,
|
|
220
224
|
);
|
|
221
225
|
|
|
222
226
|
let baseUrl = resolvedAccount.baseUrl;
|
|
@@ -238,17 +242,30 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
238
242
|
).trim();
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
let secret:
|
|
245
|
+
let secret: SecretInput | null = null;
|
|
242
246
|
if (!accountConfigured) {
|
|
243
247
|
await noteNextcloudTalkSecretHelp(prompter);
|
|
244
248
|
}
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
const secretResult = await promptSingleChannelSecretInput({
|
|
251
|
+
cfg: next,
|
|
252
|
+
prompter,
|
|
253
|
+
providerHint: "nextcloud-talk",
|
|
254
|
+
credentialLabel: "bot secret",
|
|
255
|
+
accountConfigured,
|
|
256
|
+
canUseEnv: canUseEnv && !hasConfigSecret,
|
|
257
|
+
hasConfigToken: hasConfigSecret,
|
|
258
|
+
envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
|
|
259
|
+
keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
|
|
260
|
+
inputPrompt: "Enter Nextcloud Talk bot secret",
|
|
261
|
+
preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
|
|
262
|
+
});
|
|
263
|
+
if (secretResult.action === "set") {
|
|
264
|
+
secret = secretResult.value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (secretResult.action === "use-env" || secret || baseUrl !== resolvedAccount.baseUrl) {
|
|
268
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
252
269
|
next = {
|
|
253
270
|
...next,
|
|
254
271
|
channels: {
|
|
@@ -257,40 +274,65 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
257
274
|
...next.channels?.["nextcloud-talk"],
|
|
258
275
|
enabled: true,
|
|
259
276
|
baseUrl,
|
|
277
|
+
...(secret ? { botSecret: secret } : {}),
|
|
260
278
|
},
|
|
261
279
|
},
|
|
262
280
|
};
|
|
263
281
|
} else {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
next = {
|
|
283
|
+
...next,
|
|
284
|
+
channels: {
|
|
285
|
+
...next.channels,
|
|
286
|
+
"nextcloud-talk": {
|
|
287
|
+
...next.channels?.["nextcloud-talk"],
|
|
288
|
+
enabled: true,
|
|
289
|
+
accounts: {
|
|
290
|
+
...next.channels?.["nextcloud-talk"]?.accounts,
|
|
291
|
+
[accountId]: {
|
|
292
|
+
...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
293
|
+
enabled:
|
|
294
|
+
next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
295
|
+
baseUrl,
|
|
296
|
+
...(secret ? { botSecret: secret } : {}),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
283
302
|
}
|
|
284
|
-
}
|
|
285
|
-
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const existingApiUser = resolvedAccount.config.apiUser?.trim();
|
|
306
|
+
const existingApiPasswordConfigured = Boolean(
|
|
307
|
+
hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
|
|
308
|
+
resolvedAccount.config.apiPasswordFile,
|
|
309
|
+
);
|
|
310
|
+
const configureApiCredentials = await prompter.confirm({
|
|
311
|
+
message: "Configure optional Nextcloud Talk API credentials for room lookups?",
|
|
312
|
+
initialValue: Boolean(existingApiUser && existingApiPasswordConfigured),
|
|
313
|
+
});
|
|
314
|
+
if (configureApiCredentials) {
|
|
315
|
+
const apiUser = String(
|
|
286
316
|
await prompter.text({
|
|
287
|
-
message: "
|
|
288
|
-
|
|
317
|
+
message: "Nextcloud Talk API user",
|
|
318
|
+
initialValue: existingApiUser,
|
|
319
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
289
320
|
}),
|
|
290
321
|
).trim();
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
322
|
+
const apiPasswordResult = await promptSingleChannelSecretInput({
|
|
323
|
+
cfg: next,
|
|
324
|
+
prompter,
|
|
325
|
+
providerHint: "nextcloud-talk-api",
|
|
326
|
+
credentialLabel: "API password",
|
|
327
|
+
accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
|
|
328
|
+
canUseEnv: false,
|
|
329
|
+
hasConfigToken: existingApiPasswordConfigured,
|
|
330
|
+
envPrompt: "",
|
|
331
|
+
keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
|
|
332
|
+
inputPrompt: "Enter Nextcloud Talk API password",
|
|
333
|
+
preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
|
|
334
|
+
});
|
|
335
|
+
const apiPassword = apiPasswordResult.action === "set" ? apiPasswordResult.value : undefined;
|
|
294
336
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
295
337
|
next = {
|
|
296
338
|
...next,
|
|
@@ -299,8 +341,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
299
341
|
"nextcloud-talk": {
|
|
300
342
|
...next.channels?.["nextcloud-talk"],
|
|
301
343
|
enabled: true,
|
|
302
|
-
|
|
303
|
-
...(
|
|
344
|
+
apiUser,
|
|
345
|
+
...(apiPassword ? { apiPassword } : {}),
|
|
304
346
|
},
|
|
305
347
|
},
|
|
306
348
|
};
|
|
@@ -318,8 +360,8 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
318
360
|
...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
|
|
319
361
|
enabled:
|
|
320
362
|
next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
|
|
321
|
-
|
|
322
|
-
...(
|
|
363
|
+
apiUser,
|
|
364
|
+
...(apiPassword ? { apiPassword } : {}),
|
|
323
365
|
},
|
|
324
366
|
},
|
|
325
367
|
},
|
package/src/room-info.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
|
|
2
3
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
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(),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasConfiguredSecretInput,
|
|
3
|
+
normalizeResolvedSecretInputString,
|
|
4
|
+
normalizeSecretInputString,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
|
|
9
|
+
|
|
10
|
+
export function buildSecretInputSchema() {
|
|
11
|
+
return z.union([
|
|
12
|
+
z.string(),
|
|
13
|
+
z.object({
|
|
14
|
+
source: z.enum(["env", "file", "exec"]),
|
|
15
|
+
provider: z.string().min(1),
|
|
16
|
+
id: z.string().min(1),
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
DmConfig,
|
|
4
4
|
DmPolicy,
|
|
5
5
|
GroupPolicy,
|
|
6
|
+
SecretInput,
|
|
6
7
|
} from "openclaw/plugin-sdk";
|
|
7
8
|
|
|
8
9
|
export type { DmPolicy, GroupPolicy };
|
|
@@ -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). */
|
|
@@ -79,6 +80,8 @@ export type NextcloudTalkAccountConfig = {
|
|
|
79
80
|
export type NextcloudTalkConfig = {
|
|
80
81
|
/** Optional per-account Nextcloud Talk configuration (multi-account). */
|
|
81
82
|
accounts?: Record<string, NextcloudTalkAccountConfig>;
|
|
83
|
+
/** Optional default account id when multiple accounts are configured. */
|
|
84
|
+
defaultAccount?: string;
|
|
82
85
|
} & NextcloudTalkAccountConfig;
|
|
83
86
|
|
|
84
87
|
export type CoreConfig = {
|