diff --git a/ArchiSteamFarm/Actions.cs b/ArchiSteamFarm/Actions.cs index e6a5f8084..1552d058f 100644 --- a/ArchiSteamFarm/Actions.cs +++ b/ArchiSteamFarm/Actions.cs @@ -284,15 +284,21 @@ namespace ArchiSteamFarm { return (false, string.Format(Strings.ErrorIsEmpty, nameof(inventory))); } - if (!await Bot.ArchiWebHandler.MarkSentTrades().ConfigureAwait(false) || !await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, inventory, Bot.BotConfig.SteamTradeToken).ConfigureAwait(false)) { + if (!await Bot.ArchiWebHandler.MarkSentTrades().ConfigureAwait(false)) { return (false, Strings.BotLootingFailed); } - if (Bot.HasMobileAuthenticator) { - if (!await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, targetSteamID, waitIfNeeded: true).ConfigureAwait(false)) { + (bool success, HashSet mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, inventory, Bot.BotConfig.SteamTradeToken).ConfigureAwait(false); + + if ((mobileTradeOfferIDs != null) && (mobileTradeOfferIDs.Count > 0) && Bot.HasMobileAuthenticator) { + if (!await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, targetSteamID, mobileTradeOfferIDs, true).ConfigureAwait(false)) { return (false, Strings.BotLootingFailed); } } + + if (!success) { + return (false, Strings.BotLootingFailed); + } } finally { LootingSemaphore.Release(); } diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 1dd16d3ff..ad8a9cf85 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -102,10 +102,10 @@ namespace ArchiSteamFarm { return result?.Success == true; } - internal async Task AcceptTradeOffer(ulong tradeID) { + internal async Task<(bool Success, bool RequiresMobileConfirmation)> AcceptTradeOffer(ulong tradeID) { if (tradeID == 0) { Bot.ArchiLogger.LogNullError(nameof(tradeID)); - return false; + return (false, false); } string request = "/tradeoffer/" + tradeID + "/accept"; @@ -117,7 +117,8 @@ namespace ArchiSteamFarm { { "tradeofferid", tradeID.ToString() } }; - return await UrlPostWithSession(SteamCommunityURL, request, data, referer).ConfigureAwait(false); + Steam.TradeOfferAcceptResponse response = await UrlPostToJsonObjectWithSession(SteamCommunityURL, request, data, referer).ConfigureAwait(false); + return response != null ? (true, response.RequiresMobileConfirmation) : (false, false); } internal async Task AddFreeLicense(uint subID) { @@ -1196,14 +1197,14 @@ namespace ArchiSteamFarm { return response == null ? ((EResult Result, EPurchaseResultDetail? PurchaseResult)?) null : (response.Result, response.PurchaseResultDetail); } - internal async Task SendTradeOffer(ulong partnerID, IReadOnlyCollection itemsToGive, string token = null) { + internal async Task<(bool Success, HashSet MobileTradeOfferIDs)> SendTradeOffer(ulong partnerID, IReadOnlyCollection itemsToGive, string token = null) { if ((partnerID == 0) || (itemsToGive == null) || (itemsToGive.Count == 0)) { Bot.ArchiLogger.LogNullError(nameof(partnerID) + " || " + nameof(itemsToGive)); - return false; + return (false, null); } - Steam.TradeOfferRequest singleTrade = new Steam.TradeOfferRequest(); - HashSet trades = new HashSet { singleTrade }; + Steam.TradeOfferSendRequest singleTrade = new Steam.TradeOfferSendRequest(); + HashSet trades = new HashSet { singleTrade }; foreach (Steam.Asset itemToGive in itemsToGive) { if (singleTrade.ItemsToGive.Assets.Count >= Trading.MaxItemsPerTrade) { @@ -1211,7 +1212,7 @@ namespace ArchiSteamFarm { break; } - singleTrade = new Steam.TradeOfferRequest(); + singleTrade = new Steam.TradeOfferSendRequest(); trades.Add(singleTrade); } @@ -1222,21 +1223,30 @@ namespace ArchiSteamFarm { const string referer = SteamCommunityURL + "/tradeoffer/new"; // Extra entry for sessionID - foreach (Dictionary data in trades.Select( - trade => new Dictionary(6) { - { "json_tradeoffer", JsonConvert.SerializeObject(trade) }, - { "partner", partnerID.ToString() }, - { "serverid", "1" }, - { "trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : new JObject { { "trade_offer_access_token", token } }.ToString(Formatting.None) }, - { "tradeoffermessage", "Sent by " + SharedInfo.PublicIdentifier + "/" + SharedInfo.Version } + Dictionary data = new Dictionary(6) { + { "partner", partnerID.ToString() }, + { "serverid", "1" }, + { "trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : new JObject { { "trade_offer_access_token", token } }.ToString(Formatting.None) }, + { "tradeoffermessage", "Sent by " + SharedInfo.PublicIdentifier + "/" + SharedInfo.Version } + }; + + HashSet mobileTradeOfferIDs = new HashSet(); + + foreach (Steam.TradeOfferSendRequest trade in trades) { + data["json_tradeoffer"] = JsonConvert.SerializeObject(trade); + + Steam.TradeOfferSendResponse response = await UrlPostToJsonObjectWithSession(SteamCommunityURL, request, data, referer).ConfigureAwait(false); + + if (response == null) { + return (false, mobileTradeOfferIDs); } - )) { - if (!await UrlPostWithSession(SteamCommunityURL, request, data, referer).ConfigureAwait(false)) { - return false; + + if (response.RequiresMobileConfirmation) { + mobileTradeOfferIDs.Add(response.TradeOfferID); } } - return true; + return (true, mobileTradeOfferIDs); } internal async Task SteamAwardsVote(byte voteID, uint appID) { diff --git a/ArchiSteamFarm/Json/Steam.cs b/ArchiSteamFarm/Json/Steam.cs index 210b6e76b..80707213a 100644 --- a/ArchiSteamFarm/Json/Steam.cs +++ b/ArchiSteamFarm/Json/Steam.cs @@ -483,7 +483,16 @@ namespace ArchiSteamFarm.Json { } } - internal sealed class TradeOfferRequest { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + internal sealed class TradeOfferAcceptResponse { + [JsonProperty(PropertyName = "needs_mobile_confirmation", Required = Required.Always)] + internal readonly bool RequiresMobileConfirmation; + + // Deserialized from JSON + private TradeOfferAcceptResponse() { } + } + + internal sealed class TradeOfferSendRequest { [JsonProperty(PropertyName = "me", Required = Required.Always)] internal readonly ItemList ItemsToGive = new ItemList(); @@ -496,6 +505,34 @@ namespace ArchiSteamFarm.Json { } } + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + internal sealed class TradeOfferSendResponse { + [JsonProperty(PropertyName = "needs_mobile_confirmation", Required = Required.Always)] + internal readonly bool RequiresMobileConfirmation; + + internal ulong TradeOfferID { get; private set; } + + [JsonProperty(PropertyName = "tradeofferid", Required = Required.Always)] + private string TradeOfferIDText { + set { + if (string.IsNullOrEmpty(value)) { + ASF.ArchiLogger.LogNullError(nameof(value)); + return; + } + + if (!ulong.TryParse(value, out ulong tradeOfferID) || (tradeOfferID == 0)) { + ASF.ArchiLogger.LogNullError(nameof(tradeOfferID)); + return; + } + + TradeOfferID = tradeOfferID; + } + } + + // Deserialized from JSON + private TradeOfferSendResponse() { } + } + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class UserPrivacy { [JsonProperty(PropertyName = "eCommentPermission", Required = Required.Always)] diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 7b8202f5d..d5c27c569 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -199,50 +199,51 @@ namespace ArchiSteamFarm { return; } - IList results = await Utilities.InParallel(tradeOffers.Select(ParseTrade)).ConfigureAwait(false); + IList<(ParseTradeResult TradeResult, bool RequiresMobileConfirmation)> results = await Utilities.InParallel(tradeOffers.Select(ParseTrade)).ConfigureAwait(false); if (Bot.HasMobileAuthenticator) { - HashSet acceptedWithItemLoseTradeIDs = results.Where(result => (result != null) && (result.Result == ParseTradeResult.EResult.AcceptedWithItemLose)).Select(result => result.TradeID).ToHashSet(); - if (acceptedWithItemLoseTradeIDs.Count > 0) { - await Bot.Actions.AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, 0, acceptedWithItemLoseTradeIDs, true).ConfigureAwait(false); + HashSet mobileTradeOfferIDs = results.Where(result => (result.TradeResult != null) && result.RequiresMobileConfirmation).Select(result => result.TradeResult.TradeOfferID).ToHashSet(); + if (mobileTradeOfferIDs.Count > 0) { + await Bot.Actions.AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, 0, mobileTradeOfferIDs, true).ConfigureAwait(false); } } - if (results.Any(result => (result != null) && ((result.Result == ParseTradeResult.EResult.AcceptedWithItemLose) || (result.Result == ParseTradeResult.EResult.AcceptedWithoutItemLose))) && Bot.BotConfig.SendOnFarmingFinished) { + if (results.Any(result => (result.TradeResult != null) && (result.TradeResult.Result == ParseTradeResult.EResult.Accepted)) && Bot.BotConfig.SendOnFarmingFinished) { // If we finished a trade, perform a loot if user wants to do so await Bot.Actions.SendTradeOffer(wantedTypes: Bot.BotConfig.LootableTypes).ConfigureAwait(false); } } - private async Task ParseTrade(Steam.TradeOffer tradeOffer) { + private async Task<(ParseTradeResult TradeResult, bool RequiresMobileConfirmation)> ParseTrade(Steam.TradeOffer tradeOffer) { if (tradeOffer == null) { Bot.ArchiLogger.LogNullError(nameof(tradeOffer)); - return null; + return (null, false); } if (tradeOffer.State != Steam.TradeOffer.ETradeOfferState.Active) { Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, tradeOffer.State)); - return null; + return (null, false); } ParseTradeResult result = await ShouldAcceptTrade(tradeOffer).ConfigureAwait(false); if (result == null) { Bot.ArchiLogger.LogNullError(nameof(result)); - return null; + return (null, false); } switch (result.Result) { - case ParseTradeResult.EResult.AcceptedWithItemLose: - case ParseTradeResult.EResult.AcceptedWithoutItemLose: + case ParseTradeResult.EResult.Accepted: Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.AcceptingTrade, tradeOffer.TradeOfferID)); - if (await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false)) { + (bool success, bool requiresMobileConfirmation) = await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false); + + if (success) { if (tradeOffer.ItemsToReceive.Sum(item => item.Amount) > tradeOffer.ItemsToGive.Sum(item => item.Amount)) { Bot.ArchiLogger.LogGenericTrace(string.Format(Strings.BotAcceptedDonationTrade, tradeOffer.TradeOfferID)); } } - break; + return (result, requiresMobileConfirmation); case ParseTradeResult.EResult.RejectedPermanently: case ParseTradeResult.EResult.RejectedTemporarily: if (result.Result == ParseTradeResult.EResult.RejectedPermanently) { @@ -254,17 +255,15 @@ namespace ArchiSteamFarm { } Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IgnoringTrade, tradeOffer.TradeOfferID)); - break; + return (result, false); case ParseTradeResult.EResult.RejectedAndBlacklisted: Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.RejectingTrade, tradeOffer.TradeOfferID)); await Bot.ArchiWebHandler.DeclineTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false); - break; + return (result, false); default: Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.Result), result.Result)); - return null; + return (null, false); } - - return result; } private async Task ShouldAcceptTrade(Steam.TradeOffer tradeOffer) { @@ -276,7 +275,7 @@ namespace ArchiSteamFarm { if (tradeOffer.OtherSteamID64 != 0) { // Always accept trades from SteamMasterID if (Bot.IsMaster(tradeOffer.OtherSteamID64)) { - return new ParseTradeResult(tradeOffer.TradeOfferID, tradeOffer.ItemsToGive.Count > 0 ? ParseTradeResult.EResult.AcceptedWithItemLose : ParseTradeResult.EResult.AcceptedWithoutItemLose); + return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted); } // Always deny trades from blacklisted steamIDs @@ -297,7 +296,7 @@ namespace ArchiSteamFarm { // If we accept donations and bot trades, accept it right away if (acceptDonations && acceptBotTrades) { - return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.AcceptedWithoutItemLose); + return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted); } // If we don't accept donations, neither bot trades, deny it right away @@ -307,7 +306,7 @@ namespace ArchiSteamFarm { // Otherwise we either accept donations but not bot trades, or we accept bot trades but not donations bool isBotTrade = (tradeOffer.OtherSteamID64 != 0) && Bot.Bots.Values.Any(bot => bot.SteamID == tradeOffer.OtherSteamID64); - return new ParseTradeResult(tradeOffer.TradeOfferID, (acceptDonations && !isBotTrade) || (acceptBotTrades && isBotTrade) ? ParseTradeResult.EResult.AcceptedWithoutItemLose : ParseTradeResult.EResult.RejectedPermanently); + return new ParseTradeResult(tradeOffer.TradeOfferID, (acceptDonations && !isBotTrade) || (acceptBotTrades && isBotTrade) ? ParseTradeResult.EResult.Accepted : ParseTradeResult.EResult.RejectedPermanently); } // If we don't have SteamTradeMatcher enabled, this is the end for us @@ -344,7 +343,7 @@ namespace ArchiSteamFarm { // If we're matching everything, this is enough for us if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) { - return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.AcceptedWithItemLose); + return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted); } // Get appIDs/types we're interested in @@ -367,27 +366,25 @@ namespace ArchiSteamFarm { bool accept = IsTradeNeutralOrBetter(inventory, tradeOffer.ItemsToGive, tradeOffer.ItemsToReceive); // Even if trade is not neutral+ for us right now, it might be in the future, unless we're bot account where we assume that inventory doesn't change - return new ParseTradeResult(tradeOffer.TradeOfferID, accept ? ParseTradeResult.EResult.AcceptedWithItemLose : (Bot.BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.RejectInvalidTrades) ? ParseTradeResult.EResult.RejectedPermanently : ParseTradeResult.EResult.RejectedTemporarily)); + return new ParseTradeResult(tradeOffer.TradeOfferID, accept ? ParseTradeResult.EResult.Accepted : (Bot.BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.RejectInvalidTrades) ? ParseTradeResult.EResult.RejectedPermanently : ParseTradeResult.EResult.RejectedTemporarily)); } private sealed class ParseTradeResult { internal readonly EResult Result; + internal readonly ulong TradeOfferID; - internal readonly ulong TradeID; - - internal ParseTradeResult(ulong tradeID, EResult result) { - if ((tradeID == 0) || (result == EResult.Unknown)) { - throw new ArgumentNullException(nameof(tradeID) + " || " + nameof(result)); + internal ParseTradeResult(ulong tradeOfferID, EResult result) { + if ((tradeOfferID == 0) || (result == EResult.Unknown)) { + throw new ArgumentNullException(nameof(tradeOfferID) + " || " + nameof(result)); } - TradeID = tradeID; + TradeOfferID = tradeOfferID; Result = result; } internal enum EResult : byte { Unknown, - AcceptedWithItemLose, - AcceptedWithoutItemLose, + Accepted, RejectedTemporarily, RejectedPermanently, RejectedAndBlacklisted