UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,078 lines (1,077 loc) 44 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { t as formatDocsLink } from "./links-CsLBrRff.js"; import { s as hasConfiguredSecretInput } from "./types.secrets-_0JOMGE5.js"; import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js"; import { At as boolean, Et as array, Nn as record, Rn as string, Tn as object, Xn as union, dn as literal, wn as number, yt as _enum } from "./schemas-6cH6bZ7o.js"; import { h as MarkdownConfigSchema, l as GroupPolicySchema, n as BlockStreamingCoalesceSchema, o as DmPolicySchema, z as requireOpenAllowFrom } from "./zod-schema.core-1wQTyhOa.js"; import { r as buildChannelConfigSchema } from "./config-schema-CO2ikh7C.js"; import { p as resolvePayloadMediaUrls, x as sendTextMediaPayload } from "./reply-payload-D-VMfpYI.js"; import { f as renderMessagePresentationFallbackText, h as resolveMessagePresentationControlValue, l as normalizeMessagePresentation } from "./payload-CZlThETZ.js"; import { n as resolveChannelGroupRequireMention } from "./group-policy-CShy7eWv.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-D-7_JraP.js"; import { s as createScopedChannelConfigAdapter, t as adaptScopedAccountAccessor } from "./channel-config-helpers-lj5quus3.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { t as chunkTextForOutbound } from "./text-chunking-D45TeeEs.js"; import { n as describeAccountSnapshot } from "./account-helpers-pcH3lGFY.js"; import { a as createSetupInputPresenceValidator, n as applySetupAccountConfigPatch, s as migrateBaseNameToDefaultAccount, t as applyAccountNameToChannelSection } from "./setup-helpers-DO5nSeNy.js"; import { d as stripTargetKindPrefix, i as createChatChannelPlugin, n as buildThreadAwareOutboundSessionRoute, t as buildChannelOutboundSessionRoute, u as stripChannelTargetPrefix } from "./core-DSxVv-v1.js"; import "./channel-core-IukkNCd-.js"; import { r as buildSecretInputSchema } from "./secret-input-DVCzFGxN.js"; import { t as createAccountStatusSink } from "./channel-lifecycle.core-Bfh0_sXw.js"; import { n as formatNormalizedAllowFromEntries } from "./allow-from-DCaV3hzI.js"; import { n as createDangerousNameMatchingMutableAllowlistWarningCollector, r as createRestrictSendersChannelSecurity } from "./channel-policy-CIW58SA5.js"; import { t as createChannelDirectoryAdapter } from "./directory-runtime-om8jJAZt.js"; import { t as createSetupTranslator } from "./i18n-7g_f4oaD.js"; import { f as createStandardChannelSetupStatus } from "./setup-wizard-helpers-Dv-t1nb8.js"; import "./setup-DHfdBmUU.js"; import "./setup-runtime-BvLK-H0G.js"; import { t as resolveApprovalApprovers } from "./approval-approvers-CM-4zRvR.js"; import { t as createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers-DU3OxkhL.js"; import { w as createChannelMessageAdapterFromOutbound } from "./channel-outbound-B3_Zy-kG.js"; import { o as isPrivateNetworkOptInEnabled } from "./ssrf-policy-sZLyyYht.js"; import "./ssrf-runtime-BOGN5pUi.js"; import { d as createDefaultChannelRuntimeState, u as createComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js"; import { n as buildPassiveProbedChannelStatusSummary } from "./extension-shared-B8fkO3TV.js"; import "./channel-config-primitives-Bk3iiMir.js"; import { r as createLoggedPairingApprovalNotifier } from "./channel-pairing-Bk6_JhTm.js"; import { i as createAttachedChannelResultAdapter, t as attachChannelToResult } from "./channel-send-result-Dn_C6AJS.js"; import { g as normalizeMattermostBaseUrl, i as resolveMattermostReplyToMode, n as resolveDefaultMattermostAccountId, r as resolveMattermostAccount, t as listMattermostAccountIds } from "./accounts-BVsVtEWg.js"; import { t as resolveMattermostGatewayAuthBypassPaths } from "./gateway-auth-bypass-BHgOAkk8.js"; import { n as normalizeCompatibilityConfig, t as legacyConfigRules } from "./doctor-contract--1N77YXv.js"; import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-C8luCFfV.js"; //#region extensions/mattermost/src/approval-auth.ts const MATTERMOST_USER_ID_RE = /^[a-z0-9]{26}$/; function normalizeMattermostApproverId(value) { const lowered = normalizeLowercaseStringOrEmpty(String(value).trim().replace(/^(mattermost|user):/i, "").replace(/^@/, "").trim()); return MATTERMOST_USER_ID_RE.test(lowered) ? lowered : void 0; } const mattermostApprovalAuth = createResolvedApproverActionAuthAdapter({ channelLabel: "Mattermost", resolveApprovers: ({ cfg, accountId }) => { const account = resolveMattermostAccount({ cfg, accountId }).config; return resolveApprovalApprovers({ allowFrom: account.allowFrom, normalizeApprover: normalizeMattermostApproverId }); }, normalizeSenderId: (value) => normalizeMattermostApproverId(value) }); //#endregion //#region extensions/mattermost/src/channel-config-shared.ts const mattermostMeta = { id: "mattermost", label: "Mattermost", selectionLabel: "Mattermost (plugin)", detailLabel: "Mattermost Bot", docsPath: "/channels/mattermost", docsLabel: "mattermost", blurb: "self-hosted Slack-style chat; install the plugin to enable.", systemImage: "bubble.left.and.bubble.right", order: 65, quickstartAllowFrom: true }; function normalizeMattermostAllowEntry(entry) { return normalizeLowercaseStringOrEmpty(entry.trim().replace(/^(mattermost|user):/i, "").replace(/^@/, "")); } function formatMattermostAllowEntry(entry) { const trimmed = entry.trim(); if (!trimmed) return ""; if (trimmed.startsWith("@")) { const username = trimmed.slice(1).trim(); return username ? `@${normalizeLowercaseStringOrEmpty(username)}` : ""; } return normalizeLowercaseStringOrEmpty(trimmed.replace(/^(mattermost|user):/i, "")); } const mattermostConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: "mattermost", listAccountIds: listMattermostAccountIds, resolveAccount: adaptScopedAccountAccessor(resolveMattermostAccount), defaultAccountId: resolveDefaultMattermostAccountId, clearBaseFields: [ "botToken", "baseUrl", "name" ], resolveAllowFrom: (account) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({ allowFrom, normalizeEntry: formatMattermostAllowEntry }) }); function isMattermostConfigured$1(account) { return Boolean(account.botToken && account.baseUrl); } function describeMattermostAccount(account) { return describeAccountSnapshot({ account, configured: isMattermostConfigured$1(account), extra: { botTokenSource: account.botTokenSource, baseUrl: account.baseUrl } }); } //#endregion //#region extensions/mattermost/src/config-schema-core.ts const MattermostGroupSchema = object({ /** Whether mentions are required to trigger the bot in this group. */ requireMention: boolean().optional() }).strict(); function requireMattermostOpenAllowFrom(params) { requireOpenAllowFrom({ policy: params.policy, allowFrom: params.allowFrom, ctx: params.ctx, path: ["allowFrom"], message: "channels.mattermost.dmPolicy=\"open\" requires channels.mattermost.allowFrom to include \"*\"" }); } const DmChannelRetrySchema = object({ /** Maximum number of retry attempts for DM channel creation (default: 3) */ maxRetries: number().int().min(0).max(10).optional(), /** Initial delay in milliseconds before first retry (default: 1000) */ initialDelayMs: number().int().min(100).max(6e4).optional(), /** Maximum delay in milliseconds between retries (default: 10000) */ maxDelayMs: number().int().min(1e3).max(6e4).optional(), /** Timeout for each individual DM channel creation request in milliseconds (default: 30000) */ timeoutMs: number().int().min(5e3).max(12e4).optional() }).strict().refine((data) => { if (data.initialDelayMs !== void 0 && data.maxDelayMs !== void 0) return data.initialDelayMs <= data.maxDelayMs; return true; }, { message: "initialDelayMs must be less than or equal to maxDelayMs", path: ["initialDelayMs"] }).optional(); const MattermostSlashCommandsSchema = object({ /** Enable native slash commands. "auto" resolves to false (opt-in). */ native: union([boolean(), literal("auto")]).optional(), /** Also register skill-based commands. */ nativeSkills: union([boolean(), literal("auto")]).optional(), /** Path for the callback endpoint on the gateway HTTP server. */ callbackPath: string().optional(), /** Explicit callback URL (e.g. behind reverse proxy). */ callbackUrl: string().optional() }).strict().optional(); const MattermostNetworkSchema = object({ /** Dangerous opt-in for self-hosted Mattermost on trusted private/internal hosts. */ dangerouslyAllowPrivateNetwork: boolean().optional() }).strict().optional(); const MattermostStreamingModeSchema = _enum([ "off", "partial", "block", "progress" ]); const MattermostStreamingProgressSchema = object({ label: union([string(), literal(false)]).optional(), labels: array(string()).optional(), maxLines: number().int().positive().optional(), maxLineChars: number().int().positive().optional(), toolProgress: boolean().optional() }).strict(); const MattermostStreamingPreviewSchema = object({ toolProgress: boolean().optional() }).strict(); const MattermostStreamingBlockSchema = object({ enabled: boolean().optional(), coalesce: BlockStreamingCoalesceSchema.optional() }).strict(); const MattermostStreamingSchema = union([ MattermostStreamingModeSchema, boolean(), object({ mode: MattermostStreamingModeSchema.optional(), chunkMode: _enum(["length", "newline"]).optional(), preview: MattermostStreamingPreviewSchema.optional(), progress: MattermostStreamingProgressSchema.optional(), block: MattermostStreamingBlockSchema.optional() }).strict() ]); const MattermostAccountSchemaBase = object({ name: string().optional(), capabilities: array(string()).optional(), dangerouslyAllowNameMatching: boolean().optional(), markdown: MarkdownConfigSchema, enabled: boolean().optional(), configWrites: boolean().optional(), botToken: buildSecretInputSchema().optional(), baseUrl: string().optional(), chatmode: _enum([ "oncall", "onmessage", "onchar" ]).optional(), oncharPrefixes: array(string()).optional(), requireMention: boolean().optional(), dmPolicy: DmPolicySchema.optional().default("pairing"), allowFrom: array(union([string(), number()])).optional(), groupAllowFrom: array(union([string(), number()])).optional(), groupPolicy: GroupPolicySchema.optional().default("allowlist"), textChunkLimit: number().int().positive().optional(), chunkMode: _enum(["length", "newline"]).optional(), streaming: MattermostStreamingSchema.optional(), blockStreaming: boolean().optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), replyToMode: _enum([ "off", "first", "all", "batched" ]).optional(), responsePrefix: string().optional(), actions: object({ reactions: boolean().optional() }).optional(), commands: MattermostSlashCommandsSchema, interactions: object({ callbackBaseUrl: string().optional(), allowedSourceIps: array(string()).optional() }).optional(), /** Per-group configuration (keyed by Mattermost channel ID or "*" for default). */ groups: record(string(), MattermostGroupSchema.optional()).optional(), /** Network policy overrides for self-hosted Mattermost on trusted private/internal hosts. */ network: MattermostNetworkSchema, /** Retry configuration for DM channel creation */ dmChannelRetry: DmChannelRetrySchema }).strict(); const MattermostAccountSchema = MattermostAccountSchemaBase.superRefine((value, ctx) => { requireMattermostOpenAllowFrom({ policy: value.dmPolicy, allowFrom: value.allowFrom, ctx }); }); //#endregion //#region extensions/mattermost/src/config-surface.ts const MattermostChannelConfigSchema = buildChannelConfigSchema(MattermostAccountSchemaBase.extend({ accounts: record(string(), MattermostAccountSchema.optional()).optional(), defaultAccount: string().optional() }).superRefine((value, ctx) => { requireMattermostOpenAllowFrom({ policy: value.dmPolicy, allowFrom: value.allowFrom, ctx }); }), { uiHints: { "": { label: "Mattermost", help: "Mattermost channel provider configuration for bot auth, access policy, slash commands, and preview streaming." }, dmPolicy: { label: "Mattermost DM Policy", help: "Direct message access control (\"pairing\" recommended). \"open\" requires channels.mattermost.allowFrom=[\"*\"]." }, streaming: { label: "Mattermost Streaming Mode", help: "Unified Mattermost stream preview mode: \"off\" | \"partial\" | \"block\" | \"progress\". \"progress\" keeps a single editable progress draft until final delivery." }, "streaming.mode": { label: "Mattermost Streaming Mode", help: "Canonical Mattermost preview mode: \"off\" | \"partial\" | \"block\" | \"progress\"." }, "streaming.progress.label": { label: "Mattermost Progress Label", help: "Initial progress draft title. Use \"auto\" for built-in single-word labels, a custom string, or false to hide the title." }, "streaming.progress.labels": { label: "Mattermost Progress Label Pool", help: "Candidate labels for streaming.progress.label=\"auto\". Leave unset to use OpenClaw built-in progress labels." }, "streaming.progress.maxLines": { label: "Mattermost Progress Max Lines", help: "Maximum number of compact progress lines to keep below the draft label (default: 8)." }, "streaming.progress.maxLineChars": { label: "Mattermost Progress Max Line Chars", help: "Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes." }, "streaming.progress.toolProgress": { label: "Mattermost Progress Tool Lines", help: "Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery." }, "streaming.progress.commandText": { label: "Mattermost Progress Command Text", help: "Command/exec detail in progress draft lines: \"raw\" preserves released behavior; \"status\" shows only the tool label." }, "streaming.preview.toolProgress": { label: "Mattermost Draft Tool Progress", help: "Show tool/progress activity in the live draft preview post (default: true). Set false to hide interim tool updates while the draft preview stays active." }, "streaming.preview.commandText": { label: "Mattermost Draft Command Text", help: "Command/exec detail in preview tool-progress lines: \"raw\" preserves released behavior; \"status\" shows only the tool label." }, "streaming.block.enabled": { label: "Mattermost Block Streaming Enabled", help: "Enable chunked block-style Mattermost preview delivery when channels.mattermost.streaming.mode=\"block\"." }, "streaming.block.coalesce": { label: "Mattermost Block Streaming Coalesce", help: "Merge streamed Mattermost block replies before final delivery." } } }); //#endregion //#region extensions/mattermost/src/doctor.ts function isMattermostMutableAllowEntry(raw) { const text = raw.trim(); if (!text || text === "*") return false; const lowered = normalizeLowercaseStringOrEmpty(text.replace(/^(mattermost|user):/i, "").replace(/^@/, "").trim()); if (/^[a-z0-9]{26}$/.test(lowered)) return false; return true; } const mattermostDoctor = { legacyConfigRules, normalizeCompatibilityConfig, collectMutableAllowlistWarnings: createDangerousNameMatchingMutableAllowlistWarningCollector({ channel: "mattermost", detector: isMattermostMutableAllowEntry, collectLists: (scope) => [{ pathLabel: `${scope.prefix}.allowFrom`, list: scope.account.allowFrom }, { pathLabel: `${scope.prefix}.groupAllowFrom`, list: scope.account.groupAllowFrom }] }) }; //#endregion //#region extensions/mattermost/src/group-mentions.ts function resolveMattermostGroupRequireMention(params) { const account = resolveMattermostAccount({ cfg: params.cfg, accountId: params.accountId }); const requireMentionOverride = typeof params.requireMentionOverride === "boolean" ? params.requireMentionOverride : account.requireMention; return resolveChannelGroupRequireMention({ cfg: params.cfg, channel: "mattermost", groupId: params.groupId, accountId: params.accountId, requireMentionOverride }); } //#endregion //#region extensions/mattermost/src/normalize.ts function normalizeMattermostMessagingTarget(raw) { const trimmed = raw.trim(); if (!trimmed) return; const lower = normalizeLowercaseStringOrEmpty(trimmed); if (lower.startsWith("channel:")) { const id = trimmed.slice(8).trim(); return id ? `channel:${id}` : void 0; } if (lower.startsWith("group:")) { const id = trimmed.slice(6).trim(); return id ? `channel:${id}` : void 0; } if (lower.startsWith("user:")) { const id = trimmed.slice(5).trim(); return id ? `user:${id}` : void 0; } if (lower.startsWith("mattermost:")) { const id = trimmed.slice(11).trim(); return id ? `user:${id}` : void 0; } if (trimmed.startsWith("@")) { const id = trimmed.slice(1).trim(); return id ? `@${id}` : void 0; } if (trimmed.startsWith("#")) return; } function looksLikeMattermostTargetId(raw, _normalized) { const trimmed = raw.trim(); if (!trimmed) return false; if (/^(user|channel|group|mattermost):/i.test(trimmed)) return true; if (trimmed.startsWith("@")) return true; return /^[a-z0-9]{26}$/i.test(trimmed) || /^[a-z0-9]{26}__[a-z0-9]{26}$/i.test(trimmed); } //#endregion //#region extensions/mattermost/src/session-route.ts function resolveMattermostOutboundSessionRoute(params) { let trimmed = stripChannelTargetPrefix(params.target, "mattermost"); if (!trimmed) return null; const lower = normalizeLowercaseStringOrEmpty(trimmed); const resolvedKind = params.resolvedTarget?.kind; const isUser = resolvedKind === "user" || resolvedKind !== "channel" && resolvedKind !== "group" && (lower.startsWith("user:") || trimmed.startsWith("@")); if (trimmed.startsWith("@")) trimmed = trimmed.slice(1).trim(); const rawId = stripTargetKindPrefix(trimmed); if (!rawId) return null; return buildThreadAwareOutboundSessionRoute({ route: buildChannelOutboundSessionRoute({ cfg: params.cfg, agentId: params.agentId, channel: "mattermost", accountId: params.accountId, peer: { kind: isUser ? "direct" : "channel", id: rawId }, chatType: isUser ? "direct" : "channel", from: isUser ? `mattermost:${rawId}` : `mattermost:channel:${rawId}`, to: isUser ? `user:${rawId}` : `channel:${rawId}` }), replyToId: params.replyToId, threadId: params.threadId, currentSessionKey: params.currentSessionKey, canRecoverCurrentThread: ({ route }) => route.chatType !== "direct" || (params.cfg.session?.dmScope ?? "main") !== "main" }); } //#endregion //#region extensions/mattermost/src/setup-core.ts const channel$1 = "mattermost"; function isMattermostConfigured(account) { return (Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken)) && Boolean(account.baseUrl); } function resolveMattermostAccountWithSecrets(cfg, accountId) { return resolveMattermostAccount({ cfg, accountId, allowUnresolvedSecretRef: true }); } function applyMattermostSetupConfigPatch(params) { const namedConfig = applyAccountNameToChannelSection({ cfg: params.cfg, channelKey: channel$1, accountId: params.accountId, name: params.name }); return applySetupAccountConfigPatch({ cfg: params.accountId !== "default" ? migrateBaseNameToDefaultAccount({ cfg: namedConfig, channelKey: channel$1 }) : namedConfig, channelKey: channel$1, accountId: params.accountId, patch: params.patch }); } const mattermostSetupAdapter = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ cfg, channelKey: channel$1, accountId, name }), validateInput: createSetupInputPresenceValidator({ defaultAccountOnlyEnvError: "Mattermost env vars can only be used for the default account.", whenNotUseEnv: [{ someOf: ["botToken", "token"], message: "Mattermost requires --bot-token and --http-url (or --use-env)." }, { someOf: ["httpUrl"], message: "Mattermost requires --bot-token and --http-url (or --use-env)." }], validate: ({ input }) => { const token = input.botToken ?? input.token; const baseUrl = normalizeMattermostBaseUrl(input.httpUrl); if (!input.useEnv && (!token || !baseUrl)) return "Mattermost requires --bot-token and --http-url (or --use-env)."; if (input.httpUrl && !baseUrl) return "Mattermost --http-url must include a valid base URL."; return null; } }), applyAccountConfig: ({ cfg, accountId, input }) => { const token = input.botToken ?? input.token; const baseUrl = normalizeMattermostBaseUrl(input.httpUrl); return applyMattermostSetupConfigPatch({ cfg, accountId, name: input.name, patch: input.useEnv ? {} : { ...token ? { botToken: token } : {}, ...baseUrl ? { baseUrl } : {} } }); } }; //#endregion //#region extensions/mattermost/src/setup-surface.ts const t = createSetupTranslator(); const channel = "mattermost"; const mattermostSetupWizard = { channel, status: createStandardChannelSetupStatus({ channelLabel: "Mattermost", configuredLabel: t("wizard.channels.statusConfigured"), unconfiguredLabel: t("wizard.channels.statusNeedsTokenUrl"), configuredHint: t("wizard.channels.statusConfigured"), unconfiguredHint: t("wizard.channels.statusNeedsSetup"), configuredScore: 2, unconfiguredScore: 1, resolveConfigured: ({ cfg, accountId }) => isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, accountId ?? "default")) }), introNote: { title: t("wizard.mattermost.botTokenTitle"), lines: [ t("wizard.mattermost.helpOpenConsole"), t("wizard.mattermost.helpCreateBot"), t("wizard.mattermost.helpBaseUrl"), t("wizard.mattermost.helpBotMember"), t("wizard.channels.docs", { link: formatDocsLink("/mattermost", "mattermost") }) ], shouldShow: ({ cfg, accountId }) => !isMattermostConfigured(resolveMattermostAccountWithSecrets(cfg, accountId)) }, envShortcut: { prompt: t("wizard.mattermost.envPrompt"), preferredEnvVar: "MATTERMOST_BOT_TOKEN", isAvailable: ({ cfg, accountId }) => { if (accountId !== "default") return false; const resolvedAccount = resolveMattermostAccountWithSecrets(cfg, accountId); const hasConfigValues = hasConfiguredSecretInput(resolvedAccount.config.botToken) || Boolean(resolvedAccount.config.baseUrl?.trim()); return Boolean(process.env.MATTERMOST_BOT_TOKEN?.trim() && process.env.MATTERMOST_URL?.trim() && !hasConfigValues); }, apply: ({ cfg, accountId }) => applySetupAccountConfigPatch({ cfg, channelKey: channel, accountId, patch: {} }) }, credentials: [{ inputKey: "botToken", providerHint: channel, credentialLabel: t("wizard.mattermost.botToken"), preferredEnvVar: "MATTERMOST_BOT_TOKEN", envPrompt: t("wizard.mattermost.envPrompt"), keepPrompt: t("wizard.mattermost.botTokenKeep"), inputPrompt: t("wizard.mattermost.botTokenInput"), inspect: ({ cfg, accountId }) => { const resolvedAccount = resolveMattermostAccountWithSecrets(cfg, accountId); return { accountConfigured: isMattermostConfigured(resolvedAccount), hasConfiguredValue: hasConfiguredSecretInput(resolvedAccount.config.botToken) }; }, applySet: async ({ cfg, accountId, value }) => applyMattermostSetupConfigPatch({ cfg, accountId, patch: { botToken: value } }) }], textInputs: [{ inputKey: "httpUrl", message: t("wizard.mattermost.baseUrlPrompt"), confirmCurrentValue: false, currentValue: ({ cfg, accountId }) => resolveMattermostAccountWithSecrets(cfg, accountId).baseUrl ?? process.env.MATTERMOST_URL?.trim(), initialValue: ({ cfg, accountId }) => resolveMattermostAccountWithSecrets(cfg, accountId).baseUrl ?? process.env.MATTERMOST_URL?.trim(), shouldPrompt: ({ cfg, accountId, credentialValues, currentValue }) => { const resolvedAccount = resolveMattermostAccountWithSecrets(cfg, accountId); const tokenConfigured = Boolean(resolvedAccount.botToken?.trim()) || hasConfiguredSecretInput(resolvedAccount.config.botToken); return Boolean(credentialValues.botToken) || !tokenConfigured || !currentValue; }, validate: ({ value }) => normalizeMattermostBaseUrl(value) ? void 0 : "Mattermost base URL must include a valid base URL.", normalizeValue: ({ value }) => normalizeMattermostBaseUrl(value) ?? value.trim(), applySet: async ({ cfg, accountId, value }) => applyMattermostSetupConfigPatch({ cfg, accountId, patch: { baseUrl: value } }) }], disable: (cfg) => ({ ...cfg, channels: { ...cfg.channels, mattermost: { ...cfg.channels?.mattermost, enabled: false } } }) }; //#endregion //#region extensions/mattermost/src/channel.ts const loadMattermostChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-CJivNz1p.js")); function buildMattermostPresentationButtons(presentation) { return presentation.blocks.filter((block) => block.type === "buttons").map((block) => block.buttons.flatMap((button) => { if (button.action) return []; const value = resolveMessagePresentationControlValue(button); return value ? [{ id: value, text: button.label, callback_data: value, context: { callback_data: value }, style: button.style }] : []; })).filter((row) => row.length > 0); } const MATTERMOST_PRESENTATION_CAPABILITIES = { supported: true, buttons: true, selects: false, context: true, divider: false, limits: { text: { markdownDialect: "markdown" } } }; function hasMattermostPresentationButtons(presentation) { return buildMattermostPresentationButtons(presentation).some((row) => row.length > 0); } function readMattermostPresentationButtons(payload) { const buttons = (payload.channelData?.mattermost)?.presentationButtons; return Array.isArray(buttons) ? buttons : void 0; } const mattermostSecurityAdapter = createRestrictSendersChannelSecurity({ channelKey: "mattermost", resolveDmPolicy: (account) => account.config.dmPolicy, resolveDmAllowFrom: (account) => account.config.allowFrom, resolveGroupPolicy: (account) => account.config.groupPolicy, surface: "Mattermost channels", openScope: "any member", groupPolicyPath: "channels.mattermost.groupPolicy", groupAllowFromPath: "channels.mattermost.groupAllowFrom", policyPathSuffix: "dmPolicy", normalizeDmEntry: (raw) => normalizeMattermostAllowEntry(raw) }); function describeMattermostMessageTool({ cfg, accountId }) { const enabledAccounts = (accountId ? [resolveMattermostAccount({ cfg, accountId })] : listMattermostAccountIds(cfg).map((listedAccountId) => resolveMattermostAccount({ cfg, accountId: listedAccountId }))).filter((account) => account.enabled).filter((account) => Boolean(account.botToken?.trim() && account.baseUrl?.trim())); const actions = []; if (enabledAccounts.length > 0) actions.push("send"); const baseReactions = (cfg.channels?.mattermost?.actions)?.reactions; if (enabledAccounts.some((account) => { return account.config.actions?.reactions ?? baseReactions ?? true; })) actions.push("react"); return { actions, capabilities: enabledAccounts.length > 0 ? ["presentation"] : [] }; } function hasConfiguredMattermostDirectoryAccount({ cfg, accountId }) { return (accountId ? [resolveMattermostAccount({ cfg, accountId })] : listMattermostAccountIds(cfg).map((listedAccountId) => resolveMattermostAccount({ cfg, accountId: listedAccountId }))).some((account) => Boolean(account.enabled && account.botToken?.trim() && account.baseUrl?.trim())); } async function listMattermostDirectoryGroups(params) { if (!hasConfiguredMattermostDirectoryAccount(params)) return []; return (await loadMattermostChannelRuntime()).listMattermostDirectoryGroups(params); } async function listMattermostDirectoryPeers(params) { if (!hasConfiguredMattermostDirectoryAccount(params)) return []; return (await loadMattermostChannelRuntime()).listMattermostDirectoryPeers(params); } const mattermostMessageActions = { describeMessageTool: describeMattermostMessageTool, supportsAction: ({ action }) => { return action === "send" || action === "react"; }, handleAction: async ({ action, params, cfg, accountId, mediaAccess, mediaLocalRoots, mediaReadFile }) => { if (action === "react") { const resolvedAccountId = accountId ?? resolveDefaultMattermostAccountId(cfg); const mattermostConfig = cfg.channels?.mattermost; if (!(resolveMattermostAccount({ cfg, accountId: resolvedAccountId }).config.actions?.reactions ?? mattermostConfig?.actions?.reactions ?? true)) throw new Error("Mattermost reactions are disabled in config"); const { postId, emojiName, remove } = parseMattermostReactActionParams(params); if (remove) { const result = await (await loadMattermostChannelRuntime()).removeMattermostReaction({ cfg, postId, emojiName, accountId: resolvedAccountId }); if (!result.ok) throw new Error(result.error); return { content: [{ type: "text", text: `Removed reaction :${emojiName}: from ${postId}` }], details: {} }; } const result = await (await loadMattermostChannelRuntime()).addMattermostReaction({ cfg, postId, emojiName, accountId: resolvedAccountId }); if (!result.ok) throw new Error(result.error); return { content: [{ type: "text", text: `Reacted with :${emojiName}: on ${postId}` }], details: {} }; } if (action !== "send") throw new Error(`Unsupported Mattermost action: ${action}`); const to = typeof params.to === "string" ? params.to.trim() : typeof params.target === "string" ? params.target.trim() : ""; if (!to) throw new Error("Mattermost send requires a target (to)."); const presentation = normalizeMessagePresentation(params.presentation); const message = presentation ? renderMessagePresentationFallbackText({ text: typeof params.message === "string" ? params.message : "", presentation }) : typeof params.message === "string" ? params.message : ""; const replyToId = normalizeOptionalString(params.replyToId) ?? normalizeOptionalString(params.replyTo); const resolvedAccountId = accountId || void 0; const attachmentMedia = collectMattermostAttachmentMedia(params); if (attachmentMedia.hasUnsupportedAttachmentPayload) throw new Error("Mattermost send attachments require media, mediaUrl, path, filePath, fileUrl, mediaUrls, or attachments[] with one of those fields; buffer/base64 payloads are not supported."); if (attachmentMedia.mediaUrls.length > 1) throw new Error("Mattermost send supports one attachment per message; split multiple mediaUrls or attachments[] entries into separate sends."); const buttons = presentation ? buildMattermostPresentationButtons(presentation) : []; const result = await (await loadMattermostChannelRuntime()).sendMessageMattermost(to, message, { cfg, accountId: resolvedAccountId, replyToId, buttons: buttons.length > 0 ? buttons : void 0, attachmentText: typeof params.attachmentText === "string" ? params.attachmentText : void 0, mediaUrl: attachmentMedia.mediaUrls[0], mediaLocalRoots: mediaLocalRoots ?? mediaAccess?.localRoots, mediaReadFile: mediaReadFile ?? mediaAccess?.readFile, ...mediaAccess?.workspaceDir ? { workspaceDir: mediaAccess.workspaceDir } : {}, requireMediaUpload: requiresMattermostMediaUpload(attachmentMedia.mediaUrls[0]) ? true : void 0 }); return { content: [{ type: "text", text: JSON.stringify({ ok: true, channel: "mattermost", messageId: result.messageId, channelId: result.channelId }) }], details: {} }; } }; function parseMattermostReactActionParams(params) { const postId = normalizeOptionalString(params.messageId) ?? normalizeOptionalString(params.postId); if (!postId) throw new Error("Mattermost react requires messageId (post id)"); const emojiName = normalizeOptionalString(params.emoji)?.replace(/^:+|:+$/g, ""); if (!emojiName) throw new Error("Mattermost react requires emoji"); return { postId, emojiName, remove: params.remove === true }; } function collectNonBlankStrings(values) { const collected = []; const seen = /* @__PURE__ */ new Set(); for (const value of values) { const trimmed = value?.trim(); if (trimmed && !seen.has(trimmed)) { seen.add(trimmed); collected.push(trimmed); } } return collected; } function toSnakeCaseKey(key) { return key.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase(); } function readMattermostParam(params, key) { if (Object.hasOwn(params, key)) return params[key]; const snakeKey = toSnakeCaseKey(key); return snakeKey === key || !Object.hasOwn(params, snakeKey) ? void 0 : params[snakeKey]; } function readMattermostStringParam(params, key) { const raw = readMattermostParam(params, key); return typeof raw === "string" ? normalizeOptionalString(raw) : void 0; } function readMattermostStringArrayParam(params, key) { const raw = readMattermostParam(params, key); if (Array.isArray(raw)) return raw.filter((entry) => typeof entry === "string").flatMap((entry) => { const normalized = normalizeOptionalString(entry); return normalized ? [normalized] : []; }); if (typeof raw === "string") { const normalized = normalizeOptionalString(raw); return normalized ? [normalized] : []; } return []; } function requiresMattermostMediaUpload(mediaUrl) { const normalized = normalizeOptionalString(mediaUrl); return Boolean(normalized && !/^https?:\/\//i.test(normalized)); } function collectMattermostAttachmentMedia(params) { const mediaUrlCandidates = [ readMattermostStringParam(params, "media"), readMattermostStringParam(params, "mediaUrl"), readMattermostStringParam(params, "path"), readMattermostStringParam(params, "filePath"), readMattermostStringParam(params, "fileUrl") ]; mediaUrlCandidates.push(...readMattermostStringArrayParam(params, "mediaUrls")); let hasUnsupportedAttachmentPayload = typeof params.buffer === "string" || typeof params.base64 === "string"; if (Array.isArray(params.attachments)) for (const attachment of params.attachments) { if (!attachment || typeof attachment !== "object" || Array.isArray(attachment)) continue; const record = attachment; mediaUrlCandidates.push(readMattermostStringParam(record, "media"), readMattermostStringParam(record, "mediaUrl"), readMattermostStringParam(record, "path"), readMattermostStringParam(record, "filePath"), readMattermostStringParam(record, "fileUrl"), readMattermostStringParam(record, "url")); hasUnsupportedAttachmentPayload ||= typeof record.buffer === "string"; hasUnsupportedAttachmentPayload ||= typeof record.base64 === "string"; } return { mediaUrls: collectNonBlankStrings(mediaUrlCandidates), hasUnsupportedAttachmentPayload }; } const mattermostOutbound = { deliveryMode: "direct", chunker: chunkTextForOutbound, chunkerMode: "markdown", textChunkLimit: 4e3, deliveryCapabilities: { durableFinal: { text: true, media: true, payload: true, replyTo: true, thread: true, messageSendingHooks: true } }, presentationCapabilities: MATTERMOST_PRESENTATION_CAPABILITIES, renderPresentation: ({ payload, presentation }) => { if (payload.mediaUrls && payload.mediaUrls.length > 1) return null; const buttons = buildMattermostPresentationButtons(presentation); if (!hasMattermostPresentationButtons(presentation)) return null; return { ...payload, text: renderMessagePresentationFallbackText({ text: payload.text, presentation }), channelData: { ...payload.channelData, mattermost: { ...payload.channelData?.mattermost, presentationButtons: buttons } } }; }, sendPayload: async (ctx) => { const buttons = readMattermostPresentationButtons(ctx.payload); if (buttons?.length) { const mediaUrl = resolvePayloadMediaUrls({ ...ctx.payload, mediaUrl: ctx.payload.mediaUrl ?? ctx.mediaUrl }).map((url) => url.trim()).find(Boolean); return attachChannelToResult("mattermost", await (await loadMattermostChannelRuntime()).sendMessageMattermost(ctx.to, ctx.payload.text ?? ctx.text, { cfg: ctx.cfg, accountId: ctx.accountId ?? void 0, mediaUrl, mediaLocalRoots: ctx.mediaLocalRoots ?? ctx.mediaAccess?.localRoots, mediaReadFile: ctx.mediaReadFile ?? ctx.mediaAccess?.readFile, ...ctx.mediaAccess?.workspaceDir ? { workspaceDir: ctx.mediaAccess.workspaceDir } : {}, requireMediaUpload: requiresMattermostMediaUpload(mediaUrl) ? true : void 0, replyToId: ctx.replyToId ?? (ctx.threadId != null ? String(ctx.threadId) : void 0), buttons })); } return await sendTextMediaPayload({ channel: "mattermost", ctx, adapter: mattermostOutbound }); }, resolveTarget: ({ to }) => { const trimmed = to?.trim(); if (!trimmed) return { ok: false, error: /* @__PURE__ */ new Error("Delivering to Mattermost requires --to <channelId|@username|user:ID|channel:ID>") }; return { ok: true, to: trimmed }; }, ...createAttachedChannelResultAdapter({ channel: "mattermost", sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => await (await loadMattermostChannelRuntime()).sendMessageMattermost(to, text, { cfg, accountId: accountId ?? void 0, replyToId: replyToId ?? (threadId != null ? String(threadId) : void 0) }), sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId, replyToId, threadId }) => await (await loadMattermostChannelRuntime()).sendMessageMattermost(to, text, { cfg, accountId: accountId ?? void 0, mediaUrl, mediaLocalRoots: mediaLocalRoots ?? mediaAccess?.localRoots, mediaReadFile: mediaReadFile ?? mediaAccess?.readFile, ...mediaAccess?.workspaceDir ? { workspaceDir: mediaAccess.workspaceDir } : {}, requireMediaUpload: requiresMattermostMediaUpload(mediaUrl) ? true : void 0, replyToId: replyToId ?? (threadId != null ? String(threadId) : void 0) }) }) }; const mattermostMessageAdapter = createChannelMessageAdapterFromOutbound({ id: "mattermost", outbound: mattermostOutbound, live: { capabilities: { draftPreview: true, previewFinalization: true, progressUpdates: true }, finalizer: { capabilities: { finalEdit: true, normalFallback: true, discardPending: true } } } }); const mattermostPlugin = createChatChannelPlugin({ base: { id: "mattermost", meta: { ...mattermostMeta }, setup: mattermostSetupAdapter, setupWizard: mattermostSetupWizard, capabilities: { chatTypes: [ "direct", "channel", "group", "thread" ], reactions: true, threads: true, media: true, nativeCommands: true }, streaming: { blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1e3 } }, reload: { configPrefixes: ["channels.mattermost"] }, configSchema: MattermostChannelConfigSchema, config: { ...mattermostConfigAdapter, isConfigured: isMattermostConfigured$1, describeAccount: describeMattermostAccount }, approvalCapability: mattermostApprovalAuth, doctor: mattermostDoctor, groups: { resolveRequireMention: resolveMattermostGroupRequireMention }, actions: mattermostMessageActions, message: mattermostMessageAdapter, secrets: { secretTargetRegistryEntries, collectRuntimeConfigAssignments }, directory: createChannelDirectoryAdapter({ listGroups: listMattermostDirectoryGroups, listGroupsLive: listMattermostDirectoryGroups, listPeers: listMattermostDirectoryPeers, listPeersLive: listMattermostDirectoryPeers }), messaging: { targetPrefixes: ["mattermost"], defaultMarkdownTableMode: "off", normalizeTarget: normalizeMattermostMessagingTarget, resolveDeliveryTarget: ({ conversationId, parentConversationId }) => { const parent = parentConversationId?.trim(); const child = conversationId.trim(); return parent && parent !== child ? { to: `channel:${parent}`, threadId: child } : { to: normalizeMattermostMessagingTarget(`channel:${child}`) }; }, resolveOutboundSessionRoute: (params) => resolveMattermostOutboundSessionRoute(params), targetResolver: { looksLikeId: looksLikeMattermostTargetId, hint: "<channelId|user:ID|channel:ID>", resolveTarget: async ({ cfg, accountId, input }) => { const resolved = await (await loadMattermostChannelRuntime()).resolveMattermostOpaqueTarget({ input, cfg, accountId }); if (!resolved) return null; return { to: resolved.to, kind: resolved.kind, source: "directory" }; } } }, status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { connected: false, lastConnectedAt: null, lastDisconnect: null }), buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot, { botTokenSource: snapshot.botTokenSource ?? "none", connected: snapshot.connected ?? false, baseUrl: snapshot.baseUrl ?? null }), probeAccount: async ({ account, timeoutMs }) => { const token = account.botToken?.trim(); const baseUrl = account.baseUrl?.trim(); if (!token || !baseUrl) return { ok: false, error: "bot token or baseUrl missing" }; return await (await loadMattermostChannelRuntime()).probeMattermost(baseUrl, token, timeoutMs, isPrivateNetworkOptInEnabled(account.config)); }, resolveAccountSnapshot: ({ account, runtime }) => ({ accountId: account.accountId, name: account.name, enabled: account.enabled, configured: Boolean(account.botToken && account.baseUrl), extra: { botTokenSource: account.botTokenSource, baseUrl: account.baseUrl, connected: runtime?.connected ?? false, lastConnectedAt: runtime?.lastConnectedAt ?? null, lastDisconnect: runtime?.lastDisconnect ?? null } }) }), gateway: { resolveGatewayAuthBypassPaths: ({ cfg }) => resolveMattermostGatewayAuthBypassPaths(cfg), startAccount: async (ctx) => { const account = ctx.account; const statusSink = createAccountStatusSink({ accountId: ctx.accountId, setStatus: ctx.setStatus }); statusSink({ baseUrl: account.baseUrl, botTokenSource: account.botTokenSource }); ctx.log?.info(`[${account.accountId}] starting channel`); return (await loadMattermostChannelRuntime()).monitorMattermostProvider({ botToken: account.botToken ?? void 0, baseUrl: account.baseUrl ?? void 0, accountId: account.accountId, config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, statusSink }); } } }, pairing: { text: { idLabel: "mattermostUserId", message: "OpenClaw: your access has been approved.", normalizeAllowEntry: (entry) => normalizeMattermostAllowEntry(entry), notify: createLoggedPairingApprovalNotifier(({ id }) => `[mattermost] User ${id} approved for pairing`) } }, threading: { scopedAccountReplyToMode: { resolveAccount: (cfg, accountId) => resolveMattermostAccount({ cfg, accountId: accountId ?? resolveDefaultMattermostAccountId(cfg) }), resolveReplyToMode: (account, chatType) => resolveMattermostReplyToMode(account, chatType === "direct" || chatType === "group" || chatType === "channel" ? chatType : "channel") }, resolveReplyTransport: ({ threadId, replyToId }) => ({ replyToId: replyToId ?? (threadId != null ? String(threadId) : void 0), threadId }) }, security: mattermostSecurityAdapter, outbound: mattermostOutbound }); //#endregion export { describeMattermostAccount as a, mattermostMeta as c, MattermostChannelConfigSchema as i, mattermostSetupWizard as n, isMattermostConfigured$1 as o, mattermostSetupAdapter as r, mattermostConfigAdapter as s, mattermostPlugin as t };