Confirmations logic improvements

This commit is contained in:
JustArchi 2018-10-13 00:17:45 +02:00
parent a1e2d4f8c1
commit 353e7e7b88
4 changed files with 104 additions and 54 deletions

View file

@ -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<ulong> 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();
}

View file

@ -102,10 +102,10 @@ namespace ArchiSteamFarm {
return result?.Success == true;
}
internal async Task<bool> 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<Steam.TradeOfferAcceptResponse>(SteamCommunityURL, request, data, referer).ConfigureAwait(false);
return response != null ? (true, response.RequiresMobileConfirmation) : (false, false);
}
internal async Task<bool> AddFreeLicense(uint subID) {
@ -1196,14 +1197,14 @@ namespace ArchiSteamFarm {
return response == null ? ((EResult Result, EPurchaseResultDetail? PurchaseResult)?) null : (response.Result, response.PurchaseResultDetail);
}
internal async Task<bool> SendTradeOffer(ulong partnerID, IReadOnlyCollection<Steam.Asset> itemsToGive, string token = null) {
internal async Task<(bool Success, HashSet<ulong> MobileTradeOfferIDs)> SendTradeOffer(ulong partnerID, IReadOnlyCollection<Steam.Asset> 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<Steam.TradeOfferRequest> trades = new HashSet<Steam.TradeOfferRequest> { singleTrade };
Steam.TradeOfferSendRequest singleTrade = new Steam.TradeOfferSendRequest();
HashSet<Steam.TradeOfferSendRequest> trades = new HashSet<Steam.TradeOfferSendRequest> { 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<string, string> data in trades.Select(
trade => new Dictionary<string, string>(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<string, string> data = new Dictionary<string, string>(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<ulong> mobileTradeOfferIDs = new HashSet<ulong>();
foreach (Steam.TradeOfferSendRequest trade in trades) {
data["json_tradeoffer"] = JsonConvert.SerializeObject(trade);
Steam.TradeOfferSendResponse response = await UrlPostToJsonObjectWithSession<Steam.TradeOfferSendResponse>(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<bool> SteamAwardsVote(byte voteID, uint appID) {

View file

@ -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)]

View file

@ -199,50 +199,51 @@ namespace ArchiSteamFarm {
return;
}
IList<ParseTradeResult> 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<ulong> 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<ulong> 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<ParseTradeResult> 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<ParseTradeResult> 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