From f04292e9f13c833d17c81446e9e2200ae6939e64 Mon Sep 17 00:00:00 2001 From: Artemis Tosini Date: Thu, 5 Sep 2024 22:32:20 +0000 Subject: [PATCH] lib: Start work on SteamClient struct --- Cargo.lock | 58 +++++++++ lib/Cargo.toml | 2 + lib/src/client.rs | 50 ++++++++ lib/src/connection.rs | 9 +- lib/src/error.rs | 13 ++ lib/src/lib.rs | 2 + lib/src/state/apps.rs | 273 ++++++++++++++++++++++++++++++++++++++++++ lib/src/state/mod.rs | 1 + 8 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 lib/src/client.rs create mode 100644 lib/src/state/apps.rs create mode 100644 lib/src/state/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8de6ba9..9bd5ca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,6 +896,27 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "object" version = "0.32.2" @@ -1042,6 +1063,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1700,6 +1730,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1886,6 +1933,7 @@ version = "0.1.0" dependencies = [ "async-tungstenite", "base64", + "bitflags", "color-eyre", "dialoguer", "env_logger", @@ -1894,6 +1942,7 @@ dependencies = [ "hex", "keyvalues-serde", "log", + "num_enum", "protobuf", "qrcode", "rand", @@ -2144,6 +2193,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f5aa948..21beb7f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -5,11 +5,13 @@ version.workspace = true [dependencies] async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] } +bitflags = "2.6.0" color-eyre.workspace = true flate2 = "1.0.33" futures = "0.3.30" keyvalues-serde.workspace = true log.workspace = true +num_enum = "0.7.3" protobuf.workspace = true rand = "0.8.5" reqwest = { version = "0.12", features = ["rustls-tls-native-roots"], default-features = false} diff --git a/lib/src/client.rs b/lib/src/client.rs new file mode 100644 index 0000000..8e003a0 --- /dev/null +++ b/lib/src/client.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use tokio::sync::watch; + +use crate::{ + connection::{CMContext, CMSession}, + state::apps::License, + ClientError, +}; + +struct SteamClientInner { + /// The currently active socket to a Connection Manager + /// TODO: Support recreation when one fails + pub session: CMSession, + + /// Licenses owned by current user, as told periodically by Valve. + /// Use an Arc in the receiver so that the sender doesn't block if + /// someone keeps a long-lived reference to the licenses + pub licenses: watch::Receiver>>, +} + +/// Higher-level Steam client. Unlike [crate::connection::CMSession], +/// SteamClient records state sent from the server, like owned games +/// and email address. +#[derive(Clone)] +pub struct SteamClient { + /// Inner content. + inner: Arc, +} + +impl SteamClient { + pub async fn connect(servers: &[&str]) -> Result<(Self, CMContext), ClientError> { + let (session, context) = CMSession::connect(servers[0]).await?; + + let inner = Arc::new(SteamClientInner { + session, + licenses: todo!(), + }); + + Ok((Self { inner }, context)) + } + + pub fn licenses(&self) -> Arc> { + self.inner.licenses.borrow().clone() + } + + pub fn session(&self) -> CMSession { + self.inner.session.clone() + } +} diff --git a/lib/src/connection.rs b/lib/src/connection.rs index 70bad47..05571e0 100644 --- a/lib/src/connection.rs +++ b/lib/src/connection.rs @@ -95,7 +95,7 @@ impl CMContext { let raw_messages = CMRawProtoBufMessage::try_parse_multi(&message_data)?; - let mut session = self.session.lock().expect("Lock was poisoned"); + let mut session = self.session.lock()?; for message in raw_messages.into_iter() { log::trace!("Got message: {:?}", message); @@ -164,7 +164,8 @@ impl CMContext { cx: &mut std::task::Context<'_>, ) -> Result<(), ClientError> { { - let mut session = self.session.lock().expect("Lock was poisoned"); + // If the lock was poisoned we're never going to make any forward progress anyway + let mut session = self.session.lock()?; session.send_waker = Some(cx.waker().clone()); } @@ -289,7 +290,7 @@ impl CMSession { action: EMsg, body: T, ) -> Result<(), ClientError> { - let mut inner = self.inner.lock().expect("Lock was poisoned"); + let mut inner = self.inner.lock()?; log::trace!("Sending notification of type {:?}", action); @@ -363,7 +364,7 @@ impl Future for CallServiceMethod<'_ fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { let session_arc = self.session.inner.clone(); - let mut session = session_arc.lock().expect("Lock was poisoned"); + let mut session = session_arc.lock()?; // We only have to send the message once, use jobid for that flag if self.jobid.is_none() { diff --git a/lib/src/error.rs b/lib/src/error.rs index 78cf741..e59bd4f 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -1,5 +1,8 @@ +use num_enum::TryFromPrimitiveError; use snafu::prelude::*; +use crate::state::apps::{ELicenseType, EPaymentMethod}; + #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] pub enum ClientError { @@ -62,6 +65,16 @@ pub enum ClientError { BadResponseAction { actual: vapore_proto::enums_clientserver::EMsg, }, + + #[snafu(display("Unknown value while parsing enum EPaymentMethod"))] + TryFromEPaymentMethod { + source: TryFromPrimitiveError, + }, + + #[snafu(display("Unknown value while parsing enum ELicenseType"))] + TryFromELicenseType { + source: TryFromPrimitiveError, + }, } impl From> for ClientError { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1501238..2895746 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,8 @@ +pub mod client; pub mod connection; pub mod error; pub mod message; pub mod selection; +pub mod state; pub use error::ClientError; diff --git a/lib/src/state/apps.rs b/lib/src/state/apps.rs new file mode 100644 index 0000000..73234cd --- /dev/null +++ b/lib/src/state/apps.rs @@ -0,0 +1,273 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use num_enum::TryFromPrimitive; +use snafu::prelude::*; +use vapore_proto::steammessages_clientserver::cmsg_client_license_list; + +use crate::{ + error::{TryFromELicenseTypeSnafu, TryFromEPaymentMethodSnafu}, + ClientError, +}; + +#[repr(u32)] +#[derive(Debug, Copy, Clone, TryFromPrimitive)] +pub enum EPaymentMethod { + None = 0, + ActivationCode = 1, + CreditCard = 2, + Giropay = 3, + PayPal = 4, + Ideal = 5, + PaySafeCard = 6, + Sofort = 7, + GuestPass = 8, + WebMoney = 9, + MoneyBookers = 10, + AliPay = 11, + Yandex = 12, + Kiosk = 13, + Qiwi = 14, + GameStop = 15, + HardwarePromo = 16, + MoPay = 17, + BoletoBancario = 18, + BoaCompraGold = 19, + BancoDoBrasilOnline = 20, + ItauOnline = 21, + BradescoOnline = 22, + Pagseguro = 23, + VisaBrazil = 24, + AmexBrazil = 25, + Aura = 26, + Hipercard = 27, + MastercardBrazil = 28, + DinersCardBrazil = 29, + AuthorizedDevice = 30, + MOLPoints = 31, + ClickAndBuy = 32, + Beeline = 33, + Konbini = 34, + EClubPoints = 35, + CreditCardJapan = 36, + BankTransferJapan = 37, + PayEasy = 38, + Zong = 39, + CultureVoucher = 40, + BookVoucher = 41, + HappymoneyVoucher = 42, + ConvenientStoreVoucher = 43, + GameVoucher = 44, + Multibanco = 45, + Payshop = 46, + MaestroBoaCompra = 47, + OXXO = 48, + ToditoCash = 49, + Carnet = 50, + SPEI = 51, + ThreePay = 52, + IsBank = 53, + Garanti = 54, + Akbank = 55, + YapiKredi = 56, + Halkbank = 57, + BankAsya = 58, + Finansbank = 59, + DenizBank = 60, + PTT = 61, + CashU = 62, + SantanderRio = 63, + AutoGrant = 64, + WebMoneyJapan = 65, + OneCard = 66, + PSE = 67, + Exito = 68, + Efecty = 69, + Paloto = 70, + PinValidda = 71, + MangirKart = 72, + BancoCreditoDePeru = 73, + BBVAContinental = 74, + SafetyPay = 75, + PagoEfectivo = 76, + Trustly = 77, + UnionPay = 78, + BitCoin = 79, + LicensedSite = 80, + BitCash = 81, + NetCash = 82, + Nanaco = 83, + Tenpay = 84, + WeChat = 85, + CashonDelivery = 86, + CreditCardNodwin = 87, + DebitCardNodwin = 88, + NetBankingNodwin = 89, + CashCardNodwin = 90, + WalletNodwin = 91, + MobileDegica = 92, + Naranja = 93, + Cencosud = 94, + Cabal = 95, + PagoFacil = 96, + Rapipago = 97, + BancoNacionaldeCostaRica = 98, + BancoPoplar = 99, + RedPagos = 100, + SPE = 101, + Multicaja = 102, + RedCompra = 103, + ZiraatBank = 104, + VakiflarBank = 105, + KuveytTurkBank = 106, + EkonomiBank = 107, + Pichincha = 108, + PichinchaCash = 109, + Przelewy24 = 110, + Trustpay = 111, + POLi = 112, + MercadoPago = 113, + PayU = 114, + VTCPayWallet = 115, + MrCash = 116, + EPS = 117, + Interac = 118, + VTCPayCards = 119, + VTCPayOnlineBanking = 120, + VisaElectronBoaCompra = 121, + CafeFunded = 122, + OCA = 123, + Lider = 124, + WebMoneySteamCardJapan = 125, + WebMoneySteamCardTopUpJapan = 126, + Toss = 127, + Wallet = 128, + Valve = 129, + MasterComp = 130, + Promotional = 131, + MasterSubscription = 134, + Payco = 135, + MobileWalletJapan = 136, + BoletoFlash = 137, + PIX = 138, + GCash = 139, + KakaoPay = 140, + Dana = 141, + TrueMoney = 142, + TouchnGo = 143, + LinePay = 144, + MerPay = 145, + PayPay = 146, + AlfaClick = 147, + Sberbank = 148, + YooMoney = 149, + Tinkoff = 150, + CashInCIS = 151, + AuPAY = 152, + AliPayHK = 153, + NaverPay = 154, + Linkaja = 155, + ShopeePay = 156, + GrabPay = 157, + PayNow = 158, + OnlineBankingThailand = 159, + CashOptionsThailand = 160, + OEMTicket = 256, + Split = 512, + Complimentary = 1024, + FamilyGroup = 1025, +} + +#[derive(Debug, Copy, Clone, TryFromPrimitive)] +#[repr(u32)] +pub enum ELicenseType { + NoLicense = 0, + SinglePurchase = 1, + SinglePurchaseLimitedUse = 2, + RecurringCharge = 3, + RecurringChargeLimitedUse = 4, + RecurringChargeLimitedUseWithOverages = 5, + RecurringOption = 6, + LimitedUseDelayedActivation = 7, +} + +bitflags::bitflags! { + pub struct ELicenseFlags: u32 { + const None = 0; + const Renew = 0x01; + const RenewalFailed = 0x02; + const Pending = 0x04; + const Expired = 0x08; + const CancelledByUser = 0x10; + const CancelledByAdmin = 0x20; + const LowViolenceContent = 0x40; + const ImportedFromSteam2 = 0x80; + const ForceRunRestriction = 0x100; + const RegionRestrictionExpired = 0x200; + const CancelledByFriendlyFraudLock = 0x400; + const NotActivated = 0x800; + const PendingRefund = 0x2000; + const Borrowed = 0x4000; + const ReleaseStateOverride = 0x8000; + const CancelledByPartner = 0x40000; + const NonPermanent = 0x80000; + const PreferredOwner = 0x100000; + } +} + +pub struct License { + pub package_id: u32, + pub time_created: SystemTime, + pub time_next_process: Option, + pub minute_limit: i32, + pub minutes_used: i32, + pub payment_method: EPaymentMethod, + pub flags: ELicenseFlags, + pub purchase_country_code: String, + pub license_type: ELicenseType, + pub territory_code: i32, + pub change_number: i32, + pub owner_id: u32, + pub initial_period: u32, + pub initial_time_unit: u32, + pub renewal_period: u32, + pub renewal_time_unit: u32, + pub access_token: u64, + pub master_package_id: Option, +} + +impl TryFrom for License { + type Error = ClientError; + + fn try_from(value: cmsg_client_license_list::License) -> Result { + Ok(Self { + package_id: value.package_id(), + time_created: UNIX_EPOCH + Duration::from_secs(value.time_created().into()), + time_next_process: if value.time_next_process() == 0 { + None + } else { + Some(UNIX_EPOCH + Duration::from_secs(value.time_next_process().into())) + }, + minute_limit: value.minute_limit(), + minutes_used: value.minutes_used(), + payment_method: value + .payment_method() + .try_into() + .context(TryFromEPaymentMethodSnafu {})?, + flags: ELicenseFlags::from_bits_truncate(value.flags()), + purchase_country_code: value.purchase_country_code().to_string(), + license_type: value + .license_type() + .try_into() + .context(TryFromELicenseTypeSnafu {})?, + territory_code: value.territory_code(), + change_number: value.change_number(), + owner_id: value.owner_id(), + initial_period: value.initial_period(), + initial_time_unit: value.initial_time_unit(), + renewal_period: value.renewal_period(), + renewal_time_unit: value.renewal_time_unit(), + access_token: value.access_token(), + master_package_id: value.master_package_id, + }) + } +} diff --git a/lib/src/state/mod.rs b/lib/src/state/mod.rs new file mode 100644 index 0000000..c2b34bd --- /dev/null +++ b/lib/src/state/mod.rs @@ -0,0 +1 @@ +pub mod apps;