# Noizu.Slack

[![Hex.pm](https://img.shields.io/hexpm/v/noizu_slack.svg)](https://hex.pm/packages/noizu_slack)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/noizu_slack/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/noizu-labs-sdks/elixir-slack/blob/main/LICENSE)

**Slack Web API** client for Elixir.

`Req`-based, explicit-client design: build a `Noizu.Slack.Client` and pass it to every
call. Slack's `{"ok": ...}` envelope is decoded for you — every call returns
`{:ok, map}` or `{:error, %Noizu.Slack.Error{}}` (never raised). HTTP 429 honours
`Retry-After` inside a bounded retry (3 attempts). Includes cursor streams, the
external file-upload flow, incoming webhooks, request-signature verification,
Block Kit builders, and a GitHub-markdown → mrkdwn converter.

## Installation

```elixir
def deps do
  [
    {:noizu_slack, "~> 0.1.0"}
  ]
end
```

Runtime dependencies: `req`, `jason`.

## Configuration

| Setting | Source (highest wins) | Default |
|---|---|---|
| token | `Client.new(token: …)` → `config :noizu_slack, :client, token: …` → `SLACK_BOT_TOKEN` env var | none (required at request time) |
| base URL | `:base_url` opt → app env | `https://slack.com/api` |
| user agent | `:user_agent` opt → app env | `noizu_slack/0.1.0 (+repo url)` |
| Req passthrough | `:req_options` opt → app env | `[]` |
| retry | `:retry` opt (`max_attempts`, `base_backoff_ms`, `max_retry_after_ms`, `sleep_fn`) | `3` attempts, 500 ms backoff, 30 s clamp |

```elixir
# config/runtime.exs
config :noizu_slack, :client, token: System.get_env("SLACK_BOT_TOKEN")
```

Signing secrets (for `Noizu.Slack.Signature`) and webhook URLs are passed explicitly;
the library never reads them from the environment. Treat both as secrets.

## Usage

```elixir
client = Noizu.Slack.client()

{:ok, %{"user_id" => _bot}} = Noizu.Slack.Auth.test(client)

# Messages
{:ok, %{"ts" => ts}} = Noizu.Slack.Chat.post_message(client, "C0123456", "Deploy *finished*")
{:ok, _} = Noizu.Slack.Chat.post_message(client, "C0123456", "reply", thread_ts: ts)
{:ok, _} = Noizu.Slack.Chat.update(client, "C0123456", ts, "Deploy *finished* :white_check_mark:")
{:ok, _} = Noizu.Slack.Reactions.add(client, "C0123456", ts, "rocket")

# Block Kit
alias Noizu.Slack.Blocks

{:ok, blocks} =
  Blocks.validate([
    Blocks.header("Release 1.4.0"),
    Blocks.section("*api* and *web* rolled out", fields: ["*Env*\nprod", "*By*\n<@U123>"]),
    Blocks.actions([Blocks.button("Open run", "open_run", url: "https://example.com/run/1")])
  ])

{:ok, _} = Noizu.Slack.Chat.post_message(client, "C0123456", %{text: "Release 1.4.0", blocks: blocks})

# Markdown → mrkdwn / blocks
Noizu.Slack.Mrkdwn.from_markdown("## Notes\n- **bold** and [link](https://x.io)")
#=> "*Notes*\n• *bold* and <https://x.io|link>"
blocks = Noizu.Slack.Mrkdwn.to_blocks(long_markdown)

# Reading (one page, or lazy streams across pages)
{:ok, %{"channels" => _}} = Noizu.Slack.Conversations.list(client, types: ["public_channel"])
Noizu.Slack.Conversations.history_stream(client, "C0123456", limit: 200) |> Enum.take(500)
Noizu.Slack.Users.list_stream(client) |> Stream.reject(& &1["is_bot"]) |> Enum.to_list()

# Search needs a user token — override per call
{:ok, _} = Noizu.Slack.Search.messages(client, "in:#deploys failed", token: user_token)

# Files (getUploadURLExternal → bytes → completeUploadExternal)
{:ok, _} = Noizu.Slack.Files.upload(client, "C0123456", %{filename: "report.csv", content: csv, title: "Report"})

# Incoming webhook (no client)
{:ok, "ok"} = Noizu.Slack.Webhook.post(webhook_url, %{text: "hello"})

# Verify an inbound Events API / slash-command request (raw body!)
:ok = Noizu.Slack.Signature.verify(signing_secret, ts_header, raw_body, sig_header)
```

Any unwrapped Web API method is reachable through the engine directly:

```elixir
Noizu.Slack.Request.post(client, "bookmarks.add", %{channel_id: "C1", title: "Runbook", type: "link", link: "https://…"})
Noizu.Slack.Request.get(client, "team.info")
```

## Errors

`%Noizu.Slack.Error{tag:, reason:, status:, body:, message:}` — `tag` is one of
`:api` (`ok:false`, `reason` = Slack error string), `:rate_limited`, `:http`,
`:invalid_response`, `:transport`, `:config`, `:upload`, `:webhook`, `:signature`,
`:blocks`. Slack warnings on successful calls are collected under
`body["_meta"]["warnings"]`.

## Testing

Tests stub HTTP with `Req.Test`-style plugs (`req_options: [plug: fun]`); no network.

```bash
mix deps.get
mix test
```

## License

MIT — see [LICENSE](LICENSE).
