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. seq and name exist exactly when the opcode is 0.

  • HeartbeatRequest

    op 1. Discord wants a heartbeat right now.

  • Reconnect

    op 7.

  • InvalidSession(resumable: Bool)

    op 9. d is whether the session may still be resumed.

  • Hello(interval: HeartbeatInterval)

    op 10.

  • HeartbeatAck

    op 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)

    index is 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

  • BadEnvelope

    JSON, but not an {"op": n, ...} envelope. Covers a top level that is not an object, an op that is not a number, and no op at 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 identify(identity: Identity) -> Outbound
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_count(sharding: Sharding) -> Int

How many shards the fleet has.

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.

pub fn unsharded() -> Sharding

The one fleet that needs no checking, and the default a bot connects with.

Search Document