@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.
@@ -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 payload = {
15
- type: "Create",
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 payload = {
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", // pragma: allowlist 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
@@ -6,7 +6,7 @@ import {
6
6
  isRequestBodyLimitError,
7
7
  readRequestBodyWithLimit,
8
8
  requestBodyErrorToText,
9
- } from "openclaw/plugin-sdk";
9
+ } from "openclaw/plugin-sdk/nextcloud-talk";
10
10
  import { resolveNextcloudTalkAccount } from "./accounts.js";
11
11
  import { handleNextcloudTalkInbound } from "./inbound.js";
12
12
  import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
package/src/onboarding.ts CHANGED
@@ -1,15 +1,20 @@
1
1
  import {
2
- addWildcardAllowFrom,
2
+ buildSingleChannelSecretPromptState,
3
3
  formatDocsLink,
4
+ hasConfiguredSecretInput,
5
+ mapAllowFromEntries,
4
6
  mergeAllowFromEntries,
5
- promptAccountId,
7
+ promptSingleChannelSecretInput,
8
+ resolveAccountIdForConfigure,
6
9
  DEFAULT_ACCOUNT_ID,
7
10
  normalizeAccountId,
11
+ setTopLevelChannelDmPolicyWithAllowFrom,
12
+ type SecretInput,
8
13
  type ChannelOnboardingAdapter,
9
14
  type ChannelOnboardingDmPolicy,
10
15
  type OpenClawConfig,
11
16
  type WizardPrompter,
12
- } from "openclaw/plugin-sdk";
17
+ } from "openclaw/plugin-sdk/nextcloud-talk";
13
18
  import {
14
19
  listNextcloudTalkAccountIds,
15
20
  resolveDefaultNextcloudTalkAccountId,
@@ -20,24 +25,52 @@ import type { CoreConfig, DmPolicy } from "./types.js";
20
25
  const channel = "nextcloud-talk" as const;
21
26
 
22
27
  function setNextcloudTalkDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig {
23
- const existingConfig = cfg.channels?.["nextcloud-talk"];
24
- const existingAllowFrom: string[] = (existingConfig?.allowFrom ?? []).map((x) => String(x));
25
- const allowFrom: string[] =
26
- dmPolicy === "open" ? (addWildcardAllowFrom(existingAllowFrom) as string[]) : existingAllowFrom;
27
-
28
- const newNextcloudTalkConfig = {
29
- ...existingConfig,
28
+ return setTopLevelChannelDmPolicyWithAllowFrom({
29
+ cfg,
30
+ channel: "nextcloud-talk",
30
31
  dmPolicy,
31
- allowFrom,
32
- };
32
+ getAllowFrom: (inputCfg) =>
33
+ mapAllowFromEntries(inputCfg.channels?.["nextcloud-talk"]?.allowFrom),
34
+ }) as CoreConfig;
35
+ }
36
+
37
+ function setNextcloudTalkAccountConfig(
38
+ cfg: CoreConfig,
39
+ accountId: string,
40
+ updates: Record<string, unknown>,
41
+ ): CoreConfig {
42
+ if (accountId === DEFAULT_ACCOUNT_ID) {
43
+ return {
44
+ ...cfg,
45
+ channels: {
46
+ ...cfg.channels,
47
+ "nextcloud-talk": {
48
+ ...cfg.channels?.["nextcloud-talk"],
49
+ enabled: true,
50
+ ...updates,
51
+ },
52
+ },
53
+ };
54
+ }
33
55
 
34
56
  return {
35
57
  ...cfg,
36
58
  channels: {
37
59
  ...cfg.channels,
38
- "nextcloud-talk": newNextcloudTalkConfig,
60
+ "nextcloud-talk": {
61
+ ...cfg.channels?.["nextcloud-talk"],
62
+ enabled: true,
63
+ accounts: {
64
+ ...cfg.channels?.["nextcloud-talk"]?.accounts,
65
+ [accountId]: {
66
+ ...cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId],
67
+ enabled: cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
68
+ ...updates,
69
+ },
70
+ },
71
+ },
39
72
  },
40
- } as CoreConfig;
73
+ };
41
74
  }
42
75
 
43
76
  async function noteNextcloudTalkSecretHelp(prompter: WizardPrompter): Promise<void> {
@@ -102,40 +135,10 @@ async function promptNextcloudTalkAllowFrom(params: {
102
135
  ];
103
136
  const unique = mergeAllowFromEntries(undefined, merged);
104
137
 
105
- if (accountId === DEFAULT_ACCOUNT_ID) {
106
- return {
107
- ...cfg,
108
- channels: {
109
- ...cfg.channels,
110
- "nextcloud-talk": {
111
- ...cfg.channels?.["nextcloud-talk"],
112
- enabled: true,
113
- dmPolicy: "allowlist",
114
- allowFrom: unique,
115
- },
116
- },
117
- };
118
- }
119
-
120
- return {
121
- ...cfg,
122
- channels: {
123
- ...cfg.channels,
124
- "nextcloud-talk": {
125
- ...cfg.channels?.["nextcloud-talk"],
126
- enabled: true,
127
- accounts: {
128
- ...cfg.channels?.["nextcloud-talk"]?.accounts,
129
- [accountId]: {
130
- ...cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId],
131
- enabled: cfg.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
132
- dmPolicy: "allowlist",
133
- allowFrom: unique,
134
- },
135
- },
136
- },
137
- },
138
- };
138
+ return setNextcloudTalkAccountConfig(cfg, accountId, {
139
+ dmPolicy: "allowlist",
140
+ allowFrom: unique,
141
+ });
139
142
  }
140
143
 
141
144
  async function promptNextcloudTalkAllowFromForAccount(params: {
@@ -190,22 +193,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
190
193
  shouldPromptAccountIds,
191
194
  forceAllowFrom,
192
195
  }) => {
193
- const nextcloudTalkOverride = accountOverrides["nextcloud-talk"]?.trim();
194
196
  const defaultAccountId = resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig);
195
- let accountId = nextcloudTalkOverride
196
- ? normalizeAccountId(nextcloudTalkOverride)
197
- : defaultAccountId;
198
-
199
- if (shouldPromptAccountIds && !nextcloudTalkOverride) {
200
- accountId = await promptAccountId({
201
- cfg: cfg as CoreConfig,
202
- prompter,
203
- label: "Nextcloud Talk",
204
- currentId: accountId,
205
- listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
206
- defaultAccountId,
207
- });
208
- }
197
+ const accountId = await resolveAccountIdForConfigure({
198
+ cfg,
199
+ prompter,
200
+ label: "Nextcloud Talk",
201
+ accountOverride: accountOverrides["nextcloud-talk"],
202
+ shouldPromptAccountIds,
203
+ listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[],
204
+ defaultAccountId,
205
+ });
209
206
 
210
207
  let next = cfg as CoreConfig;
211
208
  const resolvedAccount = resolveNextcloudTalkAccount({
@@ -214,10 +211,16 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
214
211
  });
215
212
  const accountConfigured = Boolean(resolvedAccount.secret && resolvedAccount.baseUrl);
216
213
  const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
217
- const canUseEnv = allowEnv && Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim());
218
214
  const hasConfigSecret = Boolean(
219
- resolvedAccount.config.botSecret || resolvedAccount.config.botSecretFile,
215
+ hasConfiguredSecretInput(resolvedAccount.config.botSecret) ||
216
+ resolvedAccount.config.botSecretFile,
220
217
  );
218
+ const secretPromptState = buildSingleChannelSecretPromptState({
219
+ accountConfigured,
220
+ hasConfigToken: hasConfigSecret,
221
+ allowEnv,
222
+ envValue: process.env.NEXTCLOUD_TALK_BOT_SECRET,
223
+ });
221
224
 
222
225
  let baseUrl = resolvedAccount.baseUrl;
223
226
  if (!baseUrl) {
@@ -238,94 +241,72 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = {
238
241
  ).trim();
239
242
  }
240
243
 
241
- let secret: string | null = null;
244
+ let secret: SecretInput | null = null;
242
245
  if (!accountConfigured) {
243
246
  await noteNextcloudTalkSecretHelp(prompter);
244
247
  }
245
248
 
246
- if (canUseEnv && !resolvedAccount.config.botSecret) {
247
- const keepEnv = await prompter.confirm({
248
- message: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
249
- initialValue: true,
250
- });
251
- if (keepEnv) {
252
- next = {
253
- ...next,
254
- channels: {
255
- ...next.channels,
256
- "nextcloud-talk": {
257
- ...next.channels?.["nextcloud-talk"],
258
- enabled: true,
259
- baseUrl,
260
- },
261
- },
262
- };
263
- } else {
264
- secret = String(
265
- await prompter.text({
266
- message: "Enter Nextcloud Talk bot secret",
267
- validate: (value) => (value?.trim() ? undefined : "Required"),
268
- }),
269
- ).trim();
270
- }
271
- } else if (hasConfigSecret) {
272
- const keep = await prompter.confirm({
273
- message: "Nextcloud Talk secret already configured. Keep it?",
274
- initialValue: true,
249
+ const secretResult = await promptSingleChannelSecretInput({
250
+ cfg: next,
251
+ prompter,
252
+ providerHint: "nextcloud-talk",
253
+ credentialLabel: "bot secret",
254
+ accountConfigured: secretPromptState.accountConfigured,
255
+ canUseEnv: secretPromptState.canUseEnv,
256
+ hasConfigToken: secretPromptState.hasConfigToken,
257
+ envPrompt: "NEXTCLOUD_TALK_BOT_SECRET detected. Use env var?",
258
+ keepPrompt: "Nextcloud Talk bot secret already configured. Keep it?",
259
+ inputPrompt: "Enter Nextcloud Talk bot secret",
260
+ preferredEnvVar: "NEXTCLOUD_TALK_BOT_SECRET",
261
+ });
262
+ if (secretResult.action === "set") {
263
+ secret = secretResult.value;
264
+ }
265
+
266
+ if (secretResult.action === "use-env" || secret || baseUrl !== resolvedAccount.baseUrl) {
267
+ next = setNextcloudTalkAccountConfig(next, accountId, {
268
+ baseUrl,
269
+ ...(secret ? { botSecret: secret } : {}),
275
270
  });
276
- if (!keep) {
277
- secret = String(
278
- await prompter.text({
279
- message: "Enter Nextcloud Talk bot secret",
280
- validate: (value) => (value?.trim() ? undefined : "Required"),
281
- }),
282
- ).trim();
283
- }
284
- } else {
285
- secret = String(
271
+ }
272
+
273
+ const existingApiUser = resolvedAccount.config.apiUser?.trim();
274
+ const existingApiPasswordConfigured = Boolean(
275
+ hasConfiguredSecretInput(resolvedAccount.config.apiPassword) ||
276
+ resolvedAccount.config.apiPasswordFile,
277
+ );
278
+ const configureApiCredentials = await prompter.confirm({
279
+ message: "Configure optional Nextcloud Talk API credentials for room lookups?",
280
+ initialValue: Boolean(existingApiUser && existingApiPasswordConfigured),
281
+ });
282
+ if (configureApiCredentials) {
283
+ const apiUser = String(
286
284
  await prompter.text({
287
- message: "Enter Nextcloud Talk bot secret",
288
- validate: (value) => (value?.trim() ? undefined : "Required"),
285
+ message: "Nextcloud Talk API user",
286
+ initialValue: existingApiUser,
287
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
289
288
  }),
290
289
  ).trim();
291
- }
292
-
293
- if (secret || baseUrl !== resolvedAccount.baseUrl) {
294
- if (accountId === DEFAULT_ACCOUNT_ID) {
295
- next = {
296
- ...next,
297
- channels: {
298
- ...next.channels,
299
- "nextcloud-talk": {
300
- ...next.channels?.["nextcloud-talk"],
301
- enabled: true,
302
- baseUrl,
303
- ...(secret ? { botSecret: secret } : {}),
304
- },
305
- },
306
- };
307
- } else {
308
- next = {
309
- ...next,
310
- channels: {
311
- ...next.channels,
312
- "nextcloud-talk": {
313
- ...next.channels?.["nextcloud-talk"],
314
- enabled: true,
315
- accounts: {
316
- ...next.channels?.["nextcloud-talk"]?.accounts,
317
- [accountId]: {
318
- ...next.channels?.["nextcloud-talk"]?.accounts?.[accountId],
319
- enabled:
320
- next.channels?.["nextcloud-talk"]?.accounts?.[accountId]?.enabled ?? true,
321
- baseUrl,
322
- ...(secret ? { botSecret: secret } : {}),
323
- },
324
- },
325
- },
326
- },
327
- };
328
- }
290
+ const apiPasswordResult = await promptSingleChannelSecretInput({
291
+ cfg: next,
292
+ prompter,
293
+ providerHint: "nextcloud-talk-api",
294
+ credentialLabel: "API password",
295
+ ...buildSingleChannelSecretPromptState({
296
+ accountConfigured: Boolean(existingApiUser && existingApiPasswordConfigured),
297
+ hasConfigToken: existingApiPasswordConfigured,
298
+ allowEnv: false,
299
+ }),
300
+ envPrompt: "",
301
+ keepPrompt: "Nextcloud Talk API password already configured. Keep it?",
302
+ inputPrompt: "Enter Nextcloud Talk API password",
303
+ preferredEnvVar: "NEXTCLOUD_TALK_API_PASSWORD",
304
+ });
305
+ const apiPassword = apiPasswordResult.action === "set" ? apiPasswordResult.value : undefined;
306
+ next = setNextcloudTalkAccountConfig(next, accountId, {
307
+ apiUser,
308
+ ...(apiPassword ? { apiPassword } : {}),
309
+ });
329
310
  }
330
311
 
331
312
  if (forceAllowFrom) {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { resolveNextcloudTalkAllowlistMatch } from "./policy.js";
2
+ import { resolveNextcloudTalkAllowlistMatch, resolveNextcloudTalkGroupAllow } from "./policy.js";
3
3
 
4
4
  describe("nextcloud-talk policy", () => {
5
5
  describe("resolveNextcloudTalkAllowlistMatch", () => {
@@ -30,4 +30,109 @@ describe("nextcloud-talk policy", () => {
30
30
  ).toBe(false);
31
31
  });
32
32
  });
33
+
34
+ describe("resolveNextcloudTalkGroupAllow", () => {
35
+ it("blocks disabled policy", () => {
36
+ expect(
37
+ resolveNextcloudTalkGroupAllow({
38
+ groupPolicy: "disabled",
39
+ outerAllowFrom: ["owner"],
40
+ innerAllowFrom: ["room-user"],
41
+ senderId: "owner",
42
+ }),
43
+ ).toEqual({
44
+ allowed: false,
45
+ outerMatch: { allowed: false },
46
+ innerMatch: { allowed: false },
47
+ });
48
+ });
49
+
50
+ it("allows open policy", () => {
51
+ expect(
52
+ resolveNextcloudTalkGroupAllow({
53
+ groupPolicy: "open",
54
+ outerAllowFrom: [],
55
+ innerAllowFrom: [],
56
+ senderId: "owner",
57
+ }),
58
+ ).toEqual({
59
+ allowed: true,
60
+ outerMatch: { allowed: true },
61
+ innerMatch: { allowed: true },
62
+ });
63
+ });
64
+
65
+ it("blocks allowlist mode when both outer and inner allowlists are empty", () => {
66
+ expect(
67
+ resolveNextcloudTalkGroupAllow({
68
+ groupPolicy: "allowlist",
69
+ outerAllowFrom: [],
70
+ innerAllowFrom: [],
71
+ senderId: "owner",
72
+ }),
73
+ ).toEqual({
74
+ allowed: false,
75
+ outerMatch: { allowed: false },
76
+ innerMatch: { allowed: false },
77
+ });
78
+ });
79
+
80
+ it("requires inner match when only room-specific allowlist is configured", () => {
81
+ expect(
82
+ resolveNextcloudTalkGroupAllow({
83
+ groupPolicy: "allowlist",
84
+ outerAllowFrom: [],
85
+ innerAllowFrom: ["room-user"],
86
+ senderId: "room-user",
87
+ }),
88
+ ).toEqual({
89
+ allowed: true,
90
+ outerMatch: { allowed: false },
91
+ innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
92
+ });
93
+ });
94
+
95
+ it("blocks when outer allowlist misses even if inner allowlist matches", () => {
96
+ expect(
97
+ resolveNextcloudTalkGroupAllow({
98
+ groupPolicy: "allowlist",
99
+ outerAllowFrom: ["team-owner"],
100
+ innerAllowFrom: ["room-user"],
101
+ senderId: "room-user",
102
+ }),
103
+ ).toEqual({
104
+ allowed: false,
105
+ outerMatch: { allowed: false },
106
+ innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
107
+ });
108
+ });
109
+
110
+ it("allows when both outer and inner allowlists match", () => {
111
+ expect(
112
+ resolveNextcloudTalkGroupAllow({
113
+ groupPolicy: "allowlist",
114
+ outerAllowFrom: ["team-owner"],
115
+ innerAllowFrom: ["room-user"],
116
+ senderId: "team-owner",
117
+ }),
118
+ ).toEqual({
119
+ allowed: false,
120
+ outerMatch: { allowed: true, matchKey: "team-owner", matchSource: "id" },
121
+ innerMatch: { allowed: false },
122
+ });
123
+
124
+ expect(
125
+ resolveNextcloudTalkGroupAllow({
126
+ groupPolicy: "allowlist",
127
+ outerAllowFrom: ["shared-user"],
128
+ innerAllowFrom: ["shared-user"],
129
+ senderId: "shared-user",
130
+ }),
131
+ ).toEqual({
132
+ allowed: true,
133
+ outerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
134
+ innerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
135
+ });
136
+ });
137
+ });
33
138
  });
package/src/policy.ts CHANGED
@@ -3,14 +3,15 @@ import type {
3
3
  ChannelGroupContext,
4
4
  GroupPolicy,
5
5
  GroupToolPolicyConfig,
6
- } from "openclaw/plugin-sdk";
6
+ } from "openclaw/plugin-sdk/nextcloud-talk";
7
7
  import {
8
8
  buildChannelKeyCandidates,
9
+ evaluateMatchedGroupAccessForPolicy,
9
10
  normalizeChannelSlug,
10
11
  resolveChannelEntryMatchWithFallback,
11
12
  resolveMentionGatingWithBypass,
12
13
  resolveNestedAllowlistDecision,
13
- } from "openclaw/plugin-sdk";
14
+ } from "openclaw/plugin-sdk/nextcloud-talk";
14
15
  import type { NextcloudTalkRoomConfig } from "./types.js";
15
16
 
16
17
  function normalizeAllowEntry(raw: string): string {
@@ -128,19 +129,8 @@ export function resolveNextcloudTalkGroupAllow(params: {
128
129
  innerAllowFrom: Array<string | number> | undefined;
129
130
  senderId: string;
130
131
  }): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } {
131
- if (params.groupPolicy === "disabled") {
132
- return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
133
- }
134
- if (params.groupPolicy === "open") {
135
- return { allowed: true, outerMatch: { allowed: true }, innerMatch: { allowed: true } };
136
- }
137
-
138
132
  const outerAllow = normalizeNextcloudTalkAllowlist(params.outerAllowFrom);
139
133
  const innerAllow = normalizeNextcloudTalkAllowlist(params.innerAllowFrom);
140
- if (outerAllow.length === 0 && innerAllow.length === 0) {
141
- return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
142
- }
143
-
144
134
  const outerMatch = resolveNextcloudTalkAllowlistMatch({
145
135
  allowFrom: params.outerAllowFrom,
146
136
  senderId: params.senderId,
@@ -149,14 +139,32 @@ export function resolveNextcloudTalkGroupAllow(params: {
149
139
  allowFrom: params.innerAllowFrom,
150
140
  senderId: params.senderId,
151
141
  });
152
- const allowed = resolveNestedAllowlistDecision({
153
- outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,
154
- outerMatched: outerAllow.length > 0 ? outerMatch.allowed : true,
155
- innerConfigured: innerAllow.length > 0,
156
- innerMatched: innerMatch.allowed,
142
+ const access = evaluateMatchedGroupAccessForPolicy({
143
+ groupPolicy: params.groupPolicy,
144
+ allowlistConfigured: outerAllow.length > 0 || innerAllow.length > 0,
145
+ allowlistMatched: resolveNestedAllowlistDecision({
146
+ outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,
147
+ outerMatched: outerAllow.length > 0 ? outerMatch.allowed : true,
148
+ innerConfigured: innerAllow.length > 0,
149
+ innerMatched: innerMatch.allowed,
150
+ }),
157
151
  });
158
152
 
159
- return { allowed, outerMatch, innerMatch };
153
+ return {
154
+ allowed: access.allowed,
155
+ outerMatch:
156
+ params.groupPolicy === "open"
157
+ ? { allowed: true }
158
+ : params.groupPolicy === "disabled"
159
+ ? { allowed: false }
160
+ : outerMatch,
161
+ innerMatch:
162
+ params.groupPolicy === "open"
163
+ ? { allowed: true }
164
+ : params.groupPolicy === "disabled"
165
+ ? { allowed: false }
166
+ : innerMatch,
167
+ };
160
168
  }
161
169
 
162
170
  export function resolveNextcloudTalkMentionGate(params: {
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { createPersistentDedupe } from "openclaw/plugin-sdk";
2
+ import { createPersistentDedupe } from "openclaw/plugin-sdk/nextcloud-talk";
3
3
 
4
4
  const DEFAULT_REPLAY_TTL_MS = 24 * 60 * 60 * 1000;
5
5
  const DEFAULT_MEMORY_MAX_SIZE = 1_000;