From 3f91b18a4de07c0e41856e02611181a6bbea3593 Mon Sep 17 00:00:00 2001 From: Archi Date: Thu, 29 Jun 2023 22:34:26 +0200 Subject: [PATCH] Refactor confirmations Make it so the design actually follows what Steam gives us now. There is no need for standalone Confirmation object anymore, rather re-use what Steam gives us. Optimize parsing type, expose it as public API. Small breaking change in HandleConfirmations() action. --- .../RemoteCommunication.cs | 5 +- .../Api/TwoFactorAuthenticationController.cs | 3 +- ...actorAuthenticationConfirmationsRequest.cs | 3 +- .../{ConfirmationData.cs => Confirmation.cs} | 35 ++++++++--- .../Steam/Data/ConfirmationsResponse.cs | 9 +-- ArchiSteamFarm/Steam/Exchange/Trading.cs | 3 +- .../Steam/Integration/ArchiWebHandler.cs | 5 +- ArchiSteamFarm/Steam/Interaction/Actions.cs | 24 ++++---- ArchiSteamFarm/Steam/Security/Confirmation.cs | 61 ------------------- .../Steam/Security/MobileAuthenticator.cs | 36 +++-------- 10 files changed, 60 insertions(+), 124 deletions(-) rename ArchiSteamFarm/Steam/Data/{ConfirmationData.cs => Confirmation.cs} (71%) delete mode 100644 ArchiSteamFarm/Steam/Security/Confirmation.cs diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index fea83e392..1e0e84182 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -37,7 +37,6 @@ using ArchiSteamFarm.Steam.Cards; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; using ArchiSteamFarm.Steam.Integration; -using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; @@ -990,7 +989,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { pendingMobileTradeOfferIDs.UnionWith(mobileTradeOfferIDs); if (pendingMobileTradeOfferIDs.Count >= MaxTradeOffersActive) { - (bool twoFactorSuccess, IReadOnlyCollection? handledConfirmations, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, pendingMobileTradeOfferIDs, true).ConfigureAwait(false); + (bool twoFactorSuccess, IReadOnlyCollection? handledConfirmations, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EConfirmationType.Trade, pendingMobileTradeOfferIDs, true).ConfigureAwait(false); if (!twoFactorSuccess) { Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ActivelyMatchingSomeConfirmationsFailed, handledConfirmations?.Count ?? 0, pendingMobileTradeOfferIDs.Count)); @@ -1086,7 +1085,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { } if (pendingMobileTradeOfferIDs.Count > 0) { - (bool twoFactorSuccess, IReadOnlyCollection? handledConfirmations, _) = Bot.IsConnectedAndLoggedOn ? await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, pendingMobileTradeOfferIDs, true).ConfigureAwait(false) : (false, null, null); + (bool twoFactorSuccess, IReadOnlyCollection? handledConfirmations, _) = Bot.IsConnectedAndLoggedOn ? await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EConfirmationType.Trade, pendingMobileTradeOfferIDs, true).ConfigureAwait(false) : (false, null, null); if (!twoFactorSuccess) { Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ActivelyMatchingSomeConfirmationsFailed, handledConfirmations?.Count ?? 0, pendingMobileTradeOfferIDs.Count)); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/TwoFactorAuthenticationController.cs b/ArchiSteamFarm/IPC/Controllers/Api/TwoFactorAuthenticationController.cs index d2daa8652..1793cf402 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/TwoFactorAuthenticationController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/TwoFactorAuthenticationController.cs @@ -30,6 +30,7 @@ using ArchiSteamFarm.IPC.Requests; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; +using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Security; using Microsoft.AspNetCore.Mvc; @@ -51,7 +52,7 @@ public sealed class TwoFactorAuthenticationController : ArchiController { ArgumentNullException.ThrowIfNull(request); - if (request.AcceptedType.HasValue && ((request.AcceptedType.Value == Confirmation.EType.Unknown) || !Enum.IsDefined(request.AcceptedType.Value))) { + if (request.AcceptedType.HasValue && ((request.AcceptedType.Value == Confirmation.EConfirmationType.Unknown) || !Enum.IsDefined(request.AcceptedType.Value))) { return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(request.AcceptedType)))); } diff --git a/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs b/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs index 1b8d6a354..6c3838603 100644 --- a/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs @@ -27,6 +27,7 @@ using System.Globalization; using System.Linq; using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; +using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Security; using Newtonsoft.Json; @@ -50,7 +51,7 @@ public sealed class TwoFactorAuthenticationConfirmationsRequest { /// Specifies the type of confirmations to handle. If not provided, all confirmation types are considered for an action. /// [JsonProperty] - public Confirmation.EType? AcceptedType { get; private set; } + public Confirmation.EConfirmationType? AcceptedType { get; private set; } /// /// A helper property which works the same as but with values written as strings - for javascript compatibility purposes. Use either this one, or , not both. diff --git a/ArchiSteamFarm/Steam/Data/ConfirmationData.cs b/ArchiSteamFarm/Steam/Data/Confirmation.cs similarity index 71% rename from ArchiSteamFarm/Steam/Data/ConfirmationData.cs rename to ArchiSteamFarm/Steam/Data/Confirmation.cs index c85652415..1b632a11f 100644 --- a/ArchiSteamFarm/Steam/Data/ConfirmationData.cs +++ b/ArchiSteamFarm/Steam/Data/Confirmation.cs @@ -19,20 +19,39 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Data; -internal sealed class ConfirmationData { - [JsonProperty(PropertyName = "creator_id", Required = Required.Always)] - internal readonly ulong CreatorID; - - [JsonProperty(PropertyName = "id", Required = Required.Always)] - internal readonly ulong ID; - +[PublicAPI] +[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] +public sealed class Confirmation { [JsonProperty(PropertyName = "nonce", Required = Required.Always)] internal readonly ulong Nonce; [JsonProperty(PropertyName = "type", Required = Required.Always)] - internal readonly string TypeText = ""; + public EConfirmationType ConfirmationType { get; private set; } + + [JsonProperty(PropertyName = "creator_id", Required = Required.Always)] + public ulong CreatorID { get; private set; } + + [JsonProperty(PropertyName = "id", Required = Required.Always)] + public ulong ID { get; private set; } + + [JsonConstructor] + private Confirmation() { } + + [PublicAPI] + public enum EConfirmationType : byte { + Unknown, + Generic, + Trade, + Market, + + // We're missing information about definition of number 4 type + PhoneNumberChange = 5, + AccountRecovery = 6 + } } diff --git a/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs b/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs index a13aa264f..2bf7843a8 100644 --- a/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs +++ b/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs @@ -19,17 +19,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Collections.Generic; +using System.Collections.Immutable; using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Data; internal sealed class ConfirmationsResponse : BooleanResponse { - [JsonProperty("conf", Required = Required.DisallowNull)] - internal readonly HashSet? Confirmations; - - [JsonProperty("needauth", Required = Required.DisallowNull)] - internal readonly bool NeedAuthentication; + [JsonProperty("conf", Required = Required.Always)] + internal readonly ImmutableHashSet Confirmations = ImmutableHashSet.Empty; [JsonConstructor] private ConfirmationsResponse() { } diff --git a/ArchiSteamFarm/Steam/Exchange/Trading.cs b/ArchiSteamFarm/Steam/Exchange/Trading.cs index 1169ee9fa..eb55e0b4a 100644 --- a/ArchiSteamFarm/Steam/Exchange/Trading.cs +++ b/ArchiSteamFarm/Steam/Exchange/Trading.cs @@ -32,7 +32,6 @@ using ArchiSteamFarm.Localization; using ArchiSteamFarm.Plugins; using ArchiSteamFarm.Steam.Cards; using ArchiSteamFarm.Steam.Data; -using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using JetBrains.Annotations; @@ -417,7 +416,7 @@ public sealed class Trading : IDisposable { if (mobileTradeResults.Count > 0) { HashSet mobileTradeOfferIDs = mobileTradeResults.Select(static tradeOffer => tradeOffer.TradeOfferID).ToHashSet(); - (bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false); + (bool twoFactorSuccess, _, _) = await Bot.Actions.HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EConfirmationType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false); if (twoFactorSuccess) { foreach (ParseTradeResult mobileTradeResult in mobileTradeResults) { diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 0c17bf509..c3d365abc 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -36,7 +36,6 @@ using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; -using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; @@ -1856,7 +1855,7 @@ public sealed class ArchiWebHandler : IDisposable { return resultInSeconds == 0 ? (byte) 0 : (byte) (resultInSeconds / 86400); } - internal async Task GetConfirmationsPage(string deviceID, string confirmationHash, ulong time) { + internal async Task GetConfirmations(string deviceID, string confirmationHash, ulong time) { if (string.IsNullOrEmpty(deviceID)) { throw new ArgumentNullException(nameof(deviceID)); } @@ -2177,7 +2176,7 @@ public sealed class ArchiWebHandler : IDisposable { foreach (Confirmation confirmation in confirmations) { data.Add(new KeyValuePair("cid[]", confirmation.ID.ToString(CultureInfo.InvariantCulture))); - data.Add(new KeyValuePair("ck[]", confirmation.Key.ToString(CultureInfo.InvariantCulture))); + data.Add(new KeyValuePair("ck[]", confirmation.Nonce.ToString(CultureInfo.InvariantCulture))); } ObjectResponse? response = await UrlPostToJsonObjectWithSession(request, data: data).ConfigureAwait(false); diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 2c4540663..c17059554 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using System.Linq; @@ -33,7 +34,6 @@ using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; -using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; @@ -133,7 +133,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable { } [PublicAPI] - public async Task<(bool Success, IReadOnlyCollection? HandledConfirmations, string Message)> HandleTwoFactorAuthenticationConfirmations(bool accept, Confirmation.EType? acceptedType = null, IReadOnlyCollection? acceptedCreatorIDs = null, bool waitIfNeeded = false) { + public async Task<(bool Success, IReadOnlyCollection? HandledConfirmations, string Message)> HandleTwoFactorAuthenticationConfirmations(bool accept, Confirmation.EConfirmationType? acceptedType = null, IReadOnlyCollection? acceptedCreatorIDs = null, bool waitIfNeeded = false) { if (Bot.BotDatabase.MobileAuthenticator == null) { return (false, null, Strings.BotNoASFAuthenticator); } @@ -149,36 +149,38 @@ public sealed class Actions : IAsyncDisposable, IDisposable { await Task.Delay(1000).ConfigureAwait(false); } - HashSet? confirmations = await Bot.BotDatabase.MobileAuthenticator.GetConfirmations().ConfigureAwait(false); + ImmutableHashSet? confirmations = await Bot.BotDatabase.MobileAuthenticator.GetConfirmations().ConfigureAwait(false); if ((confirmations == null) || (confirmations.Count == 0)) { continue; } + HashSet remainingConfirmations = confirmations.ToHashSet(); + if (acceptedType.HasValue) { - if (confirmations.RemoveWhere(confirmation => confirmation.Type != acceptedType.Value) > 0) { - if (confirmations.Count == 0) { + if (remainingConfirmations.RemoveWhere(confirmation => confirmation.ConfirmationType != acceptedType.Value) > 0) { + if (remainingConfirmations.Count == 0) { continue; } } } if (acceptedCreatorIDs?.Count > 0) { - if (confirmations.RemoveWhere(confirmation => !acceptedCreatorIDs.Contains(confirmation.Creator)) > 0) { - if (confirmations.Count == 0) { + if (remainingConfirmations.RemoveWhere(confirmation => !acceptedCreatorIDs.Contains(confirmation.CreatorID)) > 0) { + if (remainingConfirmations.Count == 0) { continue; } } } - if (!await Bot.BotDatabase.MobileAuthenticator.HandleConfirmations(confirmations, accept).ConfigureAwait(false)) { + if (!await Bot.BotDatabase.MobileAuthenticator.HandleConfirmations(remainingConfirmations, accept).ConfigureAwait(false)) { return (false, handledConfirmations?.Values, Strings.WarningFailed); } handledConfirmations ??= new Dictionary(); - foreach (Confirmation? confirmation in confirmations) { - handledConfirmations[confirmation.Creator] = confirmation; + foreach (Confirmation? confirmation in remainingConfirmations) { + handledConfirmations[confirmation.CreatorID] = confirmation; } // We've accepted *something*, if caller didn't specify the IDs, that's enough for us @@ -343,7 +345,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable { (bool success, _, HashSet? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, items, token: tradeToken, itemsPerTrade: itemsPerTrade).ConfigureAwait(false); if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) { - (bool twoFactorSuccess, _, _) = await HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false); + (bool twoFactorSuccess, _, _) = await HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EConfirmationType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false); if (!twoFactorSuccess) { return (false, Strings.BotLootingFailed); diff --git a/ArchiSteamFarm/Steam/Security/Confirmation.cs b/ArchiSteamFarm/Steam/Security/Confirmation.cs deleted file mode 100644 index 6ed34c3e7..000000000 --- a/ArchiSteamFarm/Steam/Security/Confirmation.cs +++ /dev/null @@ -1,61 +0,0 @@ -// _ _ _ ____ _ _____ -// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ -// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ -// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | -// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | -// 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 JetBrains.Annotations; -using Newtonsoft.Json; - -namespace ArchiSteamFarm.Steam.Security; - -public sealed class Confirmation { - [JsonProperty(Required = Required.Always)] - public ulong Creator { get; } - - [JsonProperty(Required = Required.Always)] - public ulong ID { get; } - - [JsonProperty(Required = Required.Always)] - public ulong Key { get; } - - [JsonProperty(Required = Required.Always)] - public EType Type { get; } - - internal Confirmation(ulong id, ulong key, ulong creator, EType type) { - ID = id > 0 ? id : throw new ArgumentOutOfRangeException(nameof(id)); - Key = key > 0 ? key : throw new ArgumentOutOfRangeException(nameof(key)); - Creator = creator > 0 ? creator : throw new ArgumentOutOfRangeException(nameof(creator)); - Type = Enum.IsDefined(type) ? type : throw new InvalidEnumArgumentException(nameof(type), (int) type, typeof(EType)); - } - - // REF: Internal documentation - [PublicAPI] - public enum EType : byte { - Unknown, - Generic, - Trade, - Market, - - // We're missing information about definition of number 4 type - PhoneNumberChange = 5, - AccountRecovery = 6 - } -} diff --git a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs index d83dbe4d6..93021e4ba 100644 --- a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs +++ b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs @@ -24,6 +24,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -142,7 +143,7 @@ public sealed class MobileAuthenticator : IDisposable { ); } - internal async Task?> GetConfirmations() { + internal async Task?> GetConfirmations() { if (Bot == null) { throw new InvalidOperationException(nameof(Bot)); } @@ -172,38 +173,17 @@ public sealed class MobileAuthenticator : IDisposable { } // ReSharper disable RedundantSuppressNullableWarningExpression - required for .NET Framework - ConfirmationsResponse? response = await Bot.ArchiWebHandler.GetConfirmationsPage(deviceID!, confirmationHash!, time).ConfigureAwait(false); + ConfirmationsResponse? response = await Bot.ArchiWebHandler.GetConfirmations(deviceID!, confirmationHash!, time).ConfigureAwait(false); - // ReSharper restore RedundantSuppressNullableWarningExpression - required for .NET Framework - - if (response?.Confirmations == null) { + if (response?.Success != true) { return null; } - if (!response.Success) { - return null; + foreach (Confirmation? confirmation in response.Confirmations.Where(static confirmation => (confirmation.ConfirmationType == Confirmation.EConfirmationType.Unknown) || !Enum.IsDefined(confirmation.ConfirmationType))) { + Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(confirmation.ConfirmationType), confirmation.ConfirmationType)); } - HashSet result = new(); - - foreach (ConfirmationData confirmation in response.Confirmations) { - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (!Enum.TryParse(confirmation.TypeText!, out Confirmation.EType type) || (type == Confirmation.EType.Unknown)) { - Bot.ArchiLogger.LogNullError(type); - - return null; - } - - if (!Enum.IsDefined(type)) { - Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(type), type)); - - return null; - } - - result.Add(new Confirmation(confirmation.ID, confirmation.Nonce, confirmation.CreatorID, type)); - } - - return result; + return response.Confirmations; } internal async Task GetSteamTime() { @@ -295,7 +275,7 @@ public sealed class MobileAuthenticator : IDisposable { // We totally ignore actual result returned by those calls, abort only if request timed out foreach (Confirmation confirmation in confirmations) { // ReSharper disable RedundantSuppressNullableWarningExpression - required for .NET Framework - bool? confirmationResult = await Bot.ArchiWebHandler.HandleConfirmation(deviceID!, confirmationHash!, time, confirmation.ID, confirmation.Key, accept).ConfigureAwait(false); + bool? confirmationResult = await Bot.ArchiWebHandler.HandleConfirmation(deviceID!, confirmationHash!, time, confirmation.ID, confirmation.Nonce, accept).ConfigureAwait(false); // ReSharper restore RedundantSuppressNullableWarningExpression - required for .NET Framework