Noizu.Slack

Hex.pm Hex Docs License: MIT

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

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

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:

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.

mix deps.get
mix test

License

MIT — see LICENSE.