1use std::collections::HashMap;
11
12use steam_enums::{EClientPersonaStateFlag, EFriendRelationship, EPersonaState, EResult};
13use steamid::SteamID;
14
15use crate::{
16 client::{FriendEntry, FriendsEvent, SteamEvent},
17 error::SteamError,
18 SteamClient,
19};
20
21impl SteamClient {
22 pub async fn request_friends(&mut self) -> Result<(), SteamError> {
27 if !self.is_logged_in() {
28 return Err(SteamError::NotLoggedOn);
29 }
30
31 let msg = steam_protos::CMsgClientFriendsList { bincremental: Some(false), friends: vec![], ..Default::default() };
32
33 self.send_message(steam_enums::EMsg::ClientFriendsList, &msg).await
34 }
35
36 pub async fn request_friends_unified(&mut self) -> Result<Vec<steam_protos::cmsg_client_friends_list::Friend>, SteamError> {
42 if !self.is_logged_in() {
43 return Err(SteamError::NotLoggedOn);
44 }
45
46 let msg = steam_protos::CFriendsListGetFriendsListRequest {
47 role_mask: None, };
49
50 let response: steam_protos::CFriendsListGetFriendsListResponse = self.send_unified_request_and_wait("FriendsList.GetFriendsList#1", &msg).await?;
51
52 let friends = response.friendslist.as_ref().map(|f| f.friends.clone()).unwrap_or_default();
53 self.handle_friends_list_unified_response(response).await;
54 Ok(friends)
55 }
56
57 pub async fn request_friends_unified_trigger(&mut self) -> Result<(), SteamError> {
63 if !self.is_logged_in() {
64 return Err(SteamError::NotLoggedOn);
65 }
66
67 let msg = steam_protos::CFriendsListGetFriendsListRequest { role_mask: None };
68
69 self.send_service_method_background("FriendsList.GetFriendsList#1", &msg, crate::client::steam_client::BackgroundTask::FriendsList).await
70 }
71
72 pub(crate) async fn handle_friends_list_unified_response(&mut self, response: steam_protos::CFriendsListGetFriendsListResponse) {
74 let friends_list = match response.friendslist {
76 Some(fl) => fl,
77 None => {
78 tracing::warn!("[SteamClient] Received empty unified friends list response");
79 return;
80 }
81 };
82
83 tracing::debug!("[SteamClient] Handling unified friends list response with {} friends", friends_list.friends.len());
84 let mut entries = Vec::new();
86 for friend in &friends_list.friends {
87 if let Some(id_64) = friend.ulfriendid {
88 let steam_id = SteamID::from(id_64);
89 let relationship = friend.efriendrelationship.unwrap_or(0);
90 let rel_enum = steam_enums::EFriendRelationship::from_i32(relationship as i32).unwrap_or(steam_enums::EFriendRelationship::None);
91
92 if rel_enum == steam_enums::EFriendRelationship::None {
93 self.my_friends.remove(&steam_id);
94 } else {
95 self.my_friends.insert(steam_id, relationship);
96 }
97
98 entries.push(FriendEntry { steam_id, relationship: rel_enum });
99 }
100 }
101
102 tracing::debug!("[SteamClient] Processed {} friend entries, emitting FriendsList event", entries.len());
103
104 self.event_queue.push_back(SteamEvent::Friends(FriendsEvent::FriendsList { incremental: friends_list.bincremental.unwrap_or(false), friends: entries }));
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct Friend {
113 pub steam_id: SteamID,
115 pub relationship: EFriendRelationship,
117}
118
119#[derive(Debug, Clone)]
121pub struct AddFriendResult {
122 pub eresult: EResult,
124 pub persona_name: Option<String>,
126}
127
128#[derive(Debug, Clone)]
130pub struct FriendsGroup {
131 pub group_id: u32,
133 pub name: String,
135 pub members: Vec<SteamID>,
137}
138
139impl SteamClient {
140 pub async fn set_persona(&mut self, state: EPersonaState, name: Option<String>) -> Result<(), SteamError> {
153 if !self.is_logged_in() {
154 return Err(SteamError::NotLoggedOn);
155 }
156
157 let msg = steam_protos::CMsgClientChangeStatus { persona_state: Some(state as u32), player_name: name.clone(), ..Default::default() };
167
168 self.session_recovery.record_persona_state(state, name);
170
171 self.send_message(steam_enums::EMsg::ClientChangeStatus, &msg).await
172 }
173
174 pub async fn add_friend(&mut self, steam_id: SteamID) -> Result<AddFriendResult, SteamError> {
179 if !self.is_logged_in() {
180 return Err(SteamError::NotLoggedOn);
181 }
182
183 let msg = steam_protos::CMsgClientAddFriend { steamid_to_add: Some(steam_id.steam_id64()), ..Default::default() };
184
185 let response: steam_protos::CMsgClientAddFriendResponse = self.send_request_and_wait(steam_enums::EMsg::ClientAddFriend, &msg).await?;
186
187 Ok(AddFriendResult {
188 eresult: steam_enums::EResult::from_i32(response.eresult.unwrap_or(0)).unwrap_or(steam_enums::EResult::Fail),
189 persona_name: response.persona_name_added,
190 })
191 }
192
193 pub async fn remove_friend(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
198 if !self.is_logged_in() {
199 return Err(SteamError::NotLoggedOn);
200 }
201
202 let msg = steam_protos::CMsgClientRemoveFriend { friendid: Some(steam_id.steam_id64()) };
203
204 self.send_message(steam_enums::EMsg::ClientRemoveFriend, &msg).await
205 }
206
207 pub async fn get_personas(&mut self, steam_ids: Vec<SteamID>) -> Result<(), SteamError> {
212 if !self.is_logged_in() {
213 return Err(SteamError::NotLoggedOn);
214 }
215
216 let msg = steam_protos::CMsgClientRequestFriendData {
217 friends: steam_ids.iter().map(|s| s.steam_id64()).collect(),
218 persona_state_requested: Some(
219 (EClientPersonaStateFlag::Status as u32)
220 | (EClientPersonaStateFlag::PlayerName as u32)
221 | (EClientPersonaStateFlag::QueryPort as u32)
222 | (EClientPersonaStateFlag::SourceID as u32)
223 | (EClientPersonaStateFlag::Presence as u32)
224 | (EClientPersonaStateFlag::Metadata as u32)
225 | (EClientPersonaStateFlag::LastSeen as u32)
226 | (EClientPersonaStateFlag::UserClanRank as u32)
227 | (EClientPersonaStateFlag::GameExtraInfo as u32)
228 | (EClientPersonaStateFlag::GameDataBlob as u32)
229 | (EClientPersonaStateFlag::ClanData as u32)
230 | (EClientPersonaStateFlag::Facebook as u32)
231 | (EClientPersonaStateFlag::RichPresence as u32)
232 | (EClientPersonaStateFlag::Broadcast as u32)
233 | (EClientPersonaStateFlag::Watching as u32),
234 ),
235 };
236
237 self.send_message(steam_enums::EMsg::ClientRequestFriendData, &msg).await
238 }
239
240 pub async fn get_personas_cached(&mut self, steam_ids: Vec<SteamID>, force_refresh: bool) -> Result<HashMap<SteamID, crate::client::UserPersona>, SteamError> {
270 if !self.is_logged_in() {
271 return Err(SteamError::NotLoggedOn);
272 }
273
274 let mut result = HashMap::new();
275 let mut to_fetch = Vec::new();
276
277 if force_refresh {
278 to_fetch = steam_ids;
279 } else {
280 let (cached, missing) = self.persona_cache.get_many(&steam_ids);
282 for persona in cached {
283 result.insert(persona.steam_id, persona);
284 }
285
286 for id in missing {
288 if let Some(persona) = self.users.get(&id) {
289 self.persona_cache.insert(id, persona.clone());
291 result.insert(id, persona.clone());
292 } else {
293 to_fetch.push(id);
294 }
295 }
296 }
297
298 if !to_fetch.is_empty() {
300 self.get_personas(to_fetch.clone()).await?;
302
303 }
313
314 Ok(result)
315 }
316
317 pub async fn block_user(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
322 if !self.is_logged_in() {
323 return Err(SteamError::NotLoggedOn);
324 }
325
326 let msg = steam_protos::CPlayerIgnoreFriendRequest { steamid: Some(steam_id.steam_id64()), unignore: Some(false) };
327
328 let _response: steam_protos::CPlayerIgnoreFriendResponse = self.send_unified_request_and_wait("Player.IgnoreFriend#1", &msg).await?;
329
330 Ok(())
331 }
332
333 pub async fn unblock_user(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
338 if !self.is_logged_in() {
339 return Err(SteamError::NotLoggedOn);
340 }
341
342 let msg = steam_protos::CPlayerIgnoreFriendRequest { steamid: Some(steam_id.steam_id64()), unignore: Some(true) };
343
344 let _response: steam_protos::CPlayerIgnoreFriendResponse = self.send_unified_request_and_wait("Player.IgnoreFriend#1", &msg).await?;
345
346 Ok(())
347 }
348
349 pub async fn create_friends_group(&mut self, name: &str) -> Result<u32, SteamError> {
357 if !self.is_logged_in() {
358 return Err(SteamError::NotLoggedOn);
359 }
360
361 let msg = steam_protos::CMsgClientCreateFriendsGroup {
362 groupname: Some(name.to_string()),
363 steamid: self.steam_id.as_ref().map(|s| s.steam_id64()),
364 ..Default::default()
365 };
366
367 let response: steam_protos::CMsgClientCreateFriendsGroupResponse = self.send_request_and_wait(steam_enums::EMsg::AMClientCreateFriendsGroup, &msg).await?;
369
370 let eresult = steam_enums::EResult::from_i32(response.eresult.unwrap_or(1) as i32).unwrap_or(steam_enums::EResult::Fail);
372 if eresult != steam_enums::EResult::OK {
373 return Err(SteamError::SteamResult(eresult));
374 }
375
376 Ok(response.groupid.unwrap_or(0) as u32)
377 }
378
379 pub async fn delete_friends_group(&mut self, group_id: u32) -> Result<(), SteamError> {
384 if !self.is_logged_in() {
385 return Err(SteamError::NotLoggedOn);
386 }
387
388 let msg = steam_protos::CMsgClientDeleteFriendsGroup { steamid: self.steam_id.as_ref().map(|s| s.steam_id64()), groupid: Some(group_id as i32) };
389
390 self.send_message(steam_enums::EMsg::AMClientDeleteFriendsGroup, &msg).await
391 }
392
393 pub async fn rename_friends_group(&mut self, group_id: u32, name: &str) -> Result<(), SteamError> {
399 if !self.is_logged_in() {
400 return Err(SteamError::NotLoggedOn);
401 }
402
403 let msg = steam_protos::CMsgClientRenameFriendsGroup { groupid: Some(group_id as i32), groupname: Some(name.to_string()) };
404
405 self.send_message(steam_enums::EMsg::AMClientManageFriendsGroup, &msg).await
406 }
407
408 pub async fn add_friend_to_group(&mut self, group_id: u32, steam_id: SteamID) -> Result<(), SteamError> {
414 if !self.is_logged_in() {
415 return Err(SteamError::NotLoggedOn);
416 }
417
418 let msg = steam_protos::CMsgClientAddFriendToGroup { groupid: Some(group_id as i32), steamiduser: Some(steam_id.steam_id64()) };
419
420 self.send_message(steam_enums::EMsg::AMClientAddFriendToGroup, &msg).await
421 }
422
423 pub async fn remove_friend_from_group(&mut self, group_id: u32, steam_id: SteamID) -> Result<(), SteamError> {
429 if !self.is_logged_in() {
430 return Err(SteamError::NotLoggedOn);
431 }
432
433 let msg = steam_protos::CMsgClientRemoveFriendFromGroup { groupid: Some(group_id as i32), steamiduser: Some(steam_id.steam_id64()) };
434
435 self.send_message(steam_enums::EMsg::AMClientRemoveFriendFromGroup, &msg).await
436 }
437
438 pub async fn set_nickname(&mut self, steam_id: SteamID, nickname: &str) -> Result<(), SteamError> {
444 if !self.is_logged_in() {
445 return Err(SteamError::NotLoggedOn);
446 }
447
448 let prefs = steam_protos::PerFriendPreferences { nickname: Some(nickname.to_string()), ..Default::default() };
450
451 let msg = steam_protos::CPlayerSetPerFriendPreferencesRequest { accountid: Some(steam_id.account_id), preferences: Some(prefs) };
452
453 self.send_service_method("Player.SetPerFriendPreferences#1", &msg).await
454 }
455
456 pub async fn get_nicknames(&mut self) -> Result<HashMap<SteamID, String>, SteamError> {
469 if !self.is_logged_in() {
470 return Err(SteamError::NotLoggedOn);
471 }
472
473 let msg = steam_protos::CPlayerGetNicknameListRequest {};
475
476 let response: steam_protos::CPlayerGetNicknameListResponse = self.send_unified_request_and_wait("Player.GetNicknameList#1", &msg).await?;
478
479 let mut nicknames = HashMap::new();
480 for nickname in response.nicknames {
481 if let Some(accountid) = nickname.accountid {
482 let steam_id = SteamID::from_individual_account_id(accountid);
483 if let Some(name) = nickname.nickname {
484 nicknames.insert(steam_id, name);
485 }
486 }
487 }
488
489 Ok(nicknames)
490 }
491
492 pub async fn get_persona_name_history(&mut self, steam_ids: Vec<SteamID>) -> Result<HashMap<SteamID, Vec<crate::types::PersonaNameHistory>>, SteamError> {
500 if !self.is_logged_in() {
501 return Err(SteamError::NotLoggedOn);
502 }
503
504 let ids: Vec<steam_protos::cmsg_client_am_get_persona_name_history::IdInstance> = steam_ids.iter().map(|s| steam_protos::cmsg_client_am_get_persona_name_history::IdInstance { steamid: Some(s.steam_id64()) }).collect();
505
506 let msg = steam_protos::CMsgClientAMGetPersonaNameHistory { id_count: Some(ids.len() as i32), ids };
507
508 let response: steam_protos::CMsgClientAMGetPersonaNameHistoryResponse = self.send_request_and_wait(steam_enums::EMsg::ClientAMGetPersonaNameHistory, &msg).await?;
509
510 let mut result = HashMap::new();
511
512 for resp in response.responses {
513 if let Some(steamid_64) = resp.steamid {
514 let steam_id = SteamID::from(steamid_64);
515 let mut history = Vec::new();
516
517 for name in resp.names {
518 history.push(crate::types::PersonaNameHistory { name: name.name.unwrap_or_default(), name_since: name.name_since.unwrap_or(0) });
519 }
520
521 result.insert(steam_id, history);
522 }
523 }
524
525 Ok(result)
526 }
527
528 pub async fn get_steam_levels(&mut self, steam_ids: Vec<SteamID>) -> Result<HashMap<SteamID, u32>, SteamError> {
536 if !self.is_logged_in() {
537 return Err(SteamError::NotLoggedOn);
538 }
539
540 let msg = steam_protos::CMsgClientFSGetFriendsSteamLevels { accountids: steam_ids.iter().map(|s| s.account_id).collect() };
541
542 let response: steam_protos::CMsgClientFSGetFriendsSteamLevelsResponse = self.send_request_and_wait(steam_enums::EMsg::ClientFSGetFriendsSteamLevels, &msg).await?;
544
545 let mut levels = HashMap::new();
546 for friend in response.friends {
547 if let Some(account_id) = friend.accountid {
548 let steam_id = SteamID::from_individual_account_id(account_id);
549 if let Some(level) = friend.level {
550 levels.insert(steam_id, level);
551 }
552 }
553 }
554
555 Ok(levels)
556 }
557
558 pub async fn get_game_badge_level(&mut self, app_id: u32) -> Result<(u32, i32, i32), SteamError> {
566 if !self.is_logged_in() {
567 return Err(SteamError::NotLoggedOn);
568 }
569
570 let msg = steam_protos::CPlayerGetGameBadgeLevelsRequest { appid: Some(app_id) };
571
572 let response: steam_protos::CPlayerGetGameBadgeLevelsResponse = self.send_unified_request_and_wait("Player.GetGameBadgeLevels#1", &msg).await?;
573
574 let mut regular = 0;
575 let mut foil = 0;
576
577 for badge in response.badges {
578 if badge.series != Some(1) {
579 continue;
580 }
581
582 if badge.border_color == Some(0) {
583 regular = badge.level.unwrap_or(0);
584 } else if badge.border_color == Some(1) {
585 foil = badge.level.unwrap_or(0);
586 }
587 }
588
589 Ok((response.player_level.unwrap_or(0), regular, foil))
590 }
591
592 pub async fn invite_to_group(&mut self, steam_id: SteamID, group_id: SteamID) -> Result<(), SteamError> {
598 if !self.is_logged_in() {
599 return Err(SteamError::NotLoggedOn);
600 }
601
602 use byteorder::{WriteBytesExt, LE};
603 let mut buf = Vec::with_capacity(17);
604 buf.write_u64::<LE>(steam_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
605 buf.write_u64::<LE>(group_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
606 buf.write_u8(1).map_err(|e| SteamError::Other(e.to_string()))?;
607
608 self.send_binary_message(steam_enums::EMsg::ClientInviteUserToClan, &buf).await
609 }
610
611 pub async fn respond_to_group_invite(&mut self, group_id: SteamID, accept: bool) -> Result<(), SteamError> {
617 if !self.is_logged_in() {
618 return Err(SteamError::NotLoggedOn);
619 }
620
621 use byteorder::{WriteBytesExt, LE};
622 let mut buf = Vec::with_capacity(9);
623 buf.write_u64::<LE>(group_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
624 buf.write_u8(if accept { 1 } else { 0 }).map_err(|e| SteamError::Other(e.to_string()))?;
625
626 self.send_binary_message(steam_enums::EMsg::ClientAcknowledgeClanInvite, &buf).await
627 }
628
629 pub async fn invite_to_game(&mut self, steam_id: SteamID, connect_string: &str) -> Result<(), SteamError> {
635 if !self.is_logged_in() {
636 return Err(SteamError::NotLoggedOn);
637 }
638
639 let msg = steam_protos::CMsgClientInviteToGame {
640 steam_id_dest: Some(steam_id.steam_id64()),
641 connect_string: Some(connect_string.to_string()),
642 ..Default::default()
643 };
644
645 self.send_message(steam_enums::EMsg::ClientInviteToGame, &msg).await
646 }
647
648 pub async fn create_quick_invite_link(&mut self, invite_limit: Option<u32>, invite_duration: Option<u32>) -> Result<crate::types::QuickInviteLink, SteamError> {
672 if !self.is_logged_in() {
673 return Err(SteamError::NotLoggedOn);
674 }
675
676 let steam_id = self.steam_id.ok_or(SteamError::NotLoggedOn)?;
678
679 let msg = steam_protos::CUserAccountCreateFriendInviteTokenRequest { invite_limit, invite_duration, invite_note: None };
680
681 let response: steam_protos::CUserAccountCreateFriendInviteTokenResponse = self.send_unified_request_and_wait("UserAccount.CreateFriendInviteToken#1", &msg).await?;
682
683 let friend_code = steam_friend_code::create_short_steam_friend_code(steam_id.account_id);
685 let invite_token = response.invite_token.unwrap_or_default();
686
687 Ok(crate::types::QuickInviteLink {
688 invite_link: format!("https://s.team/p/{}/{}", friend_code, invite_token),
689 invite_token,
690 invite_limit: response.invite_limit,
691 invite_duration: response.invite_duration,
692 time_created: response.time_created,
693 valid: response.valid.unwrap_or(true),
694 })
695 }
696
697 pub async fn list_quick_invite_links(&mut self) -> Result<Vec<crate::types::QuickInviteLink>, SteamError> {
702 if !self.is_logged_in() {
703 return Err(SteamError::NotLoggedOn);
704 }
705
706 let msg = steam_protos::CUserAccountGetFriendInviteTokensRequest {};
707
708 self.send_service_method("UserAccount.GetFriendInviteTokens#1", &msg).await?;
709
710 Ok(Vec::new())
713 }
714
715 pub async fn revoke_quick_invite_link(&mut self, link: &str) -> Result<(), SteamError> {
720 if !self.is_logged_in() {
721 return Err(SteamError::NotLoggedOn);
722 }
723
724 let (_, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
725
726 let msg = steam_protos::CUserAccountRevokeFriendInviteTokenRequest { invite_token: Some(token) };
727
728 self.send_service_method("UserAccount.RevokeFriendInviteToken#1", &msg).await
729 }
730
731 pub fn get_quick_invite_link_steam_id(&self, link: &str) -> Result<SteamID, SteamError> {
741 let (friend_code, _) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
742 let account_id = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
743 Ok(SteamID::from_individual_account_id(account_id))
744 }
745
746 pub async fn check_quick_invite_link_validity(&mut self, link: &str) -> Result<crate::types::QuickInviteLinkValidity, SteamError> {
754 if !self.is_logged_in() {
755 return Err(SteamError::NotLoggedOn);
756 }
757
758 let (friend_code, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
759 let owner_steam_id_account = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
760 let owner_steam_id = SteamID::from_individual_account_id(owner_steam_id_account);
761
762 let msg = steam_protos::CUserAccountViewFriendInviteTokenRequest { steamid: Some(owner_steam_id.steam_id64()), invite_token: Some(token) };
763
764 self.send_service_method("UserAccount.ViewFriendInviteToken#1", &msg).await?;
765
766 Ok(crate::types::QuickInviteLinkValidity {
768 valid: true, steam_id: Some(owner_steam_id),
770 invite_duration: None,
771 })
772 }
773
774 pub async fn redeem_quick_invite_link(&mut self, link: &str) -> Result<(), SteamError> {
779 if !self.is_logged_in() {
780 return Err(SteamError::NotLoggedOn);
781 }
782
783 let (friend_code, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
784 let owner_steam_id_account = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
785 let owner_steam_id = SteamID::from_individual_account_id(owner_steam_id_account);
786
787 let msg = steam_protos::CUserAccountRedeemFriendInviteTokenRequest { steamid: Some(owner_steam_id.steam_id64()), invite_token: Some(token) };
788
789 self.send_service_method("UserAccount.RedeemFriendInviteToken#1", &msg).await
790 }
791}