Skip to main content

src/discord_webhook.gleam

import gleam/http
import gleam/http/request
import gleam/httpc
import gleam/json
import gleam/int
import gleam/list
import gleam/string
import gleam/option.{ type Option, None, Some }

// Main webhook object 
pub opaque type Webhook {
  Webhook(url: String)
}

// Embed type 
pub opaque type Embed {
  Embed(
    title: Option(String),
    description: Option(String),
    color: Option(Int),
    fields: List(#(String, String, Bool)),
  )
}

// Message type 
pub opaque type Message {
  Message(content: List(String), embeds: List(Embed))
}

// Initialize an empty embed
pub fn embed() -> Embed {
  Embed(title: None, description: None, color: None, fields: [])
}

// Add embed title
pub fn embed_title(embed: Embed, title: String) -> Embed {
  Embed(..embed, title: Some(title))
}

// Add embed description
pub fn embed_description(embed: Embed, description: String) -> Embed {
  Embed(..embed, description: Some(description))
}

// Add embed color 
pub fn embed_color(embed: Embed, color: Int) -> Embed {
  Embed(..embed, color: Some(color))
}

// Appends a new field to the end of the embed's fields list
pub fn embed_field(
  embed: Embed,
  name: String,
  value: String,
  inline: Bool,
) -> Embed {
  let new_field = #(name, value, inline)
  Embed(..embed, fields: list.append(embed.fields, [new_field]))
}

// Create a message with text content
pub fn message(content: String) -> Message {
  Message(content: [content], embeds: [])
}

// Create an empty message for building from scratch
pub fn empty() -> Message {
  Message(content: [], embeds: [])
}

// Add more text to the message
pub fn add_message(msg: Message, content: String) -> Message {
  Message(..msg, content: list.append(msg.content, [content]))
}

// Add embed to message
pub fn add_embed(msg: Message, embed: Embed) -> Message {
  Message(..msg, embeds: list.append(msg.embeds, [embed]))
}

// .new() function to declare a webhook 
pub fn new(url: String) -> Webhook {
  Webhook(url: url)
}

// Private helper to convert Embed type into raw JSON format
fn embed_to_json(embed: Embed) -> json.Json {
  let title_field = case embed.title {
    Some(t) -> [#("title", json.string(t))]
    None -> []
  }

  let description_field = case embed.description {
    Some(d) -> [#("description", json.string(d))]
    None -> []
  }

  let color_field = case embed.color {
    Some(c) -> [#("color", json.int(c))]
    None -> []
  }

  let fields_json = case embed.fields {
    [] -> []
    fields ->
      [
        #(
          "fields",
          json.array(
            fields,
            fn(field) {
              json.object([
                #("name", json.string(field.0)),
                #("value", json.string(field.1)),
                #("inline", json.bool(field.2)),
              ])
            },
          ),
        )
      ]
  }

  let all_fields =
    list.flatten([title_field, description_field, color_field, fields_json])

  json.object(all_fields)
}

// Private helper to convert Message type into raw JSON text format
fn convert_json(message: Message) -> String {
  let content_field = case message.content {
    [] -> []
    [single] -> [#("content", json.string(single))]
    multiple -> [#("content", json.string(string.join(multiple, "\n")))]
  }

  let embeds_field = case message.embeds {
    [] -> []
    embeds -> [#("embeds", json.array(embeds, embed_to_json))]
  }

  json.object(list.flatten([content_field, embeds_field]))
  |> json.to_string
}

// .send() function to send the webhook
pub fn send(webhook: Webhook, msg: Message) -> Result(Nil, String) {
  let json_payload = convert_json(msg)

  case request.to(webhook.url) {
    Error(_) -> Error("Failed to parse webhook URL.\nHint: Check the webhook url.")
    Ok(base_req) -> {
      let req =
        base_req
        |> request.set_method(http.Post)
        |> request.set_header("content-type", "application/json")
        |> request.set_body(json_payload)

      case httpc.send(req) {
        Ok(response) -> case response.status {
          204 -> Ok(Nil)
          _ ->
            Error(
              "Expected status 204, got " <> int.to_string(response.status),
            )
        }
        Error(_) -> Error("Failed to fire HTTP request entirely.")
      }
    }
  }
}