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

Installation

# mix.exs
def deps do
  [
    {:mattermost, "~> 0.1"}
  ]
end

Setup

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" field

3. 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
end

Only 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
end

7. 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")
end

handle_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.")
end

handle_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}>!")
end

API 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&#39;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))
end

Context

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