From 7369016d47eff67fa873e0765cbf5852aaa9f357 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:55:35 +0100 Subject: [PATCH] Fix some gateway deserialization errors (#577) ## Description: - Discord is messing with their api again, read states are a mystery - Refactored the gateway to fully use the `Opcode` enum instead of constants (4ed68ce7a506d0354730b4b4f0b7d4267c5e2a50) - Added Last Messages request and response (4ed68ce7a506d0354730b4b4f0b7d4267c5e2a50) - Fixed a deserialization error related to `presences` in `GuildMembersChunk` being an array, not a single value (4baecf978415b680d5ef13061c24e5e363d19988) - Fixed a deserialization error with deserializing `activities` in `PresenceUpdate` as an empty array when they are sent as `null` (1b201020e9dec23e2dfcd467e49c58b151d88e0e) - Add type `OneOrMoreSnowflakes`, allow `GatewayRequestGuildMembers` to request multiple guild and user ids (644d3beb90bc1aedd2dd222cab199457d4dffddd, 85e922bc5039bd2af091b4a95d4b7f7d82ade7e2) - Updated `LazyRequest` (op 14) to use the `Snowflake` type for ids instead of just `String` (61ac7d1465f2509bed01ddefc31228c31d893704) - Fixed a deserialization error on discord.com related to experiments (they are not implemented yet, see #578) (7feb57186a8bb3f873cc4d5935280de0c8687511) - Fixed a deserialization error on discord.com related to `last_viewed` in `ReadState` being a version / counter, not a `DateTime` (fb94afa390e41caeec3883d5feaf8aa6dc9b8985) ## Commits: * fix: temporarily fix READY on Spacebar Spacebar servers have mention_count in ReadStateEntry as nullable, as well as not having the flags field This should probably be investigated further Reported by greysilly7 on the polyphony discord * fix: on DDC the user field in Relationship is not send in READY anymore * fix: for some reason presences wasn't an array?? * fix: deserialize activities: null as empty array * feat: add OneOrMoreSnowflakes type Adds a public type which allows serializing one snowflake or an array of snowflakes. Useful for e.g. request guild members, where we can request either for one user or for multiple * feat: update RequestGuildMembers, allow multiple snowflakes Updates the RequsetGuildMembers gateway event, allows to query multiple user_ids and guild_ids using OneOrMoreSnowflakes type, update its documentation * fix?: add Default to OneOrMoreSnowflakes, GatewayRequestGuildMembers * feat: add gateway last messages, refactor gateway to use Opcode enum * fix: the string in lazy request is a snowflake * fix: deserialization error related to experiments on Discord.com See also #578 on why we aren't currently juts implementing them as types * fix?: last_viewed is a counter, not a datetime thanks to MaddyUnderStars; this explanation seems to make sense considering we only need to know if we are ahead / behind the version, not when exactly it was accessed --- src/gateway/events.rs | 1 + src/gateway/gateway.rs | 86 +++++++++++++-------------- src/gateway/handle.rs | 50 +++++++++++----- src/gateway/heartbeat.rs | 12 ++-- src/gateway/mod.rs | 50 +--------------- src/types/entities/relationship.rs | 10 +++- src/types/events/call.rs | 62 +++++++++++-------- src/types/events/guild.rs | 2 +- src/types/events/lazy_request.rs | 3 +- src/types/events/message.rs | 25 ++++++++ src/types/events/presence.rs | 9 ++- src/types/events/ready.rs | 25 ++++++-- src/types/events/request_members.rs | 40 ++++++++++--- src/types/utils/mod.rs | 2 +- src/types/utils/snowflake.rs | 92 +++++++++++++++++++++++++++++ 15 files changed, 311 insertions(+), 158 deletions(-) diff --git a/src/gateway/events.rs b/src/gateway/events.rs index fdbdc1e7..41780503 100644 --- a/src/gateway/events.rs +++ b/src/gateway/events.rs @@ -72,6 +72,7 @@ pub struct Message { pub reaction_remove_emoji: Publisher, pub recent_mention_delete: Publisher, pub ack: Publisher, + pub last_messages: Publisher, } #[derive(Default, Debug)] diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index 24e9fa04..98ac9cea 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -17,7 +17,7 @@ use super::{Sink, Stream}; use crate::types::{ self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, ChannelUpdate, CloseCode, GatewayInvalidSession, GatewayReconnect, Guild, GuildRoleCreate, - GuildRoleUpdate, JsonField, RoleObject, SourceUrlField, ThreadUpdate, UpdateMessage, + GuildRoleUpdate, JsonField, Opcode, RoleObject, SourceUrlField, ThreadUpdate, UpdateMessage, WebSocketEvent, }; @@ -117,13 +117,14 @@ impl Gateway { let gateway_payload: types::GatewayReceivePayload = serde_json::from_str(&message.0).unwrap(); - if gateway_payload.op_code != GATEWAY_HELLO { + if gateway_payload.op_code != (Opcode::Hello as u8) { + warn!("GW: Received a non-hello opcode ({}) on gateway init", gateway_payload.op_code); return Err(GatewayError::NonHelloOnInitiate { opcode: gateway_payload.op_code, }); } - info!("GW: Received Hello"); + debug!("GW: Received Hello"); let gateway_hello: types::HelloData = serde_json::from_str(gateway_payload.event_data.unwrap().get()).unwrap(); @@ -348,16 +349,25 @@ impl Gateway { return; }; - // See https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes - match gateway_payload.op_code { + let op_code_res = Opcode::try_from(gateway_payload.op_code); + + if op_code_res.is_err() { + warn!("Received unrecognized gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); + trace!("Event data: {:?}", gateway_payload); + return; + } + + let op_code = op_code_res.unwrap(); + + match op_code { // An event was dispatched, we need to look at the gateway event name t - GATEWAY_DISPATCH => { + Opcode::Dispatch => { let Some(event_name) = gateway_payload.event_name else { - warn!("Gateway dispatch op without event_name"); + warn!("GW: Received dispatch without event_name"); return; }; - trace!("Gateway: Received {event_name}"); + trace!("GW: Received {event_name}"); macro_rules! handle { ($($name:literal => $($path:ident).+ $( $message_type:ty: $update_type:ty)?),*) => { @@ -483,6 +493,7 @@ impl Gateway { "INTERACTION_CREATE" => interaction.create, // TODO "INVITE_CREATE" => invite.create, // TODO "INVITE_DELETE" => invite.delete, // TODO + "LAST_MESSAGES" => message.last_messages, "MESSAGE_CREATE" => message.create, "MESSAGE_UPDATE" => message.update, // TODO "MESSAGE_DELETE" => message.delete, @@ -511,14 +522,28 @@ impl Gateway { } // We received a heartbeat from the server // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." - GATEWAY_HEARTBEAT => { + Opcode::Heartbeat => { trace!("GW: Received Heartbeat // Heartbeat Request"); // Tell the heartbeat handler it should send a heartbeat right away + let heartbeat_communication = HeartbeatThreadCommunication { + sequence_number: gateway_payload.sequence_number, + op_code: Some(Opcode::Heartbeat), + }; + self.heartbeat_handler + .send + .send(heartbeat_communication) + .await + .unwrap(); + } + Opcode::HeartbeatAck => { + trace!("GW: Received Heartbeat ACK"); + + // Tell the heartbeat handler we received an ack let heartbeat_communication = HeartbeatThreadCommunication { sequence_number: gateway_payload.sequence_number, - op_code: Some(GATEWAY_HEARTBEAT), + op_code: Some(Opcode::HeartbeatAck), }; self.heartbeat_handler @@ -527,7 +552,7 @@ impl Gateway { .await .unwrap(); } - GATEWAY_RECONNECT => { + Opcode::Reconnect => { trace!("GW: Received Reconnect"); let reconnect = GatewayReconnect {}; @@ -540,7 +565,7 @@ impl Gateway { .publish(reconnect) .await; } - GATEWAY_INVALID_SESSION => { + Opcode::InvalidSession => { trace!("GW: Received Invalid Session"); let mut resumable: bool = false; @@ -566,44 +591,19 @@ impl Gateway { .await; } // Starts our heartbeat - // We should have already handled this in gateway init - GATEWAY_HELLO => { + // We should have already handled this + Opcode::Hello => { warn!("Received hello when it was unexpected"); } - GATEWAY_HEARTBEAT_ACK => { - trace!("GW: Received Heartbeat ACK"); - - // Tell the heartbeat handler we received an ack - - let heartbeat_communication = HeartbeatThreadCommunication { - sequence_number: gateway_payload.sequence_number, - op_code: Some(GATEWAY_HEARTBEAT_ACK), - }; - - self.heartbeat_handler - .send - .send(heartbeat_communication) - .await - .unwrap(); - } - GATEWAY_IDENTIFY - | GATEWAY_UPDATE_PRESENCE - | GATEWAY_UPDATE_VOICE_STATE - | GATEWAY_RESUME - | GATEWAY_REQUEST_GUILD_MEMBERS - | GATEWAY_CALL_SYNC - | GATEWAY_LAZY_REQUEST => { - info!( - "Received unexpected opcode ({}) for current state. This might be due to a faulty server implementation and is likely not the fault of chorus.", + _ => { + warn!( + "Received unexpected opcode ({}) for current state. This might be due to a faulty server implementation, but you can open an issue on the chorus github anyway", gateway_payload.op_code ); } - _ => { - warn!("Received unrecognized gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); - } } - // If we we received a seq number we should let it know + // If we we received a sequence number we should let the heartbeat thread know if let Some(seq_num) = gateway_payload.sequence_number { let heartbeat_communication = HeartbeatThreadCommunication { sequence_number: Some(seq_num), diff --git a/src/gateway/handle.rs b/src/gateway/handle.rs index 98a77314..f165d40b 100644 --- a/src/gateway/handle.rs +++ b/src/gateway/handle.rs @@ -8,7 +8,7 @@ use log::*; use std::fmt::Debug; use super::{events::Events, *}; -use crate::types::{self, Composite, Shared}; +use crate::types::{self, Composite, Opcode, Shared}; /// Represents a handle to a Gateway connection. /// @@ -105,70 +105,90 @@ impl GatewayHandle { object } - /// Sends an identify event to the gateway + /// Sends an identify event ([types::GatewayIdentifyPayload]) to the gateway pub async fn send_identify(&self, to_send: types::GatewayIdentifyPayload) { let to_send_value = serde_json::to_value(&to_send).unwrap(); trace!("GW: Sending Identify.."); - self.send_json_event(GATEWAY_IDENTIFY, to_send_value).await; + self.send_json_event(Opcode::Identify as u8, to_send_value).await; } - /// Sends a resume event to the gateway + /// Sends a resume event ([types::GatewayResume]) to the gateway pub async fn send_resume(&self, to_send: types::GatewayResume) { let to_send_value = serde_json::to_value(&to_send).unwrap(); trace!("GW: Sending Resume.."); - self.send_json_event(GATEWAY_RESUME, to_send_value).await; + self.send_json_event(Opcode::Resume as u8, to_send_value).await; } - /// Sends an update presence event to the gateway + /// Sends an update presence event ([types::UpdatePresence]) to the gateway pub async fn send_update_presence(&self, to_send: types::UpdatePresence) { let to_send_value = serde_json::to_value(&to_send).unwrap(); trace!("GW: Sending Update Presence.."); - self.send_json_event(GATEWAY_UPDATE_PRESENCE, to_send_value) + self.send_json_event(Opcode::PresenceUpdate as u8, to_send_value) .await; } - /// Sends a request guild members to the server + /// Sends a request guild members ([types::GatewayRequestGuildMembers]) to the server pub async fn send_request_guild_members(&self, to_send: types::GatewayRequestGuildMembers) { let to_send_value = serde_json::to_value(&to_send).unwrap(); trace!("GW: Sending Request Guild Members.."); - self.send_json_event(GATEWAY_REQUEST_GUILD_MEMBERS, to_send_value) + self.send_json_event(Opcode::RequestGuildMembers as u8, to_send_value) .await; } - /// Sends an update voice state to the server + /// Sends an update voice state ([types::UpdateVoiceState]) to the server pub async fn send_update_voice_state(&self, to_send: types::UpdateVoiceState) { let to_send_value = serde_json::to_value(to_send).unwrap(); trace!("GW: Sending Update Voice State.."); - self.send_json_event(GATEWAY_UPDATE_VOICE_STATE, to_send_value) + self.send_json_event(Opcode::VoiceStateUpdate as u8, to_send_value) .await; } - /// Sends a call sync to the server + /// Sends a call sync ([types::CallSync]) to the server pub async fn send_call_sync(&self, to_send: types::CallSync) { let to_send_value = serde_json::to_value(to_send).unwrap(); trace!("GW: Sending Call Sync.."); - self.send_json_event(GATEWAY_CALL_SYNC, to_send_value).await; + self.send_json_event(Opcode::CallConnect as u8, to_send_value).await; } - /// Sends a Lazy Request + /// Sends a request call connect event (aka [types::CallSync]) to the server + /// + /// # Notes + /// Alias of [Self::send_call_sync] + pub async fn send_request_call_connect(&self, to_send: types::CallSync) { + self.send_call_sync(to_send).await + } + + /// Sends a Lazy Request ([types::LazyRequest]) to the server pub async fn send_lazy_request(&self, to_send: types::LazyRequest) { let to_send_value = serde_json::to_value(&to_send).unwrap(); trace!("GW: Sending Lazy Request.."); - self.send_json_event(GATEWAY_LAZY_REQUEST, to_send_value) + self.send_json_event(Opcode::GuildSubscriptions as u8, to_send_value) + .await; + } + + /// Sends a Request Last Messages ([types::RequestLastMessages]) to the server + /// + /// The server should respond with a [types::LastMessages] event + pub async fn send_request_last_messages(&self, to_send: types::RequestLastMessages) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("GW: Sending Request Last Messages.."); + + self.send_json_event(Opcode::RequestLastMessages as u8, to_send_value) .await; } diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 5dcc98db..2a0a6f97 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -23,13 +23,13 @@ use tokio::sync::mpsc::{Receiver, Sender}; use tokio::task; use super::*; -use crate::types; +use crate::types::{self, Opcode}; /// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms pub const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; /// Handles sending heartbeats to the gateway in another thread -#[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used +#[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is "used" #[derive(Debug)] pub(super) struct HeartbeatHandler { /// How ofter heartbeats need to be sent at a minimum @@ -98,11 +98,11 @@ impl HeartbeatHandler { if let Some(op_code) = communication.op_code { match op_code { - GATEWAY_HEARTBEAT => { + Opcode::Heartbeat => { // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately should_send = true; } - GATEWAY_HEARTBEAT_ACK => { + Opcode::HeartbeatAck => { // The server received our heartbeat last_heartbeat_acknowledged = true; } @@ -120,7 +120,7 @@ impl HeartbeatHandler { trace!("GW: Sending Heartbeat.."); let heartbeat = types::GatewayHeartbeat { - op: GATEWAY_HEARTBEAT, + op: (Opcode::Heartbeat as u8), d: last_seq_number, }; @@ -147,7 +147,7 @@ impl HeartbeatHandler { #[derive(Clone, Copy, Debug)] pub(super) struct HeartbeatThreadCommunication { /// The opcode for the communication we received, if relevant - pub(super) op_code: Option, + pub(super) op_code: Option, /// The sequence number we got from discord, if any pub(super) sequence_number: Option, } diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ce86bb43..98d34a9b 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -20,7 +20,7 @@ pub use message::*; pub use options::*; use crate::errors::GatewayError; -use crate::types::{Opcode, Snowflake}; +use crate::types::Snowflake; use std::any::Any; use std::collections::HashMap; @@ -28,54 +28,6 @@ use std::sync::{Arc, RwLock}; use tokio::sync::Mutex; -// Gateway opcodes -/// Opcode received when the server dispatches a [crate::types::WebSocketEvent] -const GATEWAY_DISPATCH: u8 = Opcode::Dispatch as u8; -/// Opcode sent when sending a heartbeat -const GATEWAY_HEARTBEAT: u8 = Opcode::Heartbeat as u8; -/// Opcode sent to initiate a session -/// -/// See [types::GatewayIdentifyPayload] -const GATEWAY_IDENTIFY: u8 = Opcode::Identify as u8; -/// Opcode sent to update our presence -/// -/// See [types::GatewayUpdatePresence] -const GATEWAY_UPDATE_PRESENCE: u8 = Opcode::PresenceUpdate as u8; -/// Opcode sent to update our state in vc -/// -/// Like muting, deafening, leaving, joining.. -/// -/// See [types::UpdateVoiceState] -const GATEWAY_UPDATE_VOICE_STATE: u8 = Opcode::VoiceStateUpdate as u8; -/// Opcode sent to resume a session -/// -/// See [types::GatewayResume] -const GATEWAY_RESUME: u8 = Opcode::Resume as u8; -/// Opcode received to tell the client to reconnect -const GATEWAY_RECONNECT: u8 = Opcode::Reconnect as u8; -/// Opcode sent to request guild member data -/// -/// See [types::GatewayRequestGuildMembers] -const GATEWAY_REQUEST_GUILD_MEMBERS: u8 = Opcode::RequestGuildMembers as u8; -/// Opcode received to tell the client their token / session is invalid -const GATEWAY_INVALID_SESSION: u8 = Opcode::InvalidSession as u8; -/// Opcode received when initially connecting to the gateway, starts our heartbeat -/// -/// See [types::HelloData] -const GATEWAY_HELLO: u8 = Opcode::Hello as u8; -/// Opcode received to acknowledge a heartbeat -const GATEWAY_HEARTBEAT_ACK: u8 = Opcode::HeartbeatAck as u8; -/// Opcode sent to get the voice state of users in a given DM/group channel -/// -/// See [types::CallSync] -const GATEWAY_CALL_SYNC: u8 = Opcode::CallConnect as u8; -/// Opcode sent to get data for a server (Lazy Loading request) -/// -/// Sent by the official client when switching to a server -/// -/// See [types::LazyRequest] -const GATEWAY_LAZY_REQUEST: u8 = Opcode::GuildSync as u8; - pub type ObservableObject = dyn Send + Sync + Any; /// Note: this is a reexport of [pubserve::Subscriber], diff --git a/src/types/entities/relationship.rs b/src/types/entities/relationship.rs index da2a9dc7..cc143405 100644 --- a/src/types/entities/relationship.rs +++ b/src/types/entities/relationship.rs @@ -9,7 +9,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::errors::ChorusError; use crate::types::{Shared, Snowflake}; -use super::{arc_rwlock_ptr_eq, PublicUser}; +use super::{option_arc_rwlock_ptr_eq, PublicUser}; #[derive(Debug, Deserialize, Serialize, Clone, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] @@ -25,7 +25,11 @@ pub struct Relationship { pub nickname: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id /// The target user - pub user: Shared, + /// + /// Note: on Discord.com, this is not sent in select places, such as READY payload. + /// + /// In such a case, you should refer to the id field and seperately fetch the user's data + pub user: Option>, /// When the user requested a relationship pub since: Option>, } @@ -36,7 +40,7 @@ impl PartialEq for Relationship { self.id == other.id && self.relationship_type == other.relationship_type && self.nickname == other.nickname - && arc_rwlock_ptr_eq(&self.user, &other.user) + && option_arc_rwlock_ptr_eq(&self.user, &other.user) && self.since == other.since } } diff --git a/src/types/events/call.rs b/src/types/events/call.rs index 7efdce10..b3f69a21 100644 --- a/src/types/events/call.rs +++ b/src/types/events/call.rs @@ -11,32 +11,42 @@ use chorus_macros::WebSocketEvent; /// Officially Undocumented; /// Is sent to a client by the server to signify a new call being created; /// -/// Ex: {"t":"CALL_CREATE","s":2,"op":0,"d":{"voice_states":[],"ringing":[],"region":"milan","message_id":"1107187514906775613","embedded_activities":[],"channel_id":"837609115475771392"}} +/// # Reference +/// See pub struct CallCreate { - pub voice_states: Vec, - /// Seems like a vec of channel ids - pub ringing: Vec, - pub region: String, - // milan - pub message_id: Snowflake, - /// What is this? - pub embedded_activities: Vec, + /// Id of the private channel this call is in pub channel_id: Snowflake, + /// Id of the messsage which created the call + pub message_id: Snowflake, + + /// The IDs of users that are being rung to join the call + pub ringing: Vec, + + // milan + pub region: String, + + /// The voice states of the users already in the call + pub voice_states: Vec, + // What is this? + //pub embedded_activities: Vec, } #[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq, WebSocketEvent)] -/// Officially Undocumented; -/// Updates the client on which calls are ringing, along with a specific call?; +/// Updates the client when metadata about a call changes. /// -/// Ex: {"t":"CALL_UPDATE","s":5,"op":0,"d":{"ringing":["837606544539254834"],"region":"milan","message_id":"1107191540234846308","guild_id":null,"channel_id":"837609115475771392"}} +/// # Reference +/// See pub struct CallUpdate { - /// Seems like a vec of channel ids + /// Id of the private channel this call is in + pub channel_id: Snowflake, + /// Id of the messsage which created the call + pub message_id: Snowflake, + + /// The IDs of users that are being rung to join the call pub ringing: Vec, - pub region: String, + // milan - pub message_id: Snowflake, - pub guild_id: Option, - pub channel_id: Snowflake, + pub region: String, } #[derive( @@ -52,11 +62,14 @@ pub struct CallUpdate { PartialOrd, Ord, )] -/// Officially Undocumented; -/// Deletes a ringing call; -/// Ex: {"t":"CALL_DELETE","s":8,"op":0,"d":{"channel_id":"837609115475771392"}} +/// Sent when a call is deleted, or becomes unavailable due to an outage. +/// +/// # Reference +/// See pub struct CallDelete { pub channel_id: Snowflake, + /// Whether the call is unavailable due to an outage + pub unavailable: Option, } #[derive( @@ -72,10 +85,13 @@ pub struct CallDelete { PartialOrd, Ord, )] -/// Officially Undocumented; -/// See ; +/// Used to request a private channel's pre-existing call data, +/// created before the connection was established. +/// +/// Fires a [CallCreate] event if a call is found. /// -/// Ex: {"op":13,"d":{"channel_id":"837609115475771392"}} +/// # Reference +/// See ; pub struct CallSync { pub channel_id: Snowflake, } diff --git a/src/types/events/guild.rs b/src/types/events/guild.rs index f599d1fa..595cdf2a 100644 --- a/src/types/events/guild.rs +++ b/src/types/events/guild.rs @@ -244,7 +244,7 @@ pub struct GuildMembersChunk { pub chunk_index: u16, pub chunk_count: u16, pub not_found: Option>, - pub presences: Option, + pub presences: Option>, pub nonce: Option, } diff --git a/src/types/events/lazy_request.rs b/src/types/events/lazy_request.rs index 6e17b8ed..ccf2d191 100644 --- a/src/types/events/lazy_request.rs +++ b/src/types/events/lazy_request.rs @@ -20,6 +20,7 @@ use super::WebSocketEvent; /// /// {"op":14,"d":{"guild_id":"848582562217590824","typing":true,"activities":true,"threads":true}} pub struct LazyRequest { + /// The guild id to request pub guild_id: Snowflake, pub typing: bool, pub activities: bool, @@ -27,6 +28,6 @@ pub struct LazyRequest { #[serde(skip_serializing_if = "Option::is_none")] pub members: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub channels: Option>>>, + pub channels: Option>>>, } diff --git a/src/types/events/message.rs b/src/types/events/message.rs index d2ca3082..cffda72a 100644 --- a/src/types/events/message.rs +++ b/src/types/events/message.rs @@ -178,3 +178,28 @@ pub struct MessageACK { pub flags: Option, pub channel_id: Snowflake, } + +#[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] +/// Used to request the last messages from channels. +/// +/// Fires a [LastMessages] events with up to 100 messages that match the request. +/// +/// # Reference +/// See +pub struct RequestLastMessages { + /// The ID of the guild the channels are in + pub guild_id: Snowflake, + /// The IDs of the channels to request last messages for (max 100) + pub channel_ids: Vec +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] +/// Sent as a response to [RequestLastMessages]. +/// +/// # Reference +/// See +pub struct LastMessages { + /// The ID of the guild the channels are in + pub guild_id: Snowflake, + pub messages: Vec +} diff --git a/src/types/events/presence.rs b/src/types/events/presence.rs index 204b8b39..5783cdb4 100644 --- a/src/types/events/presence.rs +++ b/src/types/events/presence.rs @@ -5,13 +5,14 @@ use crate::types::{events::WebSocketEvent, UserStatus}; use crate::types::{Activity, ClientStatusObject, PublicUser, Snowflake}; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DefaultOnNull}; #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] /// Sent by the client to update its status and presence; /// See pub struct UpdatePresence { - /// Unix time of when the client went idle, or n - /// one if client is not idle. + /// Unix time of when the client went idle, or none + /// if client is not idle. pub since: Option, /// the client's status (online, invisible, offline, dnd, idle..) pub status: UserStatus, @@ -19,6 +20,7 @@ pub struct UpdatePresence { pub afk: bool, } +#[serde_as] #[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, WebSocketEvent)] /// Received to tell the client that a user updated their presence / status. If you are looking for /// the PresenceUpdate used in the IDENTIFY gateway event, see @@ -30,7 +32,8 @@ pub struct PresenceUpdate { #[serde(default)] pub guild_id: Option, pub status: UserStatus, - #[serde(default)] + // This will just result in an empty array, I guess we could also use option + #[serde_as(deserialize_as = "DefaultOnNull")] pub activities: Vec, pub client_status: ClientStatusObject, } diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index ecc42818..db28725b 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -10,7 +10,8 @@ use serde::{Deserialize, Serialize}; use crate::types::entities::{Guild, User}; use crate::types::events::{Session, WebSocketEvent}; use crate::types::{ - Activity, Channel, ClientStatusObject, GuildMember, MfaAuthenticatorType, PresenceUpdate, Relationship, Snowflake, UserSettings, VoiceState + Activity, Channel, ClientStatusObject, GuildMember, MfaAuthenticatorType, PresenceUpdate, + Relationship, Snowflake, UserSettings, VoiceState, }; use crate::{UInt32, UInt64, UInt8}; @@ -89,12 +90,21 @@ pub struct GatewayReady { pub api_code_version: UInt8, #[serde(default)] /// User experiment rollouts for the user + /// /// TODO: Make User Experiments into own struct - pub experiments: Vec, + // Note: this is a pain to parse! We need a way to parse arrays into structs via the index of + // their feilds + // + // ex: [4130837190, 0, 10, -1, 0, 1932, 0, 0] + // needs to be parsed into a struct with fields corresponding to the first, second.. value in + // the array + pub experiments: Vec, #[serde(default)] /// Guild experiment rollouts for the user + /// /// TODO: Make Guild Experiments into own struct - pub guild_experiments: Vec, + // Note: this is a pain to parse! See the above TODO + pub guild_experiments: Vec, pub read_state: ReadState, } @@ -239,10 +249,13 @@ pub struct ReadState { )] /// Not documented even unofficially. Information about this type is likely to be partially incorrect. pub struct ReadStateEntry { - pub flags: u32, + /// Spacebar servers do not have flags in this entity at all (??) + pub flags: Option, pub id: Snowflake, pub last_message_id: Option, pub last_pin_timestamp: Option>, - pub last_viewed: Option>, - pub mention_count: u64, + /// A value that is incremented each time the read state is read + pub last_viewed: Option, + // Temporary adding Option to fix Spacebar servers, they have mention count as a nullable + pub mention_count: Option, } diff --git a/src/types/events/request_members.rs b/src/types/events/request_members.rs index 5228170d..1c0beb02 100644 --- a/src/types/events/request_members.rs +++ b/src/types/events/request_members.rs @@ -2,17 +2,43 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use crate::types::{events::WebSocketEvent, Snowflake}; +use crate::types::{events::WebSocketEvent, OneOrMoreSnowflakes}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, WebSocketEvent, Clone)] -/// See +#[derive(Debug, Deserialize, Default, Serialize, WebSocketEvent, Clone)] +/// Used to request members for a guild or a list of guilds. +/// +/// Fires multiple [crate::types::events::GuildMembersChunk] events (each with up to 1000 members) +/// until all members that match the request have been sent. +/// +/// # Notes +/// One of `query` or `user_ids` is required. +/// +/// If `query` is set, `limit` is required (if requesting all members, set `limit` to 0) +/// +/// # Reference +/// See pub struct GatewayRequestGuildMembers { - pub guild_id: Snowflake, + /// Id(s) of the guild(s) to get members for + pub guild_id: OneOrMoreSnowflakes, + + /// The user id(s) to request (0 - 100) + pub user_ids: Option, + + /// String that the username / nickname starts with, or an empty string for all members pub query: Option, - pub limit: u64, + + /// Maximum number of members to send matching the query (0 - 100) + /// + /// Must be 0 with an empty query + pub limit: u8, + + /// Whether to return the [Presence](crate::types::events::PresenceUpdate) of the matched + /// members pub presences: Option, - // TODO: allow array - pub user_ids: Option, + + /// Unique string to identify the received event for this specific request. + /// + /// Up to 32 bytes. If you send a longer nonce, it will be ignored pub nonce: Option, } diff --git a/src/types/utils/mod.rs b/src/types/utils/mod.rs index 4e239f42..ff6b20ec 100644 --- a/src/types/utils/mod.rs +++ b/src/types/utils/mod.rs @@ -6,7 +6,7 @@ pub use opcode::*; pub use regexes::*; pub use rights::Rights; -pub use snowflake::Snowflake; +pub use snowflake::{Snowflake, OneOrMoreSnowflakes}; pub mod jwt; pub mod opcode; diff --git a/src/types/utils/snowflake.rs b/src/types/utils/snowflake.rs index e19f7667..ab6f338c 100644 --- a/src/types/utils/snowflake.rs +++ b/src/types/utils/snowflake.rs @@ -7,6 +7,8 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; +use serde::{Serialize, Deserialize}; + use chrono::{DateTime, TimeZone, Utc}; /// 2015-01-01 @@ -138,10 +140,75 @@ impl<'d> sqlx::Decode<'d, sqlx::Postgres> for Snowflake { } } +/// A type representing either a single [Snowflake] or a [Vec] of [Snowflake]s. +/// +/// Useful for e.g. [RequestGuildMembers](crate::types::events::GatewayRequestGuildMembers), to +/// select either one specific user or multiple users. +/// +/// Should (de)serialize either as a single [Snowflake] or as an array. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[serde(untagged)] +pub enum OneOrMoreSnowflakes { + One(Snowflake), + More(Vec) +} + +// Note: allows us to have Default on the events +// that use this type +impl Default for OneOrMoreSnowflakes { + fn default() -> Self { + Snowflake::default().into() + } +} + +impl Display for OneOrMoreSnowflakes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OneOrMoreSnowflakes::One(snowflake) => write!(f, "{}", snowflake.0), + // Display as you would debug a vec of u64s + OneOrMoreSnowflakes::More(snowflake_vec) => write!(f, "{:?}", snowflake_vec.iter().map(|x| x.0)), + } + } +} + +impl From for OneOrMoreSnowflakes { + fn from(item: Snowflake) -> Self { + Self::One(item) + } +} + +impl From> for OneOrMoreSnowflakes { + fn from(item: Vec) -> Self { + if item.len() == 1 { + return Self::One(item[0]); + } + + Self::More(item) + } +} + +impl From for OneOrMoreSnowflakes { + fn from(item: u64) -> Self { + Self::One(item.into()) + } +} + +impl From> for OneOrMoreSnowflakes { + fn from(item: Vec) -> Self { + if item.len() == 1 { + return Self::One(item[0].into()); + } + + Self::More(item.into_iter().map(|x| x.into()).collect()) + } +} + #[cfg(test)] mod test { use chrono::{DateTime, Utc}; + use crate::types::utils::snowflake::OneOrMoreSnowflakes; + use super::Snowflake; #[test] @@ -157,4 +224,29 @@ mod test { let timestamp = "2016-04-30 11:18:25.796Z".parse::>().unwrap(); assert_eq!(snow.timestamp(), timestamp); } + + #[test] + fn serialize() { + let snowflake = Snowflake(1303390110099968072_u64); + let serialized = serde_json::to_string(&snowflake).unwrap(); + + assert_eq!(serialized, "\"1303390110099968072\"".to_string()); + } + + #[test] + fn serialize_one_or_more() { + let snowflake = Snowflake(1303390110099968072_u64); + let one_snowflake: OneOrMoreSnowflakes = snowflake.into(); + + let serialized = serde_json::to_string(&one_snowflake).unwrap(); + + assert_eq!(serialized, "\"1303390110099968072\"".to_string()); + + let more_snowflakes: OneOrMoreSnowflakes = vec![snowflake, snowflake, snowflake].into(); + + let serialized = serde_json::to_string(&more_snowflakes).unwrap(); + + assert_eq!(serialized, "[\"1303390110099968072\",\"1303390110099968072\",\"1303390110099968072\"]".to_string()); + + } }