use prost::Message;
use steam_enums::EChatEntryType;
use steamid::SteamID;
use tracing::{error, info};
use crate::{error::SteamError, SteamClient};
#[derive(Debug, Clone)]
pub struct ChatMessage {
pub message: String,
pub chat_entry_type: EChatEntryType,
pub contains_bbcode: bool,
}
impl Default for ChatMessage {
fn default() -> Self {
Self { message: String::new(), chat_entry_type: EChatEntryType::ChatMsg, contains_bbcode: true }
}
}
#[derive(Debug, Clone)]
pub struct SendMessageResult {
pub modified_message: String,
pub server_timestamp: u32,
pub ordinal: u32,
}
#[derive(Debug, Clone)]
pub struct HistoryMessage {
pub sender: SteamID,
pub timestamp: u32,
pub ordinal: u32,
pub message: String,
pub unread: bool,
}
#[derive(Debug, Clone)]
pub struct FriendMessageSession {
pub friend: SteamID,
pub time_last_message: u32,
pub time_last_view: u32,
pub unread_count: u32,
}
impl SteamClient {
pub async fn send_friend_message(&mut self, friend: SteamID, message: &str) -> Result<SendMessageResult, SteamError> {
self.send_friend_message_with_options(friend, message, EChatEntryType::ChatMsg, true).await
}
pub fn send_friend_message_async(&mut self, friend: SteamID, message: &str) -> Result<tokio::sync::oneshot::Receiver<Result<SendMessageResult, SteamError>>, SteamError> {
info!("[SteamClient] send_friend_message_async called");
info!("[SteamClient] Target friend: {}", friend);
info!("[SteamClient] Message length: {}", message.len());
info!("[SteamClient] is_logged_in: {}", self.is_logged_in());
info!("[SteamClient] steam_id: {:?}", self.steam_id);
if !self.is_logged_in() {
error!("[SteamClient] NOT LOGGED IN - returning NotLoggedOn error");
return Err(SteamError::NotLoggedOn);
}
let (tx, rx) = tokio::sync::oneshot::channel();
let queued = crate::client::steam_client::QueuedMessage {
friend,
message: message.to_string(),
entry_type: EChatEntryType::ChatMsg,
contains_bbcode: true,
respond_to: tx,
};
match self.chat.tx.send(queued) {
Ok(_) => {
info!("[SteamClient] Message queued to chat_queue_tx successfully for {}", friend);
}
Err(e) => {
error!("[SteamClient] Failed to queue message to chat_queue_tx: {:?}", e);
return Err(SteamError::Other("Failed to queue message".to_string()));
}
}
Ok(rx)
}
pub async fn send_friend_message_with_options(&mut self, friend: SteamID, message: &str, entry_type: EChatEntryType, contains_bbcode: bool) -> Result<SendMessageResult, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let (tx, rx) = tokio::sync::oneshot::channel();
let queued = crate::client::steam_client::QueuedMessage { friend, message: message.to_string(), entry_type, contains_bbcode, respond_to: tx };
self.chat.tx.send(queued).map_err(|_| SteamError::Other("Failed to queue message".to_string()))?;
rx.await.map_err(|_| SteamError::Other("Response channel closed".to_string()))?
}
pub async fn send_friend_typing(&mut self, friend: SteamID) -> Result<(), SteamError> {
self.send_friend_message_with_options(friend, "", EChatEntryType::Typing, false).await?;
Ok(())
}
pub async fn get_active_friend_sessions(&mut self) -> Result<Vec<FriendMessageSession>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CFriendMessagesGetActiveMessageSessionsRequest::default();
let rx = self.send_service_method_with_job("FriendMessages.GetActiveMessageSessions#1", &msg).await?;
let job_response = rx.await.map_err(|_| SteamError::ResponseTimeout)?;
let body = match job_response {
crate::internal::jobs::JobResponse::Success(bytes) => bytes,
crate::internal::jobs::JobResponse::Timeout => return Err(SteamError::ResponseTimeout),
crate::internal::jobs::JobResponse::Error(msg) => return Err(SteamError::ProtocolError(msg)),
};
let response = steam_protos::CFriendMessagesGetActiveMessageSessionsResponse::decode(&body[..]).map_err(|_| SteamError::DeserializationFailed)?;
let sessions = response
.message_sessions
.into_iter()
.map(|s| FriendMessageSession {
friend: SteamID::from_steam_id64(s.accountid_friend.unwrap_or(0) as u64),
time_last_message: s.last_message.unwrap_or(0),
time_last_view: s.last_view.unwrap_or(0),
unread_count: s.unread_message_count.unwrap_or(0),
})
.collect();
Ok(sessions)
}
pub async fn ack_friend_message(&mut self, friend: SteamID, timestamp: u32) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CFriendMessagesAckMessageNotification { steamid_partner: Some(friend.steam_id64()), timestamp: Some(timestamp) };
self.social.write().chat_last_view.insert(friend, timestamp);
self.send_service_method("FriendMessages.AckMessage#1", &msg).await
}
pub async fn get_chat_history(&mut self, friend: SteamID, start_time: u32, count: u32) -> Result<Vec<HistoryMessage>, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CFriendMessagesGetRecentMessagesRequest {
steamid1: self.steam_id.as_ref().map(|s| s.steam_id64()),
steamid2: Some(friend.steam_id64()),
count: Some(count),
most_recent_conversation: Some(start_time == 0),
rtime32_start_time: if start_time > 0 { Some(start_time) } else { None },
..Default::default()
};
let rx = self.send_service_method_with_job("FriendMessages.GetRecentMessages#1", &msg).await?;
let job_response = rx.await.map_err(|_| SteamError::ResponseTimeout)?;
let body = match job_response {
crate::internal::jobs::JobResponse::Success(bytes) => bytes,
crate::internal::jobs::JobResponse::Timeout => return Err(SteamError::ResponseTimeout),
crate::internal::jobs::JobResponse::Error(msg) => return Err(SteamError::ProtocolError(msg)),
};
let response = steam_protos::CFriendMessagesGetRecentMessagesResponse::decode(&body[..]).map_err(|_| SteamError::DeserializationFailed)?;
let last_view_timestamp = self.social.read().chat_last_view.get(&friend).copied().unwrap_or(0);
let messages = response
.messages
.into_iter()
.map(|m| HistoryMessage {
sender: SteamID::from_steam_id64(m.accountid.unwrap_or(0) as u64),
timestamp: m.timestamp.unwrap_or(0),
ordinal: m.ordinal.unwrap_or(0),
message: m.message.unwrap_or_default(),
unread: m.timestamp.unwrap_or(0) > last_view_timestamp,
})
.collect();
Ok(messages)
}
pub async fn get_recent_chat_history(&mut self, friend: SteamID) -> Result<Vec<HistoryMessage>, SteamError> {
self.get_chat_history(friend, 0, 20).await
}
pub(crate) async fn send_service_method<T: prost::Message>(&mut self, method: &str, body: &T) -> Result<(), SteamError> {
use crate::protocol::{ProtobufMessageHeader, SteamMessage};
let header = ProtobufMessageHeader {
header_length: 0,
session_id: self.auth.read().session_id,
steam_id: self.steam_id.as_ref().map(|s| s.steam_id64()).unwrap_or(0),
job_id_source: u64::MAX,
job_id_target: u64::MAX,
target_job_name: Some(method.to_string()),
routing_appid: None,
};
let msg = SteamMessage::new_proto(steam_enums::EMsg::ServiceMethodCallFromClient, header, body);
if let Some(ref mut conn) = self.connection {
conn.send(msg.encode()).await?;
}
Ok(())
}
pub(crate) async fn send_service_method_with_job<T: prost::Message>(&mut self, method: &str, body: &T) -> Result<tokio::sync::oneshot::Receiver<crate::internal::jobs::JobResponse>, SteamError> {
use crate::protocol::{ProtobufMessageHeader, SteamMessage};
info!("[SteamClient] send_service_method_with_job: method={}", method);
let (job_id, response_rx) = self.job_manager.create_job().await;
info!("[SteamClient] send_service_method_with_job: created job_id={}", job_id);
let header = ProtobufMessageHeader {
header_length: 0,
session_id: self.auth.read().session_id,
steam_id: self.steam_id.as_ref().map(|s| s.steam_id64()).unwrap_or(0),
job_id_source: job_id,
job_id_target: u64::MAX,
target_job_name: Some(method.to_string()),
routing_appid: None,
};
let msg = SteamMessage::new_proto(steam_enums::EMsg::ServiceMethodCallFromClient, header, body);
if let Some(ref mut conn) = self.connection {
let encoded = msg.encode();
info!("[SteamClient] send_service_method_with_job: Sending {} bytes to Steam connection", encoded.len());
conn.send(encoded).await?;
info!("[SteamClient] send_service_method_with_job: Sent successfully");
} else {
error!("[SteamClient] send_service_method_with_job: No connection available!");
return Err(SteamError::NotConnected);
}
Ok(response_rx)
}
pub async fn get_chat_history_background(&mut self, friend: SteamID, start_time: u32, count: u32) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let msg = steam_protos::CFriendMessagesGetRecentMessagesRequest {
steamid1: self.steam_id.as_ref().map(|s| s.steam_id64()),
steamid2: Some(friend.steam_id64()),
count: Some(count),
most_recent_conversation: Some(start_time == 0),
rtime32_start_time: if start_time > 0 { Some(start_time) } else { None },
..Default::default()
};
self.send_service_method_background("FriendMessages.GetRecentMessages#1", &msg, crate::client::steam_client::BackgroundTask::OfflineMessages(friend)).await
}
}