Skip to main content

steamkit/steam/handlers/steam_apps/
steam_apps.rs

1//! Interacting with apps and packages: licenses, PICS, depot keys and so on.
2//!
3//! Requests are `async fn`s returning the response callback.
4//! [`SteamApps::pics_get_product_info`] can get several responses, so it
5//! returns a [`ResultSet`].
6
7use std::sync::{Arc, Weak};
8
9use async_trait::async_trait;
10use steamkit_protos::steam::{
11    c_msg_client_pics_product_info_request, CMsgClientCheckAppBetaPassword,
12    CMsgClientGetAppOwnershipTicket, CMsgClientGetDepotDecryptionKey,
13    CMsgClientPicsAccessTokenRequest, CMsgClientPicsChangesSinceRequest,
14    CMsgClientPicsPrivateBetaRequest, CMsgClientPicsProductInfoRequest,
15    CMsgClientRequestFreeLicense,
16};
17
18use crate::base::client_msg::{ClientMsg, ClientMsgProtobuf, IClientMsg};
19use crate::base::generated::steam_language::internal::MsgClientGetLegacyGameKey;
20use crate::base::generated::steam_language::EMsg;
21use crate::base::packet_base::IPacketMsg;
22use crate::error::{SteamError, SteamResult};
23use crate::steam::handlers::client_msg_handler::ClientMsgHandler;
24use crate::steam::steam_client::callback_mgr::callback_msg::CallbackMsg;
25use crate::steam::steam_client::steam_client::SteamClient;
26use crate::types::job_id::ResultSet;
27
28use super::callbacks::{
29    AppOwnershipTicketCallback, CheckAppBetaPasswordCallback, DepotKeyCallback,
30    FreeLicenseCallback, GameConnectTokensCallback, GuestPassListCallback, LegacyGameKeyCallback,
31    LicenseListCallback, PICSChangesCallback, PICSProductInfoCallback, PICSTokensCallback,
32    PrivateBetaCallback, PurchaseResponseCallback, RedeemGuestPassResponseCallback,
33    VACStatusCallback,
34};
35
36/// A PICS request, for [`SteamApps::pics_get_product_info`].
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct PICSRequest {
39    id: u32,
40    access_token: u64,
41}
42
43impl PICSRequest {
44    /// Creates a product info request for an app or package id and its access token.
45    pub fn new(id: u32, access_token: u64) -> Self {
46        Self { id, access_token }
47    }
48
49    /// The id of the app or package being requested.
50    pub fn id(&self) -> u32 {
51        self.id
52    }
53
54    /// Sets the id of the app or package being requested.
55    pub fn set_id(&mut self, id: u32) {
56        self.id = id;
57    }
58
59    /// The access token associated with the request.
60    pub fn access_token(&self) -> u64 {
61        self.access_token
62    }
63
64    /// Sets the access token associated with the request.
65    pub fn set_access_token(&mut self, access_token: u64) {
66        self.access_token = access_token;
67    }
68}
69
70/// Interacts with apps and packages on the Steam network.
71pub struct SteamApps {
72    client: Weak<SteamClient>,
73}
74
75impl std::fmt::Debug for SteamApps {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("SteamApps").finish()
78    }
79}
80
81impl SteamApps {
82    /// Creates the handler and stores a weak back-reference to the client.
83    pub fn new(client: &Arc<SteamClient>) -> Self {
84        Self {
85            client: Arc::downgrade(client),
86        }
87    }
88
89    /// Upgrades the weak client reference.
90    fn client(&self) -> SteamResult<Arc<SteamClient>> {
91        self.client
92            .upgrade()
93            .ok_or_else(|| SteamError::Connection("SteamClient has been dropped".to_string()))
94    }
95
96    /// Requests an app ownership ticket for the specified app id.
97    ///
98    /// Results are returned in an [`AppOwnershipTicketCallback`].
99    pub async fn get_app_ownership_ticket(
100        &self,
101        appid: u32,
102    ) -> SteamResult<AppOwnershipTicketCallback> {
103        let client = self.client()?;
104
105        let mut request = ClientMsgProtobuf::<CMsgClientGetAppOwnershipTicket>::new(
106            EMsg::ClientGetAppOwnershipTicket,
107        );
108
109        request.body_mut().app_id = Some(appid);
110
111        let job_id = client.get_next_job_id();
112        request.set_source_job_id(job_id);
113
114        let job = client.start_job::<AppOwnershipTicketCallback>(job_id);
115
116        client.send(&request)?;
117
118        job.wait().await
119    }
120
121    /// Requests the depot decryption key for a specified depot id.
122    ///
123    /// Results are returned in a [`DepotKeyCallback`].
124    pub async fn get_depot_decryption_key(
125        &self,
126        depot_id: u32,
127        app_id: u32,
128    ) -> SteamResult<DepotKeyCallback> {
129        let client = self.client()?;
130
131        let mut request = ClientMsgProtobuf::<CMsgClientGetDepotDecryptionKey>::new(
132            EMsg::ClientGetDepotDecryptionKey,
133        );
134
135        request.body_mut().depot_id = Some(depot_id);
136        request.body_mut().app_id = Some(app_id);
137
138        let job_id = client.get_next_job_id();
139        request.set_source_job_id(job_id);
140
141        let job = client.start_job::<DepotKeyCallback>(job_id);
142
143        client.send(&request)?;
144
145        job.wait().await
146    }
147
148    /// Requests PICS access tokens for an app or a package.
149    ///
150    /// Results are returned in a [`PICSTokensCallback`].
151    pub async fn pics_get_access_tokens(
152        &self,
153        app: Option<u32>,
154        package: Option<u32>,
155    ) -> SteamResult<PICSTokensCallback> {
156        let apps: Vec<u32> = app.into_iter().collect();
157        let packages: Vec<u32> = package.into_iter().collect();
158
159        self.pics_get_access_tokens_for_ids(&apps, &packages).await
160    }
161
162    /// Requests PICS access tokens for a list of app ids and package ids.
163    ///
164    /// Results are returned in a [`PICSTokensCallback`].
165    pub async fn pics_get_access_tokens_for_ids(
166        &self,
167        app_ids: &[u32],
168        package_ids: &[u32],
169    ) -> SteamResult<PICSTokensCallback> {
170        let client = self.client()?;
171
172        let mut request = ClientMsgProtobuf::<CMsgClientPicsAccessTokenRequest>::new(
173            EMsg::ClientPICSAccessTokenRequest,
174        );
175
176        request.body_mut().appids = app_ids.to_vec();
177        request.body_mut().packageids = package_ids.to_vec();
178
179        let job_id = client.get_next_job_id();
180        request.set_source_job_id(job_id);
181
182        let job = client.start_job::<PICSTokensCallback>(job_id);
183
184        client.send(&request)?;
185
186        job.wait().await
187    }
188
189    /// Requests the changes for apps and packages since a given change number.
190    ///
191    /// Results are returned in a [`PICSChangesCallback`].
192    pub async fn pics_get_changes_since(
193        &self,
194        last_change_number: u32,
195        send_app_changelist: bool,
196        send_package_changelist: bool,
197    ) -> SteamResult<PICSChangesCallback> {
198        let client = self.client()?;
199
200        let mut request = ClientMsgProtobuf::<CMsgClientPicsChangesSinceRequest>::new(
201            EMsg::ClientPICSChangesSinceRequest,
202        );
203
204        request.body_mut().since_change_number = Some(last_change_number);
205        request.body_mut().send_app_info_changes = Some(send_app_changelist);
206        request.body_mut().send_package_info_changes = Some(send_package_changelist);
207
208        let job_id = client.get_next_job_id();
209        request.set_source_job_id(job_id);
210
211        let job = client.start_job::<PICSChangesCallback>(job_id);
212
213        client.send(&request)?;
214
215        job.wait().await
216    }
217
218    /// Requests the product information of an app or a package.
219    ///
220    /// Results are returned in a [`ResultSet`] of [`PICSProductInfoCallback`].
221    pub async fn pics_get_product_info(
222        &self,
223        app: Option<&PICSRequest>,
224        package: Option<&PICSRequest>,
225        meta_data_only: bool,
226    ) -> SteamResult<ResultSet<PICSProductInfoCallback>> {
227        let apps: Vec<PICSRequest> = app.into_iter().copied().collect();
228        let packages: Vec<PICSRequest> = package.into_iter().copied().collect();
229
230        self.pics_get_product_info_for_ids(&apps, &packages, meta_data_only)
231            .await
232    }
233
234    /// Requests the product information of a list of apps or packages.
235    ///
236    /// Results are returned in a [`ResultSet`] of [`PICSProductInfoCallback`].
237    pub async fn pics_get_product_info_for_ids(
238        &self,
239        apps: &[PICSRequest],
240        packages: &[PICSRequest],
241        meta_data_only: bool,
242    ) -> SteamResult<ResultSet<PICSProductInfoCallback>> {
243        let client = self.client()?;
244
245        let mut request = ClientMsgProtobuf::<CMsgClientPicsProductInfoRequest>::new(
246            EMsg::ClientPICSProductInfoRequest,
247        );
248
249        {
250            let body = request.body_mut();
251
252            for app in apps {
253                body.apps
254                    .push(c_msg_client_pics_product_info_request::AppInfo {
255                        appid: Some(app.id()),
256                        access_token: Some(app.access_token()),
257                        only_public_obsolete: Some(false),
258                    });
259            }
260
261            for package in packages {
262                body.packages
263                    .push(c_msg_client_pics_product_info_request::PackageInfo {
264                        packageid: Some(package.id()),
265                        access_token: Some(package.access_token()),
266                    });
267            }
268
269            body.meta_data_only = Some(meta_data_only);
270        }
271
272        let job_id = client.get_next_job_id();
273        request.set_source_job_id(job_id);
274
275        let handle = client
276            .job_manager()
277            .start_job_multiple::<PICSProductInfoCallback>(job_id, |callback| {
278                !callback.response_pending()
279            });
280
281        if !client.is_connected() {
282            client.job_manager().fail_job(job_id);
283        }
284
285        client.send(&request)?;
286
287        let result = handle.wait().await?;
288
289        Ok(result)
290    }
291
292    /// Requests a free license for the given app id.
293    ///
294    /// Results are returned in a [`FreeLicenseCallback`].
295    pub async fn request_free_license(&self, app: u32) -> SteamResult<FreeLicenseCallback> {
296        self.request_free_licenses(&[app]).await
297    }
298
299    /// Requests a free license for the given app ids.
300    ///
301    /// Results are returned in a [`FreeLicenseCallback`].
302    pub async fn request_free_licenses(&self, apps: &[u32]) -> SteamResult<FreeLicenseCallback> {
303        let client = self.client()?;
304
305        let mut request =
306            ClientMsgProtobuf::<CMsgClientRequestFreeLicense>::new(EMsg::ClientRequestFreeLicense);
307
308        request.body_mut().appids = apps.to_vec();
309
310        let job_id = client.get_next_job_id();
311        request.set_source_job_id(job_id);
312
313        let job = client.start_job::<FreeLicenseCallback>(job_id);
314
315        client.send(&request)?;
316
317        job.wait().await
318    }
319
320    /// Submits a beta password for a given app to retrieve its betas and encryption keys.
321    ///
322    /// Results are returned in a [`CheckAppBetaPasswordCallback`].
323    pub async fn check_app_beta_password(
324        &self,
325        app: u32,
326        password: &str,
327    ) -> SteamResult<CheckAppBetaPasswordCallback> {
328        let client = self.client()?;
329
330        let mut request = ClientMsgProtobuf::<CMsgClientCheckAppBetaPassword>::new(
331            EMsg::ClientCheckAppBetaPassword,
332        );
333
334        request.body_mut().app_id = Some(app);
335        request.body_mut().betapassword = Some(password.to_string());
336
337        let job_id = client.get_next_job_id();
338        request.set_source_job_id(job_id);
339
340        let job = client.start_job::<CheckAppBetaPasswordCallback>(job_id);
341
342        client.send(&request)?;
343
344        job.wait().await
345    }
346
347    /// Requests the legacy CD game keys of the requested app id.
348    ///
349    /// Results are returned in a [`LegacyGameKeyCallback`].
350    pub async fn get_legacy_game_key(&self, appid: u32) -> SteamResult<LegacyGameKeyCallback> {
351        let client = self.client()?;
352
353        let mut request = ClientMsg::<MsgClientGetLegacyGameKey>::new(EMsg::ClientGetLegacyGameKey);
354
355        request.body_mut().app_id = appid;
356
357        let job_id = client.get_next_job_id();
358        request.set_source_job_id(job_id);
359
360        let job = client.start_job::<LegacyGameKeyCallback>(job_id);
361
362        client.send(&request)?;
363
364        job.wait().await
365    }
366
367    /// Requests the private beta section of a given app.
368    ///
369    /// Results are returned in a [`PrivateBetaCallback`].
370    pub async fn pics_get_private_beta(
371        &self,
372        app: u32,
373        access_token: u64,
374        branch: &str,
375        branch_password_hash: &[u8],
376    ) -> SteamResult<PrivateBetaCallback> {
377        let client = self.client()?;
378
379        let mut request = ClientMsgProtobuf::<CMsgClientPicsPrivateBetaRequest>::new(
380            EMsg::ClientPICSPrivateBetaRequest,
381        );
382
383        request.body_mut().appid = Some(app);
384        request.body_mut().access_token = Some(access_token);
385        request.body_mut().beta_name = Some(branch.to_string());
386        request.body_mut().password_hash = Some(branch_password_hash.to_vec());
387
388        let job_id = client.get_next_job_id();
389        request.set_source_job_id(job_id);
390
391        let job = client.start_job::<PrivateBetaCallback>(job_id);
392
393        client.send(&request)?;
394
395        job.wait().await
396    }
397}
398
399#[async_trait]
400impl ClientMsgHandler for SteamApps {
401    async fn handle_msg(
402        &self,
403        client: &SteamClient,
404        packet_msg: &dyn IPacketMsg,
405    ) -> SteamResult<()> {
406        let callback: Option<Box<dyn CallbackMsg>> = match packet_msg.msg_type() {
407            EMsg::ClientLicenseList => {
408                Some(Box::new(LicenseListCallback::from_packet(packet_msg)?))
409            }
410            EMsg::ClientRequestFreeLicenseResponse => {
411                Some(Box::new(FreeLicenseCallback::from_packet(packet_msg)?))
412            }
413            EMsg::ClientPurchaseResponse => {
414                Some(Box::new(PurchaseResponseCallback::from_packet(packet_msg)?))
415            }
416            EMsg::ClientRedeemGuestPassResponse => Some(Box::new(
417                RedeemGuestPassResponseCallback::from_packet(packet_msg)?,
418            )),
419            EMsg::ClientGameConnectTokens => Some(Box::new(
420                GameConnectTokensCallback::from_packet(packet_msg)?,
421            )),
422            EMsg::ClientVACBanStatus => Some(Box::new(VACStatusCallback::from_packet(packet_msg)?)),
423            EMsg::ClientGetAppOwnershipTicketResponse => Some(Box::new(
424                AppOwnershipTicketCallback::from_packet(packet_msg)?,
425            )),
426            EMsg::ClientGetDepotDecryptionKeyResponse => {
427                Some(Box::new(DepotKeyCallback::from_packet(packet_msg)?))
428            }
429            EMsg::ClientGetLegacyGameKeyResponse => {
430                Some(Box::new(LegacyGameKeyCallback::from_packet(packet_msg)?))
431            }
432            EMsg::ClientPICSAccessTokenResponse => {
433                Some(Box::new(PICSTokensCallback::from_packet(packet_msg)?))
434            }
435            EMsg::ClientPICSChangesSinceResponse => {
436                Some(Box::new(PICSChangesCallback::from_packet(packet_msg)?))
437            }
438            EMsg::ClientPICSProductInfoResponse => {
439                Some(Box::new(PICSProductInfoCallback::from_packet(packet_msg)?))
440            }
441            EMsg::ClientUpdateGuestPassesList => {
442                Some(Box::new(GuestPassListCallback::from_packet(packet_msg)?))
443            }
444            EMsg::ClientCheckAppBetaPasswordResponse => Some(Box::new(
445                CheckAppBetaPasswordCallback::from_packet(packet_msg)?,
446            )),
447            EMsg::ClientPICSPrivateBetaResponse => {
448                Some(Box::new(PrivateBetaCallback::from_packet(packet_msg)?))
449            }
450            // Not a message we handle.
451            _ => None,
452        };
453
454        if let Some(callback) = callback {
455            client.post_callback(callback);
456        }
457
458        Ok(())
459    }
460}