@kodelyth/nextcloud-talk 2026.5.42 → 2026.6.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.
Files changed (58) hide show
  1. package/klaw.plugin.json +799 -2
  2. package/package.json +18 -6
  3. package/api.ts +0 -1
  4. package/channel-plugin-api.ts +0 -1
  5. package/contract-api.ts +0 -4
  6. package/doctor-contract-api.ts +0 -1
  7. package/index.ts +0 -20
  8. package/runtime-api.ts +0 -29
  9. package/secret-contract-api.ts +0 -5
  10. package/setup-entry.ts +0 -13
  11. package/src/accounts.test.ts +0 -31
  12. package/src/accounts.ts +0 -149
  13. package/src/api-credentials.ts +0 -31
  14. package/src/approval-auth.test.ts +0 -17
  15. package/src/approval-auth.ts +0 -27
  16. package/src/bot-preflight.test.ts +0 -135
  17. package/src/bot-preflight.ts +0 -183
  18. package/src/channel-api.ts +0 -5
  19. package/src/channel.adapters.ts +0 -52
  20. package/src/channel.core.test.ts +0 -75
  21. package/src/channel.lifecycle.test.ts +0 -91
  22. package/src/channel.status.test.ts +0 -28
  23. package/src/channel.ts +0 -225
  24. package/src/config-schema.ts +0 -79
  25. package/src/core.test.ts +0 -325
  26. package/src/doctor-contract.ts +0 -9
  27. package/src/doctor.test.ts +0 -87
  28. package/src/doctor.ts +0 -40
  29. package/src/gateway.ts +0 -109
  30. package/src/inbound.authz.test.ts +0 -146
  31. package/src/inbound.behavior.test.ts +0 -309
  32. package/src/inbound.ts +0 -392
  33. package/src/message-actions.test.ts +0 -270
  34. package/src/message-actions.ts +0 -82
  35. package/src/message-adapter.ts +0 -28
  36. package/src/monitor-runtime.ts +0 -138
  37. package/src/monitor.replay.test.ts +0 -276
  38. package/src/monitor.test-fixtures.ts +0 -30
  39. package/src/monitor.test-harness.ts +0 -59
  40. package/src/monitor.ts +0 -385
  41. package/src/normalize.ts +0 -44
  42. package/src/policy.ts +0 -111
  43. package/src/replay-guard.ts +0 -128
  44. package/src/room-info.test.ts +0 -160
  45. package/src/room-info.ts +0 -130
  46. package/src/runtime.ts +0 -9
  47. package/src/secret-contract.ts +0 -103
  48. package/src/secret-input.ts +0 -4
  49. package/src/send.cfg-threading.test.ts +0 -359
  50. package/src/send.runtime.ts +0 -8
  51. package/src/send.ts +0 -269
  52. package/src/session-route.ts +0 -40
  53. package/src/setup-core.ts +0 -250
  54. package/src/setup-surface.ts +0 -195
  55. package/src/setup.test.ts +0 -445
  56. package/src/signature.ts +0 -82
  57. package/src/types.ts +0 -195
  58. package/tsconfig.json +0 -16
@@ -1,183 +0,0 @@
1
- import { formatErrorMessage } from "klaw/plugin-sdk/error-runtime";
2
- import { readProviderJsonResponse } from "klaw/plugin-sdk/provider-http";
3
- import { fetchWithSsrFGuard } from "../runtime-api.js";
4
- import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
5
- import { resolveNextcloudTalkApiCredentials } from "./api-credentials.js";
6
- import { ssrfPolicyFromPrivateNetworkOptIn } from "./send.runtime.js";
7
-
8
- const BOT_FEATURE_RESPONSE = 2;
9
-
10
- type NextcloudTalkBotAdminEntry = {
11
- id?: number | string;
12
- name?: string;
13
- url?: string;
14
- features?: number | string;
15
- };
16
-
17
- export type NextcloudTalkBotResponseFeatureProbe = {
18
- ok: boolean;
19
- skipped?: boolean;
20
- code:
21
- | "ok"
22
- | "missing_api_credentials"
23
- | "missing_webhook_url"
24
- | "missing_base_url"
25
- | "bot_not_found"
26
- | "missing_response_feature"
27
- | "api_error"
28
- | "request_failed";
29
- message: string;
30
- botId?: string;
31
- botName?: string;
32
- features?: number;
33
- status?: number;
34
- };
35
-
36
- function normalizeUrlForMatch(value: string | undefined): string {
37
- if (!value?.trim()) {
38
- return "";
39
- }
40
- try {
41
- const url = new URL(value.trim());
42
- url.hash = "";
43
- return url.toString().replace(/\/$/, "");
44
- } catch {
45
- return value.trim().replace(/\/$/, "");
46
- }
47
- }
48
-
49
- function coerceFeatureMask(value: unknown): number | undefined {
50
- if (typeof value === "number" && Number.isFinite(value)) {
51
- return value;
52
- }
53
- if (typeof value === "string" && value.trim()) {
54
- const parsed = Number.parseInt(value, 10);
55
- return Number.isFinite(parsed) ? parsed : undefined;
56
- }
57
- return undefined;
58
- }
59
-
60
- function formatMissingResponseFeatureMessage(bot: NextcloudTalkBotAdminEntry, features?: number) {
61
- const id = bot.id == null ? "unknown" : String(bot.id);
62
- const name = bot.name?.trim() || "matching bot";
63
- const featureText = typeof features === "number" ? ` (features=${features})` : "";
64
- return `Nextcloud Talk bot "${name}" (${id}) is missing the response feature${featureText}; outbound replies will fail. Run ./occ talk:bot:state --feature webhook --feature response --feature reaction ${id} 1 or reinstall the bot with --feature response.`;
65
- }
66
-
67
- export async function probeNextcloudTalkBotResponseFeature(params: {
68
- account: ResolvedNextcloudTalkAccount;
69
- timeoutMs?: number;
70
- }): Promise<NextcloudTalkBotResponseFeatureProbe> {
71
- const { account, timeoutMs } = params;
72
- const baseUrl = account.baseUrl?.trim();
73
- if (!baseUrl) {
74
- return {
75
- ok: true,
76
- skipped: true,
77
- code: "missing_base_url",
78
- message: "Nextcloud Talk bot response feature probe skipped: baseUrl is not configured.",
79
- };
80
- }
81
-
82
- const webhookUrl = normalizeUrlForMatch(account.config.webhookPublicUrl);
83
- if (!webhookUrl) {
84
- return {
85
- ok: true,
86
- skipped: true,
87
- code: "missing_webhook_url",
88
- message:
89
- "Nextcloud Talk bot response feature probe skipped: webhookPublicUrl is not configured.",
90
- };
91
- }
92
-
93
- const credentials = resolveNextcloudTalkApiCredentials({
94
- apiUser: account.config.apiUser,
95
- apiPassword: account.config.apiPassword,
96
- apiPasswordFile: account.config.apiPasswordFile,
97
- });
98
- if (!credentials) {
99
- return {
100
- ok: true,
101
- skipped: true,
102
- code: "missing_api_credentials",
103
- message:
104
- "Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured.",
105
- };
106
- }
107
-
108
- const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/admin`;
109
- const auth = Buffer.from(`${credentials.apiUser}:${credentials.apiPassword}`, "utf-8").toString(
110
- "base64",
111
- );
112
-
113
- try {
114
- const { response, release } = await fetchWithSsrFGuard({
115
- url,
116
- init: {
117
- method: "GET",
118
- headers: {
119
- Authorization: `Basic ${auth}`,
120
- "OCS-APIRequest": "true",
121
- Accept: "application/json",
122
- },
123
- },
124
- auditContext: "nextcloud-talk.bot-response-preflight",
125
- policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
126
- timeoutMs,
127
- });
128
- try {
129
- if (!response.ok) {
130
- const body = await response.text().catch(() => "");
131
- return {
132
- ok: false,
133
- code: "api_error",
134
- status: response.status,
135
- message: `Nextcloud Talk bot response feature probe failed (${response.status})${body ? `: ${body}` : ""}`,
136
- };
137
- }
138
-
139
- const payload = await readProviderJsonResponse<{
140
- ocs?: { data?: NextcloudTalkBotAdminEntry[] };
141
- }>(response, "Nextcloud Talk bot response feature probe failed");
142
- const bots = Array.isArray(payload.ocs?.data) ? payload.ocs.data : [];
143
- const bot = bots.find((entry) => normalizeUrlForMatch(entry.url) === webhookUrl);
144
- if (!bot) {
145
- return {
146
- ok: false,
147
- code: "bot_not_found",
148
- message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`,
149
- };
150
- }
151
-
152
- const features = coerceFeatureMask(bot.features);
153
- if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) {
154
- return {
155
- ok: false,
156
- code: "missing_response_feature",
157
- botId: bot.id == null ? undefined : String(bot.id),
158
- botName: bot.name,
159
- features,
160
- message: formatMissingResponseFeatureMessage(bot, features),
161
- };
162
- }
163
-
164
- return {
165
- ok: true,
166
- code: "ok",
167
- botId: bot.id == null ? undefined : String(bot.id),
168
- botName: bot.name,
169
- features,
170
- message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`,
171
- };
172
- } finally {
173
- await release();
174
- }
175
- } catch (error) {
176
- const detail = error instanceof Error ? error.message : formatErrorMessage(error);
177
- return {
178
- ok: false,
179
- code: "request_failed",
180
- message: `Nextcloud Talk bot response feature probe failed: ${detail}`,
181
- };
182
- }
183
- }
@@ -1,5 +0,0 @@
1
- export type { ChannelPlugin } from "klaw/plugin-sdk/channel-plugin-common";
2
- export type { KlawConfig } from "klaw/plugin-sdk/config-contracts";
3
- export { clearAccountEntryFields } from "klaw/plugin-sdk/channel-plugin-common";
4
- export { DEFAULT_ACCOUNT_ID } from "klaw/plugin-sdk/account-id";
5
- export { buildChannelConfigSchema } from "klaw/plugin-sdk/channel-config-schema";
@@ -1,52 +0,0 @@
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
- };
@@ -1,75 +0,0 @@
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
- });
@@ -1,91 +0,0 @@
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
- });
@@ -1,28 +0,0 @@
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 DELETED
@@ -1,225 +0,0 @@
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
- });