README.md

# Mattermost

An Elixir bot framework for [Mattermost](https://mattermost.com). 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/3` callbacks
- 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

```elixir
# 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

```bash
curl https://your-mattermost.com/api/v4/users/me \
  -H "Authorization: Bearer YOUR_TOKEN"
# copy the "id" field
```

### 3. Configure

```elixir
# 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

```elixir
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

```elixir
# lib/my_app/application.ex
children = [
  MyApp.Bot
]
```

The WebSocket connection starts automatically when the supervisor starts.

### 6. Mount the slash command plug (Phoenix)

```elixir
# 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`:

```elixir
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.

```elixir
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.

```elixir
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.

```elixir
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

```elixir
# 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

```elixir
{: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

```elixir
{:ok, user} = API.get_user(ctx, user_id)
{:ok, user} = API.get_user_by_username(ctx, "john.doe")
{:ok, me}   = API.me(ctx)
```

### Channels

```elixir
{:ok, channel} = API.get_channel(ctx, channel_id)
```

### Slash commands

```elixir
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{}}`.

```elixir
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 |