refluxer 0.2.0

Rust API wrapper for Fluxer
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};

use crate::model::channel::Channel;
use crate::model::emoji::{Emoji, PartialEmoji};
use crate::model::guild::{Guild, Member, Role};
use crate::model::id::{ChannelId, GuildId, MessageId, RoleId, UserId};
use crate::model::message::Message;
use crate::model::sticker::Sticker;
use crate::model::user::User;

// ---------------------------------------------------------------------------
// Opcodes
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum Opcode {
    Dispatch = 0,
    Heartbeat = 1,
    Identify = 2,
    PresenceUpdate = 3,
    VoiceStateUpdate = 4,
    Resume = 6,
    Reconnect = 7,
    InvalidSession = 9,
    Hello = 10,
    HeartbeatAck = 11,
}

// ---------------------------------------------------------------------------
// Wire payloads (non-dispatch)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayPayload {
    pub op: Opcode,
    #[serde(default)]
    pub d: Option<Value>,
    #[serde(default)]
    pub s: Option<u64>,
    #[serde(default)]
    pub t: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct HelloPayload {
    pub heartbeat_interval: u64,
}

// Fluxer's Identify takes no `intents` field as of 2026-04-15 — all dispatch
// events are delivered by default. Verified against a live gateway session:
// MESSAGE_REACTION_REMOVE, VOICE_STATE_UPDATE and SESSIONS_REPLACE all flowed
// through without any intent bits being set. If Fluxer introduces intents
// later, add `pub intents: u32` here and pipe it through ClientBuilder.
#[derive(Debug, Clone, Serialize)]
pub struct IdentifyPayload {
    pub token: String,
    pub properties: IdentifyProperties,
}

#[derive(Debug, Clone, Serialize)]
pub struct IdentifyProperties {
    pub os: String,
    pub browser: String,
    pub device: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct ResumePayload {
    pub token: String,
    pub session_id: String,
    pub seq: u64,
}

// ---------------------------------------------------------------------------
// Dispatch event payloads
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
pub struct ReadyPayload {
    pub user: User,
    pub session_id: String,
    pub guilds: Vec<Value>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ResumedPayload {
    #[serde(default)]
    pub _trace: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MessageDeletePayload {
    pub id: MessageId,
    pub channel_id: ChannelId,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildDeletePayload {
    pub id: GuildId,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildMemberAddPayload {
    pub guild_id: GuildId,
    #[serde(flatten)]
    pub member: Member,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildMemberRemovePayload {
    pub guild_id: GuildId,
    pub user: User,
}

#[derive(Debug, Clone, Deserialize)]
pub struct TypingStartPayload {
    pub channel_id: ChannelId,
    pub user_id: UserId,
    pub timestamp: u64,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MessageReactionAddPayload {
    pub user_id: UserId,
    pub channel_id: ChannelId,
    pub message_id: MessageId,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
    #[serde(default)]
    pub member: Option<Member>,
    pub emoji: PartialEmoji,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MessageReactionRemovePayload {
    pub user_id: UserId,
    pub channel_id: ChannelId,
    pub message_id: MessageId,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
    pub emoji: PartialEmoji,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MessageReactionRemoveAllPayload {
    pub channel_id: ChannelId,
    pub message_id: MessageId,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MessageReactionRemoveEmojiPayload {
    pub channel_id: ChannelId,
    pub message_id: MessageId,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
    pub emoji: PartialEmoji,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildRoleCreatePayload {
    pub guild_id: GuildId,
    pub role: Role,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildRoleUpdatePayload {
    pub guild_id: GuildId,
    pub role: Role,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildRoleDeletePayload {
    pub guild_id: GuildId,
    pub role_id: RoleId,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildBanAddPayload {
    pub guild_id: GuildId,
    pub user: User,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildBanRemovePayload {
    pub guild_id: GuildId,
    pub user: User,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildMemberUpdatePayload {
    pub guild_id: GuildId,
    pub user: User,
    #[serde(default)]
    pub roles: Vec<RoleId>,
    #[serde(default)]
    pub nick: Option<String>,
    #[serde(default)]
    pub joined_at: Option<String>,
    #[serde(default)]
    pub premium_since: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ChannelPinsUpdatePayload {
    #[serde(default)]
    pub guild_id: Option<GuildId>,
    pub channel_id: ChannelId,
    #[serde(default)]
    pub last_pin_timestamp: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MessageDeleteBulkPayload {
    pub ids: Vec<MessageId>,
    pub channel_id: ChannelId,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct InviteCreatePayload {
    pub channel_id: ChannelId,
    pub code: String,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
    #[serde(default)]
    pub inviter: Option<User>,
    #[serde(default)]
    pub created_at: Option<String>,
    #[serde(default)]
    pub max_age: Option<u64>,
    #[serde(default)]
    pub max_uses: Option<u64>,
    #[serde(default)]
    pub temporary: bool,
    #[serde(default)]
    pub uses: u64,
}

#[derive(Debug, Clone, Deserialize)]
pub struct InviteDeletePayload {
    pub channel_id: ChannelId,
    pub code: String,
    #[serde(default)]
    pub guild_id: Option<GuildId>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct WebhooksUpdatePayload {
    pub guild_id: GuildId,
    pub channel_id: ChannelId,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildEmojisUpdatePayload {
    pub guild_id: GuildId,
    pub emojis: Vec<Emoji>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GuildStickersUpdatePayload {
    pub guild_id: GuildId,
    pub stickers: Vec<Sticker>,
}

// ---------------------------------------------------------------------------
// Macro + event enum
// ---------------------------------------------------------------------------

/// Declares [`GatewayEvent`] enum and its `from_dispatch` constructor.
///
/// Each entry maps a Gateway event name to an enum variant with a typed payload.
/// Unknown events and deserialization failures gracefully fall back to
/// `GatewayEvent::Unknown` with a warning log.
macro_rules! gateway_events {
    ( $( $event_name:literal => $variant:ident ( $payload:ty ) ),* $(,)? ) => {
        #[derive(Debug, Clone)]
        pub enum GatewayEvent {
            $( $variant($payload), )*
            /// An event the library does not (yet) have a typed variant for,
            /// or a known event whose payload failed to deserialize.
            Unknown {
                event: String,
                data: Value,
            },
        }

        impl GatewayEvent {
            pub fn from_dispatch(event_name: &str, data: Value) -> Self {
                let result: Result<Self, serde_json::Error> = match event_name {
                    $(
                        $event_name => serde_json::from_value(data.clone()).map(Self::$variant),
                    )*
                    _ => return Self::Unknown { event: event_name.to_string(), data },
                };

                match result {
                    Ok(event) => event,
                    Err(e) => {
                        tracing::warn!(
                            event = event_name,
                            error = %e,
                            "failed to deserialize dispatch event, treating as Unknown",
                        );
                        Self::Unknown { event: event_name.to_string(), data }
                    }
                }
            }
        }
    };
}

gateway_events! {
    "READY"                         => Ready(ReadyPayload),
    "RESUMED"                       => Resumed(ResumedPayload),
    "USER_UPDATE"                   => UserUpdate(User),
    "MESSAGE_CREATE"                => MessageCreate(Message),
    "MESSAGE_UPDATE"                => MessageUpdate(Message),
    "MESSAGE_DELETE"                => MessageDelete(MessageDeletePayload),
    "MESSAGE_DELETE_BULK"           => MessageDeleteBulk(MessageDeleteBulkPayload),
    "MESSAGE_REACTION_ADD"          => MessageReactionAdd(MessageReactionAddPayload),
    "MESSAGE_REACTION_REMOVE"       => MessageReactionRemove(MessageReactionRemovePayload),
    "MESSAGE_REACTION_REMOVE_ALL"   => MessageReactionRemoveAll(MessageReactionRemoveAllPayload),
    "MESSAGE_REACTION_REMOVE_EMOJI" => MessageReactionRemoveEmoji(MessageReactionRemoveEmojiPayload),
    "GUILD_CREATE"                  => GuildCreate(Guild),
    "GUILD_UPDATE"                  => GuildUpdate(Guild),
    "GUILD_DELETE"                  => GuildDelete(GuildDeletePayload),
    "GUILD_MEMBER_ADD"              => GuildMemberAdd(GuildMemberAddPayload),
    "GUILD_MEMBER_UPDATE"           => GuildMemberUpdate(GuildMemberUpdatePayload),
    "GUILD_MEMBER_REMOVE"           => GuildMemberRemove(GuildMemberRemovePayload),
    "GUILD_ROLE_CREATE"             => GuildRoleCreate(GuildRoleCreatePayload),
    "GUILD_ROLE_UPDATE"             => GuildRoleUpdate(GuildRoleUpdatePayload),
    "GUILD_ROLE_DELETE"             => GuildRoleDelete(GuildRoleDeletePayload),
    "GUILD_BAN_ADD"                 => GuildBanAdd(GuildBanAddPayload),
    "GUILD_BAN_REMOVE"              => GuildBanRemove(GuildBanRemovePayload),
    "CHANNEL_CREATE"                => ChannelCreate(Channel),
    "CHANNEL_UPDATE"                => ChannelUpdate(Channel),
    "CHANNEL_DELETE"                => ChannelDelete(Channel),
    "CHANNEL_PINS_UPDATE"           => ChannelPinsUpdate(ChannelPinsUpdatePayload),
    "TYPING_START"                  => TypingStart(TypingStartPayload),
    "INVITE_CREATE"                 => InviteCreate(InviteCreatePayload),
    "INVITE_DELETE"                 => InviteDelete(InviteDeletePayload),
    "WEBHOOKS_UPDATE"               => WebhooksUpdate(WebhooksUpdatePayload),
    "GUILD_EMOJIS_UPDATE"           => GuildEmojisUpdate(GuildEmojisUpdatePayload),
    "GUILD_STICKERS_UPDATE"         => GuildStickersUpdate(GuildStickersUpdatePayload),
}