glyde/gateway/frame
The {op, s, t, d} envelope, in both directions.
Structure is strict, content is lenient: a frame needs an opcode and a dispatch needs a sequence and a name. Anything else Discord sends is read or ignored, never fatal.
Types
HELLO’s heartbeat_interval, already inside [1000, 600_000]. Opaque and
built nowhere but parse: an interval of 0 read straight off the wire would
arm a repeating timer with no delay, so there is no way to write one.
pub opaque type HeartbeatInterval
pub type Inbound {
Dispatch(seq: Int, name: String, data: dynamic.Dynamic)
HeartbeatRequest
Reconnect
InvalidSession(resumable: Bool)
Hello(interval: HeartbeatInterval)
HeartbeatAck
UnknownOp(op: Int)
Undecodable(reason: Unreadable)
}
Constructors
-
Dispatch(seq: Int, name: String, data: dynamic.Dynamic)op 0.
seqandnameexist exactly when the opcode is 0. -
HeartbeatRequestop 1. Discord wants a heartbeat right now.
-
Reconnectop 7.
-
InvalidSession(resumable: Bool)op 9.
dis whether the session may still be resumed. -
Hello(interval: HeartbeatInterval)op 10.
-
HeartbeatAckop 11.
-
UnknownOp(op: Int)A well formed frame with an opcode glyde does not model. Discord adding one must not kill a session.
-
Undecodable(reason: Unreadable)Not a frame we can read at all.
Discord’s gateway opcode table, both directions. The numbers are theirs, 5
was removed and is not here, and the names carry an Op prefix because
Inbound already spells four of them.
pub type Opcode {
OpDispatch
OpHeartbeat
OpIdentify
OpPresenceUpdate
OpVoiceStateUpdate
OpResume
OpReconnect
OpRequestGuildMembers
OpInvalidSession
OpHello
OpHeartbeatAck
}
Constructors
-
OpDispatch -
OpHeartbeat -
OpIdentify -
OpPresenceUpdate -
OpVoiceStateUpdate -
OpResume -
OpReconnect -
OpRequestGuildMembers -
OpInvalidSession -
OpHello -
OpHeartbeatAck
A serialised payload and the opcode that built it. Opaque so the two cannot
disagree: outbound is the only constructor, and it derives the text from
the opcode. text is the only thing that goes on the socket, and IDENTIFY
carries the token, so a log line is the easiest place to leak one.
pub opaque type Outbound
The six opcodes a client ever writes. Outbound is typed on this rather
than Opcode, so a frame carrying a receive-only op cannot be built.
pub type SendOp {
SendHeartbeat
SendIdentify
SendPresenceUpdate
SendVoiceStateUpdate
SendResume
SendRequestGuildMembers
}
Constructors
-
SendHeartbeat -
SendIdentify -
SendPresenceUpdate -
SendVoiceStateUpdate -
SendResume -
SendRequestGuildMembers
Why a frame could not be read. A value, not a sentence: the caller decides how to say it, and a test can assert which one it got.
pub type Unreadable {
BadEnvelope
NotJson
DispatchWithoutSequence
DispatchWithoutName
HelloIntervalUnreadable
}
Constructors
-
BadEnvelopeJSON, but not an
{"op": n, ...}envelope. Covers a top level that is not an object, anopthat is not a number, and noopat all. -
NotJson -
DispatchWithoutSequence -
DispatchWithoutName -
HelloIntervalUnreadable
Values
pub fn describe_unreadable(why: Unreadable) -> String
Each line has to be true of everything its variant covers: BadEnvelope
also catches a non-object top level and an op spelled as a string, and a
heartbeat_interval can be present and still not be a whole number.
pub fn heartbeat(seq: option.Option(Int)) -> Outbound
None before this session has seen a dispatch: Discord wants a literal
null, and a 0 asks for a replay from the start and earns an op 9.
pub fn heartbeat_interval_ms(interval: HeartbeatInterval) -> Int
pub fn opcode_from_int(value: Int) -> option.Option(Opcode)
The inverse of opcode_to_int. None for 5 and for anything Discord adds
after this table was written.
pub fn opcode_to_int(op: Opcode) -> Int
pub fn outbound(op: SendOp, data: json.Json) -> Outbound
One gateway payload, serialised. s and t are receive only, so an
outbound frame is op and d. Every frame glyde sends is built here,
including the ones glyde/gateway/command encodes.
pub fn outbound_op(outbound: Outbound) -> SendOp
The opcode this frame was built with. Carried so nothing downstream has to read it back out of the JSON.
pub fn outbound_text(outbound: Outbound) -> String
The serialised frame, and the only thing that goes on the socket.
pub fn parse(text: String) -> Inbound
Total: never returns an error. HELLO’s interval is clamped on the way
through, so every HeartbeatInterval that comes out is armable.
pub fn resume(
token secret: token.Token,
session_id session_id: String,
seq seq: Int,
) -> Outbound
pub fn send_op_to_int(op: SendOp) -> Int