From d7e871033348a31a80d927c0ef051f4e0600ea98 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 11 Feb 2023 15:58:15 +0100 Subject: [PATCH] Do not announce/match with limited accounts, lockdowns and trade bans, improve ArchiCacheable We can totally make use of success previously more often --- .../RemoteCommunication.cs | 26 ++++- .../SteamTokenDumperPlugin.cs | 2 +- ArchiSteamFarm/Core/ArchiNet.cs | 2 +- ArchiSteamFarm/Helpers/ArchiCacheable.cs | 23 ++-- ArchiSteamFarm/Helpers/ECacheFallback.cs | 28 +++++ ArchiSteamFarm/Steam/Bot.cs | 10 +- .../Steam/Integration/ArchiWebHandler.cs | 100 ++++++++++++++++-- .../Steam/Security/MobileAuthenticator.cs | 8 +- 8 files changed, 165 insertions(+), 34 deletions(-) create mode 100644 ArchiSteamFarm/Helpers/ECacheFallback.cs diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index a86856fc1..d27caa423 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -29,6 +29,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; using ArchiSteamFarm.Steam; @@ -493,6 +494,20 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { } private async Task IsEligibleForMatching() { + // Bot can't be limited + if (Bot.IsAccountLimited) { + Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.IsAccountLimited)}: {Bot.IsAccountLimited}")); + + return false; + } + + // Bot can't be on lockdown + if (Bot.IsAccountLocked) { + Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.IsAccountLocked)}: {Bot.IsAccountLocked}")); + + return false; + } + // Bot must have ASF 2FA if (!Bot.HasMobileAuthenticator) { Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.HasMobileAuthenticator)}: {Bot.HasMobileAuthenticator}")); @@ -513,7 +528,16 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { if (hasValidApiKey != true) { Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(Bot.ArchiWebHandler.HasValidApiKey)}: {hasValidApiKey?.ToString() ?? "null"}")); - return hasValidApiKey; + return hasValidApiKey.HasValue ? false : null; + } + + // Bot can't be trade banned + (bool _, bool? Result) economyBan = await Bot.ArchiWebHandler.CachedEconomyBan.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); + + if (economyBan.Result != false) { + Bot.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(economyBan)}: {economyBan.Result?.ToString() ?? "null"}")); + + return economyBan.Result.HasValue ? false : null; } return true; diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs index 5624d766d..29068d9f7 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs @@ -405,7 +405,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalAppAccessTokens, appIDsToRefresh.Count)); bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingTotalDepots, appIDsToRefresh.Count)); - (_, ImmutableHashSet? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ArchiCacheable>.EFallback.SuccessPreviously).ConfigureAwait(false); + (_, ImmutableHashSet? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); using (HashSet.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) { while (true) { diff --git a/ArchiSteamFarm/Core/ArchiNet.cs b/ArchiSteamFarm/Core/ArchiNet.cs index 9cc76960a..4de0fd64c 100644 --- a/ArchiSteamFarm/Core/ArchiNet.cs +++ b/ArchiSteamFarm/Core/ArchiNet.cs @@ -69,7 +69,7 @@ internal static class ArchiNet { throw new ArgumentOutOfRangeException(nameof(steamID)); } - (_, IReadOnlyCollection? badBots) = await CachedBadBots.GetValue(ArchiCacheable>.EFallback.FailedNow).ConfigureAwait(false); + (_, IReadOnlyCollection? badBots) = await CachedBadBots.GetValue(ECacheFallback.FailedNow).ConfigureAwait(false); return badBots?.Contains(steamID); } diff --git a/ArchiSteamFarm/Helpers/ArchiCacheable.cs b/ArchiSteamFarm/Helpers/ArchiCacheable.cs index 2fd4e1c9e..36faa462d 100644 --- a/ArchiSteamFarm/Helpers/ArchiCacheable.cs +++ b/ArchiSteamFarm/Helpers/ArchiCacheable.cs @@ -47,9 +47,9 @@ public sealed class ArchiCacheable : IDisposable { public void Dispose() => InitSemaphore.Dispose(); [PublicAPI] - public async Task<(bool Success, T? Result)> GetValue(EFallback fallback = EFallback.DefaultForType) { - if (!Enum.IsDefined(fallback)) { - throw new InvalidEnumArgumentException(nameof(fallback), (int) fallback, typeof(EFallback)); + public async Task<(bool Success, T? Result)> GetValue(ECacheFallback cacheFallback = ECacheFallback.DefaultForType) { + if (!Enum.IsDefined(cacheFallback)) { + throw new InvalidEnumArgumentException(nameof(cacheFallback), (int) cacheFallback, typeof(ECacheFallback)); } if (IsInitialized && IsRecent) { @@ -66,11 +66,11 @@ public sealed class ArchiCacheable : IDisposable { (bool success, T? result) = await ResolveFunction().ConfigureAwait(false); if (!success) { - return fallback switch { - EFallback.DefaultForType => (false, default(T?)), - EFallback.FailedNow => (false, result), - EFallback.SuccessPreviously => (false, InitializedValue), - _ => throw new InvalidOperationException(nameof(fallback)) + return cacheFallback switch { + ECacheFallback.DefaultForType => (false, default(T?)), + ECacheFallback.FailedNow => (false, result), + ECacheFallback.SuccessPreviously => (false, InitializedValue), + _ => throw new InvalidOperationException(nameof(cacheFallback)) }; } @@ -97,15 +97,8 @@ public sealed class ArchiCacheable : IDisposable { } InitializedAt = DateTime.MinValue; - InitializedValue = default(T?); } finally { InitSemaphore.Release(); } } - - public enum EFallback : byte { - DefaultForType, - FailedNow, - SuccessPreviously - } } diff --git a/ArchiSteamFarm/Helpers/ECacheFallback.cs b/ArchiSteamFarm/Helpers/ECacheFallback.cs new file mode 100644 index 000000000..4384a6e1a --- /dev/null +++ b/ArchiSteamFarm/Helpers/ECacheFallback.cs @@ -0,0 +1,28 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// 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. + +namespace ArchiSteamFarm.Helpers; + +public enum ECacheFallback : byte { + DefaultForType, + FailedNow, + SuccessPreviously +} diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 6cee0dfc3..e39730f0b 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -119,6 +119,14 @@ public sealed class Bot : IAsyncDisposable, IDisposable { [PublicAPI] public bool HasMobileAuthenticator => BotDatabase.MobileAuthenticator != null; + [JsonIgnore] + [PublicAPI] + public bool IsAccountLimited => AccountFlags.HasFlag(EAccountFlags.LimitedUser) || AccountFlags.HasFlag(EAccountFlags.LimitedUserForce); + + [JsonIgnore] + [PublicAPI] + public bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown); + [JsonProperty] [PublicAPI] public bool IsConnectedAndLoggedOn => SteamClient.SteamID != null; @@ -140,8 +148,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable { public SteamFriends SteamFriends { get; } internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked; - internal bool IsAccountLimited => AccountFlags.HasFlag(EAccountFlags.LimitedUser) || AccountFlags.HasFlag(EAccountFlags.LimitedUserForce); - internal bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown); private readonly CallbackManager CallbackManager; private readonly SemaphoreSlim CallbackSemaphore = new(1, 1); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 16b41604c..c5298c0e2 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -54,6 +54,7 @@ public sealed class ArchiWebHandler : IDisposable { private const byte MinimumSessionValidityInSeconds = 10; private const string SteamAppsService = "ISteamApps"; private const string SteamUserAuthService = "ISteamUserAuth"; + private const string SteamUserService = "ISteamAuth"; private const string TwoFactorService = "ITwoFactorService"; [PublicAPI] @@ -73,6 +74,9 @@ public sealed class ArchiWebHandler : IDisposable { [PublicAPI] public ArchiCacheable CachedApiKey { get; } + [PublicAPI] + public ArchiCacheable CachedEconomyBan { get; } + [PublicAPI] public WebBrowser WebBrowser { get; } @@ -88,8 +92,9 @@ public sealed class ArchiWebHandler : IDisposable { internal ArchiWebHandler(Bot bot) { Bot = bot ?? throw new ArgumentNullException(nameof(bot)); - CachedApiKey = new ArchiCacheable(ResolveApiKey); - CachedAccessToken = new ArchiCacheable(ResolveAccessToken); + CachedAccessToken = new ArchiCacheable(ResolveAccessToken, TimeSpan.FromHours(6)); + CachedApiKey = new ArchiCacheable(ResolveApiKey, TimeSpan.FromHours(6)); + CachedEconomyBan = new ArchiCacheable(ResolveEconomyBan, TimeSpan.FromHours(6)); WebBrowser = new WebBrowser(bot.ArchiLogger, ASF.GlobalConfig?.WebProxy); } @@ -97,6 +102,7 @@ public sealed class ArchiWebHandler : IDisposable { public void Dispose() { CachedApiKey.Dispose(); CachedAccessToken.Dispose(); + CachedEconomyBan.Dispose(); SessionSemaphore.Dispose(); WebBrowser.Dispose(); } @@ -301,9 +307,9 @@ public sealed class ArchiWebHandler : IDisposable { [PublicAPI] public async Task GetPointsBalance() { - (bool success, string? accessToken) = await CachedAccessToken.GetValue().ConfigureAwait(false); + (_, string? accessToken) = await CachedAccessToken.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); - if (!success || string.IsNullOrEmpty(accessToken)) { + if (string.IsNullOrEmpty(accessToken)) { return null; } @@ -366,9 +372,9 @@ public sealed class ArchiWebHandler : IDisposable { [PublicAPI] public async Task?> GetTradeOffers(bool? activeOnly = null, bool? receivedOffers = null, bool? sentOffers = null, bool? withDescriptions = null) { - (bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false); + (_, string? steamApiKey) = await CachedApiKey.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); - if (!success || string.IsNullOrEmpty(steamApiKey)) { + if (string.IsNullOrEmpty(steamApiKey)) { return null; } @@ -570,9 +576,9 @@ public sealed class ArchiWebHandler : IDisposable { [PublicAPI] public async Task HasValidApiKey() { - (bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false); + (_, string? steamApiKey) = await CachedApiKey.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); - return success ? !string.IsNullOrEmpty(steamApiKey) : null; + return !string.IsNullOrEmpty(steamApiKey); } [PublicAPI] @@ -1787,9 +1793,9 @@ public sealed class ArchiWebHandler : IDisposable { throw new ArgumentOutOfRangeException(nameof(steamID)); } - (bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false); + (_, string? steamApiKey) = await CachedApiKey.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); - if (!success || string.IsNullOrEmpty(steamApiKey)) { + if (string.IsNullOrEmpty(steamApiKey)) { return null; } @@ -2351,7 +2357,10 @@ public sealed class ArchiWebHandler : IDisposable { internal void OnDisconnected() { Initialized = false; + + Utilities.InBackground(CachedAccessToken.Reset); Utilities.InBackground(CachedApiKey.Reset); + Utilities.InBackground(CachedEconomyBan.Reset); } internal void OnVanityURLChanged(string? vanityURL = null) => VanityURL = !string.IsNullOrEmpty(vanityURL) ? vanityURL : null; @@ -2751,6 +2760,77 @@ public sealed class ArchiWebHandler : IDisposable { } } + private async Task<(bool Success, bool? Result)> ResolveEconomyBan() { + (_, string? steamApiKey) = await CachedApiKey.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); + + if (string.IsNullOrEmpty(steamApiKey)) { + return (false, null); + } + + Dictionary arguments = new(2, StringComparer.Ordinal) { + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + { "key", steamApiKey! }, + + { "steamids", Bot.SteamID } + }; + + KeyValue? response = null; + + for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) { + if ((i > 0) && (WebLimiterDelay > 0)) { + await Task.Delay(WebLimiterDelay).ConfigureAwait(false); + } + + using WebAPI.AsyncInterface service = Bot.SteamConfiguration.GetAsyncWebAPIInterface(SteamUserService); + + service.Timeout = WebBrowser.Timeout; + + try { + response = await WebLimitRequest( + WebAPI.DefaultBaseAddress, + + // ReSharper disable once AccessToDisposedClosure + async () => await service.CallAsync(HttpMethod.Get, "GetPlayerBans", args: arguments).ConfigureAwait(false) + ).ConfigureAwait(false); + } catch (TaskCanceledException e) { + Bot.ArchiLogger.LogGenericDebuggingException(e); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericWarningException(e); + } + } + + if (response == null) { + Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)); + + return (false, null); + } + + List players = response["players"].Children; + + if (players.Count != 1) { + Bot.ArchiLogger.LogNullError(players); + + return (false, null); + } + + string? economyBanText = players[0]["EconomyBan"].AsString(); + + switch (economyBanText) { + case null: + Bot.ArchiLogger.LogNullError(economyBanText); + + return (false, null); + case "none": + return (true, false); + case "banned": + return (true, true); + default: + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(economyBanText), economyBanText)); + + return (false, null); + } + } + private async Task UnlockParentalAccount(string parentalCode) { if (string.IsNullOrEmpty(parentalCode)) { throw new ArgumentNullException(nameof(parentalCode)); diff --git a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs index 723e2d16c..e4594760d 100644 --- a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs +++ b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs @@ -147,9 +147,9 @@ public sealed class MobileAuthenticator : IDisposable { throw new InvalidOperationException(nameof(Bot)); } - (bool success, string? deviceID) = await CachedDeviceID.GetValue().ConfigureAwait(false); + (_, string? deviceID) = await CachedDeviceID.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); - if (!success || string.IsNullOrEmpty(deviceID)) { + if (string.IsNullOrEmpty(deviceID)) { Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(deviceID))); return null; @@ -301,9 +301,9 @@ public sealed class MobileAuthenticator : IDisposable { throw new InvalidOperationException(nameof(Bot)); } - (bool success, string? deviceID) = await CachedDeviceID.GetValue().ConfigureAwait(false); + (_, string? deviceID) = await CachedDeviceID.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false); - if (!success || string.IsNullOrEmpty(deviceID)) { + if (string.IsNullOrEmpty(deviceID)) { Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(deviceID))); return false;