






















@@ -0,0 +1,181 @@
1+import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
2+import { fetchWithSsrFGuard } from "../runtime-api.js";
3+import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
4+import { resolveNextcloudTalkApiCredentials } from "./api-credentials.js";
5+import { ssrfPolicyFromPrivateNetworkOptIn } from "./send.runtime.js";
6+7+const BOT_FEATURE_RESPONSE = 2;
8+9+type NextcloudTalkBotAdminEntry = {
10+id?: number | string;
11+name?: string;
12+url?: string;
13+features?: number | string;
14+};
15+16+export type NextcloudTalkBotResponseFeatureProbe = {
17+ok: boolean;
18+skipped?: boolean;
19+code:
20+| "ok"
21+| "missing_api_credentials"
22+| "missing_webhook_url"
23+| "missing_base_url"
24+| "bot_not_found"
25+| "missing_response_feature"
26+| "api_error"
27+| "request_failed";
28+message: string;
29+botId?: string;
30+botName?: string;
31+features?: number;
32+status?: number;
33+};
34+35+function normalizeUrlForMatch(value: string | undefined): string {
36+if (!value?.trim()) {
37+return "";
38+}
39+try {
40+const url = new URL(value.trim());
41+url.hash = "";
42+return url.toString().replace(/\/$/, "");
43+} catch {
44+return value.trim().replace(/\/$/, "");
45+}
46+}
47+48+function coerceFeatureMask(value: unknown): number | undefined {
49+if (typeof value === "number" && Number.isFinite(value)) {
50+return value;
51+}
52+if (typeof value === "string" && value.trim()) {
53+const parsed = Number.parseInt(value, 10);
54+return Number.isFinite(parsed) ? parsed : undefined;
55+}
56+return undefined;
57+}
58+59+function formatMissingResponseFeatureMessage(bot: NextcloudTalkBotAdminEntry, features?: number) {
60+const id = bot.id == null ? "unknown" : String(bot.id);
61+const name = bot.name?.trim() || "matching bot";
62+const featureText = typeof features === "number" ? ` (features=${features})` : "";
63+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.`;
64+}
65+66+export async function probeNextcloudTalkBotResponseFeature(params: {
67+account: ResolvedNextcloudTalkAccount;
68+timeoutMs?: number;
69+}): Promise<NextcloudTalkBotResponseFeatureProbe> {
70+const { account, timeoutMs } = params;
71+const baseUrl = account.baseUrl?.trim();
72+if (!baseUrl) {
73+return {
74+ok: true,
75+skipped: true,
76+code: "missing_base_url",
77+message: "Nextcloud Talk bot response feature probe skipped: baseUrl is not configured.",
78+};
79+}
80+81+const webhookUrl = normalizeUrlForMatch(account.config.webhookPublicUrl);
82+if (!webhookUrl) {
83+return {
84+ok: true,
85+skipped: true,
86+code: "missing_webhook_url",
87+message:
88+"Nextcloud Talk bot response feature probe skipped: webhookPublicUrl is not configured.",
89+};
90+}
91+92+const credentials = resolveNextcloudTalkApiCredentials({
93+apiUser: account.config.apiUser,
94+apiPassword: account.config.apiPassword,
95+apiPasswordFile: account.config.apiPasswordFile,
96+});
97+if (!credentials) {
98+return {
99+ok: true,
100+skipped: true,
101+code: "missing_api_credentials",
102+message:
103+"Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured.",
104+};
105+}
106+107+const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/admin`;
108+const auth = Buffer.from(`${credentials.apiUser}:${credentials.apiPassword}`, "utf-8").toString(
109+"base64",
110+);
111+112+try {
113+const { response, release } = await fetchWithSsrFGuard({
114+ url,
115+init: {
116+method: "GET",
117+headers: {
118+Authorization: `Basic ${auth}`,
119+"OCS-APIRequest": "true",
120+Accept: "application/json",
121+},
122+},
123+auditContext: "nextcloud-talk.bot-response-preflight",
124+policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
125+ timeoutMs,
126+});
127+try {
128+if (!response.ok) {
129+const body = await response.text().catch(() => "");
130+return {
131+ok: false,
132+code: "api_error",
133+status: response.status,
134+message: `Nextcloud Talk bot response feature probe failed (${response.status})${body ? `: ${body}` : ""}`,
135+};
136+}
137+138+const payload = (await response.json()) as {
139+ocs?: { data?: NextcloudTalkBotAdminEntry[] };
140+};
141+const bots = Array.isArray(payload.ocs?.data) ? payload.ocs.data : [];
142+const bot = bots.find((entry) => normalizeUrlForMatch(entry.url) === webhookUrl);
143+if (!bot) {
144+return {
145+ok: false,
146+code: "bot_not_found",
147+message: `Nextcloud Talk bot response feature probe could not find a bot with webhook URL ${webhookUrl}.`,
148+};
149+}
150+151+const features = coerceFeatureMask(bot.features);
152+if (features == null || (features & BOT_FEATURE_RESPONSE) !== BOT_FEATURE_RESPONSE) {
153+return {
154+ok: false,
155+code: "missing_response_feature",
156+botId: bot.id == null ? undefined : String(bot.id),
157+botName: bot.name,
158+ features,
159+message: formatMissingResponseFeatureMessage(bot, features),
160+};
161+}
162+163+return {
164+ok: true,
165+code: "ok",
166+botId: bot.id == null ? undefined : String(bot.id),
167+botName: bot.name,
168+ features,
169+message: `Nextcloud Talk bot "${bot.name ?? bot.id ?? "matching bot"}" has the response feature.`,
170+};
171+} finally {
172+await release();
173+}
174+} catch (error) {
175+return {
176+ok: false,
177+code: "request_failed",
178+message: `Nextcloud Talk bot response feature probe failed: ${formatErrorMessage(error)}`,
179+};
180+}
181+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。