glyde/rest

A Discord call as data. rest.request builds a gleam_http Request(Wire); rest.route gives the limiter its scheduling identity.

let config = rest.config(rest.bot(token))

let call =
  rest.post(
    [seg.lit("channels"), seg.channel(channel_id), seg.lit("messages")],
    body.json([#("content", json.string("pong"))]),
    rest.Decoded(message.decoder()),
  )

// Send it with any HTTP client, then hand the answer back to the call.
let posted = rest.response(call, status, headers, bytes)

Types

One Discord call, described and inert. Parameterised by what the endpoint returns. The bot token lives in Config and never here; a webhook or interaction credential is part of the path, and auth says so.

pub opaque type Call(a)
pub type Config {
  Config(
    token: Token,
    host: String,
    api_version: Int,
    user_agent: String,
    boundary: body.Boundary,
  )
}

Constructors

  • Config(
      token: Token,
      host: String,
      api_version: Int,
      user_agent: String,
      boundary: body.Boundary,
    )

    Arguments

    host

    Overridable for a proxy or a recording double.

pub type Expect(a) {
  Decoded(decode.Decoder(a))
  NoContent(a)
}

Constructors

  • Decoded(decode.Decoder(a))
  • NoContent(a)

    A 204 endpoint. a is what to return; usually Nil.

glyde/rest/error has the questions worth asking of one: retry_advice, counts_as_invalid_request, is_token_fatal.

pub type Failure =
  error.ApiError

Opaque and with no to_string, so the token cannot reach a log line. A closure, not a field, because string.inspect ignores opaqueness.

pub opaque type Token

Values

pub fn age_bucket(call: Call(a), created_at_ms: Int) -> Call(a)

Bucket a message deletion by the target’s age. Discord splits these and says so in no header (discord-api-docs#1295); the limiter picks the band.

pub const api_version: Int

An unversioned path routes to the deprecated v6, so this is never optional.

pub fn attach(call: Call(a), file: body.File) -> Call(a)

Add a file, turning the body multipart. Appends, because a part is named files[n] from its position and prepending would renumber the rest.

The other two bodies come back unchanged, and neither can lose a file that way: body.JsonArray is only ever a bulk replace, which takes no uploads, and a body.Finished payload already names the parts it carries, so an extra one would have no attachments entry to be matched against.

pub const base_url: String

For a request built by hand rather than from a Call. Gleam cannot concatenate constants, so a test pins it against host and api_version.

pub fn bearer(token: String) -> Token

An OAuth2 access token, sent as Authorization: Bearer <token>.

pub fn bot(token: String) -> Token

A bot token, sent as Authorization: Bot <token>.

pub const boundary: body.Boundary

Discord requires the boundary to be absent from the parts, not unpredictable, and body.encode grows this one when a part contains it.

pub fn config(token: Token) -> Config

The defaults. Reach any of them with a record update.

pub fn delete(
  segments: List(seg.Seg),
  expect: Expect(a),
) -> Call(a)
pub fn get(segments: List(seg.Seg), expect: Expect(a)) -> Call(a)
pub const host: String
pub fn patch(
  segments: List(seg.Seg),
  body: body.Body,
  expect: Expect(a),
) -> Call(a)
pub fn path_authenticated(call: Call(a)) -> Call(a)

For the webhook and interaction routes: send no Authorization header, whatever the Config holds. A 401 then means that path credential.

pub fn post(
  segments: List(seg.Seg),
  body: body.Body,
  expect: Expect(a),
) -> Call(a)
pub fn put(
  segments: List(seg.Seg),
  body: body.Body,
  expect: Expect(a),
) -> Call(a)
pub fn query(call: Call(a), params: List(query.Param)) -> Call(a)

Add query parameters. They accumulate rather than replace, so a repeated key produces ?id=1&id=2, which is how Discord reads one. query.to_string spells them out, percent-encoded, when the request is built.

Only glyde/rest/query can build a Param, so absence has no spelling here either: there is nothing to hand in for a parameter you are omitting.

pub fn reason(call: Call(a), why: String) -> Call(a)

X-Audit-Log-Reason, percent-encoded: a line break in a header value would split the request. Discord’s 512 character limit is not enforced here.

pub fn request(
  config: Config,
  call: Call(a),
) -> request.Request(body.Wire)

Always HTTPS, so no config field can send a token in the clear. A plain HTTP double is one request.set_scheme away.

pub fn response(
  call: Call(a),
  status status: Int,
  headers headers: List(#(String, String)),
  body body: BitArray,
) -> Result(a, error.ApiError)

Interpret a response with the decoder the Call carries. Success is the whole 2xx range: 201 and 204 are everyday Discord answers.

pub fn route(call: Call(a)) -> route.Route

The scheduling identity. Available without building a Request.

pub fn split_bucket(call: Call(a), name: String) -> Call(a)

Split this call into its own rate-limit bucket, for a Discord sublimit.

pub fn unauthenticated() -> Token

No Authorization header. The webhook and interaction routes drop theirs per call, so this is for a route glyde does not wrap.

pub const user_agent: String

Discord’s required shape; without it Cloudflare answers a misleading “invalid form body”. Keep the version in step with gleam.toml.

pub fn with_token(config: Config, token: Token) -> Config

Same environment, a different credential. A second rest.config would re-default host, api_version, user_agent and boundary, throwing away the proxy host you set.

Search Document