From 495a39b5a9893ced78aa9ea755ff749dcbf3425f Mon Sep 17 00:00:00 2001 From: Dominic Damoah Date: Thu, 22 Jan 2026 12:02:30 -0500 Subject: [PATCH] refactor: extract mattermost channel plugin to extension Move mattermost channel implementation from core to extensions/mattermost plugin. Extract config schema, group mentions, normalize utilities, and all mattermost-specific logic (accounts, client, monitor, probe, send) into the extension. Update imports to use plugin SDK and local modules. Add channel metadata directly in plugin definition instead of using getChatChannelMeta. Update package.json with channel and install configuration. --- extensions/mattermost/package.json | 16 +- extensions/mattermost/src/channel.ts | 54 +++-- extensions/mattermost/src/config-schema.ts | 24 +++ extensions/mattermost/src/group-mentions.ts | 14 ++ .../mattermost/src}/mattermost/accounts.ts | 7 +- .../mattermost/src}/mattermost/client.ts | 0 .../mattermost/src}/mattermost/index.ts | 0 .../src/mattermost/monitor-helpers.ts | 150 +++++++++++++ .../mattermost/src}/mattermost/monitor.ts | 203 +++++++++--------- .../mattermost/src}/mattermost/probe.ts | 0 .../mattermost/src}/mattermost/send.ts | 19 +- .../mattermost/src/normalize.ts | 0 .../mattermost/src/onboarding-helpers.ts | 42 ++++ .../mattermost/src/onboarding.ts | 14 +- extensions/mattermost/src/types.ts | 40 ++++ src/auto-reply/reply/get-reply-run.ts | 3 +- src/channels/dock.ts | 25 --- src/channels/plugins/group-mentions.ts | 10 - src/channels/registry.ts | 11 - src/infra/outbound/deliver.ts | 9 +- src/plugin-sdk/index.ts | 17 -- src/plugins/runtime/index.ts | 8 - src/plugins/runtime/types.ts | 9 - ui/src/ui/views/channels.ts | 2 +- 24 files changed, 442 insertions(+), 235 deletions(-) create mode 100644 extensions/mattermost/src/config-schema.ts create mode 100644 extensions/mattermost/src/group-mentions.ts rename {src => extensions/mattermost/src}/mattermost/accounts.ts (96%) rename {src => extensions/mattermost/src}/mattermost/client.ts (100%) rename {src => extensions/mattermost/src}/mattermost/index.ts (100%) create mode 100644 extensions/mattermost/src/mattermost/monitor-helpers.ts rename {src => extensions/mattermost/src}/mattermost/monitor.ts (80%) rename {src => extensions/mattermost/src}/mattermost/probe.ts (100%) rename {src => extensions/mattermost/src}/mattermost/send.ts (93%) rename src/channels/plugins/normalize/mattermost.ts => extensions/mattermost/src/normalize.ts (100%) create mode 100644 extensions/mattermost/src/onboarding-helpers.ts rename src/channels/plugins/onboarding/mattermost.ts => extensions/mattermost/src/onboarding.ts (91%) create mode 100644 extensions/mattermost/src/types.ts diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 8ba462f45..f98f3c446 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -6,6 +6,20 @@ "clawdbot": { "extensions": [ "./index.ts" - ] + ], + "channel": { + "id": "mattermost", + "label": "Mattermost", + "selectionLabel": "Mattermost (plugin)", + "docsPath": "/channels/mattermost", + "docsLabel": "mattermost", + "blurb": "self-hosted Slack-style chat; install the plugin to enable.", + "order": 65 + }, + "install": { + "npmSpec": "@clawdbot/mattermost", + "localPath": "extensions/mattermost", + "defaultChoice": "npm" + } } } diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 840772a17..b365fc61e 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -3,26 +3,42 @@ import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, - getChatChannelMeta, - listMattermostAccountIds, - looksLikeMattermostTargetId, migrateBaseNameToDefaultAccount, normalizeAccountId, - normalizeMattermostBaseUrl, - normalizeMattermostMessagingTarget, - resolveDefaultMattermostAccountId, - resolveMattermostAccount, - resolveMattermostGroupRequireMention, setAccountEnabledInConfigSection, - mattermostOnboardingAdapter, - MattermostConfigSchema, type ChannelPlugin, - type ResolvedMattermostAccount, } from "clawdbot/plugin-sdk"; +import { MattermostConfigSchema } from "./config-schema.js"; +import { resolveMattermostGroupRequireMention } from "./group-mentions.js"; +import { + looksLikeMattermostTargetId, + normalizeMattermostMessagingTarget, +} from "./normalize.js"; +import { mattermostOnboardingAdapter } from "./onboarding.js"; +import { + listMattermostAccountIds, + resolveDefaultMattermostAccountId, + resolveMattermostAccount, + type ResolvedMattermostAccount, +} from "./mattermost/accounts.js"; +import { normalizeMattermostBaseUrl } from "./mattermost/client.js"; +import { monitorMattermostProvider } from "./mattermost/monitor.js"; +import { probeMattermost } from "./mattermost/probe.js"; +import { sendMessageMattermost } from "./mattermost/send.js"; import { getMattermostRuntime } from "./runtime.js"; -const meta = getChatChannelMeta("mattermost"); +const meta = { + 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, +} as const; export const mattermostPlugin: ChannelPlugin = { id: "mattermost", @@ -96,8 +112,7 @@ export const mattermostPlugin: ChannelPlugin = { return { ok: true, to: trimmed }; }, sendText: async ({ to, text, accountId, deps, replyToId }) => { - const send = - deps?.sendMattermost ?? getMattermostRuntime().channel.mattermost.sendMessageMattermost; + const send = deps?.sendMattermost ?? sendMessageMattermost; const result = await send(to, text, { accountId: accountId ?? undefined, replyToId: replyToId ?? undefined, @@ -105,8 +120,7 @@ export const mattermostPlugin: ChannelPlugin = { return { channel: "mattermost", ...result }; }, sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }) => { - const send = - deps?.sendMattermost ?? getMattermostRuntime().channel.mattermost.sendMessageMattermost; + const send = deps?.sendMattermost ?? sendMessageMattermost; const result = await send(to, text, { accountId: accountId ?? undefined, mediaUrl, @@ -144,11 +158,7 @@ export const mattermostPlugin: ChannelPlugin = { if (!token || !baseUrl) { return { ok: false, error: "bot token or baseUrl missing" }; } - return await getMattermostRuntime().channel.mattermost.probeMattermost( - baseUrl, - token, - timeoutMs, - ); + return await probeMattermost(baseUrl, token, timeoutMs); }, buildAccountSnapshot: ({ account, runtime, probe }) => ({ accountId: account.accountId, @@ -256,7 +266,7 @@ export const mattermostPlugin: ChannelPlugin = { botTokenSource: account.botTokenSource, }); ctx.log?.info(`[${account.accountId}] starting channel`); - return getMattermostRuntime().channel.mattermost.monitorMattermostProvider({ + return monitorMattermostProvider({ botToken: account.botToken ?? undefined, baseUrl: account.baseUrl ?? undefined, accountId: account.accountId, diff --git a/extensions/mattermost/src/config-schema.ts b/extensions/mattermost/src/config-schema.ts new file mode 100644 index 000000000..3cbecaf34 --- /dev/null +++ b/extensions/mattermost/src/config-schema.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { BlockStreamingCoalesceSchema } from "clawdbot/plugin-sdk"; + +const MattermostAccountSchema = z + .object({ + name: z.string().optional(), + capabilities: z.array(z.string()).optional(), + enabled: z.boolean().optional(), + configWrites: z.boolean().optional(), + botToken: z.string().optional(), + baseUrl: z.string().optional(), + chatmode: z.enum(["oncall", "onmessage", "onchar"]).optional(), + oncharPrefixes: z.array(z.string()).optional(), + requireMention: z.boolean().optional(), + textChunkLimit: z.number().int().positive().optional(), + blockStreaming: z.boolean().optional(), + blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), + }) + .strict(); + +export const MattermostConfigSchema = MattermostAccountSchema.extend({ + accounts: z.record(z.string(), MattermostAccountSchema.optional()).optional(), +}); diff --git a/extensions/mattermost/src/group-mentions.ts b/extensions/mattermost/src/group-mentions.ts new file mode 100644 index 000000000..773e655ff --- /dev/null +++ b/extensions/mattermost/src/group-mentions.ts @@ -0,0 +1,14 @@ +import type { ChannelGroupContext } from "clawdbot/plugin-sdk"; + +import { resolveMattermostAccount } from "./mattermost/accounts.js"; + +export function resolveMattermostGroupRequireMention( + params: ChannelGroupContext, +): boolean | undefined { + const account = resolveMattermostAccount({ + cfg: params.cfg, + accountId: params.accountId, + }); + if (typeof account.requireMention === "boolean") return account.requireMention; + return true; +} diff --git a/src/mattermost/accounts.ts b/extensions/mattermost/src/mattermost/accounts.ts similarity index 96% rename from src/mattermost/accounts.ts rename to extensions/mattermost/src/mattermost/accounts.ts index 08ffa2f94..e75f34593 100644 --- a/src/mattermost/accounts.ts +++ b/extensions/mattermost/src/mattermost/accounts.ts @@ -1,6 +1,7 @@ -import type { ClawdbotConfig } from "../config/config.js"; -import type { MattermostAccountConfig, MattermostChatMode } from "../config/types.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; + +import type { MattermostAccountConfig, MattermostChatMode } from "../types.js"; import { normalizeMattermostBaseUrl } from "./client.js"; export type MattermostTokenSource = "env" | "config" | "none"; diff --git a/src/mattermost/client.ts b/extensions/mattermost/src/mattermost/client.ts similarity index 100% rename from src/mattermost/client.ts rename to extensions/mattermost/src/mattermost/client.ts diff --git a/src/mattermost/index.ts b/extensions/mattermost/src/mattermost/index.ts similarity index 100% rename from src/mattermost/index.ts rename to extensions/mattermost/src/mattermost/index.ts diff --git a/extensions/mattermost/src/mattermost/monitor-helpers.ts b/extensions/mattermost/src/mattermost/monitor-helpers.ts new file mode 100644 index 000000000..8c68a4f25 --- /dev/null +++ b/extensions/mattermost/src/mattermost/monitor-helpers.ts @@ -0,0 +1,150 @@ +import { Buffer } from "node:buffer"; + +import type WebSocket from "ws"; + +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + +export type ResponsePrefixContext = { + model?: string; + modelFull?: string; + provider?: string; + thinkingLevel?: string; + identityName?: string; +}; + +export function extractShortModelName(fullModel: string): string { + const slash = fullModel.lastIndexOf("/"); + const modelPart = slash >= 0 ? fullModel.slice(slash + 1) : fullModel; + return modelPart.replace(/-\d{8}$/, "").replace(/-latest$/, ""); +} + +export function formatInboundFromLabel(params: { + isGroup: boolean; + groupLabel?: string; + groupId?: string; + directLabel: string; + directId?: string; + groupFallback?: string; +}): string { + if (params.isGroup) { + const label = params.groupLabel?.trim() || params.groupFallback || "Group"; + const id = params.groupId?.trim(); + return id ? `${label} id:${id}` : label; + } + + const directLabel = params.directLabel.trim(); + const directId = params.directId?.trim(); + if (!directId || directId === directLabel) return directLabel; + return `${directLabel} id:${directId}`; +} + +type DedupeCache = { + check: (key: string | undefined | null, now?: number) => boolean; +}; + +export function createDedupeCache(options: { ttlMs: number; maxSize: number }): DedupeCache { + const ttlMs = Math.max(0, options.ttlMs); + const maxSize = Math.max(0, Math.floor(options.maxSize)); + const cache = new Map(); + + const touch = (key: string, now: number) => { + cache.delete(key); + cache.set(key, now); + }; + + const prune = (now: number) => { + const cutoff = ttlMs > 0 ? now - ttlMs : undefined; + if (cutoff !== undefined) { + for (const [entryKey, entryTs] of cache) { + if (entryTs < cutoff) { + cache.delete(entryKey); + } + } + } + if (maxSize <= 0) { + cache.clear(); + return; + } + while (cache.size > maxSize) { + const oldestKey = cache.keys().next().value as string | undefined; + if (!oldestKey) break; + cache.delete(oldestKey); + } + }; + + return { + check: (key, now = Date.now()) => { + if (!key) return false; + const existing = cache.get(key); + if (existing !== undefined && (ttlMs <= 0 || now - existing < ttlMs)) { + touch(key, now); + return true; + } + touch(key, now); + prune(now); + return false; + }, + }; +} + +export function rawDataToString( + data: WebSocket.RawData, + encoding: BufferEncoding = "utf8", +): string { + if (typeof data === "string") return data; + if (Buffer.isBuffer(data)) return data.toString(encoding); + if (Array.isArray(data)) return Buffer.concat(data).toString(encoding); + if (data instanceof ArrayBuffer) { + return Buffer.from(data).toString(encoding); + } + return Buffer.from(String(data)).toString(encoding); +} + +function normalizeAgentId(value: string | undefined | null): string { + const trimmed = (value ?? "").trim(); + if (!trimmed) return "main"; + if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed; + return ( + trimmed + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "") + .slice(0, 64) || "main" + ); +} + +type AgentEntry = NonNullable["list"]>[number]; + +function listAgents(cfg: ClawdbotConfig): AgentEntry[] { + const list = cfg.agents?.list; + if (!Array.isArray(list)) return []; + return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object")); +} + +function resolveAgentEntry(cfg: ClawdbotConfig, agentId: string): AgentEntry | undefined { + const id = normalizeAgentId(agentId); + return listAgents(cfg).find((entry) => normalizeAgentId(entry.id) === id); +} + +export function resolveIdentityName(cfg: ClawdbotConfig, agentId: string): string | undefined { + const entry = resolveAgentEntry(cfg, agentId); + return entry?.identity?.name?.trim() || undefined; +} + +export function resolveThreadSessionKeys(params: { + baseSessionKey: string; + threadId?: string | null; + parentSessionKey?: string; + useSuffix?: boolean; +}): { sessionKey: string; parentSessionKey?: string } { + const threadId = (params.threadId ?? "").trim(); + if (!threadId) { + return { sessionKey: params.baseSessionKey, parentSessionKey: undefined }; + } + const useSuffix = params.useSuffix ?? true; + const sessionKey = useSuffix + ? `${params.baseSessionKey}:thread:${threadId}` + : params.baseSessionKey; + return { sessionKey, parentSessionKey: params.parentSessionKey }; +} diff --git a/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts similarity index 80% rename from src/mattermost/monitor.ts rename to extensions/mattermost/src/mattermost/monitor.ts index fb8bd00db..7c0d98fca 100644 --- a/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -1,52 +1,21 @@ import WebSocket from "ws"; -import { - resolveEffectiveMessagesConfig, - resolveHumanDelayConfig, - resolveIdentityName, -} from "../agents/identity.js"; -import { chunkMarkdownText, resolveTextChunkLimit } from "../auto-reply/chunk.js"; -import { hasControlCommand } from "../auto-reply/command-detection.js"; -import { shouldHandleTextCommands } from "../auto-reply/commands-registry.js"; -import { formatInboundEnvelope, formatInboundFromLabel } from "../auto-reply/envelope.js"; -import { - createInboundDebouncer, - resolveInboundDebounceMs, -} from "../auto-reply/inbound-debounce.js"; -import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js"; -import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; +import type { + ChannelAccountSnapshot, + ClawdbotConfig, + ReplyPayload, + RuntimeEnv, +} from "clawdbot/plugin-sdk"; import { buildPendingHistoryContextFromMap, clearHistoryEntries, DEFAULT_GROUP_HISTORY_LIMIT, recordPendingHistoryEntry, + resolveChannelMediaMaxBytes, type HistoryEntry, -} from "../auto-reply/reply/history.js"; -import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js"; -import { - extractShortModelName, - type ResponsePrefixContext, -} from "../auto-reply/reply/response-prefix-template.js"; -import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js"; -import type { ReplyPayload } from "../auto-reply/types.js"; -import type { ClawdbotConfig } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; -import { resolveStorePath, updateLastRoute } from "../config/sessions.js"; -import { danger, logVerbose, shouldLogVerbose } from "../globals.js"; -import { createDedupeCache } from "../infra/dedupe.js"; -import { rawDataToString } from "../infra/ws.js"; -import { recordChannelActivity } from "../infra/channel-activity.js"; -import { enqueueSystemEvent } from "../infra/system-events.js"; -import { getChildLogger } from "../logging.js"; -import { mediaKindFromMime, type MediaKind } from "../media/constants.js"; -import { fetchRemoteMedia, type FetchLike } from "../media/fetch.js"; -import { saveMediaBuffer } from "../media/store.js"; -import { resolveAgentRoute } from "../routing/resolve-route.js"; -import { resolveThreadSessionKeys } from "../routing/session-key.js"; -import type { RuntimeEnv } from "../runtime.js"; -import type { ChannelAccountSnapshot } from "../channels/plugins/types.js"; -import { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; -import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js"; +} from "clawdbot/plugin-sdk"; + +import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { createMattermostClient, @@ -59,6 +28,15 @@ import { type MattermostPost, type MattermostUser, } from "./client.js"; +import { + createDedupeCache, + extractShortModelName, + formatInboundFromLabel, + rawDataToString, + resolveIdentityName, + resolveThreadSessionKeys, + type ResponsePrefixContext, +} from "./monitor-helpers.js"; import { sendMessageMattermost } from "./send.js"; export type MonitorMattermostOpts = { @@ -71,6 +49,9 @@ export type MonitorMattermostOpts = { statusSink?: (patch: Partial) => void; }; +type FetchLike = typeof fetch; +type MediaKind = "image" | "audio" | "video" | "document" | "unknown"; + type MattermostEventPayload = { event?: string; data?: { @@ -208,8 +189,9 @@ function buildMattermostWsUrl(baseUrl: string): string { } export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}): Promise { + const core = getMattermostRuntime(); const runtime = resolveRuntime(opts); - const cfg = opts.config ?? loadConfig(); + const cfg = opts.config ?? core.config.loadConfig(); const account = resolveMattermostAccount({ cfg, accountId: opts.accountId, @@ -235,7 +217,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const channelCache = new Map(); const userCache = new Map(); - const logger = getChildLogger({ module: "mattermost" }); + const logger = core.logging.getChildLogger({ module: "mattermost" }); + const logVerboseMessage = (message: string) => { + if (!core.logging.shouldLogVerbose()) return; + logger.debug?.(message); + }; const mediaMaxBytes = resolveChannelMediaMaxBytes({ cfg, @@ -262,13 +248,13 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const out: MattermostMediaInfo[] = []; for (const fileId of ids) { try { - const fetched = await fetchRemoteMedia({ + const fetched = await core.channel.media.fetchRemoteMedia({ url: `${client.apiBaseUrl}/files/${fileId}`, fetchImpl: fetchWithAuth, filePathHint: fileId, maxBytes: mediaMaxBytes, }); - const saved = await saveMediaBuffer( + const saved = await core.channel.media.saveMediaBuffer( fetched.buffer, fetched.contentType ?? undefined, "inbound", @@ -278,7 +264,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} out.push({ path: saved.path, contentType, - kind: mediaKindFromMime(contentType), + kind: core.media.mediaKindFromMime(contentType), }); } catch (err) { logger.debug?.(`mattermost: failed to download file ${fileId}: ${String(err)}`); @@ -366,7 +352,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} payload.data?.channel_display_name ?? channelInfo?.display_name ?? channelName; const roomLabel = channelName ? `#${channelName}` : channelDisplay || `#${channelId}`; - const route = resolveAgentRoute({ + const route = core.channel.routing.resolveAgentRoute({ cfg, channel: "mattermost", accountId: account.accountId, @@ -387,12 +373,12 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const sessionKey = threadKeys.sessionKey; const historyKey = kind === "dm" ? null : sessionKey; - const mentionRegexes = buildMentionRegexes(cfg, route.agentId); + const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId); const rawText = post.message?.trim() || ""; const wasMentioned = kind !== "dm" && ((botUsername ? rawText.toLowerCase().includes(`@${botUsername.toLowerCase()}`) : false) || - matchesMentionPatterns(rawText, mentionRegexes)); + core.channel.mentions.matchesMentionPatterns(rawText, mentionRegexes)); const pendingBody = rawText || (post.file_ids?.length @@ -416,11 +402,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} }); }; - const allowTextCommands = shouldHandleTextCommands({ + const allowTextCommands = core.channel.commands.shouldHandleTextCommands({ cfg, surface: "mattermost", }); - const isControlCommand = allowTextCommands && hasControlCommand(rawText, cfg); + const isControlCommand = allowTextCommands && core.channel.text.hasControlCommand(rawText, cfg); const oncharEnabled = account.chatmode === "onchar" && kind !== "dm"; const oncharPrefixes = oncharEnabled ? resolveOncharPrefixes(account.oncharPrefixes) : []; const oncharResult = oncharEnabled @@ -456,7 +442,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const bodyText = normalizeMention(baseText, botUsername); if (!bodyText) return; - recordChannelActivity({ + core.channel.activity.record({ channel: "mattermost", accountId: account.accountId, direction: "inbound", @@ -476,13 +462,13 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} kind === "dm" ? `Mattermost DM from ${senderName}` : `Mattermost message in ${roomLabel} from ${senderName}`; - enqueueSystemEvent(`${inboundLabel}: ${preview}`, { + core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, { sessionKey, contextKey: `mattermost:message:${channelId}:${post.id ?? "unknown"}`, }); const textWithId = `${bodyText}\n[mattermost message id: ${post.id ?? "unknown"} channel: ${channelId}]`; - const body = formatInboundEnvelope({ + const body = core.channel.reply.formatInboundEnvelope({ channel: "Mattermost", from: fromLabel, timestamp: typeof post.create_at === "number" ? post.create_at : undefined, @@ -498,7 +484,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} limit: historyLimit, currentMessage: combinedBody, formatEntry: (entry) => - formatInboundEnvelope({ + core.channel.reply.formatInboundEnvelope({ channel: "Mattermost", from: fromLabel, timestamp: entry.timestamp, @@ -513,11 +499,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const to = kind === "dm" ? `user:${senderId}` : `channel:${channelId}`; const mediaPayload = buildMattermostMediaPayload(mediaList); - const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({ + const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({ useAccessGroups: cfg.commands?.useAccessGroups ?? false, authorizers: [], }); - const ctxPayload = finalizeInboundContext({ + const ctxPayload = core.channel.reply.finalizeInboundContext({ Body: combinedBody, RawBody: bodyText, CommandBody: bodyText, @@ -557,10 +543,10 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} if (kind === "dm") { const sessionCfg = cfg.session; - const storePath = resolveStorePath(sessionCfg?.store, { + const storePath = core.channel.session.resolveStorePath(sessionCfg?.store, { agentId: route.agentId, }); - await updateLastRoute({ + await core.channel.session.updateLastRoute({ storePath, sessionKey: route.mainSessionKey, deliveryContext: { @@ -571,14 +557,12 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} }); } - if (shouldLogVerbose()) { - const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n"); - logVerbose( - `mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`, - ); - } + const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n"); + logVerboseMessage( + `mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`, + ); - const textLimit = resolveTextChunkLimit(cfg, "mattermost", account.accountId, { + const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4000, }); @@ -586,43 +570,45 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} identityName: resolveIdentityName(cfg, route.agentId), }; - const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ - responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, - responsePrefixContextProvider: () => prefixContext, - humanDelay: resolveHumanDelayConfig(cfg, route.agentId), - deliver: async (payload: ReplyPayload) => { - const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); - const text = payload.text ?? ""; - if (mediaUrls.length === 0) { - const chunks = chunkMarkdownText(text, textLimit); - for (const chunk of chunks.length > 0 ? chunks : [text]) { - if (!chunk) continue; - await sendMessageMattermost(to, chunk, { - accountId: account.accountId, - replyToId: threadRootId, - }); + const { dispatcher, replyOptions, markDispatchIdle } = + core.channel.reply.createReplyDispatcherWithTyping({ + responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) + .responsePrefix, + responsePrefixContextProvider: () => prefixContext, + humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), + deliver: async (payload: ReplyPayload) => { + const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); + const text = payload.text ?? ""; + if (mediaUrls.length === 0) { + const chunks = core.channel.text.chunkMarkdownText(text, textLimit); + for (const chunk of chunks.length > 0 ? chunks : [text]) { + if (!chunk) continue; + await sendMessageMattermost(to, chunk, { + accountId: account.accountId, + replyToId: threadRootId, + }); + } + } else { + let first = true; + for (const mediaUrl of mediaUrls) { + const caption = first ? text : ""; + first = false; + await sendMessageMattermost(to, caption, { + accountId: account.accountId, + mediaUrl, + replyToId: threadRootId, + }); + } } - } else { - let first = true; - for (const mediaUrl of mediaUrls) { - const caption = first ? text : ""; - first = false; - await sendMessageMattermost(to, caption, { - accountId: account.accountId, - mediaUrl, - replyToId: threadRootId, - }); - } - } - runtime.log?.(`delivered reply to ${to}`); - }, - onError: (err, info) => { - runtime.error?.(danger(`mattermost ${info.kind} reply failed: ${String(err)}`)); - }, - onReplyStart: () => sendTypingIndicator(channelId, threadRootId), - }); + runtime.log?.(`delivered reply to ${to}`); + }, + onError: (err, info) => { + runtime.error?.(`mattermost ${info.kind} reply failed: ${String(err)}`); + }, + onReplyStart: () => sendTypingIndicator(channelId, threadRootId), + }); - await dispatchReplyFromConfig({ + await core.channel.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, @@ -644,8 +630,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} } }; - const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "mattermost" }); - const debouncer = createInboundDebouncer<{ + const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({ + cfg, + channel: "mattermost", + }); + const debouncer = core.channel.debounce.createInboundDebouncer<{ post: MattermostPost; payload: MattermostEventPayload; }>({ @@ -664,7 +653,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} if (entry.post.file_ids && entry.post.file_ids.length > 0) return false; const text = entry.post.message?.trim() ?? ""; if (!text) return false; - return !hasControlCommand(text, cfg); + return !core.channel.text.hasControlCommand(text, cfg); }, onFlush: async (entries) => { const last = entries.at(-1); @@ -686,7 +675,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} await handlePost(mergedPost, last.payload, ids.length > 0 ? ids : undefined); }, onError: (err) => { - runtime.error?.(danger(`mattermost debounce flush failed: ${String(err)}`)); + runtime.error?.(`mattermost debounce flush failed: ${String(err)}`); }, }); @@ -739,7 +728,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} try { await debouncer.enqueue({ post, payload }); } catch (err) { - runtime.error?.(danger(`mattermost handler failed: ${String(err)}`)); + runtime.error?.(`mattermost handler failed: ${String(err)}`); } }); @@ -758,7 +747,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} }); ws.on("error", (err) => { - runtime.error?.(danger(`mattermost websocket error: ${String(err)}`)); + runtime.error?.(`mattermost websocket error: ${String(err)}`); opts.statusSink?.({ lastError: String(err), }); diff --git a/src/mattermost/probe.ts b/extensions/mattermost/src/mattermost/probe.ts similarity index 100% rename from src/mattermost/probe.ts rename to extensions/mattermost/src/mattermost/probe.ts diff --git a/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts similarity index 93% rename from src/mattermost/send.ts rename to extensions/mattermost/src/mattermost/send.ts index 40f038cc0..f5b22c768 100644 --- a/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -1,7 +1,4 @@ -import { loadConfig } from "../config/config.js"; -import { logVerbose, shouldLogVerbose } from "../globals.js"; -import { recordChannelActivity } from "../infra/channel-activity.js"; -import { loadWebMedia } from "../web/media.js"; +import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { createMattermostClient, @@ -34,6 +31,8 @@ type MattermostTarget = const botUserCache = new Map(); const userByNameCache = new Map(); +const getCore = () => getMattermostRuntime(); + function cacheKey(baseUrl: string, token: string): string { return `${baseUrl}::${token}`; } @@ -129,7 +128,9 @@ export async function sendMessageMattermost( text: string, opts: MattermostSendOpts = {}, ): Promise { - const cfg = loadConfig(); + const core = getCore(); + const logger = core.logging.getChildLogger({ module: "mattermost" }); + const cfg = core.config.loadConfig(); const account = resolveMattermostAccount({ cfg, accountId: opts.accountId, @@ -161,7 +162,7 @@ export async function sendMessageMattermost( const mediaUrl = opts.mediaUrl?.trim(); if (mediaUrl) { try { - const media = await loadWebMedia(mediaUrl); + const media = await core.media.loadWebMedia(mediaUrl); const fileInfo = await uploadMattermostFile(client, { channelId, buffer: media.buffer, @@ -171,8 +172,8 @@ export async function sendMessageMattermost( fileIds = [fileInfo.id]; } catch (err) { uploadError = err instanceof Error ? err : new Error(String(err)); - if (shouldLogVerbose()) { - logVerbose( + if (core.logging.shouldLogVerbose()) { + logger.debug?.( `mattermost send: media upload failed, falling back to URL text: ${String(err)}`, ); } @@ -194,7 +195,7 @@ export async function sendMessageMattermost( fileIds, }); - recordChannelActivity({ + core.channel.activity.record({ channel: "mattermost", accountId: account.accountId, direction: "outbound", diff --git a/src/channels/plugins/normalize/mattermost.ts b/extensions/mattermost/src/normalize.ts similarity index 100% rename from src/channels/plugins/normalize/mattermost.ts rename to extensions/mattermost/src/normalize.ts diff --git a/extensions/mattermost/src/onboarding-helpers.ts b/extensions/mattermost/src/onboarding-helpers.ts new file mode 100644 index 000000000..8a5d1f585 --- /dev/null +++ b/extensions/mattermost/src/onboarding-helpers.ts @@ -0,0 +1,42 @@ +import type { ClawdbotConfig, WizardPrompter } from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; + +type PromptAccountIdParams = { + cfg: ClawdbotConfig; + prompter: WizardPrompter; + label: string; + currentId?: string; + listAccountIds: (cfg: ClawdbotConfig) => string[]; + defaultAccountId: string; +}; + +export async function promptAccountId(params: PromptAccountIdParams): Promise { + const existingIds = params.listAccountIds(params.cfg); + const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID; + const choice = (await params.prompter.select({ + message: `${params.label} account`, + options: [ + ...existingIds.map((id) => ({ + value: id, + label: id === DEFAULT_ACCOUNT_ID ? "default (primary)" : id, + })), + { value: "__new__", label: "Add a new account" }, + ], + initialValue: initial, + })) as string; + + if (choice !== "__new__") return normalizeAccountId(choice); + + const entered = await params.prompter.text({ + message: `New ${params.label} account id`, + validate: (value) => (value?.trim() ? undefined : "Required"), + }); + const normalized = normalizeAccountId(String(entered)); + if (String(entered).trim() !== normalized) { + await params.prompter.note( + `Normalized account id to "${normalized}".`, + `${params.label} account`, + ); + } + return normalized; +} diff --git a/src/channels/plugins/onboarding/mattermost.ts b/extensions/mattermost/src/onboarding.ts similarity index 91% rename from src/channels/plugins/onboarding/mattermost.ts rename to extensions/mattermost/src/onboarding.ts index 3c7ffe2db..431c648ae 100644 --- a/src/channels/plugins/onboarding/mattermost.ts +++ b/extensions/mattermost/src/onboarding.ts @@ -1,14 +1,12 @@ -import type { ClawdbotConfig } from "../../../config/config.js"; +import type { ChannelOnboardingAdapter, ClawdbotConfig, WizardPrompter } from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; + import { listMattermostAccountIds, resolveDefaultMattermostAccountId, resolveMattermostAccount, -} from "../../../mattermost/accounts.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js"; -import { formatDocsLink } from "../../../terminal/links.js"; -import type { WizardPrompter } from "../../../wizard/prompts.js"; -import type { ChannelOnboardingAdapter } from "../onboarding-types.js"; -import { promptAccountId } from "./helpers.js"; +} from "./mattermost/accounts.js"; +import { promptAccountId } from "./onboarding-helpers.js"; const channel = "mattermost" as const; @@ -19,7 +17,7 @@ async function noteMattermostSetup(prompter: WizardPrompter): Promise { "2) Create a bot + copy its token", "3) Use your server base URL (e.g., https://chat.example.com)", "Tip: the bot must be a member of any channel you want it to monitor.", - `Docs: ${formatDocsLink("/channels/mattermost", "mattermost")}`, + "Docs: https://docs.clawd.bot/channels/mattermost", ].join("\n"), "Mattermost bot token", ); diff --git a/extensions/mattermost/src/types.ts b/extensions/mattermost/src/types.ts new file mode 100644 index 000000000..43e509763 --- /dev/null +++ b/extensions/mattermost/src/types.ts @@ -0,0 +1,40 @@ +import type { BlockStreamingCoalesceConfig } from "clawdbot/plugin-sdk"; + +export type MattermostChatMode = "oncall" | "onmessage" | "onchar"; + +export type MattermostAccountConfig = { + /** Optional display name for this account (used in CLI/UI lists). */ + name?: string; + /** Optional provider capability tags used for agent/runtime guidance. */ + capabilities?: string[]; + /** Allow channel-initiated config writes (default: true). */ + configWrites?: boolean; + /** If false, do not start this Mattermost account. Default: true. */ + enabled?: boolean; + /** Bot token for Mattermost. */ + botToken?: string; + /** Base URL for the Mattermost server (e.g., https://chat.example.com). */ + baseUrl?: string; + /** + * Controls when channel messages trigger replies. + * - "oncall": only respond when mentioned + * - "onmessage": respond to every channel message + * - "onchar": respond when a trigger character prefixes the message + */ + chatmode?: MattermostChatMode; + /** Prefix characters that trigger onchar mode (default: [">", "!"]). */ + oncharPrefixes?: string[]; + /** Require @mention to respond in channels. Default: true. */ + requireMention?: boolean; + /** Outbound text chunk size (chars). Default: 4000. */ + textChunkLimit?: number; + /** Disable block streaming for this account. */ + blockStreaming?: boolean; + /** Merge streamed block replies before sending. */ + blockStreamingCoalesce?: BlockStreamingCoalesceConfig; +}; + +export type MattermostConfig = { + /** Optional per-account Mattermost configuration (multi-account). */ + accounts?: Record; +} & MattermostAccountConfig; diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index a2adbf312..524b6ddaf 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -159,8 +159,7 @@ export async function runPreparedReply( const isGroupChat = sessionCtx.ChatType === "group"; const originatingChannel = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider)?.toString().toLowerCase() ?? ""; - const wasMentioned = - ctx.WasMentioned === true || (originatingChannel === "mattermost" && isGroupChat); + const wasMentioned = ctx.WasMentioned === true; const isHeartbeat = opts?.isHeartbeat === true; const typingMode = resolveTypingMode({ configured: sessionCfg?.typingMode ?? agentCfg?.typingMode, diff --git a/src/channels/dock.ts b/src/channels/dock.ts index 469880482..81b07c36a 100644 --- a/src/channels/dock.ts +++ b/src/channels/dock.ts @@ -12,7 +12,6 @@ import { requireActivePluginRegistry } from "../plugins/runtime.js"; import { resolveDiscordGroupRequireMention, resolveIMessageGroupRequireMention, - resolveMattermostGroupRequireMention, resolveSlackGroupRequireMention, resolveTelegramGroupRequireMention, resolveWhatsAppGroupRequireMention, @@ -231,30 +230,6 @@ const DOCKS: Record = { buildToolContext: (params) => buildSlackThreadingToolContext(params), }, }, - mattermost: { - id: "mattermost", - capabilities: { - chatTypes: ["direct", "channel", "group", "thread"], - media: true, - threads: true, - }, - outbound: { textChunkLimit: 4000 }, - streaming: { - blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 }, - }, - groups: { - resolveRequireMention: resolveMattermostGroupRequireMention, - }, - threading: { - buildToolContext: ({ context, hasRepliedRef }) => ({ - currentChannelId: context.To?.startsWith("channel:") - ? context.To.slice("channel:".length) - : undefined, - currentThreadTs: context.ReplyToId, - hasRepliedRef, - }), - }, - }, signal: { id: "signal", capabilities: { diff --git a/src/channels/plugins/group-mentions.ts b/src/channels/plugins/group-mentions.ts index bb7a111e8..79dfa0320 100644 --- a/src/channels/plugins/group-mentions.ts +++ b/src/channels/plugins/group-mentions.ts @@ -1,7 +1,6 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { resolveChannelGroupRequireMention } from "../../config/group-policy.js"; import type { DiscordConfig } from "../../config/types.js"; -import { resolveMattermostAccount } from "../../mattermost/accounts.js"; import { resolveSlackAccount } from "../../slack/accounts.js"; type GroupMentionParams = { @@ -185,15 +184,6 @@ export function resolveSlackGroupRequireMention(params: GroupMentionParams): boo return true; } -export function resolveMattermostGroupRequireMention(params: GroupMentionParams): boolean { - const account = resolveMattermostAccount({ - cfg: params.cfg, - accountId: params.accountId, - }); - if (typeof account.requireMention === "boolean") return account.requireMention; - return true; -} - export function resolveBlueBubblesGroupRequireMention(params: GroupMentionParams): boolean { return resolveChannelGroupRequireMention({ cfg: params.cfg, diff --git a/src/channels/registry.ts b/src/channels/registry.ts index 25fb13502..52e7a5f01 100644 --- a/src/channels/registry.ts +++ b/src/channels/registry.ts @@ -9,7 +9,6 @@ export const CHAT_CHANNEL_ORDER = [ "whatsapp", "discord", "slack", - "mattermost", "signal", "imessage", ] as const; @@ -68,16 +67,6 @@ const CHAT_CHANNEL_META: Record = { blurb: "supported (Socket Mode).", systemImage: "number", }, - mattermost: { - id: "mattermost", - label: "Mattermost", - selectionLabel: "Mattermost (Bot Token)", - detailLabel: "Mattermost Bot", - docsPath: "/channels/mattermost", - docsLabel: "mattermost", - blurb: "self-hosted Slack-style chat (bot token + URL).", - systemImage: "bubble.left.and.bubble.right", - }, signal: { id: "signal", label: "Signal", diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 74caa18c6..2d874d7e9 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -6,7 +6,6 @@ import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { sendMessageDiscord } from "../../discord/send.js"; import type { sendMessageIMessage } from "../../imessage/send.js"; -import type { sendMessageMattermost } from "../../mattermost/send.js"; import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js"; import { sendMessageSignal } from "../../signal/send.js"; import type { sendMessageSlack } from "../../slack/send.js"; @@ -29,12 +28,18 @@ type SendMatrixMessage = ( opts?: { mediaUrl?: string; replyToId?: string; threadId?: string; timeoutMs?: number }, ) => Promise<{ messageId: string; roomId: string }>; +type SendMattermostMessage = ( + to: string, + text: string, + opts?: { accountId?: string; mediaUrl?: string; replyToId?: string }, +) => Promise<{ messageId: string; channelId: string }>; + export type OutboundSendDeps = { sendWhatsApp?: typeof sendMessageWhatsApp; sendTelegram?: typeof sendMessageTelegram; sendDiscord?: typeof sendMessageDiscord; sendSlack?: typeof sendMessageSlack; - sendMattermost?: typeof sendMessageMattermost; + sendMattermost?: SendMattermostMessage; sendSignal?: typeof sendMessageSignal; sendIMessage?: typeof sendMessageIMessage; sendMatrix?: SendMatrixMessage; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index ef8f22d55..1da3650fe 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -81,7 +81,6 @@ export type { export { DiscordConfigSchema, IMessageConfigSchema, - MattermostConfigSchema, MSTeamsConfigSchema, SignalConfigSchema, SlackConfigSchema, @@ -121,7 +120,6 @@ export { resolveBlueBubblesGroupRequireMention, resolveDiscordGroupRequireMention, resolveIMessageGroupRequireMention, - resolveMattermostGroupRequireMention, resolveSlackGroupRequireMention, resolveTelegramGroupRequireMention, resolveWhatsAppGroupRequireMention, @@ -241,21 +239,6 @@ export { } from "../channels/plugins/normalize/slack.js"; export { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js"; -// Channel: Mattermost -export { - listEnabledMattermostAccounts, - listMattermostAccountIds, - resolveDefaultMattermostAccountId, - resolveMattermostAccount, - type ResolvedMattermostAccount, -} from "../mattermost/accounts.js"; -export { normalizeMattermostBaseUrl } from "../mattermost/client.js"; -export { mattermostOnboardingAdapter } from "../channels/plugins/onboarding/mattermost.js"; -export { - looksLikeMattermostTargetId, - normalizeMattermostMessagingTarget, -} from "../channels/plugins/normalize/mattermost.js"; - // Channel: Telegram export { listTelegramAccountIds, diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index e564ad2f8..4765c71c7 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -57,9 +57,6 @@ import { enqueueSystemEvent } from "../../infra/system-events.js"; import { monitorIMessageProvider } from "../../imessage/monitor.js"; import { probeIMessage } from "../../imessage/probe.js"; import { sendMessageIMessage } from "../../imessage/send.js"; -import { monitorMattermostProvider } from "../../mattermost/monitor.js"; -import { probeMattermost } from "../../mattermost/probe.js"; -import { sendMessageMattermost } from "../../mattermost/send.js"; import { shouldLogVerbose } from "../../globals.js"; import { getChildLogger } from "../../logging.js"; import { normalizeLogLevel } from "../../logging/levels.js"; @@ -233,11 +230,6 @@ export function createPluginRuntime(): PluginRuntime { monitorSlackProvider, handleSlackAction, }, - mattermost: { - probeMattermost, - sendMessageMattermost, - monitorMattermostProvider, - }, telegram: { auditGroupMembership: auditTelegramGroupMembership, collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds, diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index 31350693c..089e20c37 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -98,10 +98,6 @@ type ResolveSlackUserAllowlist = type SendMessageSlack = typeof import("../../slack/send.js").sendMessageSlack; type MonitorSlackProvider = typeof import("../../slack/index.js").monitorSlackProvider; type HandleSlackAction = typeof import("../../agents/tools/slack-actions.js").handleSlackAction; -type ProbeMattermost = typeof import("../../mattermost/probe.js").probeMattermost; -type SendMessageMattermost = typeof import("../../mattermost/send.js").sendMessageMattermost; -type MonitorMattermostProvider = - typeof import("../../mattermost/monitor.js").monitorMattermostProvider; type AuditTelegramGroupMembership = typeof import("../../telegram/audit.js").auditTelegramGroupMembership; type CollectTelegramUnmentionedGroupIds = @@ -246,11 +242,6 @@ export type PluginRuntime = { monitorSlackProvider: MonitorSlackProvider; handleSlackAction: HandleSlackAction; }; - mattermost: { - probeMattermost: ProbeMattermost; - sendMessageMattermost: SendMessageMattermost; - monitorMattermostProvider: MonitorMattermostProvider; - }; telegram: { auditGroupMembership: AuditTelegramGroupMembership; collectUnmentionedGroupIds: CollectTelegramUnmentionedGroupIds; diff --git a/ui/src/ui/views/channels.ts b/ui/src/ui/views/channels.ts index 96a6b8556..d9f148764 100644 --- a/ui/src/ui/views/channels.ts +++ b/ui/src/ui/views/channels.ts @@ -101,7 +101,7 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe if (snapshot?.channelOrder?.length) { return snapshot.channelOrder; } - return ["whatsapp", "telegram", "discord", "slack", "mattermost", "signal", "imessage", "nostr"]; + return ["whatsapp", "telegram", "discord", "slack", "signal", "imessage", "nostr"]; } function renderChannel(