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.

The payload records and their decoders live in glyde/event/*, one module per family. This module is the sum type and the table that routes to them.

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(session.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(guilds.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(messages.ReactionAdd)
  MessageReactionRemove(messages.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(presence.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(session.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(guilds.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(messages.ReactionAdd)
  • MessageReactionRemove(messages.ReactionRemove)

    Not a mirror of the add. See messages.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(presence.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.

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

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.

Search Document