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
Everything IDENTIFY puts on the wire.
pub type Identity {
Identity(
token: String,
intents: intents.Intents,
properties: Properties,
compress: Bool,
large_threshold: LargeThreshold,
shard: Sharding,
presence: option.Option(presence.Presence),
)
}
Constructors
-
Identity( token: String, intents: intents.Intents, properties: Properties, compress: Bool, large_threshold: LargeThreshold, shard: Sharding, presence: option.Option(presence.Presence), )Arguments
- compress
-
Asks for per-payload zlib. Not the
compress=query parameter, which asks for transport compression; Discord will not do both. - presence
-
What the bot looks like the moment it connects. Op 3 sends the same object once it is live.
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.
Guild size above which Discord sends an offline member list. Opaque so the clamp happens once, where the value is made, rather than again wherever it is written.
pub opaque type LargeThreshold
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. The opcode travels with
the text so nothing downstream has to read it back out of the JSON, and
text is the only thing that goes on the socket: IDENTIFY carries the
token, and a log line is the easiest place to leak one.
pub type Outbound {
Outbound(op: Opcode, text: String)
}
Constructors
-
Outbound(op: Opcode, text: String)
IDENTIFY’s properties object. Analytics only; Discord acts on none of
them. The $os spelling is deprecated, so these go out unprefixed.
pub type Properties {
Properties(os: String, browser: String, device: String)
}
Constructors
-
Properties(os: String, browser: String, device: String)
One shard’s place in the fleet, sent as [index, count]. Opaque because
index is 0-based and must be below count: sharding is the only thing
that can make one, so a fleet the gateway would answer with close 4010
cannot be built at all. Labelled and not a pair, because swapping the two
is also 4010 and takes a restart to spot.
pub opaque type Sharding
Why a fleet was refused. Both are configuration mistakes that no reconnect fixes, so they are caught before a shard exists.
pub type ShardingError {
EmptyFleet(count: Int)
IndexOutOfRange(index: Int, count: Int)
}
Constructors
-
EmptyFleet(count: Int)A fleet has at least one shard in it.
-
IndexOutOfRange(index: Int, count: Int)indexis 0-based, so shard 16 of 16 is not a shard.
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 large_threshold(value: Int) -> LargeThreshold
Clamped rather than refused: a value outside Discord’s range is a number to correct, not a connection to fail.
pub fn large_threshold_value(threshold: LargeThreshold) -> 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: Opcode, 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 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 token: String,
session_id session_id: String,
seq seq: Int,
) -> Outbound
pub fn shard_index(sharding: Sharding) -> Int
This shard’s 0-based place in the fleet.
pub fn sharding(
index index: Int,
count count: Int,
) -> Result(Sharding, ShardingError)
sharding(index: 0, count: 1) is an unsharded bot, which Discord reads as
sending no shard array at all.