1use steam_totp::{generate_confirmation_key, generate_device_id, Secret};
4
5use crate::{client::SteamUser, endpoint::steam_endpoint, error::SteamUserError, types::Confirmation};
6
7impl SteamUser {
8 #[steam_endpoint(GET, host = Community, path = "/mobileconf/getlist", kind = Auth)]
43 pub async fn get_confirmations(&self, identity_secret: &str, tag: Option<&str>) -> Result<Vec<Confirmation>, SteamUserError> {
44 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
45 let device_id = generate_device_id(steam_id, None);
46
47 let time = i64::try_from(
48 std::time::SystemTime::now()
49 .duration_since(std::time::UNIX_EPOCH)?
50 .as_secs(),
51 )
52 .unwrap_or(0);
53
54 let tag = tag.unwrap_or("conf");
55 let secret = Secret::from_string(identity_secret)?;
56 let key = generate_confirmation_key(&secret, time, tag)?;
57
58 let time_str = time.to_string();
59 let steam_id_str = steam_id.steam_id64().to_string();
60
61 let params = [("p", device_id.as_str()), ("a", steam_id_str.as_str()), ("k", key.as_str()), ("t", time_str.as_str()), ("m", "react"), ("tag", tag)];
62
63 let response: serde_json::Value = self.get_path("/mobileconf/getlist").query(¶ms).send().await?.json().await?;
66
67 if !response.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
68 let message = response.get("message").or_else(|| response.get("detail")).and_then(|v| v.as_str()).unwrap_or("Failed to get confirmation list").to_string();
69
70 if response.get("needauth").and_then(|v| v.as_bool()).unwrap_or(false) {
71 return Err(SteamUserError::SessionExpired);
72 }
73
74 return Err(SteamUserError::SteamError(message));
75 }
76
77 let confs = response
78 .get("conf")
79 .and_then(|v| v.as_array())
80 .map(|arr| {
81 arr.iter()
82 .filter_map(|v| match Confirmation::from_api(v) {
83 Ok(c) => Some(c),
84 Err(e) => {
85 tracing::warn!(error = %e, "skipping malformed confirmation entry");
86 None
87 }
88 })
89 .collect()
90 })
91 .unwrap_or_default();
92
93 Ok(confs)
94 }
95
96 #[tracing::instrument(skip(self, identity_secret), fields(accept))]
119 pub async fn respond_to_confirmation(&self, confirmation: &Confirmation, identity_secret: &str, accept: bool) -> Result<(), SteamUserError> {
120 self.respond_to_confirmations(std::slice::from_ref(confirmation), identity_secret, accept).await
121 }
122
123 #[steam_endpoint(POST, host = Community, path = "/mobileconf/multiajaxop", kind = Auth)]
147 pub async fn respond_to_confirmations(&self, confirmations: &[Confirmation], identity_secret: &str, accept: bool) -> Result<(), SteamUserError> {
148 if confirmations.is_empty() {
149 return Ok(());
150 }
151
152 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
153 let device_id = generate_device_id(steam_id, None);
154
155 let time = i64::try_from(
156 std::time::SystemTime::now()
157 .duration_since(std::time::UNIX_EPOCH)?
158 .as_secs(),
159 )
160 .unwrap_or(0);
161
162 let tag = if accept { "allow" } else { "cancel" };
163 let secret = Secret::from_string(identity_secret)?;
164 let key = generate_confirmation_key(&secret, time, tag)?;
165
166 let op = if accept { "allow" } else { "cancel" };
167
168 let time_str = time.to_string();
169 let steam_id_str = steam_id.steam_id64().to_string();
170
171 let params = [("p", device_id.as_str()), ("a", steam_id_str.as_str()), ("k", key.as_str()), ("t", time_str.as_str()), ("m", "react"), ("tag", tag), ("op", op)];
172
173 let mut form_params = Vec::new();
174 for (k, v) in params.iter() {
175 form_params.push((*k, *v));
176 }
177
178 for conf in confirmations {
180 form_params.push(("cid[]", conf.id.as_str()));
181 form_params.push(("ck[]", conf.key.as_str()));
182 }
183
184 let response: serde_json::Value = self.post_path("/mobileconf/multiajaxop").form(&form_params).send().await?.json().await?;
185
186 if response.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
187 Ok(())
188 } else {
189 let message = response.get("message").and_then(|v| v.as_str()).unwrap_or("Could not act on confirmation").to_string();
190 Err(SteamUserError::SteamError(message))
191 }
192 }
193
194 #[steam_endpoint(GET, host = Community, path = "/mobileconf/detailspage/{confirmation_id}", kind = Auth)]
209 pub async fn get_confirmation_offer_id(&self, confirmation: &Confirmation, identity_secret: &str) -> Result<Option<String>, SteamUserError> {
210 if let Some(offer_id) = confirmation.offer_id() {
211 return Ok(Some(offer_id.to_string()));
212 }
213
214 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
215 let device_id = generate_device_id(steam_id, None);
216
217 let time = i64::try_from(
218 std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)?
220 .as_secs(),
221 )
222 .unwrap_or(0);
223
224 let secret = Secret::from_string(identity_secret)?;
225 let key = generate_confirmation_key(&secret, time, "details")?;
226
227 let time_str = time.to_string();
228 let steam_id_str = steam_id.steam_id64().to_string();
229
230 let params = [("p", device_id.as_str()), ("a", steam_id_str.as_str()), ("k", key.as_str()), ("t", time_str.as_str()), ("m", "react"), ("tag", "details")];
231
232 let response = self.get_path(format!("/mobileconf/detailspage/{}", confirmation.id)).query(¶ms).send().await?.text().await?;
233
234 use scraper::{Html, Selector};
236 let document = Html::parse_document(&response);
237 let selector = Selector::parse(".tradeoffer").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
238
239 if let Some(element) = document.select(&selector).next() {
240 if let Some(id_attr) = element.value().attr("id") {
241 if let Some(parts) = id_attr.split('_').nth(1) {
243 return Ok(Some(parts.to_string()));
244 }
245 }
246 }
247
248 Ok(None)
249 }
250
251 #[tracing::instrument(skip(self, identity_secret), fields(object_id = object_id, accept = accept))]
271 pub async fn perform_confirmation_action(&self, identity_secret: &str, object_id: u64, accept: bool) -> Result<(), SteamUserError> {
272 let tag = if accept { "list" } else { "conf" };
273 let confirmations = self.get_confirmations(identity_secret, Some(tag)).await?;
274
275 let object_id_str = object_id.to_string();
276 let confirmation = confirmations.iter().find(|c| c.creator == object_id_str).ok_or(SteamUserError::ConfirmationNotFound(object_id))?;
277
278 self.respond_to_confirmation(confirmation, identity_secret, accept).await
279 }
280
281 #[tracing::instrument(skip(self, identity_secret), fields(object_id = object_id))]
287 pub async fn accept_confirmation_for_object(&self, identity_secret: &str, object_id: u64) -> Result<(), SteamUserError> {
288 self.perform_confirmation_action(identity_secret, object_id, true).await
289 }
290
291 #[tracing::instrument(skip(self, identity_secret), fields(object_id = object_id))]
297 pub async fn deny_confirmation_for_object(&self, identity_secret: &str, object_id: u64) -> Result<(), SteamUserError> {
298 self.perform_confirmation_action(identity_secret, object_id, false).await
299 }
300}