1use prost::Message;
6use steam_enums::EChatEntryType;
7use steamid::SteamID;
8use tracing::{error, info};
9
10use crate::{error::SteamError, SteamClient};
11
12#[derive(Debug, Clone)]
14pub struct ChatMessage {
15 pub message: String,
17 pub chat_entry_type: EChatEntryType,
19 pub contains_bbcode: bool,
21}
22
23impl Default for ChatMessage {
24 fn default() -> Self {
25 Self { message: String::new(), chat_entry_type: EChatEntryType::ChatMsg, contains_bbcode: true }
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct SendMessageResult {
32 pub modified_message: String,
34 pub server_timestamp: u32,
36 pub ordinal: u32,
38}
39
40#[derive(Debug, Clone)]
42pub struct HistoryMessage {
43 pub sender: SteamID,
45 pub timestamp: u32,
47 pub ordinal: u32,
49 pub message: String,
51 pub unread: bool,
53}
54
55#[derive(Debug, Clone)]
57pub struct FriendMessageSession {
58 pub friend: SteamID,
60 pub time_last_message: u32,
62 pub time_last_view: u32,
64 pub unread_count: u32,
66}
67
68impl SteamClient {
69 pub async fn send_friend_message(&mut self, friend: SteamID, message: &str) -> Result<SendMessageResult, SteamError> {
80 self.send_friend_message_with_options(friend, message, EChatEntryType::ChatMsg, true).await
81 }
82
83 pub fn send_friend_message_async(&mut self, friend: SteamID, message: &str) -> Result<tokio::sync::oneshot::Receiver<Result<SendMessageResult, SteamError>>, SteamError> {
88 info!("[SteamClient] send_friend_message_async called");
89 info!("[SteamClient] Target friend: {}", friend);
90 info!("[SteamClient] Message length: {}", message.len());
91 info!("[SteamClient] is_logged_in: {}", self.is_logged_in());
92 info!("[SteamClient] steam_id: {:?}", self.steam_id);
93
94 if !self.is_logged_in() {
95 error!("[SteamClient] NOT LOGGED IN - returning NotLoggedOn error");
96 return Err(SteamError::NotLoggedOn);
97 }
98
99 let (tx, rx) = tokio::sync::oneshot::channel();
100
101 let queued = crate::client::steam_client::QueuedMessage {
102 friend,
103 message: message.to_string(),
104 entry_type: EChatEntryType::ChatMsg,
105 contains_bbcode: true,
106 respond_to: tx,
107 };
108
109 match self.chat.tx.send(queued) {
110 Ok(_) => {
111 info!("[SteamClient] Message queued to chat_queue_tx successfully for {}", friend);
112 }
113 Err(e) => {
114 error!("[SteamClient] Failed to queue message to chat_queue_tx: {:?}", e);
115 return Err(SteamError::Other("Failed to queue message".to_string()));
116 }
117 }
118
119 Ok(rx)
120 }
121
122 pub async fn send_friend_message_with_options(&mut self, friend: SteamID, message: &str, entry_type: EChatEntryType, contains_bbcode: bool) -> Result<SendMessageResult, SteamError> {
130 if !self.is_logged_in() {
131 return Err(SteamError::NotLoggedOn);
132 }
133
134 let (tx, rx) = tokio::sync::oneshot::channel();
135
136 let queued = crate::client::steam_client::QueuedMessage { friend, message: message.to_string(), entry_type, contains_bbcode, respond_to: tx };
137
138 self.chat.tx.send(queued).map_err(|_| SteamError::Other("Failed to queue message".to_string()))?;
139
140 rx.await.map_err(|_| SteamError::Other("Response channel closed".to_string()))?
141 }
142
143 pub async fn send_friend_typing(&mut self, friend: SteamID) -> Result<(), SteamError> {
148 self.send_friend_message_with_options(friend, "", EChatEntryType::Typing, false).await?;
149 Ok(())
150 }
151
152 pub async fn get_active_friend_sessions(&mut self) -> Result<Vec<FriendMessageSession>, SteamError> {
156 if !self.is_logged_in() {
157 return Err(SteamError::NotLoggedOn);
158 }
159
160 let msg = steam_protos::CFriendMessagesGetActiveMessageSessionsRequest::default();
161 let rx = self.send_service_method_with_job("FriendMessages.GetActiveMessageSessions#1", &msg).await?;
162
163 let job_response = rx.await.map_err(|_| SteamError::ResponseTimeout)?;
165
166 let body = match job_response {
167 crate::internal::jobs::JobResponse::Success(bytes) => bytes,
168 crate::internal::jobs::JobResponse::Timeout => return Err(SteamError::ResponseTimeout),
169 crate::internal::jobs::JobResponse::Error(msg) => return Err(SteamError::ProtocolError(msg)),
170 };
171
172 let response = steam_protos::CFriendMessagesGetActiveMessageSessionsResponse::decode(&body[..]).map_err(|_| SteamError::DeserializationFailed)?;
173
174 let sessions = response
175 .message_sessions
176 .into_iter()
177 .map(|s| FriendMessageSession {
178 friend: SteamID::from_steam_id64(s.accountid_friend.unwrap_or(0) as u64),
179 time_last_message: s.last_message.unwrap_or(0),
180 time_last_view: s.last_view.unwrap_or(0),
181 unread_count: s.unread_message_count.unwrap_or(0),
182 })
183 .collect();
184
185 Ok(sessions)
186 }
187
188 pub async fn ack_friend_message(&mut self, friend: SteamID, timestamp: u32) -> Result<(), SteamError> {
194 if !self.is_logged_in() {
195 return Err(SteamError::NotLoggedOn);
196 }
197
198 let msg = steam_protos::CFriendMessagesAckMessageNotification { steamid_partner: Some(friend.steam_id64()), timestamp: Some(timestamp) };
199
200 self.social.write().chat_last_view.insert(friend, timestamp);
202
203 self.send_service_method("FriendMessages.AckMessage#1", &msg).await
204 }
205
206 pub async fn get_chat_history(&mut self, friend: SteamID, start_time: u32, count: u32) -> Result<Vec<HistoryMessage>, SteamError> {
217 if !self.is_logged_in() {
218 return Err(SteamError::NotLoggedOn);
219 }
220
221 let msg = steam_protos::CFriendMessagesGetRecentMessagesRequest {
222 steamid1: self.steam_id.as_ref().map(|s| s.steam_id64()),
223 steamid2: Some(friend.steam_id64()),
224 count: Some(count),
225 most_recent_conversation: Some(start_time == 0),
226 rtime32_start_time: if start_time > 0 { Some(start_time) } else { None },
227 ..Default::default()
228 };
229
230 let rx = self.send_service_method_with_job("FriendMessages.GetRecentMessages#1", &msg).await?;
231
232 let job_response = rx.await.map_err(|_| SteamError::ResponseTimeout)?;
234
235 let body = match job_response {
236 crate::internal::jobs::JobResponse::Success(bytes) => bytes,
237 crate::internal::jobs::JobResponse::Timeout => return Err(SteamError::ResponseTimeout),
238 crate::internal::jobs::JobResponse::Error(msg) => return Err(SteamError::ProtocolError(msg)),
239 };
240
241 let response = steam_protos::CFriendMessagesGetRecentMessagesResponse::decode(&body[..]).map_err(|_| SteamError::DeserializationFailed)?;
242
243 let last_view_timestamp = self.social.read().chat_last_view.get(&friend).copied().unwrap_or(0);
244
245 let messages = response
246 .messages
247 .into_iter()
248 .map(|m| HistoryMessage {
249 sender: SteamID::from_steam_id64(m.accountid.unwrap_or(0) as u64),
250 timestamp: m.timestamp.unwrap_or(0),
251 ordinal: m.ordinal.unwrap_or(0),
252 message: m.message.unwrap_or_default(),
253 unread: m.timestamp.unwrap_or(0) > last_view_timestamp,
254 })
255 .collect();
256
257 Ok(messages)
258 }
259
260 pub async fn get_recent_chat_history(&mut self, friend: SteamID) -> Result<Vec<HistoryMessage>, SteamError> {
268 self.get_chat_history(friend, 0, 20).await
269 }
270
271 pub(crate) async fn send_service_method<T: prost::Message>(&mut self, method: &str, body: &T) -> Result<(), SteamError> {
273 use crate::protocol::{ProtobufMessageHeader, SteamMessage};
274
275 let header = ProtobufMessageHeader {
276 header_length: 0,
277 session_id: self.auth.read().session_id,
278 steam_id: self.steam_id.as_ref().map(|s| s.steam_id64()).unwrap_or(0),
279 job_id_source: u64::MAX,
280 job_id_target: u64::MAX,
281 target_job_name: Some(method.to_string()),
282 routing_appid: None,
283 };
284
285 let msg = SteamMessage::new_proto(steam_enums::EMsg::ServiceMethodCallFromClient, header, body);
286
287 if let Some(ref mut conn) = self.connection {
288 conn.send(msg.encode()).await?;
289 }
290
291 Ok(())
292 }
293
294 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> {
296 use crate::protocol::{ProtobufMessageHeader, SteamMessage};
297
298 info!("[SteamClient] send_service_method_with_job: method={}", method);
299
300 let (job_id, response_rx) = self.job_manager.create_job().await;
302 info!("[SteamClient] send_service_method_with_job: created job_id={}", job_id);
303
304 let header = ProtobufMessageHeader {
305 header_length: 0,
306 session_id: self.auth.read().session_id,
307 steam_id: self.steam_id.as_ref().map(|s| s.steam_id64()).unwrap_or(0),
308 job_id_source: job_id,
309 job_id_target: u64::MAX,
310 target_job_name: Some(method.to_string()),
311 routing_appid: None,
312 };
313
314 let msg = SteamMessage::new_proto(steam_enums::EMsg::ServiceMethodCallFromClient, header, body);
315
316 if let Some(ref mut conn) = self.connection {
317 let encoded = msg.encode();
318 info!("[SteamClient] send_service_method_with_job: Sending {} bytes to Steam connection", encoded.len());
319 conn.send(encoded).await?;
320 info!("[SteamClient] send_service_method_with_job: Sent successfully");
321 } else {
322 error!("[SteamClient] send_service_method_with_job: No connection available!");
323 return Err(SteamError::NotConnected);
324 }
325
326 Ok(response_rx)
327 }
328
329 pub async fn get_chat_history_background(&mut self, friend: SteamID, start_time: u32, count: u32) -> Result<(), SteamError> {
334 if !self.is_logged_in() {
335 return Err(SteamError::NotLoggedOn);
336 }
337
338 let msg = steam_protos::CFriendMessagesGetRecentMessagesRequest {
339 steamid1: self.steam_id.as_ref().map(|s| s.steam_id64()),
340 steamid2: Some(friend.steam_id64()),
341 count: Some(count),
342 most_recent_conversation: Some(start_time == 0),
343 rtime32_start_time: if start_time > 0 { Some(start_time) } else { None },
344 ..Default::default()
345 };
346
347 self.send_service_method_background("FriendMessages.GetRecentMessages#1", &msg, crate::client::steam_client::BackgroundTask::OfflineMessages(friend)).await
348 }
349}