README.md

# Holler

**URL-based notification library for Elixir** — send notifications to 18 services
with a single URL string. An Elixir port of [shoutrrr](https://github.com/containrrr/shoutrrr).

## Installation

Add `holler` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:holler, "~> 0.2"}
  ]
end
```

## Usage

### Send to a single service

```elixir
Holler.send("gotify://example.com/AqEtoken123456", "Hello from Holler!")
# => :ok
```

### Send to multiple services concurrently

```elixir
urls = [
  "gotify://example.com/AqEtoken123456",
  "slack://hook:TXXXXXXXX/BXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX",
  "telegram://123456789:AABBccdd@telegram?chats=-100123456789"
]

{:ok, sender} = Holler.create_sender(urls)
Holler.Sender.send(sender, "Deploy complete!")
# => [:ok, :ok, :ok]
```

### Override per-send params

```elixir
Holler.send(
  "ntfy://ntfy.sh/my-topic",
  "Disk usage at 95%",
  %{title: "Warning", priority: "high"}
)
```

## Supported services

| Service      | URL scheme      | Example URL |
|--------------|-----------------|-------------|
| Bark         | `bark`          | `bark://:devicekey@api.day.app` |
| Discord      | `discord`       | `discord://token@webhook_id` |
| Generic      | `generic`       | `generic://example.com/webhook` |
| Google Chat  | `googlechat`    | `googlechat://chat.googleapis.com/v1/spaces/FOO/messages?key=bar&token=baz` |
| Gotify       | `gotify`        | `gotify://example.com/AqEtoken123456` |
| IFTTT        | `ifttt`         | `ifttt://webhookkey/?events=myevent` |
| Join         | `join`          | `join://shoutrrr:apikey@join/?devices=device1` |
| Matrix       | `matrix`        | `matrix://:accesstoken@matrix.example.com/` |
| Mattermost   | `mattermost`    | `mattermost://example.com/webhooktoken` |
| Ntfy         | `ntfy`          | `ntfy://ntfy.sh/my-topic` |
| OpsGenie     | `opsgenie`      | `opsgenie://api.opsgenie.com/myapikey` |
| Pushbullet   | `pushbullet`    | `pushbullet://myapitoken` |
| Pushover     | `pushover`      | `pushover://shoutrrr:token@userkey/` |
| Rocket.Chat  | `rocketchat`    | `rocketchat://example.com/tokenA/tokenB/general` |
| Slack        | `slack`         | `slack://hook:TXXXXXXXX/BXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX` |
| Teams        | `teams`         | `teams://group@tenant/altId/groupOwner?host=org.webhook.office.com` |
| Telegram     | `telegram`      | `telegram://botid:token@telegram?chats=@mychannel` |
| Zulip        | `zulip`         | `zulip://bot%40example.com:apikey@org.zulipchat.com/?stream=general` |
| Mercure      | `mercure`       | `mercure://:jwt@example.com?topic=https://example.com/books/1` |

## API

### `Holler.send/3`

```elixir
@spec send(url :: String.t(), message :: String.t(), params :: map()) ::
  :ok | {:error, term()}
```

Parses `url`, initialises the matching service, and sends `message` in one step.
`params` is merged into the config and can override per-call options like `title`
or `priority`.

### `Holler.create_sender/1`

```elixir
@spec create_sender([String.t()]) :: {:ok, Holler.Sender.t()} | {:error, term()}
```

Parses and initialises all URLs upfront. Returns a `Holler.Sender` struct that can
be reused to send to all configured services concurrently.

### `Holler.Sender.send/2`

```elixir
@spec send(Holler.Sender.t(), message :: String.t(), params :: map()) ::
  [:ok | {:error, term()}]
```

Sends `message` to all services in the sender concurrently using
`Task.async_stream`. Returns a list of results in the same order as the URLs
passed to `create_sender/1`.

## License

MIT — see [LICENSE](LICENSE).