glyde
glyde is a sans-IO Discord library that brings its own IO. glyde/gateway
is the protocol state machine; this module is the socket, clock and HTTP
client already wired to it, so a bot is the handlers and nothing else.
import envoy
import gleam/int
import glyde
import glyde/intents
pub fn main() -> Nil {
use token <- glyde.require_token(envoy.get("DISCORD_TOKEN"))
let intents = intents.new([intents.Guilds, intents.GuildMessages])
glyde.new(token:, intents:, state: 0)
|> glyde.on_message(fn(bot, pongs, message) {
case message.content {
"!ping" -> {
glyde.reply(bot, message, "pong! #" <> int.to_string(pongs + 1))
pongs + 1
}
_ -> pongs
}
})
|> glyde.run
}
A handler is given the bot’s state and returns the next one, since Gleam
has no mutable cell to close over. Handlers run in the order they were
added, each seeing what the one before returned. Nothing to remember
passes Nil.
glyde/client is the layer below, for driving the machine yourself.
Types
A bot, carrying one value of your own. Opaque and a value: every builder gives back a new one.
pub opaque type Bot(state)
Why a REST call did not produce an answer. The two halves are different kinds of problem: Discord answered and said no, or nothing answered at all.
pub type CallFailure {
Refused(error.ApiError)
Unreachable(transport.Unreachable)
}
Constructors
-
Refused(error.ApiError)Discord answered, with a non-2xx or a body its own decoder would not take.
glyde/rest/erroris where the questions about it live. -
Unreachable(transport.Unreachable)The request never got an answer: no route to the host, a timeout, a proxy speaking something that is not HTTP.
pub type ChannelId =
id.Id(id.Channel)
pub type Event =
event.Event
pub type Failure =
error.ApiError
pub type Message =
message.Message
pub type Ready =
ready.Ready
Everything the runtime does that is not a Discord event. Every variant
carries the value, never a rendering: describe is the only way back.
pub type Status {
Connecting(host: String)
Reconnecting(in_ms: Int, resuming: Bool, why: gateway.Why)
Halted(reason: gateway.Halt)
Sent(op: frame.Opcode)
CallFailed(failure: CallFailure)
Undecodable(name: String, errors: List(decode.DecodeError))
Note(gateway.Notice)
}
Constructors
-
Connecting(host: String) -
Reconnecting(in_ms: Int, resuming: Bool, why: gateway.Why)The connection is gone and a redial is armed.
whyis what ended it, so a host printing this says more than “the bot is flapping”. -
Halted(reason: gateway.Halt) -
Sent(op: frame.Opcode)A frame went out, by opcode. Never the body: IDENTIFY carries the token.
-
CallFailed(failure: CallFailure)A REST call glyde made on your behalf did not work.
-
Undecodable(name: String, errors: List(decode.DecodeError))A dispatch glyde models whose
dno longer fits its decoder: a glyde bug or a Discord schema change. The listeners saw it asevent.Raw. -
Note(gateway.Notice)A diagnostic from the core: a zombie connection, a dropped command, a frame that would not decode.
The platform half: a socket, an HTTP client and a clock.
pub type Transport =
transport.Transport
Values
pub const api_version: Int
The Discord API version glyde speaks, for both the gateway and REST.
pub fn call(
bot: Bot(state),
call: rest.Call(a),
) -> Result(a, CallFailure)
Any endpoint, so the whole REST builder stays reachable from a handler. Blocks until Discord answers and hands the answer back, so a handler can put what it got into the state it returns.
pub fn describe(status: Status) -> String
One line per status, for a host that just wants to print them. Each arm hands off to the module that owns the value, so a host writing its own logging can reach the same words.
pub fn new(
token token: String,
state state: state,
intents intents: intents.Intents,
) -> Bot(state)
One shard, no compression, a real socket, and a line on stderr when
something goes wrong. For sharding, compression or a presence at connect
time, build a gateway.Config and drive it with glyde/client.
pub fn on_event(
bot: Bot(state),
handler: fn(Bot(state), state, event.Event) -> state,
) -> Bot(state)
Every dispatch, decoded. One glyde does not model arrives as event.Raw
with Discord’s payload untouched, so the case needs no error arm. One
glyde does model whose payload no longer fits arrives as Raw too, and the
decoder errors go to on_status as Undecodable.
pub fn on_message(
bot: Bot(state),
handler: fn(Bot(state), state, message.Message) -> state,
) -> Bot(state)
Every MESSAGE_CREATE. Add as many as you like.
pub fn on_ready(
bot: Bot(state),
handler: fn(Bot(state), state, ready.Ready) -> state,
) -> Bot(state)
READY, which carries the bot’s own user and the guilds it is in. Sent again after every fresh identify, so this is not once per process.
pub fn on_status(
bot: Bot(state),
handler: fn(Status) -> Nil,
) -> Bot(state)
Watch the runtime: dials, reconnects, halts, diagnostics. Replaces the default, which prints the first three to stderr.
pub fn reply(
bot: Bot(state),
to: message.Message,
text: String,
) -> Nil
Reply to a message: same channel, message_reference set, so Discord shows
it attached rather than as a bare message.
pub fn require_token(
source: Result(String, a),
next: fn(String) -> b,
) -> b
Read a token, or panic. Right in main, where the alternative is a bot
that looks like it started and is closed with 4004 ten seconds later.
pub fn run(bot: Bot(state)) -> Nil
Connect, and keep connected until the bot halts. Blocks: the loop is this
process, and main returning takes the VM with it.
pub fn send(
bot: Bot(state),
channel: id.Id(id.Channel),
body: body.Body,
) -> Nil
Post to a channel. Build the body with message.create_body: it writes
the attachments cross-reference an upload needs.
pub fn with_transport(
bot: Bot(state),
transport: transport.Transport,
) -> Bot(state)
Run over a socket, a clock and an HTTP client of your own. Anything that
answers transport.Transport works, so a test needs no network.