# Noizu.Discord

[![Hex.pm](https://img.shields.io/hexpm/v/noizu_discord.svg)](https://hex.pm/packages/noizu_discord)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/noizu_discord/)
[![CI](https://github.com/noizu-labs-sdks/elixir-discord/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/noizu-labs-sdks/elixir-discord/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/noizu-labs-sdks/elixir-discord/blob/main/LICENSE)

**Discord REST API v10** client for Elixir (`https://discord.com/api/v10`).

Req-based, bot-token auth, bounded rate-limit-aware retry, lazy pagination
streams, embed builders with limit validation, GFM -> Discord markdown
conversion with 2000-character chunking, webhooks, and Ed25519 interaction
signature verification.

> **REST-only.** There is no gateway/websocket client: this library sends
> requests but does not receive gateway events (messages, presence, voice). Like
> the other house SDKs it stays stateless and process-free. Receive slash
> commands and component clicks through
> [HTTP interactions](https://discord.com/developers/docs/interactions/overview#preparing-for-interactions)
> (`Noizu.Discord.Interactions`).

## Installation

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

Runtime dependencies: Req and Jason. No supervision tree.

## Configuration

Token precedence: `Client.new(token: ...)` -> app env -> `DISCORD_BOT_TOKEN`.

```elixir
# config/runtime.exs (optional)
config :noizu_discord, :client,
  token: System.get_env("DISCORD_BOT_TOKEN")
```

| Environment variable | Used for |
|----------------------|----------|
| `DISCORD_BOT_TOKEN` | Bot token (sent as `Authorization: Bot <token>`); fallback when neither opts nor app env set one |

| Client option | Default |
|---|---|
| `:token` | see precedence above |
| `:base_url` | `https://discord.com/api/v10` |
| `:user_agent` | `DiscordBot (https://github.com/noizu-labs-sdks/elixir-discord, 0.1.0)` |
| `:req_options` | `[]` (merged into every `Req.request/1`, e.g. `plug:` for tests) |
| `:retry` | `max_attempts: 3`, `base_backoff_ms: 500`, `max_delay_ms: 30_000` |

## Usage

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

{:ok, me} = Noizu.Discord.Users.me(client)
{:ok, guilds} = Noizu.Discord.Guilds.me(client)

alias Noizu.Discord.{Channels, Embeds, Markdown}

embed =
  Embeds.new()
  |> Embeds.title("Deploy finished")
  |> Embeds.description(Markdown.from_markdown(notes, target: :embed))
  |> Embeds.color("#2ecc71")
  |> Embeds.field("Env", "prod", true)

{:ok, [embed]} = Embeds.validate(embed, on_overflow: :truncate)

{:ok, msg} = Channels.create_message(client, channel_id, %{content: "Done", embeds: [embed]})
{:ok, _} = Noizu.Discord.Reactions.create(client, channel_id, msg["id"], "✅")

# Long markdown in message content: convert, then split at 2000 chars
for part <- report |> Markdown.from_markdown() |> Markdown.chunk() do
  Channels.create_message(client, channel_id, %{content: part})
end

# Lazy history (newest first, `before` paging)
client |> Channels.messages_stream(channel_id) |> Enum.take(500)

# Webhooks need no bot token
Noizu.Discord.Webhooks.execute(webhook_url, %{content: "hi", username: "ci"})
```

### Results, errors, rate limits

Every call returns `{:ok, body}` (`nil` for 204) or
`{:error, %Noizu.Discord.Error{}}` — never raises. Discord's `{code, message,
errors}` envelope becomes `tag: :api, reason: <code>`; see
`Noizu.Discord.Error` for all tags.

429s are retried (up to 3 attempts, also for POST — Discord does not apply
rate-limited requests) honouring `retry_after` / `X-RateLimit-Reset-After`;
5xx are retried for idempotent methods only. Pass `meta: true` to get
`{:ok, body, %{status, attempts, rate_limit: %{bucket, limit, remaining, reset,
reset_after, scope, global}}}`.

### Interactions

```elixir
if Noizu.Discord.Interactions.verify_signature(public_key_hex, sig_hex, timestamp, raw_body) do
  Noizu.Discord.Interactions.respond(interaction["id"], interaction["token"], {:message, %{content: "pong"}})
end
```

Not covered in 0.1.0: file uploads (multipart), application command
registration, gateway.

## Development

```bash
mix deps.get
mix format --check-formatted
mix compile --warnings-as-errors
mix test        # Req.Test plug stubs only — no network
mix docs
```

## License

MIT — see [LICENSE](LICENSE).
