Files
openclaw/extensions/nextcloud-talk/src/setup-core.ts
Peter Steinberger 99d662473c fix(channels): fail-fast headless channel setup with plugin-declared env contracts (#122530)
* fix(channels): validate headless channel setup

* docs(channels): document headless provisioning

* fix(channels): repair setup metadata typing

* chore(channels): regenerate official channel catalog for env metadata

* fix(slack): keep mode-conditional env contract plugin-owned

Static --use-env declaration keeps only the unconditional SLACK_BOT_TOKEN;
socket-vs-HTTP conditional requirements (app token, signing secret) stay in
Slack's own setup validation so HTTP mode no longer demands an irrelevant
SLACK_APP_TOKEN.

* chore(sdk): regenerate api baselines and catalog after rebase

* fix(slack): align manifest env declaration with runtime contract

* chore(sdk): regenerate api baselines after rebase

* chore(sdk): regenerate api baselines after rebase

* chore(sdk): regenerate api baselines after rebase
2026-08-12 17:12:15 +00:00

253 lines
8.0 KiB
TypeScript

import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
// Nextcloud Talk plugin module implements setup core behavior.
import {
defineChannelSetupContract,
type ChannelSetupAdapter,
type ChannelSetupInput,
} from "openclaw/plugin-sdk/channel-setup";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
import {
applyAccountNameToChannelSection,
patchScopedAccountConfig,
} from "openclaw/plugin-sdk/setup";
import {
createSetupInputPresenceValidator,
mergeAllowFromEntries,
promptParsedAllowFromForAccount,
resolveSetupAccountId,
createSetupTranslator,
type WizardPrompter,
} from "openclaw/plugin-sdk/setup-runtime";
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
import {
normalizeLowercaseStringOrEmpty,
readNonEmptyStringPreservingWhitespace as readNonEmptyUntrimmedString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveDefaultNextcloudTalkAccountId, resolveNextcloudTalkAccount } from "./accounts.js";
import type { CoreConfig } from "./types.js";
const t = createSetupTranslator();
const channel = "nextcloud-talk" as const;
type NextcloudSetupInput = ChannelSetupInput & {
baseUrl?: string;
secret?: string;
secretFile?: string;
url?: string;
password?: string;
};
export function normalizeNextcloudTalkBaseUrl(value: string | undefined): string {
return value?.trim().replace(/\/+$/, "") ?? "";
}
export function validateNextcloudTalkBaseUrl(value: string): string | undefined {
if (!value) {
return "Required";
}
if (!value.startsWith("http://") && !value.startsWith("https://")) {
return "URL must start with http:// or https://";
}
return undefined;
}
export function setNextcloudTalkAccountConfig(
cfg: CoreConfig,
accountId: string,
updates: Record<string, unknown>,
clearFields?: readonly string[],
): CoreConfig {
return patchScopedAccountConfig({
cfg,
channelKey: channel,
accountId,
patch: updates,
...(clearFields ? { clearFields } : {}),
}) as CoreConfig;
}
async function promptNextcloudTalkAllowFrom(params: {
cfg: CoreConfig;
prompter: WizardPrompter;
accountId: string;
}): Promise<CoreConfig> {
return await promptParsedAllowFromForAccount({
cfg: params.cfg,
accountId: params.accountId,
defaultAccountId: params.accountId,
prompter: params.prompter,
noteTitle: t("wizard.nextcloudTalk.userIdTitle"),
noteLines: [
t("wizard.nextcloudTalk.userIdHelpAdmin"),
t("wizard.nextcloudTalk.userIdHelpLogs"),
t("wizard.nextcloudTalk.userIdHelpLowercase"),
t("wizard.channels.docs", {
link: formatDocsLink("/channels/nextcloud-talk", "nextcloud-talk"),
}),
],
message: t("wizard.nextcloudTalk.allowFromPrompt"),
placeholder: "username",
parseEntries: (raw) => ({
entries: raw
.split(/[\n,;]+/g)
.map(normalizeLowercaseStringOrEmpty)
.filter(Boolean),
}),
getExistingAllowFrom: ({ cfg, accountId }) =>
resolveNextcloudTalkAccount({ cfg, accountId }).config.allowFrom ?? [],
mergeEntries: ({ existing, parsed }) =>
mergeAllowFromEntries(
existing.map((value) => normalizeLowercaseStringOrEmpty(String(value))),
parsed,
),
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
setNextcloudTalkAccountConfig(cfg, accountId, {
dmPolicy: "allowlist",
allowFrom,
}),
});
}
async function promptNextcloudTalkAllowFromForAccount(params: {
cfg: OpenClawConfig;
prompter: WizardPrompter;
accountId?: string;
}): Promise<OpenClawConfig> {
const accountId = resolveSetupAccountId({
accountId: params.accountId,
defaultAccountId: resolveDefaultNextcloudTalkAccountId(params.cfg as CoreConfig),
});
return await promptNextcloudTalkAllowFrom({
cfg: params.cfg as CoreConfig,
prompter: params.prompter,
accountId,
});
}
export const nextcloudTalkDmPolicy = createChannelDmPolicy({
label: "Nextcloud Talk",
channel,
resolveAccount: (cfg, accountId) =>
resolveNextcloudTalkAccount({
cfg: cfg as CoreConfig,
accountId: accountId ?? resolveDefaultNextcloudTalkAccountId(cfg as CoreConfig),
}),
applyPatch: ({ cfg, account, patch }) =>
setNextcloudTalkAccountConfig(cfg as CoreConfig, account.accountId, patch),
promptAllowFrom: promptNextcloudTalkAllowFromForAccount,
});
const nextcloudTalkSetupAdapter: ChannelSetupAdapter = {
singleAccountKeysToMove: ["rooms"],
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
prepareAccountConfigInput: ({ input }) => {
const setupInput = input as NextcloudSetupInput;
return {
...setupInput,
baseUrl: setupInput.baseUrl ?? readNonEmptyUntrimmedString(setupInput.url),
secret:
setupInput.secret ??
readNonEmptyUntrimmedString(setupInput.token) ??
readNonEmptyUntrimmedString(setupInput.password),
secretFile: setupInput.secretFile ?? readNonEmptyUntrimmedString(setupInput.tokenFile),
};
},
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name,
}),
validateInput: createSetupInputPresenceValidator({
defaultAccountOnlyEnvError:
"NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
validate: ({ input }) => {
const setupInput = input as NextcloudSetupInput;
if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) {
return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
}
const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
if (!normalizedBaseUrl) {
return "Nextcloud Talk requires --base-url.";
}
const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
if (baseUrlError) {
return baseUrlError;
}
return null;
},
}),
applyAccountConfig: ({ cfg, accountId, input }) => {
const setupInput = input as NextcloudSetupInput;
const namedConfig = applyAccountNameToChannelSection({
cfg,
channelKey: channel,
accountId,
name: setupInput.name,
});
const patch = {
baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
...(setupInput.useEnv
? {}
: setupInput.secretFile
? { botSecretFile: setupInput.secretFile }
: setupInput.secret
? { botSecret: setupInput.secret }
: {}),
};
return setNextcloudTalkAccountConfig(
namedConfig as CoreConfig,
accountId,
patch,
setupInput.useEnv ? ["botSecret", "botSecretFile"] : undefined,
);
},
};
export const nextcloudTalkSetupContract = defineChannelSetupContract({
fields: {
baseUrl: {
kind: "string",
cli: { flags: "--base-url <url>", description: "Nextcloud base URL" },
},
url: {
kind: "string",
cli: { flags: "--url <url>", description: "Legacy Nextcloud base URL alias" },
},
secret: {
kind: "string",
sensitive: true,
cli: { flags: "--secret <secret>", description: "Nextcloud Talk bot secret" },
},
token: {
kind: "string",
sensitive: true,
cli: { flags: "--token <secret>", description: "Legacy Nextcloud bot secret alias" },
},
password: {
kind: "string",
sensitive: true,
cli: { flags: "--password <secret>", description: "Legacy Nextcloud bot secret alias" },
},
secretFile: {
kind: "string",
sensitive: true,
cli: { flags: "--secret-file <path>", description: "Nextcloud Talk bot secret file" },
},
tokenFile: {
kind: "string",
sensitive: true,
cli: { flags: "--token-file <path>", description: "Legacy Nextcloud bot secret file alias" },
},
useEnv: {
kind: "boolean",
cli: { flags: "--use-env", description: "Use Nextcloud Talk environment credentials" },
envVars: ["NEXTCLOUD_TALK_BOT_SECRET"],
},
},
legacyAdapter: nextcloudTalkSetupAdapter,
});