use std::collections::HashMap;
use steam_enums::{EClientPersonaStateFlag, EFriendRelationship, EPersonaState, EResult};
use steamid::SteamID;
use crate::{
client::{FriendEntry, FriendsEvent, SteamEvent},
error::SteamError,
SteamClient,
};
impl SteamClient {
pub async fn request_friends(&mut self) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientFriendsList { bincremental: Some(false), friends: vec![], ..Default::default() };
self.send_message(steam_enums::EMsg::ClientFriendsList, &msg).await
}
pub async fn request_friends_unified(&mut self) -> Result<Vec<steam_protos::cmsg_client_friends_list::Friend>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CFriendsListGetFriendsListRequest {
role_mask: None, };
let response: steam_protos::CFriendsListGetFriendsListResponse = self.send_unified_request_and_wait("FriendsList.GetFriendsList#1", &msg).await?;
let friends = response.friendslist.as_ref().map(|f| f.friends.clone()).unwrap_or_default();
self.handle_friends_list_unified_response(response).await;
Ok(friends)
}
pub async fn request_friends_unified_trigger(&mut self) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CFriendsListGetFriendsListRequest { role_mask: None };
self.send_service_method_background("FriendsList.GetFriendsList#1", &msg, crate::client::steam_client::BackgroundTask::FriendsList).await
}
pub(crate) async fn handle_friends_list_unified_response(&mut self, response: steam_protos::CFriendsListGetFriendsListResponse) {
let friends_list = match response.friendslist {
Some(fl) => fl,
None => {
tracing::warn!("[SteamClient] Received empty unified friends list response");
return;
}
};
tracing::debug!("[SteamClient] Handling unified friends list response with {} friends", friends_list.friends.len());
let mut entries = Vec::new();
for friend in &friends_list.friends {
if let Some(id_64) = friend.ulfriendid {
let steam_id = SteamID::from(id_64);
let relationship = friend.efriendrelationship.unwrap_or(0);
let rel_enum = steam_enums::EFriendRelationship::from_i32(relationship as i32).unwrap_or(steam_enums::EFriendRelationship::None);
if rel_enum == steam_enums::EFriendRelationship::None {
self.social.write().friends.remove(&steam_id);
} else {
self.social.write().friends.insert(steam_id, relationship);
}
entries.push(FriendEntry { steam_id, relationship: rel_enum });
}
}
tracing::debug!("[SteamClient] Processed {} friend entries, emitting FriendsList event", entries.len());
self.event_queue.push_back(SteamEvent::Friends(FriendsEvent::FriendsList { incremental: friends_list.bincremental.unwrap_or(false), friends: entries }));
}
}
#[derive(Debug, Clone)]
pub struct Friend {
pub steam_id: SteamID,
pub relationship: EFriendRelationship,
}
#[derive(Debug, Clone)]
pub struct AddFriendResult {
pub eresult: EResult,
pub persona_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FriendsGroup {
pub group_id: u32,
pub name: String,
pub members: Vec<SteamID>,
}
impl SteamClient {
pub async fn set_persona(&mut self, state: EPersonaState, name: Option<String>) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientChangeStatus { persona_state: Some(state as u32), player_name: name.clone(), ..Default::default() };
self.session_recovery.record_persona_state(state, name);
self.send_message(steam_enums::EMsg::ClientChangeStatus, &msg).await
}
pub async fn add_friend(&mut self, steam_id: SteamID) -> Result<AddFriendResult, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientAddFriend { steamid_to_add: Some(steam_id.steam_id64()), ..Default::default() };
let response: steam_protos::CMsgClientAddFriendResponse = self.send_request_and_wait(steam_enums::EMsg::ClientAddFriend, &msg).await?;
Ok(AddFriendResult {
eresult: steam_enums::EResult::from_i32(response.eresult.unwrap_or(0)).unwrap_or(steam_enums::EResult::Fail),
persona_name: response.persona_name_added,
})
}
pub async fn remove_friend(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientRemoveFriend { friendid: Some(steam_id.steam_id64()) };
self.send_message(steam_enums::EMsg::ClientRemoveFriend, &msg).await
}
pub async fn get_personas(&mut self, steam_ids: Vec<SteamID>) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientRequestFriendData {
friends: steam_ids.iter().map(|s| s.steam_id64()).collect(),
persona_state_requested: Some(
(EClientPersonaStateFlag::Status as u32)
| (EClientPersonaStateFlag::PlayerName as u32)
| (EClientPersonaStateFlag::QueryPort as u32)
| (EClientPersonaStateFlag::SourceID as u32)
| (EClientPersonaStateFlag::Presence as u32)
| (EClientPersonaStateFlag::Metadata as u32)
| (EClientPersonaStateFlag::LastSeen as u32)
| (EClientPersonaStateFlag::UserClanRank as u32)
| (EClientPersonaStateFlag::GameExtraInfo as u32)
| (EClientPersonaStateFlag::GameDataBlob as u32)
| (EClientPersonaStateFlag::ClanData as u32)
| (EClientPersonaStateFlag::Facebook as u32)
| (EClientPersonaStateFlag::RichPresence as u32)
| (EClientPersonaStateFlag::Broadcast as u32)
| (EClientPersonaStateFlag::Watching as u32),
),
};
self.send_message(steam_enums::EMsg::ClientRequestFriendData, &msg).await
}
pub async fn get_personas_cached(&mut self, steam_ids: Vec<SteamID>, force_refresh: bool) -> Result<HashMap<SteamID, crate::client::UserPersona>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let mut result = HashMap::new();
let mut to_fetch = Vec::new();
if force_refresh {
to_fetch = steam_ids;
} else {
let (cached, missing) = self.social.read().persona_cache.get_many(&steam_ids);
for persona in cached {
result.insert(persona.steam_id, persona);
}
for id in missing {
let maybe_persona = self.social.read().users.get(&id).cloned();
if let Some(persona) = maybe_persona {
self.social.read().persona_cache.insert(id, persona.clone());
result.insert(id, persona);
} else {
to_fetch.push(id);
}
}
}
if !to_fetch.is_empty() {
self.get_personas(to_fetch.clone()).await?;
}
Ok(result)
}
pub async fn block_user(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CPlayerIgnoreFriendRequest { steamid: Some(steam_id.steam_id64()), unignore: Some(false) };
let _response: steam_protos::CPlayerIgnoreFriendResponse = self.send_unified_request_and_wait("Player.IgnoreFriend#1", &msg).await?;
Ok(())
}
pub async fn unblock_user(&mut self, steam_id: SteamID) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CPlayerIgnoreFriendRequest { steamid: Some(steam_id.steam_id64()), unignore: Some(true) };
let _response: steam_protos::CPlayerIgnoreFriendResponse = self.send_unified_request_and_wait("Player.IgnoreFriend#1", &msg).await?;
Ok(())
}
pub async fn create_friends_group(&mut self, name: &str) -> Result<u32, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientCreateFriendsGroup {
groupname: Some(name.to_string()),
steamid: self.steam_id.as_ref().map(|s| s.steam_id64()),
..Default::default()
};
let response: steam_protos::CMsgClientCreateFriendsGroupResponse = self.send_request_and_wait(steam_enums::EMsg::AMClientCreateFriendsGroup, &msg).await?;
let eresult = steam_enums::EResult::from_i32(response.eresult.unwrap_or(1) as i32).unwrap_or(steam_enums::EResult::Fail);
if eresult != steam_enums::EResult::OK {
return Err(SteamError::SteamResult(eresult));
}
Ok(response.groupid.unwrap_or(0) as u32)
}
pub async fn delete_friends_group(&mut self, group_id: u32) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientDeleteFriendsGroup { steamid: self.steam_id.as_ref().map(|s| s.steam_id64()), groupid: Some(group_id as i32) };
self.send_message(steam_enums::EMsg::AMClientDeleteFriendsGroup, &msg).await
}
pub async fn rename_friends_group(&mut self, group_id: u32, name: &str) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientRenameFriendsGroup { groupid: Some(group_id as i32), groupname: Some(name.to_string()) };
self.send_message(steam_enums::EMsg::AMClientManageFriendsGroup, &msg).await
}
pub async fn add_friend_to_group(&mut self, group_id: u32, steam_id: SteamID) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientAddFriendToGroup { groupid: Some(group_id as i32), steamiduser: Some(steam_id.steam_id64()) };
self.send_message(steam_enums::EMsg::AMClientAddFriendToGroup, &msg).await
}
pub async fn remove_friend_from_group(&mut self, group_id: u32, steam_id: SteamID) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientRemoveFriendFromGroup { groupid: Some(group_id as i32), steamiduser: Some(steam_id.steam_id64()) };
self.send_message(steam_enums::EMsg::AMClientRemoveFriendFromGroup, &msg).await
}
pub async fn set_nickname(&mut self, steam_id: SteamID, nickname: &str) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let prefs = steam_protos::PerFriendPreferences { nickname: Some(nickname.to_string()), ..Default::default() };
let msg = steam_protos::CPlayerSetPerFriendPreferencesRequest { accountid: Some(steam_id.account_id), preferences: Some(prefs) };
self.send_service_method("Player.SetPerFriendPreferences#1", &msg).await
}
pub async fn get_nicknames(&mut self) -> Result<HashMap<SteamID, String>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CPlayerGetNicknameListRequest {};
let response: steam_protos::CPlayerGetNicknameListResponse = self.send_unified_request_and_wait("Player.GetNicknameList#1", &msg).await?;
let mut nicknames = HashMap::new();
for nickname in response.nicknames {
if let Some(accountid) = nickname.accountid {
let steam_id = SteamID::from_individual_account_id(accountid);
if let Some(name) = nickname.nickname {
nicknames.insert(steam_id, name);
}
}
}
Ok(nicknames)
}
pub async fn get_persona_name_history(&mut self, steam_ids: Vec<SteamID>) -> Result<HashMap<SteamID, Vec<crate::types::PersonaNameHistory>>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
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();
let msg = steam_protos::CMsgClientAMGetPersonaNameHistory { id_count: Some(ids.len() as i32), ids };
let response: steam_protos::CMsgClientAMGetPersonaNameHistoryResponse = self.send_request_and_wait(steam_enums::EMsg::ClientAMGetPersonaNameHistory, &msg).await?;
let mut result = HashMap::new();
for resp in response.responses {
if let Some(steamid_64) = resp.steamid {
let steam_id = SteamID::from(steamid_64);
let mut history = Vec::new();
for name in resp.names {
history.push(crate::types::PersonaNameHistory { name: name.name.unwrap_or_default(), name_since: name.name_since.unwrap_or(0) });
}
result.insert(steam_id, history);
}
}
Ok(result)
}
pub async fn get_steam_levels(&mut self, steam_ids: Vec<SteamID>) -> Result<HashMap<SteamID, u32>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientFSGetFriendsSteamLevels { accountids: steam_ids.iter().map(|s| s.account_id).collect() };
let response: steam_protos::CMsgClientFSGetFriendsSteamLevelsResponse = self.send_request_and_wait(steam_enums::EMsg::ClientFSGetFriendsSteamLevels, &msg).await?;
let mut levels = HashMap::new();
for friend in response.friends {
if let Some(account_id) = friend.accountid {
let steam_id = SteamID::from_individual_account_id(account_id);
if let Some(level) = friend.level {
levels.insert(steam_id, level);
}
}
}
Ok(levels)
}
pub async fn get_game_badge_level(&mut self, app_id: u32) -> Result<(u32, i32, i32), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CPlayerGetGameBadgeLevelsRequest { appid: Some(app_id) };
let response: steam_protos::CPlayerGetGameBadgeLevelsResponse = self.send_unified_request_and_wait("Player.GetGameBadgeLevels#1", &msg).await?;
let mut regular = 0;
let mut foil = 0;
for badge in response.badges {
if badge.series != Some(1) {
continue;
}
if badge.border_color == Some(0) {
regular = badge.level.unwrap_or(0);
} else if badge.border_color == Some(1) {
foil = badge.level.unwrap_or(0);
}
}
Ok((response.player_level.unwrap_or(0), regular, foil))
}
pub async fn invite_to_group(&mut self, steam_id: SteamID, group_id: SteamID) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
use byteorder::{WriteBytesExt, LE};
let mut buf = Vec::with_capacity(17);
buf.write_u64::<LE>(steam_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
buf.write_u64::<LE>(group_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
buf.write_u8(1).map_err(|e| SteamError::Other(e.to_string()))?;
self.send_binary_message(steam_enums::EMsg::ClientInviteUserToClan, &buf).await
}
pub async fn respond_to_group_invite(&mut self, group_id: SteamID, accept: bool) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
use byteorder::{WriteBytesExt, LE};
let mut buf = Vec::with_capacity(9);
buf.write_u64::<LE>(group_id.steam_id64()).map_err(|e| SteamError::Other(e.to_string()))?;
buf.write_u8(if accept { 1 } else { 0 }).map_err(|e| SteamError::Other(e.to_string()))?;
self.send_binary_message(steam_enums::EMsg::ClientAcknowledgeClanInvite, &buf).await
}
pub async fn invite_to_game(&mut self, steam_id: SteamID, connect_string: &str) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CMsgClientInviteToGame {
steam_id_dest: Some(steam_id.steam_id64()),
connect_string: Some(connect_string.to_string()),
..Default::default()
};
self.send_message(steam_enums::EMsg::ClientInviteToGame, &msg).await
}
pub async fn create_quick_invite_link(&mut self, invite_limit: Option<u32>, invite_duration: Option<u32>) -> Result<crate::types::QuickInviteLink, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let steam_id = self.steam_id.ok_or(SteamError::NotLoggedOn)?;
let msg = steam_protos::CUserAccountCreateFriendInviteTokenRequest { invite_limit, invite_duration, invite_note: None };
let response: steam_protos::CUserAccountCreateFriendInviteTokenResponse = self.send_unified_request_and_wait("UserAccount.CreateFriendInviteToken#1", &msg).await?;
let friend_code = steam_friend_code::create_short_steam_friend_code(steam_id.account_id);
let invite_token = response.invite_token.unwrap_or_default();
Ok(crate::types::QuickInviteLink {
invite_link: format!("https://s.team/p/{}/{}", friend_code, invite_token),
invite_token,
invite_limit: response.invite_limit,
invite_duration: response.invite_duration,
time_created: response.time_created,
valid: response.valid.unwrap_or(true),
})
}
pub async fn list_quick_invite_links(&mut self) -> Result<Vec<crate::types::QuickInviteLink>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CUserAccountGetFriendInviteTokensRequest {};
self.send_service_method("UserAccount.GetFriendInviteTokens#1", &msg).await?;
Ok(Vec::new())
}
pub async fn revoke_quick_invite_link(&mut self, link: &str) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let (_, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
let msg = steam_protos::CUserAccountRevokeFriendInviteTokenRequest { invite_token: Some(token) };
self.send_service_method("UserAccount.RevokeFriendInviteToken#1", &msg).await
}
pub fn get_quick_invite_link_steam_id(&self, link: &str) -> Result<SteamID, SteamError> {
let (friend_code, _) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
let account_id = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
Ok(SteamID::from_individual_account_id(account_id))
}
pub async fn check_quick_invite_link_validity(&mut self, link: &str) -> Result<crate::types::QuickInviteLinkValidity, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let (friend_code, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
let owner_steam_id_account = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
let owner_steam_id = SteamID::from_individual_account_id(owner_steam_id_account);
let msg = steam_protos::CUserAccountViewFriendInviteTokenRequest { steamid: Some(owner_steam_id.steam_id64()), invite_token: Some(token) };
self.send_service_method("UserAccount.ViewFriendInviteToken#1", &msg).await?;
Ok(crate::types::QuickInviteLinkValidity {
valid: true, steam_id: Some(owner_steam_id),
invite_duration: None,
})
}
pub async fn redeem_quick_invite_link(&mut self, link: &str) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let (friend_code, token) = steam_friend_code::parse_quick_invite_link(link).ok_or_else(|| SteamError::Other("Invalid quick invite link format".into()))?;
let owner_steam_id_account = steam_friend_code::parse_short_steam_friend_code(&friend_code).ok_or_else(|| SteamError::Other("Invalid friend code".into()))?;
let owner_steam_id = SteamID::from_individual_account_id(owner_steam_id_account);
let msg = steam_protos::CUserAccountRedeemFriendInviteTokenRequest { steamid: Some(owner_steam_id.steam_id64()), invite_token: Some(token) };
self.send_service_method("UserAccount.RedeemFriendInviteToken#1", &msg).await
}
}