1use std::collections::HashMap;
20
21use prost::Message;
22use steam_enums::{EChatEntryType, EFriendRelationship, EPersonaState, EResult};
23use steamid::SteamID;
24
25use super::{
26 binary_kv::{parse_binary_kv, BinaryKvValue},
27 vdf::{parse_vdf, VdfValue},
28};
29
30#[derive(Debug, Clone, PartialEq)]
32pub enum ParseError {
33 DecodeError(String),
35 MissingField(String),
37 InvalidData(String),
39}
40
41impl std::fmt::Display for ParseError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 ParseError::DecodeError(msg) => write!(f, "Decode error: {}", msg),
45 ParseError::MissingField(field) => write!(f, "Missing field: {}", field),
46 ParseError::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
47 }
48 }
49}
50
51impl std::error::Error for ParseError {}
52
53#[derive(Debug, Clone)]
59pub struct LogonData {
60 pub steam_id: SteamID,
61 pub eresult: EResult,
62 pub heartbeat_seconds: i32,
63 pub public_ip: Option<std::net::Ipv4Addr>,
64 pub vanity_url: Option<String>,
65 pub cell_id: Option<u32>,
66 pub client_instance_id: Option<u64>,
67}
68
69#[derive(Debug, Clone)]
71pub struct FriendsListData {
72 pub incremental: bool,
73 pub friends: Vec<FriendData>,
74}
75
76#[derive(Debug, Clone)]
78pub struct FriendData {
79 pub steam_id: SteamID,
80 pub relationship: EFriendRelationship,
81}
82
83#[derive(Debug, Clone)]
85pub struct PersonaData {
86 pub steam_id: SteamID,
87 pub player_name: String,
88 pub persona_state: EPersonaState,
89 pub avatar_hash: Option<String>,
90 pub game_name: Option<String>,
91 pub game_id: Option<u64>,
92 pub last_logoff: Option<u32>,
93 pub last_logon: Option<u32>,
94 pub last_seen_online: Option<u32>,
95 pub game_server_ip: Option<u32>,
96 pub game_server_port: Option<u32>,
97 pub rich_presence: HashMap<String, String>,
98}
99
100#[derive(Debug, Clone)]
102pub struct LicenseData {
103 pub package_id: u32,
104 pub time_created: u32,
105 pub license_type: u32,
106 pub flags: u32,
107 pub access_token: u64,
108}
109
110#[derive(Debug, Clone)]
112pub enum ChatData {
113 Message { sender: SteamID, message: String, chat_entry_type: EChatEntryType, timestamp: u32, ordinal: u32 },
114 Typing { sender: SteamID },
115}
116
117#[derive(Debug, Clone)]
119pub struct ProductInfoData {
120 pub apps: HashMap<u32, AppInfoData>,
121 pub packages: HashMap<u32, PackageInfoData>,
122 pub unknown_apps: Vec<u32>,
123 pub unknown_packages: Vec<u32>,
124}
125
126#[derive(Debug, Clone)]
128pub struct AppInfoData {
129 pub app_id: u32,
130 pub change_number: u32,
131 pub missing_token: bool,
132 pub app_info: Option<VdfValue>,
133}
134
135#[derive(Debug, Clone)]
137pub struct PackageInfoData {
138 pub package_id: u32,
139 pub change_number: u32,
140 pub missing_token: bool,
141 pub package_info: Option<BinaryKvValue>,
142}
143
144#[derive(Debug, Clone)]
146pub struct AccessTokensData {
147 pub app_tokens: HashMap<u32, u64>,
148 pub package_tokens: HashMap<u32, u64>,
149 pub app_denied: Vec<u32>,
150 pub package_denied: Vec<u32>,
151}
152
153#[derive(Debug, Clone)]
155pub struct ChangesData {
156 pub current_change_number: u32,
157 pub app_changes: Vec<AppChange>,
158 pub package_changes: Vec<PackageChange>,
159}
160
161#[derive(Debug, Clone)]
163pub struct AppChange {
164 pub app_id: u32,
165 pub change_number: u32,
166 pub needs_token: bool,
167}
168
169#[derive(Debug, Clone)]
171pub struct PackageChange {
172 pub package_id: u32,
173 pub change_number: u32,
174 pub needs_token: bool,
175}
176
177pub fn parse_logon_response(body: &[u8]) -> Result<LogonData, ParseError> {
187 let msg = steam_protos::CMsgClientLogonResponse::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
188
189 let eresult = EResult::from_i32(msg.eresult.unwrap_or(2)).unwrap_or(EResult::Fail);
190 let steam_id = SteamID::from_steam_id64(msg.client_supplied_steamid.unwrap_or(0));
191
192 let public_ip = msg.public_ip.and_then(|ip_msg| match ip_msg.ip {
193 Some(steam_protos::cmsg_ip_address::Ip::V4(addr)) => Some(std::net::Ipv4Addr::from(addr.to_be_bytes())),
194 _ => None,
195 });
196
197 Ok(LogonData {
198 steam_id,
199 eresult,
200 heartbeat_seconds: msg.heartbeat_seconds.unwrap_or(0),
201 public_ip,
202 vanity_url: msg.vanity_url,
203 cell_id: msg.cell_id,
204 client_instance_id: msg.client_instance_id,
205 })
206}
207
208pub fn parse_logged_off(body: &[u8]) -> Result<EResult, ParseError> {
214 let msg = steam_protos::CMsgClientLoggedOff::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
215
216 Ok(EResult::from_i32(msg.eresult.unwrap_or(2)).unwrap_or(EResult::Fail))
217}
218
219pub fn parse_friends_list(body: &[u8]) -> Result<FriendsListData, ParseError> {
225 let msg = steam_protos::CMsgClientFriendsList::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
226
227 let friends = msg
228 .friends
229 .iter()
230 .map(|f| FriendData {
231 steam_id: SteamID::from_steam_id64(f.ulfriendid.unwrap_or(0)),
232 relationship: EFriendRelationship::from_i32(f.efriendrelationship.unwrap_or(0) as i32).unwrap_or(EFriendRelationship::None),
233 })
234 .collect();
235
236 Ok(FriendsListData { incremental: msg.bincremental.unwrap_or(false), friends })
237}
238
239pub fn parse_persona_state(body: &[u8]) -> Result<PersonaData, ParseError> {
245 let msg = steam_protos::CMsgClientPersonaState::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
246
247 let friend = msg.friends.first().ok_or_else(|| ParseError::MissingField("friends".to_string()))?;
248
249 let rich_presence = friend.rich_presence.iter().map(|rp| (rp.key.clone().unwrap_or_default(), rp.value.clone().unwrap_or_default())).collect();
250
251 Ok(PersonaData {
252 steam_id: SteamID::from_steam_id64(friend.friendid.unwrap_or(0)),
253 player_name: friend.player_name.clone().unwrap_or_default(),
254 persona_state: EPersonaState::from_i32(friend.persona_state.unwrap_or(0) as i32).unwrap_or(EPersonaState::Offline),
255 avatar_hash: friend.avatar_hash.as_ref().map(hex::encode),
256 game_name: friend.game_name.clone(),
257 game_id: friend.gameid,
258 last_logoff: friend.last_logoff,
259 last_logon: friend.last_logon,
260 last_seen_online: friend.last_seen_online,
261 game_server_ip: friend.game_server_ip,
262 game_server_port: friend.game_server_port,
263 rich_presence,
264 })
265}
266
267pub fn parse_license_list(body: &[u8]) -> Result<Vec<LicenseData>, ParseError> {
273 let msg = steam_protos::CMsgClientLicenseList::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
274
275 Ok(msg
276 .licenses
277 .iter()
278 .map(|l| LicenseData {
279 package_id: l.package_id.unwrap_or(0),
280 time_created: l.time_created.unwrap_or(0),
281 license_type: l.license_type.unwrap_or(0),
282 flags: l.flags.unwrap_or(0),
283 access_token: l.access_token.unwrap_or(0),
284 })
285 .collect())
286}
287
288pub fn parse_cm_list(body: &[u8]) -> Result<Vec<String>, ParseError> {
294 let msg = steam_protos::CMsgClientCMList::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
295
296 Ok(msg.cm_websocket_addresses)
297}
298
299pub fn parse_service_method(body: &[u8]) -> Result<ChatData, ParseError> {
305 let msg = steam_protos::CFriendMessagesIncomingMessageNotification::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
306
307 let sender = SteamID::from_steam_id64(msg.steamid_friend.unwrap_or(0));
308
309 if msg.chat_entry_type == Some(EChatEntryType::Typing as i32) {
310 Ok(ChatData::Typing { sender })
311 } else {
312 Ok(ChatData::Message {
313 sender,
314 message: msg.message.unwrap_or_default(),
315 chat_entry_type: EChatEntryType::from_i32(msg.chat_entry_type.unwrap_or(1)).unwrap_or(EChatEntryType::ChatMsg),
316 timestamp: msg.rtime32_server_timestamp.unwrap_or(0),
317 ordinal: msg.ordinal.unwrap_or(0),
318 })
319 }
320}
321
322pub fn parse_pics_product_info(body: &[u8]) -> Result<ProductInfoData, ParseError> {
328 let msg = steam_protos::CMsgClientPICSProductInfoResponse::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
329
330 let mut apps = HashMap::new();
331 let mut packages = HashMap::new();
332
333 for app in &msg.apps {
334 let app_id = app.appid.unwrap_or(0);
335 let app_info = app.buffer.as_ref().and_then(|buf| {
336 let text = String::from_utf8_lossy(buf);
337 let text = text.trim_end_matches('\0');
338 parse_vdf(text).ok().and_then(|v| match v {
339 VdfValue::Object(mut map) => map.remove("appinfo").or(Some(VdfValue::Object(map))),
340 _ => Some(v),
341 })
342 });
343
344 apps.insert(
345 app_id,
346 AppInfoData {
347 app_id,
348 change_number: app.change_number.unwrap_or(0),
349 missing_token: app.missing_token.unwrap_or(false),
350 app_info,
351 },
352 );
353 }
354
355 for pkg in &msg.packages {
356 let package_id = pkg.packageid.unwrap_or(0);
357 let package_info = pkg.buffer.as_ref().and_then(|buf| {
358 parse_binary_kv(buf).ok().and_then(|v| match v {
359 BinaryKvValue::Object(mut map) => {
360 let pkg_id_str = package_id.to_string();
361 map.remove(&pkg_id_str).or(Some(BinaryKvValue::Object(map)))
362 }
363 _ => Some(v),
364 })
365 });
366
367 packages.insert(
368 package_id,
369 PackageInfoData {
370 package_id,
371 change_number: pkg.change_number.unwrap_or(0),
372 missing_token: pkg.missing_token.unwrap_or(false),
373 package_info,
374 },
375 );
376 }
377
378 Ok(ProductInfoData { apps, packages, unknown_apps: msg.unknown_appids.clone(), unknown_packages: msg.unknown_packageids.clone() })
379}
380
381pub fn parse_pics_access_tokens(body: &[u8]) -> Result<AccessTokensData, ParseError> {
387 let msg = steam_protos::CMsgClientPICSAccessTokenResponse::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
388
389 let mut app_tokens = HashMap::new();
390 let mut package_tokens = HashMap::new();
391
392 for token in &msg.app_access_tokens {
393 app_tokens.insert(token.appid.unwrap_or(0), token.access_token.unwrap_or(0));
394 }
395
396 for token in &msg.package_access_tokens {
397 package_tokens.insert(token.packageid.unwrap_or(0), token.access_token.unwrap_or(0));
398 }
399
400 Ok(AccessTokensData {
401 app_tokens,
402 package_tokens,
403 app_denied: msg.app_denied_tokens.clone(),
404 package_denied: msg.package_denied_tokens.clone(),
405 })
406}
407
408pub fn parse_pics_changes(body: &[u8]) -> Result<ChangesData, ParseError> {
414 let msg = steam_protos::CMsgClientPICSChangesSinceResponse::decode(body).map_err(|e| ParseError::DecodeError(e.to_string()))?;
415
416 let app_changes = msg
417 .app_changes
418 .iter()
419 .map(|c| AppChange {
420 app_id: c.appid.unwrap_or(0),
421 change_number: c.change_number.unwrap_or(0),
422 needs_token: c.needs_token.unwrap_or(false),
423 })
424 .collect();
425
426 let package_changes = msg
427 .package_changes
428 .iter()
429 .map(|c| PackageChange {
430 package_id: c.packageid.unwrap_or(0),
431 change_number: c.change_number.unwrap_or(0),
432 needs_token: c.needs_token.unwrap_or(false),
433 })
434 .collect();
435
436 Ok(ChangesData { current_change_number: msg.current_change_number.unwrap_or(0), app_changes, package_changes })
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
448 fn test_parse_logon_response_success() {
449 let msg = steam_protos::CMsgClientLogonResponse {
450 eresult: Some(1), client_supplied_steamid: Some(76561198000000000),
452 ..Default::default()
453 };
454 let bytes = msg.encode_to_vec();
455
456 let result = parse_logon_response(&bytes).expect("parse failed");
457 assert_eq!(result.eresult, EResult::OK);
458 assert_eq!(result.steam_id.steam_id64(), 76561198000000000);
459 }
460
461 #[test]
462 fn test_parse_logon_response_failure() {
463 let msg = steam_protos::CMsgClientLogonResponse {
464 eresult: Some(5), ..Default::default()
466 };
467 let bytes = msg.encode_to_vec();
468
469 let result = parse_logon_response(&bytes).expect("parse failed");
470 assert_eq!(result.eresult, EResult::InvalidPassword);
471 }
472
473 #[test]
474 fn test_parse_logon_response_invalid_bytes() {
475 let bytes = vec![0xFF, 0xFF, 0xFF];
476 let result = parse_logon_response(&bytes);
477 assert!(matches!(result, Err(ParseError::DecodeError(_))));
478 }
479
480 #[test]
485 fn test_parse_logged_off() {
486 let msg = steam_protos::CMsgClientLoggedOff {
487 eresult: Some(6), };
489 let bytes = msg.encode_to_vec();
490
491 let result = parse_logged_off(&bytes).expect("parse failed");
492 assert_eq!(result, EResult::LoggedInElsewhere);
493 }
494
495 #[test]
500 fn test_parse_friends_list_empty() {
501 let msg = steam_protos::CMsgClientFriendsList { bincremental: Some(false), friends: vec![], ..Default::default() };
502 let bytes = msg.encode_to_vec();
503
504 let result = parse_friends_list(&bytes).expect("parse failed");
505 assert!(!result.incremental);
506 assert!(result.friends.is_empty());
507 }
508
509 #[test]
510 fn test_parse_friends_list_with_friends() {
511 let msg = steam_protos::CMsgClientFriendsList {
512 bincremental: Some(true),
513 friends: vec![
514 steam_protos::cmsg_client_friends_list::Friend {
515 ulfriendid: Some(76561198000000001),
516 efriendrelationship: Some(3), },
518 steam_protos::cmsg_client_friends_list::Friend {
519 ulfriendid: Some(76561198000000002),
520 efriendrelationship: Some(2), },
522 ],
523 ..Default::default()
524 };
525 let bytes = msg.encode_to_vec();
526
527 let result = parse_friends_list(&bytes).expect("parse failed");
528 assert!(result.incremental);
529 assert_eq!(result.friends.len(), 2);
530 assert_eq!(result.friends[0].steam_id.steam_id64(), 76561198000000001);
531 }
532
533 #[test]
538 fn test_parse_persona_state() {
539 let msg = steam_protos::CMsgClientPersonaState {
540 friends: vec![steam_protos::cmsg_client_persona_state::Friend {
541 friendid: Some(76561198000000000),
542 player_name: Some("TestPlayer".to_string()),
543 persona_state: Some(1), ..Default::default()
545 }],
546 ..Default::default()
547 };
548 let bytes = msg.encode_to_vec();
549
550 let result = parse_persona_state(&bytes).expect("parse failed");
551 assert_eq!(result.player_name, "TestPlayer");
552 assert_eq!(result.persona_state, EPersonaState::Online);
553 }
554
555 #[test]
556 fn test_parse_persona_state_no_friends() {
557 let msg = steam_protos::CMsgClientPersonaState { friends: vec![], ..Default::default() };
558 let bytes = msg.encode_to_vec();
559
560 let result = parse_persona_state(&bytes);
561 assert!(matches!(result, Err(ParseError::MissingField(_))));
562 }
563
564 #[test]
569 fn test_parse_license_list() {
570 let msg = steam_protos::CMsgClientLicenseList {
571 licenses: vec![steam_protos::cmsg_client_license_list::License {
572 package_id: Some(12345),
573 time_created: Some(1600000000),
574 license_type: Some(1),
575 flags: Some(0),
576 access_token: Some(999),
577 ..Default::default()
578 }],
579 ..Default::default()
580 };
581 let bytes = msg.encode_to_vec();
582
583 let result = parse_license_list(&bytes).expect("parse failed");
584 assert_eq!(result.len(), 1);
585 assert_eq!(result[0].package_id, 12345);
586 assert_eq!(result[0].access_token, 999);
587 }
588
589 #[test]
594 fn test_parse_cm_list() {
595 let msg = steam_protos::CMsgClientCMList {
596 cm_websocket_addresses: vec!["wss://cm1.steampowered.com".to_string(), "wss://cm2.steampowered.com".to_string()],
597 ..Default::default()
598 };
599 let bytes = msg.encode_to_vec();
600
601 let result = parse_cm_list(&bytes).expect("parse failed");
602 assert_eq!(result.len(), 2);
603 assert!(result[0].starts_with("wss://"));
604 }
605
606 #[test]
611 fn test_parse_service_method_message() {
612 let msg = steam_protos::CFriendMessagesIncomingMessageNotification {
613 steamid_friend: Some(76561198000000000),
614 message: Some("Hello!".to_string()),
615 chat_entry_type: Some(1), rtime32_server_timestamp: Some(1600000000),
617 ordinal: Some(5),
618 ..Default::default()
619 };
620 let bytes = msg.encode_to_vec();
621
622 let result = parse_service_method(&bytes).expect("parse failed");
623 match result {
624 ChatData::Message { sender, message, timestamp, .. } => {
625 assert_eq!(sender.steam_id64(), 76561198000000000);
626 assert_eq!(message, "Hello!");
627 assert_eq!(timestamp, 1600000000);
628 }
629 _ => panic!("Expected Message variant"),
630 }
631 }
632
633 #[test]
634 fn test_parse_service_method_typing() {
635 let msg = steam_protos::CFriendMessagesIncomingMessageNotification {
636 steamid_friend: Some(76561198000000000),
637 chat_entry_type: Some(EChatEntryType::Typing as i32),
638 ..Default::default()
639 };
640 let bytes = msg.encode_to_vec();
641
642 let result = parse_service_method(&bytes).expect("parse failed");
643 assert!(matches!(result, ChatData::Typing { .. }));
644 }
645
646 #[test]
651 fn test_parse_pics_access_tokens() {
652 let msg = steam_protos::CMsgClientPICSAccessTokenResponse {
653 app_access_tokens: vec![steam_protos::cmsg_client_pics_access_token_response::AppToken { appid: Some(440), access_token: Some(123456789) }],
654 package_access_tokens: vec![steam_protos::cmsg_client_pics_access_token_response::PackageToken { packageid: Some(550), access_token: Some(987654321) }],
655 app_denied_tokens: vec![10],
656 package_denied_tokens: vec![20],
657 };
658 let bytes = msg.encode_to_vec();
659
660 let result = parse_pics_access_tokens(&bytes).expect("parse failed");
661
662 assert_eq!(result.app_tokens.get(&440), Some(&123456789));
663 assert_eq!(result.package_tokens.get(&550), Some(&987654321));
664 assert_eq!(result.app_denied, vec![10]);
665 assert_eq!(result.package_denied, vec![20]);
666 }
667
668 #[test]
669 fn test_parse_pics_changes() {
670 let msg = steam_protos::CMsgClientPICSChangesSinceResponse {
671 current_change_number: Some(100),
672 app_changes: vec![steam_protos::cmsg_client_pics_changes_since_response::AppChange { appid: Some(440), change_number: Some(99), needs_token: Some(true) }],
673 package_changes: vec![steam_protos::cmsg_client_pics_changes_since_response::PackageChange { packageid: Some(550), change_number: Some(98), needs_token: Some(false) }],
674 ..Default::default()
675 };
676 let bytes = msg.encode_to_vec();
677
678 let result = parse_pics_changes(&bytes).expect("parse failed");
679
680 assert_eq!(result.current_change_number, 100);
681 assert_eq!(result.app_changes.len(), 1);
682 assert_eq!(result.app_changes[0].app_id, 440);
683 assert!(result.app_changes[0].needs_token);
684
685 assert_eq!(result.package_changes.len(), 1);
686 assert_eq!(result.package_changes[0].package_id, 550);
687 assert!(!result.package_changes[0].needs_token);
688 }
689}