From ec59e2e55626b591649849bbbd44aefdaa4d4b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Fri, 18 Jul 2025 09:52:31 +0200 Subject: [PATCH] Add support for new trade messages acknowledge --- ArchiSteamFarm/Steam/Bot.cs | 5 ++++- ArchiSteamFarm/Steam/Exchange/Trading.cs | 18 ++++++++++++++++ .../Steam/Integration/ArchiWebHandler.cs | 11 ++++++++++ ArchiSteamFarm/Steam/Interaction/Actions.cs | 6 ++++++ ArchiSteamFarm/Steam/Storage/BotDatabase.cs | 21 +++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 7bc01db4e..6d0db19a5 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -164,6 +164,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable { [PublicAPI] public SteamFriends SteamFriends { get; } + [JsonIgnore] + [PublicAPI] + public Trading Trading { get; } + internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked; internal bool HasLoginCodeReady => !string.IsNullOrEmpty(TwoFactorCode) || !string.IsNullOrEmpty(AuthCode); @@ -179,7 +183,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private readonly SteamClient SteamClient; private readonly ConcurrentHashSet SteamFamilySharingIDs = []; private readonly SteamUser SteamUser; - private readonly Trading Trading; private readonly SemaphoreSlim UnpackBoosterPacksSemaphore = new(1, 1); private IEnumerable<(string FilePath, EFileType FileType)> RelatedFiles { diff --git a/ArchiSteamFarm/Steam/Exchange/Trading.cs b/ArchiSteamFarm/Steam/Exchange/Trading.cs index 9265d33ca..8ef42d798 100644 --- a/ArchiSteamFarm/Steam/Exchange/Trading.cs +++ b/ArchiSteamFarm/Steam/Exchange/Trading.cs @@ -22,6 +22,7 @@ // limitations under the License. using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -44,6 +45,9 @@ public sealed class Trading : IDisposable { internal const byte MaxItemsPerTrade = byte.MaxValue; // This is decided upon various factors, mainly stability of Steam servers when dealing with huge trade offers internal const byte MaxTradesPerAccount = 5; // This is limit introduced by Valve + [PublicAPI] + public static readonly FrozenSet TradeRestrictionsAppIDs = [730]; + private readonly Bot Bot; private readonly ConcurrentHashSet HandledTradeOfferIDs = []; private readonly SemaphoreSlim TradesSemaphore = new(1, 1); @@ -58,6 +62,15 @@ public sealed class Trading : IDisposable { public void Dispose() => TradesSemaphore.Dispose(); + [PublicAPI] + public async Task AcknowledgeTradeRestrictions() { + if (Bot.BotDatabase.TradeRestrictionsAcknowledged) { + return true; + } + + return Bot.BotDatabase.TradeRestrictionsAcknowledged = await Bot.ArchiWebHandler.AcknowledgeTradeRestrictions().ConfigureAwait(false); + } + [PublicAPI] public static Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), List> GetInventorySets(IReadOnlyCollection inventory) { if ((inventory == null) || (inventory.Count == 0)) { @@ -285,6 +298,11 @@ public sealed class Trading : IDisposable { IList tradeResults = await Utilities.InParallel(tasks).ConfigureAwait(false); + if (!Bot.BotDatabase.TradeRestrictionsAcknowledged && tradeResults.Any(static result => ((result.Result == ParseTradeResult.EResult.Accepted) && (result.ItemsToGive?.Any(static item => TradeRestrictionsAppIDs.Contains(item.AppID)) == true)) || (result.ItemsToReceive?.Any(static item => TradeRestrictionsAppIDs.Contains(item.AppID)) == true))) { + // We should normally fail the process in case of a failure here, but since the popup could be marked already in the past, we'll allow it in hope it wasn't needed after all + await AcknowledgeTradeRestrictions().ConfigureAwait(false); + } + if (Bot.HasMobileAuthenticator) { HashSet mobileTradeResults = tradeResults.Where(static result => result is { Result: ParseTradeResult.EResult.Accepted, Confirmed: false }).ToHashSet(); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index b21bca5db..5c1fea88e 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -1507,6 +1507,17 @@ public sealed class ArchiWebHandler : IDisposable { return response?.Content != null ? (true, response.Content.RequiresMobileConfirmation) : (false, false); } + internal async Task AcknowledgeTradeRestrictions() { + Uri request = new(SteamCommunityURL, "/trade/new/acknowledge"); + + // Extra entry for sessionID + Dictionary data = new(2, StringComparer.Ordinal) { + { "message", "1" } + }; + + return await UrlPostWithSession(request, data: data).ConfigureAwait(false); + } + internal async Task<(EResult Result, EPurchaseResultDetail PurchaseResult)> AddFreeLicense(uint subID) { ArgumentOutOfRangeException.ThrowIfZero(subID); diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 890757001..2cd12d9c3 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -459,6 +459,12 @@ public sealed class Actions : IAsyncDisposable, IDisposable { return (false, Strings.BotLootingFailed); } + // In similar way we might need to accept popup on Steam side, we limit it only to cases that we're aware of, as sending this request otherwise is additional overhead for no reason + if (!Bot.BotDatabase.TradeRestrictionsAcknowledged && items.Any(static item => Trading.TradeRestrictionsAppIDs.Contains(item.AppID))) { + // We should normally fail the process in case of a failure here, but since the popup could be marked already in the past, we'll allow it in hope it wasn't needed after all + await Bot.Trading.AcknowledgeTradeRestrictions().ConfigureAwait(false); + } + (bool success, _, HashSet? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, items, token: tradeToken, customMessage: customMessage, itemsPerTrade: itemsPerTrade).ConfigureAwait(false); if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) { diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs index fd7cfbcf8..36151ce50 100644 --- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs +++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs @@ -50,6 +50,21 @@ public sealed class BotDatabase : GenericDatabase { internal bool HasGamesToRedeemInBackground => GamesToRedeemInBackgroundCount > 0; + [JsonIgnore] + [PublicAPI] + public bool TradeRestrictionsAcknowledged { + get => BackingTradeRestrictionsAcknowledged; + + internal set { + if (BackingTradeRestrictionsAcknowledged == value) { + return; + } + + BackingTradeRestrictionsAcknowledged = value; + Utilities.InBackground(Save); + } + } + internal string? AccessToken { get => BackingAccessToken; @@ -166,6 +181,9 @@ public sealed class BotDatabase : GenericDatabase { [JsonInclude] private string? BackingSteamGuardData { get; set; } + [JsonInclude] + private bool BackingTradeRestrictionsAcknowledged { get; set; } + [JsonDisallowNull] [JsonInclude] private OrderedDictionary GamesToRedeemInBackground { get; init; } = new(); @@ -228,6 +246,9 @@ public sealed class BotDatabase : GenericDatabase { [UsedImplicitly] public bool ShouldSerializeBackingSteamGuardData() => !string.IsNullOrEmpty(BackingSteamGuardData); + [UsedImplicitly] + public bool ShouldSerializeBackingTradeRestrictionsAcknowledged() => BackingTradeRestrictionsAcknowledged; + [UsedImplicitly] public bool ShouldSerializeExtraStorePackages() => ExtraStorePackages.Count > 0;