Skip to main content

steamkit/base/
client_msg.rs

1//! Client messages: protobuf-backed ([`ClientMsgProtobuf`]) and struct-backed ([`ClientMsg`], [`Msg`]).
2
3use crate::base::generated::steam_language::EMsg;
4use crate::base::generated::steam_language_internal::{
5    ExtendedClientMsgHdr, ISteamSerializableHeader, ISteamSerializableMessage, MsgHdr,
6    MsgHdrProtoBuf, SteamSerializable, SteamSerializableDeserialize,
7};
8use crate::base::msg_base::{MsgBase, Reader};
9use crate::base::packet_base::IPacketMsg;
10use crate::error::SteamError;
11use crate::types::job_id::JobID;
12use crate::types::steam_id::SteamID;
13use steamkit_protos::steam::CMsgProtoBufHeader;
14
15/// A unified interface into client messages.
16pub trait IClientMsg: Send + Sync {
17    /// Whether this client message is protobuf backed.
18    fn is_proto(&self) -> bool;
19
20    /// The network message type of this client message.
21    fn msg_type(&self) -> EMsg;
22
23    /// The session id of this client message.
24    fn session_id(&self) -> i32;
25
26    /// Sets the session id of this client message.
27    fn set_session_id(&mut self, id: i32);
28
29    /// The SteamID of this client message.
30    fn steam_id(&self) -> Option<SteamID>;
31
32    /// Sets the SteamID of this client message.
33    fn set_steam_id(&mut self, id: SteamID);
34
35    /// The target job id of this client message.
36    fn target_job_id(&self) -> JobID;
37
38    /// Sets the target job id of this client message.
39    fn set_target_job_id(&mut self, id: JobID);
40
41    /// The source job id of this client message.
42    fn source_job_id(&self) -> JobID;
43
44    /// Sets the source job id of this client message.
45    fn set_source_job_id(&mut self, id: JobID);
46
47    /// Serialises this client message to a byte array.
48    fn serialize(&self) -> Vec<u8>;
49}
50
51/// Access to the raw 64-bit value of a [`SteamID`], account instance bits
52/// included. This is what goes into the protobuf header.
53pub trait SteamIDRaw {
54    /// The raw 64-bit value of this SteamID.
55    fn raw_u64(&self) -> u64;
56}
57
58impl SteamIDRaw for SteamID {
59    fn raw_u64(&self) -> u64 {
60        self.value()
61    }
62}
63
64/// Empty body for header-only protobuf messages.
65///
66/// Use `ClientMsgProtobuf` with this body to read a message when you only care
67/// about the header. Serializing it writes just the header.
68#[derive(Clone, Copy, PartialEq, Eq, ::prost::Message)]
69pub struct SteamProtobufHeaderOnly {}
70
71/// A protobuf-backed client message.
72pub struct ClientMsgProtobuf<TBody: prost::Message + Default = SteamProtobufHeaderOnly> {
73    /// The header of this message.
74    pub header: MsgHdrProtoBuf,
75    /// The body of this message.
76    pub body: TBody,
77    /// The raw payload that follows the body.
78    pub payload: MsgBase,
79}
80
81impl<TBody> ClientMsgProtobuf<TBody>
82where
83    TBody: prost::Message + Default,
84{
85    /// Creates a new message of the given type.
86    pub fn new(e_msg: EMsg) -> Self {
87        Self::with_payload_reserve(e_msg, 0)
88    }
89
90    /// Like [`new`](Self::new), but reserves `payload_reserve` bytes for the payload.
91    pub fn with_payload_reserve(e_msg: EMsg, payload_reserve: usize) -> Self {
92        let mut header = MsgHdrProtoBuf::default();
93        header.msg = e_msg;
94        ClientMsgProtobuf {
95            header,
96            body: TBody::default(),
97            payload: MsgBase::new(payload_reserve),
98        }
99    }
100
101    /// Creates a reply to `reply_to`, targeting its source job.
102    pub fn reply(e_msg: EMsg, reply_to: &dyn IClientMsg) -> Self {
103        let mut msg = Self::new(e_msg);
104        msg.set_target_job_id(reply_to.source_job_id());
105        msg
106    }
107
108    /// Parses a received packet. The body is decoded from the bytes after the header.
109    pub fn from_packet(msg: &dyn IPacketMsg) -> Result<Self, SteamError> {
110        if !msg.is_proto() {
111            return Err(SteamError::InvalidArgument(
112                "ClientMsgProtobuf used for non-proto message!".to_string(),
113            ));
114        }
115
116        let data = msg.get_data();
117        let mut reader = Reader::new(data);
118        let header = MsgHdrProtoBuf::read(&mut reader)?;
119        let body_offset = reader.position();
120
121        let mut body_bytes = data.get(body_offset..).ok_or_else(|| {
122            SteamError::Malformed("protobuf body offset is outside of the packet".to_string())
123        })?;
124        let body = TBody::decode(&mut body_bytes)?;
125
126        let mut payload = MsgBase::new(0);
127        if !body_bytes.is_empty() {
128            payload.write_bytes(body_bytes);
129            payload.seek(0);
130        }
131
132        Ok(ClientMsgProtobuf {
133            header,
134            body,
135            payload,
136        })
137    }
138
139    /// The body structure of this message.
140    pub fn body(&self) -> &TBody {
141        &self.body
142    }
143
144    /// The mutable body structure of this message.
145    pub fn body_mut(&mut self) -> &mut TBody {
146        &mut self.body
147    }
148
149    /// Replaces the body structure of this message.
150    pub fn set_body(&mut self, body: TBody) {
151        self.body = body;
152    }
153
154    /// The header of this message.
155    pub fn header(&self) -> &MsgHdrProtoBuf {
156        &self.header
157    }
158
159    /// The mutable header of this message.
160    pub fn header_mut(&mut self) -> &mut MsgHdrProtoBuf {
161        &mut self.header
162    }
163
164    /// Shorthand for the protobuf header.
165    pub fn proto_header(&self) -> &CMsgProtoBufHeader {
166        &self.header.proto
167    }
168
169    /// Mutable shorthand for the protobuf header.
170    pub fn proto_header_mut(&mut self) -> &mut CMsgProtoBufHeader {
171        &mut self.header.proto
172    }
173
174    /// The payload buffer of this message.
175    pub fn payload(&self) -> &MsgBase {
176        &self.payload
177    }
178
179    /// The mutable payload buffer of this message.
180    pub fn payload_mut(&mut self) -> &mut MsgBase {
181        &mut self.payload
182    }
183}
184
185impl<TBody> IClientMsg for ClientMsgProtobuf<TBody>
186where
187    TBody: prost::Message + Default,
188{
189    fn is_proto(&self) -> bool {
190        true
191    }
192
193    fn msg_type(&self) -> EMsg {
194        self.header.msg
195    }
196
197    fn session_id(&self) -> i32 {
198        self.header.proto.client_sessionid.unwrap_or(0)
199    }
200
201    fn set_session_id(&mut self, id: i32) {
202        self.header.proto.client_sessionid = Some(id);
203    }
204
205    fn steam_id(&self) -> Option<SteamID> {
206        Some(SteamID::from_id(self.header.proto.steamid.unwrap_or(0)))
207    }
208
209    fn set_steam_id(&mut self, id: SteamID) {
210        self.header.proto.steamid = Some(id.raw_u64());
211    }
212
213    fn target_job_id(&self) -> JobID {
214        JobID::from_value(self.header.proto.jobid_target.unwrap_or(u64::MAX))
215    }
216
217    fn set_target_job_id(&mut self, id: JobID) {
218        self.header.proto.jobid_target = Some(id.value());
219    }
220
221    fn source_job_id(&self) -> JobID {
222        JobID::from_value(self.header.proto.jobid_source.unwrap_or(u64::MAX))
223    }
224
225    fn set_source_job_id(&mut self, id: JobID) {
226        self.header.proto.jobid_source = Some(id.value());
227    }
228
229    fn serialize(&self) -> Vec<u8> {
230        let mut out = Vec::new();
231        self.header.write(&mut out);
232        out.extend_from_slice(&self.body.encode_to_vec());
233        out.extend_from_slice(&self.payload.payload);
234        out
235    }
236}
237
238/// A struct-backed client message with an extended header.
239pub struct ClientMsg<TBody>
240where
241    TBody: SteamSerializableDeserialize + ISteamSerializableMessage,
242{
243    /// The header of this message.
244    pub header: ExtendedClientMsgHdr,
245    /// The body of this message.
246    pub body: TBody,
247    /// The raw payload that follows the body.
248    pub payload: MsgBase,
249}
250
251impl<TBody> ClientMsg<TBody>
252where
253    TBody: SteamSerializableDeserialize + ISteamSerializableMessage,
254{
255    /// Creates a new message of the given type. The type is stored in the header too.
256    pub fn new(e_msg: EMsg) -> Self {
257        Self::with_payload_reserve(e_msg, 0)
258    }
259
260    /// Like [`new`](Self::new), but reserves `payload_reserve` bytes for the payload.
261    pub fn with_payload_reserve(e_msg: EMsg, payload_reserve: usize) -> Self {
262        let mut header = ExtendedClientMsgHdr::default();
263        header.set_emsg(e_msg);
264        ClientMsg {
265            header,
266            body: TBody::default(),
267            payload: MsgBase::new(payload_reserve),
268        }
269    }
270
271    /// Creates a reply to `reply_to`, targeting its source job.
272    pub fn reply(e_msg: EMsg, reply_to: &dyn IClientMsg) -> Self {
273        let mut msg = Self::new(e_msg);
274        msg.set_target_job_id(reply_to.source_job_id());
275        msg
276    }
277
278    /// Parses a received packet.
279    pub fn from_packet(msg: &dyn IPacketMsg) -> Result<Self, SteamError> {
280        let data = msg.get_data();
281        let mut reader = Reader::new(data);
282        let header = ExtendedClientMsgHdr::read(&mut reader)?;
283        let body = TBody::read(&mut reader)?;
284
285        let mut payload = MsgBase::new(0);
286        let remaining = reader.read_up_to(reader.remaining());
287        if !remaining.is_empty() {
288            payload.write_bytes(&remaining);
289            payload.seek(0);
290        }
291
292        Ok(ClientMsg {
293            header,
294            body,
295            payload,
296        })
297    }
298
299    /// The body structure of this message.
300    pub fn body(&self) -> &TBody {
301        &self.body
302    }
303
304    /// The mutable body structure of this message.
305    pub fn body_mut(&mut self) -> &mut TBody {
306        &mut self.body
307    }
308
309    /// The header of this message.
310    pub fn header(&self) -> &ExtendedClientMsgHdr {
311        &self.header
312    }
313
314    /// The mutable header of this message.
315    pub fn header_mut(&mut self) -> &mut ExtendedClientMsgHdr {
316        &mut self.header
317    }
318
319    /// The payload buffer of this message.
320    pub fn payload(&self) -> &MsgBase {
321        &self.payload
322    }
323
324    /// The mutable payload buffer of this message.
325    pub fn payload_mut(&mut self) -> &mut MsgBase {
326        &mut self.payload
327    }
328}
329
330impl<TBody> IClientMsg for ClientMsg<TBody>
331where
332    TBody: SteamSerializableDeserialize + ISteamSerializableMessage,
333{
334    fn is_proto(&self) -> bool {
335        false
336    }
337
338    fn msg_type(&self) -> EMsg {
339        self.header.msg
340    }
341
342    fn session_id(&self) -> i32 {
343        self.header.session_id
344    }
345
346    fn set_session_id(&mut self, id: i32) {
347        self.header.session_id = id;
348    }
349
350    fn steam_id(&self) -> Option<SteamID> {
351        Some(SteamID::from_id(self.header.steam_id))
352    }
353
354    fn set_steam_id(&mut self, id: SteamID) {
355        self.header.steam_id = id.raw_u64();
356    }
357
358    fn target_job_id(&self) -> JobID {
359        JobID::from_value(self.header.target_job_id)
360    }
361
362    fn set_target_job_id(&mut self, id: JobID) {
363        self.header.target_job_id = id.value();
364    }
365
366    fn source_job_id(&self) -> JobID {
367        JobID::from_value(self.header.source_job_id)
368    }
369
370    fn set_source_job_id(&mut self, id: JobID) {
371        self.header.source_job_id = id.value();
372    }
373
374    fn serialize(&self) -> Vec<u8> {
375        let mut out = Vec::new();
376        self.header.write(&mut out);
377        self.body.write(&mut out);
378        out.extend_from_slice(&self.payload.payload);
379        out
380    }
381}
382
383/// A struct-backed message without session or client information.
384pub struct Msg<TBody>
385where
386    TBody: SteamSerializableDeserialize + ISteamSerializableMessage,
387{
388    /// The header of this message.
389    pub header: MsgHdr,
390    /// The body of this message.
391    pub body: TBody,
392    /// The raw payload that follows the body.
393    pub payload: MsgBase,
394    /// Session id. `MsgHdr` has no field for it, so it's only kept locally.
395    pub session_id: i32,
396    /// SteamID. `MsgHdr` has no field for it, so it's only kept locally.
397    pub steam_id: Option<SteamID>,
398}
399
400impl<TBody> Msg<TBody>
401where
402    TBody: SteamSerializableDeserialize + ISteamSerializableMessage,
403{
404    /// Creates a new message of the given type.
405    pub fn new(e_msg: EMsg) -> Self {
406        Self::with_payload_reserve(e_msg, 0)
407    }
408
409    /// Like [`new`](Self::new), but reserves `payload_reserve` bytes for the payload.
410    pub fn with_payload_reserve(e_msg: EMsg, payload_reserve: usize) -> Self {
411        let mut header = MsgHdr::default();
412        header.set_emsg(e_msg);
413        Msg {
414            header,
415            body: TBody::default(),
416            payload: MsgBase::new(payload_reserve),
417            session_id: 0,
418            steam_id: None,
419        }
420    }
421
422    /// Creates a reply to `reply_to`, targeting its source job.
423    pub fn reply(e_msg: EMsg, reply_to: &dyn IClientMsg) -> Self {
424        let mut msg = Self::new(e_msg);
425        msg.set_target_job_id(reply_to.source_job_id());
426        msg
427    }
428
429    /// Parses a received packet.
430    pub fn from_packet(msg: &dyn IPacketMsg) -> Result<Self, SteamError> {
431        let data = msg.get_data();
432        let mut reader = Reader::new(data);
433        let header = MsgHdr::read(&mut reader)?;
434        let body = TBody::read(&mut reader)?;
435
436        let mut payload = MsgBase::new(0);
437        let remaining = reader.read_up_to(reader.remaining());
438        if !remaining.is_empty() {
439            payload.write_bytes(&remaining);
440            payload.seek(0);
441        }
442
443        Ok(Msg {
444            header,
445            body,
446            payload,
447            session_id: 0,
448            steam_id: None,
449        })
450    }
451
452    /// The body structure of this message.
453    pub fn body(&self) -> &TBody {
454        &self.body
455    }
456
457    /// The mutable body structure of this message.
458    pub fn body_mut(&mut self) -> &mut TBody {
459        &mut self.body
460    }
461
462    /// The header of this message.
463    pub fn header(&self) -> &MsgHdr {
464        &self.header
465    }
466
467    /// The mutable header of this message.
468    pub fn header_mut(&mut self) -> &mut MsgHdr {
469        &mut self.header
470    }
471
472    /// The payload buffer of this message.
473    pub fn payload(&self) -> &MsgBase {
474        &self.payload
475    }
476
477    /// The mutable payload buffer of this message.
478    pub fn payload_mut(&mut self) -> &mut MsgBase {
479        &mut self.payload
480    }
481}
482
483impl<TBody> IClientMsg for Msg<TBody>
484where
485    TBody: SteamSerializableDeserialize + ISteamSerializableMessage,
486{
487    fn is_proto(&self) -> bool {
488        false
489    }
490
491    fn msg_type(&self) -> EMsg {
492        self.header.msg
493    }
494
495    fn session_id(&self) -> i32 {
496        self.session_id
497    }
498
499    fn set_session_id(&mut self, id: i32) {
500        self.session_id = id;
501    }
502
503    fn steam_id(&self) -> Option<SteamID> {
504        self.steam_id
505    }
506
507    fn set_steam_id(&mut self, id: SteamID) {
508        self.steam_id = Some(id);
509    }
510
511    fn target_job_id(&self) -> JobID {
512        JobID::from_value(self.header.target_job_id)
513    }
514
515    fn set_target_job_id(&mut self, id: JobID) {
516        self.header.target_job_id = id.value();
517    }
518
519    fn source_job_id(&self) -> JobID {
520        JobID::from_value(self.header.source_job_id)
521    }
522
523    fn set_source_job_id(&mut self, id: JobID) {
524        self.header.source_job_id = id.value();
525    }
526
527    fn serialize(&self) -> Vec<u8> {
528        let mut out = Vec::new();
529        self.header.write(&mut out);
530        self.body.write(&mut out);
531        out.extend_from_slice(&self.payload.payload);
532        out
533    }
534}