@kodelyth/nextcloud-talk 2026.5.42 → 2026.6.1
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/klaw.plugin.json +799 -2
- package/package.json +16 -4
- package/api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -4
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -29
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -31
- package/src/accounts.ts +0 -149
- package/src/api-credentials.ts +0 -31
- package/src/approval-auth.test.ts +0 -17
- package/src/approval-auth.ts +0 -27
- package/src/bot-preflight.test.ts +0 -135
- package/src/bot-preflight.ts +0 -183
- package/src/channel-api.ts +0 -5
- package/src/channel.adapters.ts +0 -52
- package/src/channel.core.test.ts +0 -75
- package/src/channel.lifecycle.test.ts +0 -91
- package/src/channel.status.test.ts +0 -28
- package/src/channel.ts +0 -225
- package/src/config-schema.ts +0 -79
- package/src/core.test.ts +0 -325
- package/src/doctor-contract.ts +0 -9
- package/src/doctor.test.ts +0 -87
- package/src/doctor.ts +0 -40
- package/src/gateway.ts +0 -109
- package/src/inbound.authz.test.ts +0 -146
- package/src/inbound.behavior.test.ts +0 -309
- package/src/inbound.ts +0 -392
- package/src/message-actions.test.ts +0 -270
- package/src/message-actions.ts +0 -82
- package/src/message-adapter.ts +0 -28
- package/src/monitor-runtime.ts +0 -138
- package/src/monitor.replay.test.ts +0 -276
- package/src/monitor.test-fixtures.ts +0 -30
- package/src/monitor.test-harness.ts +0 -59
- package/src/monitor.ts +0 -385
- package/src/normalize.ts +0 -44
- package/src/policy.ts +0 -111
- package/src/replay-guard.ts +0 -128
- package/src/room-info.test.ts +0 -160
- package/src/room-info.ts +0 -130
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -103
- package/src/secret-input.ts +0 -4
- package/src/send.cfg-threading.test.ts +0 -359
- package/src/send.runtime.ts +0 -8
- package/src/send.ts +0 -269
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -250
- package/src/setup-surface.ts +0 -195
- package/src/setup.test.ts +0 -445
- package/src/signature.ts +0 -82
- package/src/types.ts +0 -195
- package/tsconfig.json +0 -16
package/src/bot-preflight.ts
DELETED
|
@@ -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
|
-
}
|
package/src/channel-api.ts
DELETED
|
@@ -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";
|
package/src/channel.adapters.ts
DELETED
|
@@ -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
|
-
};
|
package/src/channel.core.test.ts
DELETED
|
@@ -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
|
-
});
|