diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 200c54878961d..1d2294df409d8 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -19,6 +19,12 @@
"allowedVersions": "<= 3.1",
"matchManagers": [ "nuget" ],
"matchPackageNames": [ "Microsoft.Extensions.Configuration.Json", "Microsoft.Extensions.Logging.Configuration" ]
+ },
+ {
+ // TODO: Stick with SK2 version until we can remove the submodule
+ "enabled": false,
+ "matchManagers": [ "nuget" ],
+ "matchPackageNames": [ "Microsoft.Win32.Registry", "protobuf-net" ]
}
]
}
diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj
index e2717031a294e..81725cbc9261f 100644
--- a/ArchiSteamFarm/ArchiSteamFarm.csproj
+++ b/ArchiSteamFarm/ArchiSteamFarm.csproj
@@ -13,16 +13,21 @@
+
-
+
+
+
+ Temp\SteamKit2.dll
+
diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs
index a4de903c6171d..a564550b768a0 100644
--- a/ArchiSteamFarm/Core/Utilities.cs
+++ b/ArchiSteamFarm/Core/Utilities.cs
@@ -112,7 +112,7 @@ public static async void InBackground(Action action, bool longRunning = false) {
public static void InBackground(Func function, bool longRunning = false) {
ArgumentNullException.ThrowIfNull(function);
- InBackground(void () => function(), longRunning);
+ InBackground(void() => function(), longRunning);
}
[PublicAPI]
diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs
index e39730f0befbc..1cf416265683a 100644
--- a/ArchiSteamFarm/Steam/Bot.cs
+++ b/ArchiSteamFarm/Steam/Bot.cs
@@ -157,6 +157,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
private readonly SemaphoreSlim MessagingSemaphore = new(1, 1);
private readonly ConcurrentDictionary PastNotifications = new();
private readonly SemaphoreSlim SendCompleteTypesSemaphore = new(1, 1);
+ private readonly SteamAuthentication SteamAuthentication;
private readonly SteamClient SteamClient;
private readonly ConcurrentHashSet SteamFamilySharingIDs = new();
private readonly SteamUser SteamUser;
@@ -178,11 +179,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
}
- ///
- /// Login keys are not guaranteed to be valid, we should use them only if we don't have full details available from the user
- ///
- private bool ShouldUseLoginKeys => BotConfig.UseLoginKeys && (!BotConfig.IsSteamPasswordSet || !HasMobileAuthenticator);
-
[JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")]
private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture);
@@ -302,6 +298,8 @@ private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) {
CallbackManager.Subscribe(OnGuestPassList);
CallbackManager.Subscribe(OnLicenseList);
+ SteamAuthentication = SteamClient.GetHandler() ?? throw new InvalidOperationException(nameof(SteamAuthentication));
+
SteamFriends = SteamClient.GetHandler() ?? throw new InvalidOperationException(nameof(SteamFriends));
CallbackManager.Subscribe(OnFriendsList);
CallbackManager.Subscribe(OnPersonaState);
@@ -311,7 +309,6 @@ private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) {
SteamUser = SteamClient.GetHandler() ?? throw new InvalidOperationException(nameof(SteamUser));
CallbackManager.Subscribe(OnLoggedOff);
CallbackManager.Subscribe(OnLoggedOn);
- CallbackManager.Subscribe(OnLoginKey);
CallbackManager.Subscribe(OnMachineAuth);
CallbackManager.Subscribe(OnVanityURLChangedCallback);
CallbackManager.Subscribe(OnWalletUpdate);
@@ -1637,6 +1634,57 @@ internal async Task Rename(string newBotName) {
return true;
}
+ internal async Task RequestInput(ASF.EUserInputType inputType, bool previousCodeWasIncorrect) {
+ if ((inputType == ASF.EUserInputType.None) || !Enum.IsDefined(inputType)) {
+ throw new InvalidEnumArgumentException(nameof(inputType), (int) inputType, typeof(ASF.EUserInputType));
+ }
+
+ switch (inputType) {
+ case ASF.EUserInputType.SteamGuard when !string.IsNullOrEmpty(AuthCode):
+ string? savedAuthCode = AuthCode;
+
+ AuthCode = null;
+
+ return savedAuthCode;
+ case ASF.EUserInputType.TwoFactorAuthentication when !string.IsNullOrEmpty(TwoFactorCode):
+ string? savedTwoFactorCode = TwoFactorCode;
+
+ TwoFactorCode = null;
+
+ return savedTwoFactorCode;
+ case ASF.EUserInputType.TwoFactorAuthentication when BotDatabase.MobileAuthenticator != null:
+ if (previousCodeWasIncorrect) {
+ // There is a possibility that our cached time is no longer appropriate, so we should reset the cache in this case in order to fetch it upon the next login attempt
+ // Yes, this might as well be just invalid 2FA credentials, but we can't be sure about that, and we have LoginFailures designed to verify that for us
+ await MobileAuthenticator.ResetSteamTimeDifference().ConfigureAwait(false);
+ }
+
+ string? generatedTwoFactorCode = await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(generatedTwoFactorCode)) {
+ return generatedTwoFactorCode;
+ }
+
+ break;
+ }
+
+ RequiredInput = inputType;
+
+ string? input = await Logging.GetUserInput(inputType, BotName).ConfigureAwait(false);
+
+ // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
+ if (string.IsNullOrEmpty(input) || !SetUserInput(inputType, input!)) {
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(input)));
+
+ Stop();
+
+ return null;
+ }
+
+ // We keep user input set in case we need to use it again due to disconnection, OnLoggedOn() will reset it for us
+ return input;
+ }
+
internal void RequestPersonaStateUpdate() {
if (!IsConnectedAndLoggedOn) {
return;
@@ -1938,6 +1986,102 @@ private async Task HandleCallbacks() {
}
}
+ private async Task HandleLoginResult(EResult result, EResult extendedResult) {
+ if (!Enum.IsDefined(result)) {
+ throw new InvalidEnumArgumentException(nameof(result), (int) result, typeof(EResult));
+ }
+
+ if (!Enum.IsDefined(extendedResult)) {
+ throw new InvalidEnumArgumentException(nameof(extendedResult), (int) extendedResult, typeof(EResult));
+ }
+
+ // Keep LastLogOnResult for OnDisconnected()
+ LastLogOnResult = result;
+
+ HeartBeatFailures = 0;
+ StopConnectionFailureTimer();
+
+ switch (result) {
+ case EResult.AccountDisabled:
+ // Those failures are permanent, we should Stop() the bot if any of those happen
+ ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, result, extendedResult));
+ Stop();
+
+ break;
+ case EResult.AccountLogonDenied:
+ case EResult.InvalidLoginAuthCode:
+ RequiredInput = ASF.EUserInputType.SteamGuard;
+
+ string? authCode = await Logging.GetUserInput(ASF.EUserInputType.SteamGuard, BotName).ConfigureAwait(false);
+
+ // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
+ if (string.IsNullOrEmpty(authCode) || !SetUserInput(ASF.EUserInputType.SteamGuard, authCode!)) {
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(authCode)));
+
+ Stop();
+ }
+
+ break;
+ case EResult.AccountLoginDeniedNeedTwoFactor when !HasMobileAuthenticator:
+ case EResult.TwoFactorCodeMismatch when !HasMobileAuthenticator:
+ RequiredInput = ASF.EUserInputType.TwoFactorAuthentication;
+
+ string? twoFactorCode = await Logging.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName).ConfigureAwait(false);
+
+ // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
+ if (string.IsNullOrEmpty(twoFactorCode) || !SetUserInput(ASF.EUserInputType.TwoFactorAuthentication, twoFactorCode!)) {
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(twoFactorCode)));
+
+ Stop();
+ }
+
+ break;
+ case EResult.AccountLoginDeniedNeedTwoFactor:
+ case EResult.InvalidPassword:
+ case EResult.NoConnection:
+ case EResult.PasswordRequiredToKickSession: // Not sure about this one, it seems to be just generic "try again"? #694
+ case EResult.RateLimitExceeded:
+ case EResult.ServiceUnavailable:
+ case EResult.Timeout:
+ case EResult.TryAnotherCM:
+ case EResult.TwoFactorCodeMismatch:
+ ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, result, extendedResult));
+
+ switch (result) {
+ case EResult.AccountLoginDeniedNeedTwoFactor:
+ case EResult.TwoFactorCodeMismatch:
+ // There is a possibility that our cached time is no longer appropriate, so we should reset the cache in this case in order to fetch it upon the next login attempt
+ // Yes, this might as well be just invalid 2FA credentials, but we can't be sure about that, and we have LoginFailures designed to verify that for us
+ await MobileAuthenticator.ResetSteamTimeDifference().ConfigureAwait(false);
+
+ if (++LoginFailures >= MaxLoginFailures) {
+ LoginFailures = 0;
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidAuthenticatorDuringLogin, MaxLoginFailures));
+ Stop();
+ }
+
+ break;
+ case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.RefreshToken) && (++LoginFailures >= MaxLoginFailures):
+ LoginFailures = 0;
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidPasswordDuringLogin, MaxLoginFailures));
+ Stop();
+
+ break;
+ }
+
+ break;
+ case EResult.OK:
+ break;
+ default:
+ // Unexpected result, shutdown immediately
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(result), result));
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, result, extendedResult));
+ Stop();
+
+ break;
+ }
+ }
+
private async void HeartBeat(object? state = null) {
if (!KeepRunning || !IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) {
return;
@@ -2265,22 +2409,17 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) {
}
}
- string? loginKey = null;
-
- if (ShouldUseLoginKeys && string.IsNullOrEmpty(AuthCode) && string.IsNullOrEmpty(TwoFactorCode)) {
- loginKey = BotDatabase.LoginKey;
+ string? refreshToken = BotDatabase.RefreshToken;
- // Decrypt login key if needed
- // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
- if (!string.IsNullOrEmpty(loginKey) && (loginKey!.Length > 19) && BotConfig.PasswordFormat.HasTransformation()) {
- loginKey = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey).ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(refreshToken)) {
+ // Decrypt refreshToken if needed
+ if (BotConfig.PasswordFormat.HasTransformation()) {
+ // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
+ refreshToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, refreshToken!).ConfigureAwait(false);
}
- } else {
- // If we're not using login keys, ensure we don't have any saved
- BotDatabase.LoginKey = null;
}
- if (!await InitLoginAndPassword(string.IsNullOrEmpty(loginKey)).ConfigureAwait(false)) {
+ if (!await InitLoginAndPassword(string.IsNullOrEmpty(refreshToken)).ConfigureAwait(false)) {
Stop();
return;
@@ -2318,22 +2457,57 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) {
ArchiLogger.LogGenericInfo(Strings.BotLoggingIn);
- if (string.IsNullOrEmpty(TwoFactorCode) && (BotDatabase.MobileAuthenticator != null)) {
- // We should always include 2FA token, even if it's not required
- TwoFactorCode = await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false);
- }
-
InitConnectionFailureTimer();
+ if (string.IsNullOrEmpty(refreshToken)) {
+ SteamAuthentication.AuthPollResult pollResponse;
+
+ try {
+ using CancellationTokenSource authCancellationTokenSource = new();
+
+ SteamAuthentication.CredentialsAuthSession authSession = await SteamAuthentication.BeginAuthSessionViaCredentials(
+ new SteamAuthentication.AuthSessionDetails {
+ Authenticator = new BotCredentialsProvider(this, authCancellationTokenSource),
+ DeviceFriendlyName = SharedInfo.PublicIdentifier,
+ GuardData = BotConfig.UseLoginKeys ? BotDatabase.SteamGuardData : null,
+ IsPersistentSession = true,
+ Password = password,
+ Username = username
+ }
+ ).ConfigureAwait(false);
+
+ pollResponse = await authSession.StartPolling(authCancellationTokenSource.Token).ConfigureAwait(false);
+ } catch (AuthenticationException e) {
+ ArchiLogger.LogGenericWarningException(e);
+
+ await HandleLoginResult(e.Result, e.Result).ConfigureAwait(false);
+
+ ReconnectOnUserInitiated = true;
+ SteamClient.Disconnect();
+
+ return;
+ } catch (OperationCanceledException) {
+ // This is okay, we already took care of that and can ignore it here
+ return;
+ }
+
+ refreshToken = pollResponse.RefreshToken;
+
+ if (BotConfig.UseLoginKeys) {
+ BotDatabase.RefreshToken = BotConfig.PasswordFormat.HasTransformation() ? ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken) : refreshToken;
+
+ if (!string.IsNullOrEmpty(pollResponse.NewGuardData)) {
+ BotDatabase.SteamGuardData = pollResponse.NewGuardData;
+ }
+ }
+ }
+
SteamUser.LogOnDetails logOnDetails = new() {
- AuthCode = AuthCode,
+ AccessToken = refreshToken,
CellID = ASF.GlobalDatabase?.CellID,
LoginID = LoginID,
- LoginKey = loginKey,
- Password = password,
SentryFileHash = sentryFileHash,
- ShouldRememberPassword = ShouldUseLoginKeys,
- TwoFactorCode = TwoFactorCode,
+ ShouldRememberPassword = BotConfig.UseLoginKeys,
Username = username
};
@@ -2380,9 +2554,9 @@ private async void OnDisconnected(SteamClient.DisconnectedCallback callback) {
case EResult.AccountDisabled:
// Do not attempt to reconnect, those failures are permanent
return;
- case EResult.InvalidPassword when !string.IsNullOrEmpty(BotDatabase.LoginKey):
+ case EResult.InvalidPassword when !string.IsNullOrEmpty(BotDatabase.RefreshToken):
// We can retry immediately
- BotDatabase.LoginKey = null;
+ BotDatabase.RefreshToken = null;
ArchiLogger.LogGenericInfo(Strings.BotRemovedExpiredLoginKey);
break;
@@ -2681,6 +2855,7 @@ private async void OnLicenseList(SteamApps.LicenseListCallback callback) {
private void OnLoggedOff(SteamUser.LoggedOffCallback callback) {
ArgumentNullException.ThrowIfNull(callback);
+ // Keep LastLogOnResult for OnDisconnected()
LastLogOnResult = callback.Result;
ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOff, callback.Result));
@@ -2716,231 +2891,133 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
// Always reset one-time-only access tokens when we get OnLoggedOn() response
AuthCode = TwoFactorCode = null;
- // Keep LastLogOnResult for OnDisconnected()
- LastLogOnResult = callback.Result;
-
- HeartBeatFailures = 0;
- StopConnectionFailureTimer();
-
- switch (callback.Result) {
- case EResult.AccountDisabled:
- // Those failures are permanent, we should Stop() the bot if any of those happen
- ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult));
- Stop();
-
- break;
- case EResult.AccountLogonDenied:
- case EResult.InvalidLoginAuthCode:
- RequiredInput = ASF.EUserInputType.SteamGuard;
-
- string? authCode = await Logging.GetUserInput(ASF.EUserInputType.SteamGuard, BotName).ConfigureAwait(false);
-
- // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
- if (string.IsNullOrEmpty(authCode) || !SetUserInput(ASF.EUserInputType.SteamGuard, authCode!)) {
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(authCode)));
-
- Stop();
- }
-
- break;
- case EResult.AccountLoginDeniedNeedTwoFactor when !HasMobileAuthenticator:
- case EResult.TwoFactorCodeMismatch when !HasMobileAuthenticator:
- RequiredInput = ASF.EUserInputType.TwoFactorAuthentication;
-
- string? twoFactorCode = await Logging.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName).ConfigureAwait(false);
-
- // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
- if (string.IsNullOrEmpty(twoFactorCode) || !SetUserInput(ASF.EUserInputType.TwoFactorAuthentication, twoFactorCode!)) {
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(twoFactorCode)));
-
- Stop();
- }
-
- break;
- case EResult.OK:
- AccountFlags = callback.AccountFlags;
- SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID));
-
- ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}"));
+ await HandleLoginResult(callback.Result, callback.ExtendedResult).ConfigureAwait(false);
- // Old status for these doesn't matter, we'll update them if needed
- LoginFailures = 0;
- LibraryLocked = PlayingBlocked = false;
-
- if (PlayingWasBlocked && (PlayingWasBlockedTimer == null)) {
- InitPlayingWasBlockedTimer();
- }
+ if (callback.Result != EResult.OK) {
+ return;
+ }
- if (IsAccountLimited) {
- ArchiLogger.LogGenericWarning(Strings.BotAccountLimited);
- }
+ AccountFlags = callback.AccountFlags;
+ SteamID = callback.ClientSteamID ?? throw new InvalidOperationException(nameof(callback.ClientSteamID));
- if (IsAccountLocked) {
- ArchiLogger.LogGenericWarning(Strings.BotAccountLocked);
- }
+ ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotLoggedOn, $"{SteamID}{(!string.IsNullOrEmpty(callback.VanityURL) ? $"/{callback.VanityURL}" : "")}"));
- if ((callback.CellID != 0) && (ASF.GlobalDatabase != null) && (callback.CellID != ASF.GlobalDatabase.CellID)) {
- ASF.GlobalDatabase.CellID = callback.CellID;
- }
+ // Old status for these doesn't matter, we'll update them if needed
+ LoginFailures = 0;
+ LibraryLocked = PlayingBlocked = false;
- // Handle steamID-based maFile
- if (!HasMobileAuthenticator) {
- string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, $"{SteamID}{SharedInfo.MobileAuthenticatorExtension}");
+ if (PlayingWasBlocked && (PlayingWasBlockedTimer == null)) {
+ InitPlayingWasBlockedTimer();
+ }
- if (File.Exists(maFilePath)) {
- await ImportAuthenticatorFromFile(maFilePath).ConfigureAwait(false);
- }
- }
+ if (IsAccountLimited) {
+ ArchiLogger.LogGenericWarning(Strings.BotAccountLimited);
+ }
- if (callback.ParentalSettings != null) {
- (SteamParentalActive, string? steamParentalCode) = ValidateSteamParental(callback.ParentalSettings, BotConfig.SteamParentalCode, Program.SteamParentalGeneration);
+ if (IsAccountLocked) {
+ ArchiLogger.LogGenericWarning(Strings.BotAccountLocked);
+ }
- if (SteamParentalActive) {
- // Steam parental enabled
- if (!string.IsNullOrEmpty(steamParentalCode)) {
- // We were able to automatically generate it, potentially with help of the config
- if (BotConfig.SteamParentalCode != steamParentalCode) {
- // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
- if (!SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) {
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode)));
+ if ((callback.CellID != 0) && (ASF.GlobalDatabase != null) && (callback.CellID != ASF.GlobalDatabase.CellID)) {
+ ASF.GlobalDatabase.CellID = callback.CellID;
+ }
- Stop();
+ // Handle steamID-based maFile
+ if (!HasMobileAuthenticator) {
+ string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, $"{SteamID}{SharedInfo.MobileAuthenticatorExtension}");
- break;
- }
- }
- } else {
- // We failed to generate the pin ourselves, ask the user
- RequiredInput = ASF.EUserInputType.SteamParentalCode;
+ if (File.Exists(maFilePath)) {
+ await ImportAuthenticatorFromFile(maFilePath).ConfigureAwait(false);
+ }
+ }
- steamParentalCode = await Logging.GetUserInput(ASF.EUserInputType.SteamParentalCode, BotName).ConfigureAwait(false);
+ if (callback.ParentalSettings != null) {
+ (SteamParentalActive, string? steamParentalCode) = ValidateSteamParental(callback.ParentalSettings, BotConfig.SteamParentalCode, Program.SteamParentalGeneration);
- // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
- if (string.IsNullOrEmpty(steamParentalCode) || !SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) {
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode)));
+ if (SteamParentalActive) {
+ // Steam parental enabled
+ if (!string.IsNullOrEmpty(steamParentalCode)) {
+ // We were able to automatically generate it, potentially with help of the config
+ if (BotConfig.SteamParentalCode != steamParentalCode) {
+ // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
+ if (!SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) {
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode)));
- Stop();
+ Stop();
- break;
- }
+ return;
}
}
} else {
- // Steam parental disabled
- SteamParentalActive = false;
- }
+ // We failed to generate the pin ourselves, ask the user
+ RequiredInput = ASF.EUserInputType.SteamParentalCode;
- ArchiWebHandler.OnVanityURLChanged(callback.VanityURL);
+ steamParentalCode = await Logging.GetUserInput(ASF.EUserInputType.SteamParentalCode, BotName).ConfigureAwait(false);
- if (!await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce ?? throw new InvalidOperationException(nameof(callback.WebAPIUserNonce)), SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
- if (!await RefreshSession().ConfigureAwait(false)) {
- break;
- }
- }
+ // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
+ if (string.IsNullOrEmpty(steamParentalCode) || !SetUserInput(ASF.EUserInputType.SteamParentalCode, steamParentalCode!)) {
+ ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(steamParentalCode)));
- // Pre-fetch API key for future usage if possible
- Utilities.InBackground(ArchiWebHandler.HasValidApiKey);
+ Stop();
- if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground) {
- Utilities.InBackground(() => RedeemGamesInBackground());
+ return;
+ }
}
+ }
+ } else {
+ // Steam parental disabled
+ SteamParentalActive = false;
+ }
- ArchiHandler.SetCurrentMode(BotConfig.UserInterfaceMode);
- ArchiHandler.RequestItemAnnouncements();
-
- // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly
- RequestPersonaStateUpdate();
-
- Utilities.InBackground(InitializeFamilySharing);
-
- ResetPersonaState();
-
- if (BotConfig.SteamMasterClanID != 0) {
- Utilities.InBackground(
- async () => {
- if (!await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false)) {
- ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
- }
-
- await JoinMasterChatGroupID().ConfigureAwait(false);
- }
- );
- }
+ ArchiWebHandler.OnVanityURLChanged(callback.VanityURL);
- if (BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) {
- Utilities.InBackground(() => ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID));
- }
+ if (!await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce ?? throw new InvalidOperationException(nameof(callback.WebAPIUserNonce)), SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) {
+ if (!await RefreshSession().ConfigureAwait(false)) {
+ return;
+ }
+ }
- if (CardsFarmer.Paused) {
- // Emit initial game playing status in this case
- Utilities.InBackground(ResetGamesPlayed);
- }
+ // Pre-fetch API key for future usage if possible
+ Utilities.InBackground(ArchiWebHandler.HasValidApiKey);
- SteamPICSChanges.OnBotLoggedOn();
+ if ((GamesRedeemerInBackgroundTimer == null) && BotDatabase.HasGamesToRedeemInBackground) {
+ Utilities.InBackground(() => RedeemGamesInBackground());
+ }
- await PluginsCore.OnBotLoggedOn(this).ConfigureAwait(false);
+ ArchiHandler.SetCurrentMode(BotConfig.UserInterfaceMode);
+ ArchiHandler.RequestItemAnnouncements();
- break;
- case EResult.AccountLoginDeniedNeedTwoFactor:
- case EResult.InvalidPassword:
- case EResult.NoConnection:
- case EResult.PasswordRequiredToKickSession: // Not sure about this one, it seems to be just generic "try again"? #694
- case EResult.RateLimitExceeded:
- case EResult.ServiceUnavailable:
- case EResult.Timeout:
- case EResult.TryAnotherCM:
- case EResult.TwoFactorCodeMismatch:
- ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult));
+ // Sometimes Steam won't send us our own PersonaStateCallback, so request it explicitly
+ RequestPersonaStateUpdate();
- switch (callback.Result) {
- case EResult.AccountLoginDeniedNeedTwoFactor:
- case EResult.TwoFactorCodeMismatch:
- // There is a possibility that our cached time is no longer appropriate, so we should reset the cache in this case in order to fetch it upon the next login attempt
- // Yes, this might as well be just invalid 2FA credentials, but we can't be sure about that, and we have LoginFailures designed to verify that for us
- await MobileAuthenticator.ResetSteamTimeDifference().ConfigureAwait(false);
+ Utilities.InBackground(InitializeFamilySharing);
- if (++LoginFailures >= MaxLoginFailures) {
- LoginFailures = 0;
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidAuthenticatorDuringLogin, MaxLoginFailures));
- Stop();
- }
+ ResetPersonaState();
- break;
- case EResult.InvalidPassword when string.IsNullOrEmpty(BotDatabase.LoginKey) && (++LoginFailures >= MaxLoginFailures):
- LoginFailures = 0;
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotInvalidPasswordDuringLogin, MaxLoginFailures));
- Stop();
+ if (BotConfig.SteamMasterClanID != 0) {
+ Utilities.InBackground(
+ async () => {
+ if (!await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false)) {
+ ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(ArchiWebHandler.JoinGroup)));
+ }
- break;
+ await JoinMasterChatGroupID().ConfigureAwait(false);
}
-
- break;
- default:
- // Unexpected result, shutdown immediately
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(callback.Result), callback.Result));
- ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, callback.Result, callback.ExtendedResult));
- Stop();
-
- break;
+ );
}
- }
-
- private void OnLoginKey(SteamUser.LoginKeyCallback callback) {
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(callback.LoginKey);
- if (!ShouldUseLoginKeys) {
- return;
+ if (BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.SteamGroup)) {
+ Utilities.InBackground(() => ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID));
}
- string? loginKey = callback.LoginKey;
-
- if (BotConfig.PasswordFormat.HasTransformation()) {
- loginKey = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey);
+ if (CardsFarmer.Paused) {
+ // Emit initial game playing status in this case
+ Utilities.InBackground(ResetGamesPlayed);
}
- BotDatabase.LoginKey = loginKey;
- SteamUser.AcceptNewLoginKey(callback);
+ SteamPICSChanges.OnBotLoggedOn();
+
+ await PluginsCore.OnBotLoggedOn(this).ConfigureAwait(false);
}
private async void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
diff --git a/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs b/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs
new file mode 100644
index 0000000000000..ee1cce833d71b
--- /dev/null
+++ b/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs
@@ -0,0 +1,80 @@
+// _ _ _ ____ _ _____
+// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
+// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
+// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
+// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
+// |
+// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
+// Contact: JustArchi@JustArchi.net
+// |
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// |
+// http://www.apache.org/licenses/LICENSE-2.0
+// |
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using ArchiSteamFarm.Core;
+using ArchiSteamFarm.Localization;
+using SteamKit2;
+
+namespace ArchiSteamFarm.Steam.Integration;
+
+internal sealed class BotCredentialsProvider : IAuthenticator {
+ private const byte MaxLoginFailures = 5;
+
+ private readonly Bot Bot;
+ private readonly CancellationTokenSource CancellationTokenSource;
+
+ private byte LoginFailures;
+
+ internal BotCredentialsProvider(Bot bot, CancellationTokenSource cancellationTokenSource) {
+ ArgumentNullException.ThrowIfNull(bot);
+ ArgumentNullException.ThrowIfNull(cancellationTokenSource);
+
+ Bot = bot;
+ CancellationTokenSource = cancellationTokenSource;
+ }
+
+ public Task AcceptDeviceConfirmation() => Task.FromResult(false);
+
+ public async Task ProvideDeviceCode(bool previousCodeWasIncorrect) => await ProvideInput(ASF.EUserInputType.TwoFactorAuthentication, previousCodeWasIncorrect).ConfigureAwait(false);
+
+ public async Task ProvideEmailCode(string email, bool previousCodeWasIncorrect) => await ProvideInput(ASF.EUserInputType.SteamGuard, previousCodeWasIncorrect).ConfigureAwait(false);
+
+ private async Task ProvideInput(ASF.EUserInputType inputType, bool previousCodeWasIncorrect) {
+ if (!Enum.IsDefined(inputType)) {
+ throw new InvalidEnumArgumentException(nameof(inputType), (int) inputType, typeof(ASF.EUserInputType));
+ }
+
+ if (previousCodeWasIncorrect && (++LoginFailures >= MaxLoginFailures)) {
+ EResult reason = inputType == ASF.EUserInputType.TwoFactorAuthentication ? EResult.TwoFactorCodeMismatch : EResult.InvalidLoginAuthCode;
+
+ Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.BotUnableToLogin, reason, reason));
+
+ if (++LoginFailures >= MaxLoginFailures) {
+ CancellationTokenSource.Cancel();
+
+ return "";
+ }
+ }
+
+ string? result = await Bot.RequestInput(inputType, previousCodeWasIncorrect).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(result)) {
+ CancellationTokenSource.Cancel();
+ }
+
+ return result ?? "";
+ }
+}
diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs
index df3f0c7edf321..6cebd9c186646 100644
--- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs
+++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs
@@ -62,38 +62,54 @@ internal uint GamesToRedeemInBackgroundCount {
[JsonProperty(Required = Required.DisallowNull)]
private readonly OrderedDictionary GamesToRedeemInBackground = new();
- internal string? LoginKey {
- get => BackingLoginKey;
+ internal MobileAuthenticator? MobileAuthenticator {
+ get => BackingMobileAuthenticator;
set {
- if (BackingLoginKey == value) {
+ if (BackingMobileAuthenticator == value) {
return;
}
- BackingLoginKey = value;
+ BackingMobileAuthenticator = value;
Utilities.InBackground(Save);
}
}
- internal MobileAuthenticator? MobileAuthenticator {
- get => BackingMobileAuthenticator;
+ internal string? RefreshToken {
+ get => BackingRefreshToken;
set {
- if (BackingMobileAuthenticator == value) {
+ if (BackingRefreshToken == value) {
return;
}
- BackingMobileAuthenticator = value;
+ BackingRefreshToken = value;
Utilities.InBackground(Save);
}
}
- [JsonProperty($"_{nameof(LoginKey)}")]
- private string? BackingLoginKey;
+ internal string? SteamGuardData {
+ get => BackingSteamGuardData;
+
+ set {
+ if (BackingSteamGuardData == value) {
+ return;
+ }
+
+ BackingSteamGuardData = value;
+ Utilities.InBackground(Save);
+ }
+ }
[JsonProperty($"_{nameof(MobileAuthenticator)}")]
private MobileAuthenticator? BackingMobileAuthenticator;
+ [JsonProperty]
+ private string? BackingRefreshToken;
+
+ [JsonProperty]
+ private string? BackingSteamGuardData;
+
private BotDatabase(string filePath) : this() {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
@@ -111,10 +127,13 @@ private BotDatabase() {
}
[UsedImplicitly]
- public bool ShouldSerializeBackingLoginKey() => !string.IsNullOrEmpty(BackingLoginKey);
+ public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null;
[UsedImplicitly]
- public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null;
+ public bool ShouldSerializeBackingRefreshToken() => !string.IsNullOrEmpty(BackingRefreshToken);
+
+ [UsedImplicitly]
+ public bool ShouldSerializeBackingSteamGuardData() => !string.IsNullOrEmpty(BackingSteamGuardData);
[UsedImplicitly]
public bool ShouldSerializeFarmingBlacklistAppIDs() => FarmingBlacklistAppIDs.Count > 0;
diff --git a/ArchiSteamFarm/Temp/SteamKit2.dll b/ArchiSteamFarm/Temp/SteamKit2.dll
new file mode 100644
index 0000000000000..205d64420020b
Binary files /dev/null and b/ArchiSteamFarm/Temp/SteamKit2.dll differ
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7cb7348f59643..2f7a9a73cd7f6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,11 +7,13 @@
+
+