From 4c330ded167046bdbb4f595fcb2a14e1c1211229 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Fri, 12 Jan 2018 19:43:34 +0100 Subject: [PATCH] Closes #728 --- ArchiSteamFarm/BotDatabase.cs | 8 +- ArchiSteamFarm/GlobalConfig.cs | 3 + ArchiSteamFarm/MobileAuthenticator.cs | 183 +++++++++++++------------- ArchiSteamFarm/config/ASF.json | 1 + 4 files changed, 99 insertions(+), 96 deletions(-) diff --git a/ArchiSteamFarm/BotDatabase.cs b/ArchiSteamFarm/BotDatabase.cs index ac5cf6359..c3c291aa6 100644 --- a/ArchiSteamFarm/BotDatabase.cs +++ b/ArchiSteamFarm/BotDatabase.cs @@ -61,13 +61,7 @@ namespace ArchiSteamFarm { // This constructor is used only by deserializer private BotDatabase() { } - public void Dispose() { - // Those are objects that are always being created if constructor doesn't throw exception - FileSemaphore.Dispose(); - - // Those are objects that might be null and the check should be in-place - MobileAuthenticator?.Dispose(); - } + public void Dispose() => FileSemaphore.Dispose(); internal async Task AddBlacklistedFromTradesSteamIDs(IReadOnlyCollection steamIDs) { if ((steamIDs == null) || (steamIDs.Count == 0)) { diff --git a/ArchiSteamFarm/GlobalConfig.cs b/ArchiSteamFarm/GlobalConfig.cs index 73f1ce510..00c0ec1ae 100644 --- a/ArchiSteamFarm/GlobalConfig.cs +++ b/ArchiSteamFarm/GlobalConfig.cs @@ -46,6 +46,9 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] internal readonly HashSet Blacklist = new HashSet(); + [JsonProperty(Required = Required.DisallowNull)] + internal readonly byte ConfirmationsLimiterDelay = 10; + [JsonProperty(Required = Required.DisallowNull)] internal readonly byte ConnectionTimeout = DefaultConnectionTimeout; diff --git a/ArchiSteamFarm/MobileAuthenticator.cs b/ArchiSteamFarm/MobileAuthenticator.cs index 5330244fb..e064f495a 100644 --- a/ArchiSteamFarm/MobileAuthenticator.cs +++ b/ArchiSteamFarm/MobileAuthenticator.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -31,12 +32,14 @@ using HtmlAgilityPack; using Newtonsoft.Json; namespace ArchiSteamFarm { - internal sealed class MobileAuthenticator : IDisposable { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + internal sealed class MobileAuthenticator { private const byte CodeDigits = 5; private const byte CodeInterval = 30; private const byte SteamTimeTTL = 24; // For how many hours we can assume that SteamTimeDifference is correct private static readonly char[] CodeCharacters = { '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y' }; + private static readonly SemaphoreSlim ConfirmationsSemaphore = new SemaphoreSlim(1, 1); private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1, 1); private static DateTime LastSteamTimeCheck; @@ -45,8 +48,6 @@ namespace ArchiSteamFarm { // "ERROR" is being used by SteamDesktopAuthenticator internal bool HasCorrectDeviceID => !string.IsNullOrEmpty(DeviceID) && !DeviceID.Equals("ERROR"); - private readonly SemaphoreSlim ConfirmationsSemaphore = new SemaphoreSlim(1, 1); - #pragma warning disable 649 [JsonProperty(PropertyName = "identity_secret", Required = Required.Always)] private readonly string IdentitySecret; @@ -64,8 +65,6 @@ namespace ArchiSteamFarm { private MobileAuthenticator() { } - public void Dispose() => ConfirmationsSemaphore.Dispose(); - internal bool CorrectDeviceID(string deviceID) { if (string.IsNullOrEmpty(deviceID)) { Bot.ArchiLogger.LogNullError(nameof(deviceID)); @@ -135,60 +134,73 @@ namespace ArchiSteamFarm { return null; } - HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetConfirmations(DeviceID, confirmationHash, time).ConfigureAwait(false); + await ConfirmationsSemaphore.WaitAsync().ConfigureAwait(false); - HtmlNodeCollection confirmationNodes = htmlDocument?.DocumentNode.SelectNodes("//div[@class='mobileconf_list_entry']"); - if (confirmationNodes == null) { - return null; - } + try { + HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetConfirmations(DeviceID, confirmationHash, time).ConfigureAwait(false); - HashSet result = new HashSet(); - - foreach (HtmlNode confirmationNode in confirmationNodes) { - string idString = confirmationNode.GetAttributeValue("data-confid", null); - if (string.IsNullOrEmpty(idString)) { - Bot.ArchiLogger.LogNullError(nameof(idString)); + HtmlNodeCollection confirmationNodes = htmlDocument?.DocumentNode.SelectNodes("//div[@class='mobileconf_list_entry']"); + if (confirmationNodes == null) { return null; } - if (!uint.TryParse(idString, out uint id) || (id == 0)) { - Bot.ArchiLogger.LogNullError(nameof(id)); - return null; + HashSet result = new HashSet(); + + foreach (HtmlNode confirmationNode in confirmationNodes) { + string idString = confirmationNode.GetAttributeValue("data-confid", null); + if (string.IsNullOrEmpty(idString)) { + Bot.ArchiLogger.LogNullError(nameof(idString)); + return null; + } + + if (!uint.TryParse(idString, out uint id) || (id == 0)) { + Bot.ArchiLogger.LogNullError(nameof(id)); + return null; + } + + string keyString = confirmationNode.GetAttributeValue("data-key", null); + if (string.IsNullOrEmpty(keyString)) { + Bot.ArchiLogger.LogNullError(nameof(keyString)); + return null; + } + + if (!ulong.TryParse(keyString, out ulong key) || (key == 0)) { + Bot.ArchiLogger.LogNullError(nameof(key)); + return null; + } + + HtmlNode descriptionNode = confirmationNode.SelectSingleNode(".//div[@class='mobileconf_list_entry_description']/div"); + if (descriptionNode == null) { + Bot.ArchiLogger.LogNullError(nameof(descriptionNode)); + return null; + } + + Steam.ConfirmationDetails.EType type; + + string description = descriptionNode.InnerText; + if (description.StartsWith("Sell - ", StringComparison.Ordinal)) { + type = Steam.ConfirmationDetails.EType.Market; + } else if (description.StartsWith("Trade with ", StringComparison.Ordinal) || description.Equals("Error loading trade details")) { + type = Steam.ConfirmationDetails.EType.Trade; + } else { + Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(description), description)); + type = Steam.ConfirmationDetails.EType.Other; + } + + result.Add(new Confirmation(id, key, type)); } - string keyString = confirmationNode.GetAttributeValue("data-key", null); - if (string.IsNullOrEmpty(keyString)) { - Bot.ArchiLogger.LogNullError(nameof(keyString)); - return null; - } - - if (!ulong.TryParse(keyString, out ulong key) || (key == 0)) { - Bot.ArchiLogger.LogNullError(nameof(key)); - return null; - } - - HtmlNode descriptionNode = confirmationNode.SelectSingleNode(".//div[@class='mobileconf_list_entry_description']/div"); - if (descriptionNode == null) { - Bot.ArchiLogger.LogNullError(nameof(descriptionNode)); - return null; - } - - Steam.ConfirmationDetails.EType type; - - string description = descriptionNode.InnerText; - if (description.StartsWith("Sell - ", StringComparison.Ordinal)) { - type = Steam.ConfirmationDetails.EType.Market; - } else if (description.StartsWith("Trade with ", StringComparison.Ordinal) || description.Equals("Error loading trade details")) { - type = Steam.ConfirmationDetails.EType.Trade; + return result; + } finally { + if (Program.GlobalConfig.ConfirmationsLimiterDelay == 0) { + ConfirmationsSemaphore.Release(); } else { - Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(description), description)); - type = Steam.ConfirmationDetails.EType.Other; + Task.Run(async () => { + await Task.Delay(Program.GlobalConfig.ConfirmationsLimiterDelay * 1000).ConfigureAwait(false); + ConfirmationsSemaphore.Release(); + }).Forget(); } - - result.Add(new Confirmation(id, key, type)); } - - return result; } internal async Task HandleConfirmations(IReadOnlyCollection confirmations, bool accept) { @@ -202,47 +214,40 @@ namespace ArchiSteamFarm { return false; } - await ConfirmationsSemaphore.WaitAsync().ConfigureAwait(false); - - try { - uint time = await GetSteamTime().ConfigureAwait(false); - if (time == 0) { - Bot.ArchiLogger.LogNullError(nameof(time)); - return false; - } - - string confirmationHash = GenerateConfirmationKey(time, "conf"); - if (string.IsNullOrEmpty(confirmationHash)) { - Bot.ArchiLogger.LogNullError(nameof(confirmationHash)); - return false; - } - - bool? result = await Bot.ArchiWebHandler.HandleConfirmations(DeviceID, confirmationHash, time, confirmations, accept).ConfigureAwait(false); - if (!result.HasValue) { - // Request timed out - return false; - } - - if (result.Value) { - // Request succeeded - return true; - } - - // Our multi request failed, this is almost always Steam fuckup that happens randomly - // In this case, we'll accept all pending confirmations one-by-one, synchronously (as Steam can't handle them in parallel) - // We totally ignore actual result returned by those calls, abort only if request timed out - - foreach (Confirmation confirmation in confirmations) { - bool? confirmationResult = await Bot.ArchiWebHandler.HandleConfirmation(DeviceID, confirmationHash, time, confirmation.ID, confirmation.Key, accept).ConfigureAwait(false); - if (!confirmationResult.HasValue) { - return false; - } - } - - return true; - } finally { - ConfirmationsSemaphore.Release(); + uint time = await GetSteamTime().ConfigureAwait(false); + if (time == 0) { + Bot.ArchiLogger.LogNullError(nameof(time)); + return false; } + + string confirmationHash = GenerateConfirmationKey(time, "conf"); + if (string.IsNullOrEmpty(confirmationHash)) { + Bot.ArchiLogger.LogNullError(nameof(confirmationHash)); + return false; + } + + bool? result = await Bot.ArchiWebHandler.HandleConfirmations(DeviceID, confirmationHash, time, confirmations, accept).ConfigureAwait(false); + if (!result.HasValue) { + // Request timed out + return false; + } + + if (result.Value) { + // Request succeeded + return true; + } + + // Our multi request failed, this is almost always Steam fuckup that happens randomly + // In this case, we'll accept all pending confirmations one-by-one, synchronously (as Steam can't handle them in parallel) + // We totally ignore actual result returned by those calls, abort only if request timed out + foreach (Confirmation confirmation in confirmations) { + bool? confirmationResult = await Bot.ArchiWebHandler.HandleConfirmation(DeviceID, confirmationHash, time, confirmation.ID, confirmation.Key, accept).ConfigureAwait(false); + if (!confirmationResult.HasValue) { + return false; + } + } + + return true; } internal void Init(Bot bot) => Bot = bot ?? throw new ArgumentNullException(nameof(bot)); diff --git a/ArchiSteamFarm/config/ASF.json b/ArchiSteamFarm/config/ASF.json index ca74065f4..d2958024d 100644 --- a/ArchiSteamFarm/config/ASF.json +++ b/ArchiSteamFarm/config/ASF.json @@ -2,6 +2,7 @@ "AutoRestart": true, "BackgroundGCPeriod": 0, "Blacklist": [], + "ConfirmationsLimiterDelay": 10, "ConnectionTimeout": 60, "CurrentCulture": null, "Debug": false,