glyde/event

Gateway dispatch events, decoded.

decode is total: an unmodelled name, or a payload that did not fit, comes back as Raw with d untouched, so a host’s case needs no error arm. dispatch is the same decode with the outcome kept apart, which is how glyde’s own schema drift is told from an event it never modelled.

Types

pub type Dispatch {
  Dispatch(name: String, data: dynamic.Dynamic, outcome: Outcome)
}

Constructors

  • Dispatch(name: String, data: dynamic.Dynamic, outcome: Outcome)

    Arguments

    data

    d exactly as it arrived, the hatch for anything glyde does not model.

One decoded dispatch.

pub type Event {
  ReadyEvent(ready.Ready)
  ResumedEvent
  RateLimitedEvent(RateLimited)
  GuildCreateAvailable(guild: guild.Guild)
  GuildCreateUnavailable(id: id.Id(id.Guild))
  GuildUpdate(guild: guild.Guild)
  GuildUnavailable(id: id.Id(id.Guild))
  GuildRemoved(id: id.Id(id.Guild))
  GuildMemberAdd(
    guild_id: id.Id(id.Guild),
    member: member.GuildMember,
  )
  GuildMemberRemove(guild_id: id.Id(id.Guild), user: user.User)
  GuildMemberUpdate(
    guild_id: id.Id(id.Guild),
    member: member.GuildMember,
  )
  GuildMembersChunk(MembersChunk)
  GuildRoleCreate(guild_id: id.Id(id.Guild), role: role.Role)
  GuildRoleUpdate(guild_id: id.Id(id.Guild), role: role.Role)
  GuildRoleDelete(
    guild_id: id.Id(id.Guild),
    role_id: id.Id(id.Role),
  )
  GuildBanAdd(guild_id: id.Id(id.Guild), user: user.User)
  GuildBanRemove(guild_id: id.Id(id.Guild), user: user.User)
  GuildEmojisUpdate(
    guild_id: id.Id(id.Guild),
    emojis: List(emoji.GuildEmoji),
  )
  ChannelCreate(channel: channel.Channel)
  ChannelUpdate(channel: channel.Channel)
  ChannelDelete(channel: channel.Channel)
  ChannelPinsUpdate(
    guild_id: option.Option(id.Id(id.Guild)),
    channel_id: id.Id(id.Channel),
    last_pin_timestamp: option.Option(String),
  )
  ThreadCreate(channel: channel.Channel)
  ThreadUpdate(channel: channel.Channel)
  ThreadDelete(
    id: id.Id(id.Channel),
    guild_id: id.Id(id.Guild),
    parent_id: option.Option(id.Id(id.Channel)),
    type_: channel.ChannelType,
  )
  MessageCreate(message: message.Message)
  MessageUpdate(message: message.MessageUpdate)
  MessageDelete(
    id: id.Id(id.Message),
    channel_id: id.Id(id.Channel),
    guild_id: option.Option(id.Id(id.Guild)),
  )
  MessageDeleteBulk(
    ids: List(id.Id(id.Message)),
    channel_id: id.Id(id.Channel),
    guild_id: option.Option(id.Id(id.Guild)),
  )
  MessageReactionAdd(ReactionAdd)
  MessageReactionRemove(ReactionRemove)
  MessageReactionRemoveAll(
    channel_id: id.Id(id.Channel),
    message_id: id.Id(id.Message),
    guild_id: option.Option(id.Id(id.Guild)),
  )
  MessageReactionRemoveEmoji(
    channel_id: id.Id(id.Channel),
    message_id: id.Id(id.Message),
    guild_id: option.Option(id.Id(id.Guild)),
    emoji: emoji.Emoji,
  )
  InteractionCreate(interaction: interaction.Interaction)
  TypingStartEvent(TypingStart)
  UserUpdate(user: user.User)
  VoiceStateUpdate(voice_state.VoiceState)
  VoiceServerUpdate(
    token: String,
    guild_id: id.Id(id.Guild),
    endpoint: option.Option(String),
  )
  Raw(name: String, data: dynamic.Dynamic)
}

Constructors

  • ReadyEvent(ready.Ready)

    READY. The handshake reads session_id and resume_gateway_url out of this same d, so a decoder bug here cannot cost a session.

  • ResumedEvent

    RESUMED. Replay is finished and everything after this is new. d is {"_trace":[…]}, Discord’s own routing breadcrumb.

  • RateLimitedEvent(RateLimited)

    RATE_LIMITED. A dispatch, not an opcode: it arrives with "op":0 and advances the sequence, so a client switching on op never sees it.

  • GuildCreateAvailable(guild: guild.Guild)

    GUILD_CREATE for a guild that is up. Sent on connect for every guild the bot is in, then again whenever it joins one.

  • GuildCreateUnavailable(id: id.Id(id.Guild))

    GUILD_CREATE for a guild that is still down. Two keys, no more.

  • GuildUpdate(guild: guild.Guild)
  • GuildUnavailable(id: id.Id(id.Guild))

    GUILD_DELETE during a Discord outage. Still the bot’s guild, and it comes back as a GUILD_CREATE. Do not evict it from a cache.

  • GuildRemoved(id: id.Id(id.Guild))

    GUILD_DELETE because the bot was kicked or banned, or the guild was deleted. This one is permanent.

  • GuildMemberAdd(
      guild_id: id.Id(id.Guild),
      member: member.GuildMember,
    )

    d is the member object with an extra guild_id, which is why the id sits beside the member rather than inside it.

  • GuildMemberRemove(guild_id: id.Id(id.Guild), user: user.User)
  • GuildMemberUpdate(
      guild_id: id.Id(id.Guild),
      member: member.GuildMember,
    )

    A partial member: deaf, mute and flags can be missing despite the docs. Privileged behind GuildMembers, bar the bot’s own member.

  • GuildMembersChunk(MembersChunk)

    The answer to REQUEST_GUILD_MEMBERS, at most 1000 members per chunk.

  • GuildRoleCreate(guild_id: id.Id(id.Guild), role: role.Role)
  • GuildRoleUpdate(guild_id: id.Id(id.Guild), role: role.Role)
  • GuildRoleDelete(
      guild_id: id.Id(id.Guild),
      role_id: id.Id(id.Role),
    )
  • GuildBanAdd(guild_id: id.Id(id.Guild), user: user.User)

    Needs GuildModeration plus either BAN_MEMBERS or VIEW_AUDIT_LOG.

  • GuildBanRemove(guild_id: id.Id(id.Guild), user: user.User)
  • GuildEmojisUpdate(
      guild_id: id.Id(id.Guild),
      emojis: List(emoji.GuildEmoji),
    )

    emojis is the guild’s whole list, never a delta. Replace, do not merge: a deleted emoji is only ever signalled by its absence here.

  • ChannelCreate(channel: channel.Channel)
  • ChannelUpdate(channel: channel.Channel)
  • ChannelDelete(channel: channel.Channel)
  • ChannelPinsUpdate(
      guild_id: option.Option(id.Id(id.Guild)),
      channel_id: id.Id(id.Channel),
      last_pin_timestamp: option.Option(String),
    )

    last_pin_timestamp is ISO-8601, and None once the last pin is removed. The event does not say which message was pinned.

  • ThreadCreate(channel: channel.Channel)

    channel carries newly_created, which tells a thread opened on a brand new message from one opened on an old one.

  • ThreadUpdate(channel: channel.Channel)
  • ThreadDelete(
      id: id.Id(id.Channel),
      guild_id: id.Id(id.Guild),
      parent_id: option.Option(id.Id(id.Channel)),
      type_: channel.ChannelType,
    )

    Four keys and no more. It looks like a channel and is not one, and the channel decoder would accept it: a channel needs only id and type.

  • MessageCreate(message: message.Message)

    Without MessageContent, content, embeds, attachments and components are empty unless the message mentions the bot or is a DM.

  • MessageUpdate(message: message.MessageUpdate)

    Not a Message. Discord sends only what changed, so everything but id and channel_id is optional: an embed-only edit has no author.

  • MessageDelete(
      id: id.Id(id.Message),
      channel_id: id.Id(id.Channel),
      guild_id: option.Option(id.Id(id.Guild)),
    )
  • MessageDeleteBulk(
      ids: List(id.Id(id.Message)),
      channel_id: id.Id(id.Channel),
      guild_id: option.Option(id.Id(id.Guild)),
    )

    Guild channels only. Discord does not list this one under DIRECT_MESSAGES, so a DM bot relying on it to clean up never gets it.

  • MessageReactionAdd(ReactionAdd)
  • MessageReactionRemove(ReactionRemove)

    Not a mirror of the add. See ReactionRemove.

  • MessageReactionRemoveAll(
      channel_id: id.Id(id.Channel),
      message_id: id.Id(id.Message),
      guild_id: option.Option(id.Id(id.Guild)),
    )

    Every reaction on the message is gone.

  • MessageReactionRemoveEmoji(
      channel_id: id.Id(id.Channel),
      message_id: id.Id(id.Message),
      guild_id: option.Option(id.Id(id.Guild)),
      emoji: emoji.Emoji,
    )

    Every reaction of one emoji is gone.

  • InteractionCreate(interaction: interaction.Interaction)

    Ungated: a bot with no intents still gets every interaction.

  • TypingStartEvent(TypingStart)
  • UserUpdate(user: user.User)

    The bot’s own user changed. Not sent for anyone else.

  • VoiceStateUpdate(voice_state.VoiceState)

    A null channel_id on the state means the user left voice, and there is no separate event for that. voice_state.has_left asks it directly.

  • VoiceServerUpdate(
      token: String,
      guild_id: id.Id(id.Guild),
      endpoint: option.Option(String),
    )

    endpoint is None when the voice server went away. Disconnect and wait for the next one. Reconnecting to nothing loops.

  • Raw(name: String, data: dynamic.Dynamic)

    A dispatch glyde did not model, or one whose d did not fit, with d intact. is_modelled tells those apart; dispatch gives the errors.

pub type MembersChunk {
  MembersChunk(
    guild_id: id.Id(id.Guild),
    members: List(member.GuildMember),
    chunk_index: Int,
    chunk_count: Int,
    not_found: List(String),
    nonce: option.Option(String),
  )
}

Constructors

  • MembersChunk(
      guild_id: id.Id(id.Guild),
      members: List(member.GuildMember),
      chunk_index: Int,
      chunk_count: Int,
      not_found: List(String),
      nonce: option.Option(String),
    )

    Arguments

    members

    At most 1000. glyde does not reassemble the chunks: count them with chunk_index and chunk_count.

    not_found

    Echoes the request back, so this is whatever the caller sent, a real snowflake or not. A String, never an Id.

    nonce

    Absent when the request’s nonce was over 32 bytes, which Discord drops silently.

The three things one dispatch can be. Malformed is a glyde bug or a Discord schema change: report it. Unmodelled is ordinary.

pub type Outcome {
  Decoded(Event)
  Unmodelled
  Malformed(errors: List(decode.DecodeError))
}

Constructors

Keyed by opcode, and open. Only opcode 8’s shape is documented, and meta can be missing altogether.

pub type RateLimitMeta {
  MemberRequestMeta(
    guild_id: id.Id(id.Guild),
    nonce: option.Option(String),
  )
  UnknownMeta(opcode: Int, raw: dynamic.Dynamic)
}

Constructors

  • MemberRequestMeta(
      guild_id: id.Id(id.Guild),
      nonce: option.Option(String),
    )
  • UnknownMeta(opcode: Int, raw: dynamic.Dynamic)

    Any other opcode, and any opcode 8 whose meta had no guild_id. raw is the meta object as sent, or null when there was none.

The answer to going over a gateway send limit, today one REQUEST_GUILD_MEMBERS per guild per 30 seconds.

pub type RateLimited {
  RateLimited(
    opcode: Int,
    retry_after: Float,
    meta: RateLimitMeta,
  )
}

Constructors

  • RateLimited(opcode: Int, retry_after: Float, meta: RateLimitMeta)

    Arguments

    opcode

    The send opcode that was limited. 8 today.

    retry_after

    SECONDS, fractional, and a bare 30 when the value is whole. retry_after_ms is usually what you want.

Not a mirror of ReactionRemove: the add carries member, message_author_id and burst_colors, and the remove carries none.

pub type ReactionAdd {
  ReactionAdd(
    user_id: id.Id(id.User),
    channel_id: id.Id(id.Channel),
    message_id: id.Id(id.Message),
    guild_id: option.Option(id.Id(id.Guild)),
    member: option.Option(member.GuildMember),
    emoji: emoji.Emoji,
    message_author_id: option.Option(id.Id(id.User)),
    burst: Bool,
    burst_colors: List(String),
    type_: message.ReactionType,
  )
}

Constructors

Three fields fewer than ReactionAdd, which is Discord’s shape.

pub type ReactionRemove {
  ReactionRemove(
    user_id: id.Id(id.User),
    channel_id: id.Id(id.Channel),
    message_id: id.Id(id.Message),
    guild_id: option.Option(id.Id(id.Guild)),
    emoji: emoji.Emoji,
    burst: Bool,
    type_: message.ReactionType,
  )
}

Constructors

pub type TypingStart {
  TypingStart(
    channel_id: id.Id(id.Channel),
    guild_id: option.Option(id.Id(id.Guild)),
    user_id: id.Id(id.User),
    timestamp: Int,
    member: option.Option(member.GuildMember),
  )
}

Constructors

Values

pub fn decode(name: String, data: dynamic.Dynamic) -> Event

Decode one dispatch. Total.

pub fn dispatch(name: String, data: dynamic.Dynamic) -> Dispatch

Decode one dispatch, keeping d and saying which of the three happened. Total.

pub fn is_modelled(name: String) -> Bool

The question to ask about a Raw: an unmodelled name is ordinary, a modelled one that came back Raw is a payload that did not fit.

pub fn name(event: Event) -> String

The wire t an event came from. The inverse of decoder_for’s table: a name added to one and not the other silently drops an event.

pub fn retry_after_ms(limited: RateLimited) -> Int

retry_after in milliseconds, never negative: “retry now” is the only reading of a negative delay a host can act on. Our floor, not Discord’s.

Search Document