titanium_gateway/
payload.rs

1//! Gateway payload structures.
2//!
3//! These structures represent the JSON payloads sent and received over the Gateway WebSocket.
4//! Where possible, zero-copy parsing is used via `serde_json::value::RawValue`.
5
6use crate::opcode::OpCode;
7use serde::{Deserialize, Serialize};
8use titanium_model::{Application, Intents, UnavailableGuild, User};
9
10/// A raw Gateway payload for initial parsing.
11///
12/// Uses `RawValue` (standard) or `Value` (SIMD) for the `d` field to defer parsing.
13#[derive(Debug, Deserialize)]
14pub struct RawGatewayPayload<'a> {
15    /// Opcode for the payload.
16    pub op: OpCode,
17
18    /// Event data.
19    #[cfg(not(feature = "simd"))]
20    #[serde(borrow)]
21    pub d: Option<&'a serde_json::value::RawValue>,
22
23    /// Event data (fully parsed to Value when using SIMD).
24    #[cfg(feature = "simd")]
25    pub d: Option<titanium_model::json::Value>,
26
27    /// Sequence number (for Dispatch events).
28    #[allow(dead_code)]
29    pub s: Option<u64>,
30
31    /// Event name (for Dispatch events).
32    #[allow(dead_code)]
33    pub t: Option<String>,
34
35    #[cfg(feature = "simd")]
36    #[serde(skip)]
37    pub _marker: std::marker::PhantomData<&'a ()>,
38}
39
40/// A fully parsed Gateway payload.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct GatewayPayload<D> {
43    /// Opcode for the payload.
44    pub op: OpCode,
45
46    /// Event data.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub d: Option<D>,
49
50    /// Sequence number (for Dispatch events).
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub s: Option<u64>,
53
54    /// Event name (for Dispatch events).
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub t: Option<String>,
57}
58
59impl<D: Serialize> GatewayPayload<D> {
60    /// Create a new payload with only opcode and data.
61    pub fn new(op: OpCode, data: D) -> Self {
62        Self {
63            op,
64            d: Some(data),
65            s: None,
66            t: None,
67        }
68    }
69
70    /// Create a payload with no data (e.g., HeartbeatAck).
71    pub fn opcode_only(op: OpCode) -> GatewayPayload<()> {
72        GatewayPayload {
73            op,
74            d: None,
75            s: None,
76            t: None,
77        }
78    }
79}
80
81// ============================================================================
82// Hello Payload (Received after connection)
83// ============================================================================
84
85/// Payload for the Hello opcode (op 10).
86///
87/// Received immediately after connecting to the Gateway.
88#[derive(Debug, Clone, Deserialize)]
89pub struct HelloPayload {
90    /// Interval (in milliseconds) at which to send heartbeats.
91    pub heartbeat_interval: u64,
92}
93
94// ============================================================================
95// Identify Payload (Sent to authenticate)
96// ============================================================================
97
98/// Payload for the Identify opcode (op 2).
99///
100/// Sent to authenticate and start a new session.
101#[derive(Debug, Clone, Serialize)]
102pub struct IdentifyPayload<'a> {
103    /// Authentication token.
104    pub token: std::borrow::Cow<'a, str>,
105
106    /// Gateway intents.
107    pub intents: Intents,
108
109    /// Connection properties.
110    pub properties: ConnectionProperties<'a>,
111
112    /// Whether to enable payload compression.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub compress: Option<bool>,
115
116    /// Threshold for large guilds (50-250, default 50).
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub large_threshold: Option<u8>,
119
120    /// Shard information: [shard_id, total_shards].
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub shard: Option<[u16; 2]>,
123
124    /// Initial presence.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub presence: Option<PresenceUpdate>,
127}
128
129impl<'a> IdentifyPayload<'a> {
130    /// Create a new Identify payload with required fields.
131    pub fn new(token: impl Into<std::borrow::Cow<'a, str>>, intents: Intents) -> Self {
132        Self {
133            token: token.into(),
134            intents,
135            properties: ConnectionProperties::default(),
136            compress: None,
137            large_threshold: Some(250),
138            shard: None,
139            presence: None,
140        }
141    }
142
143    /// Set shard information.
144    pub fn with_shard(mut self, shard_id: u16, total_shards: u16) -> Self {
145        self.shard = Some([shard_id, total_shards]);
146        self
147    }
148}
149
150/// Connection properties sent with Identify.
151#[derive(Debug, Clone, Serialize)]
152pub struct ConnectionProperties<'a> {
153    /// Operating system.
154    pub os: std::borrow::Cow<'a, str>,
155
156    /// Library name.
157    pub browser: std::borrow::Cow<'a, str>,
158
159    /// Library name (again, for device).
160    pub device: std::borrow::Cow<'a, str>,
161}
162
163impl<'a> Default for ConnectionProperties<'a> {
164    fn default() -> Self {
165        Self {
166            os: std::borrow::Cow::Owned(std::env::consts::OS.to_string()),
167            browser: std::borrow::Cow::Borrowed("titanium-rs"),
168            device: std::borrow::Cow::Borrowed("titanium-rs"),
169        }
170    }
171}
172
173/// Presence update payload.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct PresenceUpdate {
176    /// Unix timestamp (milliseconds) of when the client went idle.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub since: Option<u64>,
179
180    /// User's activities.
181    pub activities: Vec<Activity>,
182
183    /// User's status.
184    pub status: Status,
185
186    /// Whether the client is AFK.
187    pub afk: bool,
188}
189
190/// Activity for presence.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct Activity {
193    /// Activity name.
194    pub name: String,
195
196    /// Activity type.
197    #[serde(rename = "type")]
198    pub activity_type: ActivityType,
199
200    /// Stream URL (only for Streaming type).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub url: Option<String>,
203}
204
205/// Activity type.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(from = "u8", into = "u8")]
208pub enum ActivityType {
209    /// Playing {name}
210    Playing,
211    /// Streaming {name}
212    Streaming,
213    /// Listening to {name}
214    Listening,
215    /// Watching {name}
216    Watching,
217    /// {emoji} {name}
218    Custom,
219    /// Competing in {name}
220    Competing,
221}
222
223impl From<u8> for ActivityType {
224    fn from(value: u8) -> Self {
225        match value {
226            0 => ActivityType::Playing,
227            1 => ActivityType::Streaming,
228            2 => ActivityType::Listening,
229            3 => ActivityType::Watching,
230            4 => ActivityType::Custom,
231            5 => ActivityType::Competing,
232            _ => ActivityType::Playing,
233        }
234    }
235}
236
237impl From<ActivityType> for u8 {
238    fn from(value: ActivityType) -> Self {
239        match value {
240            ActivityType::Playing => 0,
241            ActivityType::Streaming => 1,
242            ActivityType::Listening => 2,
243            ActivityType::Watching => 3,
244            ActivityType::Custom => 4,
245            ActivityType::Competing => 5,
246        }
247    }
248}
249
250/// User status.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
252#[serde(rename_all = "lowercase")]
253pub enum Status {
254    /// Online.
255    #[default]
256    Online,
257    /// Do Not Disturb.
258    Dnd,
259    /// Away / Idle.
260    Idle,
261    /// Invisible (shown as offline).
262    Invisible,
263    /// Offline.
264    Offline,
265}
266
267// ============================================================================
268// Resume Payload (Sent to resume a session)
269// ============================================================================
270
271/// Payload for the Resume opcode (op 6).
272#[derive(Debug, Clone, Serialize)]
273pub struct ResumePayload<'a> {
274    /// Authentication token.
275    pub token: std::borrow::Cow<'a, str>,
276
277    /// Session ID from the previous Ready event.
278    pub session_id: std::borrow::Cow<'a, str>,
279
280    /// Last sequence number received.
281    pub seq: u64,
282}
283
284// ============================================================================
285// Ready Event (Received after successful Identify)
286// ============================================================================
287
288/// Payload for the READY dispatch event.
289#[derive(Debug, Clone, Deserialize)]
290pub struct ReadyEvent<'a> {
291    /// Gateway protocol version.
292    pub v: u8,
293
294    /// Current user.
295    #[serde(borrow)]
296    pub user: User<'a>,
297
298    /// Guilds the user is in (unavailable during initial connection).
299    pub guilds: Vec<UnavailableGuild>,
300
301    /// Session ID for resuming.
302    pub session_id: String,
303
304    /// URL to use for resuming the session.
305    pub resume_gateway_url: String,
306
307    /// Shard information: [shard_id, total_shards].
308    #[serde(default)]
309    pub shard: Option<[u16; 2]>,
310
311    /// Application information.
312    pub application: Application,
313}
314
315// ============================================================================
316// Heartbeat Payload
317// ============================================================================
318
319/// Create a Heartbeat payload.
320///
321/// The heartbeat payload is just the sequence number (or null if no events received).
322pub fn create_heartbeat_payload(sequence: Option<u64>) -> String {
323    match sequence {
324        Some(seq) => format!(r#"{{"op":1,"d":{}}}"#, seq),
325        None => r#"{"op":1,"d":null}"#.to_string(),
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_hello_payload() {
335        let json = r#"{"heartbeat_interval": 41250}"#;
336        let payload: HelloPayload = serde_json::from_str(json).unwrap();
337        assert_eq!(payload.heartbeat_interval, 41250);
338    }
339
340    #[test]
341    fn test_identify_serialization() {
342        let identify =
343            IdentifyPayload::new("test_token", Intents::GUILDS | Intents::GUILD_MESSAGES)
344                .with_shard(0, 1);
345
346        let json = serde_json::to_string(&identify).unwrap();
347        assert!(json.contains("test_token"));
348        assert!(json.contains("shard"));
349    }
350
351    #[test]
352    fn test_heartbeat_payload() {
353        let payload = create_heartbeat_payload(Some(42));
354        assert_eq!(payload, r#"{"op":1,"d":42}"#);
355
356        let payload_null = create_heartbeat_payload(None);
357        assert_eq!(payload_null, r#"{"op":1,"d":null}"#);
358    }
359}