Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8aa30a7
feat: add Mattermost messaging adapter
unverbraucht Mar 11, 2026
c8fb24e
Merge main into feat/mattermost
unverbraucht Mar 11, 2026
d26723c
fix: close missing braces in mattermost instance loop in initialize_a…
unverbraucht Mar 11, 2026
7f6ee4a
feat: add Mattermost to configuration UI
unverbraucht Mar 11, 2026
b069b56
Merge branch 'main' into feat/mattermost
unverbraucht Mar 13, 2026
4416b6d
feat: enforce dm_allowed_users filter for Mattermost DMs
unverbraucht Mar 13, 2026
daca014
feat: fix Mattermost adapter gaps identified in GLM-5 review
unverbraucht Mar 13, 2026
12ff932
fix: address all Mattermost PR review findings
unverbraucht Mar 14, 2026
f307f25
docs: add docstrings to Mattermost adapter and target functions
unverbraucht Mar 14, 2026
67c6a96
fix: wire Mattermost into channel_ids check and require_mention routing
unverbraucht Mar 14, 2026
b42929f
fix: address new Mattermost PR review findings
unverbraucht Mar 14, 2026
1fd2b2b
fix: avoid unnecessary clone in StreamChunk throttled edit path
unverbraucht Mar 14, 2026
abc77b2
fix: address coderabbitai PR review findings in Mattermost adapter
unverbraucht Mar 14, 2026
0d98cfb
fix: address remaining coderabbitai Mattermost review findings
unverbraucht Mar 15, 2026
5641416
fix: address duplicate coderabbitai findings in Mattermost adapter
unverbraucht Mar 15, 2026
4403e47
fix: address new Mattermost PR review findings
unverbraucht Mar 15, 2026
43aa84c
fix: address new Mattermost PR review findings
unverbraucht Mar 15, 2026
8db2feb
chore(docs): rewrite mattermost.md as user-facing documentation
unverbraucht Mar 15, 2026
aa8c2c7
fix: address coderabbitai PR review findings in Mattermost adapter
unverbraucht Mar 15, 2026
956d11f
fix: address coderabbitai PR review findings in Mattermost adapter
unverbraucht Mar 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ prometheus = { version = "0.13", optional = true }
pdf-extract = "0.10.0"
open = "5.3.3"
urlencoding = "2.1.3"
url = "2"
moka = "0.12.13"

[features]
Expand Down
118 changes: 118 additions & 0 deletions docs/mattermost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Mattermost

Spacebot connects to Mattermost via a bot account using the Mattermost REST API and WebSocket event stream. The integration can be configured in the web UI or via `config.toml`.

## Features

### Supported
- Receive messages from channels and direct messages
- Send text replies (long messages are automatically split into multiple posts)
- Streaming replies with live edit-in-place updates and typing indicator
- Thread-aware replies (replies stay in the originating thread)
- File/image attachments (up to a configurable size limit)
- Emoji reactions
- Fetch channel history (used for context window)
- Multiple named instances (connect to more than one Mattermost server)
- Per-team and per-channel allowlists
- DM allowlist (fail-closed: DMs are blocked unless the sender is explicitly listed)
- `require_mention` routing (only respond when the bot is @-mentioned)

### Not supported (compared to Slack/Discord)
- Slash commands — Mattermost slash commands are not handled; use @-mentions instead
- Ephemeral (private) messages
- Message threading via `parent_id` lookup — threads are followed when the inbound message carries a root ID, but the bot cannot independently look up thread context
- User/channel autocomplete
- Presence or status events
- App marketplace / interactive components (buttons, modals)

## Setup

### 1. Create a bot account

In Mattermost: **System Console → Integrations → Bot Accounts → Add Bot Account**

- Give it a username (e.g. `spacebot`)
- Copy the generated access token

The bot must be added to any team and channel it should respond in. Bot accounts in Mattermost are not automatically visible in channels.

### 2. Configure in config.toml

```toml
[messaging.mattermost]
enabled = true
base_url = "https://mattermost.example.com" # origin only, no path
token = "your-bot-access-token"
team_id = "team_id_here" # optional: default team for events without one
max_attachment_bytes = 52428800 # optional: default 50 MB
dm_allowed_users = [] # optional: user IDs allowed to DM the bot
```

The token can also be supplied via the `MATTERMOST_TOKEN` environment variable and `base_url` via `MATTERMOST_BASE_URL`.

> **Security**: `base_url` must use `https` for non-localhost hosts. Plain `http` is only accepted for `localhost` / `127.0.0.1` / `::1`.

### 3. Wire up a binding

Bindings connect Mattermost channels to agents:

```toml
[[bindings]]
agent_id = "my-agent"
channel = "mattermost"
channel_ids = ["channel_id_here"] # leave empty to match all channels
require_mention = false
```

To scope a binding to a specific Mattermost team, add `team_id`:

```toml
[[bindings]]
agent_id = "my-agent"
channel = "mattermost"
team_id = "team_id_here"
channel_ids = ["channel_id_here"]
```

## Multiple servers (named instances)

```toml
[[messaging.mattermost.instances]]
name = "corp"
enabled = true
base_url = "https://mattermost.corp.example.com"
token = "corp-bot-token"
team_id = "corp_team_id"

[[messaging.mattermost.instances]]
name = "community"
enabled = true
base_url = "https://community.example.com"
token = "community-bot-token"
```

Named instance tokens can be supplied via `MATTERMOST_CORP_TOKEN` / `MATTERMOST_COMMUNITY_TOKEN` etc.

In bindings, reference a named instance with the `adapter` field:

```toml
[[bindings]]
agent_id = "my-agent"
channel = "mattermost"
adapter = "corp"
channel_ids = ["channel_id_here"]
```

## Web UI

All of the above can be configured in the Spacebot web interface under **Settings → Messaging → Mattermost**. The UI supports adding credentials, enabling/disabling the adapter, and managing bindings with team and channel scoping.

## Finding IDs

Mattermost does not display IDs in the UI by default. The easiest ways to retrieve them:

- **Team ID**: `GET /api/v4/teams/name/{team_name}` → `.id`
- **Channel ID**: `GET /api/v4/teams/{team_id}/channels/name/{channel_name}` → `.id`
- **User ID**: `GET /api/v4/users/username/{username}` → `.id`

Alternatively, enable **Account Settings → Advanced → Enable post formatting** and inspect the network tab when loading a channel — team and channel IDs appear in the request URLs.
2 changes: 2 additions & 0 deletions interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,8 @@ export interface CreateMessagingInstanceRequest {
webhook_port?: number;
webhook_bind?: string;
webhook_auth_token?: string;
mattermost_base_url?: string;
mattermost_token?: string;
};
}

Expand Down
2 changes: 1 addition & 1 deletion interface/src/components/ChannelEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import {PlatformIcon} from "@/lib/platformIcons";
import {TagInput} from "@/components/TagInput";

type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook";
type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook" | "mattermost";

interface ChannelEditModalProps {
platform: Platform;
Expand Down
41 changes: 40 additions & 1 deletion interface/src/components/ChannelSettingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {TagInput} from "@/components/TagInput";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faPlus} from "@fortawesome/free-solid-svg-icons";

type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook";
type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook" | "mattermost";

const PLATFORM_LABELS: Record<Platform, string> = {
discord: "Discord",
Expand All @@ -36,13 +36,15 @@ const PLATFORM_LABELS: Record<Platform, string> = {
twitch: "Twitch",
email: "Email",
webhook: "Webhook",
mattermost: "Mattermost",
};

const DOC_LINKS: Partial<Record<Platform, string>> = {
discord: "https://docs.spacebot.sh/discord-setup",
slack: "https://docs.spacebot.sh/slack-setup",
telegram: "https://docs.spacebot.sh/telegram-setup",
twitch: "https://docs.spacebot.sh/twitch-setup",
mattermost: "https://docs.spacebot.sh/mattermost-setup",
};

// --- Platform Catalog (Left Column) ---
Expand All @@ -59,6 +61,7 @@ export function PlatformCatalog({onAddInstance}: PlatformCatalogProps) {
"twitch",
"email",
"webhook",
"mattermost",
];

const COMING_SOON = [
Expand Down Expand Up @@ -636,6 +639,17 @@ export function AddInstanceCard({platform, isDefault, onCancel, onCreated}: AddI
credentials.webhook_bind = credentialInputs.webhook_bind.trim();
if (credentialInputs.webhook_auth_token?.trim())
credentials.webhook_auth_token = credentialInputs.webhook_auth_token.trim();
} else if (platform === "mattermost") {
if (!credentialInputs.mattermost_base_url?.trim()) {
setMessage({text: "Server URL is required", type: "error"});
return;
}
if (!credentialInputs.mattermost_token?.trim()) {
setMessage({text: "Access token is required", type: "error"});
return;
}
credentials.mattermost_base_url = credentialInputs.mattermost_base_url.trim();
credentials.mattermost_token = credentialInputs.mattermost_token.trim();
}

if (!isDefault && !instanceName.trim()) {
Expand Down Expand Up @@ -925,6 +939,31 @@ export function AddInstanceCard({platform, isDefault, onCancel, onCreated}: AddI
</>
)}

{platform === "mattermost" && (
<>
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">Server URL</label>
<Input
size="lg"
value={credentialInputs.mattermost_base_url ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, mattermost_base_url: e.target.value})}
placeholder="https://mattermost.example.com"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">Access Token</label>
<Input
type="password"
size="lg"
value={credentialInputs.mattermost_token ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, mattermost_token: e.target.value})}
placeholder="Personal access token from Mattermost account settings"
onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }}
/>
</div>
</>
)}

{docLink && (
<p className="text-xs text-ink-faint">
Need help?{" "}
Expand Down
3 changes: 2 additions & 1 deletion interface/src/lib/platformIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDiscord, faSlack, faTelegram, faTwitch, faWhatsapp } from "@fortawesome/free-brands-svg-icons";
import { faLink, faEnvelope, faComments, faComment } from "@fortawesome/free-solid-svg-icons";
import { faLink, faEnvelope, faComments, faComment, faServer } from "@fortawesome/free-solid-svg-icons";

interface PlatformIconProps {
platform: string;
Expand All @@ -16,6 +16,7 @@ export function PlatformIcon({ platform, className = "text-ink-faint", size = "1
twitch: faTwitch,
webhook: faLink,
email: faEnvelope,
mattermost: faServer,
whatsapp: faWhatsapp,
matrix: faComments,
imessage: faComment,
Expand Down
2 changes: 1 addition & 1 deletion interface/src/routes/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ function ThemePreview({ themeId }: { themeId: ThemeId }) {
);
}

type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook";
type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook" | "mattermost";

function ChannelsSection() {
const [expandedKey, setExpandedKey] = useState<string | null>(null);
Expand Down
Loading