From e0ef2c6a464a5d424d7ee109dbaa1efd76e88445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=B2=D0=B8=D0=B4=20=D0=A7=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D1=8F=D1=82=D0=BE=D0=B2?= Date: Thu, 1 Feb 2024 01:21:56 +0200 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=D1=82?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 2 + .../NebulaAuth.LegacyConverter.csproj | 14 + NebulaAuth.LegacyConverter/Program.cs | 51 + NebulaAuth.sln | 40 + NebulaAuth.sln.DotSettings | 2 + NebulaAuth/App.xaml | 50 + NebulaAuth/App.xaml.cs | 35 + NebulaAuth/AssemblyInfo.cs | 10 + .../BackgroundImageVisibleConverter.cs | 20 + .../Background/BackgroundSourceConverter.cs | 28 + NebulaAuth/Converters/CoefficientConverter.cs | 19 + .../Converters/ColorToBrushConverter.cs | 30 + .../MultiCommandParamaterConverter.cs | 18 + .../Converters/OnOffBoolTextConverter.cs | 23 + NebulaAuth/Converters/ProxyTextConverter.cs | 24 + .../Converters/ReverseBooleanConverter.cs | 18 + .../Converters/SelectedProxyTextConverter.cs | 26 + NebulaAuth/Converters/ValueConverterGroup.cs | 23 + NebulaAuth/Core/DialogsController.cs | 76 ++ NebulaAuth/Core/LocalizationManager.cs | 105 ++ NebulaAuth/Core/MyUpdateManager.cs | 12 + NebulaAuth/Core/SnackbarController.cs | 56 ++ NebulaAuth/Core/ThemeManager.cs | 70 ++ NebulaAuth/Core/TrayManager.cs | 77 ++ NebulaAuth/FodyWeavers.xml | 3 + NebulaAuth/MainWindow.xaml | 211 ++++ NebulaAuth/MainWindow.xaml.cs | 144 +++ NebulaAuth/Model/Comparers/ProxyComparer.cs | 26 + .../Model/Comparers/ProxyDataComparer.cs | 31 + .../Model/Entities/LoginConfirmationResult.cs | 21 + NebulaAuth/Model/Entities/MaProxy.cs | 32 + NebulaAuth/Model/Entities/Mafile.cs | 38 + .../Model/Entities/MarketMultiConfirmation.cs | 16 + NebulaAuth/Model/Exceptions/CantAlignTime.cs | 26 + NebulaAuth/Model/MaClient.cs | 236 +++++ NebulaAuth/Model/PHandler.cs | 79 ++ NebulaAuth/Model/ProxyStorage.cs | 107 ++ NebulaAuth/Model/SessionHandler.cs | 94 ++ NebulaAuth/Model/Settings.cs | 70 ++ NebulaAuth/Model/Shell.cs | 41 + NebulaAuth/Model/Storage.cs | 210 ++++ NebulaAuth/NLog.config | 26 + NebulaAuth/NebulaAuth - Backup.csproj | 58 ++ NebulaAuth/NebulaAuth.csproj | 64 ++ NebulaAuth/Theme/Background.jpg | Bin 0 -> 729652 bytes NebulaAuth/Theme/Brushes.xaml | 9 + NebulaAuth/Theme/FontScaleWindow.cs | 200 ++++ NebulaAuth/Theme/Palette.xaml | 146 +++ NebulaAuth/Theme/Palette.xaml.cs | 15 + NebulaAuth/Theme/WindowStyle/NativeMethods.cs | 94 ++ .../Theme/WindowStyle/WindowChromeHelper.cs | 76 ++ .../WindowChromeRenderedBehavior.cs | 114 +++ NebulaAuth/Theme/WindowStyle/WindowStyle.xaml | 105 ++ .../Theme/WindowStyle/WindowStyle.xaml.cs | 36 + NebulaAuth/Theme/nebula lock.ico | Bin 0 -> 218134 bytes NebulaAuth/Theme/nebula.ico | Bin 0 -> 128485 bytes NebulaAuth/Utility/ErrorTranslatorHelper.cs | 39 + NebulaAuth/Utility/ExceptionHandler.cs | 97 ++ NebulaAuth/View/ConfirmationTemplates.xaml | 188 ++++ .../View/Dialogs/ConfirmCancelDialog.xaml | 46 + .../View/Dialogs/ConfirmCancelDialog.xaml.cs | 21 + NebulaAuth/View/Dialogs/LoginAgainDialog.xaml | 39 + .../View/Dialogs/LoginAgainDialog.xaml.cs | 15 + .../View/Dialogs/SetCryptPasswordDialog.xaml | 34 + .../Dialogs/SetCryptPasswordDialog.xaml.cs | 15 + NebulaAuth/View/Dialogs/WaitLoginDialog.xaml | 43 + .../View/Dialogs/WaitLoginDialog.xaml.cs | 72 ++ NebulaAuth/View/LinkerView.xaml | 133 +++ NebulaAuth/View/LinkerView.xaml.cs | 15 + NebulaAuth/View/ProxyManagerView.xaml | 112 +++ NebulaAuth/View/ProxyManagerView.xaml.cs | 15 + NebulaAuth/View/SettingsView.xaml | 73 ++ NebulaAuth/View/SettingsView.xaml.cs | 23 + NebulaAuth/ViewModel/MainVM.cs | 191 ++++ NebulaAuth/ViewModel/MainVM_Code.cs | 43 + NebulaAuth/ViewModel/MainVM_Confirmations.cs | 105 ++ NebulaAuth/ViewModel/MainVM_File.cs | 207 ++++ NebulaAuth/ViewModel/MainVM_Groups.cs | 139 +++ NebulaAuth/ViewModel/MainVM_Localization.cs | 17 + NebulaAuth/ViewModel/MainVM_Other.cs | 37 + NebulaAuth/ViewModel/MainVM_Proxy.cs | 121 +++ NebulaAuth/ViewModel/MainVM_Timer.cs | 140 +++ NebulaAuth/ViewModel/Other/LinkAccountVM.cs | 418 ++++++++ NebulaAuth/ViewModel/Other/LoginAgainVM.cs | 10 + NebulaAuth/ViewModel/Other/ProxyManagerVM.cs | 160 +++ .../ViewModel/Other/SetEncryptPasswordVM.cs | 8 + NebulaAuth/ViewModel/Other/SettingsVM.cs | 122 +++ NebulaAuth/localization.loc.json | 949 ++++++++++++++++++ SteamLibForked/Account/MobileSessionData.cs | 51 + SteamLibForked/Account/SessionData.cs | 62 ++ SteamLibForked/Account/SteamAuthToken.cs | 49 + SteamLibForked/Account/SteamId.cs | 180 ++++ .../Api/Mobile/SteamAuthenticatorLinkerApi.cs | 305 ++++++ SteamLibForked/Api/Mobile/SteamMobileApi.cs | 89 ++ .../Api/Mobile/SteamMobileConfirmationsApi.cs | 96 ++ SteamLibForked/Api/SteamGlobalApi.cs | 78 ++ .../Authentication/AdmissionHelper.cs | 176 ++++ .../Authentication/Default/LoginExecutor.cs | 108 ++ .../Default/LoginExecutorOptions.cs | 21 + .../Authentication/Default/LoginResultJson.cs | 18 + .../Authentication/Default/LoginStage.cs | 227 +++++ .../Default/NullLoginConsumer.cs | 8 + .../Authentication/Default/RsaKeyJson.cs | 16 + .../LoginV2/FinalizeLoginJson.cs | 31 + .../Authentication/LoginV2/LoginV2Executor.cs | 246 +++++ .../LoginV2/LoginV2ExecutorOptions.cs | 42 + .../Authentication/SessionDataValidator.cs | 96 ++ .../Authentication/SteamTokenHelper.cs | 184 ++++ .../TransferSession[folder]/OAuthData.cs | 13 + .../TransferParameters.cs | 23 + SteamLibForked/Core/Constants.cs | 36 + SteamLibForked/Core/Enums/Country.cs | 758 ++++++++++++++ SteamLibForked/Core/Enums/Currency.cs | 100 ++ .../Core/Enums/SteamAccessTokenType.cs | 28 + SteamLibForked/Core/Enums/SteamDomain.cs | 12 + SteamLibForked/Core/HealthMonitor.cs | 149 +++ .../Core/Interfaces/ICaptchaResolver.cs | 6 + .../Core/Interfaces/IConfirmationConsumer.cs | 6 + .../Core/Interfaces/IEmailAuthProvider.cs | 15 + .../Core/Interfaces/IPhoneNumberProvider.cs | 6 + .../Core/Interfaces/ISessionData.cs | 23 + .../Core/Interfaces/ISmsCodeProvider.cs | 6 + .../Core/Interfaces/ISteamGuardProvider.cs | 7 + .../Core/StatusCodes/SteamStatusCode.cs | 166 +++ .../Core/StatusCodes/SteamStatusCode_Codes.cs | 270 +++++ SteamLibForked/Core/SteamDomains.cs | 74 ++ .../CantLoadConfirmationsException.cs | 21 + .../General/UnsupportedResponseException.cs | 21 + SteamLibForked/Exceptions/LoginException.cs | 49 + .../Mobile/AuthenticatorLinkerException.cs | 42 + .../Mobile/BadMobileCookiesException.cs | 32 + .../Exceptions/SessionExpiredException.cs | 11 + .../Exceptions/SessionInvalidException.cs | 12 + .../Exceptions/SteamStatusCodeException.cs | 23 + .../Exceptions/UnknownSteamDomainException.cs | 25 + .../MobileSteamClient/MobileData.cs | 31 + .../ProtoCore/Enums/EAuthSessionGuardType.cs | 16 + .../ProtoCore/Enums/EAuthTokenPlatformType.cs | 12 + SteamLibForked/ProtoCore/Enums/EResult.cs | 132 +++ .../ProtoCore/Exceptions/EResutException.cs | 28 + .../Exceptions/UnknownEResultException.cs | 22 + .../ProtoCore/Interfaces/IProtoMsg.cs | 6 + SteamLibForked/ProtoCore/ProtoHelpers.cs | 38 + .../ProtoCore/ProtoHttpClientExtension.cs | 229 +++++ SteamLibForked/ProtoCore/ProtoResponse.cs | 102 ++ .../Services/AuthenticationService.cs | 228 +++++ .../Services/AuthenticatorLinkerService.cs | 138 +++ .../ProtoCore/Services/EmptyMessage.cs | 10 + SteamLibForked/SteamLibForked.csproj | 21 + .../Models[folder]/CheckPhoneResult.cs | 9 + .../Models[folder]/LinkOptions.cs | 30 + .../Models[folder]/LinkResult.cs | 38 + .../SteamAuthenticatorLinker.cs | 162 +++ .../AccountRecoveryConfirmation.cs | 8 + .../Concrete[folder]/Confirmation.cs | 44 + .../Concrete[folder]/MarketConfirmation.cs | 13 + .../RegisterApiKeyConfirmation.cs | 8 + .../Concrete[folder]/TradeConfirmation.cs | 16 + .../Confirmations/ConfirmationType.cs | 11 + .../Details[Folder]/ConfirmationDetails.cs | 6 + .../MarketConfirmationDetails.cs | 6 + .../TradeDetailsConfirmation.cs | 6 + .../SteamMobile/SteamGuardCodeGenerator.cs | 65 ++ SteamLibForked/SteamMobile/TimeAligner.cs | 87 ++ SteamLibForked/Utility/EncryptionHelper.cs | 74 ++ SteamLibForked/Utility/EnvironmentUtility.cs | 224 +++++ .../Utility/MaFiles/DeserializedMafileData.cs | 144 +++ .../Utility/MaFiles/LegacyMafile.cs | 38 + .../Utility/MaFiles/MafileCredits.cs | 17 + .../Utility/MaFiles/MafileSerializer.cs | 136 +++ .../MaFiles/MafileSerializer_SessionData.cs | 81 ++ .../MaFiles/MafileSerializer_Utility.cs | 132 +++ .../MaFiles/MafileSerializer_Validate.cs | 92 ++ .../Utility/MaFiles/MafileSerializer_Write.cs | 111 ++ SteamLibForked/Utility/Models/Enumeration.cs | 44 + SteamLibForked/Utility/SteamIdParser.cs | 157 +++ SteamLibForked/Utility/Utilities.cs | 28 + SteamLibForked/Web/ClientBuilder.cs | 51 + .../Web/Converters/SteamIdConverters.cs | 106 ++ .../StringToLongIfNeededConverter.cs | 16 + .../Web/Models/Account/UserWalletSchema.cs | 15 + .../Web/Models/Account/WalletSchema.cs | 32 + .../GlobalMarketInfo/GlobalInfoModel.cs | 23 + .../GlobalMarketInfo/MarketWalletSchema.cs | 61 ++ .../Web/Models/Mobile/Confirmations.cs | 41 + .../HTML/MarketGlobalInfoScrapper.cs | 80 ++ .../JSON/MobileConfirmationScrapper.cs | 137 +++ .../Web/Scrappers/JSON/StaticJson.cs | 18 + .../SteamWebApi[folder]/SteamApi_Market.cs | 28 + 189 files changed, 14229 insertions(+) create mode 100644 .gitattributes create mode 100644 NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj create mode 100644 NebulaAuth.LegacyConverter/Program.cs create mode 100644 NebulaAuth.sln create mode 100644 NebulaAuth.sln.DotSettings create mode 100644 NebulaAuth/App.xaml create mode 100644 NebulaAuth/App.xaml.cs create mode 100644 NebulaAuth/AssemblyInfo.cs create mode 100644 NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs create mode 100644 NebulaAuth/Converters/Background/BackgroundSourceConverter.cs create mode 100644 NebulaAuth/Converters/CoefficientConverter.cs create mode 100644 NebulaAuth/Converters/ColorToBrushConverter.cs create mode 100644 NebulaAuth/Converters/MultiCommandParamaterConverter.cs create mode 100644 NebulaAuth/Converters/OnOffBoolTextConverter.cs create mode 100644 NebulaAuth/Converters/ProxyTextConverter.cs create mode 100644 NebulaAuth/Converters/ReverseBooleanConverter.cs create mode 100644 NebulaAuth/Converters/SelectedProxyTextConverter.cs create mode 100644 NebulaAuth/Converters/ValueConverterGroup.cs create mode 100644 NebulaAuth/Core/DialogsController.cs create mode 100644 NebulaAuth/Core/LocalizationManager.cs create mode 100644 NebulaAuth/Core/MyUpdateManager.cs create mode 100644 NebulaAuth/Core/SnackbarController.cs create mode 100644 NebulaAuth/Core/ThemeManager.cs create mode 100644 NebulaAuth/Core/TrayManager.cs create mode 100644 NebulaAuth/FodyWeavers.xml create mode 100644 NebulaAuth/MainWindow.xaml create mode 100644 NebulaAuth/MainWindow.xaml.cs create mode 100644 NebulaAuth/Model/Comparers/ProxyComparer.cs create mode 100644 NebulaAuth/Model/Comparers/ProxyDataComparer.cs create mode 100644 NebulaAuth/Model/Entities/LoginConfirmationResult.cs create mode 100644 NebulaAuth/Model/Entities/MaProxy.cs create mode 100644 NebulaAuth/Model/Entities/Mafile.cs create mode 100644 NebulaAuth/Model/Entities/MarketMultiConfirmation.cs create mode 100644 NebulaAuth/Model/Exceptions/CantAlignTime.cs create mode 100644 NebulaAuth/Model/MaClient.cs create mode 100644 NebulaAuth/Model/PHandler.cs create mode 100644 NebulaAuth/Model/ProxyStorage.cs create mode 100644 NebulaAuth/Model/SessionHandler.cs create mode 100644 NebulaAuth/Model/Settings.cs create mode 100644 NebulaAuth/Model/Shell.cs create mode 100644 NebulaAuth/Model/Storage.cs create mode 100644 NebulaAuth/NLog.config create mode 100644 NebulaAuth/NebulaAuth - Backup.csproj create mode 100644 NebulaAuth/NebulaAuth.csproj create mode 100644 NebulaAuth/Theme/Background.jpg create mode 100644 NebulaAuth/Theme/Brushes.xaml create mode 100644 NebulaAuth/Theme/FontScaleWindow.cs create mode 100644 NebulaAuth/Theme/Palette.xaml create mode 100644 NebulaAuth/Theme/Palette.xaml.cs create mode 100644 NebulaAuth/Theme/WindowStyle/NativeMethods.cs create mode 100644 NebulaAuth/Theme/WindowStyle/WindowChromeHelper.cs create mode 100644 NebulaAuth/Theme/WindowStyle/WindowChromeRenderedBehavior.cs create mode 100644 NebulaAuth/Theme/WindowStyle/WindowStyle.xaml create mode 100644 NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs create mode 100644 NebulaAuth/Theme/nebula lock.ico create mode 100644 NebulaAuth/Theme/nebula.ico create mode 100644 NebulaAuth/Utility/ErrorTranslatorHelper.cs create mode 100644 NebulaAuth/Utility/ExceptionHandler.cs create mode 100644 NebulaAuth/View/ConfirmationTemplates.xaml create mode 100644 NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml create mode 100644 NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs create mode 100644 NebulaAuth/View/Dialogs/LoginAgainDialog.xaml create mode 100644 NebulaAuth/View/Dialogs/LoginAgainDialog.xaml.cs create mode 100644 NebulaAuth/View/Dialogs/SetCryptPasswordDialog.xaml create mode 100644 NebulaAuth/View/Dialogs/SetCryptPasswordDialog.xaml.cs create mode 100644 NebulaAuth/View/Dialogs/WaitLoginDialog.xaml create mode 100644 NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs create mode 100644 NebulaAuth/View/LinkerView.xaml create mode 100644 NebulaAuth/View/LinkerView.xaml.cs create mode 100644 NebulaAuth/View/ProxyManagerView.xaml create mode 100644 NebulaAuth/View/ProxyManagerView.xaml.cs create mode 100644 NebulaAuth/View/SettingsView.xaml create mode 100644 NebulaAuth/View/SettingsView.xaml.cs create mode 100644 NebulaAuth/ViewModel/MainVM.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Code.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Confirmations.cs create mode 100644 NebulaAuth/ViewModel/MainVM_File.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Groups.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Localization.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Other.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Proxy.cs create mode 100644 NebulaAuth/ViewModel/MainVM_Timer.cs create mode 100644 NebulaAuth/ViewModel/Other/LinkAccountVM.cs create mode 100644 NebulaAuth/ViewModel/Other/LoginAgainVM.cs create mode 100644 NebulaAuth/ViewModel/Other/ProxyManagerVM.cs create mode 100644 NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs create mode 100644 NebulaAuth/ViewModel/Other/SettingsVM.cs create mode 100644 NebulaAuth/localization.loc.json create mode 100644 SteamLibForked/Account/MobileSessionData.cs create mode 100644 SteamLibForked/Account/SessionData.cs create mode 100644 SteamLibForked/Account/SteamAuthToken.cs create mode 100644 SteamLibForked/Account/SteamId.cs create mode 100644 SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs create mode 100644 SteamLibForked/Api/Mobile/SteamMobileApi.cs create mode 100644 SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs create mode 100644 SteamLibForked/Api/SteamGlobalApi.cs create mode 100644 SteamLibForked/Authentication/AdmissionHelper.cs create mode 100644 SteamLibForked/Authentication/Default/LoginExecutor.cs create mode 100644 SteamLibForked/Authentication/Default/LoginExecutorOptions.cs create mode 100644 SteamLibForked/Authentication/Default/LoginResultJson.cs create mode 100644 SteamLibForked/Authentication/Default/LoginStage.cs create mode 100644 SteamLibForked/Authentication/Default/NullLoginConsumer.cs create mode 100644 SteamLibForked/Authentication/Default/RsaKeyJson.cs create mode 100644 SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs create mode 100644 SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs create mode 100644 SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs create mode 100644 SteamLibForked/Authentication/SessionDataValidator.cs create mode 100644 SteamLibForked/Authentication/SteamTokenHelper.cs create mode 100644 SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs create mode 100644 SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs create mode 100644 SteamLibForked/Core/Constants.cs create mode 100644 SteamLibForked/Core/Enums/Country.cs create mode 100644 SteamLibForked/Core/Enums/Currency.cs create mode 100644 SteamLibForked/Core/Enums/SteamAccessTokenType.cs create mode 100644 SteamLibForked/Core/Enums/SteamDomain.cs create mode 100644 SteamLibForked/Core/HealthMonitor.cs create mode 100644 SteamLibForked/Core/Interfaces/ICaptchaResolver.cs create mode 100644 SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs create mode 100644 SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs create mode 100644 SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs create mode 100644 SteamLibForked/Core/Interfaces/ISessionData.cs create mode 100644 SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs create mode 100644 SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs create mode 100644 SteamLibForked/Core/StatusCodes/SteamStatusCode.cs create mode 100644 SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs create mode 100644 SteamLibForked/Core/SteamDomains.cs create mode 100644 SteamLibForked/Exceptions/CantLoadConfirmationsException.cs create mode 100644 SteamLibForked/Exceptions/General/UnsupportedResponseException.cs create mode 100644 SteamLibForked/Exceptions/LoginException.cs create mode 100644 SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs create mode 100644 SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs create mode 100644 SteamLibForked/Exceptions/SessionExpiredException.cs create mode 100644 SteamLibForked/Exceptions/SessionInvalidException.cs create mode 100644 SteamLibForked/Exceptions/SteamStatusCodeException.cs create mode 100644 SteamLibForked/Exceptions/UnknownSteamDomainException.cs create mode 100644 SteamLibForked/MobileSteamClient/MobileData.cs create mode 100644 SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs create mode 100644 SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs create mode 100644 SteamLibForked/ProtoCore/Enums/EResult.cs create mode 100644 SteamLibForked/ProtoCore/Exceptions/EResutException.cs create mode 100644 SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs create mode 100644 SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs create mode 100644 SteamLibForked/ProtoCore/ProtoHelpers.cs create mode 100644 SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs create mode 100644 SteamLibForked/ProtoCore/ProtoResponse.cs create mode 100644 SteamLibForked/ProtoCore/Services/AuthenticationService.cs create mode 100644 SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs create mode 100644 SteamLibForked/ProtoCore/Services/EmptyMessage.cs create mode 100644 SteamLibForked/SteamLibForked.csproj create mode 100644 SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs create mode 100644 SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs create mode 100644 SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs create mode 100644 SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs create mode 100644 SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs create mode 100644 SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs create mode 100644 SteamLibForked/SteamMobile/TimeAligner.cs create mode 100644 SteamLibForked/Utility/EncryptionHelper.cs create mode 100644 SteamLibForked/Utility/EnvironmentUtility.cs create mode 100644 SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs create mode 100644 SteamLibForked/Utility/MaFiles/LegacyMafile.cs create mode 100644 SteamLibForked/Utility/MaFiles/MafileCredits.cs create mode 100644 SteamLibForked/Utility/MaFiles/MafileSerializer.cs create mode 100644 SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs create mode 100644 SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs create mode 100644 SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs create mode 100644 SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs create mode 100644 SteamLibForked/Utility/Models/Enumeration.cs create mode 100644 SteamLibForked/Utility/SteamIdParser.cs create mode 100644 SteamLibForked/Utility/Utilities.cs create mode 100644 SteamLibForked/Web/ClientBuilder.cs create mode 100644 SteamLibForked/Web/Converters/SteamIdConverters.cs create mode 100644 SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs create mode 100644 SteamLibForked/Web/Models/Account/UserWalletSchema.cs create mode 100644 SteamLibForked/Web/Models/Account/WalletSchema.cs create mode 100644 SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs create mode 100644 SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs create mode 100644 SteamLibForked/Web/Models/Mobile/Confirmations.cs create mode 100644 SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs create mode 100644 SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs create mode 100644 SteamLibForked/Web/Scrappers/JSON/StaticJson.cs create mode 100644 SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj b/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj new file mode 100644 index 0000000..fa02bbb --- /dev/null +++ b/NebulaAuth.LegacyConverter/NebulaAuth.LegacyConverter.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/NebulaAuth.LegacyConverter/Program.cs b/NebulaAuth.LegacyConverter/Program.cs new file mode 100644 index 0000000..a1076ad --- /dev/null +++ b/NebulaAuth.LegacyConverter/Program.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using SteamLib.Utility.MaFiles; + +var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); +if (currentPath != null) + Environment.CurrentDirectory = currentPath; + + +Console.WriteLine(currentPath); +foreach (var path in args) +{ + + if (Path.Exists(path) == false) + { + Console.WriteLine($"NOT VALID PATH: '{path}'"); + continue; + } + Console.WriteLine("Reading: " + path); + try + { + var text = File.ReadAllText(path); + var maf = MafileSerializer.Deserialize(text, out _); + var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented); + var name = Path.GetFileNameWithoutExtension(path); + Write(legacy, name); + + Console.WriteLine("DONE: " + name); + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: {ex.Message}"); + Console.WriteLine(ex); + } + finally + { + Console.WriteLine("-----------------------------------------"); + } + + +} + +Console.WriteLine("Press any key to exit..."); +Console.ReadKey(); + + +void Write(string maf, string name) +{ + + var path = Path.Combine(name + "_legacy.mafile"); + File.WriteAllText(path, maf); +} \ No newline at end of file diff --git a/NebulaAuth.sln b/NebulaAuth.sln new file mode 100644 index 0000000..9f72133 --- /dev/null +++ b/NebulaAuth.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NebulaAuth", "NebulaAuth\NebulaAuth.csproj", "{0FD01700-6D5C-451B-93BA-0860647E8F13}" + ProjectSection(ProjectDependencies) = postProject + {09F02072-F91D-4DAA-87BC-A34D3E150570} = {09F02072-F91D-4DAA-87BC-A34D3E150570} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NebulaAuth.LegacyConverter", "NebulaAuth.LegacyConverter\NebulaAuth.LegacyConverter.csproj", "{2D78A7D9-986A-4890-8A91-7ABD57A91830}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamLibForked", "SteamLibForked\SteamLibForked.csproj", "{09F02072-F91D-4DAA-87BC-A34D3E150570}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FD01700-6D5C-451B-93BA-0860647E8F13}.Release|Any CPU.Build.0 = Release|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D78A7D9-986A-4890-8A91-7ABD57A91830}.Release|Any CPU.Build.0 = Release|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09F02072-F91D-4DAA-87BC-A34D3E150570}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FA34DDD5-BDC0-4C0A-B347-4ECDBF446900} + EndGlobalSection +EndGlobal diff --git a/NebulaAuth.sln.DotSettings b/NebulaAuth.sln.DotSettings new file mode 100644 index 0000000..5b88222 --- /dev/null +++ b/NebulaAuth.sln.DotSettings @@ -0,0 +1,2 @@ + + DO_NOT_SHOW \ No newline at end of file diff --git a/NebulaAuth/App.xaml b/NebulaAuth/App.xaml new file mode 100644 index 0000000..8f47884 --- /dev/null +++ b/NebulaAuth/App.xaml @@ -0,0 +1,50 @@ + + + + #1E2025 + pack://application:,,,/Fonts/Roboto/#Roboto + pack://application:,,,/Fonts/Roboto/#Roboto Symbols + + + + + + + + + + + + True + False + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/App.xaml.cs b/NebulaAuth/App.xaml.cs new file mode 100644 index 0000000..bf9d06f --- /dev/null +++ b/NebulaAuth/App.xaml.cs @@ -0,0 +1,35 @@ +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Exceptions; +using System; +using System.Windows; +using CodingSeb.Localization; + +namespace NebulaAuth; + + +public partial class App : Application +{ + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + LocManager.Init(); + LocManager.SetApplicationLocalization(Settings.Instance.Language); + try + { + Shell.Initialize(); + } + catch (Exception ex) + { + var msg = ex.Message; + if (ex is CantAlignTimeException) + { + msg = Loc.Tr(LocManager.GetCodeBehind("CantAlignTimeError")); + } + + MessageBox.Show(msg); + throw; + } + } +} diff --git a/NebulaAuth/AssemblyInfo.cs b/NebulaAuth/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/NebulaAuth/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs b/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs new file mode 100644 index 0000000..d886d19 --- /dev/null +++ b/NebulaAuth/Converters/Background/BackgroundImageVisibleConverter.cs @@ -0,0 +1,20 @@ +using NebulaAuth.Model; +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace NebulaAuth.Converters.Background; + +public class BackgroundImageVisibleConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is not BackgroundMode.Color ? Visibility.Visible : Visibility.Hidden; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs b/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs new file mode 100644 index 0000000..b66349a --- /dev/null +++ b/NebulaAuth/Converters/Background/BackgroundSourceConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using System.IO; +using System.Windows.Data; +using System.Windows.Media.Imaging; +using NebulaAuth.Model; + +namespace NebulaAuth.Converters.Background; + +public class BackgroundSourceConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is BackgroundMode.Custom) + { + if(File.Exists("Background.png")) + return new BitmapImage(new Uri(Path.GetFullPath("Background.png"))); + } + + + return new BitmapImage(new Uri("pack://application:,,,/Theme/Background.jpg")); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/CoefficientConverter.cs b/NebulaAuth/Converters/CoefficientConverter.cs new file mode 100644 index 0000000..e79ddc8 --- /dev/null +++ b/NebulaAuth/Converters/CoefficientConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +[ValueConversion(typeof(double), typeof(double))] +public class CoefficientConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return System.Convert.ToDouble(value) * System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return (double)value / (double)parameter; + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ColorToBrushConverter.cs b/NebulaAuth/Converters/ColorToBrushConverter.cs new file mode 100644 index 0000000..c84794b --- /dev/null +++ b/NebulaAuth/Converters/ColorToBrushConverter.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; +using System; + +namespace NebulaAuth.Converters; + +[ValueConversion(typeof(Color), typeof(Brush))] +public class ColorToBrushConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Color color) + { + SolidColorBrush rv = new(color); + rv.Freeze(); + return rv; + } + return Binding.DoNothing; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is SolidColorBrush brush) + { + return brush.Color; + } + return default(Color); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/MultiCommandParamaterConverter.cs b/NebulaAuth/Converters/MultiCommandParamaterConverter.cs new file mode 100644 index 0000000..d228e06 --- /dev/null +++ b/NebulaAuth/Converters/MultiCommandParamaterConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class MultiCommandParameterConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + return values.Clone(); + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/OnOffBoolTextConverter.cs b/NebulaAuth/Converters/OnOffBoolTextConverter.cs new file mode 100644 index 0000000..73e4b47 --- /dev/null +++ b/NebulaAuth/Converters/OnOffBoolTextConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class OnOffBoolTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not bool b) + { + throw new InvalidCastException($"Can't cast value {value} to 'bool'"); + } + + return b ? "вкл" : "выкл"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ProxyTextConverter.cs b/NebulaAuth/Converters/ProxyTextConverter.cs new file mode 100644 index 0000000..889a351 --- /dev/null +++ b/NebulaAuth/Converters/ProxyTextConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using NebulaAuth.Model.Entities; + +namespace NebulaAuth.Converters; + +public class ProxyTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not MaProxy p) + { + return string.Empty; + } + + return $"{p.Id}: {p.Data.Address}"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ReverseBooleanConverter.cs b/NebulaAuth/Converters/ReverseBooleanConverter.cs new file mode 100644 index 0000000..020a17b --- /dev/null +++ b/NebulaAuth/Converters/ReverseBooleanConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class ReverseBooleanConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool)value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(bool)value; + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/SelectedProxyTextConverter.cs b/NebulaAuth/Converters/SelectedProxyTextConverter.cs new file mode 100644 index 0000000..5885ce8 --- /dev/null +++ b/NebulaAuth/Converters/SelectedProxyTextConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using NebulaAuth.Model.Entities; + +namespace NebulaAuth.Converters; + +public class SelectedProxyTextConverter : IMultiValueConverter +{ + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is not MaProxy proxy) + { + return string.Empty; + } + + var proxyExist = (bool)values[1]; + return proxyExist ? $"{proxy.Id}: {proxy.Data.Address}" : ""; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Converters/ValueConverterGroup.cs b/NebulaAuth/Converters/ValueConverterGroup.cs new file mode 100644 index 0000000..4d7b5fd --- /dev/null +++ b/NebulaAuth/Converters/ValueConverterGroup.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class ValueConverterGroup : List, IValueConverter +{ + #region IValueConverter Members + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture)); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + + #endregion +} \ No newline at end of file diff --git a/NebulaAuth/Core/DialogsController.cs b/NebulaAuth/Core/DialogsController.cs new file mode 100644 index 0000000..349f7d8 --- /dev/null +++ b/NebulaAuth/Core/DialogsController.cs @@ -0,0 +1,76 @@ +using System.Threading.Tasks; +using MaterialDesignThemes.Wpf; +using NebulaAuth.Model.Entities; +using NebulaAuth.View; +using NebulaAuth.View.Dialogs; +using NebulaAuth.ViewModel.Other; + +namespace NebulaAuth.Core; + +public static class DialogsController +{ + + #region CommonDialogs + + public static async Task ShowConfirmCancelDialog(string? msg = null) + { + + var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg); + + var result = await DialogHost.Show(content); + return result != null && (bool) result; + } + + //public static async Task ShowTextFieldDialog(string? msg = null) + //{ + // var content = msg == null ? new TextFieldDialog() : new TextFieldDialog(msg); + // var result = await DialogHost.Show(content); + // return result as string; + //} + + #endregion + + public static async Task ShowLoginAgainDialog(string username) + { + var vm = new LoginAgainVM + { + UserName = username + }; + var content = new LoginAgainDialog() + { + DataContext = vm + }; + var result = await DialogHost.Show(content); + if (result is true) + { + return vm; + } + + return null; + } + + public static async Task ShowProxyManager(MaProxy? currentProxy) + { + var vm = new ProxyManagerVM(); + var view = new ProxyManagerView + { + DataContext = vm + }; + await DialogHost.Show(view); + } + public static void CloseDialog() + { + DialogHost.Close(null); + } + public static async Task ShowLinkerDialog() + { + var vm = new LinkAccountVM(); + var view = new LinkerView() + { + DataContext = vm + }; + await DialogHost.Show(view); + } + + +} \ No newline at end of file diff --git a/NebulaAuth/Core/LocalizationManager.cs b/NebulaAuth/Core/LocalizationManager.cs new file mode 100644 index 0000000..7b30221 --- /dev/null +++ b/NebulaAuth/Core/LocalizationManager.cs @@ -0,0 +1,105 @@ +using CodingSeb.Localization; +using CodingSeb.Localization.Loaders; +using System; +using System.IO; +using System.Reflection; +using System.Text; + +namespace NebulaAuth.Core; + +public static class LocManager +{ + public const string CODE_BEHIND_PATH_PART = "CodeBehind"; + public const string COMMON_PATH_PART = "Common"; + public static void SetApplicationLocalization(LocalizationLanguage language) + { + Loc.Instance.CurrentLanguage = GetLanguageCode(language); + //Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(GetLanguageCode(language)); + //Thread.CurrentThread.CurrentCulture= CultureInfo.GetCultureInfo(GetLanguageCode(language)); + } + + + public static string GetLanguageCode(LocalizationLanguage language) + { + return language switch + { + LocalizationLanguage.English => "en", + LocalizationLanguage.Russian => "ru", + LocalizationLanguage.Ukrainian => "ua", + _ => throw new ArgumentOutOfRangeException(nameof(language), language, null) + }; + } + + + + public static void Init() + { + Loc.LogOutMissingTranslations = true; + + LocalizationLoader.Instance.FileLanguageLoaders.Add(new JsonFileLoader()); + + ReloadFiles(); + } + + public static void ReloadFiles() + { + string exampleFileFileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "localization.loc.json"); + LocalizationLoader.Instance.ClearAllTranslations(); + LocalizationLoader.Instance.AddFile(exampleFileFileName); + } + + public static string? GetCodeBehind(params string[] path) + { + return GetConcat(CODE_BEHIND_PATH_PART, path); + } + + public static string GetCodeBehindOrDefault(string def, params string[] path) + { + return GetCodeBehind(path) ?? def; + } + + public static string? GetCommon(params string[] path) + { + return GetConcat(COMMON_PATH_PART, path); + } + + public static string GetCommonOrDefault(string def, params string[] path) + { + return GetCommon(path) ?? def; + } + + + public static string? Get(params string[] path) + { + var sb = new StringBuilder(); + foreach (var part in path) + { + sb.Append('.'); + sb.Append(part); + } + + sb.Remove(0, 1); + var fullPath = sb.ToString(); + var message = Loc.Tr(fullPath, "~|*ABSENT|~"); + return message == "~|*ABSENT|~" ? null : message; + } + + public static string GetOrDefault(string def, params string[] path) + { + return Get(path) ?? def; + } + private static string? GetConcat(string first, string[] path) + { + string[] newArray = new string[path.Length + 1]; + newArray[0] = first; + Array.Copy(path, 0, newArray, 1, path.Length); + return Get(newArray); + } +} + +public enum LocalizationLanguage +{ + English, + Russian, + Ukrainian +} \ No newline at end of file diff --git a/NebulaAuth/Core/MyUpdateManager.cs b/NebulaAuth/Core/MyUpdateManager.cs new file mode 100644 index 0000000..dbe48de --- /dev/null +++ b/NebulaAuth/Core/MyUpdateManager.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + + +namespace NebulaAuth.Core; + +public static class UpdateManager +{ + public static async Task CheckForUpdates() + { + + } +} \ No newline at end of file diff --git a/NebulaAuth/Core/SnackbarController.cs b/NebulaAuth/Core/SnackbarController.cs new file mode 100644 index 0000000..fc0accb --- /dev/null +++ b/NebulaAuth/Core/SnackbarController.cs @@ -0,0 +1,56 @@ +using System; +using MaterialDesignThemes.Wpf; + +namespace NebulaAuth.Core; + +public class SnackbarController +{ + + public static SnackbarMessageQueue MessageQueue { get; } = new() { DiscardDuplicates = true}; + private const int MIN_SNACKBAR_TIME = 1000; + public SnackbarController() + { + + } + /// + /// + /// + /// + /// Default duration is 1 second + public static void SendSnackbar(string text, TimeSpan? duration = null) + { + duration ??= GetSnackbarTime(text); + MessageQueue.Enqueue(text, null, null, null, false, false, duration.Value); + } + + /// + /// + /// + /// + /// Default duration is 1 second + public static void SendSnackbarWithButton(string text, string actionText = "OK", Action? action = null, TimeSpan? duration = null) + { + duration ??= GetSnackbarTime(text); + Action argAction; + if (action == null) + { + argAction = _ => { }; + } + else + { + argAction = _ => action(); + } + + MessageQueue.Enqueue(text, actionText, argAction, null, false, false, duration); + } + + private static TimeSpan GetSnackbarTime(string str) + { + if (str.Length <= 100) return TimeSpan.FromMilliseconds(MIN_SNACKBAR_TIME); + + var length = str.Length / 0.07; + return TimeSpan.FromMilliseconds(length); + + } + +} \ No newline at end of file diff --git a/NebulaAuth/Core/ThemeManager.cs b/NebulaAuth/Core/ThemeManager.cs new file mode 100644 index 0000000..ffbe285 --- /dev/null +++ b/NebulaAuth/Core/ThemeManager.cs @@ -0,0 +1,70 @@ +using NebulaAuth.Model; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Color = System.Drawing.Color; + +namespace NebulaAuth.Core; + +public static class ThemeManager +{ + public static System.Windows.Media.Color DefaultBackgroundColor = System.Windows.Media.Color.FromRgb(30, 32, 37); + private static readonly Window MainWindow = Application.Current.MainWindow!; + static ThemeManager() + { + Settings.Instance.PropertyChanged += SettingsOnPropertyChanged; + } + + private static void SettingsOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Settings.BackgroundColor)) + { + UpdateBackground(); + } + else if (e.PropertyName == nameof(Settings.IconColor)) + { + UpdateIcon(); + } + } + + private static void UpdateIcon() + { + var color = Settings.Instance.IconColor; + if (color == null) + { + MainWindow.TaskbarItemInfo = null; + } + else + { + var c = color.Value; + var diameter = 14; + var bitmap = new Bitmap(diameter, diameter); + var graphics = Graphics.FromImage(bitmap); + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + var brush = new SolidBrush(Color.FromArgb(c.A, c.R, c.G, c.B)); + graphics.FillEllipse(brush, 0, 0, diameter, diameter); + var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + + MainWindow.TaskbarItemInfo = new(); + MainWindow.TaskbarItemInfo.Overlay = bitmapSource; + } + } + + private static void UpdateBackground() + { + var color = Settings.Instance.BackgroundColor ?? DefaultBackgroundColor; + Application.Current.Resources["WindowBackground"] = new SolidColorBrush(color); + + } + + public static void InitializeTheme() + { + UpdateIcon(); + UpdateBackground(); + } +} \ No newline at end of file diff --git a/NebulaAuth/Core/TrayManager.cs b/NebulaAuth/Core/TrayManager.cs new file mode 100644 index 0000000..0b2e580 --- /dev/null +++ b/NebulaAuth/Core/TrayManager.cs @@ -0,0 +1,77 @@ +using System; +using System.Drawing; +using System.IO; +using System.Windows; +using System.Windows.Forms; +using NebulaAuth.Model; +using Application = System.Windows.Application; + +namespace NebulaAuth.Core; + +public static class TrayManager +{ + private static NotifyIcon _notifyIcon; + private static readonly Window MainWindow = Application.Current.MainWindow!; + public static bool IsEnabled => Settings.Instance.HideToTray; + + + public static void InitializeTray() + { + _notifyIcon = new NotifyIcon(); + _notifyIcon.Text = "NebulaAuth"; + + Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/Theme/nebula lock.ico"))!.Stream; + _notifyIcon.Icon = new Icon(iconStream); + + _notifyIcon.MouseDoubleClick += NotifyIcon_MouseDoubleClick; + + var contextMenu = new ContextMenuStrip(); + + contextMenu.Items.Add("Выйти", null!, onClick: OnExitClick); + + _notifyIcon.ContextMenuStrip = contextMenu; + + MainWindow.StateChanged += MainWindow_StateChanged; + } + + private static void OnExitClick(object? sender, EventArgs e) + { + ExitApplication(); + } + + private static void NotifyIcon_MouseDoubleClick(object? sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + ShowMainWindow(); + } + } + + private static void MainWindow_StateChanged(object? sender, EventArgs e) + { + if (MainWindow.WindowState == WindowState.Minimized) + { + HideMainWindow(); + } + } + + private static void ShowMainWindow() + { + MainWindow.Show(); + MainWindow.WindowState = WindowState.Normal; + _notifyIcon.Visible = false; + } + + private static void HideMainWindow() + { + if(IsEnabled == false) return; + _notifyIcon.Visible = true; + MainWindow.Hide(); + } + + private static void ExitApplication() + { + _notifyIcon.Dispose(); + Application.Current.Shutdown(); + } +} \ No newline at end of file diff --git a/NebulaAuth/FodyWeavers.xml b/NebulaAuth/FodyWeavers.xml new file mode 100644 index 0000000..4025f81 --- /dev/null +++ b/NebulaAuth/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/NebulaAuth/MainWindow.xaml b/NebulaAuth/MainWindow.xaml new file mode 100644 index 0000000..44b26a8 --- /dev/null +++ b/NebulaAuth/MainWindow.xaml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + Отпустите, чтобы добавить mafile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs b/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs new file mode 100644 index 0000000..032e0c9 --- /dev/null +++ b/NebulaAuth/Theme/WindowStyle/WindowStyle.xaml.cs @@ -0,0 +1,36 @@ +using System.Windows; + +namespace NebulaAuth.Theme.WindowStyle +{ + /// + /// Логика взаимодействия для WindowStyle.xaml + /// + partial class WindowStyle + { + public WindowStyle() + { + InitializeComponent(); + } + + private void OnCloseClick(object sender, RoutedEventArgs e) + { + var window = (Window)((FrameworkElement)sender).TemplatedParent; + + window.Close(); + } + + private void OnMaximizeRestoreClick(object sender, RoutedEventArgs e) + { + var window = (Window)((FrameworkElement)sender).TemplatedParent; + + window.WindowState = window.WindowState == WindowState.Normal ? WindowState.Maximized : WindowState.Normal; + } + + private void OnMinimizeClick(object sender, RoutedEventArgs e) + { + var window = (Window)((FrameworkElement)sender).TemplatedParent; + + window.WindowState = WindowState.Minimized; + } + } +} diff --git a/NebulaAuth/Theme/nebula lock.ico b/NebulaAuth/Theme/nebula lock.ico new file mode 100644 index 0000000000000000000000000000000000000000..c0d5d5c8515eac51f9bbcc755b6b9345e41caf51 GIT binary patch literal 218134 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

unebeJZk6enCXF&-P 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

9oOZ0jEIE{n4$u#_`GXvMy19Y|G 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 P^@ejS|1byt<8S{9O_{^k literal 0 HcmV?d00001 diff --git a/NebulaAuth/Theme/nebula.ico b/NebulaAuth/Theme/nebula.ico new file mode 100644 index 0000000000000000000000000000000000000000..0fb629084a1493589aa6f4a121fff2bbcc7c371d GIT binary patch literal 128485 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%0DKqR2abe( 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 literal 0 HcmV?d00001 diff --git a/NebulaAuth/Utility/ErrorTranslatorHelper.cs b/NebulaAuth/Utility/ErrorTranslatorHelper.cs new file mode 100644 index 0000000..b024bc6 --- /dev/null +++ b/NebulaAuth/Utility/ErrorTranslatorHelper.cs @@ -0,0 +1,39 @@ +using CodingSeb.Localization; +using NebulaAuth.Core; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore.Enums; + +namespace NebulaAuth.Utility; + +public static class ErrorTranslatorHelper +{ + + public static string TranslateLoginError(LoginError error) + { + var result = GetMessage("Login", error.ToString()); + return result ?? error.ToString(); + } + + + public static string TranslateEResult(EResult eResult) + { + var result = GetMessage("EResult", eResult.ToString()); + return result ?? eResult.ToString(); + + } + + public static string TranslateLinkerError(AuthenticatorLinkerError error) + { + return GetMessage("AuthenticatorLinkerError", error.ToString()) ?? error.ToString(); + } + + public static string? GetMessage(string path, string name) + { + var fullPath = LocManager.CODE_BEHIND_PATH_PART + "." + "ErrorTranslator" + "." + path + "." + name; + var message = Loc.Tr(fullPath, "|ABSENT|"); + if (message == "|ABSENT|") return null; + return message; + + } +} \ No newline at end of file diff --git a/NebulaAuth/Utility/ExceptionHandler.cs b/NebulaAuth/Utility/ExceptionHandler.cs new file mode 100644 index 0000000..901d92d --- /dev/null +++ b/NebulaAuth/Utility/ExceptionHandler.cs @@ -0,0 +1,97 @@ +using NebulaAuth.Core; +using NebulaAuth.Model; +using SteamLib.Exceptions; +using SteamLib.Exceptions.General; +using SteamLib.ProtoCore.Exceptions; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace NebulaAuth.Utility; + +public static class ExceptionHandler +{ + private const string EXCEPTION_HANDLER_LOC_PATH = "ExceptionHandler"; + public static bool Handle(Exception ex, string? prefix = null, string? postfix = null, bool handleAllExceptions = false) + { + string msg; + Shell.Logger.Error(ex); + switch (ex) + { + case SessionExpiredException: + { + msg = "SessionExpiredException".GetCodeBehindLocalization(); + break; + } + case SessionInvalidException: + { + msg = "SessionExpiredException".GetCodeBehindLocalization(); + break; + } + case TaskCanceledException e: + { + msg = e.InnerException is TimeoutException + ? "TimeoutException".GetCodeBehindLocalization() + : "TaskCanceledException".GetCodeBehindLocalization(); + break; + } + case HttpRequestException e: + { + var str = "RequestError".GetCommonLocalization() + ": "; + if (e.StatusCode != null) + { + msg = str + e.StatusCode; + } + else if (e.InnerException != null) + { + msg = (str + e.InnerException.Message); + } + else + { + msg = (str + e.Message); + } + + break; + } + case UnsupportedResponseException: + { + msg = "UnsupportedResponseException".GetCodeBehindLocalization(); + break; + } + case CantLoadConfirmationsException e: + { + msg = e.Message; + break; + } + case EResultException e: + { + msg = "Error".GetCommonLocalization() + ": " + ErrorTranslatorHelper.TranslateEResult(e.Result); + break; + } + case LoginException e: + { + msg = "LoginException".GetCodeBehindLocalization() + ": " + ErrorTranslatorHelper.TranslateLoginError(e.Error); + break; + } + case not null when handleAllExceptions: + { + msg = "UnknownException".GetCodeBehindLocalization() + ex.Message; + break; + } + default: + return false; + } + + SnackbarController.SendSnackbar(prefix + msg + postfix); + return true; + } + private static string GetCommonLocalization(this string key) + { + return LocManager.GetCommonOrDefault(key, key); + } + + private static string GetCodeBehindLocalization(this string key) + { + return LocManager.GetCodeBehindOrDefault(key, EXCEPTION_HANDLER_LOC_PATH, key); + } +} \ No newline at end of file diff --git a/NebulaAuth/View/ConfirmationTemplates.xaml b/NebulaAuth/View/ConfirmationTemplates.xaml new file mode 100644 index 0000000..b4ec36f --- /dev/null +++ b/NebulaAuth/View/ConfirmationTemplates.xaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml new file mode 100644 index 0000000..85b364b --- /dev/null +++ b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs new file mode 100644 index 0000000..9a8999b --- /dev/null +++ b/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml.cs @@ -0,0 +1,21 @@ +using System.Windows.Controls; + +namespace NebulaAuth.View.Dialogs +{ + ///

+ /// Логика взаимодействия для ConfirmCancelDialog.xaml + /// + public partial class ConfirmCancelDialog : UserControl + { + public ConfirmCancelDialog() + { + InitializeComponent(); + } + + public ConfirmCancelDialog(string msg) + { + InitializeComponent(); + ConfirmTextBlock.Text = msg; + } + } +} diff --git a/NebulaAuth/View/Dialogs/LoginAgainDialog.xaml b/NebulaAuth/View/Dialogs/LoginAgainDialog.xaml new file mode 100644 index 0000000..464abce --- /dev/null +++ b/NebulaAuth/View/Dialogs/LoginAgainDialog.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs b/NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs new file mode 100644 index 0000000..24483c8 --- /dev/null +++ b/NebulaAuth/View/Dialogs/WaitLoginDialog.xaml.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; + +namespace NebulaAuth.View.Dialogs +{ + /// + /// Логика взаимодействия для WaitLoginDialog.xaml + /// + public partial class WaitLoginDialog : ICaptchaResolver + { + private TaskCompletionSource _tcs = new(); + public WaitLoginDialog() + { + InitializeComponent(); + } + + public async Task Resolve(Uri imageUrl, HttpClient client) + { + + CaptchaGrid.Visibility = Visibility.Visible; + var stream = await client.GetStreamAsync(imageUrl); + return await Application.Current.Dispatcher.Invoke(async () => + { + var image = await LoadImage(stream); + CaptchaImage.Source = image; + try + { + return await _tcs.Task; + } + catch (TaskCanceledException) + { + throw new LoginException(LoginError.CaptchaRequired); + } + }); + } + + private async Task LoadImage(Stream stream) + { + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + ms.Position = 0; + + var image = new BitmapImage(); + + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.StreamSource = ms; + image.EndInit(); + await stream.DisposeAsync(); + return image; + } + + private void CancelButton_OnClick(object sender, RoutedEventArgs e) + { + _tcs.SetCanceled(); + } + + private void SendCaptchaBtn_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrWhiteSpace(CaptchaTB.Text)) return; + var oldTcs = _tcs; + _tcs = new TaskCompletionSource(); + oldTcs.SetResult(CaptchaTB.Text); + } + } +} diff --git a/NebulaAuth/View/LinkerView.xaml b/NebulaAuth/View/LinkerView.xaml new file mode 100644 index 0000000..c6954ba --- /dev/null +++ b/NebulaAuth/View/LinkerView.xaml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/ProxyManagerView.xaml.cs b/NebulaAuth/View/ProxyManagerView.xaml.cs new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/NebulaAuth/View/ProxyManagerView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace NebulaAuth.View +{ + /// + /// Логика взаимодействия для ProxyManagerView.xaml + /// + public partial class ProxyManagerView : UserControl + { + public ProxyManagerView() + { + InitializeComponent(); + } + } +} diff --git a/NebulaAuth/View/SettingsView.xaml b/NebulaAuth/View/SettingsView.xaml new file mode 100644 index 0000000..d5e032b --- /dev/null +++ b/NebulaAuth/View/SettingsView.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NebulaAuth/View/SettingsView.xaml.cs b/NebulaAuth/View/SettingsView.xaml.cs new file mode 100644 index 0000000..6e12243 --- /dev/null +++ b/NebulaAuth/View/SettingsView.xaml.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace NebulaAuth.View +{ + /// + /// Логика взаимодействия для SettingsView.xaml + /// + public partial class SettingsView : UserControl + { + public SettingsView() + { + InitializeComponent(); + } + + private void ColorPicker_OnColorChanged(object sender, RoutedPropertyChangedEventArgs e) + { + Debug.WriteLine(e.NewValue); + } + } +} diff --git a/NebulaAuth/ViewModel/MainVM.cs b/NebulaAuth/ViewModel/MainVM.cs new file mode 100644 index 0000000..0aea031 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM.cs @@ -0,0 +1,191 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MaterialDesignThemes.Wpf; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using SteamLib.Exceptions; +using SteamLib.SteamMobile; +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NebulaAuth.View.Dialogs; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM : ObservableObject +{ + [ObservableProperty] + private ObservableCollection _maFiles = Storage.MaFiles; + public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue; + + public Mafile? SelectedMafile + { + get => _selectedMafile; + set => SetMafile(value); + } + private Mafile? _selectedMafile; + + + public MainVM() + { + CreateCodeTimer(); + _confirmTimer = new Timer(ConfirmByTimer, null, TimeSpan.FromSeconds(_timerCheckSeconds), TimeSpan.FromSeconds(_timerCheckSeconds)); + Proxies = new ObservableCollection(ProxyStorage.Proxies.Select(kvp => + new MaProxy(kvp.Key, kvp.Value))); + Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged; + QueryGroups(); + SessionHandler.LoginStarted += SessionHandlerOnLoginStarted; + SessionHandler.LoginCompleted += SessionHandlerOnLoginCompleted; + } + + + private void SetMafile(Mafile? mafile) + { + if (mafile != SelectedMafile) + { + _selectedMafile = mafile; + OnPropertyChanged(nameof(SelectedMafile)); + MaClient.SetAccount(mafile); + OnPropertyChanged(nameof(ConfirmationsVisible)); + if (Settings.DisableTimersOnChange) OffTimer(dispatcher: false); + SetCurrentProxy(); + OnPropertyChanged(nameof(IsDefaultProxy)); + if (mafile != null) Code = SteamGuardCodeGenerator.GenerateCode(mafile.SharedSecret); + } + } + + + [RelayCommand] + public async Task LoginAgain() + { + if (SelectedMafile == null) + { + return; + } + var loginAgainVm = await DialogsController.ShowLoginAgainDialog(SelectedMafile.AccountName); + if (loginAgainVm == null) + { + return; + } + var password = loginAgainVm.Password; + var waitDialog = new WaitLoginDialog(); + var wait = DialogHost.Show(waitDialog); + try + { + await MaClient.LoginAgain(SelectedMafile, password, loginAgainVm.SavePassword, waitDialog); + SnackbarController.SendSnackbar(GetLocalizationOrDefault("SuccessfulLogin")); + } + catch (LoginException ex) + { + SnackbarController.SendSnackbar(ErrorTranslatorHelper.TranslateLoginError(ex.Error), TimeSpan.FromSeconds(1.5)); + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex)) + { + Shell.Logger.Error(ex); + } + finally + { + DialogsController.CloseDialog(); + await wait; + } + } + private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + SearchText = string.Empty; + } + + + [RelayCommand] + private async Task RefreshSession() + { + if (SelectedMafile == null) return; + try + { + await MaClient.RefreshSession(SelectedMafile); + SnackbarController.SendSnackbar(GetLocalizationOrDefault("SessionRefreshed")); + } + catch (Exception ex) when (ExceptionHandler.Handle(ex)) + { + Shell.Logger.Error(ex); + } + } + + [RelayCommand] + public async Task LinkAccount() + { + OffTimer(false); + await DialogsController.ShowLinkerDialog(); + } + + [RelayCommand] + private async Task RemoveAuthenticator() + { + var selectedMafile = SelectedMafile; + if (selectedMafile == null) return; + if (string.IsNullOrWhiteSpace(selectedMafile.RevocationCode)) + { + SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Error", "Error") + ": " + GetLocalizationOrDefault("MissingRCode")); + return; + } + try + { + if (await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("ConfirmRemovingAuthenticator"))) + { + var result = await SessionHandler.Handle(() => MaClient.RemoveAuthenticator(SelectedMafile), SelectedMafile); + SnackbarController.SendSnackbar( + result.Success ? GetLocalizationOrDefault("AuthenticatorRemoved") : GetLocalizationOrDefault("AuthenticatorNotRemoved")); + + if (result.Success) + { + Storage.MoveToRemoved(selectedMafile); + MaFiles.Remove(selectedMafile); + } + } + else + { + SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Canceled", "Canceled")); + } + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex, handleAllExceptions: true)) + { + Shell.Logger.Error(ex); + } + } + + [RelayCommand] + private async Task ConfirmLogin() + { + if (SelectedMafile == null) return; + + try + { + LoginConfirmationResult res = await SessionHandler.Handle(() => MaClient.ConfirmLoginRequest(SelectedMafile), SelectedMafile); + if (res.Success) + { + SnackbarController.SendSnackbar($"{GetLocalizationOrDefault("ConfirmLoginSuccess")} {res.IP} ({res.Country})"); + } + else + { + string msg = res.Error switch + { + LoginConfirmationError.NoRequests => GetLocalizationOrDefault("ConfirmLoginFailedNoRequests"), + LoginConfirmationError.MoreThanOneRequest => GetLocalizationOrDefault("ConfirmLoginFailedMoreThanOneRequest"), //TODO + _ => throw new ArgumentOutOfRangeException() + }; + SnackbarController.SendSnackbar(msg); + } + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex, handleAllExceptions: true)) + { + Shell.Logger.Error(ex); + } + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Code.cs b/NebulaAuth/ViewModel/MainVM_Code.cs new file mode 100644 index 0000000..3496e9d --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Code.cs @@ -0,0 +1,43 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using SteamLib.SteamMobile; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Windows; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM +{ + private Timer _codeTimer; + [ObservableProperty] private double _codeProgress; + [ObservableProperty] private string _code; + + + [MemberNotNull(nameof(_codeTimer))] + [MemberNotNull(nameof(_code))] + private void CreateCodeTimer() + { + var currentTime = TimeAligner.GetSteamTime(); + _codeTimer = new Timer(UpdateCode, null, 0, 1000); + } + + private void UpdateCode(object? state = null) + { + var currentTime = TimeAligner.GetSteamTime(); + var untilChange = currentTime - currentTime / 30L * 30L; + var codeProgress = untilChange / 30D * 100; + + string? code = null; + if (untilChange == 0 && SelectedMafile != null) + { + code = SteamGuardCodeGenerator.GenerateCode(SelectedMafile!.SharedSecret); + } + + if(Application.Current == null) return; + Application.Current.Dispatcher.BeginInvoke(() => + { + CodeProgress = codeProgress; + if (code != null) Code = code; + }); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Confirmations.cs b/NebulaAuth/ViewModel/MainVM_Confirmations.cs new file mode 100644 index 0000000..041cd0e --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Confirmations.cs @@ -0,0 +1,105 @@ +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using SteamLib.SteamMobile.Confirmations; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Confirmations +{ + public ObservableCollection Confirmations { get; } = new(); + private Mafile? _confirmationsLoadedForMafile; + public bool ConfirmationsVisible => SelectedMafile == _confirmationsLoadedForMafile; + + [RelayCommand] + public async Task GetConfirmations() + { + if (SelectedMafile == null) return; + var maf = SelectedMafile; + List conf; + try + { + var confI = await SessionHandler.Handle(() => MaClient.GetConfirmations(SelectedMafile), SelectedMafile); + conf = confI.ToList(); + } + catch (Exception ex) + when (ExceptionHandler.Handle(ex)) + { + return; + } + + + _confirmationsLoadedForMafile = maf; + OnPropertyChanged(nameof(ConfirmationsVisible)); + Confirmations.Clear(); + var marketConfirmations = conf.Where(c => c.ConfType == ConfirmationType.MarketSellTransaction).ToList(); + + if (marketConfirmations.Count > 1) + { + var indexOfLast = conf.IndexOf(marketConfirmations.First()); + foreach (var mCon in marketConfirmations) + { + conf.Remove(mCon); + } + + var mConf = new MarketMultiConfirmation(marketConfirmations.Cast()); + conf.Insert(indexOfLast, mConf); + } + + foreach (var con in conf) + { + Confirmations.Add(con); + } + } + + private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm) + { + bool result; + try + { + if (confirmation is MarketMultiConfirmation multi) + { + result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm); + } + else + { + result = await MaClient.SendConfirmation(mafile, confirmation, confirm); + } + } + catch (Exception ex) + when(ExceptionHandler.Handle(ex)) + { + return; + } + + if (result) + { + Confirmations.Remove(confirmation); + } + else + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("ConfirmationError")); + } + } + + [RelayCommand] + private Task Confirm(Confirmation? confirmation) + { + if (SelectedMafile == null || confirmation == null) return Task.CompletedTask; + return SendConfirmation(SelectedMafile, confirmation, true); + } + [RelayCommand] + private Task Cancel(Confirmation? confirmation) + { + if (SelectedMafile == null || confirmation == null) return Task.CompletedTask; + return SendConfirmation(SelectedMafile, confirmation, false); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_File.cs b/NebulaAuth/ViewModel/MainVM_File.cs new file mode 100644 index 0000000..d115f86 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_File.cs @@ -0,0 +1,207 @@ +using CommunityToolkit.Mvvm.Input; +using MaterialDesignThemes.Wpf; +using Microsoft.Win32; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.View; +using NebulaAuth.ViewModel.Other; +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using AchiesUtilities.Extensions; +using NebulaAuth.Model.Entities; +using SteamLib.Exceptions; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //File //TODO: Refactor +{ + + public Settings Settings => Settings.Instance; + + [RelayCommand] + private void OpenMafileFolder() + { var mafile = SelectedMafile; + + var path = Storage.MafileFolder; + if (mafile?.SessionData != null) + { + var mafPath = Path.Combine(Storage.MafileFolder, mafile.SessionData.SteamId.Steam64 + ".maFile"); + if (File.Exists(mafPath)) + { + path = $"/e, /select, \"{mafPath}\""; ; + } + } + + Process.Start("explorer", path); + } + + + [RelayCommand] + private Task AddMafile() + { + var openFileDialog = new OpenFileDialog + { + Filter = "Mafile|*.mafile;*.maFile", + Multiselect = false, + + }; + var fs = openFileDialog.ShowDialog(); + if (fs != true) return Task.CompletedTask; + var path = openFileDialog.FileName; + return AddMafile(new[] { path }); + + } + public async Task AddMafile(string[] path) + { + bool? confirmOverwrite = null; + var added = 0; + var notAdded = 0; + var errors = 0; + foreach (var str in path) + { + try + { + Storage.AddNewMafile(str, confirmOverwrite ?? false); + added++; + } + catch (FormatException) + { + errors++; + } + catch (IOException) + { + confirmOverwrite ??= await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("ConfirmMafileOverwrite")); + + if (confirmOverwrite == true) + { + Storage.AddNewMafile(str, true); + added++; + } + else if (confirmOverwrite == false) + { + notAdded++; + } + } + catch (SessionInvalidException ex) + { + if (path.Length == 1 && ex.Data.Contains("mafile")) + { + var mafile = (Mafile)ex.Data["mafile"]!; + if (await HandleAddMafileWithoutSession(mafile)) + { + added++; + } + else + { + errors++; + } + } + else + { + SnackbarController.SendSnackbar($"{GetLocalizationOrDefault("MafileImportError")} {Path.GetFileName(str)}{GetLocalizationOrDefault("MissingSessionInMafile")}", TimeSpan.FromSeconds(4)); + } + } + } + + var msg = GetLocalizationOrDefault("Import"); + if (added > 0) + { + msg += $" {GetLocalizationOrDefault("ImportAdded")} {added}."; + } + if (notAdded > 0) + { + msg += $" {GetLocalizationOrDefault("ImportSkipped")} {notAdded}."; + } + + if (errors > 0) + { + msg += $" {GetLocalizationOrDefault("ImportErrors")} {errors}."; + } + SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2)); + } + + private async Task HandleAddMafileWithoutSession(Mafile data) + { + var oldMafile = SelectedMafile; + SelectedMafile = data; + await LoginAgain(); + var result = data.SessionData != null; + if (result) + { + var existed = MaFiles.FirstOrDefault(m => m.AccountName == data.AccountName); //TODO: more elegant way to handle overwrite + if (existed != null) + { + MaFiles.Remove(existed); + } + MaFiles.Add(data); + SelectedMafile = data; + } + else + { + SelectedMafile = oldMafile; + } + return result; + + } + + [RelayCommand] + private async Task RemoveMafile() + { + if (SelectedMafile == null) return; + var confirm = + await DialogsController.ShowConfirmCancelDialog(GetLocalizationOrDefault("RemoveMafileConfirmation")); + + if (!confirm) return; + try + { + Storage.MoveToRemoved(SelectedMafile); + } + catch (UnauthorizedAccessException) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("CantRemoveAlreadyExist")); + } + catch (Exception ex) + { + SnackbarController.SendSnackbar( + $"{GetLocalizationOrDefault("CantRemoveMafile")} {ex.Message}"); + } + } + + [RelayCommand] + private async Task OpenSettingsDialog() + { + var vm = new SettingsVM(); + var view = new SettingsView() + { + DataContext = vm + }; + await DialogHost.Show(view); + } + + [RelayCommand] + private async Task CopyMafileFromBuffer() + { + StringCollection files; + try + { + files = Clipboard.GetFileDropList(); + } + catch (Exception ex) + { + Shell.Logger.Error(ex); + return; + } + + var arr = files.Cast().ToArray(); + if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return; + + + + await AddMafile(arr); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Groups.cs b/NebulaAuth/ViewModel/MainVM_Groups.cs new file mode 100644 index 0000000..5bf60b3 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Groups.cs @@ -0,0 +1,139 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Groups +{ + [ObservableProperty] + private ObservableCollection _groups = new(); + + + public string? SelectedGroup + { + get => _selectedGroup; + set + { + if (SetProperty(ref _selectedGroup, value)) + PerformSearch(); + } + } + + private string? _selectedGroup; + + public string SearchText + { + get => _searchText; + set + { + if(SetProperty(ref _searchText, value)) + PerformSearch(); + } + + } + private string _searchText = string.Empty; + + + + + [RelayCommand] + private void AddGroup(string? value) + { + var mafile = SelectedMafile; + if (mafile == null) return; + if (string.IsNullOrEmpty(value)) return; + mafile.Group = value; + Storage.UpdateMafile(mafile); + QueryGroups(); + SelectedGroup = value; + OnPropertyChanged(nameof(SelectedMafile)); //For bindings + + } + + [RelayCommand] + private void AddToGroup(object[]? value) + { + if (value == null) return; + if (value.Length < 2) return; + var group = (string?)value[0]; + var mafile = (Mafile?)value[1]; + + if (group == null || mafile == null) return; + mafile.Group = group; + Storage.UpdateMafile(mafile); + QueryGroups(); + PerformSearch(); + OnPropertyChanged(nameof(SelectedMafile)); + } + + [RelayCommand] + private void RemoveGroup(Mafile? mafile) + { + if (mafile?.Group == null) return; + var mafGroup = mafile.Group; + mafile.Group = null; + Storage.UpdateMafile(mafile); + OnPropertyChanged(nameof(SelectedMafile)); //For bindings + QueryGroups(); + if (Groups.All(g => g.Equals(mafGroup) == false)) + { + SelectedGroup = null; + } + PerformSearch(); + } + + + private void QueryGroups() + { + var groups = Storage.MaFiles + .Where(m => string.IsNullOrWhiteSpace(m.Group) == false) + .Select(m => m.Group) + .Distinct() + .Order() + .ToList(); + + Groups = new ObservableCollection(groups!); + } + private void PerformSearch() + { + if (string.IsNullOrWhiteSpace(SelectedGroup) && string.IsNullOrWhiteSpace(SearchText)) + { + MaFiles = Storage.MaFiles; + return; + } + long? searchSteamId = null; + if (long.TryParse(SearchText, out var steamId)) + { + searchSteamId = steamId; + } + var query = Storage.MaFiles.AsEnumerable(); + if (!string.IsNullOrWhiteSpace(SearchText)) + { + query = query.Where(SearchPredicate); + } + if (!string.IsNullOrWhiteSpace(SelectedGroup)) + { + query = query.Where(m => m.Group != null && m.Group.Equals(SelectedGroup)); + } + var perform = query.ToList(); + MaFiles = new ObservableCollection(perform); + if (SelectedMafile != null && !MaFiles.Contains(SelectedMafile)) + { + SelectedMafile = MaFiles.FirstOrDefault(); + } + bool SearchPredicate(Mafile mafile) + { + if (!mafile.AccountName.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase)) + { + return mafile.SessionData != null && mafile.SessionData.SteamId.Steam64.Id.Equals(searchSteamId); + } + return true; + } + } + +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Localization.cs b/NebulaAuth/ViewModel/MainVM_Localization.cs new file mode 100644 index 0000000..b89b8c9 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Localization.cs @@ -0,0 +1,17 @@ +using NebulaAuth.Core; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM +{ + private const string LOC_PATH = "MainVM"; + private static string? GetLocalization(string key) + { + return LocManager.GetCodeBehind(LOC_PATH, key); + } + + private static string GetLocalizationOrDefault(string key) + { + return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Other.cs b/NebulaAuth/ViewModel/MainVM_Other.cs new file mode 100644 index 0000000..af830ba --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Other.cs @@ -0,0 +1,37 @@ +using MaterialDesignThemes.Wpf; +using System; +using System.Windows; +using NebulaAuth.View.Dialogs; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Other +{ + private void SessionHandlerOnLoginCompleted(object? sender, EventArgs e) + { + var currentSession = DialogHost.GetDialogSession(null); + Application.Current.Dispatcher.BeginInvoke(() => + { + if (currentSession is { Content: WaitLoginDialog, IsEnded: false }) + { + try + { + currentSession.Close(); + } + catch + { + //Ignored + } + } + }); + } + + private async void SessionHandlerOnLoginStarted(object? sender, EventArgs e) + { + if (DialogHost.IsDialogOpen(null)) return; + await Application.Current.Dispatcher.BeginInvoke(async () => + { + await DialogHost.Show(new WaitLoginDialog()); + }); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Proxy.cs b/NebulaAuth/ViewModel/MainVM_Proxy.cs new file mode 100644 index 0000000..8bd7936 --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Proxy.cs @@ -0,0 +1,121 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using NebulaAuth.Model.Comparers; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM +{ + public ObservableCollection Proxies { get; } + + public MaProxy? SelectedProxy + { + get => _selectedProxy; + set + { + if (SetProperty(ref _selectedProxy, value)) + { + OnPropertyChanged(nameof(IsDefaultProxy)); + OnProxyChanged(); + }; + } + + } + private MaProxy? _selectedProxy; + + [ObservableProperty] private bool _proxyExist = true; + public bool IsDefaultProxy => SelectedProxy == null && MaClient.DefaultProxy != null; + private bool _handleProxyChange; + + private void SetCurrentProxy() + { + if (ReferenceEquals(_selectedProxy, SelectedMafile?.Proxy) == false && _selectedProxy?.Equals(SelectedMafile?.Proxy) == false) + _handleProxyChange = true; + + if (SelectedMafile == null) + { + SelectedProxy = null; + return; + } + if (SelectedMafile.Proxy == null) + { + SelectedProxy = SelectedMafile.Proxy; + } + else + { + var existed = Proxies.FirstOrDefault(p => p.Id == SelectedMafile.Proxy.Id); + + + if (existed == null || ProxyDataComparer.Equal(existed.Data, SelectedMafile.Proxy.Data) == false) + { + SelectedProxy = SelectedMafile.Proxy; + } + else + { + SelectedProxy = existed; + } + } + + CheckProxyExist(); + + } + + private void CheckProxyExist() + { + if (SelectedProxy == null) + { + ProxyExist = true; + return; + } + + var selectedId = SelectedProxy.Id; + ProxyExist = ProxyStorage.Proxies.TryGetValue(selectedId, out var existedProxy) + && ProxyStorage.CompareProxy(SelectedProxy.Data, existedProxy); + } + + [RelayCommand] + private async Task OpenProxyManager() + { + await DialogsController.ShowProxyManager(SelectedProxy); + var oldSelection = SelectedProxy; + Proxies.Clear(); + foreach (var kvp in ProxyStorage.Proxies) + { + Proxies.Add(new MaProxy(kvp.Key, kvp.Value)); + } + SelectedProxy = oldSelection; + SetCurrentProxy(); + OnPropertyChanged(nameof(IsDefaultProxy)); + _handleProxyChange = false; + } + + [RelayCommand] + private void RemoveProxy() + { + if (SelectedMafile == null) return; + SelectedMafile.Proxy = null; + SelectedProxy = null; + Storage.UpdateMafile(SelectedMafile); + ProxyExist = true; + } + + private void OnProxyChanged() + { + if (_handleProxyChange) + { + _handleProxyChange = false; + return; + } + + if (SelectedMafile == null) return; + ProxyExist = true; + SelectedMafile.Proxy = SelectedProxy; + Storage.UpdateMafile(SelectedMafile); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/MainVM_Timer.cs b/NebulaAuth/ViewModel/MainVM_Timer.cs new file mode 100644 index 0000000..f16a99e --- /dev/null +++ b/NebulaAuth/ViewModel/MainVM_Timer.cs @@ -0,0 +1,140 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using NebulaAuth.Core; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using SteamLib.Exceptions; +using SteamLib.SteamMobile.Confirmations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace NebulaAuth.ViewModel; + +public partial class MainVM //Timer +{ + private readonly Timer _confirmTimer; + [ObservableProperty] private bool _marketTimerEnabled; + [ObservableProperty] private bool _tradeTimerEnabled; + private int _timerCheckSeconds = Settings.Instance.TimerSeconds; + public int TimerCheckSeconds + { + get => _timerCheckSeconds; + set => SetTimer(value); + } + + + private async void ConfirmByTimer(object? state = null) + { + if (SelectedMafile == null) + return; + var selected = SelectedMafile; + if (InterruptTimer(selected, null)) return; + List conf; + try + { + conf = (await HandleTimerRequest(() => MaClient.GetConfirmations(selected), SelectedMafile)).ToList(); + } + catch (ApplicationException ex) + { + Shell.Logger.Warn(ex, "Error GetConf in timer."); + return; + } + if (InterruptTimer(selected, conf)) return; + var toConfirm = new List(); + if (MarketTimerEnabled) + { + var market = conf.Where(c => c.ConfType == ConfirmationType.MarketSellTransaction); + toConfirm.AddRange(market); + } + if (TradeTimerEnabled) + { + var trade = conf.Where(c => c.ConfType == ConfirmationType.Trade); + toConfirm.AddRange(trade); + } + if (InterruptTimer(selected, toConfirm)) return; + bool result; + try + { + Shell.Logger.Debug("Sending confirmations. Count: {count}", toConfirm.Count); + result = await HandleTimerRequest(() => MaClient.SendMultipleConfirmation(SelectedMafile, toConfirm, confirm: true), SelectedMafile); + } + catch (ApplicationException ex) + { + Shell.Logger.Warn(ex, "MultiConf error in Timer."); + return; + } + SnackbarController.SendSnackbar(result ? $"{GetLocalizationOrDefault("TimerConfirmed")} {toConfirm.Count}" : GetLocalizationOrDefault("TimerNotConfirmed")); + } + + private bool InterruptTimer(Mafile cachedValue, List? confirmations) + { + return SelectedMafile == null + || (MarketTimerEnabled || TradeTimerEnabled) == false + || SelectedMafile != cachedValue + || confirmations is { Count: 0 }; + + } + + private void OffTimer(bool dispatcher) + { + if (dispatcher) + { + Application.Current.Dispatcher.BeginInvoke(new Action(OffTimerInline)); + } + else + { + OffTimerInline(); + } + return; + + void OffTimerInline() + { + MarketTimerEnabled = false; + TradeTimerEnabled = false; + } + } + + private async Task HandleTimerRequest(Func> func, Mafile mafile) + { + Exception innerException; + try + { + return await SessionHandler.Handle(func, mafile); + } + catch (SessionInvalidException ex) + { + Shell.Logger.Warn("Session error while requesting in timer. Timer disabled"); + SnackbarController.SendSnackbar(GetLocalizationOrDefault("TimerSessionError")); + OffTimer(dispatcher: true); + innerException = ex; + } + catch (Exception ex) when (ExceptionHandler.Handle(ex, GetLocalizationOrDefault("TimerPrefix"))) + { + innerException = ex; + } + throw new ApplicationException("Swallowed", innerException); + } + + + + + private void SetTimer(int value) + { + if (_timerCheckSeconds == value) return; + if (value < 10) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("TimerTooFast")); + OnPropertyChanged(nameof(TimerCheckSeconds)); + return; + } + + Settings.TimerSeconds = value; + _timerCheckSeconds = value; + OnPropertyChanged(nameof(TimerCheckSeconds)); + _confirmTimer.Change(value * 1000, value * 1000); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/LinkAccountVM.cs b/NebulaAuth/ViewModel/Other/LinkAccountVM.cs new file mode 100644 index 0000000..9697241 --- /dev/null +++ b/NebulaAuth/ViewModel/Other/LinkAccountVM.cs @@ -0,0 +1,418 @@ +using AchiesUtilities.Collections; +using AchiesUtilities.Web.Proxy; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MaterialDesignThemes.Wpf; +using NebulaAuth.Model; +using NebulaAuth.Model.Entities; +using NebulaAuth.Utility; +using Newtonsoft.Json; +using NLog; +using SteamLib; +using SteamLib.Account; +using SteamLib.Authentication; +using SteamLib.Authentication.LoginV2; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.SteamMobile.AuthenticatorLinker; +using SteamLib.Web; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; +using NebulaAuth.Core; + +namespace NebulaAuth.ViewModel.Other; + +public partial class LinkAccountVM : ObservableObject, IEmailProvider, IPhoneNumberProvider, ISmsCodeProvider +{ + private const string LOCALIZATION_KEY = "LinkVM"; + private static Logger Logger => Shell.Logger; + private static Microsoft.Extensions.Logging.ILogger Logger2 => Shell.ExtensionsLogger; + #region Properties + + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsPasswordFieldVisible))] + private bool _isLogin; + + + [ObservableProperty] private bool _isEmailCode; + [ObservableProperty] private bool _isPhoneNumber; + [ObservableProperty] private bool _isEmailConfirmation; + [ObservableProperty] private bool _isLinkCode; + [ObservableProperty] private bool _isCompleted; + + [ObservableProperty] private bool _isFieldVisible = true; + + + + [ObservableProperty] private string _fieldText; + [ObservableProperty] private string _passFieldText; + [ObservableProperty] private string _hintText = GetLocalizationOrDefault("EnterLoginAndPassword"); + + private TaskCompletionSource _emailCodeTcs = new(); + private TaskCompletionSource _phoneNumberTcs = new(); + private TaskCompletionSource _emailConfTcs = new(); + private TaskCompletionSource _linkCodeTcs = new(); + + private bool isLinkStarted; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(ProceedCommand))] + private bool _canProceed = true; + public bool IsPasswordFieldVisible => !IsLogin; + + private LoginV2ExecutorOptions _loginV2ExecutorOptions; + private SteamAuthenticatorLinker _linker; + private MobileSessionData _sessionData; + + #endregion + + #region HttpClient + + private static HttpClient Client { get; } + private static HttpClientHandler Handler { get; } + private static DynamicProxy Proxy { get; } + public ObservableDictionary Proxies => ProxyStorage.Proxies; + + public KeyValuePair? SelectedProxy + { + get => _selectedProxy; + set + { + SetProperty(ref _selectedProxy, value); + SetProxy(); + } + } + private KeyValuePair? _selectedProxy; + + + #endregion + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public LinkAccountVM() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + if (MaClient.DefaultProxy != null) + { + var def = Proxies.FirstOrDefault(p => p.Value.Equals(MaClient.DefaultProxy)); + if (def.Value != null!) + { + SelectedProxy = def; + } + } + } + + [RelayCommand(AllowConcurrentExecutions = true, CanExecute = nameof(CanProceed))] + public async Task Proceed() + { + if (IsCompleted) + DialogHost.Close(null); + + CanProceed = false; + + #region Login + + if (IsLogin == false) + { + SetProxy(); + ClearCookies(); + _loginV2ExecutorOptions = new LoginV2ExecutorOptions(LoginV2Executor.NullConsumer, Client) + { + DeviceDetails = LoginV2ExecutorOptions.GetMobileDefaultDevice(), + WebsiteId = "Mobile", + EmailAuthProvider = this, + Logger = Logger2 + }; + + try + { + IsLogin = true; + var userName = FieldText; + var pass = PassFieldText; + FieldText = string.Empty; + IsFieldVisible = false; + HintText = string.Empty; + _sessionData = (MobileSessionData)await LoginV2Executor.DoLogin(_loginV2ExecutorOptions, userName, pass); + Handler.CookieContainer.SetSteamMobileCookies(_sessionData); + IsEmailCode = true; + } + catch (EResultException ex) + { + Logger.Error(ex, "Link exception on login"); + HintText = GetLocalizationOrDefault("CantLogin") + ErrorTranslatorHelper.TranslateEResult(ex.Result); + InvokeOnDispatcher(ResetState); + return; + } + catch (LoginException ex) + { + Logger.Error(ex, "Link exception on login"); + HintText = GetLocalizationOrDefault("CantLogin") + ErrorTranslatorHelper.TranslateLoginError(ex.Error); + InvokeOnDispatcher(ResetState); + return; + } + catch (Exception ex) + { + Logger.Error(ex, "Link exception on login"); + HintText = GetLocalizationOrDefault("CantLogin") + ex.Message; + InvokeOnDispatcher(ResetState); + return; + } + + } + + if (IsEmailCode == false) + { + _emailCodeTcs.SetResult(FieldText); + HintText = string.Empty; + FieldText = string.Empty; + _emailCodeTcs = new TaskCompletionSource(); + IsFieldVisible = false; + return; + } + + #endregion + + if (isLinkStarted) + goto linkStarted; + + try + { + isLinkStarted = true; + var linkOptions = new LinkOptions(Client, LoginV2Executor.NullConsumer, this, + null, this, this, backupHandler: Backup, Logger2); + _linker = new SteamAuthenticatorLinker(linkOptions); + var result = await _linker.LinkAccount(_sessionData); + IsLinkCode = true; + IsCompleted = true; + var mafile = Mafile.FromMobileDataExtended(result); + Storage.SaveMafile(mafile); + File.Delete(Path.Combine("mafiles_backup", mafile.AccountName + ".mafile")); + HintText = + string.Format(GetLocalizationOrDefault("MafileLinked"), + mafile.RevocationCode, + mafile.SessionData?.SteamId.Steam64); + + CanProceed = true; + return; + } + catch (AuthenticatorLinkerException ex) + { + Logger.Error(ex, "Link exception"); + HintText = $"{GetLocalizationCommon("Error")}: {ErrorTranslatorHelper.TranslateLinkerError(ex.Error)}"; + InvokeOnDispatcher(ResetState); + return; + + } + catch (HttpRequestException ex) + { + var msg = ex.StatusCode?.ToString() ?? ex.Message; + HintText = $"{GetLocalizationCommon("RequestError")}: {msg}"; + InvokeOnDispatcher(ResetState); + return; + } + catch (EResultException ex) + { + Logger.Error(ex, "Link exception"); + HintText = GetLocalizationOrDefault("ErrorWithCode") + ErrorTranslatorHelper.TranslateEResult(ex.Result); + InvokeOnDispatcher(ResetState); + return; + } + catch (Exception ex) + { + Logger.Error(ex, "Link exception"); + HintText = GetLocalizationOrDefault("UnknownError") + ex.Message; + InvokeOnDispatcher(ResetState); + return; + } + + + linkStarted: + if (IsPhoneNumber == false) + { + var phoneText = FieldText; + FieldText = string.Empty; + + if (string.IsNullOrWhiteSpace(phoneText)) + { + HintText = string.Empty; + IsFieldVisible = false; + _phoneNumberTcs.SetResult(null); + _phoneNumberTcs = new(); + return; + } + + if (!string.IsNullOrWhiteSpace(phoneText) && phoneText.Length >= 4 && long.TryParse(phoneText, out var phone)) + { + HintText = string.Empty; + IsFieldVisible = false; + _phoneNumberTcs.SetResult(phone); + _phoneNumberTcs = new(); + return; + } + else + { + HintText = GetLocalizationOrDefault("PleaseEnterCorrectPhone"); + CanProceed = true; + return; + } + } + + if (IsEmailConfirmation == false) + { + HintText = string.Empty; + _emailConfTcs.SetResult(); + _emailConfTcs = new(); + CanProceed = false; + return; + } + + if (IsLinkCode == false) + { + var linkCode = FieldText; + FieldText = string.Empty; + if (!string.IsNullOrWhiteSpace(linkCode) && linkCode.Length >= 4) + { + HintText = string.Empty; + IsFieldVisible = false; + _linkCodeTcs.SetResult(linkCode); + _linkCodeTcs = new(); + return; + } + else + { + HintText = GetLocalizationOrDefault("PleaseEnterCorrectCode"); + CanProceed = true; + return; + } + } + + } + + [RelayCommand] + public void ResetProxy() + { + if (IsPasswordFieldVisible == false) return; + SelectedProxy = null; + } + + private void InvokeOnDispatcher(Action action) + { + Application.Current.Dispatcher.BeginInvoke(action, null); + } + + private void ResetState() + { + PassFieldText = string.Empty; + IsLogin = false; + IsFieldVisible = true; + IsEmailCode = false; + isLinkStarted = false; + IsPhoneNumber = false; + IsEmailConfirmation = false; + CanProceed = true; + _emailCodeTcs = new TaskCompletionSource(); + } + + private void Backup(MobileDataExtended data) + { + if (Directory.Exists("mafiles_backup") == false) + { + Directory.CreateDirectory("mafiles_backup"); + } + + var json = JsonConvert.SerializeObject(data, Formatting.Indented); + File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json); + } + + #region Providers + + public int MaxRetryCount { get; } + public Task GetEmailAuthCode(ILoginConsumer caller) + { + CanProceed = true; + HintText = GetLocalizationOrDefault("EnterEmailCode"); + IsFieldVisible = true; + return _emailCodeTcs.Task; + } + + public Task GetAddAuthenticatorCode(ILoginConsumer caller) + { + IsPhoneNumber = true; + IsEmailConfirmation = true; + CanProceed = true; + HintText = GetLocalizationOrDefault("EnterEmailCode"); + IsFieldVisible = true; + return _linkCodeTcs.Task; + } + + public Task ConfirmEmailLink(ILoginConsumer caller, EmailConfirmationType confirmationType) + { + IsPhoneNumber = true; + CanProceed = true; + HintText = GetLocalizationOrDefault("ClickOnEmailLink"); + return _emailConfTcs.Task; + } + + public Task GetPhoneNumber(ILoginConsumer caller) + { + CanProceed = true; + HintText = GetLocalizationOrDefault("EnterPhoneNumber"); + IsFieldVisible = true; + return _phoneNumberTcs.Task; + } + public async Task GetSmsCode(ILoginConsumer caller, long? phoneNumber, string? hint) + { + IsPhoneNumber = true; + IsEmailConfirmation = true; + CanProceed = true; + HintText = string.Format(GetLocalizationOrDefault("PhoneHint"), hint); + IsFieldVisible = true; + var code = await _linkCodeTcs.Task; + return int.Parse(code); + } + + #endregion + + + #region Client + + static LinkAccountVM() + { + Proxy = new DynamicProxy(null); + var clientPair = ClientBuilder.BuildMobileClient(Proxy, null); + Client = clientPair.Client; + Handler = clientPair.Handler; + } + + private void ClearCookies() + { + Handler.CookieContainer.ClearMobileSessionCookies(); + } + + private void SetProxy() + { + Proxy.SetData(SelectedProxy?.Value); + } + + #endregion + + + private static string GetLocalizationOrDefault(string key) + { + return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key); + } + + private static string GetLocalizationCommon(string key) + { + return LocManager.GetCommonOrDefault(key, key); + } + + +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/LoginAgainVM.cs b/NebulaAuth/ViewModel/Other/LoginAgainVM.cs new file mode 100644 index 0000000..a9c07b6 --- /dev/null +++ b/NebulaAuth/ViewModel/Other/LoginAgainVM.cs @@ -0,0 +1,10 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace NebulaAuth.ViewModel.Other; + +public partial class LoginAgainVM : ObservableObject +{ + [ObservableProperty] private string _password = null!; + [ObservableProperty] private bool _savePassword; + [ObservableProperty] private string _userName = null!; +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/ProxyManagerVM.cs b/NebulaAuth/ViewModel/Other/ProxyManagerVM.cs new file mode 100644 index 0000000..ba668e1 --- /dev/null +++ b/NebulaAuth/ViewModel/Other/ProxyManagerVM.cs @@ -0,0 +1,160 @@ +using AchiesUtilities.Collections; +using AchiesUtilities.Web.Proxy; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Core; +using NebulaAuth.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows; + +namespace NebulaAuth.ViewModel.Other; + +public partial class ProxyManagerVM : ObservableObject +{ + private const string LOCALIZATION_KEY = "ProxyManagerVM"; + + [ObservableProperty] private KeyValuePair? _selectedProxy; + [ObservableProperty] private string _addProxyField = string.Empty; + [ObservableProperty] private KeyValuePair? _defaultProxy; + public ObservableDictionary Proxies => ProxyStorage.Proxies; + + private static readonly Regex IdRegex = new(@"(?:\{(\d+)\})"); + + + public ProxyManagerVM() + { + if (MaClient.DefaultProxy != null) + DefaultProxy = Proxies.FirstOrDefault(kvp => kvp.Value.Equals(MaClient.DefaultProxy)); + } + + [RelayCommand] + private void AddProxy() + { + if (string.IsNullOrEmpty(AddProxyField)) return; + if (AddProxyField.Contains(Environment.NewLine)) + { + var split = AddProxyField.Split(Environment.NewLine); + var idPresent = (bool?)null; + var proxies = new List>(); + var i = 0; + foreach (var str in split) + { + i++; + int? id = null; + var match = IdRegex.Match(str); + if (match.Success) id = int.Parse(match.Groups[1].Value); + idPresent ??= match.Success; + + if (idPresent.Value != match.Success) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("WrongFormatSomeIdsMissing")); + return; + } + + + + try + { + var proxy = ProxyData.Parse(str, ProxyStorage.FORMAT); + if (id != null && proxies.Any(kvp => kvp.Key == id)) + { + SnackbarController.SendSnackbar(string.Format(GetLocalizationOrDefault("DuplicateId"), id)); + return; + } + proxies.Add(new KeyValuePair(id, proxy)); + } + catch (FormatException) + { + SnackbarController.SendSnackbar(string.Format(GetLocalizationOrDefault("WrongFormatOnLine"), i)); + return; + } + } + + foreach (var kvp in proxies) + { + ProxyStorage.SetProxy(kvp.Key, kvp.Value); + } + } + else + { + int? id = null; + var input = AddProxyField; + if (IdRegex.IsMatch(AddProxyField)) + { + id = int.Parse(IdRegex.Match(AddProxyField).Groups[1].Value); + input = IdRegex.Replace(input, ""); + } + + ProxyData data; + try + { + data = ProxyData.Parse(input, ProxyStorage.FORMAT); + } + catch (FormatException) + { + SnackbarController.SendSnackbar(GetLocalizationOrDefault("WrongFormat")); + return; + } + + ProxyStorage.SetProxy(id, data); + + + } + AddProxyField = string.Empty; + CheckIfDefaultProxyStay(); + } + + private void CheckIfDefaultProxyStay() + { + if (!DefaultProxy.HasValue || Proxies.Any(kvp => kvp.Equals(DefaultProxy.Value))) return; + DefaultProxy = null; + MaClient.DefaultProxy = null; + } + + [RelayCommand] + private void RemoveProxy() + { + if (SelectedProxy == null) return; + ProxyStorage.RemoveProxy(SelectedProxy.Value.Key); + CheckIfDefaultProxyStay(); + } + + [RelayCommand] + private void SetDefault() + { + if (SelectedProxy == null) return; + DefaultProxy = SelectedProxy; + MaClient.DefaultProxy = SelectedProxy.Value.Value; + ProxyStorage.Save(); + } + + [RelayCommand] + private void RemoveDefault() + { + DefaultProxy = null; + MaClient.DefaultProxy = null; + ProxyStorage.Save(); + } + + [RelayCommand] + private void CopyProxy(ProxyData? obj) + { + if (obj == null) return; + Clipboard.SetText(obj.ToString(ProxyStorage.FORMAT)); + } + + [RelayCommand] + private void CopyProxyAddress(ProxyData? obj) + { + if (obj == null) return; + Clipboard.SetText(obj.ToString(ProxyStorage.ADDRESS_FORMAT)); + } + + private static string GetLocalizationOrDefault(string key) + { + return LocManager.GetCodeBehindOrDefault(key, LOCALIZATION_KEY, key); + } +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs b/NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs new file mode 100644 index 0000000..a2d1efa --- /dev/null +++ b/NebulaAuth/ViewModel/Other/SetEncryptPasswordVM.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace NebulaAuth.ViewModel.Other; + +public partial class SetEncryptPasswordVM : ObservableObject +{ + [ObservableProperty] private string? _password; +} \ No newline at end of file diff --git a/NebulaAuth/ViewModel/Other/SettingsVM.cs b/NebulaAuth/ViewModel/Other/SettingsVM.cs new file mode 100644 index 0000000..073f0ef --- /dev/null +++ b/NebulaAuth/ViewModel/Other/SettingsVM.cs @@ -0,0 +1,122 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NebulaAuth.Model; +using System.Collections.Generic; +using System.Windows.Media; +using NebulaAuth.Core; + +namespace NebulaAuth.ViewModel.Other; + +public partial class SettingsVM : ObservableObject +{ + public Settings Settings => Settings.Instance; + + public bool DisableTimersOnChange + { + get => Settings.DisableTimersOnChange; + set => Settings.DisableTimersOnChange = value; + } + public BackgroundMode BackgroundMode + { + get => Settings.BackgroundMode; + set => Settings.BackgroundMode = value; + } + + public bool HideToTray + { + get => Settings.HideToTray; + set => Settings.HideToTray = value; + } + + public Dictionary BackgroundModes => new() + { + {BackgroundMode.Default, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "Default")}, + {BackgroundMode.Custom, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "Custom")}, + {BackgroundMode.Color, LocManager.GetOrDefault("Default", "SettingsDialog", "BackgroundMode", "NoBackground")}, + }; + + public Dictionary Languages { get; } = new() + { + {LocalizationLanguage.English, "English"}, + {LocalizationLanguage.Russian, "Русский"}, + {LocalizationLanguage.Ukrainian, "Українська"} + }; + + public Color? BackgroundColor + { + get => Settings.BackgroundColor; + set => Settings.BackgroundColor = value; + } + + public Color? IconColor + { + get => Settings.IconColor; + set => Settings.IconColor = value; + + } + + public bool UseIcon + { + get => IconColor != null; + set + { + if (value == false) + IconColor = null; + else + IconColor = Color.FromRgb(202, 39, 39); + + OnPropertyChanged(); + OnPropertyChanged(nameof(IconColor)); + } + } + + public bool UseBackground + { + get => BackgroundColor != null; + set + { + if (value == false) + BackgroundColor = null; + else + BackgroundColor = Color.FromRgb(202, 39, 39); + + OnPropertyChanged(); + OnPropertyChanged(nameof(BackgroundColor)); + } + } + + + + public LocalizationLanguage Language + { + get => Settings.Language; + set + { + Settings.Language = value; + LocManager.SetApplicationLocalization(value); + } + } + + public bool LegacyMode + { + get => Settings.LegacyMode; + set => Settings.LegacyMode = value; + } + + public bool AllowAutoUpdate + { + get => Settings.AllowAutoUpdate; + set => Settings.AllowAutoUpdate = value; + } + + [ObservableProperty] private string _password; + + + [RelayCommand] + public void SetPassword() + { + PHandler.SetPassword(Password); + Settings.IsPasswordSet = PHandler.IsPasswordSet; + } + +} \ No newline at end of file diff --git a/NebulaAuth/localization.loc.json b/NebulaAuth/localization.loc.json new file mode 100644 index 0000000..3119657 --- /dev/null +++ b/NebulaAuth/localization.loc.json @@ -0,0 +1,949 @@ +{ + "LanguageName": { + "en": "English", + "ru": "Русский", + "ua": "Українська" + }, + "LanguageWord": { + "en": "Language", + "ru": "Язык", + "ua": "Мова" + }, + "Common": { + "No": { + "en": "No", + "ru": "Нет", + "ua": "Ні" + }, + "Yes": { + "en": "Yes", + "ru": "Да", + "ua": "Так" + }, + "Error": { + "en": "Error", + "ru": "Ошибка", + "ua": "Помилка" + }, + "RequestError": { + "en": "Request error", + "ru": "Ошибка запроса", + "ua": "Помилка запиту" + }, + "Canceled": { + "en": "Canceled", + "ru": "Отменено", + "ua": "Скасовано" + }, + "Abbreviations": { + "Time": { + "Seconds": { + "en": "sec", + "ru": "сек", + "ua": "сек" + } + }, + "Count": { + "Items": { + "en": "items", + "ru": "шт", + "ua": "шт" + } + } + } + }, + "MainWindow": { + "Menu": { + "File": { + "Caption": { + "en": "File", + "ru": "Файл", + "ua": "Файл" + }, + "Import": { + "en": "Import", + "ru": "Импорт", + "ua": "Імпорт" + }, + "Remove": { + "en": "Remove", + "ru": "Удалить", + "ua": "Видалити" + }, + "OpenFolder": { + "en": "Open folder", + "ru": "Открыть папку", + "ua": "Відкрити папку" + }, + "Settings": { + "en": "Settings", + "ru": "Настройки", + "ua": "Налаштування" + } + }, + "Account": { + "Caption": { + "en": "Account", + "ru": "Аккаунт", + "ua": "Аккаунт" + }, + "Link": { + "en": "Link", + "ru": "Привязать", + "ua": "Прив'язати" + }, + "Unlink": { + "en": "Unlink", + "ru": "Отвязать", + "ua": "Відв'язати" + }, + "RefreshSession": { + "en": "Refresh session", + "ru": "Обновить сессию", + "ua": "Оновити сесію" + }, + "LoginAgain": { + "en": "Login", + "ru": "Войти", + "ua": "Увійти" + } + } + }, + "AppBar": { + "GroupsHint": { + "en": "Groups", + "ru": "Группы", + "ua": "Групи" + }, + "Proxy": { + "ProxyHint": { + "en": "Proxy", + "ru": "Прокси", + "ua": "Проксі" + }, + "ProxyOpenManagerHint": { + "en": "Open proxy manager", + "ru": "Открыть менеджер прокси", + "ua": "Відкрити менеджер проксі" + }, + "ProxyAlert": { + "DefaultInUse": { + "en": "Default proxy is in use", + "ru": "Используется прокси по умолчанию", + "ua": "Використовується проксі за замовчуванням" + }, + "MafileProxyInUse": { + "en": "Mafile proxy is in use", + "ru": "Используется прокси из mafile", + "ua": "Використовується проксі з mafile" + } + } + }, + + "TradeTimerHint": { + "en": "Trade", + "ru": "Трейд", + "ua": "Трейд" + }, + "MarketTimerHint": { + "en": "Market", + "ru": "Маркет", + "ua": "Маркет" + } + }, + "LeftPart": { + "SearchBoxHint": { + "en": "Search", + "ru": "Поиск", + "ua": "Пошук" + } + }, + "RightPart": { + "LoadConfirmations": { + "en": "Load confirmations", + "ru": "Загрузить подтверждения", + "ua": "Завантажити підтвердження" + }, + "ConfirmLogin": { + "en": "Confirm login", + "ru": "Подтвердить вход", + "ua": "Підтвердити вхід" + } + }, + "Footer": { + "Account": { + "en": "Account: ", + "ru": "Аккаунт: ", + "ua": "Аккаунт: " + }, + "Group": { + "en": "Group: ", + "ru": "Группа: ", + "ua": "Група: " + } + }, + "ConfirmationTemplates": { + "TradeWith": { + "en": "Trade with", + "ru": "Трейд с", + "ua": "Трейд з" + }, + "Recovery": { + "en": "Recovery", + "ru": "Восстановление", + "ua": "Відновлення" + }, + "ApiKey": { + "en": "Api key", + "ru": "Api ключ", + "ua": "Api ключ" + }, + "Market": { + "en": "Market", + "ru": "Маркет", + "ua": "Маркет" + } + }, + "CodeCopied": { + "ru": "Скопировано", + "en": "Copied", + "ua": "Скопійовано" + }, + "ContextMenus": { + "Mafile": { + "AddToGroup": { + "en": "Add to group", + "ru": "Добавить в группу", + "ua": "Додати в групу" + }, + "RemoveFromGroup": { + "en": "Remove from group", + "ru": "Удалить из группы", + "ua": "Видалити з групи" + } + }, + "Proxy": { + "Copy": { + "en": "Copy", + "ru": "Копировать", + "ua": "Копіювати" + }, + "CopyAddress": { + "en": "Copy address", + "ru": "Копировать адрес", + "ua": "Копіювати адресу" + } + } + } + }, + "SettingsDialog": { + "Title": { + "en": "Settings", + "ru": "Настройки", + "ua": "Налаштування" + }, + "BackgroundHint": { + "en": "Background", + "ru": "Фон", + "ua": "Фон" + }, + "BackgroundMode": { + "Default": { + "en": "Default", + "ru": "По умолчанию", + "ua": "За замовчуванням" + }, + "Custom": { + "en": "Custom", + "ru": "Свой", + "ua": "Свій" + }, + "NoBackground": { + "en": "No background", + "ru": "Без фона", + "ua": "Без фону" + } + }, + "DisableTimersOnSwitch": { + "en": "Disable timers on account switch", + "ru": "Отключать таймеры при смене аккаунта", + "ua": "Вимикати таймери при зміні аккаунта" + }, + "MinimizeToTray": { + "en": "Minimize to tray", + "ru": "Сворачивать в трей", + "ua": "Згортати в трей" + }, + "UseIndicator": { + "en": "Use indicator", + "ru": "Использовать индикатор", + "ua": "Використовувати індикатор" + }, + "UseCustomColor": { + "en": "Custom window color", + "ru": "Свой цвет окна", + "ua": "Свій колір вікна" + }, + "PasswordBox": { + "CurrentCryptPassword": { + "en": "Current encryption password", + "ru": "Текущий пароль шифрования", + "ua": "Поточний пароль шифрування" + }, + "Hint": { + "en": "Doesn't saved on disk", + "ru": "Не сохраняется в файл", + "ua": "Не зберігається у файлах" + } + }, + "LegacyMafileMode": { + "en": "Legacy mafile mode", + "ru": "Режим старых мафайлов", + "ua": "Режим старих мафайлів" + }, + "LegacyMafileModeHint": { + "en": "Use old mafile format for compatibility with old versions of Steam Desktop Authenticator", + "ru": "Использовать старый формат мафайлов для совместимости со старыми версиями Steam Desktop Authenticator", + "ua": "Використовувати старий формат мафайлів для сумісності зі старими версіями Steam Desktop Authenticator" + }, + "AllowAutoUpdate": { + "en": "Allow auto update", + "ru": "Разрешить автообновление", + "ua": "Дозволити автооновлення" + } + }, + "LoginAgainDialog": { + "Title": { + "en": "Login", + "ru": "Вход", + "ua": "Вхід" + }, + "LoginFor": { + "en": "Login for", + "ru": "Войти в", + "ua": "Увійти в" + }, + "LoginButton": { + "en": "Login", + "ru": "Войти", + "ua": "Увійти" + }, + "CancelButton": { + "en": "Cancel", + "ru": "Отмена", + "ua": "Відміна" + }, + "PasswordBox": { + "en": "Password", + "ru": "Пароль", + "ua": "Пароль" + }, + "SaveEncryptedPassword": { + "en": "Save encrypted password to mafile", + "ru": "Сохранить зашифрованный пароль в мафайл", + "ua": "Зберегти зашифрований пароль в мафайлі" + } + }, + "ProxyManagerDialog": { + "Title": { + "en": "Proxy manager", + "ru": "Менеджер прокси", + "ua": "Менеджер проксі" + }, + "DefaultProxy": { + "en": "Default proxy:", + "ru": "Прокси по умолчанию:", + "ua": "Проксі за замовчуванням:" + } + + }, + "WaitLoginDialog": { + "Text": { + "en": "Login in progress. Please wait", + "ru": "Производится логин. Ожидайте", + "ua": "Виконується вхід. Зачекайте" + } + }, + "LinkerDialog": { + "Title": { + "en": "Linker", + "ru": "Привязка", + "ua": "Прив'язка" + }, + "Proxy": { + "en": "Proxy", + "ru": "Прокси", + "ua": "Проксі" + }, + "Authorization": { + "en": "Authorization", + "ru": "Авторизация", + "ua": "Авторизація" + }, + "EmailCode": { + "en": "Email code", + "ru": "Код из письма", + "ua": "Код з листа" + }, + "PhoneNumber": { + "en": "Phone number", + "ru": "Номер телефона", + "ua": "Номер телефону" + }, + "EmailLink": { + "en": "Email link", + "ru": "Ссылка из письма", + "ua": "Посилання з листа" + }, + "SmsOrCode": { + "en": "Sms or code", + "ru": "Смс или код", + "ua": "Смс або код" + }, + "Completed": { + "en": "Completed", + "ru": "Завершено", + "ua": "Завершено" + }, + "Message": { + "en": "Message:", + "ru": "Сообщение:", + "ua": "Повідомлення:" + }, + "ProceedButton": { + "en": "Proceed", + "ru": "Продолжить", + "ua": "Продовжити" + } + }, + "SetEncryptedPasswordDialog": { + "DialogText": { + "en": "You have previously set an encryption password for passwords in mafiles, specify it so that the application can automatically log in in case of problems with the session. (Optional)", + "ru": "Ранее вы устанавливали пароль шифрования для паролей в мафайлах, укажите его, чтобы приложение могло автоматически авторизоваться в случае проблем с сессией. (Не обязательно)", + "ua": "Раніше ви встановлювали пароль шифрування для паролів в мафайлах, вкажіть його, щоб додаток міг автоматично авторизуватися у випадку проблем з сесією. (Не обов'язково)" + }, + "Ok": { + "en": "Ok", + "ru": "Ок", + "ua": "Ок" + }, + "Cancel": { + "en": "Cancel", + "ru": "Отмена", + "ua": "Відміна" + }, + "Password": { + "en": "Password", + "ru": "Пароль", + "ua": "Пароль" + } + }, + "CodeBehind": { + "MainVM": { + "SuccessfulLogin": { + "en": "Successful login", + "ru": "Успешный вход", + "ua": "Успішний вхід" + }, + "SessionRefreshed": { + "en": "Session refreshed", + "ru": "Сессия обновлена", + "ua": "Сесія оновлена" + }, + "MissingRCode": { + "en": "Missing RCode", + "ru": "Отсутствует RCode", + "ua": "Відсутній RCode" + }, + "ConfirmRemovingAuthenticator": { + "ru": "Вы точно уверены что хотите удалить аутентификатор?", + "en": "Are you sure you want to remove authenticator?", + "ua": "Ви точно впевнені що хочете видалити аутентифікатор?" + }, + "AuthenticatorRemoved": { + "ru": "Аутентификатор удален", + "en": "Authenticator removed", + "ua": "Аутентифікатор видалено" + }, + "AuthenticatorNotRemoved": { + "ru": "Что-то пошло не так", + "en": "Something went wrong", + "ua": "Щось пішло не так" + }, + "ConfirmLoginSuccess": { + "ru": "Вход осуществлен:", + "en": "Login success:", + "ua": "Вхід здійснено:" + }, + "ConfirmLoginFailedNoRequests": { + "ru": "Запросов на вход не обнаружено", + "en": "No login requests found", + "ua": "Запитів на вхід не виявлено" + }, + "ConfirmLoginFailedMoreThanOneRequest": { + "ru": "Обнаружено несколько запросов на вход. В целях безопасности операция отменена", + "en": "Multiple login requests found. For security reasons, the operation is canceled", + "ua": "Виявлено декілька запитів на вхід. З метою безпеки операція скасована" + }, + "ConfirmationError": { + "ru": "Ошибка подтверждения", + "en": "Confirmation error", + "ua": "Помилка підтвердження" + }, + "ConfirmMafileOverwrite": { + "ru": "Внимание, мафайл уже присутствует в папке mafiles.\r\nПерезаписать мафайлы, которые конфликтуют?", + "en": "Attention, mafile already exists in mafiles folder.\r\nOverwrite conflicting mafiles?", + "ua": "Увага, мафайл вже присутній у папці mafiles.\r\nПерезаписати мафайли, які конфліктують?" + }, + "MafileImportError": { + "ru": "Ошибка импорта мафайла", + "en": "Mafile import error", + "ua": "Помилка імпорту мафайла" + }, + "MissingSessionInMafile": { + "ru": ". У него отсутствует сессия. Добавьте этот файл отдельно от остальных", + "en": ". It has no session. Add this file separately from the others", + "ua": ". В нього відсутня сесія. Додайте цей файл окремо від інших" + }, + "Import": { + "ru": "Импорт", + "en": "Import", + "ua": "Імпорт" + }, + "ImportAdded": { + "ru": "добавлено:", + "en": "added:", + "ua": "додано:" + }, + "ImportSkipped": { + "ru": "пропущено:", + "en": "skipped:", + "ua": "пропущено:" + }, + "ImportErrors": { + "ru": "ошибки:", + "en": "errors:", + "ua": "помилки:" + }, + "RemoveMafileConfirmation": { + "ru": "Удалить мафайл? Это действие переместит мафайл в mafiles_removed", + "en": "Remove mafile? This action will move mafile to mafiles_removed", + "ua": "Видалити мафайл? Ця дія перемістить мафайл в mafiles_removed" + }, + "CantRemoveAlreadyExist": { + "ru": "Не удалось переместить мафайл, возможно в папке уже существует мафайл с таким именем.", + "en": "Can't move mafile, maybe mafile with same name already exists in mafiles_removed folder.", + "ua": "Не вдалося перемістити мафайл, можливо в папці вже існує мафайл з таким іменем." + }, + "CantRemoveMafile": { + "ru": "Не удалось удалить (переместить) мафайл:", + "en": "Can't remove mafile:", + "ua": "Не вдалося видалити (перемістити) мафайл:" + }, + "TimerConfirmed": { + "ru": "Авто: подтверждено ", + "en": "Auto: confirmed ", + "ua": "Авто: підтверджено " + }, + "TimerNotConfirmed": { + "ru": "Авто: подтверждение не сработало", + "en": "Auto: confirmation was unsuccessful", + "ua": "Авто: підтвердження не спрацювало" + }, + "TimerSessionError": { + "ru": "Авто: необходимо обновить сессию или перелогиниться", + "en": "Auto: need to refresh session or relogin", + "ua": "Авто: необхідно оновити сесію або перелогінитися" + }, + "TimerPrefix": { + "ru": "Авто: ", + "en": "Auto: ", + "ua": "Авто: " + }, + "TimerTooFast": { + "ru": "Слишком быстрый таймер.", + "en": "Too fast timer.", + "ua": "Занадто швидкий таймер." + } + }, + "ErrorTranslator": { + "Login": { + "CaptchaRequired": { + "en": "Captcha", + "ru": "Капча", + "ua": "Капча" + }, + "InvalidCredentials": { + "en": "Invalid password", + "ru": "Неверный пароль", + "ua": "Невірний пароль" + + }, + "EmailAuthRequired": { + "en": "Email code required", + "ru": "Требуется код с почты", + "ua": "Потрібен код з пошти" + }, + "InvalidEmailAuthCode": { + "en": "Invalid email code", + "ru": "Неверный код с почты", + "ua": "Невірний код з пошти" + }, + "InvalidTwoFactorCode": { + "en": "Invalid two factor code", + "ru": "Неверный двухфакторный код", + "ua": "Невірний двофакторний код" + }, + "UndefinedError": { + "en": "Undefined error", + "ru": "Неизвестная ошибка", + "ua": "Невідома помилка" + } + }, + "EResult": { + "InvalidPassword": { + "ru": "Неверный пароль", + "en": "Invalid password", + "ua": "Невірний пароль" + }, + "InvalidState": { + "ru": "Неверное состояние (11)", + "en": "Invalid state (11)", + "ua": "Невірний стан (11)" + + }, + "Timeout": { + "ru": "Таймаут (16)", + "en": "Timeout (16)", + "ua": "Таймаут (16)" + + }, + "Banned": { + "ru": "Забанен (17)", + "en": "Banned (17)", + "ua": "Забанений (17)" + }, + "AccountNotFound": { + "ru": "Аккаунт не найден (18)", + "en": "Account not found (18)", + "ua": "Аккаунт не знайдено (18)" + }, + "InvalidSteamID": { + "ru": "Неправильный SteamID (19)", + "en": "Invalid SteamID (19)", + "ua": "Неправильний SteamID (19)" + }, + "ServiceUnavailable": { + "ru": "Сервер не отвечает (20)", + "en": "Service unavailable (20)", + "ua": "Сервер не відповідає (20)" + }, + "Pending": { + "ru": "Ожидание (21)", + "en": "Pending (21)", + "ua": "Очікування (21)" + }, + "LimitExceeded": { + "ru": "Лимит превышен (25)", + "en": "Limit exceeded (25)", + "ua": "Ліміт перевищено (25)" + }, + "Expired": { + "ru": "Истекло (27)", + "en": "Expired (27)", + "ua": "Термін дії закінчився (27)" + }, + "Blocked": { + "ru": "Заблокировано (40)", + "en": "Blocked (40)", + "ua": "Заблоковано (40)" + }, + "AccountDisabled": { + "ru": "Аккаунт отключен (43)", + "en": "Account disabled (43)", + "ua": "Аккаунт відключено (43)" + + }, + "Suspended": { + "ru": "Отключено (Suspended 51)", + "en": "Suspended (51)", + "ua": "Відключено (Suspended 51)" + }, + "Cancelled": { + "ru": "Отмена (52)", + "en": "Cancelled (52)", + "ua": "Скасовано (52)" + + }, + "ExpiredLoginAuthCode": { + "ru": "Истекло время кода (71)", + "en": "Expired login auth code (71)", + "ua": "Термін дії коду закінчився (71)" + + }, + "AccountLockedDown": { + "ru": "Аккаунт заблокирован (КТ 73)", + "en": "Account locked down (73)", + "ua": "Аккаунт заблоковано (КТ 73)" + + }, + "UnexpectedError": { + "ru": "Неожиданная ошибка (79)", + "en": "Unexpected error (79)", + "ua": "Непередбачувана помилка (79)" + }, + "Disabled": { + "ru": "Отключено (80)", + "en": "Disabled (80)", + "ua": "Відключено (80)" + }, + "RegionLocked": { + "ru": "Регион заблокирован (83)", + "en": "Region locked (83)", + "ua": "Регіон заблоковано (83)" + }, + "RateLimitExceeded": { + "ru": "Слишком много запросов (84)", + "en": "Rate limit exceeded (84)", + "ua": "Забагато запитів (84)" + }, + "TwoFactorCodeMismatch": { + "ru": "2FA код не подошел (88)", + "en": "2FA code mismatch (88)", + "ua": "2FA код не підійшов (88)" + }, + "TwoFactorActivationCodeMismatch": { + "ru": "2FA код не прошел проверку (89)", + "en": "2FA activation code mismatch (89)", + "ua": "2FA код не пройшов перевірку (89)" + + }, + "TimeNotSynced": { + "ru": "Время не синхронизировано (93)", + "en": "Time not synced (93)", + "ua": "Час не синхронізовано (93)" + + }, + "SMSCodeFailed": { + "ru": "Ошибка SMS-кода (94)", + "en": "SMS code failed (94)", + "ua": "Помилка SMS-коду (94)" + }, + "AccountLimitExceeded": { + "ru": "Лимит аккаунта превышен (95)", + "en": "Account limit exceeded (95)", + "ua": "Ліміт аккаунта перевищено (95)" + + }, + "EmailSendFailure": { + "ru": "Не удалось отправить письмо (99)", + "en": "Email send failure (99)", + "ua": "Не вдалося відправити лист (99)" + + }, + "NeedCaptcha": { + "ru": "Капча (101)", + "en": "Need captcha (101)", + "ua": "Капча (101)" + + }, + "IPBanned": { + "ru": "IP заблокирован (105)", + "en": "IP banned (105)", + "ua": "IP заблоковано (105)" + + }, + "LimitedUserAccount": { + "ru": "Аккаунт с лимитом (112)", + "en": "Limited user account (112)", + "ua": "Аккаунт з лімітом (112)" + }, + "AccountDeleted": { + "ru": "Аккаунт удален (114)", + "en": "Account deleted (114)", + "ua": "Аккаунт видалено (114)" + + }, + "PhoneNumberIsVOIP": { + "ru": "Номера VOIP запрещены (127)", + "en": "Phone number is VOIP (127)", + "ua": "Номера VOIP заборонені (127)" + } + }, + "AuthenticatorLinkerError": { + "PhoneAlreadyAttached": { + "ru": "Телефон привязан, попробуйте через 20 минут", + "en": "Phone already attached, try again in 20 minutes", + "ua": "Телефон прив'язано, спробуйте через 20 хвилин" + }, + "InvalidPhoneNumber": { + "ru": "Неверный номер телефона", + "en": "Invalid phone number", + "ua": "Невірний номер телефону" + }, + "CantAttachPhone": { + "ru": "Не удается привязать телефон", + "en": "Can't attach phone", + "ua": "Не вдається прив'язати телефон" + }, + "CantConfirmAttachingEmail": { + "ru": "Не удалось подтвердить письмо", + "en": "Can't confirm attaching email", + "ua": "Не вдалося підтвердити лист" + }, + "CantSendSms": { + "ru": "Не удалось отправить SMS", + "en": "Can't send SMS", + "ua": "Не вдалося відправити SMS" + }, + "AuthenticatorPresent": { + "ru": "Аутентификатор привязан", + "en": "Authenticator present", + "ua": "Аутентифікатор прив'язано" + }, + "BadConfirmationCode": { + "ru": "Код не подошел", + "en": "Bad confirmation code", + "ua": "Код не підійшов" + }, + "UnableToGenerateCorrectCodes": { + "ru": "Не удается сгенерировать коды", + "en": "Unable to generate correct codes", + "ua": "Не вдається згенерувати коди" + }, + "InvalidStateWithStatus2": { + "ru": "Неверное состояние (InvalidState) со статусом 2. Попробуйте привязку с помощью СМС. Если СМС придет, но ошибка повторится. Нужно попробовать еще раз.", + "en": "Invalid state (InvalidState) with status 2. Try to attach with SMS. If SMS will come, but error will repeat. You need to try again.", + "ua": "Невірний стан (InvalidState) зі статусом 2. Спробуйте прив'язати з допомогою СМС. Якщо СМС прийде, але помилка повториться. Потрібно спробувати ще раз." + }, + "GeneralFailure": { + "ru": "General Failure", + "en": "General Failure", + "ua": "General Failure" + } + } + }, + "ExceptionHandler": { + "SessionExpiredException": { + "ru": "Сессия истекла. Попробуйте обновить ее через меню", + "en": "Session expired. Try to refresh it through menu", + "ua": "Сесія закінчилася. Спробуйте оновити її через меню" + }, + "SessionInvalidException": { + "ru": "Сессия невалидна. Нужно залогиниться заново", + "en": "Session invalid. Need to login again", + "ua": "Сесія невалідна. Потрібно залогінитися знову" + }, + "TaskCanceledException": { + "ru": "Произошла отмена операции", + "en": "Operation canceled", + "ua": "Операція скасована" + }, + "TimeoutException": { + "ru": "Таймаут подключения. Проверьте ваше соединение с Proxy или интернетом", + "en": "Connection timeout. Check your connection with Proxy or internet", + "ua": "Таймаут підключення. Перевірте ваше з'єднання з Proxy або інтернетом" + }, + "UnsupportedResponseException": { + "ru": "Получен неожиданный ответ от Steam. Результат сохранен в log.log. Свяжитесь с разработчиком", + "en": "Unexpected response from Steam. Result saved in log.log. Contact developer", + "ua": "Отримано неочікувану відповідь від Steam. Результат збережено в log.log. Зв'яжіться з розробником" + }, + "LoginException": { + "ru": "Ошибка входа: ", + "en": "Login error: ", + "ua": "Помилка входу: " + }, + "UnknownException": { + "ru": "Неизвестная ошибка: ", + "en": "Unknown error: ", + "ua": "Невідома помилка: " + } + + }, + "ProxyManagerVM": { + "WrongFormatSomeIdsMissing": { + "ru": "Неверный формат. Некоторые прокси не имеют ID", + "en": "Wrong format. Some proxies have no ID", + "ua": "Невірний формат. Деякі проксі не мають ID" + }, + "DuplicateId": { + "ru": "Прокси с ID {0} добавлен несколько раз", + "en": "Proxy with ID {0} added multiple times", + "ua": "Проксі з ID {0} додано декілька разів" + }, + "WrongFormatOnLine": { + "ru": "Прокси на строке {0} имеет неверный формат.", + "en": "Proxy on line {0} has wrong format.", + "ua": "Проксі на строці {0} має невірний формат." + }, + "WrongFormat": { + "ru": "Прокси имеет неверный формат.", + "en": "Proxy has wrong format.", + "ua": "Проксі має невірний формат." + } + }, + "LinkVM": { + "EnterLoginAndPassword": { + "ru": "Введите логин и пароль", + "en": "Enter login and password", + "ua": "Введіть логін і пароль" + }, + "CantLogin": { + "ru": "Не удалось залогиниться: ", + "en": "Can't login: ", + "ua": "Не вдалося залогінитися: " + }, + "MafileLinked": { + "ru": "Мафайл успешно привязан. RCode: {0}\r\nИмя файла: {1}.mafile", + "en": "Mafile successfully linked. RCode: {0}\r\nFile name: {1}.mafile", + "ua": "Мафайл успішно прив'язано. RCode: {0}\r\nІм'я файлу: {1}.mafile" + }, + "ErrorWithCode": { + "ru": "Ошибка с кодом: ", + "en": "Error with code: ", + "ua": "Помилка з кодом: " + }, + "UnknownError": { + "ru": "Неизвестная ошибка: ", + "en": "Unknown error: ", + "ua": "Невідома помилка: " + }, + "PleaseEnterCorrectPhone": { + "ru": "Пожалуйста, введите корректный номер телефона", + "en": "Please enter correct phone number", + "ua": "Будь ласка, введіть коректний номер телефону" + }, + "PleaseEnterCorrectCode": { + "ru": "Пожалуйста, введите корректный код", + "en": "Please enter correct code", + "ua": "Будь ласка, введіть коректний код" + }, + "EnterEmailCode": { + "ru": "Введите код из письма", + "en": "Enter code from email", + "ua": "Введіть код з листа" + }, + "ClickOnEmailLink": { + "ru": "На почту было отправлено письмо, нажмите по ссылке затем - 'Продолжить'", + "en": "Email was sent, click on link and then - 'Proceed'", + "ua": "На пошту було відправлено лист, натисніть по посиланню потім - 'Продовжити'" + }, + "PhoneHint": { + "ru": "На телефон {0} была отправлена СМС", + "en": "SMS was sent to phone {0}", + "ua": "На телефон {0} було відправлено СМС" + }, + "EnterPhoneNumber": { + "ru": "Введите номер телефона (без знака '+' и др. символов)", + "en": "Enter phone number (without '+' and other symbols)", + "ua": "Введіть номер телефону (без знака '+' та ін. символів)" + } + } + }, + "CantAlignTimeError": { + "ru": "Не удается синхронизировать время со Steam. Возможно отсутствует подключение к интернету или Steam недоступен. Подробная ошибка сохранена в log.log", + "en": "Can't align time with Steam. Maybe no internet connection or Steam is unavailable. Detailed error saved in log.log", + "ua": "Не вдається синхронізувати час з Steam. Можливо відсутнє підключення до інтернету або Steam недоступний. Детальна помилка збережена в log.log" + } +} diff --git a/SteamLibForked/Account/MobileSessionData.cs b/SteamLibForked/Account/MobileSessionData.cs new file mode 100644 index 0000000..e68b6bd --- /dev/null +++ b/SteamLibForked/Account/MobileSessionData.cs @@ -0,0 +1,51 @@ +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace SteamLib.Account; + +//WARNING: Any changes here should be reflected in MafileSerializer.cs + +public sealed class MobileSessionData : SessionData, IMobileSessionData +{ + public SteamAuthToken? MobileToken { get; private set; } + + public SteamAuthToken? GetMobileToken() => MobileToken; + + + [MemberNotNull(nameof(MobileToken))] + public void SetMobileToken(SteamAuthToken token) + { + if (token.Type != SteamAccessTokenType.Mobile) + throw new ArgumentException("Token must be of type MobileAccess", nameof(token)) + { + Data = { { "ActualType", token.Type } } + }; + + MobileToken = token; + } + + [JsonConstructor] + public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, + SteamAuthToken? mobileToken, IDictionary? tokens) + : base(sessionId, steamId, refreshToken, tokens) + { + MobileToken = mobileToken; + } + + public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, + SteamAuthToken? mobileToken, IEnumerable? tokens) + : base(sessionId, steamId, refreshToken, tokens) + { + MobileToken = mobileToken; + } + + public override MobileSessionData Clone() => (MobileSessionData)((ISessionData)this).Clone(); + object ICloneable.Clone() + { + return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens); + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Account/SessionData.cs b/SteamLibForked/Account/SessionData.cs new file mode 100644 index 0000000..662e585 --- /dev/null +++ b/SteamLibForked/Account/SessionData.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Web.Converters; +using System.Collections.Concurrent; + +namespace SteamLib.Account; + +public class SessionData : ISessionData +{ + [JsonIgnore] + public bool? IsValid { get; set; } + public string SessionId { get; } + + [JsonConverter(typeof(SteamIdToSteam64Converter))] + public SteamId SteamId { get; } + public SteamAuthToken RefreshToken { get; } + public ConcurrentDictionary Tokens { get; } + + [JsonConstructor] + public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IDictionary? tokens) + { + SessionId = sessionId; + SteamId = steamId; + RefreshToken = refreshToken; + Tokens = new ConcurrentDictionary(tokens ?? new Dictionary()); + } + + public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IEnumerable? tokens) + { + SessionId = sessionId; + SteamId = steamId; + RefreshToken = refreshToken; + tokens ??= Array.Empty(); + Tokens = new ConcurrentDictionary(tokens.ToDictionary(t => t.Domain, t => t)); + } + + public virtual SteamAuthToken? GetToken(SteamDomain domain) + { + if (domain == SteamDomain.Undefined) + throw new ArgumentException("SteamDomain.Undefined is not allowed"); + if (Tokens.TryGetValue(domain, out var token)) + return token; + return null; + + } + + public void SetToken(SteamDomain domain, SteamAuthToken token) + { + Tokens[domain] = token; + } + + + + public virtual SessionData Clone() => (SessionData)((ICloneable)this).Clone(); + object ICloneable.Clone() + { + return new SessionData(SessionId, SteamId, RefreshToken, Tokens); + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Account/SteamAuthToken.cs b/SteamLibForked/Account/SteamAuthToken.cs new file mode 100644 index 0000000..2ea2507 --- /dev/null +++ b/SteamLibForked/Account/SteamAuthToken.cs @@ -0,0 +1,49 @@ +using AchiesUtilities.Models; +using AchiesUtilities.Newtonsoft.JSON.Converters.Special; +using Newtonsoft.Json; +using SteamLib.Authentication; +using SteamLib.Core.Enums; +using SteamLib.Web.Converters; + +namespace SteamLib.Account; + +public readonly struct SteamAuthToken +{ + public string Token { get; } + + [JsonConverter(typeof(SteamIdToSteam64Converter))] + public SteamId SteamId { get; } + + [JsonConverter(typeof(UnixTimeStampConverter))] + public UnixTimeStamp Expires { get; } + public SteamDomain Domain { get; init; } + public SteamAccessTokenType Type { get; } + + [JsonIgnore] + public bool IsExpired => Expires.Time < DateTime.UtcNow; + + [JsonIgnore] + public string SignedToken { get; } + + public SteamAuthToken(string token, long steamId, UnixTimeStamp expires, SteamDomain domain, SteamAccessTokenType type) + { + Token = token; + Expires = expires; + Domain = domain; + Type = type; + SteamId = SteamId.FromSteam64(steamId); + SignedToken = SteamTokenHelper.CombineJwtWithSteamId(SteamId.Steam64.Id, Token); + } + + [JsonConstructor] + public SteamAuthToken(string token, SteamId steamId, UnixTimeStamp expires, SteamDomain domain, SteamAccessTokenType type) + { + Token = token; + SteamId = steamId; + Expires = expires; + Domain = domain; + Type = type; + SignedToken = SteamTokenHelper.CombineJwtWithSteamId(SteamId.Steam64.Id, Token); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Account/SteamId.cs b/SteamLibForked/Account/SteamId.cs new file mode 100644 index 0000000..4985a58 --- /dev/null +++ b/SteamLibForked/Account/SteamId.cs @@ -0,0 +1,180 @@ +using SteamLib.Utility; + +namespace SteamLib.Account; + +public struct SteamId //TODO: validation in parse methods +{ + public SteamId64 Steam64 { get; } + public SteamId2 Steam2 { get; } + public SteamId3 Steam3 { get; } + public SteamId(SteamId64 steam64, char type = 'U', short universe = 0) + { + Steam64 = steam64; + Steam2 = steam64.ToSteam2(universe); + Steam3 = steam64.ToSteam3(type); + } + + public SteamId(SteamId2 steam2, char type = 'U') + { + Steam2 = steam2; + Steam64 = steam2.ToSteam64(); + Steam3 = steam2.ToSteam3(type); + } + + public SteamId(SteamId3 steam3, byte universe = 0) + { + Steam3 = steam3; + Steam64 = steam3.ToSteam64(); + Steam2 = steam3.ToSteam2(universe); + } + + public static SteamId FromSteam64(long steam64, char type = 'U', short universe = 0) => new(new SteamId64(steam64), type, universe); + public override string ToString() => Steam64.ToString(); + public static bool TryParse(string input, out SteamId result) => SteamIdParser.TryParse(input, out result); + + /// + public static SteamId Parse(string input) => SteamIdParser.Parse(input); + public static bool operator ==(SteamId left, SteamId right) => left.Equals(right); + public static bool operator !=(SteamId left, SteamId right) => !left.Equals(right); + public bool Equals(SteamId other) => Steam64.Equals(other.Steam64); + public override bool Equals(object? obj) => obj is SteamId other && Equals(other); + public override int GetHashCode() => Steam64.GetHashCode(); +} + +public struct SteamId64 +{ + public const long SEED = 76561197960265728L; + public long Id { get; } + public SteamId64(long id) + { + if (id < SEED) + throw new ArgumentOutOfRangeException(nameof(id),$"Invalid SteamID provided {id}"); + Id = id; + } + + public SteamId2 ToSteam2(short universe = 0) + { + var accountIdLowBit = (byte)(Id & 1); + var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF; + return new SteamId2((byte)universe, accountIdLowBit, accountIdHighBits); + } + + public SteamId3 ToSteam3(char type = 'U') + { + var accountIdLowBit = (byte)(Id & 1); + var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF; + return new SteamId3(accountIdLowBit + accountIdHighBits * 2, type); + } + + + public override string ToString() + { + return Id.ToString(); + } + + public ulong ToUlong() => (ulong)Id; + public long ToLong() => Id; + + public bool Equals(SteamId64 other) => Id == other.Id; + public override bool Equals(object? obj) => obj is SteamId64 other && Equals(other); + public override int GetHashCode() => Id.GetHashCode(); + public static bool operator ==(SteamId64 left, SteamId64 right) => left.Equals(right); + public static bool operator !=(SteamId64 left, SteamId64 right) => !left.Equals(right); + public static bool TryParse(string input, out SteamId64 result) => SteamIdParser.TryParse64(input, out result); + /// + public static SteamId64 Parse(string input) => SteamIdParser.Parse64(input); + + public static implicit operator long(SteamId64 steamId64) => steamId64.ToLong(); +} + +public struct SteamId2 +{ + + public byte Universe { get; } + public byte LowestBit { get; } + public int HighestBit { get; } + + + public SteamId2(byte lowestBit, int highestBit) + { + if(lowestBit > 1) + throw new ArgumentOutOfRangeException(nameof(lowestBit), $"Invalid SteamID2 lowestBit provided {lowestBit}. Max value is 1"); + + if(highestBit < 0) + throw new ArgumentOutOfRangeException(nameof(highestBit), $"Invalid SteamID2 highestBit provided {highestBit}"); + + LowestBit = lowestBit; + HighestBit = highestBit; + Universe = 0; + } + + public SteamId2(byte universe, byte lowestBit, int highestBit) + { + Universe = universe; + LowestBit = lowestBit; + HighestBit = highestBit; + } + + public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + LowestBit + HighestBit * 2); + + public SteamId3 ToSteam3(char type = 'U') => new SteamId3(HighestBit * 2 + LowestBit); + + public override string ToString() => $"STEAM_{Universe}:{LowestBit}:{HighestBit}"; + + public bool Equals(SteamId2 other) => Universe == other.Universe && LowestBit == other.LowestBit && HighestBit == other.HighestBit; + public override bool Equals(object? obj) => obj is SteamId2 other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Universe, LowestBit, HighestBit); + public static bool operator ==(SteamId2 left, SteamId2 right) => left.Equals(right); + public static bool operator !=(SteamId2 left, SteamId2 right) => !left.Equals(right); + public static bool TryParse(string input, out SteamId2 result) => SteamIdParser.TryParse2(input, out result); + /// + public static SteamId2 Parse(string input) => SteamIdParser.Parse2(input); +} + +public struct SteamId3 +{ + public char Type { get; } + public int Id { get; } + + public SteamId3(int id, char type = 'U') + { + if (id < 0) + throw new ArgumentOutOfRangeException(nameof(id), $"Invalid SteamID provided {id}"); + Type = type; + Id = id; + } + + public SteamId2 ToSteam2(byte universe = 0) + { + var bit = Id % 2; + var highestBits = Id / 2; + return new SteamId2(universe, (byte)bit, highestBits); + } + + + public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + Id); + public override string ToString() => $"[{Type}:1:{Id}]"; + + public string ToString(bool withBrackets) + { + if (withBrackets) + { + return ToString(); + } + else + { + return $"{Type}:1:{Id}"; + } + } + + public int ToInt() => Id; + public bool Equals(SteamId3 other) => Type == other.Type && Id == other.Id; + public override bool Equals(object? obj) => obj is SteamId3 other && Equals(other); + public readonly override int GetHashCode() => HashCode.Combine(Type, Id); + public static bool operator ==(SteamId3 left, SteamId3 right) => left.Equals(right); + public static bool operator !=(SteamId3 left, SteamId3 right) => !left.Equals(right); + public static bool TryParse(string input, out SteamId3 result) => SteamIdParser.TryParse3(input, out result); + + /// + public static SteamId3 Parse(string input) => SteamIdParser.Parse3(input); +} \ No newline at end of file diff --git a/SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs b/SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs new file mode 100644 index 0000000..3f933e7 --- /dev/null +++ b/SteamLibForked/Api/Mobile/SteamAuthenticatorLinkerApi.cs @@ -0,0 +1,305 @@ +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using SteamLib.Authentication; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Core.StatusCodes; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Services; +using SteamLib.SteamMobile; +using SteamLib.SteamMobile.AuthenticatorLinker; + +namespace SteamLib.Api.Mobile; + +[PublicAPI] +public static class SteamAuthenticatorLinkerApi +{ + private const string PHONE_REQ = SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax"; + + #region Global + + public static Task HasPhone(HttpClient client, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/AccountPhoneStatus/v1"; + + var reqUri = AddAccessToken(uri, accessToken); + return client.PostProto(reqUri, request: null); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task LinkRequest(HttpClient client, ISessionData data, string deviceId) + { + const string uri = SteamConstants.STEAM_API + "ITwoFactorService/AddAuthenticator/v1"; + data.EnsureValidated(); + var reqUri = AddAccessToken(uri, data.GetAccessToken()); + var req = new AddAuthenticator_Request + { + SteamId = (ulong)data.SteamId.Steam64.Id, + AuthenticatorType = 1, + DeviceIdentifier = deviceId, + Version = 2 + }; + + var resp = await client.PostProtoMsg(reqUri, req); + if (resp is {Result: EResult.InvalidState, ResponseMsg.Status: 2}) + { + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidStateWithStatus2); + } + + return resp.GetResponseEnsureSuccess(); + } + + public static async Task FinalizeLink(HttpClient client, string confirmationCode, ISessionData data, + byte[] sharedSecret, bool validateSmsCode) + { + const string uri = SteamConstants.STEAM_API + "ITwoFactorService/FinalizeAddAuthenticator/v1"; + data.EnsureValidated(); + var reqUri = AddAccessToken(uri, data.GetAccessToken()); + var time = await TimeAligner.GetSteamTimeAsync(); + if (validateSmsCode) + { + var validateSmsReq = new FinalizeAddAuthenticator_Request + { + SteamId = data.SteamId.Steam64.ToUlong(), + AuthenticatorTime = (ulong) time, + ConfirmationCode = confirmationCode, + ValidateConfirmationCode = true + }; + + var validateResp = await client.PostProto(reqUri, validateSmsReq); + if (validateResp.Success == false || validateResp.Status != 2) + { + if (validateResp.Status == 89) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.BadConfirmationCode); + throw new AuthenticatorLinkerException("Can't accept sms code. Status: " + validateResp.Status); + } + } + + var tries = 0; + + + while (tries < 30) + { + tries++; + time = await TimeAligner.GetSteamTimeAsync(); + var code = SteamGuardCodeGenerator.GenerateCode(sharedSecret, time); + + var req = new FinalizeAddAuthenticator_Request + { + SteamId = data.SteamId.Steam64.ToUlong(), + AuthenticatorCode = code, + AuthenticatorTime = (ulong)time, + ConfirmationCode = confirmationCode, + }; + + + var resp = await client.PostProto(reqUri, req); + + if (resp.Success && resp.WantMore == false) + { + return new LinkResult(); + } + + if (resp.Status == 89) + { + return new LinkResult(LinkError.BadConfirmationCode); + } + + + if (resp.Status == 88 && tries >= 30) + { + return new LinkResult(LinkError.UnableToGenerateCorrectCodes); + } + } + return new LinkResult(LinkError.GeneralFailure); + } + + public static async Task IsValidPhoneNumber(HttpClient client, long phoneNumber, string sessionId) + { + return await CheckPhoneNumber(client, phoneNumber, sessionId) == CheckPhoneResult.Valid; + } + public static async Task CheckPhoneNumber(HttpClient client, long phoneNumber, string sessionId) + { + var phone = '+' + phoneNumber.ToString(); + var data = new Dictionary + { + {"phoneNumber", phone}, + {"sessionID", sessionId} + }; + + var reqContent = new FormUrlEncodedContent(data); + var req = new HttpRequestMessage(HttpMethod.Post, SteamConstants.STEAM_STORE + "phone/validate"); + req.Content = reqContent; + req.Headers.Referrer = new Uri("https://store.steampowered.com/phone/add"); + var resp = await client.SendAsync(req); + var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var j = JObject.Parse(content); + + if (j["success"]!.Value() == false) + return CheckPhoneResult.GeneralFailure; + + if (j["is_voip"]!.Value()) + return CheckPhoneResult.VoIp; + + return j["is_valid"]!.Value() ? CheckPhoneResult.Valid : CheckPhoneResult.NotValid; + } + + public static async Task AttachPhone(HttpClient client, long phoneNumber, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/SetAccountPhoneNumber/v1"; + var phone = '+' + phoneNumber.ToString(); + var req = new SetAccountPhoneNumber_Request + { + PhoneNumber = phone + }; + + var reqUri = AddAccessToken(uri, accessToken); + var resp = + await client.PostProtoMsg(reqUri, req); + + return resp.Result == EResult.Pending; + } + public static async Task CheckEmailConfirmation(HttpClient client, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/IsAccountWaitingForEmailConfirmation/v1"; + + var reqUri = AddAccessToken(uri, accessToken); + + var i = 0; + while (i < 5) + { + i++; + var resp = await client.PostProto(reqUri, new EmptyMessage()); ; + + if (resp.IsWaiting == false) return true; + + await Task.Delay(resp.SecondsToWait * 1000); + } + + return false; + } + public static Task SendSmsCode(HttpClient client, string accessToken) + { + const string uri = SteamConstants.STEAM_API + "IPhoneService/SendPhoneVerificationCode/v1"; + + var reqUri = AddAccessToken(uri, accessToken); + return client.PostProto(reqUri, new SendPhoneVerificationCode_Request()); + } + public static async Task CheckSmsCode(HttpClient client, int smsCode, string sessionId) + { + var data = new Dictionary + { + {"op", "check_sms_code"}, + {"arg", smsCode.ToString()}, + {"checkfortos", "0"}, + {"skipvoip", "1"}, + {"sessionid", sessionId} + }; + + var content = new FormUrlEncodedContent(data); + var resp = await client.PostAsync(PHONE_REQ, content); + var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var success = SteamStatusCode.Translate(respContent, out _).Equals(SteamStatusCode.Ok); + + if (success) return true; + + await Task.Delay(5000); + var hasPhone = await HasPhone(client, sessionId); + return hasPhone.HasPhone; + } + private static FormUrlEncodedContent CreateAjaxContent(string op, string arg, string sessionId) + { + var data = new Dictionary + { + {"op",op}, + {"arg", arg}, + {"sessionid", sessionId} + }; + return new FormUrlEncodedContent(data); + } + + public static string GenerateDeviceId() => "android:" + Guid.NewGuid(); + private static string AddAccessToken(string uri, string accessToken) => uri + "?access_token=" + accessToken; + + #endregion + + #region ForLinker + + internal static Task LinkRequest(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return LinkRequest(linker.Client, linker.SessionData, linker.DeviceId ??= GenerateDeviceId()); + } + + internal static Task IsValidPhoneNumber(this SteamAuthenticatorLinker linker, long phoneNumber) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return IsValidPhoneNumber(linker.Client, phoneNumber, linker.SessionData.SessionId); + } + + internal static Task HasPhone(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return HasPhone(linker.Client, linker.SessionData.GetAccessToken()); + } + internal static Task AttachPhone(this SteamAuthenticatorLinker linker, long phoneNumber) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return AttachPhone(linker.Client, phoneNumber, linker.SessionData.GetAccessToken()); + } + internal static Task CheckEmailConfirmation(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return CheckEmailConfirmation(linker.Client, linker.SessionData.GetAccessToken()); + } + + internal static Task SendSmsCode(this SteamAuthenticatorLinker linker) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return SendSmsCode(linker.Client, linker.SessionData.GetAccessToken()); + } + internal static Task CheckSmsCode(this SteamAuthenticatorLinker linker, int smsCode) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return CheckSmsCode(linker.Client, smsCode, linker.SessionData.SessionId); + } + + internal static Task FinalizeLink(this SteamAuthenticatorLinker linker, string confirmationCode, byte[] sharedSecret, bool validateSmsCode) + { + if (linker.SessionData == null) + throw new InvalidOperationException("SessionData is null"); + + return FinalizeLink(linker.Client, confirmationCode, linker.SessionData, sharedSecret, validateSmsCode); + } + + + #endregion + private static string GetAccessToken(this ISessionData s) => s.GetToken(SteamDomain.Community)?.Token ?? throw new SessionInvalidException("Access token was null. MobileEndpoints requires valid AccessToken"); +} \ No newline at end of file diff --git a/SteamLibForked/Api/Mobile/SteamMobileApi.cs b/SteamLibForked/Api/Mobile/SteamMobileApi.cs new file mode 100644 index 0000000..d1cee0d --- /dev/null +++ b/SteamLibForked/Api/Mobile/SteamMobileApi.cs @@ -0,0 +1,89 @@ +using System.Net; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using SteamLib.Core; +using SteamLib.Exceptions; +using SteamLib.ProtoCore; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Services; + +namespace SteamLib.Api.Mobile; + + +[PublicAPI] +public static class SteamMobileApi +//TODO: cancellation token +//TODO: HealthMonitor +{ + + private const string GENERATE_ACCESS_TOKEN = + SteamConstants.STEAM_API + "IAuthenticationService/GenerateAccessTokenForApp/v1"; + + + /// + /// + /// + /// + /// + /// + /// Refreshed AccessToken + /// + public static async Task RefreshJwt(HttpClient client, string refreshToken, long steamId) + { + var req = new GenerateAccessTokenForApp_Request + { + RefreshToken = refreshToken, + SteamId = steamId, + TokenRenewalType = true + }; + + try + { + var resp = await client.PostProto(GENERATE_ACCESS_TOKEN, req); + return resp.AccessToken; + } + catch (EResultException ex) + when (ex.Result == EResult.AccessDenied) + { + throw new SessionInvalidException("RefreshToken is not accepted by Steam. You must login again and use new token"); + } + } + + public static async Task HasPhoneAttached(HttpClient client, string sessionId) + { + var data = new Dictionary + { + {"op", "has_phone"}, + {"arg", "null"}, + {"sessionid", sessionId} + + }; + var content = new FormUrlEncodedContent(data); + var resp = await client.PostAsync(SteamConstants.STEAM_COMMUNITY + "steamguard/phoneajax", content); + var respContent = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var json = JObject.Parse(respContent); + return json["has_phone"]!.Value(); + } + + + public static async Task RemoveAuthenticator(HttpClient client, string accessToken, string rCode) + { + var req = SteamConstants.STEAM_API + "ITwoFactorService/RemoveAuthenticator/v1?access_token=" + accessToken; + var reqData = new RemoveAuthenticator_Request + { + RevocationCode = rCode, + RevocationReason = 1, + SteamGuardScheme = 1 + }; + try + { + return await client.PostProto(req, reqData); + } + catch (HttpRequestException ex) + when (ex.StatusCode is HttpStatusCode.Unauthorized) + { + throw new SessionExpiredException("Session expired"); + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs b/SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs new file mode 100644 index 0000000..41b5ec8 --- /dev/null +++ b/SteamLibForked/Api/Mobile/SteamMobileConfirmationsApi.cs @@ -0,0 +1,96 @@ +using SteamLib.Core; +using SteamLib.Exceptions; +using SteamLib.SteamMobile.Confirmations; +using SteamLib.SteamMobile; +using System.Net; +using AchiesUtilities.Web.Extensions; +using SteamLib.Web.Scrappers.JSON; +using SteamLib.Core.StatusCodes; +using SteamLib.Utility; + +namespace SteamLib.Api.Mobile; + +public static class SteamMobileConfirmationsApi +{ + private const string CONF = SteamConstants.STEAM_COMMUNITY + "mobileconf"; + private const string CONF_OP = SteamConstants.STEAM_COMMUNITY + "mobileconf/ajaxop"; + private const string MULTI_CONF_OP = SteamConstants.STEAM_COMMUNITY + "mobileconf/multiajaxop"; + + public static async Task> GetConfirmations(HttpClient client, MobileData data, long steamId) + { + var nvc = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, "list"); + + var req = new Uri(CONF + "/getlist" + nvc.ToQueryString()); + var reqMsg = new HttpRequestMessage(HttpMethod.Get, req); + var resp = await client.SendAsync(reqMsg); + + var respStr = await resp.Content.ReadAsStringAsync(); + if (resp.StatusCode == HttpStatusCode.Redirect) + { + throw new SessionExpiredException("Mobile session expired"); + } + + resp.EnsureSuccessStatusCode(); + + + return HealthMonitor.LogOnException(respStr, MobileConfirmationScrapper.Scrap, typeof(SessionExpiredException)); + } + + public static async Task SendConfirmation(HttpClient client, Confirmation confirmation, long steamId, MobileData data, bool confirm) + { + var op = confirm ? "allow" : "cancel"; + + var query = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, op).ToList(); + + var id = confirmation.Id.ToString(); + var key = confirmation.Nonce.ToString(); + + query.Insert(0, new KeyValuePair("op", op)); + query.Add(new KeyValuePair("cid", id)); + query.Add(new KeyValuePair("ck", key)); + + var req = CONF_OP + query.ToQueryString(); + var resp = await client.GetStringAsync(req); + var successCode = HealthMonitor.LogOnException(resp, () => SteamStatusCode.Translate(resp, out _)); + + return successCode.Equals(SteamStatusCode.Ok); + + } + + + public static async Task SendMultipleConfirmations(HttpClient client, IEnumerable confirmations, + long steamId, MobileData data, bool confirm) + { + var op = confirm ? "allow" : "cancel"; + var query = GetConfirmationKvp(steamId, data.DeviceId, data.IdentitySecret, op).ToList(); + query.Insert(0, new KeyValuePair("op", op)); + + foreach (var confirmation in confirmations) + { + query.Add(new KeyValuePair("cid[]", confirmation.Id.ToString())); + query.Add(new KeyValuePair("ck[]", confirmation.Nonce.ToString())); + } + + var content = new FormUrlEncodedContent(query); + var resp = await client.PostAsync(MULTI_CONF_OP, content); + var respStr = await resp.Content.ReadAsStringAsync(); + var successCode = HealthMonitor.LogOnException(respStr, () => SteamStatusCode.Translate(respStr, out _)); + return successCode.Equals(SteamStatusCode.Ok); + + } + + internal static IEnumerable> GetConfirmationKvp(long steamId, string deviceId, string identitySecret, string tag = "conf") + { + var time = TimeAligner.GetSteamTime(); + var hash = EncryptionHelper.GenerateConfirmationHash(time, identitySecret, tag); + return new KeyValuePair[] + { + new("p", deviceId), + new("a", steamId.ToString()), + new("k", hash), + new("t", time.ToString()), + new("m", "react"), + new("tag", tag) + }; + } +} \ No newline at end of file diff --git a/SteamLibForked/Api/SteamGlobalApi.cs b/SteamLibForked/Api/SteamGlobalApi.cs new file mode 100644 index 0000000..39f9421 --- /dev/null +++ b/SteamLibForked/Api/SteamGlobalApi.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; +using SteamLib.Authentication; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.StatusCodes; +using SteamLib.Exceptions; + +namespace SteamLib.Api; + +public static class SteamGlobalApi +{ + /// + /// Raw AccessToken value + /// + /// + /// + /// + /// + /// + public static async Task RefreshJwt(HttpClient client, SteamDomain domain, CancellationToken cancellationToken = default) + { + var domainUri = SteamDomains.GetDomain(domain); + var data = new Dictionary + { + {"redir", domainUri} + }; + var cont = new FormUrlEncodedContent(data); + var resp = await client.PostAsync("https://login.steampowered.com/jwt/ajaxrefresh", cont, cancellationToken); + var respStr = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync(cancellationToken); + var jwtRefresh = JsonConvert.DeserializeObject(respStr); + if (jwtRefresh?.Success != true) + { + Exception? inner = null; + if (jwtRefresh?.Error != null) + { + var errorCode = jwtRefresh.Error.Value; + var translated = SteamStatusCode.Translate(errorCode, out _); + inner = new SteamStatusCodeException(translated, respStr); + } + throw new SessionInvalidException("AjaxRefresh was not successful. 'steamRefresh_steam' cookie is missing or refresh token is invalid", inner); + + } + + data = new Dictionary() + { + {"steamID", jwtRefresh.SteamId.ToString()}, + {"nonce", jwtRefresh.Nonce}, + {"redir", jwtRefresh.Redir}, + {"auth", jwtRefresh.Auth}, + }; + + cont = new FormUrlEncodedContent(data); + var update = await client.PostAsync(jwtRefresh.LoginUrl, cont, cancellationToken); + var updateResp = await update.Content.ReadAsStringAsync(cancellationToken); + if (updateResp != "{\"result\":1}") + { + throw new SessionInvalidException( + "AjaxRefresh (set-token) response was not successful. Response string stored in Exception.Data") + { + Data = {{"Response", updateResp}} + }; + } + return SteamTokenHelper.ExtractJwtFromSetCookiesHeader(update.Headers); + } + + + private class JwtRefreshJson + { + [JsonProperty("success")] public bool Success { get; set; } + [JsonProperty("login_url")] public string LoginUrl { get; set; } = string.Empty; + [JsonProperty("steamID")] public long SteamId { get; set; } + [JsonProperty("nonce")] public string Nonce { get; set; } = string.Empty; + [JsonProperty("redir")] public string Redir { get; set; } = string.Empty; + [JsonProperty("auth")] public string Auth { get; set; } = string.Empty; + [JsonProperty("error")] public int? Error { get; set; } + } +} + diff --git a/SteamLibForked/Authentication/AdmissionHelper.cs b/SteamLibForked/Authentication/AdmissionHelper.cs new file mode 100644 index 0000000..0408494 --- /dev/null +++ b/SteamLibForked/Authentication/AdmissionHelper.cs @@ -0,0 +1,176 @@ +using AchiesUtilities.Extensions; +using JetBrains.Annotations; +using SteamLib.Account; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using System.Net; + +namespace SteamLib.Authentication; + +[PublicAPI] +public static class AdmissionHelper +{ + public const string ACCESS_COOKIE_NAME = "steamLoginSecure"; + public const string REFRESH_COOKIE_NAME = "steamRefresh_steam"; + public const string LANGUAGE_COOKIE_NAME = "Steam_Language"; + + #region Main + + /// + /// Clear and set new session + /// + public static void SetSteamCookies(this CookieContainer container, ISessionData sessionData, string setLanguage = "english") + { + container.ClearSteamCookies(setLanguage); + + + AddRefreshToken(container, sessionData.RefreshToken); + + var community = SteamDomains.GetDomainUri(SteamDomain.Community); + container.Add(community, new Cookie("sessionid", sessionData.SessionId, "/")); + container.Add(community, new Cookie("Steam_Language", setLanguage, "/")); + TransferCommunityCookies(container); + foreach (var domain in SteamDomains.AllDomains) + { + var token = sessionData.GetToken(domain); + if (token == null) continue; + AddTokenCookie(container, token.Value); + } + } + + public static void SetDomainCookie(this CookieContainer container, SteamDomain domain, SteamAuthToken token) + { + var uri = SteamDomains.GetDomainUri(domain); + foreach (var cookie in container.GetCookies(uri).Where(c => c.Expired == false && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME))) + { + cookie.Expired = true; + } + + AddTokenCookie(container, token); + } + + + /// + /// Clear and set new session + /// + public static void SetSteamMobileCookies(this CookieContainer container, IMobileSessionData mobileSession, + string setLanguage = "english") + { + + container.ClearSteamCookies(setLanguage); + container.AddMinimalMobileCookies(); + + AddRefreshToken(container, mobileSession.RefreshToken); + + var community = SteamDomains.GetDomainUri(SteamDomain.Community); + container.Add(community, new Cookie("steamid", mobileSession.SteamId.Steam64.ToString())); + container.Add(community, new Cookie("sessionid", mobileSession.SessionId)); + container.Add(community, new Cookie("Steam_Language", setLanguage)); + TransferCommunityCookies(container); + + foreach (var domain in SteamDomains.AllDomains) + { + var token = mobileSession.GetToken(domain); + if (token == null) continue; + AddTokenCookie(container, token.Value); + } + } + + + public static void AddMinimalMobileCookies(this CookieContainer container) + { + var community = SteamDomains.GetDomainUri(SteamDomain.Community); + container.Add(community, new Cookie("mobileClientVersion", "777777 3.6.1")); + container.Add(community, new Cookie("mobileClient", "android")); + } + + #endregion + + #region Clear + public static void ClearSteamCookies(this CookieContainer container, string setLanguage = "english") + { + var cookies = container.GetAllCookies().Where(IsSteamCookie).ToList(); + foreach (var cookie in cookies) + { + cookie.Expired = true; + } + container.Add(SteamDomains.GetDomainUri(SteamDomain.Community), new Cookie(LANGUAGE_COOKIE_NAME, setLanguage)); + TransferCommunityCookies(container); + } + + public static void ClearMobileSessionCookies(this CookieContainer container, string setLanguage = "english") + { + container.ClearSteamCookies(setLanguage); + container.AddMinimalMobileCookies(); + TransferCommunityCookies(container); + } + + + #endregion + + #region Helpers + public static void TransferCommunityCookies(CookieContainer container) + { + var cookies = container.GetAllCookies(); + + + foreach (Cookie cookie in cookies) + { + if (cookie.Domain.Contains("steamcommunity.com") == false || cookie.Expired || cookie.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)) continue; + + + container.Add(SteamDomains.GetDomainUri(SteamDomain.Store), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly }); + container.Add(SteamDomains.GetDomainUri(SteamDomain.Help), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly }); + container.Add(SteamDomains.GetDomainUri(SteamDomain.TV), new Cookie(cookie.Name, cookie.Value, cookie.Path) { Expires = cookie.Expires, Secure = cookie.Secure, HttpOnly = cookie.HttpOnly }); + } + } + + public static void AddRefreshToken(CookieContainer container, SteamAuthToken token) + { + if (token.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) + throw new ArgumentException($"Token must be of type Refresh or MobileRefresh. Provided token has type: {token.Type}", nameof(token)); + var refreshToken = token.SignedToken; + container.Add(SteamDomains.LoginSteamDomain, new Cookie(REFRESH_COOKIE_NAME, refreshToken) + { + Expires = token.Expires.ToLocalDateTime() + }); + } + + public static void AddTokenCookie(CookieContainer container, SteamAuthToken token) + { + if (token.Type is not SteamAccessTokenType.AccessToken) + throw new ArgumentException($"Token must be of type AccessToken. Provided token has type: {token.Type}", nameof(token)); + + AddTokenCookie(container, token.Domain, token); + } + private static void AddTokenCookie(CookieContainer container, SteamDomain domain, SteamAuthToken token) + { + var domainUri = SteamDomains.GetDomainUri(token.Domain); + container.Add(domainUri, new Cookie(ACCESS_COOKIE_NAME, token.SignedToken) + { + HttpOnly = true, + Secure = true, + Expires = token.Expires.ToLocalDateTime() + }); + } + + + public static bool IsSteamCookie(Cookie cookie) => + cookie.Domain.Contains("steamcommunity.com") || cookie.Domain.Contains("steampowered.com") || cookie.Domain.Contains("steam.tv"); + + + public static string? GetSessionId(this CookieContainer container, string domain = "steamcommunity.com") + { + var cookies = container.GetAllCookies(); + return cookies + .FirstOrDefault(c => c.Name.Equals("sessionid", StringComparison.InvariantCultureIgnoreCase) + && c.Expired == false + && c.Domain.Contains(domain, StringComparison.InvariantCultureIgnoreCase))? + .Value; + } + + + #endregion + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginExecutor.cs b/SteamLibForked/Authentication/Default/LoginExecutor.cs new file mode 100644 index 0000000..01cef5c --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginExecutor.cs @@ -0,0 +1,108 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Authentication; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; + +namespace SteamLib.Login.Default; + +[Obsolete("Method removed from Steam")] +public class LoginExecutor +{ + public static ILoginConsumer NullConsumer { get; } = new NullLoginConsumer(); + public ILoginConsumer Caller { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ICaptchaResolver? CaptchaResolver { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + + private LoginExecutor(LoginExecutorOptions options) + { + Caller = options.Consumer; + HttpClient = options.HttpClient; + Logger = options.Logger; + EmailAuthProvider = options.EmailAuthProvider; + SteamGuardProvider = options.SteamGuardProvider; + CaptchaResolver = options.CaptchaResolver; + } + + + public static async Task DoLogin(LoginExecutorOptions options, string username, string password, CancellationToken cancellationToken = default) //TODO: logs + { + var executor = new LoginExecutor(options); + var client = executor.HttpClient; + + LoginStage loginStage = new GetRsaStage(username); + loginStage = await ((GetRsaStage)loginStage).Proceed(client, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + if (loginStage is ReadyToLoginStage rdyToLoginStage) + { + loginStage = await rdyToLoginStage.Proceed(client, password, string.Empty, cancellationToken); + } + cancellationToken.ThrowIfCancellationRequested(); + var twoFactorTry = 0; + var emailTry = 0; + while (loginStage.GetType() != typeof(LoginErrorStage) && loginStage is not LoginSuccessStage) + { + cancellationToken.ThrowIfCancellationRequested(); + switch (loginStage) + { + case CaptchaNeededStage captchaNeededStage: + { + if (executor.CaptchaResolver == null) throw new LoginException(LoginError.CaptchaRequired); + var captchaText = await executor.CaptchaResolver.Resolve(captchaNeededStage.CaptchaImage, client); + loginStage = await captchaNeededStage.Proceed(client, captchaText, cancellationToken); + break; + } + case EmailAuthStage emailAuthStage: + { + if (executor.EmailAuthProvider == null) throw new LoginException(LoginError.EmailAuthRequired); + if (emailTry > executor.EmailAuthProvider.MaxRetryCount) throw new LoginException(LoginError.InvalidEmailAuthCode); + var code = await executor.EmailAuthProvider.GetEmailAuthCode(executor.Caller); + loginStage = await emailAuthStage.Proceed(client, code, cancellationToken); + emailTry++; + break; + } + case TwoFactorStage twoFactorStage: + { + if (executor.SteamGuardProvider == null) throw new LoginException(LoginError.SteamGuardRequired); + if (twoFactorTry > executor.SteamGuardProvider.MaxRetryCount) throw new LoginException(LoginError.InvalidTwoFactorCode); + var twoFactor = await executor.SteamGuardProvider.GetSteamGuardCode(executor.Caller); + loginStage = await twoFactorStage.Proceed(client, twoFactor, cancellationToken); + twoFactorTry++; + break; + } + case ReadyToLoginStage readyToLoginStage: //When captcha proceeded, stage goes there + { + loginStage = await readyToLoginStage.Proceed(client, password, string.Empty, cancellationToken); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(loginStage)); + } + } + if (loginStage is LoginErrorStage error) + { + var content = await error.Response.Content.ReadAsStringAsync(cancellationToken); + if (content.Contains("The account name or password that you have entered is incorrect")) + { + throw new LoginException(LoginError.InvalidCredentials); + } + throw new LoginException(content); + } + + + if (loginStage is not LoginSuccessStage successStage) + { + throw new InvalidOperationException("Unexpected login stage at this point. " + loginStage.GetType()) + { + Data = { { "stage", loginStage } } + }; + } + + return successStage.TransferParameters; + + } + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginExecutorOptions.cs b/SteamLibForked/Authentication/Default/LoginExecutorOptions.cs new file mode 100644 index 0000000..a77393f --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginExecutorOptions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Core.Interfaces; + +namespace SteamLib.Login.Default; + + +public class LoginExecutorOptions +{ + public ILoginConsumer Consumer { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ICaptchaResolver? CaptchaResolver { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + public LoginExecutorOptions(ILoginConsumer consumer, HttpClient httpClient) + { + Consumer = consumer; + HttpClient = httpClient; + } + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginResultJson.cs b/SteamLibForked/Authentication/Default/LoginResultJson.cs new file mode 100644 index 0000000..bddae65 --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginResultJson.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace SteamLib.Login.Default; + +#pragma warning disable CS8618 +/// +/// Class to Deserialize the json response strings after the login/> +/// +internal class LoginResultJson +{ + [JsonProperty("success")] public bool Success { get; set; } + [JsonProperty("message")] public string Message { get; set; } + [JsonProperty("captcha_needed")] public bool CaptchaNeeded { get; set; } + [JsonProperty("captcha_gid")] public string CaptchaGid { get; set; } + [JsonProperty("emailauth_needed")] public bool EmailAuthNeeded { get; set; } + [JsonProperty("emailsteamid")] public string EmailSteamId { get; set; } + [JsonProperty("requires_twofactor")] public bool RequiresTwoFactor { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/LoginStage.cs b/SteamLibForked/Authentication/Default/LoginStage.cs new file mode 100644 index 0000000..22844c0 --- /dev/null +++ b/SteamLibForked/Authentication/Default/LoginStage.cs @@ -0,0 +1,227 @@ +using AchiesUtilities.Web.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamLib.Authentication; +using SteamLib.Utility; + +namespace SteamLib.Login.Default; + +internal abstract class LoginStage +{ + protected Dictionary Data { get; } + protected LoginStage() + { + Data = new Dictionary(); + } + + protected LoginStage(Dictionary data) + { + Data = data; + } + + protected bool CheckIfLoginCompleted(LoginResultJson json) + { + return !json.CaptchaNeeded && !json.EmailAuthNeeded && !json.RequiresTwoFactor && json.Success; + } + protected async Task ConvertJson(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + var webResponseString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(webResponseString)!; + } + protected async Task CompleteLogin(HttpResponseMessage response) + { + var jsonStr = await response.Content.ReadAsStringAsync(); + var json = JObject.Parse(jsonStr); + var transferParameters = json["transfer_parameters"]!.ToObject()!; + return new LoginSuccessStage(transferParameters); + } + protected async Task LoginRequest(HttpClient client, CancellationToken cancellationToken = default) + { + return await client.PostAsync("https://steamcommunity.com/login/dologin", new FormUrlEncodedContent(Data), cancellationToken); + } +} +internal class LoginErrorStage : LoginStage +{ + public readonly string ErrorString; + public readonly int ErrorCode; + public readonly LoginStage Stage; + public readonly HttpResponseMessage Response; + public LoginErrorStage(string errorString, HttpResponseMessage responseMessage, LoginStage stage) + { + ErrorString = errorString; + Stage = stage; + ErrorCode = Utilities.GetSuccessCode(responseMessage.Content.ReadAsStringSync()); + Response = responseMessage; + } +} +internal class GetRsaStage : LoginStage +{ + public GetRsaStage(Dictionary data) : base(data) { } + public GetRsaStage(string userName) + { + Data["username"] = userName; + } + + + /// + /// + /// + /// + /// + /// if success + public async Task Proceed(HttpClient client, CancellationToken cancellationToken = default) + { + var content = new FormUrlEncodedContent(Data); + var response = await client.PostAsync("https://steamcommunity.com/login/getrsakey", content, cancellationToken); + var responseString = await response.Content.ReadAsStringAsync(cancellationToken); + var rsaJson = JsonConvert.DeserializeObject(responseString)!; + if (!rsaJson.success) + return new LoginErrorStage("Can't get RSA", response, this); + + Data["rsatimestamp"] = rsaJson.timestamp; + return new ReadyToLoginStage(Data, rsaJson.publickey_exp, rsaJson.publickey_mod); + } + +} +internal class ReadyToLoginStage : LoginStage +{ + private readonly string _publicKeyExp; + private readonly string _publicKeyMod; + internal ReadyToLoginStage(Dictionary data, string publicKeyExp, string publicKeyMod) : base(data) + { + _publicKeyExp = publicKeyExp; + _publicKeyMod = publicKeyMod; + } + + public async Task Proceed(HttpClient client, string password, string loginFriendlyName, CancellationToken cancellationToken = default) + { + var encryptedBase64Password = EncryptionHelper.ToBase64EncryptedPassword(_publicKeyExp, _publicKeyMod, password); + var unixTimestamp = (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + Data["password"] = encryptedBase64Password; + Data["remember_login"] = "true"; + Data["loginfriendlyname"] = loginFriendlyName; + Data["donotcache"] = unixTimestamp + "000"; // Added three "0"'s because Steam has a weird unix timestamp interpretation. + + _ = await client.GetAsync("https://steamcommunity.com/", cancellationToken); + var webResponse = await LoginRequest(client, cancellationToken); + var result = await ConvertJson(webResponse); + + if (CheckIfLoginCompleted(result)) + { + return await CompleteLogin(webResponse); + } + + if (result.CaptchaNeeded) + { + return new CaptchaNeededStage(Data, result.CaptchaGid); + } + + if (result.EmailAuthNeeded) + { + return new EmailAuthStage(Data); + } + + if (result.RequiresTwoFactor) + { + return new TwoFactorStage(Data); + } + + return new LoginErrorStage("Can't proceed login.", webResponse, this); + } +} +internal class TwoFactorStage : LoginStage +{ + internal TwoFactorStage(Dictionary data) : base(data) { } + public async Task Proceed(HttpClient client, string twoFactorCode, CancellationToken cancellationToken = default) + { + Data["twofactorcode"] = twoFactorCode; + var webResponse = await LoginRequest(client, cancellationToken); + var result = await ConvertJson(webResponse); + + if (CheckIfLoginCompleted(result)) + { + return await CompleteLogin(webResponse); + } + + if (result.CaptchaNeeded) + { + return new CaptchaNeededStage(Data, result.CaptchaGid); + } + + if (result.RequiresTwoFactor) + { + return new TwoFactorStage(Data); + } + + return new LoginErrorStage("Can't proceed login. Bad TwoFactor code", webResponse, this); + } + +} +internal class EmailAuthStage : LoginStage +{ + internal EmailAuthStage(Dictionary data) : base(data) { } + + public async Task Proceed(HttpClient client, string emailCode, CancellationToken cancellationToken = default) + { + Data["emailauth"] = emailCode; + var webResponse = await LoginRequest(client, cancellationToken); + var result = await ConvertJson(webResponse); + + if (CheckIfLoginCompleted(result)) + { + return await CompleteLogin(webResponse); + } + + if (result.CaptchaNeeded) + { + return new CaptchaNeededStage(Data, result.CaptchaGid); + } + + if (result.EmailAuthNeeded) + { + return new EmailAuthStage(Data); + } + + + return new LoginErrorStage("Can't proceed login. Bad Email code", webResponse, this); + } +} +internal class CaptchaNeededStage : LoginStage +{ + internal Uri CaptchaImage { get; } + public CaptchaNeededStage(Dictionary data, string captchaGid) : base(data) + { + captchaGid = Uri.EscapeDataString(captchaGid); + Data["captchagid"] = captchaGid; + CaptchaImage = new Uri("https://steamcommunity.com/login/rendercaptcha?gid=" + captchaGid); + } + + /// + /// + /// + /// + /// + /// + /// or + public async Task Proceed(HttpClient client, string captchaText, CancellationToken cancellationToken = default) + { + Data["captcha_text"] = captchaText; + //When captcha required we need to do login from start + + var rsaStage = new GetRsaStage(Data); + var res = await rsaStage.Proceed(client, cancellationToken); + + return res; + } +} +internal class LoginSuccessStage : LoginStage +{ + public TransferParameters TransferParameters { get; } + public LoginSuccessStage(TransferParameters transferParameters) + { + TransferParameters = transferParameters; + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/NullLoginConsumer.cs b/SteamLibForked/Authentication/Default/NullLoginConsumer.cs new file mode 100644 index 0000000..0d23b9f --- /dev/null +++ b/SteamLibForked/Authentication/Default/NullLoginConsumer.cs @@ -0,0 +1,8 @@ +using SteamLib.Core.Interfaces; + +namespace SteamLib.Login.Default; + +internal class NullLoginConsumer : ILoginConsumer +{ + public string FriendlyName { get; } = "null"; +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/Default/RsaKeyJson.cs b/SteamLibForked/Authentication/Default/RsaKeyJson.cs new file mode 100644 index 0000000..a22af43 --- /dev/null +++ b/SteamLibForked/Authentication/Default/RsaKeyJson.cs @@ -0,0 +1,16 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +#pragma warning disable CS8618 + +namespace SteamLib.Login.Default; + +/// +/// Class to Deserialize the json response strings of the getResKey request/> +/// +internal class RsaKeyJson +{ + public bool success { get; set; } + public string publickey_mod { get; set; } + public string publickey_exp { get; set; } + public string timestamp { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs b/SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs new file mode 100644 index 0000000..bfe479c --- /dev/null +++ b/SteamLibForked/Authentication/LoginV2/FinalizeLoginJson.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace SteamLib.Authentication.LoginV2; + +class FinalizeLoginJson +{ + [JsonProperty("steamID")] + public ulong SteamId { get; set; } + + [JsonProperty("transfer_info")] + public List TransferInfo { get; set; } +} + +class TransferInfo +{ + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("params")] + public TransferInfoParams TransferInfoParams { get; set; } +} + + +class TransferInfoParams +{ + [JsonProperty("nonce")] + public string Nonce { get; set; } + + [JsonProperty("auth")] + public string Auth { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs b/SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs new file mode 100644 index 0000000..ee5c35f --- /dev/null +++ b/SteamLibForked/Authentication/LoginV2/LoginV2Executor.cs @@ -0,0 +1,246 @@ +using AchiesUtilities.Web.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamLib.Account; +using SteamLib.Core; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Core.StatusCodes; +using SteamLib.Exceptions; +using SteamLib.Login.Default; +using SteamLib.ProtoCore; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Services; +using SteamLib.Utility; +using SteamLib.Web; + +namespace SteamLib.Authentication.LoginV2; + +public class LoginV2Executor +{ + public static ILoginConsumer NullConsumer { get; } = new NullLoginConsumer(); + public ILoginConsumer Caller { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ICaptchaResolver? CaptchaResolver { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + public string WebsiteId { get; init; } + public DeviceDetails DeviceDetails { get; init; } + private LoginV2Executor(LoginV2ExecutorOptions options) + { + Caller = options.Consumer; + HttpClient = options.HttpClient; + Logger = options.Logger; + EmailAuthProvider = options.EmailAuthProvider; + SteamGuardProvider = options.SteamGuardProvider; + WebsiteId = options.GetWebsiteIdOrDefault(); + DeviceDetails = options.DeviceDetails ?? DeviceDetails.CreateDefault(); + } + + + /// + /// Do login on SteamCommunity.
+ /// Some functions requires proper SessionId. But contains only SteamCommunity related SessionId and some functions on other services may not work + ///
+ /// + /// + /// + /// + /// or depending on which token type is returned + /// + /// + /// + /// + public static async Task DoLogin(LoginV2ExecutorOptions options, string username, string password, CancellationToken cancellationToken = default) //TODO: logs + { + var executor = new LoginV2Executor(options); + var client = executor.HttpClient; + + var globalData = await SteamWebApi.GetMarketGlobalInfo(client, cancellationToken); + var sessionId = globalData.SessionId; + + var rsgMsg = new GetPasswordRSAPublicKey_Request + { + AccountName = username + }; + var rsaResp = await client.GetProto( + "https://api.steampowered.com/IAuthenticationService/GetPasswordRSAPublicKey/v1", rsgMsg, cancellationToken: cancellationToken); + + + var encodedPassword = EncryptionHelper.ToBase64EncryptedPassword(rsaResp.PublickKeyExp, rsaResp.PublickKeyMod, password); + + + var beginAuthMsg = new BeginAuthSessionViaCredentials_Request + { + DeviceFriendlyName = string.Empty, + AccountName = username, + EncryptedPassword = encodedPassword, + EncryptionTimestamp = rsaResp.Timestamp, + RememberLogin = true, + PlatformType = executor.DeviceDetails.PlatformType, + Persistence = 1, + WebsiteId = executor.WebsiteId, + DeviceDetails = executor.DeviceDetails, + }; + + BeginAuthSessionViaCredentials_Response beginAuthResp; + try + { + beginAuthResp = await client.PostProto( + "https://api.steampowered.com/IAuthenticationService/BeginAuthSessionViaCredentials/v1", beginAuthMsg, cancellationToken: cancellationToken); + + } + catch (EResultException ex) + when (ex.Result == EResult.InvalidPassword) + { + throw new LoginException(LoginError.InvalidCredentials); + } + + + var clientId = beginAuthResp.ClientId; + var steamId = (long)beginAuthResp.Steamid; + + var conf = beginAuthResp.AllowedConfirmations.FirstOrDefault(c => + c.ConfirmationType is EAuthSessionGuardType.EmailCode or EAuthSessionGuardType.DeviceCode); + + conf ??= beginAuthResp.AllowedConfirmations.FirstOrDefault(); + + switch (conf?.ConfirmationType) + { + case EAuthSessionGuardType.None: + break; + case EAuthSessionGuardType.DeviceCode: + case EAuthSessionGuardType.EmailCode: + await UpdateWithCode(executor, clientId, (ulong)steamId, conf.ConfirmationType); + break; + case EAuthSessionGuardType.Unknown: + case EAuthSessionGuardType.DeviceConfirmation: + case EAuthSessionGuardType.EmailConfirmation: + case EAuthSessionGuardType.MachineToken: + case EAuthSessionGuardType.LegacyMachineAuth: + throw new NotSupportedException( + $"Auth confirmation type of {conf.ConfirmationType} is not implemented yet"); + default: + throw new ArgumentOutOfRangeException(nameof(conf.ConfirmationType), conf?.ConfirmationType, "Unknown confirmation type or null"); + } + + var pollSessionMsg = new PollAuthSessionStatus_Request + { + ClientId = clientId, + RequestId = beginAuthResp.RequestId, + + }; + + var pollResp = + await client.PostProto( + "https://api.steampowered.com/IAuthenticationService/PollAuthSessionStatus/v1", pollSessionMsg, cancellationToken: cancellationToken); + + SteamAuthToken refreshToken; + try + { + refreshToken = SteamTokenHelper.Parse(pollResp.RefreshToken); + if (refreshToken.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) + throw new ArgumentException("Refresh token must be of type Refresh or MobileRefresh. No 'renew' audience found in JWT.", + nameof(pollResp.RefreshToken)); //Argument exception for less code + } + catch (ArgumentException ex) + { + throw new LoginException(null, + "Steam returned invalid refresh token or it's schema was unsupported. See inner exception for more details", + LoginError.UndefinedError, ex); + } + + var data = new Dictionary + { + {"nonce", pollResp.RefreshToken}, + {"sessionid", sessionId} + }; + + var finalize = await client.PostAsync("https://login.steampowered.com/jwt/finalizelogin", new FormUrlEncodedContent(data), cancellationToken); + var finalizeContent = await finalize.EnsureSuccessStatusCode().Content.ReadAsStringAsync(cancellationToken); + + var finalizeResp = + HealthMonitor.LogOnException(finalizeContent, () => JsonConvert.DeserializeObject(finalizeContent)!); + + + List tokens = new(); + foreach (var transferInfo in finalizeResp.TransferInfo) + { + var transferData = new Dictionary + { + {"nonce", transferInfo.TransferInfoParams.Nonce}, + {"auth", transferInfo.TransferInfoParams.Auth}, + {"steamID", steamId.ToString()} + }; + + + try + { + var req = new HttpRequestMessage(HttpMethod.Post, transferInfo.Url); + req.Content = new FormUrlEncodedContent(transferData); + req.Headers.Referrer = SteamDomains.GetDomainUri(SteamDomain.Store); + var transferResp = await client.SendAsync(req, cancellationToken); + var transferContent = await transferResp.ReadAsStringEnsureSuccessAsync(cancellationToken); + var status = JObject.Parse(transferContent); + var result = status["result"]?.Value(); + if(result != null) + SteamStatusCode.ValidateSuccessOrThrow(result.Value); //TODO: Fix steam.tv token transfer (result always 8). But who really cares.. + + var tokenStr = SteamTokenHelper.ExtractJwtFromSetCookiesHeader(transferResp.Headers); + var token = SteamTokenHelper.Parse(tokenStr); + tokens.Add(token); + } + catch (Exception ex) + { + executor.Logger?.Log(LogLevel.Warning, ex, "Can't transfer tokens for URI: {uri}", transferInfo.Url); + } + } + + var accessToken = SteamTokenHelper.Parse(pollResp.AccessToken); + + if (accessToken.Type == SteamAccessTokenType.Mobile) + { + return new MobileSessionData(sessionId, SteamId.FromSteam64(steamId), refreshToken, accessToken, tokens); + } + else + { + return new SessionData(sessionId, SteamId.FromSteam64(steamId), refreshToken, tokens); + } + + + + } + + private static async Task UpdateWithCode(LoginV2Executor executor, ulong clientId, ulong steamId, EAuthSessionGuardType guardType) + { + string? code; + if (guardType == EAuthSessionGuardType.DeviceCode) + { + if(executor.SteamGuardProvider != null) + code = await executor.SteamGuardProvider.GetSteamGuardCode(executor.Caller); + else + { + throw new LoginException(LoginError.SteamGuardRequired); + } + } + else + { + var t = executor.EmailAuthProvider?.GetEmailAuthCode(executor.Caller) ?? + throw new LoginException(LoginError.EmailAuthRequired); + code = await t; + } + var updateCodeMsg = new UpdateAuthSessionWithSteamGuardCode_Request + { + ClientId = clientId, + Code = code, + CodeType = guardType, + Steamid = steamId + }; + + await executor.HttpClient.PostProtoEnsureSuccess(new Uri("https://api.steampowered.com/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1"), updateCodeMsg); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs b/SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs new file mode 100644 index 0000000..a2e1c53 --- /dev/null +++ b/SteamLibForked/Authentication/LoginV2/LoginV2ExecutorOptions.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Core.Interfaces; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Services; + +namespace SteamLib.Authentication.LoginV2; + +public class LoginV2ExecutorOptions +{ + public ILoginConsumer Consumer { get; } + public HttpClient HttpClient { get; } + public ILogger? Logger { get; init; } + public IEmailProvider? EmailAuthProvider { get; init; } + public ISteamGuardProvider? SteamGuardProvider { get; init; } + public DeviceDetails? DeviceDetails { get; init; } + + /// + /// Gets or sets the website id that the login will be performed for. + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". + /// Default value will be set to "Community" + /// + /// The website id. + public string? WebsiteId { get; init; } + public LoginV2ExecutorOptions(ILoginConsumer consumer, HttpClient httpClient) + { + Consumer = consumer; + HttpClient = httpClient; + } + + + + public string GetWebsiteIdOrDefault() + { + return WebsiteId ?? "Community"; + } + + + public static DeviceDetails GetMobileDefaultDevice() //FORTEST + { + return new DeviceDetails("Pixel 6 Pro", EAuthTokenPlatformType.MobileApp, -500, 528); + } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/SessionDataValidator.cs b/SteamLibForked/Authentication/SessionDataValidator.cs new file mode 100644 index 0000000..4e6cdf8 --- /dev/null +++ b/SteamLibForked/Authentication/SessionDataValidator.cs @@ -0,0 +1,96 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using SteamLib.Account; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; + +namespace SteamLib.Authentication; + +public static class SessionDataValidator +{ + [PublicAPI] + public static Dictionary> Validators { get; } = new(); + public static ValidateOptionsResult Validate(string? name, ISessionData data) + { + if(Validators.TryGetValue(data.GetType(), out var validator)) + return validator.Validate(name, data); + + return Validate(data); + + } + + public static ValidateOptionsResult Validate(string? name, T data) where T : ISessionData + { + if (Validators.TryGetValue(typeof(T), out var validator)) + return validator.Validate(name, data); + + return Validate(data); + } + + + private static ValidateOptionsResult Validate(ISessionData data) + { + if (data == null!) + { + return ValidateOptionsResult.Fail("SessionData cannot be null"); + } + + if (string.IsNullOrWhiteSpace(data.SessionId)) + { + return ValidateOptionsResult.Fail("SessionId cannot be null or empty"); + } + + if (data.SteamId.Steam64.Id < SteamId64.SEED) + { + return ValidateOptionsResult.Fail($"SteamId '{data.SteamId.Steam64}' is invalid."); + } + + if (string.IsNullOrWhiteSpace(data.RefreshToken.Token)) + { + return ValidateOptionsResult.Fail("RefreshToken cannot be null or empty"); + } + + if (data.RefreshToken.SteamId.Steam64.Id < SteamId64.SEED) + { + return ValidateOptionsResult.Fail($"SteamId in RefreshToken '{data.SteamId.Steam64}' is invalid."); + } + + + if (data is IMobileSessionData && data.RefreshToken.Type != SteamAccessTokenType.MobileRefresh) + { + return ValidateOptionsResult.Fail($"RefreshToken '{data.RefreshToken.Token}' is not of type MobileRefresh"); + } + else if (data.RefreshToken.Type is not (SteamAccessTokenType.Refresh or SteamAccessTokenType.MobileRefresh)) + { + return ValidateOptionsResult.Fail($"RefreshToken '{data.RefreshToken.Token}' is not of type Refresh or MobileRefresh"); + } + + if (data.RefreshToken.Expires.ToLong() == 0L) + { + return ValidateOptionsResult.Fail($"RefreshToken '{data.RefreshToken.Token}' has invalid expiration date"); + } + + + + return ValidateOptionsResult.Success; + } + public static bool IsValid(this ISessionData data) + { + return Validate(null, data).Succeeded; + } + + public static void EnsureValidated(this ISessionData data) + { + if(data.IsValid == true) return; + if (data.IsValid == false) + throw new SessionInvalidException( + "SessionData was validated before and validation was not passed. Session is invalid"); + var validation = Validate(null, data); + data.IsValid = validation.Succeeded; + if (validation.Failed) + { + throw new SessionInvalidException("SessionData is invalid. " + validation.FailureMessage); + } + } + } diff --git a/SteamLibForked/Authentication/SteamTokenHelper.cs b/SteamLibForked/Authentication/SteamTokenHelper.cs new file mode 100644 index 0000000..63fb5db --- /dev/null +++ b/SteamLibForked/Authentication/SteamTokenHelper.cs @@ -0,0 +1,184 @@ +using AchiesUtilities.Models; +using Microsoft.IdentityModel.JsonWebTokens; +using SteamLib.Account; +using SteamLib.Core.Enums; +using System.Collections.ObjectModel; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; + +namespace SteamLib.Authentication; + +public static class SteamTokenHelper +{ + public static readonly Regex SteamTokenRegex = + new(@"^(?(?7\d{16,})(?:%7C%7C))?(?.+)$", RegexOptions.Compiled); + + public static readonly IReadOnlyDictionary AudDomains = + new ReadOnlyDictionary(new Dictionary + { + {"web:community", SteamDomain.Community}, + {"web:store", SteamDomain.Store}, + {"web:help", SteamDomain.Help}, + {"web:steamtv", SteamDomain.TV}, + {"web:checkout", SteamDomain.Checkout}, + }); + + + public static bool TryParse(string input, out SteamAuthToken result) + { + return Parse(input, out result, true); + } + + public static SteamAuthToken Parse(string input) + { + if (Parse(input, out var result, false)) + { + return result; + } + else + { + throw new ArgumentException("Provided Token is not valid JWT token", nameof(input)); + } + } + + private static bool Parse(string input, out SteamAuthToken result, bool trying) + { + result = default; + var match = SteamTokenRegex.Match(input); + if (match.Success == false) + { + if (trying) return false; + throw new ArgumentException("Provided Token is not valid SteamLoginSecure or SteamAccess token"); + } + + + var jwtStr = match.Groups["jwt"].Value; + JsonWebToken jwt; + try + { + jwt = new JsonWebToken(jwtStr); + } + catch (Exception e) + { + if (trying) return false; + throw new ArgumentException("Provided Token is not valid JWT token", nameof(input), e); + } + + var steamIdStr = jwt.Subject; + if (steamIdStr == null || long.TryParse(steamIdStr, out var steamId) == false || steamId < SteamId64.SEED) + { + if (trying) return false; + throw new ArgumentException( + $"Provided Token is not valid JWT token. Subject claim is null or not valid SteamId '{steamIdStr}'", + nameof(input)); + } + + SteamId id; + try + { + id = SteamId.FromSteam64(steamId); + } + catch (Exception ex) + { + if (trying) return false; + throw new ArgumentException( + $"Provided Token is not valid JWT token. Subject is claim not valid SteamId '{steamIdStr}'", + nameof(input), ex); + } + + var exp = jwt.ValidTo; + if (exp == DateTime.MinValue) + { + if (trying) + return false; + + throw new ArgumentException("Provided Token has no 'exp' claim.", nameof(input)); + } + + var expires = UnixTimeStamp.FromDateTime(exp); + var domain = SteamDomain.Undefined; + var audiences = jwt.Audiences.ToList(); + var tokenType = SteamAccessTokenType.Unknown; + if (jwt.Audiences.ToList().Count > 0) + { + var aud = audiences[0]; + if (AudDomains.TryGetValue(aud, out domain) == false && aud != "web") + { + if (trying) + { + result = new SteamAuthToken(jwtStr, id, expires, domain, tokenType); + return false; + } + + throw new ArgumentException($"Provided Token has invalid 'aud' claim. Value: {aud}", nameof(input)); + } + tokenType = SteamAccessTokenType.AccessToken; + + if (aud == "web") + { + + + var isMobile = audiences.Any(a => a.Equals("mobile")); + var isRefresh = audiences.Any(a => a.Equals("renew")); + if (isMobile ^ isRefresh) + { + tokenType = isMobile ? SteamAccessTokenType.Mobile : SteamAccessTokenType.Refresh; + } + else if (isMobile && isRefresh) + { + tokenType = SteamAccessTokenType.MobileRefresh; + } + else if (audiences.Count == 1) + { + tokenType = SteamAccessTokenType.Web; + } + else + { + if (trying) + { + result = new SteamAuthToken(jwtStr, id, expires, domain, SteamAccessTokenType.Unknown); + return false; + } + var auds = string.Join(", ", audiences); + throw new ArgumentException($"Provided Token has invalid 'aud' claim combination. Values: {auds}", nameof(input)); + } + + } + } + + + result = new SteamAuthToken(jwtStr, id, expires, domain, tokenType); + return true; + } + + public static string ExtractJwtFromSetCookiesHeader(HttpHeaders headers) + { + var setCookies = headers.GetValues("Set-Cookie"); + var steamLoginSecureHeader = setCookies.FirstOrDefault(x => x.StartsWith("steamLoginSecure=")); + if (steamLoginSecureHeader == null) throw new ArgumentException("Can't find steamLoginSecure cookie in Set-Cookie header"); + + var firstCookiePart = steamLoginSecureHeader + .ToCharArray() + .SkipWhile(ch => !ch.Equals('=')) + .Skip(1) + .TakeWhile(ch => !ch.Equals(';')) + .ToArray(); + + var steamLoginSecure = new string(firstCookiePart); + return ExtractJwtToken(steamLoginSecure); + } + + public static string CombineJwtWithSteamId(long steamId, string loginValue) => steamId + "%7C%7C" + loginValue; + internal static bool CheckIfProperLoginValue(string value) => value.Contains("%7C%7C"); + internal static string CombineLoginValueIfNeeded(long steamId, string loginValue) + { + return CheckIfProperLoginValue(loginValue) == false ? CombineJwtWithSteamId(steamId, loginValue) : loginValue; + } + + public static string ExtractJwtToken(string steamLoginSecure) + { + var match = SteamTokenRegex.Match(steamLoginSecure); + if (match.Success == false) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure"); + return match.Groups["jwt"].Value; + } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs b/SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs new file mode 100644 index 0000000..324b246 --- /dev/null +++ b/SteamLibForked/Authentication/TransferSession[folder]/OAuthData.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace SteamLib.Authentication; + +#pragma warning disable CS8618 +public class OAuthData +{ + [JsonProperty("steamid")] public long SteamId { get; set; } + [JsonProperty("oauth_token")] public string OAuthToken { get; set; } + [JsonProperty("webcookie")] public string WebCookie { get; set; } + [JsonProperty("wgtoken")] public string SteamLogin { get; set; } + [JsonProperty("wgtoken_secure")] public string SteamLoginSecure { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs b/SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs new file mode 100644 index 0000000..168905a --- /dev/null +++ b/SteamLibForked/Authentication/TransferSession[folder]/TransferParameters.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace SteamLib.Authentication; + +#pragma warning disable CS8618 +public class TransferParameters +{ + [JsonProperty("steamid")] + public long SteamId { get; set; } + + [JsonProperty("token_secure")] + public string TokenSecure { get; set; } + + [JsonProperty("auth")] + public string Auth { get; set; } + + [JsonProperty("remember_login")] + public bool RememberLogin { get; set; } + + [JsonProperty("webcookie")] + public string WebCookie { get; set; } + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Constants.cs b/SteamLibForked/Core/Constants.cs new file mode 100644 index 0000000..0c6e8dd --- /dev/null +++ b/SteamLibForked/Core/Constants.cs @@ -0,0 +1,36 @@ +namespace SteamLib.Core; + +public static class SteamConstants +{ + #region Base + + public const string STEAM_STORE = "https://store.steampowered.com/"; + public const string STEAM_COMMUNITY = "https://steamcommunity.com/"; + public const string STEAM_API = "https://api.steampowered.com/"; + public const string STEAM_HELP = "https://help.steampowered.com/"; + public const string STEAM_TV = "https://steam.tv/"; + public const string STEAM_CHECKOUT = "https://checkout.steampowered.com/"; + public const string LOGIN_STEAM = "https://login.steampowered.com/"; + + + #endregion + + #region Endpoints + + public const string STEAM_MARKET = STEAM_COMMUNITY + "market/"; + public const string STORE_API = STEAM_STORE + "api/"; + + #endregion + + + #region Misc + + public const string ENGLISH = "english"; + + #endregion + + + + + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/Country.cs b/SteamLibForked/Core/Enums/Country.cs new file mode 100644 index 0000000..92037bf --- /dev/null +++ b/SteamLibForked/Core/Enums/Country.cs @@ -0,0 +1,758 @@ +// ReSharper disable All + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace SteamLib.Core.Enums; + + +public enum Country +{ + + Afghanistan, + Aland_Islands, + Albania, + Algeria, + American_Samoa, + Andorra, + Angola, + Anguilla, + Antigua_and_Barbuda, + Argentina, + Armenia, + Aruba, + Australia, + Austria, + Azerbaijan, + Bahamas, + Bahrain, + Bangladesh, + Barbados, + Belarus, + Belgium, + Belize, + Bhutan, + Bolivia, + Bosnia_and_Herzegovina, + Botswana, + Brazil, + Brunei_Darussalam, + Bulgaria, + Burundi, + Cambodia, + Cameroon, + Canada, + Cape_Verde, + Cayman_Islands, + Chad, + Chile, + China, + Colombia, + Congo, + Costa_Rica, + Cote_d_Ivoire, + Croatia, + Cyprus, + Czech_Republic, + Denmark, + Dominica, + Dominican_Republic, + Ecuador, + Egypt, + El_Salvador, + Estonia, + Eswatini, + Ethiopia, + Faroe_Islands, + Finland, + France, + French_Guiana, + French_Polynesia, + Gabon, + Georgia, + Germany, + Ghana, + Gibraltar, + Greece, + Greenland, + Grenada, + Guadeloupe, + Guam, + Guatemala, + Guernsey, + Guyana, + Haiti, + Honduras, + Hong_Kong, + Hungary, + Iceland, + India, + Indonesia, + Iran, + Iraq, + Ireland, + Isle_of_Man, + Israel, + Italy, + Jamaica, + Japan, + Jersey, + Jordan, + Kazakhstan, + Kenya, + Korea, + Kuwait, + Kyrgyzstan, + Laos, + Latvia, + Lebanon, + Libya, + Liechtenstein, + Lithuania, + Luxembourg, + Macau, + Macedonia, + Madagascar, + Malaysia, + Maldives, + Mali, + Malta, + Martinique, + Mauritius, + Mayotte, + Mexico, + Moldova, + Monaco, + Mongolia, + Montenegro, + Morocco, + Mozambique, + Myanmar, + Namibia, + Nepal, + Netherlands, + New_Zealand, + Nicaragua, + Nigeria, + Northern_Mariana_Islands, + Norway, + Oman, + Pakistan, + Palestinian_Territory, + Panama, + Papua_New_Guinea, + Paraguay, + Peru, + Philippines, + Poland, + Portugal, + Puerto_Rico, + Qatar, + Reunion, + Romania, + Russian_Federation, + Rwanda, + Saint_Kitts_and_Nevis, + Saint_Lucia, + Saint_Vincent_and_the_Grenadines, + Saudi_Arabia, + Senegal, + Serbia, + Seychelles, + Singapore, + Slovakia, + Slovenia, + South_Africa, + Spain, + Sri_Lanka, + Sudan, + Suriname, + Sweden, + Switzerland, + Taiwan, + Tajikistan, + Tanzania, + Thailand, + Togo, + Trinidad_and_Tobago, + Tunisia, + Turkey, + Turkmenistan, + Turks_and_Caicos_Islands, + Uganda, + Ukraine, + United_Arab_Emirates, + United_Kingdom, + United_States, + Uruguay, + Uzbekistan, + Venezuela, + Viet_Nam, + Virgin_Islands, + Yemen, + Zambia, + Zimbabwe, + +} +public static class CountryResolver +{ + public static IReadOnlyDictionary ResolveString { get; } = new Dictionary + { + { Country.Afghanistan , "Afghanistan" }, + { Country.Aland_Islands , "Aland Islands" }, + { Country.Albania , "Albania" }, + { Country.Algeria , "Algeria" }, + { Country.American_Samoa , "American Samoa" }, + { Country.Andorra , "Andorra" }, + { Country.Angola , "Angola" }, + { Country.Anguilla , "Anguilla" }, + { Country.Antigua_and_Barbuda , "Antigua and Barbuda" }, + { Country.Argentina , "Argentina" }, + { Country.Armenia , "Armenia" }, + { Country.Aruba , "Aruba" }, + { Country.Australia , "Australia" }, + { Country.Austria , "Austria" }, + { Country.Azerbaijan , "Azerbaijan" }, + { Country.Bahamas , "Bahamas" }, + { Country.Bahrain , "Bahrain" }, + { Country.Bangladesh , "Bangladesh" }, + { Country.Barbados , "Barbados" }, + { Country.Belarus , "Belarus" }, + { Country.Belgium , "Belgium" }, + { Country.Belize , "Belize" }, + { Country.Bhutan , "Bhutan" }, + { Country.Bolivia , "Bolivia" }, + { Country.Bosnia_and_Herzegovina , "Bosnia and Herzegovina" }, + { Country.Botswana , "Botswana" }, + { Country.Brazil , "Brazil" }, + { Country.Brunei_Darussalam , "Brunei Darussalam" }, + { Country.Bulgaria , "Bulgaria" }, + { Country.Burundi , "Burundi" }, + { Country.Cambodia , "Cambodia" }, + { Country.Cameroon , "Cameroon" }, + { Country.Canada , "Canada" }, + { Country.Cape_Verde , "Cape Verde" }, + { Country.Cayman_Islands , "Cayman Islands" }, + { Country.Chad , "Chad" }, + { Country.Chile , "Chile" }, + { Country.China , "China" }, + { Country.Colombia , "Colombia" }, + { Country.Congo , "Congo, the Democratic Republic of the" }, + { Country.Costa_Rica , "Costa Rica" }, + { Country.Cote_d_Ivoire , "Cote d'Ivoire" }, + { Country.Croatia , "Croatia" }, + { Country.Cyprus , "Cyprus" }, + { Country.Czech_Republic , "Czech Republic" }, + { Country.Denmark , "Denmark" }, + { Country.Dominica , "Dominica" }, + { Country.Dominican_Republic , "Dominican Republic" }, + { Country.Ecuador , "Ecuador" }, + { Country.Egypt , "Egypt" }, + { Country.El_Salvador , "El Salvador" }, + { Country.Estonia , "Estonia" }, + { Country.Eswatini , "Eswatini" }, + { Country.Ethiopia , "Ethiopia" }, + { Country.Faroe_Islands , "Faroe Islands" }, + { Country.Finland , "Finland" }, + { Country.France , "France" }, + { Country.French_Guiana , "French Guiana" }, + { Country.French_Polynesia , "French Polynesia" }, + { Country.Gabon , "Gabon" }, + { Country.Georgia , "Georgia" }, + { Country.Germany , "Germany" }, + { Country.Ghana , "Ghana" }, + { Country.Gibraltar , "Gibraltar" }, + { Country.Greece , "Greece" }, + { Country.Greenland , "Greenland" }, + { Country.Grenada , "Grenada" }, + { Country.Guadeloupe , "Guadeloupe" }, + { Country.Guam , "Guam" }, + { Country.Guatemala , "Guatemala" }, + { Country.Guernsey , "Guernsey" }, + { Country.Guyana , "Guyana" }, + { Country.Haiti , "Haiti" }, + { Country.Honduras , "Honduras" }, + { Country.Hong_Kong , "Hong Kong" }, + { Country.Hungary , "Hungary" }, + { Country.Iceland , "Iceland" }, + { Country.India , "India" }, + { Country.Indonesia , "Indonesia" }, + { Country.Iran , "Iran" }, + { Country.Iraq , "Iraq" }, + { Country.Ireland , "Ireland" }, + { Country.Isle_of_Man , "Isle of Man" }, + { Country.Israel , "Israel" }, + { Country.Italy , "Italy" }, + { Country.Jamaica , "Jamaica" }, + { Country.Japan , "Japan" }, + { Country.Jersey , "Jersey" }, + { Country.Jordan , "Jordan" }, + { Country.Kazakhstan , "Kazakhstan" }, + { Country.Kenya , "Kenya" }, + { Country.Korea , "Korea, Republic of" }, + { Country.Kuwait , "Kuwait" }, + { Country.Kyrgyzstan , "Kyrgyzstan" }, + { Country.Laos , "Lao People's Democratic Republic" }, + { Country.Latvia , "Latvia" }, + { Country.Lebanon , "Lebanon" }, + { Country.Libya , "Libya" }, + { Country.Liechtenstein , "Liechtenstein" }, + { Country.Lithuania , "Lithuania" }, + { Country.Luxembourg , "Luxembourg" }, + { Country.Macau , "Macau" }, + { Country.Macedonia , "Macedonia, the former Yugoslav Republic of" }, + { Country.Madagascar , "Madagascar" }, + { Country.Malaysia , "Malaysia" }, + { Country.Maldives , "Maldives" }, + { Country.Mali , "Mali" }, + { Country.Malta , "Malta" }, + { Country.Martinique , "Martinique" }, + { Country.Mauritius , "Mauritius" }, + { Country.Mayotte , "Mayotte" }, + { Country.Mexico , "Mexico" }, + { Country.Moldova , "Moldova, Republic of" }, + { Country.Monaco , "Monaco" }, + { Country.Mongolia , "Mongolia" }, + { Country.Montenegro , "Montenegro" }, + { Country.Morocco , "Morocco" }, + { Country.Mozambique , "Mozambique" }, + { Country.Myanmar , "Myanmar" }, + { Country.Namibia , "Namibia" }, + { Country.Nepal , "Nepal" }, + { Country.Netherlands , "Netherlands" }, + { Country.New_Zealand , "New Zealand" }, + { Country.Nicaragua , "Nicaragua" }, + { Country.Nigeria , "Nigeria" }, + { Country.Northern_Mariana_Islands , "Northern Mariana Islands" }, + { Country.Norway , "Norway" }, + { Country.Oman , "Oman" }, + { Country.Pakistan , "Pakistan" }, + { Country.Palestinian_Territory , "Palestinian Territory, Occupied" }, + { Country.Panama , "Panama" }, + { Country.Papua_New_Guinea , "Papua New Guinea" }, + { Country.Paraguay , "Paraguay" }, + { Country.Peru , "Peru" }, + { Country.Philippines , "Philippines" }, + { Country.Poland , "Poland" }, + { Country.Portugal , "Portugal" }, + { Country.Puerto_Rico , "Puerto Rico" }, + { Country.Qatar , "Qatar" }, + { Country.Reunion , "Reunion" }, + { Country.Romania , "Romania" }, + { Country.Russian_Federation , "Russian Federation" }, + { Country.Rwanda , "Rwanda" }, + { Country.Saint_Kitts_and_Nevis , "Saint Kitts and Nevis" }, + { Country.Saint_Lucia , "Saint Lucia" }, + { Country.Saint_Vincent_and_the_Grenadines , "Saint Vincent and the Grenadines" }, + { Country.Saudi_Arabia , "Saudi Arabia" }, + { Country.Senegal , "Senegal" }, + { Country.Serbia , "Serbia" }, + { Country.Seychelles , "Seychelles" }, + { Country.Singapore , "Singapore" }, + { Country.Slovakia , "Slovakia" }, + { Country.Slovenia , "Slovenia" }, + { Country.South_Africa , "South Africa" }, + { Country.Spain , "Spain" }, + { Country.Sri_Lanka , "Sri Lanka" }, + { Country.Sudan , "Sudan" }, + { Country.Suriname , "Suriname" }, + { Country.Sweden , "Sweden" }, + { Country.Switzerland , "Switzerland" }, + { Country.Taiwan , "Taiwan" }, + { Country.Tajikistan , "Tajikistan" }, + { Country.Tanzania , "Tanzania, United Republic of" }, + { Country.Thailand , "Thailand" }, + { Country.Togo , "Togo" }, + { Country.Trinidad_and_Tobago , "Trinidad and Tobago" }, + { Country.Tunisia , "Tunisia" }, + { Country.Turkey , "Turkey" }, + { Country.Turkmenistan , "Turkmenistan" }, + { Country.Turks_and_Caicos_Islands , "Turks and Caicos Islands" }, + { Country.Uganda , "Uganda" }, + { Country.Ukraine , "Ukraine" }, + { Country.United_Arab_Emirates , "United Arab Emirates" }, + { Country.United_Kingdom , "United Kingdom" }, + { Country.United_States , "United States" }, + { Country.Uruguay , "Uruguay" }, + { Country.Uzbekistan , "Uzbekistan" }, + { Country.Venezuela , "Venezuela" }, + { Country.Viet_Nam , "Viet Nam" }, + { Country.Virgin_Islands , "Virgin Islands, U.S." }, + { Country.Yemen , "Yemen" }, + { Country.Zambia , "Zambia" }, + { Country.Zimbabwe , "Zimbabwe" }, + }; + public static IReadOnlyDictionary ResolveCountry { get; } = new Dictionary() + { + {"Afghanistan", Country.Afghanistan}, + {"Aland Islands", Country.Aland_Islands}, + {"Albania", Country.Albania}, + {"Algeria", Country.Algeria}, + {"American Samoa", Country.American_Samoa}, + {"Andorra", Country.Andorra}, + {"Angola", Country.Angola}, + {"Anguilla", Country.Anguilla}, + {"Antigua and Barbuda", Country.Antigua_and_Barbuda}, + {"Argentina", Country.Argentina}, + {"Armenia", Country.Armenia}, + {"Aruba", Country.Aruba}, + {"Australia", Country.Australia}, + {"Austria", Country.Austria}, + {"Azerbaijan", Country.Azerbaijan}, + {"Bahamas", Country.Bahamas}, + {"Bahrain", Country.Bahrain}, + {"Bangladesh", Country.Bangladesh}, + {"Barbados", Country.Barbados}, + {"Belarus", Country.Belarus}, + {"Belgium", Country.Belgium}, + {"Belize", Country.Belize}, + {"Bhutan", Country.Bhutan}, + {"Bolivia", Country.Bolivia}, + {"Bosnia and Herzegovina", Country.Bosnia_and_Herzegovina}, + {"Botswana", Country.Botswana}, + {"Brazil", Country.Brazil}, + {"Brunei Darussalam", Country.Brunei_Darussalam}, + {"Bulgaria", Country.Bulgaria}, + {"Burundi", Country.Burundi}, + {"Cambodia", Country.Cambodia}, + {"Cameroon", Country.Cameroon}, + {"Canada", Country.Canada}, + {"Cape Verde", Country.Cape_Verde}, + {"Cayman Islands", Country.Cayman_Islands}, + {"Chad", Country.Chad}, + {"Chile", Country.Chile}, + {"China", Country.China}, + {"Colombia", Country.Colombia}, + {"Congo, the Democratic Republic of the", Country.Congo}, + {"Costa Rica", Country.Costa_Rica}, + {"Cote d'Ivoire", Country.Cote_d_Ivoire}, + {"Croatia", Country.Croatia}, + {"Cyprus", Country.Cyprus}, + {"Czech Republic", Country.Czech_Republic}, + {"Denmark", Country.Denmark}, + {"Dominica", Country.Dominica}, + {"Dominican Republic", Country.Dominican_Republic}, + {"Ecuador", Country.Ecuador}, + {"Egypt", Country.Egypt}, + {"El Salvador", Country.El_Salvador}, + {"Estonia", Country.Estonia}, + {"Eswatini", Country.Eswatini}, + {"Ethiopia", Country.Ethiopia}, + {"Faroe Islands", Country.Faroe_Islands}, + {"Finland", Country.Finland}, + {"France", Country.France}, + {"French Guiana", Country.French_Guiana}, + {"French Polynesia", Country.French_Polynesia}, + {"Gabon", Country.Gabon}, + {"Georgia", Country.Georgia}, + {"Germany", Country.Germany}, + {"Ghana", Country.Ghana}, + {"Gibraltar", Country.Gibraltar}, + {"Greece", Country.Greece}, + {"Greenland", Country.Greenland}, + {"Grenada", Country.Grenada}, + {"Guadeloupe", Country.Guadeloupe}, + {"Guam", Country.Guam}, + {"Guatemala", Country.Guatemala}, + {"Guernsey", Country.Guernsey}, + {"Guyana", Country.Guyana}, + {"Haiti", Country.Haiti}, + {"Honduras", Country.Honduras}, + {"Hong Kong", Country.Hong_Kong}, + {"Hungary", Country.Hungary}, + {"Iceland", Country.Iceland}, + {"India", Country.India}, + {"Indonesia", Country.Indonesia}, + {"Iran", Country.Iran}, + {"Iraq", Country.Iraq}, + {"Ireland", Country.Ireland}, + {"Isle of Man", Country.Isle_of_Man}, + {"Israel", Country.Israel}, + {"Italy", Country.Italy}, + {"Jamaica", Country.Jamaica}, + {"Japan", Country.Japan}, + {"Jersey", Country.Jersey}, + {"Jordan", Country.Jordan}, + {"Kazakhstan", Country.Kazakhstan}, + {"Kenya", Country.Kenya}, + {"Korea, Republic of", Country.Korea}, + {"Kuwait", Country.Kuwait}, + {"Kyrgyzstan", Country.Kyrgyzstan}, + {"Lao People's Democratic Republic", Country.Laos}, + {"Latvia", Country.Latvia}, + {"Lebanon", Country.Lebanon}, + {"Libya", Country.Libya}, + {"Liechtenstein", Country.Liechtenstein}, + {"Lithuania", Country.Lithuania}, + {"Luxembourg", Country.Luxembourg}, + {"Macau", Country.Macau}, + {"Macedonia, the former Yugoslav Republic of", Country.Macedonia}, + {"Madagascar", Country.Madagascar}, + {"Malaysia", Country.Malaysia}, + {"Maldives", Country.Maldives}, + {"Mali", Country.Mali}, + {"Malta", Country.Malta}, + {"Martinique", Country.Martinique}, + {"Mauritius", Country.Mauritius}, + {"Mayotte", Country.Mayotte}, + {"Mexico", Country.Mexico}, + {"Moldova, Republic of", Country.Moldova}, + {"Monaco", Country.Monaco}, + {"Mongolia", Country.Mongolia}, + {"Montenegro", Country.Montenegro}, + {"Morocco", Country.Morocco}, + {"Mozambique", Country.Mozambique}, + {"Myanmar", Country.Myanmar}, + {"Namibia", Country.Namibia}, + {"Nepal", Country.Nepal}, + {"Netherlands", Country.Netherlands}, + {"New Zealand", Country.New_Zealand}, + {"Nicaragua", Country.Nicaragua}, + {"Nigeria", Country.Nigeria}, + {"Northern Mariana Islands", Country.Northern_Mariana_Islands}, + {"Norway", Country.Norway}, + {"Oman", Country.Oman}, + {"Pakistan", Country.Pakistan}, + {"Palestinian Territory, Occupied", Country.Palestinian_Territory}, + {"Panama", Country.Panama}, + {"Papua New Guinea", Country.Papua_New_Guinea}, + {"Paraguay", Country.Paraguay}, + {"Peru", Country.Peru}, + {"Philippines", Country.Philippines}, + {"Poland", Country.Poland}, + {"Portugal", Country.Portugal}, + {"Puerto Rico", Country.Puerto_Rico}, + {"Qatar", Country.Qatar}, + {"Reunion", Country.Reunion}, + {"Romania", Country.Romania}, + {"Russian Federation", Country.Russian_Federation}, + {"Rwanda", Country.Rwanda}, + {"Saint Kitts and Nevis", Country.Saint_Kitts_and_Nevis}, + {"Saint Lucia", Country.Saint_Lucia}, + {"Saint Vincent and the Grenadines", Country.Saint_Vincent_and_the_Grenadines}, + {"Saudi Arabia", Country.Saudi_Arabia}, + {"Senegal", Country.Senegal}, + {"Serbia", Country.Serbia}, + {"Seychelles", Country.Seychelles}, + {"Singapore", Country.Singapore}, + {"Slovakia", Country.Slovakia}, + {"Slovenia", Country.Slovenia}, + {"South Africa", Country.South_Africa}, + {"Spain", Country.Spain}, + {"Sri Lanka", Country.Sri_Lanka}, + {"Sudan", Country.Sudan}, + {"Suriname", Country.Suriname}, + {"Sweden", Country.Sweden}, + {"Switzerland", Country.Switzerland}, + {"Taiwan", Country.Taiwan}, + {"Tajikistan", Country.Tajikistan}, + {"Tanzania, United Republic of", Country.Tanzania}, + {"Thailand", Country.Thailand}, + {"Togo", Country.Togo}, + {"Trinidad and Tobago", Country.Trinidad_and_Tobago}, + {"Tunisia", Country.Tunisia}, + {"Turkey", Country.Turkey}, + {"Turkmenistan", Country.Turkmenistan}, + {"Turks and Caicos Islands", Country.Turks_and_Caicos_Islands}, + {"Uganda", Country.Uganda}, + {"Ukraine", Country.Ukraine}, + {"United Arab Emirates", Country.United_Arab_Emirates}, + {"United Kingdom", Country.United_Kingdom}, + {"United States", Country.United_States}, + {"Uruguay", Country.Uruguay}, + {"Uzbekistan", Country.Uzbekistan}, + {"Venezuela", Country.Venezuela}, + {"Viet Nam", Country.Viet_Nam}, + {"Virgin Islands, U.S.", Country.Virgin_Islands}, + {"Yemen", Country.Yemen}, + {"Zambia", Country.Zambia}, + {"Zimbabwe", Country.Zimbabwe}, + }; + + public static IReadOnlyDictionary ResolveByCountryCode = new ReadOnlyDictionary( + new Dictionary() + { + {"US",Country.United_States}, + {"CA",Country.Canada}, + {"AF",Country.Afghanistan}, + {"AX",Country.Aland_Islands}, + {"AL",Country.Albania}, + {"DZ",Country.Algeria}, + {"AS",Country.American_Samoa}, + {"AD",Country.Andorra}, + {"AO",Country.Angola}, + {"AI",Country.Anguilla}, + {"AG",Country.Antigua_and_Barbuda}, + {"AR",Country.Argentina}, + {"AM",Country.Armenia}, + {"AW",Country.Aruba}, + {"AU",Country.Australia}, + {"AT",Country.Austria}, + {"AZ",Country.Azerbaijan}, + {"BS",Country.Bahamas}, + {"BH",Country.Bahrain}, + {"BD",Country.Bangladesh}, + {"BB",Country.Barbados}, + {"BY",Country.Belarus}, + {"BE",Country.Belgium}, + {"BZ",Country.Belize}, + {"BT",Country.Bhutan}, + {"BO",Country.Bolivia}, + {"BA",Country.Bosnia_and_Herzegovina}, + {"BW",Country.Botswana}, + {"BR",Country.Brazil}, + {"BN",Country.Brunei_Darussalam}, + {"BG",Country.Bulgaria}, + {"BI",Country.Burundi}, + {"KH",Country.Cambodia}, + {"CM",Country.Cameroon}, + {"CV",Country.Cape_Verde}, + {"KY",Country.Cayman_Islands}, + {"TD",Country.Chad}, + {"CL",Country.Chile}, + {"CN",Country.China}, + {"CO",Country.Colombia}, + {"CD",Country.Congo}, + {"CR",Country.Costa_Rica}, + {"CI", Country.Cote_d_Ivoire}, + {"HR",Country.Croatia}, + {"CY",Country.Cyprus}, + {"CZ",Country.Czech_Republic}, + {"DK",Country.Denmark}, + {"DM",Country.Dominica}, + {"DO",Country.Dominican_Republic}, + {"EC",Country.Ecuador}, + {"EG",Country.Egypt}, + {"SV",Country.El_Salvador}, + {"EE",Country.Estonia}, + {"ET",Country.Ethiopia}, + {"FO",Country.Faroe_Islands}, + {"FI",Country.Finland}, + {"FR",Country.France}, + {"GF",Country.French_Guiana}, + {"PF",Country.French_Polynesia}, + {"GA",Country.Gabon}, + {"GE",Country.Georgia}, + {"DE",Country.Germany}, + {"GH",Country.Ghana}, + {"GI",Country.Gibraltar}, + {"GR",Country.Greece}, + {"GL",Country.Greenland}, + {"GD",Country.Grenada}, + {"GP",Country.Guadeloupe}, + {"GU",Country.Guam}, + {"GT",Country.Guatemala}, + {"GG",Country.Guernsey}, + {"GY",Country.Guyana}, + {"HT",Country.Haiti}, + {"HN",Country.Honduras}, + {"HK",Country.Hong_Kong}, + {"HU",Country.Hungary}, + {"IS",Country.Iceland}, + {"IN",Country.India}, + {"ID",Country.Indonesia}, + {"IQ",Country.Iraq}, + {"IE",Country.Ireland}, + {"IR",Country.Iran}, + {"IM",Country.Isle_of_Man}, + {"IL",Country.Israel}, + {"IT",Country.Italy}, + {"JM",Country.Jamaica}, + {"JP",Country.Japan}, + {"JE",Country.Jersey}, + {"JO",Country.Jordan}, + {"KZ",Country.Kazakhstan}, + {"KE",Country.Kenya}, + {"KR",Country.Korea}, + {"KW",Country.Kuwait}, + {"KG",Country.Kyrgyzstan}, + {"LA",Country.Laos}, + {"LV",Country.Latvia}, + {"LB",Country.Lebanon}, + {"LY",Country.Libya}, + {"LI",Country.Liechtenstein}, + {"LT",Country.Lithuania}, + {"LU",Country.Luxembourg}, + {"MO",Country.Macau}, + {"MK",Country.Macedonia}, + {"MG",Country.Madagascar}, + {"MY",Country.Malaysia}, + {"MV",Country.Maldives}, + {"ML",Country.Mali}, + {"MT",Country.Malta}, + {"MQ",Country.Martinique}, + {"MU",Country.Mauritius}, + {"YT",Country.Mayotte}, + {"MX",Country.Mexico}, + {"MD",Country.Moldova}, + {"MC",Country.Monaco}, + {"MN",Country.Mongolia}, + {"ME",Country.Montenegro}, + {"MA",Country.Morocco}, + {"MZ",Country.Mozambique}, + {"MM",Country.Myanmar}, + {"NA",Country.Namibia}, + {"NP",Country.Nepal}, + {"NL",Country.Netherlands}, + {"NZ",Country.New_Zealand}, + {"NI",Country.Nicaragua}, + {"NG",Country.Nigeria}, + {"MP",Country.Northern_Mariana_Islands}, + {"NO",Country.Norway}, + {"OM",Country.Oman}, + {"PK",Country.Pakistan}, + {"PS",Country.Palestinian_Territory}, + {"PA",Country.Panama}, + {"PG",Country.Papua_New_Guinea}, + {"PY",Country.Paraguay}, + {"PE",Country.Peru}, + {"PH",Country.Philippines}, + {"PL",Country.Poland}, + {"PT",Country.Portugal}, + {"PR",Country.Puerto_Rico}, + {"QA",Country.Qatar}, + {"RE",Country.Reunion}, + {"RO",Country.Romania}, + {"RU",Country.Russian_Federation}, + {"RW",Country.Rwanda}, + {"LC",Country.Saint_Lucia}, + {"SA",Country.Saudi_Arabia}, + {"SN",Country.Senegal}, + {"RS",Country.Serbia}, + {"SC",Country.Seychelles}, + {"SG",Country.Singapore}, + {"SK",Country.Slovakia}, + {"SI",Country.Slovenia}, + {"ZA",Country.South_Africa}, + {"ES",Country.Spain}, + {"LK",Country.Sri_Lanka}, + {"KN",Country.Saint_Kitts_and_Nevis}, + {"PM", Country.Saint_Vincent_and_the_Grenadines}, + {"SD",Country.Sudan}, + {"SR",Country.Suriname}, + {"SE",Country.Sweden}, + {"CH",Country.Switzerland}, + {"TW",Country.Taiwan}, + {"TJ",Country.Tajikistan}, + {"TZ",Country.Tanzania}, + {"TH",Country.Thailand}, + {"TG",Country.Togo}, + {"TT",Country.Trinidad_and_Tobago}, + {"TN",Country.Tunisia}, + {"TR",Country.Turkey}, + {"TM",Country.Turkmenistan}, + {"TC",Country.Turks_and_Caicos_Islands}, + {"UG",Country.Uganda}, + {"UA",Country.Ukraine}, + {"GB",Country.United_Kingdom}, + {"VI", Country.Virgin_Islands}, + {"AE",Country.United_Arab_Emirates}, + {"UY",Country.Uruguay}, + {"UZ",Country.Uzbekistan}, + {"VE",Country.Venezuela}, + {"VN",Country.Viet_Nam}, + {"YE",Country.Yemen}, + {"ZM",Country.Zambia}, + {"ZW",Country.Zimbabwe}, + }); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/Currency.cs b/SteamLibForked/Core/Enums/Currency.cs new file mode 100644 index 0000000..714cc82 --- /dev/null +++ b/SteamLibForked/Core/Enums/Currency.cs @@ -0,0 +1,100 @@ +// ReSharper disable InconsistentNaming +namespace SteamLib.Core.Enums; + +public enum Currency +{ + None = 0, + USD = 1, + GBP = 2, + EUR = 3, + CHF = 4, + RUB = 5, + PLN = 6, + BRL = 7, + JPY = 8, + NOK = 9, + IDR = 10, + MYR = 11, + PHP = 12, + SGD = 13, + THB = 14, + VND = 15, + KRW = 16, + TRY = 17, + UAH = 18, + MXN = 19, + CAD = 20, + AUD = 21, + NZD = 22, + CNY = 23, + INR = 24, + CLP = 25, + PEN = 26, + COP = 27, + ZAR = 28, + HKD = 29, + TWD = 30, + SAR = 31, + AED = 32, + ARS = 34, + ILS = 35, + KZT = 37, + KWD = 38, + QAR = 39, + CRC = 40, + UYU = 41 +} +public static class CurrencyInfo +{ + public static int ToInt(this Currency currency) + { + return (int)currency; + } + + public static string ToIntString(this Currency currency) + { + return currency.ToInt().ToString(); + } + public static IReadOnlyDictionary CurrencySymbols { get; } = new Dictionary + { + { Currency.USD, "$" }, + { Currency.GBP, "£" }, + { Currency.EUR, "€" }, + { Currency.CHF, "CHF" }, + { Currency.RUB, "pуб." }, + { Currency.PLN, "zł" }, + { Currency.BRL, "R$" }, + { Currency.JPY, "¥" }, + { Currency.NOK, "kr" }, + { Currency.IDR, "Rp" }, + { Currency.MYR, "RM" }, + { Currency.PHP, "P" }, + { Currency.SGD, "S$" }, + { Currency.THB, "฿" }, + { Currency.VND, "₫" }, + { Currency.KRW, "₩" }, + { Currency.TRY, "TL" }, + { Currency.UAH, "₴" }, + { Currency.MXN, "Mex$" }, + { Currency.CAD, "CDN$" }, + { Currency.AUD, "A$" }, + { Currency.NZD, "NZ$" }, + { Currency.CNY, "¥" }, + { Currency.INR, "₹" }, + { Currency.CLP, "CLP$" }, + { Currency.PEN, "S/." }, + { Currency.COP, "COL$" }, + { Currency.ZAR, "R" }, + { Currency.HKD, "HK$" }, + { Currency.TWD, "NT$" }, + { Currency.SAR, "SR" }, + { Currency.AED, "AED" }, + { Currency.ARS, "ARS$" }, + { Currency.ILS, "₪" }, + { Currency.KZT, "₸" }, + { Currency.KWD, "KD" }, + { Currency.QAR, "QR" }, + { Currency.CRC, "₡" }, + { Currency.UYU, "$U" }, + }; +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/SteamAccessTokenType.cs b/SteamLibForked/Core/Enums/SteamAccessTokenType.cs new file mode 100644 index 0000000..12552d3 --- /dev/null +++ b/SteamLibForked/Core/Enums/SteamAccessTokenType.cs @@ -0,0 +1,28 @@ +using SteamLib.Api.Mobile; + +namespace SteamLib.Core.Enums; + +public enum SteamAccessTokenType +{ + Unknown = 0, + /// + /// Temporary token that returned after login + /// + Web, + /// + /// Access token that attached to domain + /// + AccessToken, + /// + /// Refresh token that can be used to refresh AccessToken + /// + Refresh, + /// + /// Mobile token that can be used to access mobile endpoints. Returned after login or by refreshing with + /// + Mobile, + /// + /// Refresh token that can be used to refresh Mobile token + /// + MobileRefresh, +} \ No newline at end of file diff --git a/SteamLibForked/Core/Enums/SteamDomain.cs b/SteamLibForked/Core/Enums/SteamDomain.cs new file mode 100644 index 0000000..f20f267 --- /dev/null +++ b/SteamLibForked/Core/Enums/SteamDomain.cs @@ -0,0 +1,12 @@ +namespace SteamLib.Core.Enums; + +public enum SteamDomain +{ + Undefined = 0, + Community = 1, + Store = 2, + Help = 3, + TV = 4, + Checkout = 5, +} + diff --git a/SteamLibForked/Core/HealthMonitor.cs b/SteamLibForked/Core/HealthMonitor.cs new file mode 100644 index 0000000..8dd8d3f --- /dev/null +++ b/SteamLibForked/Core/HealthMonitor.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.Logging; + +namespace SteamLib.Core; + +public static class HealthMonitor +{ + public static ILogger? FatalLogger { get; set; } + internal static void LogUnexpected(string response, Exception ex) + { + FatalLogger?.LogCritical(ex, "Unexpected response detected:\n{response}", response); + } + + + internal static T LogOnException(string resp, Func del) + { + try + { + return del(); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del) + { + try + { + return del(resp); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + internal static T LogOnException(string resp, Func del, params Type[] exceptExceptions) + { + try + { + return del(); + } + catch (Exception ex) + when (exceptExceptions.Any(t => t == ex.GetType()) == false) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del, params Type[] exceptExceptions) + { + try + { + return del(resp); + } + catch (Exception ex) + when (exceptExceptions.Any(t => t == ex.GetType()) == false) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del, Func logPredicate) + { + try + { + return del(); + } + catch (Exception ex) + when (logPredicate(ex)) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static T LogOnException(string resp, Func del, Func logPredicate) + { + try + { + return del(resp); + } + catch (Exception ex) + when (logPredicate(ex)) + { + LogUnexpected(resp, ex); + throw; + } + } + + internal static void LogOnException(string resp, Action del) + { + try + { + del(); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + internal static void LogOnException(string resp, Action del, Func logPredicate) + { + try + { + del(); + } + catch (Exception ex) + when (logPredicate(ex)) + { + LogUnexpected(resp, ex); + throw; + } + } + + + internal static async Task LogOnExceptionAsync(string resp, Func> del) + { + try + { + return await del(); + } + catch (Exception ex) + { + LogUnexpected(resp, ex); + throw; + } + } + internal static async Task LogOnExceptionAsync(string resp, Func> del, params Type[] exceptExceptions) + { + try + { + return await del(); + } + catch (Exception ex) + when (exceptExceptions.Any(t => t == ex.GetType()) == false) + { + LogUnexpected(resp, ex); + throw; + } + } + + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ICaptchaResolver.cs b/SteamLibForked/Core/Interfaces/ICaptchaResolver.cs new file mode 100644 index 0000000..a2d75d5 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ICaptchaResolver.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface ICaptchaResolver +{ + public Task Resolve(Uri imageUrl, HttpClient client); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs b/SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs new file mode 100644 index 0000000..82dacd5 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/IConfirmationConsumer.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface ILoginConsumer +{ + public string FriendlyName { get; } +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs b/SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs new file mode 100644 index 0000000..6bd5eeb --- /dev/null +++ b/SteamLibForked/Core/Interfaces/IEmailAuthProvider.cs @@ -0,0 +1,15 @@ +namespace SteamLib.Core.Interfaces; + +public enum EmailConfirmationType +{ + AttachPhoneAuthenticator +} +public interface IEmailProvider +{ + public int MaxRetryCount { get; } + public Task GetEmailAuthCode(ILoginConsumer caller); + public Task GetAddAuthenticatorCode(ILoginConsumer caller); + + public Task ConfirmEmailLink(ILoginConsumer caller, EmailConfirmationType confirmationType); + +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs b/SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs new file mode 100644 index 0000000..ca58421 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/IPhoneNumberProvider.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface IPhoneNumberProvider +{ + public Task GetPhoneNumber(ILoginConsumer caller); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ISessionData.cs b/SteamLibForked/Core/Interfaces/ISessionData.cs new file mode 100644 index 0000000..2cbcdda --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ISessionData.cs @@ -0,0 +1,23 @@ +using SteamLib.Account; +using SteamLib.Core.Enums; + +namespace SteamLib.Core.Interfaces; + +//WARNING: Any changes here should be reflected in MafileSerializer.cs +public interface ISessionData : ICloneable +{ + internal bool? IsValid { get; set; } + public bool IsExpired => RefreshToken.IsExpired; + public string SessionId { get; } + public SteamId SteamId { get; } + public SteamAuthToken RefreshToken { get; } + + public SteamAuthToken? GetToken(SteamDomain domain); + public void SetToken(SteamDomain domain, SteamAuthToken token); +} + +public interface IMobileSessionData : ISessionData +{ + public SteamAuthToken? GetMobileToken(); + public void SetMobileToken(SteamAuthToken token); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs b/SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs new file mode 100644 index 0000000..d7810fe --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ISmsCodeProvider.cs @@ -0,0 +1,6 @@ +namespace SteamLib.Core.Interfaces; + +public interface ISmsCodeProvider +{ + public Task GetSmsCode(ILoginConsumer caller, long? phoneNumber, string? phoneHint); +} \ No newline at end of file diff --git a/SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs b/SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs new file mode 100644 index 0000000..dacbb88 --- /dev/null +++ b/SteamLibForked/Core/Interfaces/ISteamGuardProvider.cs @@ -0,0 +1,7 @@ +namespace SteamLib.Core.Interfaces; + +public interface ISteamGuardProvider +{ + public int MaxRetryCount { get; } + public ValueTask GetSteamGuardCode(ILoginConsumer caller); +} \ No newline at end of file diff --git a/SteamLibForked/Core/StatusCodes/SteamStatusCode.cs b/SteamLibForked/Core/StatusCodes/SteamStatusCode.cs new file mode 100644 index 0000000..73fa04d --- /dev/null +++ b/SteamLibForked/Core/StatusCodes/SteamStatusCode.cs @@ -0,0 +1,166 @@ +using SteamLib.Exceptions; +using SteamLib.Utility; +using SteamLib.Utility.Models; +using System.Collections.ObjectModel; + +namespace SteamLib.Core.StatusCodes; + +public partial class SteamStatusCode : Enumeration +{ + public static IReadOnlyDictionary StatusCodes { get; } + + protected SteamStatusCode(int id, string name) : base(id, name) { } + + static SteamStatusCode() + { + StatusCodes = new ReadOnlyDictionary(GetAll().ToDictionary(ssc => ssc.Id, ssc => ssc)); + } + + /// + /// Tries to translate status code. If no status code was found with will be thrown + /// + /// + /// + /// + /// if value is universal. if value is specific + /// + public static SteamStatusCode Translate(string response, out bool isUniversal) where T : SteamStatusCode + { + int statusCode; + try + { + statusCode = Utilities.GetSuccessCode(response); + } + catch + { + throw new SteamStatusCodeException(Undefined, response); + } + + return Translate(statusCode, out isUniversal); + } + + + + /// + /// Translate to specific or + /// + /// + /// + /// + /// if value is universal. if value is specific + /// + public static SteamStatusCode Translate(int statusCode, out bool isUniversal) where T : SteamStatusCode + { + if (statusCode < 2) + { + var status = StatusCodes[statusCode]; + isUniversal = true; + return status; + } + + var allSpecific = GetAll(); + var translated = allSpecific.FirstOrDefault(s => s.Id == statusCode); + + isUniversal = translated == null; + + if (translated != null) + { + return translated; + } + if (translated == null && StatusCodes.TryGetValue(statusCode, out var universal)) + { + return universal; + } + + return translated ?? new SteamStatusCode(statusCode, nameof(Unknown)); + } + + + public override bool Equals(object? obj) + { + if (obj is not SteamStatusCode translated) + return false; + + if (this.Id < 2) + { + return translated.Id == this.Id; + } + else + { + return base.Equals(translated); + } + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + + /// + /// + /// + /// + /// + public static void ValidateSuccessOrThrow(string response) where T : SteamStatusCode + { + var translated = Translate(response, out _); + if (translated.Id != 1) + throw new SteamStatusCodeException(translated, response); + } + + /// + /// Same as with generic parameter
+ /// (Used in case when steam status code is not defined for this operation) + ///
+ /// + /// + public static void ValidateSuccessOrThrow(string response) + { + ValidateSuccessOrThrow(response); + } + + /// + /// + /// + /// + /// + public static void ValidateSuccessOrThrow(int statusCode) where T : SteamStatusCode + { + if (statusCode == 1) return; + var translated = Translate(statusCode, out _); + throw new SteamStatusCodeException(translated, statusCode.ToString()); + } + + + /// + /// Same as with generic parameter
+ /// (Used in case when steam status code is not defined for this operation) + ///
+ /// + /// + public static void ValidateSuccessOrThrow(int statusCode) + { + ValidateSuccessOrThrow(statusCode); + } + + /// + /// Validates that status code is or specific + /// + /// + /// + /// + /// + public static SteamStatusCode TranslateOrThrow(string response, out bool isUniversal) where T : SteamStatusCode + { + var translated = Translate(response, out isUniversal); + if (translated.Id < 1) + throw new SteamStatusCodeException(translated, response); + return translated; + } + + public override string ToString() + { + return $"{Name} ({Id})"; + } +} \ No newline at end of file diff --git a/SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs b/SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs new file mode 100644 index 0000000..6f6b85b --- /dev/null +++ b/SteamLibForked/Core/StatusCodes/SteamStatusCode_Codes.cs @@ -0,0 +1,270 @@ +// ReSharper disable InconsistentNaming +namespace SteamLib.Core.StatusCodes; + +public partial class SteamStatusCode //Codes +{ + /// + /// Status code has no translation yet + /// + public static readonly SteamStatusCode Unknown = new(-1, nameof(Unknown)); + + /// + /// Response ended without status code even though it should have been there + /// + public static readonly SteamStatusCode Undefined = new(0, nameof(Undefined)); + + ///1 + public static readonly SteamStatusCode Ok = new(1, nameof(Ok)); + ///2 + public static readonly SteamStatusCode Fail = new(2, nameof(Fail)); + ///3 + public static readonly SteamStatusCode NoConnection = new(3, nameof(NoConnection)); + ///4 + public static readonly SteamStatusCode NoConnectionRetry = new(4, nameof(NoConnectionRetry)); + ///5 + public static readonly SteamStatusCode InvalidPassword = new(5, nameof(InvalidPassword)); + ///6 + public static readonly SteamStatusCode LoggedInElsewhere = new(6, nameof(LoggedInElsewhere)); + ///7 + public static readonly SteamStatusCode InvalidProtocolVer = new(7, nameof(InvalidProtocolVer)); + ///8 + public static readonly SteamStatusCode InvalidParam = new(8, nameof(InvalidParam)); + ///9 + public static readonly SteamStatusCode FileNotFound = new(9, nameof(FileNotFound)); + ///10 + public static readonly SteamStatusCode Busy = new(10, nameof(Busy)); + ///11 + public static readonly SteamStatusCode InvalidState = new(11, nameof(InvalidState)); + ///12 + public static readonly SteamStatusCode InvalidName = new(12, nameof(InvalidName)); + ///13 + public static readonly SteamStatusCode InvalidEmail = new(13, nameof(InvalidEmail)); + ///14 + public static readonly SteamStatusCode DuplicateName = new(14, nameof(DuplicateName)); + ///15 + public static readonly SteamStatusCode AccessDenied = new(15, nameof(AccessDenied)); + ///16 + public static readonly SteamStatusCode Timeout = new(16, nameof(Timeout)); + ///17 + public static readonly SteamStatusCode Banned = new(17, nameof(Banned)); + ///18 + public static readonly SteamStatusCode AccountNotFound = new(18, nameof(AccountNotFound)); + ///19 + public static readonly SteamStatusCode InvalidSteamID = new(19, nameof(InvalidSteamID)); + ///20 + public static readonly SteamStatusCode ServiceUnavailable = new(20, nameof(ServiceUnavailable)); + ///21 + public static readonly SteamStatusCode NotLoggedOn = new(21, nameof(NotLoggedOn)); + ///22 + public static readonly SteamStatusCode Pending = new(22, nameof(Pending)); + ///23 + public static readonly SteamStatusCode EncryptionFailure = new(23, nameof(EncryptionFailure)); + ///24 + public static readonly SteamStatusCode InsufficientPrivilege = new(24, nameof(InsufficientPrivilege)); + ///25 + public static readonly SteamStatusCode LimitExceeded = new(25, nameof(LimitExceeded)); + ///26 + public static readonly SteamStatusCode Revoked = new(26, nameof(Revoked)); + ///27 + public static readonly SteamStatusCode Expired = new(27, nameof(Expired)); + ///28 + public static readonly SteamStatusCode AlreadyRedeemed = new(28, nameof(AlreadyRedeemed)); + ///29 + public static readonly SteamStatusCode DuplicateRequest = new(29, nameof(DuplicateRequest)); + ///30 + public static readonly SteamStatusCode AlreadyOwned = new(30, nameof(AlreadyOwned)); + ///31 + public static readonly SteamStatusCode IPNotFound = new(31, nameof(IPNotFound)); + ///32 + public static readonly SteamStatusCode PersistFailed = new(32, nameof(PersistFailed)); + ///33 + public static readonly SteamStatusCode LockingFailed = new(33, nameof(LockingFailed)); + ///34 + public static readonly SteamStatusCode LogonSessionReplaced = new(34, nameof(LogonSessionReplaced)); + ///35 + public static readonly SteamStatusCode ConnectFailed = new(35, nameof(ConnectFailed)); + ///36 + public static readonly SteamStatusCode HandshakeFailed = new(36, nameof(HandshakeFailed)); + ///37 + public static readonly SteamStatusCode IOFailure = new(37, nameof(IOFailure)); + ///38 + public static readonly SteamStatusCode RemoteDisconnect = new(38, nameof(RemoteDisconnect)); + ///39 + public static readonly SteamStatusCode ShoppingCartNotFound = new(39, nameof(ShoppingCartNotFound)); + ///40 + public static readonly SteamStatusCode Blocked = new(40, nameof(Blocked)); + ///41 + public static readonly SteamStatusCode Ignored = new(41, nameof(Ignored)); + ///42 + public static readonly SteamStatusCode NoMatch = new(42, nameof(NoMatch)); + ///43 + public static readonly SteamStatusCode AccountDisabled = new(43, nameof(AccountDisabled)); + ///44 + public static readonly SteamStatusCode ServiceReadOnly = new(44, nameof(ServiceReadOnly)); + ///45 + public static readonly SteamStatusCode AccountNotFeatured = new(45, nameof(AccountNotFeatured)); + ///46 + public static readonly SteamStatusCode AdministratorOK = new(46, nameof(AdministratorOK)); + ///47 + public static readonly SteamStatusCode ContentVersion = new(47, nameof(ContentVersion)); + ///48 + public static readonly SteamStatusCode TryAnotherCM = new(48, nameof(TryAnotherCM)); + ///49 + public static readonly SteamStatusCode PasswordRequiredToKickSession = new(49, nameof(PasswordRequiredToKickSession)); + ///50 + public static readonly SteamStatusCode AlreadyLoggedInElsewhere = new(50, nameof(AlreadyLoggedInElsewhere)); + ///51 + public static readonly SteamStatusCode Suspended = new(51, nameof(Suspended)); + ///52 + public static readonly SteamStatusCode Cancelled = new(52, nameof(Cancelled)); + ///53 + public static readonly SteamStatusCode DataCorruption = new(53, nameof(DataCorruption)); + ///54 + public static readonly SteamStatusCode DiskFull = new(54, nameof(DiskFull)); + ///55 + public static readonly SteamStatusCode RemoteCallFailed = new(55, nameof(RemoteCallFailed)); + ///56 + public static readonly SteamStatusCode PasswordUnset = new(56, nameof(PasswordUnset)); + ///57 + public static readonly SteamStatusCode ExternalAccountUnlinked = new(57, nameof(ExternalAccountUnlinked)); + ///58 + public static readonly SteamStatusCode PSNTicketInvalid = new(58, nameof(PSNTicketInvalid)); + ///59 + public static readonly SteamStatusCode ExternalAccountAlreadyLinked = new(59, nameof(ExternalAccountAlreadyLinked)); + ///60 + public static readonly SteamStatusCode RemoteFileConflict = new(60, nameof(RemoteFileConflict)); + ///61 + public static readonly SteamStatusCode IllegalPassword = new(61, nameof(IllegalPassword)); + ///62 + public static readonly SteamStatusCode SameAsPreviousValue = new(62, nameof(SameAsPreviousValue)); + ///63 + public static readonly SteamStatusCode AccountLogonDenied = new(63, nameof(AccountLogonDenied)); + ///64 + public static readonly SteamStatusCode CannotUseOldPassword = new(64, nameof(CannotUseOldPassword)); + ///65 + public static readonly SteamStatusCode InvalidLoginAuthCode = new(65, nameof(InvalidLoginAuthCode)); + ///66 + public static readonly SteamStatusCode AccountLogonDeniedNoMail = new(66, nameof(AccountLogonDeniedNoMail)); + ///67 + public static readonly SteamStatusCode HardwareNotCapableOfIPT = new(67, nameof(HardwareNotCapableOfIPT)); + ///68 + public static readonly SteamStatusCode IPTInitError = new(68, nameof(IPTInitError)); + ///69 + public static readonly SteamStatusCode ParentalControlRestricted = new(69, nameof(ParentalControlRestricted)); + ///70 + public static readonly SteamStatusCode FacebookQueryError = new(70, nameof(FacebookQueryError)); + ///71 + public static readonly SteamStatusCode ExpiredLoginAuthCode = new(71, nameof(ExpiredLoginAuthCode)); + ///72 + public static readonly SteamStatusCode IPLoginRestrictionFailed = new(72, nameof(IPLoginRestrictionFailed)); + ///73 + public static readonly SteamStatusCode AccountLockedDown = new(73, nameof(AccountLockedDown)); + ///74 + public static readonly SteamStatusCode AccountLogonDeniedVerifiedEmailRequired = new(74, nameof(AccountLogonDeniedVerifiedEmailRequired)); + ///75 + public static readonly SteamStatusCode NoMatchingURL = new(75, nameof(NoMatchingURL)); + ///76 + public static readonly SteamStatusCode BadResponse = new(76, nameof(BadResponse)); + ///77 + public static readonly SteamStatusCode RequirePasswordReEntry = new(77, nameof(RequirePasswordReEntry)); + ///78 + public static readonly SteamStatusCode ValueOutOfRange = new(78, nameof(ValueOutOfRange)); + ///79 + public static readonly SteamStatusCode UnexpectedError = new(79, nameof(UnexpectedError)); + ///80 + public static readonly SteamStatusCode Disabled = new(80, nameof(Disabled)); + ///81 + public static readonly SteamStatusCode InvalidCEGSubmission = new(81, nameof(InvalidCEGSubmission)); + ///82 + public static readonly SteamStatusCode RestrictedDevice = new(82, nameof(RestrictedDevice)); + ///83 + public static readonly SteamStatusCode RegionLocked = new(83, nameof(RegionLocked)); + ///84 + public static readonly SteamStatusCode RateLimitExceeded = new(84, nameof(RateLimitExceeded)); + ///85 + public static readonly SteamStatusCode AccountLoginDeniedNeedTwoFactor = new(85, nameof(AccountLoginDeniedNeedTwoFactor)); + ///86 + public static readonly SteamStatusCode ItemDeleted = new(86, nameof(ItemDeleted)); + ///87 + public static readonly SteamStatusCode AccountLoginDeniedThrottle = new(87, nameof(AccountLoginDeniedThrottle)); + ///88 + public static readonly SteamStatusCode TwoFactorCodeMismatch = new(88, nameof(TwoFactorCodeMismatch)); + ///89 + public static readonly SteamStatusCode TwoFactorActivationCodeMismatch = new(89, nameof(TwoFactorActivationCodeMismatch)); + ///90 + public static readonly SteamStatusCode AccountAssociatedToMultiplePartners = new(90, nameof(AccountAssociatedToMultiplePartners)); + ///91 + public static readonly SteamStatusCode NotModified = new(91, nameof(NotModified)); + ///92 + public static readonly SteamStatusCode NoMobileDevice = new(92, nameof(NoMobileDevice)); + ///93 + public static readonly SteamStatusCode TimeNotSynced = new(93, nameof(TimeNotSynced)); + ///94 + public static readonly SteamStatusCode SmsCodeFailed = new(94, nameof(SmsCodeFailed)); + ///95 + public static readonly SteamStatusCode AccountLimitExceeded = new(95, nameof(AccountLimitExceeded)); + ///96 + public static readonly SteamStatusCode AccountActivityLimitExceeded = new(96, nameof(AccountActivityLimitExceeded)); + ///97 + public static readonly SteamStatusCode PhoneActivityLimitExceeded = new(97, nameof(PhoneActivityLimitExceeded)); + ///98 + public static readonly SteamStatusCode RefundToWallet = new(98, nameof(RefundToWallet)); + ///99 + public static readonly SteamStatusCode EmailSendFailure = new(99, nameof(EmailSendFailure)); + ///100 + public static readonly SteamStatusCode NotSettled = new(100, nameof(NotSettled)); + ///101 + public static readonly SteamStatusCode NeedCaptcha = new(101, nameof(NeedCaptcha)); + ///102 + public static readonly SteamStatusCode GSLTDenied = new(102, nameof(GSLTDenied)); + ///103 + public static readonly SteamStatusCode GSOwnerDenied = new(103, nameof(GSOwnerDenied)); + ///104 + public static readonly SteamStatusCode InvalidItemType = new(104, nameof(InvalidItemType)); + ///105 + public static readonly SteamStatusCode IPBanned = new(105, nameof(IPBanned)); + ///106 + public static readonly SteamStatusCode GSLTExpired = new(106, nameof(GSLTExpired)); + ///107 + public static readonly SteamStatusCode InsufficientFunds = new(107, nameof(InsufficientFunds)); + ///108 + public static readonly SteamStatusCode TooManyPending = new(108, nameof(TooManyPending)); + ///109 + public static readonly SteamStatusCode NoSiteLicensesFound = new(109, nameof(NoSiteLicensesFound)); + ///110 + public static readonly SteamStatusCode WGNetworkSendExceeded = new(110, nameof(WGNetworkSendExceeded)); + ///111 + public static readonly SteamStatusCode AccountNotFriends = new(111, nameof(AccountNotFriends)); + ///112 + public static readonly SteamStatusCode LimitedUserAccount = new(112, nameof(LimitedUserAccount)); + ///113 + public static readonly SteamStatusCode CantRemoveItem = new(113, nameof(CantRemoveItem)); + ///114 + public static readonly SteamStatusCode AccountDeleted = new(114, nameof(AccountDeleted)); + ///115 + public static readonly SteamStatusCode ExistingUserCancelledLicense = new(115, nameof(ExistingUserCancelledLicense)); + ///116 + public static readonly SteamStatusCode CommunityCooldown = new(116, nameof(CommunityCooldown)); + ///117 + public static readonly SteamStatusCode NoLauncherSpecified = new(117, nameof(NoLauncherSpecified)); + ///118 + public static readonly SteamStatusCode MustAgreeToSSA = new(118, nameof(MustAgreeToSSA)); + ///119 + public static readonly SteamStatusCode LauncherMigrated = new(119, nameof(LauncherMigrated)); + ///120 + public static readonly SteamStatusCode SteamRealmMismatch = new(120, nameof(SteamRealmMismatch)); + ///121 + public static readonly SteamStatusCode InvalidSignature = new(121, nameof(InvalidSignature)); + ///122 + public static readonly SteamStatusCode ParseFailure = new(122, nameof(ParseFailure)); + ///123 + public static readonly SteamStatusCode NoVerifiedPhone = new(123, nameof(NoVerifiedPhone)); + ///124 + public static readonly SteamStatusCode InsufficientBattery = new(124, nameof(InsufficientBattery)); + ///125 + public static readonly SteamStatusCode ChargerRequired = new(125, nameof(ChargerRequired)); + ///126 + public static readonly SteamStatusCode CachedCredentialInvalid = new(126, nameof(CachedCredentialInvalid)); + ///127 + public static readonly SteamStatusCode PhoneNumberIsVOIP = new(127, nameof(PhoneNumberIsVOIP)); +} \ No newline at end of file diff --git a/SteamLibForked/Core/SteamDomains.cs b/SteamLibForked/Core/SteamDomains.cs new file mode 100644 index 0000000..7eb8471 --- /dev/null +++ b/SteamLibForked/Core/SteamDomains.cs @@ -0,0 +1,74 @@ +using System.Collections.ObjectModel; +using SteamLib.Core.Enums; +using SteamLib.Exceptions; + +namespace SteamLib.Core; + +public static class SteamDomains +{ + public static readonly Uri LoginSteamDomain = new(SteamConstants.LOGIN_STEAM); + public static IReadOnlyDictionary Domains { get; } = new ReadOnlyDictionary( + new Dictionary + { + {SteamDomain.Community, SteamConstants.STEAM_COMMUNITY}, + {SteamDomain.Store, SteamConstants.STEAM_STORE}, + {SteamDomain.Help, SteamConstants.STEAM_HELP}, + {SteamDomain.TV, SteamConstants.STEAM_TV}, + {SteamDomain.Checkout, SteamConstants.STEAM_CHECKOUT} + }); + + public static IReadOnlyDictionary DomainUris { get; } + = new ReadOnlyDictionary( + Domains.ToDictionary(x => x.Key, x => new Uri(x.Value)) + ); + + + + public static SteamDomain[] AllDomains => new[] + { + SteamDomain.Community, + SteamDomain.Store, + SteamDomain.Help, + SteamDomain.TV, + SteamDomain.Checkout + }; + + public static Uri GetDomainUri(SteamDomain domain) + { + if (domain == SteamDomain.Undefined) + throw new ArgumentException("Invalid domain", nameof(domain)); + return DomainUris[domain]; + } + + public static SteamDomain GetDomain(string domain) + { + if (TryGetDomain(domain, out var result)) + { + return result; + } + else + { + throw UnknownSteamDomainException.Create(domain); + } + } + public static string GetDomain(SteamDomain domain) + { + if (domain == SteamDomain.Undefined) + throw new ArgumentException("Invalid domain", nameof(domain)); + return Domains[domain]; + } + + public static bool TryGetDomain(string domain, out SteamDomain result) + { + var uri = new Uri(domain); + var found = DomainUris.FirstOrDefault(x => x.Value.Host == uri.Host); + if (found.Value == default) + { + result = SteamDomain.Undefined; + return false; + } + + result = found.Key; + return true; + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/CantLoadConfirmationsException.cs b/SteamLibForked/Exceptions/CantLoadConfirmationsException.cs new file mode 100644 index 0000000..3b94a9c --- /dev/null +++ b/SteamLibForked/Exceptions/CantLoadConfirmationsException.cs @@ -0,0 +1,21 @@ +namespace SteamLib.Exceptions; + +[Serializable] +public class CantLoadConfirmationsException : Exception +{ + public LoadConfirmationsError Error { get; init; } + public CantLoadConfirmationsException() { } + public CantLoadConfirmationsException(string message) : base(message) { } + public CantLoadConfirmationsException(string message, Exception inner) : base(message, inner) { } + protected CantLoadConfirmationsException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} + + +public enum LoadConfirmationsError +{ + Unknown, + TryAgainLater, + NotSetupToReceiveConfirmations +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/General/UnsupportedResponseException.cs b/SteamLibForked/Exceptions/General/UnsupportedResponseException.cs new file mode 100644 index 0000000..5b60c82 --- /dev/null +++ b/SteamLibForked/Exceptions/General/UnsupportedResponseException.cs @@ -0,0 +1,21 @@ +namespace SteamLib.Exceptions.General; + +public class UnsupportedResponseException : Exception +{ + public string Response { get; } + + public UnsupportedResponseException(string response) : base("Request failed with unsupported response") + { + Response = response; + } + + public UnsupportedResponseException(string response, Exception? inner) : base("Request failed with unsupported response", inner) + { + Response = response; + } + + public UnsupportedResponseException(string response, string? message, Exception? inner = null) : base(message, inner) + { + Response = response; + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/LoginException.cs b/SteamLibForked/Exceptions/LoginException.cs new file mode 100644 index 0000000..c1fc5ae --- /dev/null +++ b/SteamLibForked/Exceptions/LoginException.cs @@ -0,0 +1,49 @@ +using SteamLib.Core.Interfaces; + +namespace SteamLib.Exceptions; + +public class LoginException : Exception +{ + public LoginError Error { get; } + public string? Response { get; } + public LoginException(LoginError error) + : base($"Login was unsuccessful. Error {error}") + { + Error = error; + } + + public LoginException(string response) + { + Error = LoginError.UndefinedError; + Response = response; + } + + public LoginException(string? response, string? message, LoginError? error, Exception? innerException) : base( + message, innerException) + { + Response = response; + Error = error ?? LoginError.UndefinedError; + } +} + +public enum LoginError +{ + + CaptchaRequired, + InvalidCredentials, + InvalidEmailAuthCode, + InvalidTwoFactorCode, + /// + /// SteamEmail authentication is required to login but no was provided + /// + EmailAuthRequired, + /// + /// SteamGuard is required to login but no was provided + /// + SteamGuardRequired, + /// + /// Some error occurred while trying to login. + /// + UndefinedError + +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs b/SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs new file mode 100644 index 0000000..1c695cf --- /dev/null +++ b/SteamLibForked/Exceptions/Mobile/AuthenticatorLinkerException.cs @@ -0,0 +1,42 @@ +namespace SteamLib.Exceptions.Mobile; + + +public enum AuthenticatorLinkerError +{ + NoEmailProvider, + NoSmsCodeProvider, + PhoneAlreadyAttached, + InvalidPhoneNumber, + CantAttachPhone, + CantConfirmAttachingEmail, + CantSendSms, + AuthenticatorPresent, + BadConfirmationCode, + UnableToGenerateCorrectCodes, + InvalidStateWithStatus2, + GeneralFailure, + +} +public class AuthenticatorLinkerException : Exception +{ + public AuthenticatorLinkerError Error { get; } + public bool OnFinalization { get; init; } + public AuthenticatorLinkerException(AuthenticatorLinkerError error) : base($"Linking failed due to error: {error}") + { + Error = error; + } + + public AuthenticatorLinkerException(int code) + : base($"Linking failed due to error: {AuthenticatorLinkerError.GeneralFailure} ({code})") + { + Error = AuthenticatorLinkerError.GeneralFailure; + } + + public AuthenticatorLinkerException(string message) : base(message) + { + Error = AuthenticatorLinkerError.GeneralFailure; + } + + public AuthenticatorLinkerException(){} + +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs b/SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs new file mode 100644 index 0000000..0683e35 --- /dev/null +++ b/SteamLibForked/Exceptions/Mobile/BadMobileCookiesException.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace SteamLib.Exceptions.Mobile; + +[Serializable] +public class BadMobileCookiesException : Exception +{ + // + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + // + + public BadMobileCookiesException() : base("You are using deafult HttpClient without mobile specific cookies. Login can't be proceeded with these cookies") + { + } + + public BadMobileCookiesException(string message) : base(message) + { + } + + public BadMobileCookiesException(string message, Exception inner) : base(message, inner) + { + } + + protected BadMobileCookiesException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/SessionExpiredException.cs b/SteamLibForked/Exceptions/SessionExpiredException.cs new file mode 100644 index 0000000..5a763c1 --- /dev/null +++ b/SteamLibForked/Exceptions/SessionExpiredException.cs @@ -0,0 +1,11 @@ +namespace SteamLib.Exceptions; +public class SessionExpiredException : SessionInvalidException +{ + public const string SESSION_EXPIRED_MSG = "Session expired and won't longer work. You must login to get new session"; + public SessionExpiredException() { } + public SessionExpiredException(string message) : base(message) { } + public SessionExpiredException(string message, Exception inner) : base(message, inner) { } + protected SessionExpiredException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} diff --git a/SteamLibForked/Exceptions/SessionInvalidException.cs b/SteamLibForked/Exceptions/SessionInvalidException.cs new file mode 100644 index 0000000..3cad890 --- /dev/null +++ b/SteamLibForked/Exceptions/SessionInvalidException.cs @@ -0,0 +1,12 @@ +namespace SteamLib.Exceptions; + +public class SessionInvalidException : Exception +{ + public const string SESSION_NULL_MSG = "Session was null. Before acting SteamClient must be logged in"; + public SessionInvalidException() { } + public SessionInvalidException(string message) : base(message) { } + public SessionInvalidException(string message, Exception? inner) : base(message, inner) { } + protected SessionInvalidException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} diff --git a/SteamLibForked/Exceptions/SteamStatusCodeException.cs b/SteamLibForked/Exceptions/SteamStatusCodeException.cs new file mode 100644 index 0000000..8f60cdf --- /dev/null +++ b/SteamLibForked/Exceptions/SteamStatusCodeException.cs @@ -0,0 +1,23 @@ +using SteamLib.Core.StatusCodes; + +namespace SteamLib.Exceptions; + +public class SteamStatusCodeException : Exception +{ + public SteamStatusCode StatusCode { get; } + public string? Response { get; } + + public SteamStatusCodeException(SteamStatusCode statusCode, string? response, Exception? innerException = null) + : base($"Steam return not successful status code {statusCode}", innerException) + { + StatusCode = statusCode; + Response = response; + } + + public SteamStatusCodeException(string message, SteamStatusCode statusCode, string? response = null, Exception? innerException = null) + : base(message, innerException) + { + StatusCode = statusCode; + Response = response; + } +} \ No newline at end of file diff --git a/SteamLibForked/Exceptions/UnknownSteamDomainException.cs b/SteamLibForked/Exceptions/UnknownSteamDomainException.cs new file mode 100644 index 0000000..b76d71f --- /dev/null +++ b/SteamLibForked/Exceptions/UnknownSteamDomainException.cs @@ -0,0 +1,25 @@ +namespace SteamLib.Exceptions; + +public class UnknownSteamDomainException : Exception +{ + public string? Domain { get; } + public UnknownSteamDomainException(string? domain) + { + Domain = domain; + } + + public UnknownSteamDomainException(string? domain, string? message) : base(message) + { + Domain = domain; + } + + public UnknownSteamDomainException(string? domain, string? message, Exception? inner) : base(message, inner) + { + Domain = domain; + } + + public static UnknownSteamDomainException Create(string? domain, Exception? inner = null) + { + return new(domain, $"Unknown Steam domain: {domain}", inner); + } +} \ No newline at end of file diff --git a/SteamLibForked/MobileSteamClient/MobileData.cs b/SteamLibForked/MobileSteamClient/MobileData.cs new file mode 100644 index 0000000..e0e6789 --- /dev/null +++ b/SteamLibForked/MobileSteamClient/MobileData.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using SteamLib.Account; + +namespace SteamLib; + +//WARNING: Any changes here should be reflected in MafileSerializer.cs + +public class MobileData +{ + [JsonRequired] public string SharedSecret { get; set; } = null!; + [JsonRequired] public string IdentitySecret { get; set; } = null!; + [JsonRequired] public string DeviceId { get; set; } = null!; + +} + +public class MobileDataExtended : MobileData +{ + public string? RevocationCode { get; set; } = null!; + public string AccountName { get; set; } = null!; + public MobileSessionData? SessionData { get; set; } + + + #region Unused + public long ServerTime { get; set; } //Unused + public ulong SerialNumber { get; set; } //Unused //greater than long must be ulong or string + public string Uri { get; set; } = null!;//Unused + public string TokenGid { get; set; } = null!;//Unused + public string Secret1 { get; set; } = null!; //Unused + + #endregion +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs b/SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs new file mode 100644 index 0000000..1bd2133 --- /dev/null +++ b/SteamLibForked/ProtoCore/Enums/EAuthSessionGuardType.cs @@ -0,0 +1,16 @@ +using ProtoBuf; + +namespace SteamLib.ProtoCore.Enums; + +[ProtoContract] +public enum EAuthSessionGuardType +{ + Unknown = 0, + None = 1, + EmailCode = 2, + DeviceCode = 3, + DeviceConfirmation = 4, + EmailConfirmation = 5, + MachineToken = 6, + LegacyMachineAuth = 7, +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs b/SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs new file mode 100644 index 0000000..b61c0f9 --- /dev/null +++ b/SteamLibForked/ProtoCore/Enums/EAuthTokenPlatformType.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace SteamLib.ProtoCore.Enums; + +[ProtoContract] +public enum EAuthTokenPlatformType +{ + Unknown = 0, + SteamClient = 1, + WebBrowser = 2, + MobileApp = 3, +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Enums/EResult.cs b/SteamLibForked/ProtoCore/Enums/EResult.cs new file mode 100644 index 0000000..30d78a2 --- /dev/null +++ b/SteamLibForked/ProtoCore/Enums/EResult.cs @@ -0,0 +1,132 @@ +namespace SteamLib.ProtoCore.Enums; + +public enum EResult +{ + Invalid = 0, + OK = 1, + Fail = 2, + NoConnection = 3, + InvalidPassword = 5, + LoggedInElsewhere = 6, + InvalidProtocolVer = 7, + InvalidParam = 8, + FileNotFound = 9, + Busy = 10, + InvalidState = 11, + InvalidName = 12, + InvalidEmail = 13, + DuplicateName = 14, + AccessDenied = 15, + Timeout = 16, + Banned = 17, + AccountNotFound = 18, + InvalidSteamID = 19, + ServiceUnavailable = 20, + NotLoggedOn = 21, + Pending = 22, + EncryptionFailure = 23, + InsufficientPrivilege = 24, + LimitExceeded = 25, + Revoked = 26, + Expired = 27, + AlreadyRedeemed = 28, + DuplicateRequest = 29, + AlreadyOwned = 30, + IPNotFound = 31, + PersistFailed = 32, + LockingFailed = 33, + LogonSessionReplaced = 34, + ConnectFailed = 35, + HandshakeFailed = 36, + IOFailure = 37, + RemoteDisconnect = 38, + ShoppingCartNotFound = 39, + Blocked = 40, + Ignored = 41, + NoMatch = 42, + AccountDisabled = 43, + ServiceReadOnly = 44, + AccountNotFeatured = 45, + AdministratorOK = 46, + ContentVersion = 47, + TryAnotherCM = 48, + PasswordRequiredToKickSession = 49, + AlreadyLoggedInElsewhere = 50, + Suspended = 51, + Cancelled = 52, + DataCorruption = 53, + DiskFull = 54, + RemoteCallFailed = 55, + PasswordUnset = 56, + ExternalAccountUnlinked = 57, + PSNTicketInvalid = 58, + ExternalAccountAlreadyLinked = 59, + RemoteFileConflict = 60, + IllegalPassword = 61, + SameAsPreviousValue = 62, + AccountLogonDenied = 63, + CannotUseOldPassword = 64, + InvalidLoginAuthCode = 65, + AccountLogonDeniedNoMail = 66, + HardwareNotCapableOfIPT = 67, + IPTInitError = 68, + ParentalControlRestricted = 69, + FacebookQueryError = 70, + ExpiredLoginAuthCode = 71, + IPLoginRestrictionFailed = 72, + AccountLockedDown = 73, + AccountLogonDeniedVerifiedEmailRequired = 74, + NoMatchingURL = 75, + BadResponse = 76, + RequirePasswordReEntry = 77, + ValueOutOfRange = 78, + UnexpectedError = 79, + Disabled = 80, + InvalidCEGSubmission = 81, + RestrictedDevice = 82, + RegionLocked = 83, + RateLimitExceeded = 84, + AccountLoginDeniedNeedTwoFactor = 85, + ItemDeleted = 86, + AccountLoginDeniedThrottle = 87, + TwoFactorCodeMismatch = 88, + TwoFactorActivationCodeMismatch = 89, + AccountAssociatedToMultiplePartners = 90, + NotModified = 91, + NoMobileDevice = 92, + TimeNotSynced = 93, + SMSCodeFailed = 94, + AccountLimitExceeded = 95, + AccountActivityLimitExceeded = 96, + PhoneActivityLimitExceeded = 97, + RefundToWallet = 98, + EmailSendFailure = 99, + NotSettled = 100, + NeedCaptcha = 101, + GSLTDenied = 102, + GSOwnerDenied = 103, + InvalidItemType = 104, + IPBanned = 105, + GSLTExpired = 106, + InsufficientFunds = 107, + TooManyPending = 108, + NoSiteLicensesFound = 109, + WGNetworkSendExceeded = 110, + AccountNotFriends = 111, + LimitedUserAccount = 112, + CantRemoveItem = 113, + AccountDeleted = 114, + ExistingUserCancelledLicense = 115, + CommunityCooldown = 116, + NoLauncherSpecified = 117, + MustAgreeToSSA = 118, + LauncherMigrated = 119, + SteamRealmMismatch = 120, + InvalidSignature = 121, + ParseFailure = 122, + NoVerifiedPhone = 123, + InsufficientBattery = 124, + ChargerRequired = 125, + CachedCredentialInvalid = 126, + PhoneNumberIsVOIP = 127, +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Exceptions/EResutException.cs b/SteamLibForked/ProtoCore/Exceptions/EResutException.cs new file mode 100644 index 0000000..0e2276e --- /dev/null +++ b/SteamLibForked/ProtoCore/Exceptions/EResutException.cs @@ -0,0 +1,28 @@ +using SteamLib.ProtoCore.Enums; + +namespace SteamLib.ProtoCore.Exceptions; + +[Serializable] +public class EResultException : Exception +{ + public EResult Result { get; } + public EResultException() { } + public EResultException(EResult result) : base("EResult error: " + result) + { + Result = result; + } + + public EResultException(string message, EResult result, Exception inner) : base(message, inner) + { + Result = result; + } + + public EResultException(EResult result, Exception inner) : base("EResult error: " + result, inner) + { + Result = result; + } + + protected EResultException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } +} diff --git a/SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs b/SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs new file mode 100644 index 0000000..a340a89 --- /dev/null +++ b/SteamLibForked/ProtoCore/Exceptions/UnknownEResultException.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace SteamLib.ProtoCore.Exceptions; + +[Serializable] +public class UnknownEResultException : Exception +{ + public int EResult { get; } + public UnknownEResultException() + {} + + public UnknownEResultException(int eResult) : base("Got unknown EResult: " + eResult) + { + EResult = eResult; + } + + protected UnknownEResultException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs b/SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs new file mode 100644 index 0000000..ce1a6ce --- /dev/null +++ b/SteamLibForked/ProtoCore/Interfaces/IProtoMsg.cs @@ -0,0 +1,6 @@ +namespace SteamLib.ProtoCore.Interfaces; + +public interface IProtoMsg +{ + +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/ProtoHelpers.cs b/SteamLibForked/ProtoCore/ProtoHelpers.cs new file mode 100644 index 0000000..fcb7b74 --- /dev/null +++ b/SteamLibForked/ProtoCore/ProtoHelpers.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using ProtoBuf; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore; + +public static class ProtoHelpers +{ + public static byte[] ProtoToBytes(IProtoMsg obj) + { + using var stream = new MemoryStream(); + Serializer.Serialize(stream, obj); + return stream.ToArray(); + } + + [return: NotNullIfNotNull("obj")] + public static string? ProtoToString(IProtoMsg? obj) + { + return obj == null ? null : Convert.ToBase64String(ProtoToBytes(obj)); + } + + public static EResult GetEResult(HttpResponseMessage response) + { + if (response.Headers.TryGetValues("x-eresult", out var val)) + { + var eResultInt = Convert.ToInt32(val.Single()); + + if (Enum.IsDefined(typeof(EResult), eResultInt) == false) + throw new UnknownEResultException(eResultInt); + + return (EResult)eResultInt; + } + + return EResult.Invalid; + } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs b/SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs new file mode 100644 index 0000000..83b0e8e --- /dev/null +++ b/SteamLibForked/ProtoCore/ProtoHttpClientExtension.cs @@ -0,0 +1,229 @@ +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Exceptions; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore; + +public static class ProtoHttpClientExtension +{ + #region Post + + #region ProtoResp + public static Task> PostProtoMsg(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, uri, HttpMethod.Post, str, cancellationToken); + } + public static Task> PostProtoMsg(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + } + + #endregion + + #region ProtoConverted + public static async Task PostProto(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, uri, HttpMethod.Post, str, cancellationToken); + return res.GetResponseEnsureSuccess(); + } + public static async Task PostProto(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + return res.GetResponseEnsureSuccess(); + } + + #endregion + + #region EResult + + public static Task PostProto(this HttpClient client, Uri uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, uri, HttpMethod.Post, str, cancellationToken); + } + + public static Task PostProto(this HttpClient client, string uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + } + + + public static async Task PostProtoEnsureSuccess(this HttpClient client, Uri uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, uri, HttpMethod.Post, str, cancellationToken); + res.EnsureSuccessEResult(); + } + + public static async Task PostProtoEnsureSuccess(this HttpClient client, string uri, IProtoMsg request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, new Uri(uri), HttpMethod.Post, str, cancellationToken); + res.EnsureSuccessEResult(); + } + + #endregion + + #endregion + + #region Get + + #region ProtoResp + public static Task> GetProtoMsg(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, uri, HttpMethod.Get, str, cancellationToken); + } + public static Task> GetProtoMsg(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithResponse(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + } + #endregion + + #region ProtoConverted + public static async Task GetProto(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, uri, HttpMethod.Get, str, cancellationToken); + res.Result.EnsureSuccessEResult(); + + return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null"); + } + public static async Task GetProto(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithResponse(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + res.Result.EnsureSuccessEResult(); + return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null"); + } + + #endregion + + #region EResult + public static Task GetProto(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, uri, HttpMethod.Get, str, cancellationToken); + } + + public static Task GetProto(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + return SendProtoWithEResult(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + } + public static async Task GetProtoEnsureSuccess(this HttpClient client, Uri uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, uri, HttpMethod.Get, str, cancellationToken); + res.EnsureSuccessEResult(); + } + + public static async Task GetProtoEnsureSuccess(this HttpClient client, string uri, IProtoMsg? request, + CancellationToken cancellationToken = default) + { + var str = ProtoHelpers.ProtoToString(request); + var res = await SendProtoWithEResult(client, new Uri(uri), HttpMethod.Get, str, cancellationToken); + res.EnsureSuccessEResult(); + } + #endregion + + #endregion + + + private static async Task> SendProtoWithResponse(this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken) where TProtoResponse : class, IProtoMsg + { + var resp = await SendProto(client, uri, method, protoMsg, cancellationToken); + return await ProtoResponse.FromHttpResponseAsync(resp, cancellationToken); + } + + private static async Task SendProtoWithEResult(HttpClient client, Uri uri, HttpMethod method, string? protoMsg, + CancellationToken cancellationToken) + { + var resp = await SendProto(client, uri, method, protoMsg, cancellationToken); + resp.EnsureSuccessStatusCode(); + return ProtoHelpers.GetEResult(resp); + } + + + public static Task SendProto(this HttpClient client, Uri uri, HttpMethod method, IProtoMsg? protoMsg, CancellationToken cancellationToken = default) + { + return client.SendProto(uri, method, ProtoHelpers.ProtoToString(protoMsg), cancellationToken); + } + + public static Task SendProto(this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken = default) + { + HttpContent? content = null; + + protoMsg ??= string.Empty; + + if (method.Equals(HttpMethod.Get)) + { + var uriStr = uri.ToString(); + var uriWithQuery = uriStr.Contains('?') + ? uriStr + "&input_protobuf_encoded=" + protoMsg + : uriStr + "?input_protobuf_encoded=" + protoMsg; + uri = new Uri(uriWithQuery); + } + else + { + var cont = new KeyValuePair[] + { + new("input_protobuf_encoded", protoMsg) + }; + content = new FormUrlEncodedContent(cont); + } + + var req = new HttpRequestMessage(method, uri) + { + Content = content + }; + + return client.SendAsync(req, cancellationToken); + } + public static void EnsureSuccessEResult(this EResult eResult) + { + if (eResult != EResult.OK) + { + throw new EResultException(eResult); + } + } + + /// + /// + /// + /// + /// + /// + /// + /// + public static T GetResponseEnsureSuccess(this ProtoResponse response) where T : class, IProtoMsg + { + if (response.Result != EResult.OK) + { + throw new EResultException(response.Result); + } + + return response.GetResponse(); + } +} diff --git a/SteamLibForked/ProtoCore/ProtoResponse.cs b/SteamLibForked/ProtoCore/ProtoResponse.cs new file mode 100644 index 0000000..f0aa86e --- /dev/null +++ b/SteamLibForked/ProtoCore/ProtoResponse.cs @@ -0,0 +1,102 @@ +using ProtoBuf; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore; + +public class ProtoResponse +{ + public EResult Result { get; } + public IProtoMsg? ProtoObject { get; protected set; } + + + protected ProtoResponse(EResult result, IProtoMsg? protoObject) + { + Result = result; + ProtoObject = protoObject; + } + + +} + +public class ProtoResponse : ProtoResponse + where TProtoResponse : IProtoMsg +{ + public TProtoResponse? ResponseMsg + { + get => ProtoObject == null ? default : (TProtoResponse)ProtoObject; + set => ProtoObject = value; + } + + public ProtoResponse(EResult result, TProtoResponse? protoObject) : base(result, protoObject) + { } + + public static ProtoResponse FromHttpResponse(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + EResult eResult = EResult.Invalid; + if (response.Headers.TryGetValues("x-eresult", out var val)) + { + var eResultInt = Convert.ToInt32(val.Single()); + eResult = (EResult)eResultInt; + } + + TProtoResponse? msg = default(TProtoResponse); + Stream? stream = null; + try + { + stream = response.Content.ReadAsStream(); + msg = Serializer.Deserialize(stream); + } + catch + { + //Ignored + } + finally + { + stream?.Dispose(); + } + return new ProtoResponse(eResult, msg); + } + + public static async Task> FromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + { + response.EnsureSuccessStatusCode(); + EResult eResult = EResult.Invalid; + if (response.Headers.TryGetValues("x-eresult", out var val)) + { + var eResultInt = Convert.ToInt32(val.Single()); + eResult = (EResult)eResultInt; + } + + TProtoResponse? msg = default(TProtoResponse); + Stream? stream = null; + try + { + stream = await response.Content.ReadAsStreamAsync(cancellationToken); + msg = Serializer.Deserialize(stream); + } + catch + { + //Ignored + } + finally { if (stream != null) await stream.DisposeAsync(); } + + + + + return new ProtoResponse(eResult, msg); + } + + /// + /// Ensures that is not and returns it otherwise throws . + /// + /// + /// + public TProtoResponse GetResponse() + { + if (ResponseMsg == null) + throw new NullReferenceException("ResponseMsg in response was null"); + return ResponseMsg; + } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Services/AuthenticationService.cs b/SteamLibForked/ProtoCore/Services/AuthenticationService.cs new file mode 100644 index 0000000..ca5cc35 --- /dev/null +++ b/SteamLibForked/ProtoCore/Services/AuthenticationService.cs @@ -0,0 +1,228 @@ +using System.Security.Cryptography; +using ProtoBuf; +using SteamLib.ProtoCore.Enums; +using SteamLib.ProtoCore.Interfaces; +using SteamLib.Utility; +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +#pragma warning disable CS8618 + +namespace SteamLib.ProtoCore.Services; + +[ProtoContract] +public class GetPasswordRSAPublicKey_Request : IProtoMsg +{ + [ProtoMember(1)] public string AccountName { get; set; } +} + +[ProtoContract] +public class GetPasswordRSAPublicKey_Response : IProtoMsg +{ + [ProtoMember(1)] public string PublickKeyMod { get; set; } + [ProtoMember(2)] public string PublickKeyExp { get; set; } + [ProtoMember(3)] public ulong Timestamp { get; set; } +} + + +[ProtoContract] +public class BeginAuthSessionViaCredentials_Request : IProtoMsg +{ + [ProtoMember(1)] public string DeviceFriendlyName { get; set; } + [ProtoMember(2)] public string AccountName { get; set; } + [ProtoMember(3)] public string EncryptedPassword { get; set; } + [ProtoMember(4)] public ulong EncryptionTimestamp { get; set; } + [ProtoMember(5)] public bool RememberLogin { get; set; } + [ProtoMember(6)] public EAuthTokenPlatformType PlatformType { get; set; } + [ProtoMember(7)] public int Persistence { get; set; } + + /// + /// Gets or sets the website id that the login will be performed for. + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". + /// + /// The website id. + [ProtoMember(8)] public string WebsiteId { get; set; } + [ProtoMember(9)] public DeviceDetails DeviceDetails { get; set; } = new(); + [ProtoMember(10)] public string GuardData { get; set; } + [ProtoMember(11)] public uint Language { get; set; } + +} + +[ProtoContract] +public class DeviceDetails : IProtoMsg +{ + /// + /// User-Agent if platform is SteamCommunity + /// + [ProtoMember(1)] public string DeviceFriendlyName { get; } + [ProtoMember(2)] public EAuthTokenPlatformType PlatformType { get; } + [ProtoMember(3)] public int? OsType { get; } + [ProtoMember(4)] public uint? GamingDeviceType { get; } + + #region Members + public DeviceDetails() + { + } + + public DeviceDetails(string deviceFriendlyName, EAuthTokenPlatformType platformType, int? osType, uint? gamingDeviceType) + { + DeviceFriendlyName = deviceFriendlyName; + PlatformType = platformType; + OsType = osType; + GamingDeviceType = gamingDeviceType; + } + + public DeviceDetails(string deviceFriendlyName, EAuthTokenPlatformType platformType, EOSType? osType, uint? gamingDeviceType) + { + DeviceFriendlyName = deviceFriendlyName; + PlatformType = platformType; + OsType = (int?)osType; + GamingDeviceType = gamingDeviceType; + } + + public static DeviceDetails CreateDefault() + { + return new DeviceDetails("", EAuthTokenPlatformType.WebBrowser, (int?)null, null); + } + + public static DeviceDetails CreateCommunityDetails(string userAgent) + { + return new DeviceDetails(userAgent, EAuthTokenPlatformType.WebBrowser, (int?)null, null); + } + + public static DeviceDetails CreateMobileDetails(string deviceName) + { + return new DeviceDetails(deviceName, EAuthTokenPlatformType.MobileApp, EOSType.AndroidUnknown, 528); + } + + + #endregion +} + +[ProtoContract] +public class BeginAuthSessionViaCredentials_Response : IProtoMsg +{ + [ProtoMember(1)] public ulong ClientId { get; set; } + [ProtoMember(2)] public byte[] RequestId { get; set; } + [ProtoMember(3)] public float Interval { get; set; } + [ProtoMember(4)] public List AllowedConfirmations { get; } = new(); + [ProtoMember(5)] public ulong Steamid { get; set; } + [ProtoMember(6)] public string WeakToken { get; set; } + [ProtoMember(7)] public string AgreementSessionUrl { get; set; } + [ProtoMember(8)] public string ExtendedErrorMessage { get; set; } +} + +[ProtoContract] +public class AllowedConfirmationMsg : IProtoMsg +{ + [ProtoMember(1)] public EAuthSessionGuardType ConfirmationType { get; set; } + [ProtoMember(2)] public string AssociatedMessage { get; set; } +} + +[ProtoContract] +public class PollAuthSessionStatus_Request : IProtoMsg +{ + [ProtoMember(1)] public ulong ClientId { get; set; } + [ProtoMember(2)] public byte[] RequestId { get; set; } + [ProtoMember(3)] public ulong TokenToRevoke { get; set; } +} + +[ProtoContract] +public class PollAuthSessionStatus_Response : IProtoMsg +{ + [ProtoMember(1)] public ulong NewClientId { get; set; } + [ProtoMember(2)] public string NewChallengeUrl { get; set; } + [ProtoMember(3)] public string RefreshToken { get; set; } + [ProtoMember(4)] public string AccessToken { get; set; } + [ProtoMember(5)] public bool HadRemoteInteraction { get; set; } + [ProtoMember(6)] public string AccountName { get; set; } + [ProtoMember(7)] public string NewGuardData { get; set; } + [ProtoMember(8)] public string AgreementSessionUrl { get; set; } + +} + + + +[ProtoContract] +public class UpdateAuthSessionWithSteamGuardCode_Request : IProtoMsg +{ + [ProtoMember(1)] public ulong ClientId { get; set; } + [ProtoMember(2, DataFormat = DataFormat.FixedSize)] public ulong Steamid { get; set; } + [ProtoMember(3)] public string Code { get; set; } + [ProtoMember(4)] public EAuthSessionGuardType CodeType { get; set; } + +} + +[ProtoContract] +public class GetAuthSessionsForAccount_Response : IProtoMsg +{ + [ProtoMember(1)] public List ClientIds { get; set; } = new(); +} + +[ProtoContract] +public class GetAuthSessionInfo_Request : IProtoMsg +{ + [ProtoMember(1)] + public ulong ClientId { get; set; } +} + +[ProtoContract] +public class GetAuthSessionInfo_Response : IProtoMsg +{ + [ProtoMember(1)] public string IP { get; set; } + [ProtoMember(2)] public string GeoLoc { get; set; } + [ProtoMember(3)] public string City { get; set; } + [ProtoMember(4)] public string State { get; set; } + [ProtoMember(5)] public string Country { get; set; } + [ProtoMember(6)] public EAuthTokenPlatformType PlatformType { get; set; } + [ProtoMember(7)] public string DeviceFriendlyName { get; set; } + [ProtoMember(8)] public int Version { get; set; } + [ProtoMember(9)] public bool RequestorLocationMismatch { get; set; } + [ProtoMember(10)] public bool LoginHistory { get; set; } + [ProtoMember(11)] public bool HighUsageLogin { get; set; } + [ProtoMember(12)] public int RequestedPersistence { get; set; } +} + +[ProtoContract] +public class UpdateAuthSessionWithMobileConfirmation_Request : IProtoMsg +{ + [ProtoMember(1)] public int Version { get; set; } + [ProtoMember(2)] public ulong ClientId { get; set; } + [ProtoMember(3, DataFormat = DataFormat.FixedSize)] public ulong Steamid { get; set; } + [ProtoMember(4)] public byte[] Signature { get; set; } + [ProtoMember(5)] public bool Confirm { get; set; } + [ProtoMember(6)] public int Persistence { get; set; } + + + public void ComputeSignature(string privateKey) + { + byte[] signatureData = new byte[2 + 8 + 8]; + BitConverter.GetBytes(Version).CopyTo(signatureData, 0); + BitConverter.GetBytes(ClientId).CopyTo(signatureData, 2); + BitConverter.GetBytes(Steamid).CopyTo(signatureData, 10); + + using (var hmac = new HMACSHA256(Convert.FromBase64String(privateKey))) + { + Signature= hmac.ComputeHash(signatureData); + } + + } + +} + +[ProtoContract] +public class GenerateAccessTokenForApp_Request : IProtoMsg +{ + [ProtoMember(1)] + public string RefreshToken { get; set; } + [ProtoMember(2, DataFormat = DataFormat.FixedSize)] + public long SteamId { get; set; } + + [ProtoMember(3)] public bool TokenRenewalType { get; set; } = true; //enum: ETokenRenewalType +} + +[ProtoContract] +public class GenerateAccessTokenForApp_Response : IProtoMsg +{ + [ProtoMember(1)] + public string AccessToken { get; set; } +} diff --git a/SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs b/SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs new file mode 100644 index 0000000..d26121d --- /dev/null +++ b/SteamLibForked/ProtoCore/Services/AuthenticatorLinkerService.cs @@ -0,0 +1,138 @@ +using ProtoBuf; +using SteamLib.Account; +using SteamLib.ProtoCore.Interfaces; +// ReSharper disable InconsistentNaming + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +#pragma warning disable CS8618 + +namespace SteamLib.ProtoCore.Services; + +[ProtoContract] +public class AccountPhoneStatus_Response : IProtoMsg +{ + [ProtoMember(1)] public bool HasPhone { get; set; } +} + +[ProtoContract] +public class AddAuthenticator_Request : IProtoMsg +{ + [ProtoMember(1, DataFormat = DataFormat.FixedSize)] public ulong SteamId { get; set; } + [ProtoMember(4)] public int AuthenticatorType { get; set; } + [ProtoMember(5)] public string DeviceIdentifier { get; set; } + [ProtoMember(6)] public string SmsPhoneId { get; set; } + [ProtoMember(8)] public int Version { get; set; } +} + +[ProtoContract] +public class AddAuthenticator_Response : IProtoMsg +{ + [ProtoMember(1)] public byte[] SharedSecret { get; set; } + [ProtoMember(2)] public ulong SerialNumber { get; set; } + [ProtoMember(3)] public string RevocationCode { get; set; } + [ProtoMember(4)] public string Uri { get; set; } + [ProtoMember(5)] public long ServerTime { get; set; } + [ProtoMember(6)] public string AccountName { get; set; } + [ProtoMember(7)] public string TokenGid { get; set; } + [ProtoMember(8)] public byte[] IdentitySecret { get; set; } + [ProtoMember(9)] public byte[] Secret1 { get; set; } + [ProtoMember(10)] public int Status { get; set; } + [ProtoMember(11)] public string PhoneNumberHint { get; set; } + + /// + /// 2 - PhoneNumber + /// 3 - EmailCode + /// + [ProtoMember(12)] public int ConfirmType { get; set; } + + + public MobileDataExtended ToMobileDataExtended(string deviceId, MobileSessionData? sessionData) + { + return new MobileDataExtended + { + SharedSecret = Convert.ToBase64String(SharedSecret), + SerialNumber = SerialNumber, + RevocationCode = RevocationCode, + Uri = Uri, + ServerTime = ServerTime, + AccountName = AccountName, + TokenGid = TokenGid, + IdentitySecret = Convert.ToBase64String(IdentitySecret), + Secret1 = Convert.ToBase64String(Secret1), + SessionData = sessionData, + DeviceId = deviceId + }; + } +} + +[ProtoContract] +public class SetAccountPhoneNumber_Request : IProtoMsg +{ + [ProtoMember(1)] public string PhoneNumber { get; set; } + [ProtoMember(2)] public string CountryCode { get; set; } + +} + + +[ProtoContract] +public class SetAccountPhoneNumber_Response : IProtoMsg +{ + [ProtoMember(1)] public string EmailHint { get; set; } + [ProtoMember(2)] public string PhoneNumber { get; set; } + +} + + +[ProtoContract] +public class IsAccountWaitingForEmailConfirmation_Response : IProtoMsg +{ + [ProtoMember(1)] public bool IsWaiting { get; set; } + [ProtoMember(2)] public int SecondsToWait { get; set; } + +} + +[ProtoContract] +public class SendPhoneVerificationCode_Request : IProtoMsg +{ + [ProtoMember(1, IsRequired = true)] public int Language { get; set; } = 0; +} + + +[ProtoContract] +public class FinalizeAddAuthenticator_Request : IProtoMsg +{ + [ProtoMember(1, DataFormat = DataFormat.FixedSize)] public ulong SteamId { get; set; } + [ProtoMember(2)] public string AuthenticatorCode { get; set; } + [ProtoMember(3)] public ulong AuthenticatorTime { get; set; } + [ProtoMember(4)] public string ConfirmationCode { get; set; } + [ProtoMember(6)] public bool ValidateConfirmationCode { get; set; } +} + + + +[ProtoContract] +public class FinalizeAddAuthenticator_Response : IProtoMsg +{ + [ProtoMember(1)] public bool Success { get; set; } + + [ProtoMember(2)] public bool WantMore { get; set; } + [ProtoMember(3)] public ulong ServerTime { get; set; } + [ProtoMember(4)] public int Status { get; set; } +} + +[ProtoContract] +public class RemoveAuthenticator_Request : IProtoMsg +{ + [ProtoMember(2)] public string RevocationCode { get; set; } + [ProtoMember(5)] public int RevocationReason { get; set; } + [ProtoMember(6)] public int SteamGuardScheme { get; set; } + +} + +[ProtoContract] +public class RemoveAuthenticator_Response : IProtoMsg +{ + [ProtoMember(1)] public bool Success { get; set; } + [ProtoMember(5)] public int RevocationAttemptsRemaining { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/ProtoCore/Services/EmptyMessage.cs b/SteamLibForked/ProtoCore/Services/EmptyMessage.cs new file mode 100644 index 0000000..5dbc384 --- /dev/null +++ b/SteamLibForked/ProtoCore/Services/EmptyMessage.cs @@ -0,0 +1,10 @@ +using ProtoBuf; +using SteamLib.ProtoCore.Interfaces; + +namespace SteamLib.ProtoCore.Services; + +[ProtoContract] +public class EmptyMessage : IProtoMsg +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamLibForked.csproj b/SteamLibForked/SteamLibForked.csproj new file mode 100644 index 0000000..d73ab15 --- /dev/null +++ b/SteamLibForked/SteamLibForked.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs new file mode 100644 index 0000000..919dced --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/CheckPhoneResult.cs @@ -0,0 +1,9 @@ +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +public enum CheckPhoneResult +{ + Valid, + GeneralFailure, + VoIp, + NotValid +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs new file mode 100644 index 0000000..a68b29b --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkOptions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using SteamLib.Core.Interfaces; + +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +public class LinkOptions +{ + public ILoginConsumer LoginConsumer { get; } + public HttpClient HttpClient { get; } + public IEmailProvider? EmailProvider { get; } + public ISmsCodeProvider? SmsCodeProvider { get; } + public ICaptchaResolver? CaptchaResolver { get; } + public IPhoneNumberProvider? PhoneNumberProvider { get; } + public Action? BackupHandler { get; } + public ILogger? Logger { get; } + + public LinkOptions(HttpClient httpClient, ILoginConsumer loginConsumer, ISmsCodeProvider? smsCodeProvider, + ICaptchaResolver? captchaResolver, IEmailProvider? emailProvider, IPhoneNumberProvider? phoneNumberProvider, Action? backupHandler = null, + ILogger? logger = null) + { + EmailProvider = emailProvider; + SmsCodeProvider = smsCodeProvider; + CaptchaResolver = captchaResolver; + HttpClient = httpClient; + LoginConsumer = loginConsumer; + BackupHandler = backupHandler; + PhoneNumberProvider = phoneNumberProvider; + Logger = logger; + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs new file mode 100644 index 0000000..cf1196d --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/Models[folder]/LinkResult.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +public class LinkResult +{ + [MemberNotNullWhen(false, nameof(Error))] + public bool Success { get; } + + public LinkError? Error { get; } + public int? Code { get; } + public LinkResult() + { + Success = true; + + } + + public LinkResult(LinkError error) + { + Success = false; + Error = error; + } + + public LinkResult(int code) + { + Success = false; + Code = code; + Error = LinkError.GeneralFailure; + } + +} + +public enum LinkError +{ + GeneralFailure, + BadConfirmationCode, + UnableToGenerateCorrectCodes, +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs b/SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs new file mode 100644 index 0000000..fcb8282 --- /dev/null +++ b/SteamLibForked/SteamMobile/AuthenticatorLinker/SteamAuthenticatorLinker.cs @@ -0,0 +1,162 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using SteamLib.Account; +using SteamLib.Api.Mobile; +using SteamLib.Authentication; +using SteamLib.Core.Enums; +using SteamLib.Core.Interfaces; +using SteamLib.Exceptions; +using SteamLib.Exceptions.Mobile; +using SteamLib.ProtoCore.Enums; + +namespace SteamLib.SteamMobile.AuthenticatorLinker; + +[PublicAPI] +public class SteamAuthenticatorLinker +{ + public ILoginConsumer Consumer { get; } + public LinkOptions Options { get; } + public ISessionData? SessionData { get; private set; } + internal HttpClient Client => Options.HttpClient; + internal string? DeviceId { get; set; } + private ILogger? Logger => Options.Logger; + public SteamAuthenticatorLinker(LinkOptions options) + { + Options = options; + Consumer = options.LoginConsumer; + } + + public async Task LinkAccount(MobileSessionData data) + { + Dictionary scopeData = new() + { + {"consumer", Consumer.FriendlyName} + }; + using var ctx = Options.Logger?.BeginScope(scopeData); + + SessionData = data; + + data.EnsureValidated(); + if (data.RefreshToken.IsExpired) + { + Logger?.LogError("Session expired"); + throw new SessionExpiredException(SessionExpiredException.SESSION_EXPIRED_MSG); + } + + var accessToken = data.GetMobileToken(); + if (accessToken == null) + throw new SessionInvalidException("SessionData must provide valid AccessToken to access mobile endpoints"); + + if (accessToken.Value.IsExpired || accessToken.Value.Type != SteamAccessTokenType.Mobile) + { + if (accessToken.Value.Type != SteamAccessTokenType.Mobile) + Logger?.LogWarning("Provided access token is not of type Mobile. Actual type: {actualType}", accessToken.Value.Type); + + var refreshed = await SteamMobileApi.RefreshJwt(Options.HttpClient, data.RefreshToken.Token, data.RefreshToken.SteamId.Steam64); + accessToken = SteamTokenHelper.Parse(refreshed); + data.SetMobileToken(accessToken.Value); + } + + if (accessToken.Value.Type != SteamAccessTokenType.Mobile) + throw new SessionInvalidException("SessionData must provide AccessToken of type Mobile to access mobile endpoints"); + + + var hasPhoneResp = await this.HasPhone(); + var hasPhone = hasPhoneResp.HasPhone; + + + if (hasPhone && Options.SmsCodeProvider == null) + { + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.PhoneAlreadyAttached); + } + + long? phoneNumber = null; + + if (hasPhone == false && Options.PhoneNumberProvider != null) + { + phoneNumber = await Options.PhoneNumberProvider.GetPhoneNumber(Consumer); + } + + if (hasPhone == false && phoneNumber != null) + { + Logger?.LogInformation("Attaching phone number {phoneNumber}", phoneNumber); + //if (await this.IsValidPhoneNumber(phoneNumber.Value) == false) + // throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidPhoneNumber); + + if (await this.AttachPhone(phoneNumber.Value) == false) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantAttachPhone); + + await Options.EmailProvider!.ConfirmEmailLink(Consumer, EmailConfirmationType.AttachPhoneAuthenticator); + + if (await this.CheckEmailConfirmation() == false) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantConfirmAttachingEmail); + + var sendSms = await this.SendSmsCode(); + if (sendSms != EResult.OK) + throw new AuthenticatorLinkerException($"Can't send SMS code: {sendSms} ({(int)sendSms})"); + Logger?.LogDebug("SMS code sent"); + } + + Logger?.LogInformation("Starting LinkRequest"); + var resp = await this.LinkRequest(); + + var mobileData = resp.ToMobileDataExtended(DeviceId!, data.Clone()); + Options.BackupHandler?.Invoke(mobileData); + + if (resp.Status == 29) + { + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.AuthenticatorPresent); + } + + if (resp.Status != 1) + { + throw new AuthenticatorLinkerException(resp.Status); + } + + + string code; + var isPhone = resp.ConfirmType is 1 or 2; + if (isPhone) + { + if (Options.SmsCodeProvider == null) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.NoSmsCodeProvider); + + var smsCode = await Options.SmsCodeProvider.GetSmsCode(Consumer, phoneNumber, resp.PhoneNumberHint); + code = smsCode.ToString(); + } + else if (resp.ConfirmType == 3) + { + if (Options.EmailProvider == null) + throw new AuthenticatorLinkerException(AuthenticatorLinkerError.NoEmailProvider); + + code = await Options.EmailProvider.GetAddAuthenticatorCode(Consumer); + } + else + { + throw new ArgumentOutOfRangeException(nameof(resp.ConfirmType), resp.ConfirmType, + $"ConfirmType {resp.ConfirmType} not supported"); + } + + Logger?.LogInformation("Finalizing link"); + var result = await this.FinalizeLink(code, resp.SharedSecret, isPhone); + + if (result.Success == false) + { + var error = result.Error switch + { + LinkError.GeneralFailure => throw new AuthenticatorLinkerException(result.Code!.Value) { OnFinalization = true }, + LinkError.BadConfirmationCode => AuthenticatorLinkerError.BadConfirmationCode, + LinkError.UnableToGenerateCorrectCodes => AuthenticatorLinkerError.UnableToGenerateCorrectCodes, + _ => throw new ArgumentOutOfRangeException(nameof(result.Error), result.Error, $"LinkError {result.Error} not supported") + }; + + throw new AuthenticatorLinkerException(error) { OnFinalization = true }; + } + + Logger?.LogInformation("Linking completed"); + return mobileData; + } + + + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs new file mode 100644 index 0000000..826b5f4 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/AccountRecoveryConfirmation.cs @@ -0,0 +1,8 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class AccountRecoveryConfirmation : Confirmation +{ + public AccountRecoveryConfirmation(long id, ulong nonce, long creator, string typeName) : base(id, nonce, 6, creator, ConfirmationType.AccountRecovery, typeName) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs new file mode 100644 index 0000000..6b904af --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/Confirmation.cs @@ -0,0 +1,44 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class Confirmation +{ + public long Id { get; } + + /// + /// The unique key used to act upon this confirmation. + /// + public ulong Nonce { get; } + + /// + /// The value of the data-type HTML attribute returned for this contribution. + /// + public int IntType; + + /// + /// Represents either the Trade Offer ID or market transaction ID that caused this confirmation to be created. + /// + public long CreatorId { get; } + + /// + /// The type of this confirmation. + /// + public readonly ConfirmationType ConfType; + + public string TypeName { get; init; } + public string HeadLine { get; init; } = string.Empty; + + public DateTime Time { get; set; } + + public virtual ConfirmationDetails? Details { get; } + + public Confirmation(long id, ulong nonce, int intType, long creatorId, ConfirmationType confType, string typeName) + { + Id = id; + Nonce = nonce; + IntType = intType; + CreatorId = creatorId; + ConfType = confType; + TypeName = typeName; + } + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs new file mode 100644 index 0000000..74433ef --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/MarketConfirmation.cs @@ -0,0 +1,13 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class MarketConfirmation : Confirmation +{ + + public string ItemImageUri { get; init; } = string.Empty; + public string ItemName { get; init; } = string.Empty; + public string PriceString { get; init; } = string.Empty; + public MarketConfirmation(long id, ulong key, long creator, string typeName) : base(id, key, 3, creator, ConfirmationType.MarketSellTransaction, typeName) + { + + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs new file mode 100644 index 0000000..65354f1 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/RegisterApiKeyConfirmation.cs @@ -0,0 +1,8 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class RegisterApiKeyConfirmation : Confirmation +{ + public RegisterApiKeyConfirmation(long id, ulong nonce, long creatorId, string typeName) : base(id, nonce, 8, creatorId, ConfirmationType.RegisterApiKey, typeName) + { + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs new file mode 100644 index 0000000..7cce95a --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Concrete[folder]/TradeConfirmation.cs @@ -0,0 +1,16 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class TradeConfirmation : Confirmation +{ + public string UserAvatarUri { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public bool IsReceiveNothing { get; set; } + public override TradeConfirmationDetails? Details { get; } + + public long TradeId => CreatorId; + + public TradeConfirmation(long id, ulong nonce, long creator, string typeName) : base(id, nonce, 1, creator, ConfirmationType.Trade, typeName) + { + + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs b/SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs new file mode 100644 index 0000000..903ee74 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/ConfirmationType.cs @@ -0,0 +1,11 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public enum ConfirmationType +{ + Unknown, + Trade = 2, + MarketSellTransaction = 3, + AccountRecovery = 6, + RegisterApiKey = 9 + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs new file mode 100644 index 0000000..a307751 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/ConfirmationDetails.cs @@ -0,0 +1,6 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class ConfirmationDetails +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs new file mode 100644 index 0000000..936e564 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/MarketConfirmationDetails.cs @@ -0,0 +1,6 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class MarketConfirmationDetails : ConfirmationDetails +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs new file mode 100644 index 0000000..92aeaa8 --- /dev/null +++ b/SteamLibForked/SteamMobile/Confirmations/Details[Folder]/TradeDetailsConfirmation.cs @@ -0,0 +1,6 @@ +namespace SteamLib.SteamMobile.Confirmations; + +public class TradeConfirmationDetails : ConfirmationDetails +{ + +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs b/SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs new file mode 100644 index 0000000..edd77b1 --- /dev/null +++ b/SteamLibForked/SteamMobile/SteamGuardCodeGenerator.cs @@ -0,0 +1,65 @@ +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Text; +using SteamLib.Core.Interfaces; + +namespace SteamLib.SteamMobile; + +public class SteamGuardCodeGenerator : ISteamGuardProvider +{ + private static readonly byte[] SteamGuardCodeTranslations = { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 }; + + public string SharedSecret { get; } + + int ISteamGuardProvider.MaxRetryCount => ProviderMaxRetryCount; + public int ProviderMaxRetryCount { get; set; } + + public SteamGuardCodeGenerator(string sharedSecret) + { + SharedSecret = sharedSecret; + } + + + public ValueTask GetSteamGuardCode(ILoginConsumer caller) => ValueTask.FromResult(GenerateCode()); + public string GenerateCode() => GenerateCode(SharedSecret); + + + public static string GenerateCode(string sharedSecret) => GenerateCode(sharedSecret, TimeAligner.GetSteamTime()); + public static string GenerateCode(string sharedSecret, long time) + { + var sharedSecretUnescaped = Regex.Unescape(sharedSecret); + var sharedSecretArray = Convert.FromBase64String(sharedSecretUnescaped); + return GenerateCode(sharedSecretArray, time); + } + + public static string GenerateCode(byte[] sharedSecret, long time) + { + var timeArray = new byte[8]; + + time /= 30L; + + for (var i = 8; i > 0; i--) + { + timeArray[i - 1] = (byte)time; + time >>= 8; + } + + HMACSHA1 hmacGenerator = new() + { + Key = sharedSecret + }; + var hashedData = hmacGenerator.ComputeHash(timeArray); + var codeArray = new byte[5]; + + var b = (byte)(hashedData[19] & 0xF); + var codePoint = (hashedData[b] & 0x7F) << 24 | (hashedData[b + 1] & 0xFF) << 16 | (hashedData[b + 2] & 0xFF) << 8 | (hashedData[b + 3] & 0xFF); + + for (var i = 0; i < 5; ++i) + { + codeArray[i] = SteamGuardCodeTranslations[codePoint % SteamGuardCodeTranslations.Length]; + codePoint /= SteamGuardCodeTranslations.Length; + } + + return Encoding.UTF8.GetString(codeArray); + } +} \ No newline at end of file diff --git a/SteamLibForked/SteamMobile/TimeAligner.cs b/SteamLibForked/SteamMobile/TimeAligner.cs new file mode 100644 index 0000000..cee4720 --- /dev/null +++ b/SteamLibForked/SteamMobile/TimeAligner.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using AchiesUtilities.Models; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using SteamLib.Core; + +namespace SteamLib.SteamMobile; + + +//TODO: Refactor +[PublicAPI] +public static class TimeAligner +{ + private static bool _aligned; + private static int _timeDifference; + public static UnixTimeStamp UtcNow => UnixTimeStamp.FromDateTime(DateTime.UtcNow); + + private const string TIME_ALIGN_ENDPOINT = "ITwoFactorService/QueryTime/v0001"; + + + public static async ValueTask GetSteamTimeAsync() + { + if (!_aligned) + { + await AlignTimeAsync(); + } + return UtcNow.ToLong() + _timeDifference; + } + + public static long GetSteamTime() + { + if (!_aligned) + { + AlignTime(); + } + return UtcNow.ToLong() + _timeDifference; + } + + public static void AlignTime() + { + var client = new HttpClient(); + var sw = new Stopwatch(); + try + { + sw.Start(); + var req = new HttpRequestMessage(HttpMethod.Post, SteamConstants.STEAM_API + TIME_ALIGN_ENDPOINT); + var response = client.Send(req).EnsureSuccessStatusCode(); + sw.Stop(); + var stream = new StreamReader(response.Content.ReadAsStream()); + var respStr = stream.ReadToEnd(); + stream.Dispose(); + var j = JObject.Parse(respStr); + var time = j["response"]!["server_time"]!.Value(); + var now = UtcNow - sw.Elapsed; + _timeDifference = (int)(time - now.ToLong()); + _aligned = true; + } + finally + { + client.Dispose(); + } + } + + public static async Task AlignTimeAsync() + { + var client = new HttpClient(); + var sw = new Stopwatch(); + try + { + sw.Start(); + var response = await client.PostAsync(SteamConstants.STEAM_API + TIME_ALIGN_ENDPOINT, null); + sw.Stop(); + var respStr = await response.Content.ReadAsStringAsync(); + var j = JObject.Parse(respStr); + var time = j["response"]!["server_time"]!.Value(); + var now = UtcNow - sw.Elapsed; + _timeDifference = (int)(time - now.ToLong()); + _aligned = true; + + } + finally + { + sw.Stop(); + client.Dispose(); + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/EncryptionHelper.cs b/SteamLibForked/Utility/EncryptionHelper.cs new file mode 100644 index 0000000..8b0fdec --- /dev/null +++ b/SteamLibForked/Utility/EncryptionHelper.cs @@ -0,0 +1,74 @@ +using System.Security.Cryptography; +using System.Text; + +namespace SteamLib.Utility; + +public static class EncryptionHelper +{ + public static byte[] HexStringToByteArray(string hex) + { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + + public static string ToBase64EncryptedPassword(string keyExp, string keyMod, string password) + { + // RSA Encryption. + RSACryptoServiceProvider rsa = new(); + RSAParameters rsaParameters = new() + { + Exponent = HexStringToByteArray(keyExp), + Modulus = HexStringToByteArray(keyMod) + }; + rsa.ImportParameters(rsaParameters); + + // Encrypt the password and convert it. + byte[] bytePassword = Encoding.ASCII.GetBytes(password); + byte[] encodedPassword = rsa.Encrypt(bytePassword, false); + return Convert.ToBase64String(encodedPassword); + } + + public static string GenerateConfirmationHash(long time, string identitySecret, string tag = "conf") + { + var hashedBytes = GenerateConfirmationHashBytes(time, identitySecret, tag); + var encodedData = Convert.ToBase64String(hashedBytes, Base64FormattingOptions.None); + var hash = encodedData; + return hash; + } + public static byte[] GenerateConfirmationHashBytes(long time, string identitySecret, string tag = "conf") + { + byte[] decode = Convert.FromBase64String(identitySecret); + int n2; + if (tag.Length > 32) + { + n2 = 8 + 32; + } + else + { + n2 = 8 + tag.Length; + } + byte[] array = new byte[n2]; + int n3 = 8; + while (true) + { + int n4 = n3 - 1; + if (n3 <= 0) + { + break; + } + array[n4] = (byte)time; + time >>= 8; + n3 = n4; + } + Array.Copy(Encoding.UTF8.GetBytes(tag), 0, array, 8, n2 - 8); + + var hmacGenerator = new HMACSHA1 + { + Key = decode + }; + var hashedData = hmacGenerator.ComputeHash(array); + return hashedData; + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/EnvironmentUtility.cs b/SteamLibForked/Utility/EnvironmentUtility.cs new file mode 100644 index 0000000..a3111a2 --- /dev/null +++ b/SteamLibForked/Utility/EnvironmentUtility.cs @@ -0,0 +1,224 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace SteamLib.Utility; + +public static class EnvironmentUtility +{ + // ReSharper disable once InconsistentNaming + public static EOSType GetOSType() + { + var osVer = Environment.OSVersion; + var ver = osVer.Version; + + return osVer.Platform switch + { + PlatformID.Win32Windows => ver.Minor switch + { + 0 => EOSType.Win95, + 10 => EOSType.Win98, + 90 => EOSType.WinME, + _ => EOSType.WinUnknown, + }, + + PlatformID.Win32NT => ver.Major switch + { + 4 => EOSType.WinNT, + 5 => ver.Minor switch + { + 0 => EOSType.Win2000, + 1 => EOSType.WinXP, + // Assume nobody runs Windows XP Professional x64 Edition + // It's an edition of Windows Server 2003 anyway. + 2 => EOSType.Win2003, + _ => EOSType.WinUnknown, + }, + 6 => ver.Minor switch + { + 0 => EOSType.WinVista, // Also Server 2008 + 1 => EOSType.Windows7, // Also Server 2008 R2 + 2 => EOSType.Windows8, // Also Server 2012 + // Note: The OSVersion property reports the same version number (6.2.0.0) for both Windows 8 and Windows 8.1.- http://msdn.microsoft.com/en-us/library/system.environment.osversion(v=vs.110).aspx + // In practice, this will only get hit if the application targets Windows 8.1 in the app manifest. + // See http://msdn.microsoft.com/en-us/library/windows/desktop/dn481241(v=vs.85).aspx for more info. + 3 => EOSType.Windows81, // Also Server 2012 R2 + _ => EOSType.WinUnknown, + }, + 10 when ver.Build >= 22000 => EOSType.Win11, + 10 => EOSType.Windows10,// Also Server 2016, Server 2019, Server 2022 + _ => EOSType.WinUnknown, + }, + + // The specific minor versions only exist in Valve's enum for LTS versions + PlatformID.Unix when RuntimeInformation.IsOSPlatform(OSPlatform.Linux) => ver.Major switch + { + 2 => ver.Minor switch + { + 2 => EOSType.Linux22, + 4 => EOSType.Linux24, + 6 => EOSType.Linux26, + _ => EOSType.LinuxUnknown, + }, + 3 => ver.Minor switch + { + 2 => EOSType.Linux32, + 5 => EOSType.Linux35, + 6 => EOSType.Linux36, + 10 => EOSType.Linux310, + 16 => EOSType.Linux316, + 18 => EOSType.Linux318, + _ => EOSType.Linux3x, + }, + 4 => ver.Minor switch + { + 1 => EOSType.Linux41, + 4 => EOSType.Linux44, + 9 => EOSType.Linux49, + 14 => EOSType.Linux414, + 19 => EOSType.Linux419, + _ => EOSType.Linux4x, + }, + 5 => ver.Minor switch + { + 4 => EOSType.Linux54, + 10 => EOSType.Linux510, + _ => EOSType.Linux5x, + }, + 6 => EOSType.Linux6x, + 7 => EOSType.Linux7x, + _ => EOSType.LinuxUnknown, + }, + + PlatformID.Unix when RuntimeInformation.IsOSPlatform(OSPlatform.OSX) => ver.Major switch + { + 11 => EOSType.MacOS107, // "Lion" + 12 => EOSType.MacOS108, // "Mountain Lion" + 13 => EOSType.MacOS109, // "Mavericks" + 14 => EOSType.MacOS1010, // "Yosemite" + 15 => EOSType.MacOS1011, // El Capitan + 16 => EOSType.MacOS1012, // Sierra + 17 => EOSType.Macos1013, // High Sierra + 18 => EOSType.Macos1014, // Mojave + 19 => EOSType.Macos1015, // Catalina + 20 => EOSType.MacOS11, // Big Sur + 21 => EOSType.MacOS12, // Monterey + _ => EOSType.MacOSUnknown, + }, + + _ => EOSType.Unknown, + }; + } +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum EOSType +{ + Unknown = -1, + Web = -700, + IOSUnknown = -600, + IOS1 = -599, + IOS2 = -598, + IOS3 = -597, + IOS4 = -596, + IOS5 = -595, + IOS6 = -594, + IOS6_1 = -593, + IOS7 = -592, + IOS7_1 = -591, + IOS8 = -590, + IOS8_1 = -589, + IOS8_2 = -588, + IOS8_3 = -587, + IOS8_4 = -586, + IOS9 = -585, + IOS9_1 = -584, + IOS9_2 = -583, + IOS9_3 = -582, + IOS10 = -581, + IOS10_1 = -580, + IOS10_2 = -579, + IOS10_3 = -578, + IOS11 = -577, + IOS11_1 = -576, + IOS11_2 = -575, + IOS11_3 = -574, + IOS11_4 = -573, + IOS12 = -572, + IOS12_1 = -571, + AndroidUnknown = -500, + Android6 = -499, + Android7 = -498, + Android8 = -497, + Android9 = -496, + UMQ = -400, + PS3 = -300, + MacOSUnknown = -102, + MacOS104 = -101, + MacOS105 = -100, + MacOS1058 = -99, + MacOS106 = -95, + MacOS1063 = -94, + MacOS1064_slgu = -93, + MacOS1067 = -92, + MacOS107 = -90, + MacOS108 = -89, + MacOS109 = -88, + MacOS1010 = -87, + MacOS1011 = -86, + MacOS1012 = -85, + Macos1013 = -84, + Macos1014 = -83, + Macos1015 = -82, + MacOS1016 = -81, + MacOS11 = -80, + MacOS111 = -79, + MacOS1017 = -78, + MacOS12 = -77, + MacOS13 = -76, + MacOSMax = -1, + LinuxUnknown = -203, + Linux22 = -202, + Linux24 = -201, + Linux26 = -200, + Linux32 = -199, + Linux35 = -198, + Linux36 = -197, + Linux310 = -196, + Linux316 = -195, + Linux318 = -194, + Linux3x = -193, + Linux4x = -192, + Linux41 = -191, + Linux44 = -190, + Linux49 = -189, + Linux414 = -188, + Linux419 = -187, + Linux5x = -186, + Linux54 = -185, + Linux6x = -184, + Linux7x = -183, + Linux510 = -182, + LinuxMax = -101, + WinUnknown = 0, + Win311 = 1, + Win95 = 2, + Win98 = 3, + WinME = 4, + WinNT = 5, + Win2000 = 6, + WinXP = 7, + Win2003 = 8, + WinVista = 9, + Windows7 = 10, + Win2008 = 11, + Win2012 = 12, + Windows8 = 13, + Windows81 = 14, + Win2012R2 = 15, + Windows10 = 16, + Win2016 = 17, + Win2019 = 18, + Win2022 = 19, + Win11 = 20, + WinMAX = 21, +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs b/SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs new file mode 100644 index 0000000..0bbca7a --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/DeserializedMafileData.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; + +namespace SteamLib.Utility.MaFiles; + +public enum DeserializedMafileSessionResult +{ + Missing, + Invalid, + Expired, + Valid, +} +public class DeserializedMafileData +{ + public int? Version { get; init; } + public bool IsExtended { get; init; } + public DeserializedMafileSessionResult SessionResult { get; init; } + public Dictionary? UnusedProperties { get; init; } = null; + public HashSet? MissingProperties { get; init; } = new(); + + + public bool IsActual => Version == MafileSerializer.MAFILE_VERSION; + public bool IsLegacy => Version == 0; + public bool HasUnusedProperties => UnusedProperties is { Count: > 0 }; + public bool HasMissingProperties => MissingProperties is { Count: > 0 }; + + internal static DeserializedMafileData Create(int? version = null, bool isExtended = false, Dictionary? properties = null, HashSet? missingProperties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Missing) + { + return new DeserializedMafileData + { + Version = version, + UnusedProperties = properties, + IsExtended = isExtended, + MissingProperties = missingProperties, + SessionResult = sessionResult + }; + } + + internal static DeserializedMafileData CreateActual(Dictionary? properties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Valid) + { + return new DeserializedMafileData + { + Version = MafileSerializer.MAFILE_VERSION, + UnusedProperties = properties, + IsExtended = true, + SessionResult = sessionResult + }; + } +} + + +/// +/// Representation class just for displaying deserialization result +/// +[PublicAPI] +public class DeserializedMafileResult +{ + public static readonly HashSet ImportantProperties = new() + { + nameof(MobileDataExtended.RevocationCode), + nameof(MobileDataExtended.AccountName), + }; + + public static readonly HashSet NotImportantProperties = new() + { + nameof(MobileDataExtended.ServerTime), + nameof(MobileDataExtended.SerialNumber), + nameof(MobileDataExtended.Uri), + nameof(MobileDataExtended.TokenGid), + nameof(MobileDataExtended.Secret1), + }; + + public int? Version { get; init; } + + public bool IsExtended { get; init; } + public DeserializedMafileSessionResult SessionResult { get; init; } + + public HashSet? MissingImportantProperties { get; init; } = new(); + public HashSet? MissingProperties { get; init; } = new(); + public Dictionary? UnusedProperties { get; init; } + + public bool IsActual => Version == MafileSerializer.MAFILE_VERSION; + public bool HasUnusedProperties => UnusedProperties is { Count: > 0 }; + public bool HasMissingProperties => MissingProperties is { Count: > 0 }; + public bool HasMissingImportantProperties => MissingImportantProperties is { Count: > 0 }; + public bool HasSession => SessionResult == DeserializedMafileSessionResult.Valid; + + + public static DeserializedMafileResult FromData(DeserializedMafileData data) + { + HashSet? missingImportantProperties = null; + HashSet? missingProperties = null; + if (data is { IsExtended: true, HasMissingProperties: true }) + { + var important = data.MissingProperties?.Intersect(ImportantProperties).ToList(); + if (important?.Count > 0) missingImportantProperties = important.ToHashSet(); + var notImportant = data.MissingProperties?.Intersect(NotImportantProperties).ToList(); + if (notImportant?.Count > 0) missingProperties = notImportant.ToHashSet(); + } + + return new DeserializedMafileResult + { + Version = data.Version, + IsExtended = data.IsExtended, + MissingImportantProperties = missingImportantProperties, + MissingProperties = missingProperties, + UnusedProperties = data.UnusedProperties?.ToDictionary(x => x.Key, x => JTokenToString(x.Value)), + SessionResult = data.SessionResult + }; + } + + private static string JTokenToString(JProperty property) + { + switch (property.Value.Type) + { + case JTokenType.None: + case JTokenType.Null: + return "null"; + case JTokenType.Object: + return "object"; + case JTokenType.Array: + return "array"; + case JTokenType.Integer: + return property.Value.Value().ToString(); + case JTokenType.Float: + return property.Value.Value().ToString(CultureInfo.InvariantCulture); + case JTokenType.String: + return property.Value.Value()!; + case JTokenType.Boolean: + return property.Value.Value().ToString(); + case JTokenType.Date: + return property.Value.Value().ToString(CultureInfo.InvariantCulture); + case JTokenType.Guid: + return property.Value.Value().ToString(); + case JTokenType.Uri: + return property.Value.Value()!.ToString(); + case JTokenType.TimeSpan: + return property.Value.Value().ToString(); + default: + return "unknown"; + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/LegacyMafile.cs b/SteamLibForked/Utility/MaFiles/LegacyMafile.cs new file mode 100644 index 0000000..b540b33 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/LegacyMafile.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using SteamLib.Web.Converters; + +namespace SteamLib.Utility.MaFiles; + +internal class LegacyMafile //TODO: move +{ + + [JsonProperty("shared_secret")] + [JsonRequired] + public string SharedSecret { get; set; } + + [JsonProperty("identity_secret")] + [JsonRequired] + public string IdentitySecret { get; set; } + + [JsonProperty("device_id")] + [JsonRequired] + public string DeviceId { get; set; } + + [JsonProperty("revocation_code")] + public string RevocationCode { get; set; } + + [JsonProperty("account_name")] + public string AccountName { get; set; } + + [JsonProperty("Session")] + public object? SessionData { get; set; } + [JsonProperty("server_time")] public long ServerTime { get; set; } //Unused + + [JsonProperty("serial_number")] + [JsonConverter(typeof(StringToLongIfNeededConverter))] + public string SerialNumber { get; set; } //Unused + [JsonProperty("uri")] public string Uri { get; set; } //Unused + [JsonProperty("token_gid")] public string TokenGid { get; set; } //Unused + [JsonProperty("secret_1")] public string Secret1 { get; set; } //Unused + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileCredits.cs b/SteamLibForked/Utility/MaFiles/MafileCredits.cs new file mode 100644 index 0000000..67d29d2 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileCredits.cs @@ -0,0 +1,17 @@ +namespace SteamLib.Utility.MaFiles; + +public class MafileCredits : IMafileCredits +{ + internal static readonly MafileCredits Instance = new(); + private const string ORIGINAL_AUTHOR = "Achies"; + private const string MOBILE_APP = "https://github.com/achiez/NebulaAuth"; + + public string OriginalAuthor => ORIGINAL_AUTHOR; + public string BestOpenSourceMobileApp => MOBILE_APP; + +} + +public interface IMafileCredits +{ + string OriginalAuthor { get; } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer.cs new file mode 100644 index 0000000..9f34bb8 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer.cs @@ -0,0 +1,136 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamLib.Account; + +namespace SteamLib.Utility.MaFiles; + +public static partial class MafileSerializer +{ + public const int MAFILE_VERSION = 2; + + public const string SIGNATURE_PROPERTY_NAME = "@SLSV"; + private static readonly HashSet ActualProperties = typeof(MobileDataExtended).GetProperties().Select(x => x.Name).ToHashSet(); + + + public static MobileData Deserialize(string json, out DeserializedMafileData mafileData) + { + + var j = JObject.Parse(json); + + var unusedProperties = j.Properties().ToDictionary(x => x.Name, x => x); + var versionToken = GetToken(j, SIGNATURE_PROPERTY_NAME); + unusedProperties.Remove(CREDITS_PROPERTY_NAME); + int? version = null; + if (versionToken is { Type: JTokenType.Integer }) + { + version = versionToken.Value(); + } + + unusedProperties.Remove(SIGNATURE_PROPERTY_NAME); + if (version == MAFILE_VERSION) + { + try + { + var unused = new Dictionary(); + foreach (var (key, prop) in unusedProperties) + { + if (ActualProperties.Contains(key)) + continue; + unused.Add(key, prop); + } + + var data = j.ToObject()!; + data.SessionData = Validate.ValidateMobileData(data, out var sessionResult); + + mafileData = DeserializedMafileData.CreateActual(unused, sessionResult); + return data; + + } + catch (Exception ex) + { + throw new JsonSerializationException("MafileSerializer found actual 'Version' signature but some error occurred during deserialization", ex); + } + } + + var sharedSecretToken = GetTokenOrThrow(j, nameof(MobileData.SharedSecret), unusedProperties, "sharedsecret", "shared_secret", "shared"); + var identitySecretToken = GetTokenOrThrow(j, nameof(MobileData.IdentitySecret), unusedProperties, "identitysecret", "identity_secret", "identity"); + var deviceIdToken = GetTokenOrThrow(j, nameof(MobileData.DeviceId), unusedProperties, "deviceid", "device_id", "device"); + + + var sharedSecret = GetBase64(nameof(MobileData.SharedSecret), sharedSecretToken); + var identitySecret = GetBase64(nameof(MobileData.IdentitySecret), identitySecretToken); + var deviceId = deviceIdToken.Value(); + Validate.NotNullOrEmpty(nameof(MobileData.DeviceId), deviceId); + + var accountNameToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.AccountName), "account_name", "accountname"); + var revocationCodeToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.RevocationCode), "revocation_code", "rcode", "r_code", "revocationcode"); + var sessionDataToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.SessionData), "session_data", "sessiondata", "session"); + + var accountName = accountNameToken?.Value(); + var revocationCode = revocationCodeToken?.Value(); + + + var minified + = string.IsNullOrWhiteSpace(accountName) + && string.IsNullOrWhiteSpace(revocationCode) + && (sessionDataToken == null || sessionDataToken.Type == JTokenType.Null); + + + if (minified) + { + mafileData = DeserializedMafileData.Create(version, false, unusedProperties); + return new MobileData + { + DeviceId = deviceId, + IdentitySecret = identitySecret, + SharedSecret = sharedSecret + }; + } + + + var missingProperties = new List(); + if (string.IsNullOrWhiteSpace(accountName)) missingProperties.Add(nameof(MobileDataExtended.AccountName)); + if (string.IsNullOrWhiteSpace(revocationCode)) missingProperties.Add(nameof(MobileDataExtended.RevocationCode)); + + var serverTime = GetLong(j, unusedProperties, nameof(MobileDataExtended.ServerTime), "server_time", "servertime"); + var serialNumber = GetULong(j, unusedProperties, nameof(MobileDataExtended.SerialNumber), "serial_number", "serialnumber"); + var uri = GetString(j,unusedProperties, nameof(MobileDataExtended.Uri), "url", "uri"); + var tokenGid = GetString(j, unusedProperties, nameof(MobileDataExtended.TokenGid), "token_gid", "tokengid"); + var secret1 = GetString(j, unusedProperties, nameof(MobileDataExtended.Secret1), "secret_1", "seecret1"); + + if(serverTime == null) missingProperties.Add(nameof(MobileDataExtended.ServerTime)); + if(serialNumber == null) missingProperties.Add(nameof(MobileDataExtended.SerialNumber)); + if(string.IsNullOrWhiteSpace(uri)) missingProperties.Add(nameof(MobileDataExtended.Uri)); + if(string.IsNullOrWhiteSpace(tokenGid)) missingProperties.Add(nameof(MobileDataExtended.TokenGid)); + if(string.IsNullOrWhiteSpace(secret1)) missingProperties.Add(nameof(MobileDataExtended.Secret1)); + + MobileSessionData? sessionData = null; + var sResult = DeserializedMafileSessionResult.Missing; + if (sessionDataToken is { Type: JTokenType.Object }) + { + sessionData = DeserializeMobileSessionData((JObject) sessionDataToken, out sResult); + } + + + mafileData = DeserializedMafileData.Create(version, true, unusedProperties, missingProperties.ToHashSet(), sResult); + return new MobileDataExtended + { + DeviceId = deviceId, + IdentitySecret = identitySecret, + SharedSecret = sharedSecret, + AccountName = accountName ?? string.Empty, + RevocationCode = revocationCode, + ServerTime = serverTime.GetValueOrDefault(), + SerialNumber = serialNumber.GetValueOrDefault(), + Uri = uri ?? string.Empty, + TokenGid = tokenGid ?? string.Empty, + Secret1 = secret1 ?? string.Empty, + SessionData = sessionData + }; + + } + + + + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs new file mode 100644 index 0000000..bdbac5f --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_SessionData.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json.Linq; +using SteamLib.Account; +using SteamLib.Authentication; +using SteamLib.Core.Enums; + +namespace SteamLib.Utility.MaFiles; + +public partial class MafileSerializer //SessionData +{ + private static MobileSessionData? DeserializeMobileSessionData(JObject j, out DeserializedMafileSessionResult result) + { + result = DeserializedMafileSessionResult.Invalid; + var refreshTokenToken = GetToken(j, nameof(MobileSessionData.RefreshToken), "refreshtoken", "refresh_token", + "refresh", "OAuthToken"); + + + SteamAuthToken refreshToken; + if (refreshTokenToken == null || refreshTokenToken.Type == JTokenType.Null) return null; + if (refreshTokenToken.Type == JTokenType.String && SteamTokenHelper.TryParse(refreshTokenToken.Value()!, out var parsed)) + { + refreshToken = parsed; + } + else if (refreshTokenToken.Type == JTokenType.Object) + { + try + { + refreshToken = refreshTokenToken.ToObject(); + } + catch + { + return null; + } + } + else + { + return null; + } + + if (refreshToken.IsExpired || refreshToken.Type != SteamAccessTokenType.MobileRefresh) + { + result = DeserializedMafileSessionResult.Expired; + return null; + } + + var sessionId = GetString(j, "sessionid", "session_id", "session"); + var accessTokenToken = GetToken(j, "accesstoken", "access_token", "access"); + accessTokenToken ??= GetToken(j, "steamLoginSecure"); + + if (string.IsNullOrWhiteSpace(sessionId)) return null; + + SteamAuthToken? accessToken = null; + if (accessTokenToken == null || accessTokenToken.Type == JTokenType.Null) + { + accessToken = null; + } + if (accessTokenToken is { Type: JTokenType.Object }) + { + try + { + accessToken = refreshTokenToken.ToObject(); + } + catch + { + // ignored + } + } + else if (accessTokenToken is { Type: JTokenType.String } && + SteamTokenHelper.TryParse(accessTokenToken.Value()!, out var token) && token.Type == SteamAccessTokenType.Mobile) + { + accessToken = token; + } + + + var sessionData = new MobileSessionData(sessionId, refreshToken.SteamId, refreshToken, accessToken, new Dictionary()); + sessionData.IsValid = SessionDataValidator.Validate(null, sessionData).Succeeded; + if(sessionData.IsValid == false) + return null; + result = DeserializedMafileSessionResult.Valid; + return sessionData; + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs new file mode 100644 index 0000000..43486ff --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_Utility.cs @@ -0,0 +1,132 @@ +using Newtonsoft.Json.Linq; + +namespace SteamLib.Utility.MaFiles; + +public partial class MafileSerializer //Utility +{ + private static JToken? GetToken(JObject j, params string[] aliases) + { + foreach (var name in aliases) + { + if (j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) + { + return token; + } + + } + return null; + } + + private static JToken? GetToken(JObject j, Dictionary removeFrom, params string[] aliases) + { + foreach (var name in aliases) + { + if (!j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) continue; + var parent = token.Parent as JProperty; + removeFrom.Remove(parent!.Name); + return token; + + } + return null; + } + + private static JToken GetTokenOrThrow(JObject j, string propertyName, Dictionary removeFrom, params string[] aliases) + { + foreach (var name in aliases) + { + if (!j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) continue; + if (token.Type == JTokenType.Null) + { + throw new ArgumentException($"Required property {propertyName} is null"); + } + var parent = token.Parent as JProperty; + removeFrom.Remove(parent!.Name); + return token; + + } + + throw new ArgumentException($"Required property {propertyName} not found"); + } + + + private static string? GetString(JObject j, params string[] aliases) + { + var token = GetToken(j, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + + return token.Value(); + } + + private static string? GetString(JObject j, Dictionary removeFrom, + params string[] aliases) + { + var token = GetToken(j, removeFrom, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + + + return token.Value(); + + } + + private static long? GetLong(JObject j, Dictionary removeFrom, + params string[] aliases) + { + var token = GetToken(j, removeFrom, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.String when long.TryParse(token.Value()!, out var r) => r, + _ => null + }; + } + + private static ulong? GetULong(JObject j, Dictionary removeFrom, + params string[] aliases) + { + var token = GetToken(j, removeFrom, aliases); + if (token == null || token.Type == JTokenType.Null) + return null; + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.String when ulong.TryParse(token.Value()!, out var r) => r, + _ => null + }; + } + + private static string GetBase64(string propertyName, JToken token) + { + if (token.Type == JTokenType.Null) throw new ArgumentNullException(propertyName, $"{propertyName} is null"); + if (token.Type == JTokenType.String) + { + var result = token.Value()!; + Validate.IsValidBase64(propertyName, result); + return result; + } + else if (token.Type == JTokenType.Array) + { + try + { + var result = token.Value()!; + var base64 = Convert.ToBase64String(result); + return base64; + } + catch (Exception e) + { + throw new ArgumentException($"{propertyName}'s Array is not valid base64", e); + } + } + + throw new ArgumentOutOfRangeException(nameof(token.Type), + $"Not valid token type for base64 property '{propertyName}'. Type: {token.Type}"); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs new file mode 100644 index 0000000..7e7acfa --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_Validate.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json.Linq; +using SteamLib.Account; +using SteamLib.Authentication; +using SteamLib.Core.Interfaces; + +namespace SteamLib.Utility.MaFiles; + +public partial class MafileSerializer //Validate +{ + private static class Validate + { + public static void NotNull(string name, [System.Diagnostics.CodeAnalysis.NotNull] object? o) + { + if (o == null) + { + throw new ArgumentNullException(name, $"{name} is null"); + } + } + public static void NotNull(string name, [System.Diagnostics.CodeAnalysis.NotNull] JToken? o) + { + if (o == null || o.Type == JTokenType.Null) + { + throw new ArgumentNullException(name, $"{name} is null"); + } + } + + + public static void NotNullOrEmpty(string name, [System.Diagnostics.CodeAnalysis.NotNull] string? s) + { + if (string.IsNullOrWhiteSpace(s)) + { + throw new ArgumentException($"{name} is null or empty"); + } + } + + + + public static void IsValidBase64(string name, string base64) + { + var buffer = new Span(new byte[base64.Length]); + if(Convert.TryFromBase64String(base64, buffer, out _) == false) + throw new ArgumentException($"{name} is not valid base64 string"); + + } + + public static MobileSessionData? ValidateMobileData(MobileData data, out DeserializedMafileSessionResult sessionResult) + { + + NotNullOrEmpty(nameof(data.SharedSecret), data.SharedSecret); + NotNullOrEmpty(nameof(data.IdentitySecret), data.IdentitySecret); + NotNullOrEmpty(nameof(data.DeviceId), data.DeviceId); + IsValidBase64(nameof(data.SharedSecret), data.SharedSecret); + IsValidBase64(nameof(data.IdentitySecret), data.IdentitySecret); + + sessionResult = DeserializedMafileSessionResult.Missing; + if (data is MobileDataExtended d) + { + NotNullOrEmpty(nameof(MobileDataExtended.AccountName), d.AccountName); + NotNullOrEmpty(nameof(MobileDataExtended.RevocationCode), d.RevocationCode); + if (d.SessionData == null) return null; + + sessionResult = DeserializedMafileSessionResult.Invalid; + if (d.SessionData.RefreshToken.IsExpired) + { + sessionResult = DeserializedMafileSessionResult.Expired; + return null; + } + + d.SessionData.IsValid = SessionDataValidator.Validate(null, d.SessionData).Succeeded; + if (d.SessionData.IsValid == false) return null; + + sessionResult = DeserializedMafileSessionResult.Valid; + return d.SessionData; + } + return null; + } + + public static bool ValidateSessionData(ISessionData sessionData, out bool isOutdated) + { + + if (sessionData.RefreshToken.IsExpired) + { + isOutdated = true; + return false; + } + + isOutdated = false; + return SessionDataValidator.Validate(null, sessionData).Succeeded; + + } + } +} \ No newline at end of file diff --git a/SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs b/SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs new file mode 100644 index 0000000..89a2b84 --- /dev/null +++ b/SteamLibForked/Utility/MaFiles/MafileSerializer_Write.cs @@ -0,0 +1,111 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Xml; +using Formatting = Newtonsoft.Json.Formatting; + +namespace SteamLib.Utility.MaFiles; + +public static partial class MafileSerializer //Write +{ + private const string CREDITS_PROPERTY_NAME = "Credits"; + public static string Serialize(MobileData mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null) + { + using var w = new StringWriter(); + using var write = new JsonTextWriter(w); + write.Formatting = formatting; + var j = JObject.FromObject(mobileData); + j.Add(SIGNATURE_PROPERTY_NAME, MAFILE_VERSION); + if (sign) + { + credits ??= MafileCredits.Instance; + var obj = JObject.FromObject(credits); + j.Add(CREDITS_PROPERTY_NAME, obj); + } + j.WriteTo(write); + return w.ToString(); + + + } + + public static async Task SerializeAsync(MobileData mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null) + { + await using var w = new StringWriter(); + await using var write = new JsonTextWriter(w); + write.Formatting = formatting; + var j = JObject.FromObject(mobileData); + j.Add(SIGNATURE_PROPERTY_NAME, j); + if (sign) + { + credits ??= MafileCredits.Instance; + var obj = JObject.FromObject(credits); + j.Add(CREDITS_PROPERTY_NAME, obj); + } + + + await j.WriteToAsync(write); + return w.ToString(); + + } + + public static string SerializeLegacy(MobileData mobileData, Formatting formatting, Dictionary? additionalProperties = null, bool sign = true, MafileCredits? credits = null) + { + var result = new LegacyMafile + { + SharedSecret = mobileData.SharedSecret, + IdentitySecret = mobileData.IdentitySecret, + DeviceId = mobileData.DeviceId, + + }; + + if (mobileData is MobileDataExtended ext) + { + result.RevocationCode = ext.RevocationCode ?? string.Empty; + result.AccountName = ext.AccountName; + result.SessionData = ext.SessionData == null + ? null + : new + { + AccesToken = ext.SessionData?.MobileToken?.Token, + steamLoginSecure = ext.SessionData?.MobileToken?.SignedToken, + RefreshToken = ext.SessionData?.RefreshToken.Token, + SteamID = ext.SessionData?.SteamId.Steam64.Id, + SessionID = ext.SessionData?.SessionId + }; + result.ServerTime = ext.ServerTime; + result.SerialNumber = ext.SerialNumber.ToString(); + result.Uri = ext.Uri; + result.TokenGid = ext.TokenGid; + result.Secret1 = ext.Secret1; + + } + + + using var w = new StringWriter(); + using var write = new JsonTextWriter(w); + write.Formatting = formatting; + var j = JObject.FromObject(result); + if (additionalProperties != null) + { + foreach (var (name, value) in additionalProperties) + { + JToken? jToken = null; + if (value != null) + { + jToken = JToken.FromObject(value); + } + + j.Add(name, jToken); + } + } + + if (sign) + { + credits ??= MafileCredits.Instance; + var obj = JObject.FromObject(credits); + j.Add(CREDITS_PROPERTY_NAME, obj); + } + j.WriteTo(write); + return w.ToString(); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/Models/Enumeration.cs b/SteamLibForked/Utility/Models/Enumeration.cs new file mode 100644 index 0000000..a5dbc9d --- /dev/null +++ b/SteamLibForked/Utility/Models/Enumeration.cs @@ -0,0 +1,44 @@ +using System.Reflection; + +namespace SteamLib.Utility.Models; + +public abstract class Enumeration(int id, string name) : IComparable +{ + public string Name { get; } = name; + public int Id { get; } = id; + + public override string ToString() => Name; + + //public static IEnumerable GetAll() where T : Enumeration => + // typeof(T).GetFields(BindingFlags.Public | + // BindingFlags.Static | + // BindingFlags.DeclaredOnly) + // .Select(f => f.GetValue(null)) + // .Cast(); + + public static IEnumerable GetAll() where T : Enumeration + { + var res = typeof(T).GetFields(BindingFlags.Public | + BindingFlags.Static | + BindingFlags.DeclaredOnly) + .Select(f => f.GetValue(null)) + .Cast(); + return res; + } + + public override bool Equals(object? obj) + { + if (obj is not Enumeration otherValue) + { + return false; + } + return GetType() == obj.GetType() && Id.Equals(otherValue.Id); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Id, GetType()); + } + + public int CompareTo(object? other) => Id.CompareTo(((Enumeration?)other)?.Id); +} \ No newline at end of file diff --git a/SteamLibForked/Utility/SteamIdParser.cs b/SteamLibForked/Utility/SteamIdParser.cs new file mode 100644 index 0000000..d0a622a --- /dev/null +++ b/SteamLibForked/Utility/SteamIdParser.cs @@ -0,0 +1,157 @@ +using System.Text.RegularExpressions; +using SteamLib.Account; + +namespace SteamLib.Utility; + +public static class SteamIdParser +{ + + public static readonly Regex Steam64Regex = new("^7656119([0-9]{10})$", RegexOptions.Compiled); + public static readonly Regex Steam2Regex = new("STEAM_(?[0-9]):(?[0-9]):(?[0-9]{1,10})", RegexOptions.Compiled); + public static readonly Regex Steam3Regex = new("^\\[?(?[a-zA-Z]):1:(?[0-9]{1,10})\\]$", RegexOptions.Compiled); + + + #region TryParse + public static bool TryParse(string input, out SteamId result) + { + if (TryParse64(input, out var steam64)) + { + result = new SteamId(steam64); + return true; + } + + if (TryParse2(input, out var steam2)) + { + result = new SteamId(steam2); + return true; + } + + if (TryParse3(input, out var steam3)) + { + result = new SteamId(steam3); + return true; + } + + + result = new SteamId(new SteamId2(0, 0)); + return false; + } + + public static bool TryParse64(string input, out SteamId64 result) + { + var match64 = Steam64Regex.Match(input); + if (match64.Success) + { + result = new SteamId64(long.Parse(match64.Value)); + return true; + } + + result = default; + return false; + } + + public static bool TryParse2(string input, out SteamId2 result) + { + var match2 = Steam2Regex.Match(input); + if (match2.Success) + { + var universe = byte.Parse(match2.Groups["universe"].Value); + var lowestBit = byte.Parse(match2.Groups["lowestBit"].Value); + var highestBits = int.Parse(match2.Groups["highestBits"].Value); + result = new SteamId2(universe, lowestBit, highestBits); + return true; + } + + result = default; + return false; + } + + public static bool TryParse3(string input, out SteamId3 result) + { + var match3 = Steam3Regex.Match(input); + if (match3.Success) + { + var type = match3.Groups["type"].Value[0]; + var id = int.Parse(match3.Groups["id"].Value); + result = new SteamId3(id, type); + return true; + } + + + result = default; + return false; + } + #endregion + + #region Parse + + public static SteamId Parse(string input) + { + if (TryParse64(input, out var steam64)) + { + return new SteamId(steam64); + } + + if (TryParse2(input, out var steam2)) + { + return new SteamId(steam2); + + } + + if (TryParse3(input, out var steam3)) + { + return new SteamId(steam3); + } + + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId64, SteamId2 or SteamId3."); + } + + public static SteamId64 Parse64(string input) + { + var match64 = Steam64Regex.Match(input); + if (match64.Success) + { + return new SteamId64(long.Parse(match64.Value)); + } + else + { + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId64."); + } + + } + + public static SteamId2 Parse2(string input) + { + var match2 = Steam2Regex.Match(input); + if (match2.Success) + { + var universe = byte.Parse(match2.Groups["universe"].Value); + var lowestBit = byte.Parse(match2.Groups["lowestBit"].Value); + var highestBits = int.Parse(match2.Groups["highestBits"].Value); + return new SteamId2(universe, lowestBit, highestBits); + } + else + { + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId2."); + } + } + + public static SteamId3 Parse3(string input) + { + var match3 = Steam3Regex.Match(input); + if (match3.Success) + { + var type = match3.Groups["type"].Value[0]; + var id = int.Parse(match3.Groups["id"].Value); + return new SteamId3(id, type); + + } + else + { + throw new FormatException($"The input string '{input}' was not in a correct format or not real SteamId3."); + } + } + + #endregion + +} \ No newline at end of file diff --git a/SteamLibForked/Utility/Utilities.cs b/SteamLibForked/Utility/Utilities.cs new file mode 100644 index 0000000..76c7046 --- /dev/null +++ b/SteamLibForked/Utility/Utilities.cs @@ -0,0 +1,28 @@ +using System.Text.RegularExpressions; + +namespace SteamLib.Utility; + +public static class Utilities //TODO: refactor +{ + public static int GetSuccessCode(string response) + { + Regex regexInt = new("\"success\":(\\d*)"); + var matchInt = regexInt.Match(response); + + if (!string.IsNullOrEmpty(matchInt.Groups[1].Value)) + return Convert.ToInt32(matchInt.Groups[1].Value); + + Regex regexBool = new("\"success\":(\\w*)"); + var matchBool = regexBool.Match(response); + + if (!string.IsNullOrEmpty(matchBool.Groups[1].Value)) + return bool.Parse(matchBool.Groups[1].Value) ? 1 : 0; + return 0; + } + + public static FormUrlEncodedContent ToFormContent(this IDictionary dic) => new(dic); //TODO: убрать и зарефакторить + + public static int DivideByDecimal(this int i, decimal value) => (int) (i / value); + public static int DivideByDouble(this int i, double value) => (int)(i / value); + +} \ No newline at end of file diff --git a/SteamLibForked/Web/ClientBuilder.cs b/SteamLibForked/Web/ClientBuilder.cs new file mode 100644 index 0000000..65505d9 --- /dev/null +++ b/SteamLibForked/Web/ClientBuilder.cs @@ -0,0 +1,51 @@ +using System.Net; +using System.Net.Http.Headers; +using AchiesUtilities.Web.Models; +using SteamLib.Authentication; +using SteamLib.Core.Interfaces; + +namespace SteamLib.Web; + +public static class ClientBuilder +{ + public static HttpClientHandlerPair BuildMobileClient(IWebProxy? proxy, IMobileSessionData? sessionData, bool disposeHandler = true) + { + sessionData?.EnsureValidated(); + var handler = new HttpClientHandler(); + var client = new HttpClient(handler, disposeHandler); + + client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/javascript, text/html, application/xml, text/xml, */*"); + client.DefaultRequestHeaders.UserAgent.ParseAdd("okhttp/3.12.12"); + + if (proxy != null) + { + handler.Proxy = proxy; + } + + var container = handler.CookieContainer; + if (sessionData == null) + { + container.ClearMobileSessionCookies(); + } + else + { + container.SetSteamMobileCookies(sessionData); + } + + ConfigureCommon(handler, client); + return new HttpClientHandlerPair(client, handler); + + } + + + private static void ConfigureCommon(HttpClientHandler handler, HttpClient client) + { + ConfigureCommonClient(client); + handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; + } + private static void ConfigureCommonClient(HttpClient client) + { + client.Timeout = TimeSpan.FromSeconds(50); + client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US")); + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Converters/SteamIdConverters.cs b/SteamLibForked/Web/Converters/SteamIdConverters.cs new file mode 100644 index 0000000..a0ada56 --- /dev/null +++ b/SteamLibForked/Web/Converters/SteamIdConverters.cs @@ -0,0 +1,106 @@ +using Newtonsoft.Json; +using SteamLib.Account; + +namespace SteamLib.Web.Converters; + +public class SteamIdToSteam64Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId value, JsonSerializer serializer) + { + writer.WriteValue(value.Steam64.ToLong()); + } + + public override SteamId ReadJson(JsonReader reader, Type objectType, SteamId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.Value is long l) + { + return SteamId.FromSteam64(l); + } + + var str = (string) reader.Value!; + return new SteamId(SteamId64.Parse(str)); + } +} + +public class SteamIdToSteam2Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId value, JsonSerializer serializer) + { + writer.WriteValue(value.Steam2.ToString()); + } + + public override SteamId ReadJson(JsonReader reader, Type objectType, SteamId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + + var str = (string)reader.Value!; + return new SteamId(SteamId2.Parse(str)); + } +} + + +public class SteamIdToSteam3Converter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId value, JsonSerializer serializer) + { + writer.WriteValue(value.Steam3.ToString()); + } + + public override SteamId ReadJson(JsonReader reader, Type objectType, SteamId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + + var str = (string)reader.Value!; + return new SteamId(SteamId3.Parse(str)); + } +} + +public class Steam64ToLongConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId64 value, JsonSerializer serializer) + { + writer.WriteValue(value.ToLong()); + } + + public override SteamId64 ReadJson(JsonReader reader, Type objectType, SteamId64 existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.Value is long l) + { + return new SteamId64(l); + } + var str = (string)reader.Value!; + return SteamId64.Parse(str); + } +} + +public class SteamId2ToStringConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId2 value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override SteamId2 ReadJson(JsonReader reader, Type objectType, SteamId2 existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var str = (string)reader.Value!; + return SteamId2.Parse(str); + } +} + +public class SteamId3ToStringConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, SteamId3 value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override SteamId3 ReadJson(JsonReader reader, Type objectType, SteamId3 existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var str = (string)reader.Value!; + return SteamId3.Parse(str); + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs b/SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs new file mode 100644 index 0000000..24ff267 --- /dev/null +++ b/SteamLibForked/Web/Converters/StringToLongIfNeededConverter.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace SteamLib.Web.Converters; + +public class StringToLongIfNeededConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, string? value, JsonSerializer serializer) + { + writer.WriteValue(value); + } + + public override string ReadJson(JsonReader reader, Type objectType, string? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value?.ToString() ?? "null"; + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/Account/UserWalletSchema.cs b/SteamLibForked/Web/Models/Account/UserWalletSchema.cs new file mode 100644 index 0000000..9a65fcd --- /dev/null +++ b/SteamLibForked/Web/Models/Account/UserWalletSchema.cs @@ -0,0 +1,15 @@ +using AchiesUtilities.Newtonsoft.JSON.Converters.Common; +using Newtonsoft.Json; + +namespace SteamLib.Web.Models.Account; + +#pragma warning disable CS8618 +public class UserWalletSchema +{ + [JsonProperty("amount")] + [JsonConverter(typeof(IntToStringConverter))] + public int Amount { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/Account/WalletSchema.cs b/SteamLibForked/Web/Models/Account/WalletSchema.cs new file mode 100644 index 0000000..1134134 --- /dev/null +++ b/SteamLibForked/Web/Models/Account/WalletSchema.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +#pragma warning disable CS8618 + +namespace SteamLib.Web.Models.Account; + +public class WalletInfoSchema +{ + [JsonProperty("success")] + public int Success { get; set; } + + [JsonProperty("currency")] + public string Currency { get; set; } + + [JsonProperty("country_code")] + public string CountryCode { get; set; } + + [JsonProperty("alternate_min_amount")] + public bool AlternateMinAmount { get; set; } + + [JsonProperty("amounts")] + public List Amounts { get; set; } + + [JsonProperty("related_trans_type")] + public bool RelatedTransType { get; set; } + + [JsonProperty("related_trainsid")] + public bool RelatedTrainsId { get; set; } + + [JsonProperty("user_wallet")] + public UserWalletSchema UserWalletSchema { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs b/SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs new file mode 100644 index 0000000..92556e9 --- /dev/null +++ b/SteamLibForked/Web/Models/GlobalMarketInfo/GlobalInfoModel.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +#pragma warning disable CS8618 + +namespace SteamLib.Web.Models.GlobalMarketInfo; + +[PublicAPI] +public class GlobalInfoModel +{ + [MemberNotNullWhen(true, nameof(SteamId))] + public bool IsLoggedIn { get; set; } + + /// + /// if is or wallet doesn't exist + /// + public MarketWalletSchema? WalletInfo { get; set; } + public bool RequiresBillingInfo { get; set; } + public bool HasBillingStates { get; set; } + public string CountryCode { get; set; } + public string Language { get; set; } + public long? SteamId { get; set; } + public string SessionId { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs b/SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs new file mode 100644 index 0000000..f11a5f9 --- /dev/null +++ b/SteamLibForked/Web/Models/GlobalMarketInfo/MarketWalletSchema.cs @@ -0,0 +1,61 @@ +using AchiesUtilities.Newtonsoft.JSON.Converters.Common; +using Newtonsoft.Json; +using SteamLib.Core.Enums; + +#pragma warning disable CS8618 + +namespace SteamLib.Web.Models.GlobalMarketInfo; + +public class MarketWalletSchema +{ + [JsonProperty("wallet_currency")] + public Currency WalletCurrency { get; set; } + + [JsonProperty("wallet_country")] + public string WalletCountry { get; set; } + + [JsonProperty("wallet_state")] + public string WalletState { get; set; } + + [JsonProperty("wallet_fee")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletFee { get; set; } + + [JsonProperty("wallet_fee_minimum")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletFeeMinimum { get; set; } + + [JsonProperty("wallet_fee_percent")] + [JsonConverter(typeof(StringToDoubleConverter))] + public double WalletFeePercent { get; set; } + + [JsonProperty("wallet_publisher_fee_percent_default")] + [JsonConverter(typeof(StringToDoubleConverter))] + public double WalletPublisherFeePercentDefault { get; set; } + + [JsonProperty("wallet_fee_base")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletFeeBase { get; set; } + + [JsonProperty("wallet_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletBalance { get; set; } + + [JsonProperty("wallet_delayed_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletDelayedBalance { get; set; } + + [JsonProperty("wallet_max_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletMaxBalance { get; set; } + + [JsonProperty("wallet_trade_max_balance")] + [JsonConverter(typeof(IntToStringConverter))] + public int WalletTradeMaxBalance { get; set; } + + [JsonProperty("success")] + public int Success { get; set; } + + [JsonProperty("rwgrsn")] + public int Rwgrsn { get; set; } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Models/Mobile/Confirmations.cs b/SteamLibForked/Web/Models/Mobile/Confirmations.cs new file mode 100644 index 0000000..6ea4788 --- /dev/null +++ b/SteamLibForked/Web/Models/Mobile/Confirmations.cs @@ -0,0 +1,41 @@ +using AchiesUtilities.Models; +using Newtonsoft.Json; +using SteamLib.SteamMobile.Confirmations; + +namespace SteamLib.Web.Models.Mobile; + +public class ConfirmationsJson +{ + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("needauth")] + public bool NeedAuth { get; set; } + + [JsonProperty("message")] + public string? Message { get; set; } + + + [JsonProperty("detail")] + public string? Detail { get; set; } + [JsonProperty("conf")] public List Conf { get; set; } = new(); + +} + + +public class ConfirmationJson +{ + [JsonProperty("type")] public ConfirmationType Type { get; set; } + [JsonProperty("type_name")] public string TypeName { get; set; } = string.Empty; + [JsonProperty("id")] public long Id { get; set; } + [JsonProperty("creator_id")] public long CreatorId { get; set; } + [JsonProperty("nonce")] public ulong Nonce { get; set; } + [JsonProperty("creation_time")] public UnixTimeStamp CreationTime { get; set; } + [JsonProperty("cancel")] public string Cancel { get; set; } = string.Empty; + [JsonProperty("accept")] public string Confirm { get; set; } = string.Empty; + [JsonProperty("icon")] public Uri? Icon { get; set; } + [JsonProperty("multi")] public bool Multi { get; set; } + [JsonProperty("headline")] public string Headline { get; set; } = string.Empty; + [JsonProperty("summary")] public List Summary { get; set; } = null!; + [JsonProperty("warn")] public List Warn { get; set; } = null!; +} diff --git a/SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs b/SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs new file mode 100644 index 0000000..216b324 --- /dev/null +++ b/SteamLibForked/Web/Scrappers/HTML/MarketGlobalInfoScrapper.cs @@ -0,0 +1,80 @@ +using System.Text.RegularExpressions; +using HtmlAgilityPack; +using Newtonsoft.Json; +using SteamLib.Web.Models.GlobalMarketInfo; + +namespace SteamLib.Web.Scrappers.HTML; + +public static class MarketGlobalInfoScrapper +{ + private static readonly Regex LoggedInRegex = new("var g_bLoggedIn = (.+);", RegexOptions.Compiled); + private static readonly Regex ReqBillingInfoRegex = new("var g_bRequiresBillingInfo = (.+);", RegexOptions.Compiled); + private static readonly Regex CountryCodeRegex = new("var g_strCountryCode = \"(.+)\";", RegexOptions.Compiled); + private static readonly Regex HasBillingStates = new("var g_bHasBillingStates = (.+);", RegexOptions.Compiled); + private static readonly Regex LanguageRegex = new("var g_strLanguage = \"(.+)\";", RegexOptions.Compiled); + private static readonly Regex WalletInfoRegex = new("var g_rgWalletInfo = (.+);", RegexOptions.Compiled); + + private static readonly Regex SessionIdRegex = new("g_sessionID = \"(.+)\";", RegexOptions.Compiled); + private static readonly Regex SteamIdRegex = new("g_steamID = \"(.+)\";", RegexOptions.Compiled); + + public static GlobalInfoModel Scrap(string html) + { + var document = new HtmlDocument(); + document.LoadHtml(html); + + var scriptNodes = document.DocumentNode.SelectNodes("//*[@id=\"responsive_page_template_content\"]/script"); + var index = scriptNodes.Count > 2 ? 1 : 0; //If account is limited or market unavailable elements will be displaced + + var scriptNode = scriptNodes[index]; + var script = scriptNode.InnerText!; + + var logged = GetBool(script, LoggedInRegex); + var hasBillingStates = GetBool(script, HasBillingStates); + var reqBillingInfo = GetBool(script, ReqBillingInfoRegex); + + var country = CountryCodeRegex.Match(html).Groups[1].Value; + var language = LanguageRegex.Match(html).Groups[1].Value; + + + var walletInfoStr = WalletInfoRegex.Match(html).Groups[1].Value; + MarketWalletSchema? wallet = null; + + var sessionScriptNode = document.DocumentNode.SelectSingleNode($"//div[@class='responsive_page_content']/script"); + var sessionScript = sessionScriptNode.InnerText!; + + var sessionId = SessionIdRegex.Match(sessionScript).Groups[1].Value; + long? steamId = null; + + + + if (logged) + { + wallet = JsonConvert.DeserializeObject(walletInfoStr); + var steamIdStr = SteamIdRegex.Match(sessionScript).Groups[1].Value; + steamId = long.Parse(new string(steamIdStr.Where(char.IsDigit).ToArray())); + } + + + + return new GlobalInfoModel + { + CountryCode = country, + HasBillingStates = hasBillingStates, + IsLoggedIn = logged, + Language = language, + RequiresBillingInfo = reqBillingInfo, + WalletInfo = wallet, + SessionId = sessionId, + SteamId = steamId + }; + + + + } + private static bool GetBool(string script, Regex regex) + { + var value = regex.Match(script).Groups[1].Value; + return bool.Parse(value); + } + +} \ No newline at end of file diff --git a/SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs b/SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs new file mode 100644 index 0000000..2b4f2fd --- /dev/null +++ b/SteamLibForked/Web/Scrappers/JSON/MobileConfirmationScrapper.cs @@ -0,0 +1,137 @@ +using Newtonsoft.Json; +using SteamLib.Exceptions; +using SteamLib.Exceptions.General; +using SteamLib.SteamMobile.Confirmations; +using SteamLib.Web.Models.Mobile; + +namespace SteamLib.Web.Scrappers.JSON; + +public static class MobileConfirmationScrapper +{ + public static Dictionary ErrorMessages { get; } = new() + { + {"Oh nooooooes!", LoadConfirmationsError.TryAgainLater}, + {"You are not set up to receive mobile confirmations", LoadConfirmationsError.NotSetupToReceiveConfirmations} + }; + + public static List Scrap(string response) + { + ConfirmationsJson conf; + try + { + conf = JsonConvert.DeserializeObject(response, StaticJson.DefaultSettings)!; + } + catch (Exception ex) + { + throw new UnsupportedResponseException(response, ex); + } + + if (conf.NeedAuth) + { + throw new SessionExpiredException(); + } + + if (conf is {Success: false, Message: not null}) + { + var error = LoadConfirmationsError.Unknown; + if (ErrorMessages.TryGetValue(conf.Message, out var e)) + { + error = e; + } + + throw new CantLoadConfirmationsException(conf.Message) + { + Error = error + }; + + } + + if (conf.Success == false) + { + throw new UnsupportedResponseException(response); + } + + + var result = new List(); + if (conf.Conf.Count == 0) return result; + + + foreach (var confirmationJson in conf.Conf) + { + result.Add(GetConcrete(confirmationJson)); + } + + return result; + } + + + private static Confirmation GetConcrete(ConfirmationJson json) + { + return json.Type switch + { + ConfirmationType.Trade => GetTradeConfirmation(json), + ConfirmationType.AccountRecovery => GetAccountRecoveryConfirmation(json), + ConfirmationType.MarketSellTransaction => GetMarketConfirmation(json), + ConfirmationType.RegisterApiKey => GetRegisterApiKeyConfirmation(json), + _ => new Confirmation(json.Id, json.Nonce, (int)json.Type, json.CreatorId, json.Type, json.TypeName) + { + Time = json.CreationTime.ToLocalDateTime() + }, + }; + } + + private static TradeConfirmation GetTradeConfirmation(ConfirmationJson json) + { + + var avatarUri = json.Icon; + + var userName = json.Headline; + + + var receiveNodeText = string.Empty; + if (json.Summary.Count > 0) + { + receiveNodeText = json.Summary[1]; + } + + var receiveNothing = receiveNodeText == "You will receive nothing"; + + + return new TradeConfirmation(json.Id, json.Nonce, json.CreatorId, json.TypeName) + { + UserAvatarUri = avatarUri!.ToString(), + UserName = userName, + IsReceiveNothing = receiveNothing, + Time = json.CreationTime.ToLocalDateTime() + }; + } + + private static AccountRecoveryConfirmation GetAccountRecoveryConfirmation(ConfirmationJson confirmation) + { + return new AccountRecoveryConfirmation(confirmation.Id, confirmation.Nonce, confirmation.CreatorId, confirmation.TypeName) + { + Time = confirmation.CreationTime.ToLocalDateTime() + }; + } + private static RegisterApiKeyConfirmation GetRegisterApiKeyConfirmation(ConfirmationJson confirmation) + { + return new RegisterApiKeyConfirmation(confirmation.Id, confirmation.Nonce, confirmation.CreatorId, confirmation.TypeName) + { + Time = confirmation.CreationTime.ToLocalDateTime() + }; + } + + private static MarketConfirmation GetMarketConfirmation(ConfirmationJson confirmation) //TODO: test this + { + + var itemName = confirmation.Headline; + var itemImageUri = confirmation.Icon?.ToString() ?? string.Empty; + return new MarketConfirmation(confirmation.Id, confirmation.Nonce, confirmation.CreatorId, confirmation.TypeName) + { + Time = confirmation.CreationTime.ToLocalDateTime(), + ItemImageUri = itemImageUri, + ItemName = itemName, + PriceString = string.Empty //TODO: + }; + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/Scrappers/JSON/StaticJson.cs b/SteamLibForked/Web/Scrappers/JSON/StaticJson.cs new file mode 100644 index 0000000..4571186 --- /dev/null +++ b/SteamLibForked/Web/Scrappers/JSON/StaticJson.cs @@ -0,0 +1,18 @@ +using AchiesUtilities.Newtonsoft.JSON; +using Newtonsoft.Json; + +namespace SteamLib.Web.Scrappers.JSON; + +public static class StaticJson //TODO: remove? +{ + public static JsonSerializerSettings DefaultSettings { get; } + + + static StaticJson() + { + DefaultSettings = new JsonSerializerSettings + { + Converters = StaticConverters.AllDefaultConverters + }; + } +} \ No newline at end of file diff --git a/SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs b/SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs new file mode 100644 index 0000000..6f0a404 --- /dev/null +++ b/SteamLibForked/Web/SteamWebApi[folder]/SteamApi_Market.cs @@ -0,0 +1,28 @@ +using SteamLib.Core; +using SteamLib.Exceptions.General; +using SteamLib.Web.Models.GlobalMarketInfo; +using SteamLib.Web.Scrappers.HTML; + +namespace SteamLib.Web; + +public static partial class SteamWebApi +{ + /// + /// Login is not required + /// + /// + /// + /// + public static async Task GetMarketGlobalInfo(HttpClient client, CancellationToken cancellationToken = default) + { + var resp = await client.GetStringAsync(SteamConstants.STEAM_MARKET, cancellationToken); + try + { + return MarketGlobalInfoScrapper.Scrap(resp); + } + catch (Exception ex) + { + throw new UnsupportedResponseException(resp, ex); + } + } +} \ No newline at end of file