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.social.write().friends.remove(&steam_id);
94 } else {
95 self.social.write().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.social.read().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 {
290 let maybe_persona = self.social.read().users.get(&id).cloned();
291 if let Some(persona) = maybe_persona {
292 self.social.read().persona_cache.insert(id, persona.clone());
293 result.insert(id, persona);
294 } else {
295 to_fetch.push(id);
296 }
297 }
298 }
299
300 if !to_fetch.is_empty() {
302 self.get_personas(to_fetch.clone()).await?;
304
305 }
315
316 Ok(result)
317 }
318
319 pub async fn block_user(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
324 if !self.is_logged_in() {
325 return Err(SteamError::NotLoggedOn);
326 }
327
328 let msg = steam_protos::CPlayerIgnoreFriendRequest { steamid: Some(steam_id.steam_id64()), unignore: Some(false) };
329
330 let _response: steam_protos::CPlayerIgnoreFriendResponse = self.send_unified_request_and_wait("Player.IgnoreFriend#1", &msg).await?;
331
332 Ok(())
333 }
334
335 pub async fn unblock_user(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
340 if !self.is_logged_in() {
341 return Err(SteamError::NotLoggedOn);
342 }
343
344 let msg = steam_protos::CPlayerIgnoreFriendRequest { steamid: Some(steam_id.steam_id64()), unignore: Some(true) };
345
346 let _response: steam_protos::CPlayerIgnoreFriendResponse = self.send_unified_request_and_wait("Player.IgnoreFriend#1", &msg).await?;
347
348 Ok(())
349 }
350
351 pub async fn create_friends_group(&mut self, name: &str) -> Result<u32, SteamError> {
359 if !self.is_logged_in() {
360 return Err(SteamError::NotLoggedOn);
361 }
362
363 let msg = steam_protos::CMsgClientCreateFriendsGroup {
364 groupname: Some(name.to_string()),
365 steamid: self.steam_id.as_ref().map(|s| s.steam_id64()),
366 ..Default::default()
367 };
368
369 let response: steam_protos::CMsgClientCreateFriendsGroupResponse = self.send_request_and_wait(steam_enums::EMsg::AMClientCreateFriendsGroup, &msg).await?;
371
372 let eresult = steam_enums::EResult::from_i32(response.eresult.unwrap_or(1) as i32).unwrap_or(steam_enums::EResult::Fail);
374 if eresult != steam_enums::EResult::OK {
375 return Err(SteamError::SteamResult(eresult));
376 }
377
378 Ok(response.groupid.unwrap_or(0) as u32)
379 }
380
381 pub async fn delete_friends_group(&mut self, group_id: u32) -> Result<(), SteamError> {
386 if !self.is_logged_in() {
387 return Err(SteamError::NotLoggedOn);
388 }
389
390 let msg = steam_protos::CMsgClientDeleteFriendsGroup { steamid: self.steam_id.as_ref().map(|s| s.steam_id64()), groupid: Some(group_id as i32) };
391
392 self.send_message(steam_enums::EMsg::AMClientDeleteFriendsGroup, &msg).await
393 }
394
395 pub async fn rename_friends_group(&mut self, group_id: u32, name: &str) -> Result<(), SteamError> {
401 if !self.is_logged_in() {
402 return Err(SteamError::NotLoggedOn);
403 }
404
405 let msg = steam_protos::CMsgClientRenameFriendsGroup { groupid: Some(group_id as i32), groupname: Some(name.to_string()) };
406
407 self.send_message(steam_enums::EMsg::AMClientManageFriendsGroup, &msg).await
408 }
409
410 pub async fn add_friend_to_group(&mut self, group_id: u32, steam_id: SteamID) -> Result<(), SteamError> {
416 if !self.is_logged_in() {
417 return Err(SteamError::NotLoggedOn);
418 }
419
420 let msg = steam_protos::CMsgClientAddFriendToGroup { groupid: Some(group_id as i32), steamiduser: Some(steam_id.steam_id64()) };
421
422 self.send_message(steam_enums::EMsg::AMClientAddFriendToGroup, &msg).await
423 }
424
425 pub async fn remove_friend_from_group(&mut self, group_id: u32, steam_id: SteamID) -> Result<(), SteamError> {
431 if !self.is_logged_in() {
432 return Err(SteamError::NotLoggedOn);
433 }
434
435 let msg = steam_protos::CMsgClientRemoveFriendFromGroup { groupid: Some(group_id as i32), steamiduser: Some(steam_id.steam_id64()) };
436
437 self.send_message(steam_enums::EMsg::AMClientRemoveFriendFromGroup, &msg).await
438 }
439
440 pub async fn set_nickname(&mut self, steam_id: SteamID, nickname: &str) -> Result<(), SteamError> {
446 if !self.is_logged_in() {
447 return Err(SteamError::NotLoggedOn);
448 }
449
450 let prefs = steam_protos::PerFriendPreferences { nickname: Some(nickname.to_string()), ..Default::default() };
452
453 let msg = steam_protos::CPlayerSetPerFriendPreferencesRequest { accountid: Some(steam_id.account_id), preferences: Some(prefs) };
454
455 self.send_service_method("Player.SetPerFriendPreferences#1", &msg).await
456 }
457
458 pub async fn get_nicknames(&mut self) -> Result<HashMap<SteamID, String>, SteamError> {
471 if !self.is_logged_in() {
472 return Err(SteamError::NotLoggedOn);
473 }
474
475 let msg = steam_protos::CPlayerGetNicknameListRequest {};
477
478 let response: steam_protos::CPlayerGetNicknameListResponse = self.send_unified_request_and_wait("Player.GetNicknameList#1", &msg).await?;
480
481 let mut nicknames = HashMap::new();
482 for nickname in response.nicknames {
483 if let Some(accountid) = nickname.accountid {
484 let steam_id = SteamID::from_individual_account_id(accountid);
485 if let Some(name) = nickname.nickname {
486 nicknames.insert(steam_id, name);
487 }
488 }
489 }
490
491 Ok(nicknames)
492 }
493
494 pub async fn get_persona_name_history(&mut self, steam_ids: Vec<SteamID>) -> Result<HashMap<SteamID, Vec<crate::types::PersonaNameHistory>>, SteamError> {
502 if !self.is_logged_in() {
503 return Err(SteamError::NotLoggedOn);
504 }
505
506 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();
507
508 let msg = steam_protos::CMsgClientAMGetPersonaNameHistory { id_count: Some(ids.len() as i32), ids };
509
510 let response: steam_protos::CMsgClientAMGetPersonaNameHistoryResponse = self.send_request_and_wait(steam_enums::EMsg::ClientAMGetPersonaNameHistory, &msg).await?;
511
512 let mut result = HashMap::new();
513
514 for resp in response.responses {
515 if let Some(steamid_64) = resp.steamid {
516 let steam_id = SteamID::from(steamid_64);
517 let mut history = Vec::new();
518
519 for name in resp.names {
520 history.push(crate::types::PersonaNameHistory { name: name.name.unwrap_or_default(), name_since: name.name_since.unwrap_or(0) });
521 }
522
523 result.insert(steam_id, history);
524 }
525 }
526
527 Ok(result)
528 }
529
530 pub async fn get_steam_levels(&mut self, steam_ids: Vec<SteamID>) -> Result<HashMap<SteamID, u32>, SteamError> {
538 if !self.is_logged_in() {
539 return Err(SteamError::NotLoggedOn);
540 }
541
542 let msg = steam_protos::CMsgClientFSGetFriendsSteamLevels { accountids: steam_ids.iter().map(|s| s.account_id).collect() };
543
544 let response: steam_protos::CMsgClientFSGetFriendsSteamLevelsResponse = self.send_request_and_wait(steam_enums::EMsg::ClientFSGetFriendsSteamLevels, &msg).await?;
546
547 let mut levels = HashMap::new();
548 for friend in response.friends {
549 if let Some(account_id) = friend.accountid {
550 let steam_id = SteamID::from_individual_account_id(account_id);
551 if let Some(level) = friend.level {
552 levels.insert(steam_id, level);
553 }
554 }
555 }
556
557 Ok(levels)
558 }
559
560 pub async fn get_game_badge_level(&mut self, app_id: u32) -> Result<(u32, i32, i32), SteamError> {
568 if !self.is_logged_in() {
569 return Err(SteamError::NotLoggedOn);
570 }
571
572 let msg = steam_protos::CPlayerGetGameBadgeLevelsRequest { appid: Some(app_id) };
573
574 let response: steam_protos::CPlayerGetGameBadgeLevelsResponse = self.send_unified_request_and_wait("Player.GetGameBadgeLevels#1", &msg).await?;
575
576 let mut regular = 0;
577 let mut foil = 0;
578
579 for badge in response.badges {
580 if badge.series != Some(1) {
581 continue;
582 }
583
584 if badge.border_color == Some(0) {
585 regular = badge.level.unwrap_or(0);
586 } else if badge.border_color == Some(1) {
587 foil = badge.level.unwrap_or(0);
588 }
589 }
590
591 Ok((response.player_level.unwrap_or(0), regular, foil))
592 }
593
594 pub async fn invite_to_group(&mut self, steam_id: SteamID, group_id: SteamID) -> Result<(), SteamError> {
600 if !self.is_logged_in() {
601 return Err(SteamError::NotLoggedOn);
602 }
603
604 use byteorder::{WriteBytesExt, LE};
605 let mut buf = Vec::with_capacity(17);
606 buf.write_u64::<LE>(steam_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
607 buf.write_u64::<LE>(group_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
608 buf.write_u8(1).map_err(|e| SteamError::Other(e.to_string()))?;
609
610 self.send_binary_message(steam_enums::EMsg::ClientInviteUserToClan, &buf).await
611 }
612
613 pub async fn respond_to_group_invite(&mut self, group_id: SteamID, accept: bool) -> Result<(), SteamError> {
619 if !self.is_logged_in() {
620 return Err(SteamError::NotLoggedOn);
621 }
622
623 use byteorder::{WriteBytesExt, LE};
624 let mut buf = Vec::with_capacity(9);
625 buf.write_u64::<LE>(group_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
626 buf.write_u8(if accept { 1 } else { 0 }).map_err(|e| SteamError::Other(e.to_string()))?;
627
628 self.send_binary_message(steam_enums::EMsg::ClientAcknowledgeClanInvite, &buf).await
629 }
630
631 pub async fn invite_to_game(&mut self, steam_id: SteamID, connect_string: &str) -> Result<(), SteamError> {
637 if !self.is_logged_in() {
638 return Err(SteamError::NotLoggedOn);
639 }
640
641 let msg = steam_protos::CMsgClientInviteToGame {
642 steam_id_dest: Some(steam_id.steam_id64()),
643 connect_string: Some(connect_string.to_string()),
644 ..Default::default()
645 };
646
647 self.send_message(steam_enums::EMsg::ClientInviteToGame, &msg).await
648 }
649
650 pub async fn create_quick_invite_link(&mut self, invite_limit: Option<u32>, invite_duration: Option<u32>) -> Result<crate::types::QuickInviteLink, SteamError> {
674 if !self.is_logged_in() {
675 return Err(SteamError::NotLoggedOn);
676 }
677
678 let steam_id = self.steam_id.ok_or(SteamError::NotLoggedOn)?;
680
681 let msg = steam_protos::CUserAccountCreateFriendInviteTokenRequest { invite_limit, invite_duration, invite_note: None };
682
683 let response: steam_protos::CUserAccountCreateFriendInviteTokenResponse = self.send_unified_request_and_wait("UserAccount.CreateFriendInviteToken#1", &msg).await?;
684
685 let friend_code = steam_friend_code::create_short_steam_friend_code(steam_id.account_id);
687 let invite_token = response.invite_token.unwrap_or_default();
688
689 Ok(crate::types::QuickInviteLink {
690 invite_link: format!("https://s.team/p/{}/{}", friend_code, invite_token),
691 invite_token,
692 invite_limit: response.invite_limit,
693 invite_duration: response.invite_duration,
694 time_created: response.time_created,
695 valid: response.valid.unwrap_or(true),
696 })
697 }
698
699 pub async fn list_quick_invite_links(&mut self) -> Result<Vec<crate::types::QuickInviteLink>, SteamError> {
704 if !self.is_logged_in() {
705 return Err(SteamError::NotLoggedOn);
706 }
707
708 let msg = steam_protos::CUserAccountGetFriendInviteTokensRequest {};
709
710 self.send_service_method("UserAccount.GetFriendInviteTokens#1", &msg).await?;
711
712 Ok(Vec::new())
715 }
716
717 pub async fn revoke_quick_invite_link(&mut self, link: &str) -> Result<(), SteamError> {
722 if !self.is_logged_in() {
723 return Err(SteamError::NotLoggedOn);
724 }
725
726 let (_, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
727
728 let msg = steam_protos::CUserAccountRevokeFriendInviteTokenRequest { invite_token: Some(token) };
729
730 self.send_service_method("UserAccount.RevokeFriendInviteToken#1", &msg).await
731 }
732
733 pub fn get_quick_invite_link_steam_id(&self, link: &str) -> Result<SteamID, SteamError> {
743 let (friend_code, _) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
744 let account_id = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
745 Ok(SteamID::from_individual_account_id(account_id))
746 }
747
748 pub async fn check_quick_invite_link_validity(&mut self, link: &str) -> Result<crate::types::QuickInviteLinkValidity, SteamError> {
756 if !self.is_logged_in() {
757 return Err(SteamError::NotLoggedOn);
758 }
759
760 let (friend_code, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
761 let owner_steam_id_account = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
762 let owner_steam_id = SteamID::from_individual_account_id(owner_steam_id_account);
763
764 let msg = steam_protos::CUserAccountViewFriendInviteTokenRequest { steamid: Some(owner_steam_id.steam_id64()), invite_token: Some(token) };
765
766 self.send_service_method("UserAccount.ViewFriendInviteToken#1", &msg).await?;
767
768 Ok(crate::types::QuickInviteLinkValidity {
770 valid: true, steam_id: Some(owner_steam_id),
772 invite_duration: None,
773 })
774 }
775
776 pub async fn redeem_quick_invite_link(&mut self, link: &str) -> Result<(), SteamError> {
781 if !self.is_logged_in() {
782 return Err(SteamError::NotLoggedOn);
783 }
784
785 let (friend_code, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
786 let owner_steam_id_account = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
787 let owner_steam_id = SteamID::from_individual_account_id(owner_steam_id_account);
788
789 let msg = steam_protos::CUserAccountRedeemFriendInviteTokenRequest { steamid: Some(owner_steam_id.steam_id64()), invite_token: Some(token) };
790
791 self.send_service_method("UserAccount.RedeemFriendInviteToken#1", &msg).await
792 }
793}