@kodelyth/nextcloud-talk 2026.5.39 → 2026.5.42

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.
Files changed (78) hide show
  1. package/api.ts +1 -0
  2. package/channel-plugin-api.ts +1 -0
  3. package/contract-api.ts +4 -0
  4. package/dist/api.js +2 -0
  5. package/dist/channel-ej3z6XJ5.js +2094 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/contract-api.js +2 -0
  8. package/dist/doctor-contract-Dia7keG4.js +7 -0
  9. package/dist/doctor-contract-api.js +2 -0
  10. package/dist/index.js +22 -0
  11. package/dist/runtime-api-DCIDXlUd.js +14 -0
  12. package/dist/runtime-api.js +2 -0
  13. package/dist/secret-contract-DQ2wQ4m1.js +86 -0
  14. package/dist/secret-contract-api.js +2 -0
  15. package/dist/setup-entry.js +15 -0
  16. package/doctor-contract-api.ts +1 -0
  17. package/index.ts +20 -0
  18. package/klaw.plugin.json +2 -799
  19. package/package.json +4 -4
  20. package/runtime-api.ts +29 -0
  21. package/secret-contract-api.ts +5 -0
  22. package/setup-entry.ts +13 -0
  23. package/src/accounts.test.ts +31 -0
  24. package/src/accounts.ts +149 -0
  25. package/src/api-credentials.ts +31 -0
  26. package/src/approval-auth.test.ts +17 -0
  27. package/src/approval-auth.ts +27 -0
  28. package/src/bot-preflight.test.ts +135 -0
  29. package/src/bot-preflight.ts +183 -0
  30. package/src/channel-api.ts +5 -0
  31. package/src/channel.adapters.ts +52 -0
  32. package/src/channel.core.test.ts +75 -0
  33. package/src/channel.lifecycle.test.ts +91 -0
  34. package/src/channel.status.test.ts +28 -0
  35. package/src/channel.ts +225 -0
  36. package/src/config-schema.ts +79 -0
  37. package/src/core.test.ts +325 -0
  38. package/src/doctor-contract.ts +9 -0
  39. package/src/doctor.test.ts +87 -0
  40. package/src/doctor.ts +40 -0
  41. package/src/gateway.ts +109 -0
  42. package/src/inbound.authz.test.ts +146 -0
  43. package/src/inbound.behavior.test.ts +309 -0
  44. package/src/inbound.ts +392 -0
  45. package/src/message-actions.test.ts +270 -0
  46. package/src/message-actions.ts +82 -0
  47. package/src/message-adapter.ts +28 -0
  48. package/src/monitor-runtime.ts +138 -0
  49. package/src/monitor.replay.test.ts +276 -0
  50. package/src/monitor.test-fixtures.ts +30 -0
  51. package/src/monitor.test-harness.ts +59 -0
  52. package/src/monitor.ts +385 -0
  53. package/src/normalize.ts +44 -0
  54. package/src/policy.ts +111 -0
  55. package/src/replay-guard.ts +128 -0
  56. package/src/room-info.test.ts +160 -0
  57. package/src/room-info.ts +130 -0
  58. package/src/runtime.ts +9 -0
  59. package/src/secret-contract.ts +103 -0
  60. package/src/secret-input.ts +4 -0
  61. package/src/send.cfg-threading.test.ts +359 -0
  62. package/src/send.runtime.ts +8 -0
  63. package/src/send.ts +269 -0
  64. package/src/session-route.ts +40 -0
  65. package/src/setup-core.ts +250 -0
  66. package/src/setup-surface.ts +195 -0
  67. package/src/setup.test.ts +445 -0
  68. package/src/signature.ts +82 -0
  69. package/src/types.ts +195 -0
  70. package/tsconfig.json +16 -0
  71. package/api.js +0 -7
  72. package/channel-plugin-api.js +0 -7
  73. package/contract-api.js +0 -7
  74. package/doctor-contract-api.js +0 -7
  75. package/index.js +0 -7
  76. package/runtime-api.js +0 -7
  77. package/secret-contract-api.js +0 -7
  78. package/setup-entry.js +0 -7
@@ -0,0 +1,52 @@
1
+ import { formatAllowFromLowercase } from "klaw/plugin-sdk/allow-from";
2
+ import {
3
+ adaptScopedAccountAccessor,
4
+ createScopedChannelConfigAdapter,
5
+ createScopedDmSecurityResolver,
6
+ } from "klaw/plugin-sdk/channel-config-helpers";
7
+ import { createPairingPrefixStripper } from "klaw/plugin-sdk/channel-pairing";
8
+ import { normalizeLowercaseStringOrEmpty } from "klaw/plugin-sdk/string-coerce-runtime";
9
+ import {
10
+ listNextcloudTalkAccountIds,
11
+ resolveDefaultNextcloudTalkAccountId,
12
+ resolveNextcloudTalkAccount,
13
+ type ResolvedNextcloudTalkAccount,
14
+ } from "./accounts.js";
15
+ import type { CoreConfig } from "./types.js";
16
+
17
+ export const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter<
18
+ ResolvedNextcloudTalkAccount,
19
+ ResolvedNextcloudTalkAccount,
20
+ CoreConfig
21
+ >({
22
+ sectionKey: "nextcloud-talk",
23
+ listAccountIds: listNextcloudTalkAccountIds,
24
+ resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
25
+ defaultAccountId: resolveDefaultNextcloudTalkAccountId,
26
+ clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"],
27
+ resolveAllowFrom: (account) => account.config.allowFrom,
28
+ formatAllowFrom: (allowFrom) =>
29
+ formatAllowFromLowercase({
30
+ allowFrom,
31
+ stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
32
+ }),
33
+ });
34
+
35
+ export const nextcloudTalkSecurityAdapter = {
36
+ resolveDmPolicy: createScopedDmSecurityResolver<ResolvedNextcloudTalkAccount>({
37
+ channelKey: "nextcloud-talk",
38
+ resolvePolicy: (account) => account.config.dmPolicy,
39
+ resolveAllowFrom: (account) => account.config.allowFrom,
40
+ policyPathSuffix: "dmPolicy",
41
+ normalizeEntry: (raw) =>
42
+ normalizeLowercaseStringOrEmpty(raw.trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, "")),
43
+ }),
44
+ };
45
+
46
+ export const nextcloudTalkPairingTextAdapter = {
47
+ idLabel: "nextcloudUserId",
48
+ message: "Klaw: your access has been approved.",
49
+ normalizeAllowEntry: createPairingPrefixStripper(/^(nextcloud-talk|nc-talk|nc):/i, (entry) =>
50
+ normalizeLowercaseStringOrEmpty(entry),
51
+ ),
52
+ };
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ nextcloudTalkConfigAdapter,
4
+ nextcloudTalkPairingTextAdapter,
5
+ nextcloudTalkSecurityAdapter,
6
+ } from "./channel.adapters.js";
7
+ import { NextcloudTalkConfigSchema } from "./config-schema.js";
8
+ import type { CoreConfig } from "./types.js";
9
+
10
+ describe("nextcloud talk channel core", () => {
11
+ it("accepts SecretRef botSecret and apiPassword at top-level", () => {
12
+ const result = NextcloudTalkConfigSchema.safeParse({
13
+ baseUrl: "https://cloud.example.com",
14
+ botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
15
+ apiUser: "bot",
16
+ apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
17
+ });
18
+ expect(result.success).toBe(true);
19
+ });
20
+
21
+ it("accepts SecretRef botSecret and apiPassword on account", () => {
22
+ const result = NextcloudTalkConfigSchema.safeParse({
23
+ accounts: {
24
+ main: {
25
+ baseUrl: "https://cloud.example.com",
26
+ botSecret: {
27
+ source: "env",
28
+ provider: "default",
29
+ id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
30
+ },
31
+ apiUser: "bot",
32
+ apiPassword: {
33
+ source: "env",
34
+ provider: "default",
35
+ id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
36
+ },
37
+ },
38
+ },
39
+ });
40
+ expect(result.success).toBe(true);
41
+ });
42
+
43
+ it("normalizes trimmed DM allowlist prefixes to lowercase ids", () => {
44
+ const resolveDmPolicy = nextcloudTalkSecurityAdapter.resolveDmPolicy;
45
+ if (!resolveDmPolicy) {
46
+ throw new Error("resolveDmPolicy unavailable");
47
+ }
48
+
49
+ const cfg = {
50
+ channels: {
51
+ "nextcloud-talk": {
52
+ baseUrl: "https://cloud.example.com",
53
+ botSecret: "secret",
54
+ dmPolicy: "allowlist",
55
+ allowFrom: [" nc:User-Id "],
56
+ },
57
+ },
58
+ } as CoreConfig;
59
+
60
+ const result = resolveDmPolicy({
61
+ cfg,
62
+ account: nextcloudTalkConfigAdapter.resolveAccount(cfg, "default"),
63
+ });
64
+ if (!result) {
65
+ throw new Error("nextcloud-talk resolveDmPolicy returned null");
66
+ }
67
+
68
+ expect(result.policy).toBe("allowlist");
69
+ expect(result.allowFrom).toEqual([" nc:User-Id "]);
70
+ expect(result.normalizeEntry?.(" nc:User-Id ")).toBe("user-id");
71
+ expect(nextcloudTalkPairingTextAdapter.normalizeAllowEntry(" nextcloud-talk:User-Id ")).toBe(
72
+ "user-id",
73
+ );
74
+ });
75
+ });
@@ -0,0 +1,91 @@
1
+ import { createStartAccountContext } from "klaw/plugin-sdk/channel-test-helpers";
2
+ import {
3
+ expectStopPendingUntilAbort,
4
+ startAccountAndTrackLifecycle,
5
+ waitForStartedMocks,
6
+ } from "klaw/plugin-sdk/channel-test-helpers";
7
+ import { afterEach, describe, expect, it, vi } from "vitest";
8
+ import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
9
+
10
+ const hoisted = vi.hoisted(() => ({
11
+ monitorNextcloudTalkProvider: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("./monitor-runtime.js", () => ({
15
+ monitorNextcloudTalkProvider: hoisted.monitorNextcloudTalkProvider,
16
+ }));
17
+
18
+ const { nextcloudTalkGatewayAdapter } = await import("./gateway.js");
19
+
20
+ type NextcloudTalkStartAccount = NonNullable<typeof nextcloudTalkGatewayAdapter.startAccount>;
21
+
22
+ function requireStartAccount(): NextcloudTalkStartAccount {
23
+ const startAccount = nextcloudTalkGatewayAdapter.startAccount;
24
+ if (!startAccount) {
25
+ throw new Error("Expected Nextcloud Talk gateway startAccount");
26
+ }
27
+ return startAccount;
28
+ }
29
+
30
+ function buildAccount(): ResolvedNextcloudTalkAccount {
31
+ return {
32
+ accountId: "default",
33
+ enabled: true,
34
+ baseUrl: "https://nextcloud.example.com",
35
+ secret: "secret", // pragma: allowlist secret
36
+ secretSource: "config", // pragma: allowlist secret
37
+ config: {
38
+ baseUrl: "https://nextcloud.example.com",
39
+ botSecret: "secret", // pragma: allowlist secret
40
+ webhookPath: "/nextcloud-talk-webhook",
41
+ webhookPort: 8788,
42
+ },
43
+ };
44
+ }
45
+
46
+ function mockStartedMonitor() {
47
+ const stop = vi.fn();
48
+ hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
49
+ return stop;
50
+ }
51
+
52
+ function startNextcloudAccount(abortSignal?: AbortSignal) {
53
+ return requireStartAccount()(
54
+ createStartAccountContext({
55
+ account: buildAccount(),
56
+ abortSignal,
57
+ }),
58
+ );
59
+ }
60
+
61
+ describe("nextcloud-talk startAccount lifecycle", () => {
62
+ afterEach(() => {
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ it("keeps startAccount pending until abort, then stops the monitor", async () => {
67
+ const stop = mockStartedMonitor();
68
+ const { abort, task, isSettled } = startAccountAndTrackLifecycle({
69
+ startAccount: requireStartAccount(),
70
+ account: buildAccount(),
71
+ });
72
+ await expectStopPendingUntilAbort({
73
+ waitForStarted: waitForStartedMocks(hoisted.monitorNextcloudTalkProvider),
74
+ isSettled,
75
+ abort,
76
+ task,
77
+ stop,
78
+ });
79
+ });
80
+
81
+ it("stops immediately when startAccount receives an already-aborted signal", async () => {
82
+ const stop = mockStartedMonitor();
83
+ const abort = new AbortController();
84
+ abort.abort();
85
+
86
+ await startNextcloudAccount(abort.signal);
87
+
88
+ expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
89
+ expect(stop).toHaveBeenCalledOnce();
90
+ });
91
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nextcloudTalkPlugin } from "./channel.js";
3
+
4
+ describe("nextcloud-talk channel status", () => {
5
+ it("surfaces missing response feature probes as config issues", () => {
6
+ const issues = nextcloudTalkPlugin.status?.collectStatusIssues?.([
7
+ {
8
+ accountId: "default",
9
+ configured: true,
10
+ probe: {
11
+ ok: false,
12
+ code: "missing_response_feature",
13
+ message: "Nextcloud Talk bot is missing --feature response.",
14
+ },
15
+ },
16
+ ]);
17
+
18
+ expect(issues).toEqual([
19
+ {
20
+ channel: "nextcloud-talk",
21
+ accountId: "default",
22
+ kind: "config",
23
+ message: "Nextcloud Talk bot is missing --feature response.",
24
+ fix: "Add --feature response to the Talk bot.",
25
+ },
26
+ ]);
27
+ });
28
+ });
package/src/channel.ts ADDED
@@ -0,0 +1,225 @@
1
+ import { describeWebhookAccountSnapshot } from "klaw/plugin-sdk/account-helpers";
2
+ import { createChatChannelPlugin } from "klaw/plugin-sdk/channel-core";
3
+ import { createLoggedPairingApprovalNotifier } from "klaw/plugin-sdk/channel-pairing";
4
+ import { createAllowlistProviderRouteAllowlistWarningCollector } from "klaw/plugin-sdk/channel-policy";
5
+ import {
6
+ buildWebhookChannelStatusSummary,
7
+ createComputedAccountStatusAdapter,
8
+ createDefaultChannelRuntimeState,
9
+ } from "klaw/plugin-sdk/status-helpers";
10
+ import { resolveNextcloudTalkAccount, type ResolvedNextcloudTalkAccount } from "./accounts.js";
11
+ import { nextcloudTalkApprovalAuth } from "./approval-auth.js";
12
+ import { probeNextcloudTalkBotResponseFeature } from "./bot-preflight.js";
13
+ import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, type ChannelPlugin } from "./channel-api.js";
14
+ import {
15
+ nextcloudTalkConfigAdapter,
16
+ nextcloudTalkPairingTextAdapter,
17
+ nextcloudTalkSecurityAdapter,
18
+ } from "./channel.adapters.js";
19
+ import { NextcloudTalkConfigSchema } from "./config-schema.js";
20
+ import { nextcloudTalkDoctor } from "./doctor.js";
21
+ import { nextcloudTalkGatewayAdapter } from "./gateway.js";
22
+ import { nextcloudTalkMessageActions } from "./message-actions.js";
23
+ import { nextcloudTalkMessageAdapter } from "./message-adapter.js";
24
+ import {
25
+ looksLikeNextcloudTalkTargetId,
26
+ normalizeNextcloudTalkMessagingTarget,
27
+ } from "./normalize.js";
28
+ import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
29
+ import { getNextcloudTalkRuntime } from "./runtime.js";
30
+ import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
31
+ import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
32
+ import { nextcloudTalkSetupAdapter } from "./setup-core.js";
33
+ import { nextcloudTalkSetupWizard } from "./setup-surface.js";
34
+ import type { CoreConfig } from "./types.js";
35
+
36
+ const meta = {
37
+ id: "nextcloud-talk",
38
+ label: "Nextcloud Talk",
39
+ selectionLabel: "Nextcloud Talk (self-hosted)",
40
+ docsPath: "/channels/nextcloud-talk",
41
+ docsLabel: "nextcloud-talk",
42
+ blurb: "Self-hosted chat via Nextcloud Talk webhook bots.",
43
+ aliases: ["nc-talk", "nc"],
44
+ order: 65,
45
+ quickstartAllowFrom: true,
46
+ };
47
+
48
+ const collectNextcloudTalkSecurityWarnings =
49
+ createAllowlistProviderRouteAllowlistWarningCollector<ResolvedNextcloudTalkAccount>({
50
+ providerConfigPresent: (cfg) =>
51
+ (cfg.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] !== undefined,
52
+ resolveGroupPolicy: (account) => account.config.groupPolicy,
53
+ resolveRouteAllowlistConfigured: (account) =>
54
+ Boolean(account.config.rooms) && Object.keys(account.config.rooms ?? {}).length > 0,
55
+ restrictSenders: {
56
+ surface: "Nextcloud Talk rooms",
57
+ openScope: "any member in allowed rooms",
58
+ groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
59
+ groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
60
+ },
61
+ noRouteAllowlist: {
62
+ surface: "Nextcloud Talk rooms",
63
+ routeAllowlistPath: "channels.nextcloud-talk.rooms",
64
+ routeScope: "room",
65
+ groupPolicyPath: "channels.nextcloud-talk.groupPolicy",
66
+ groupAllowFromPath: "channels.nextcloud-talk.groupAllowFrom",
67
+ },
68
+ });
69
+
70
+ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
71
+ createChatChannelPlugin({
72
+ base: {
73
+ id: "nextcloud-talk",
74
+ meta,
75
+ setupWizard: nextcloudTalkSetupWizard,
76
+ capabilities: {
77
+ chatTypes: ["direct", "group"],
78
+ reactions: true,
79
+ threads: false,
80
+ media: true,
81
+ nativeCommands: false,
82
+ blockStreaming: true,
83
+ },
84
+ reload: { configPrefixes: ["channels.nextcloud-talk"] },
85
+ configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
86
+ config: {
87
+ ...nextcloudTalkConfigAdapter,
88
+ isConfigured: (account) => Boolean(account.secret?.trim() && account.baseUrl?.trim()),
89
+ describeAccount: (account) =>
90
+ describeWebhookAccountSnapshot({
91
+ account,
92
+ configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
93
+ extra: {
94
+ secretSource: account.secretSource,
95
+ baseUrl: account.baseUrl ? "[set]" : "[missing]",
96
+ },
97
+ }),
98
+ },
99
+ approvalCapability: nextcloudTalkApprovalAuth,
100
+ doctor: nextcloudTalkDoctor,
101
+ groups: {
102
+ resolveRequireMention: ({ cfg, accountId, groupId }) => {
103
+ const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
104
+ const rooms = account.config.rooms;
105
+ if (!rooms || !groupId) {
106
+ return true;
107
+ }
108
+
109
+ const roomConfig = rooms[groupId];
110
+ if (roomConfig?.requireMention !== undefined) {
111
+ return roomConfig.requireMention;
112
+ }
113
+
114
+ const wildcardConfig = rooms["*"];
115
+ if (wildcardConfig?.requireMention !== undefined) {
116
+ return wildcardConfig.requireMention;
117
+ }
118
+
119
+ return true;
120
+ },
121
+ resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
122
+ },
123
+ messaging: {
124
+ targetPrefixes: ["nextcloud-talk", "nc-talk", "nc"],
125
+ normalizeTarget: normalizeNextcloudTalkMessagingTarget,
126
+ resolveOutboundSessionRoute: (params) => resolveNextcloudTalkOutboundSessionRoute(params),
127
+ targetResolver: {
128
+ looksLikeId: looksLikeNextcloudTalkTargetId,
129
+ hint: "<roomToken>",
130
+ },
131
+ },
132
+ secrets: {
133
+ secretTargetRegistryEntries,
134
+ collectRuntimeConfigAssignments,
135
+ },
136
+ setup: nextcloudTalkSetupAdapter,
137
+ status: createComputedAccountStatusAdapter<ResolvedNextcloudTalkAccount>({
138
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
139
+ buildChannelSummary: ({ snapshot }) =>
140
+ buildWebhookChannelStatusSummary(snapshot, {
141
+ secretSource: snapshot.secretSource ?? "none",
142
+ }),
143
+ collectStatusIssues: (accounts) =>
144
+ accounts.flatMap((account) => {
145
+ const probe = account.probe as
146
+ | { ok?: boolean; code?: string; message?: string }
147
+ | undefined;
148
+ if (
149
+ !probe ||
150
+ probe.ok !== false ||
151
+ probe.code !== "missing_response_feature" ||
152
+ !probe.message
153
+ ) {
154
+ return [];
155
+ }
156
+ return [
157
+ {
158
+ channel: "nextcloud-talk",
159
+ accountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
160
+ kind: "config",
161
+ message: probe.message,
162
+ fix: "Add --feature response to the Talk bot.",
163
+ } as const,
164
+ ];
165
+ }),
166
+ probeAccount: async ({ account, timeoutMs }) =>
167
+ await probeNextcloudTalkBotResponseFeature({ account, timeoutMs }),
168
+ resolveAccountSnapshot: ({ account }) => ({
169
+ accountId: account.accountId,
170
+ name: account.name,
171
+ enabled: account.enabled,
172
+ configured: Boolean(account.secret?.trim() && account.baseUrl?.trim()),
173
+ extra: {
174
+ secretSource: account.secretSource,
175
+ baseUrl: account.baseUrl ? "[set]" : "[missing]",
176
+ mode: "webhook",
177
+ },
178
+ }),
179
+ }),
180
+ gateway: nextcloudTalkGatewayAdapter,
181
+ message: nextcloudTalkMessageAdapter,
182
+ actions: nextcloudTalkMessageActions,
183
+ },
184
+ pairing: {
185
+ text: {
186
+ ...nextcloudTalkPairingTextAdapter,
187
+ notify: createLoggedPairingApprovalNotifier(
188
+ ({ id }) => `[nextcloud-talk] User ${id} approved for pairing`,
189
+ ),
190
+ },
191
+ },
192
+ security: {
193
+ ...nextcloudTalkSecurityAdapter,
194
+ collectWarnings: collectNextcloudTalkSecurityWarnings,
195
+ },
196
+ outbound: {
197
+ base: {
198
+ deliveryMode: "direct",
199
+ chunker: (text, limit) =>
200
+ getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit),
201
+ chunkerMode: "markdown",
202
+ textChunkLimit: 4000,
203
+ },
204
+ attachedResults: {
205
+ channel: "nextcloud-talk",
206
+ sendText: async ({ cfg, to, text, accountId, replyToId }) =>
207
+ await nextcloudTalkMessageAdapter.send.text({
208
+ cfg,
209
+ to,
210
+ text,
211
+ accountId,
212
+ replyToId,
213
+ }),
214
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) =>
215
+ await nextcloudTalkMessageAdapter.send.media({
216
+ cfg,
217
+ to,
218
+ text,
219
+ mediaUrl: mediaUrl ?? "",
220
+ accountId,
221
+ replyToId,
222
+ }),
223
+ },
224
+ },
225
+ });
@@ -0,0 +1,79 @@
1
+ import {
2
+ DmPolicySchema,
3
+ GroupPolicySchema,
4
+ MarkdownConfigSchema,
5
+ ReplyRuntimeConfigSchemaShape,
6
+ ToolPolicySchema,
7
+ requireOpenAllowFrom,
8
+ } from "klaw/plugin-sdk/channel-config-schema";
9
+ import { requireChannelOpenAllowFrom } from "klaw/plugin-sdk/extension-shared";
10
+ import { z } from "zod";
11
+ import { buildSecretInputSchema } from "./secret-input.js";
12
+
13
+ const NextcloudTalkRoomSchema = z
14
+ .object({
15
+ requireMention: z.boolean().optional(),
16
+ tools: ToolPolicySchema,
17
+ skills: z.array(z.string()).optional(),
18
+ enabled: z.boolean().optional(),
19
+ allowFrom: z.array(z.string()).optional(),
20
+ systemPrompt: z.string().optional(),
21
+ })
22
+ .strict();
23
+
24
+ const NextcloudTalkNetworkSchema = z
25
+ .object({
26
+ /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
27
+ dangerouslyAllowPrivateNetwork: z.boolean().optional(),
28
+ })
29
+ .strict()
30
+ .optional();
31
+
32
+ const NextcloudTalkAccountSchemaBase = z
33
+ .object({
34
+ name: z.string().optional(),
35
+ enabled: z.boolean().optional(),
36
+ markdown: MarkdownConfigSchema,
37
+ baseUrl: z.string().optional(),
38
+ botSecret: buildSecretInputSchema().optional(),
39
+ botSecretFile: z.string().optional(),
40
+ apiUser: z.string().optional(),
41
+ apiPassword: buildSecretInputSchema().optional(),
42
+ apiPasswordFile: z.string().optional(),
43
+ dmPolicy: DmPolicySchema.optional().default("pairing"),
44
+ webhookPort: z.number().int().positive().optional(),
45
+ webhookHost: z.string().optional(),
46
+ webhookPath: z.string().optional(),
47
+ webhookPublicUrl: z.string().optional(),
48
+ allowFrom: z.array(z.string()).optional(),
49
+ groupAllowFrom: z.array(z.string()).optional(),
50
+ groupPolicy: GroupPolicySchema.optional().default("allowlist"),
51
+ rooms: z.record(z.string(), NextcloudTalkRoomSchema.optional()).optional(),
52
+ /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
53
+ network: NextcloudTalkNetworkSchema,
54
+ ...ReplyRuntimeConfigSchemaShape,
55
+ })
56
+ .strict();
57
+
58
+ const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine((value, ctx) => {
59
+ requireChannelOpenAllowFrom({
60
+ channel: "nextcloud-talk",
61
+ policy: value.dmPolicy,
62
+ allowFrom: value.allowFrom,
63
+ ctx,
64
+ requireOpenAllowFrom,
65
+ });
66
+ });
67
+
68
+ export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
69
+ accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
70
+ defaultAccount: z.string().optional(),
71
+ }).superRefine((value, ctx) => {
72
+ requireChannelOpenAllowFrom({
73
+ channel: "nextcloud-talk",
74
+ policy: value.dmPolicy,
75
+ allowFrom: value.allowFrom,
76
+ ctx,
77
+ requireOpenAllowFrom,
78
+ });
79
+ });