Mattermost
An Elixir bot framework for Mattermost. Handles the WebSocket connection, event dispatch, and slash command routing so you can focus on your bot's logic.
Features
- Real-time event listening via WebSocket (connects and authenticates automatically)
- Slash command handling via a Plug — mounts directly into Phoenix, no extra server
-
Clean
handle_message/2,handle_command/3,handle_event/3callbacks -
Explicit
Mattermost.API.*functions for sending messages, DMs, file uploads - Personal Access Token authentication
-
Typed structs for all API types (
Post,User,Channel,FileInfo,Command)
Installation
# mix.exs
def deps do
[
{:mattermost, "~> 0.1"}
]
endSetup
1. Create a bot account in Mattermost
Go to System Console → Integrations → Bot Accounts and create a bot. Copy the generated token.
Alternatively, create a Personal Access Token under Profile → Security → Personal Access Tokens.
2. Get the bot user ID
curl https://your-mattermost.com/api/v4/users/me \
-H "Authorization: Bearer YOUR_TOKEN"
# copy the "id" field3. Configure
# config/runtime.exs
config :mattermost,
base_url: "https://your-mattermost.com",
token: System.get_env("MM_TOKEN"),
bot_user_id: System.get_env("MM_BOT_USER_ID")4. Define your bot
defmodule MyApp.Bot do
use Mattermost.Bot
alias Mattermost.{API, Post}
def handle_message(%Post{text: "ping"}, ctx) do
API.reply(ctx, "pong")
end
def handle_command("deploy", env, ctx) do
API.reply(ctx, "Deploying to #{env}...")
end
endOnly define the callbacks you need. Catch-all no-ops are automatically injected for any callbacks you leave out.
5. Add to your supervision tree
# lib/my_app/application.ex
children = [
MyApp.Bot
]The WebSocket connection starts automatically when the supervisor starts.
6. Mount the slash command plug (Phoenix)
# lib/my_app_web/router.ex
scope "/agent" do
forward "/commands", Mattermost.Plug, handler: MyApp.Bot
end7. Register slash commands in Mattermost
Go to Main Menu → Integrations → Slash Commands → Add Slash Command:
| Field | Value |
|---|---|
| Command Trigger Word | deploy |
| Request URL | https://your-domain.com/agent/commands/deploy |
| Request Method | POST |
| Autocomplete | ✓ |
Or register programmatically via iex:
config = Mattermost.from_env()
ctx = %Mattermost.Context{config: config, channel_id: nil, user_id: nil, post: nil}
{:ok, teams} = Mattermost.Client.request(config, :get, "/teams")
team_id = hd(teams)["id"]
Mattermost.API.register_command(ctx, team_id, "deploy",
"https://your-domain.com/agent/commands/deploy",
description: "Deploy to an environment",
auto_complete: true,
auto_complete_hint: "[staging|production]"
)Callbacks
handle_message/2
Called for every post in any channel the bot is a member of. The bot's own messages are automatically filtered.
def handle_message(%Mattermost.Post{text: "help"}, ctx) do
Mattermost.API.reply(ctx, "Commands: ping, help")
endhandle_command/3
Called when a slash command webhook fires. command is the trigger word
(without /), text is everything the user typed after it.
def handle_command("status", _text, ctx) do
Mattermost.API.reply(ctx, "All systems operational.")
endhandle_event/3
Called for all other WebSocket events. event is an atom like :user_added,
:channel_created, :reaction_added, etc.
def handle_event(:user_added, %{"user_id" => uid}, ctx) do
Mattermost.API.send_message(ctx, ctx.channel_id, "Welcome <@#{uid}>!")
endAPI Reference
All Mattermost.API.* functions take a %Mattermost.Context{} as their first argument.
Messages
# Reply in the same channel as the incoming post
API.reply(ctx, "Hello!")
# Send to a specific channel
API.send_message(ctx, channel_id, "Build passed!")
# Send a direct message
API.dm_user(ctx, user_id, "Heads up!")
# Reply in a thread
API.reply(ctx, "Done.", root_id: post.id)
# Edit or delete
API.update_post(ctx, post_id, "Corrected text")
API.delete_post(ctx, post_id)Files
{:ok, [file]} = API.upload_file(ctx, channel_id, "/tmp/report.pdf", "report.pdf")
API.send_message(ctx, channel_id, "Here's the report:", file_ids: [file.id])Users
{:ok, user} = API.get_user(ctx, user_id)
{:ok, user} = API.get_user_by_username(ctx, "john.doe")
{:ok, me} = API.me(ctx)Channels
{:ok, channel} = API.get_channel(ctx, channel_id)Slash commands
API.register_command(ctx, team_id, "deploy", url, description: "Deploy", auto_complete: true)
{:ok, commands} = API.list_commands(ctx, team_id)
API.update_command(ctx, command_id, url: "https://new-url.com/commands/deploy")
API.delete_command(ctx, command_id)Message options
All message functions accept an optional keyword list:
| Option | Description |
|---|---|
root_id: | Reply in a thread (post ID) |
file_ids: | List of uploaded file IDs to attach |
props: | Map of arbitrary post props |
Error handling
All API functions return {:ok, result} or {:error, %Mattermost.Error{}}.
case API.get_user(ctx, user_id) do
{:ok, user} -> user.username
{:error, %Mattermost.Error{status: 404}} -> "not found"
{:error, err} -> IO.puts(to_string(err))
endContext
The %Mattermost.Context{} passed to every callback contains:
| Field | Description |
|---|---|
config | %Mattermost{} connection config |
channel_id | Channel where the event occurred |
user_id | User who triggered the event |
post | %Mattermost.Post{} for message events, nil for slash commands |