diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index 68563a077..d28731970 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -41,10 +41,13 @@ using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; +using Newtonsoft.Json.Linq; +using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { + private const string MatchActivelyTradeOfferIDsStorageKey = $"{nameof(ItemsMatcher)}-{nameof(MatchActively)}-TradeOfferIDs"; private const byte MaxAnnouncementTTL = 60; // Maximum amount of minutes we can wait if the next announcement doesn't happen naturally private const byte MaxTradeOffersActive = 10; // The actual upper limit is 30, but we should use lower amount to allow some bots to react before we hit the maximum allowed private const byte MinAnnouncementTTL = 5; // Minimum amount of minutes we must wait before the next Announcement @@ -657,6 +660,50 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { return false; } + // Cancel previous trade offers sent and deprioritize SteamIDs that didn't answer us in this round + HashSet? matchActivelyTradeOfferIDs = null; + + JToken? matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey); + + if (matchActivelyTradeOfferIDsToken != null) { + try { + matchActivelyTradeOfferIDs = matchActivelyTradeOfferIDsToken.Values().ToHashSet(); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericWarningException(e); + } + } + + matchActivelyTradeOfferIDs ??= new HashSet(); + + HashSet deprioritizedSteamIDs = new(); + + if (matchActivelyTradeOfferIDs.Count > 0) { + // This is not a mandatory step, we allow it to fail + HashSet? sentTradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, false, true, false).ConfigureAwait(false); + + if (sentTradeOffers != null) { + HashSet activeTradeOfferIDs = new(); + + foreach (TradeOffer tradeOffer in sentTradeOffers.Where(tradeOffer => (tradeOffer.State == ETradeOfferState.Active) && matchActivelyTradeOfferIDs.Contains(tradeOffer.TradeOfferID))) { + deprioritizedSteamIDs.Add(tradeOffer.OtherSteamID64); + + if (!await Bot.ArchiWebHandler.CancelTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false)) { + activeTradeOfferIDs.Add(tradeOffer.TradeOfferID); + } + } + + if (!matchActivelyTradeOfferIDs.SetEquals(activeTradeOfferIDs)) { + matchActivelyTradeOfferIDs = activeTradeOfferIDs; + + if (matchActivelyTradeOfferIDs.Count > 0) { + Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs)); + } else { + Bot.BotDatabase.DeleteFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey); + } + } + } + } + HashSet pendingMobileTradeOfferIDs = new(); byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration; @@ -664,7 +711,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { byte failuresInRow = 0; uint matchedSets = 0; - foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) { + foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) { if (failuresInRow >= WebBrowser.MaxTries) { Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(failuresInRow)} >= {WebBrowser.MaxTries}")); @@ -840,7 +887,13 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}"); - (bool success, HashSet? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false); + (bool success, HashSet? tradeOfferIDs, HashSet? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false); + + if (tradeOfferIDs?.Count > 0) { + matchActivelyTradeOfferIDs.UnionWith(tradeOfferIDs); + + Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs)); + } if (mobileTradeOfferIDs?.Count > 0) { pendingMobileTradeOfferIDs.UnionWith(mobileTradeOfferIDs); diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 1841fa18d..8fd8d31a7 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -4,7 +4,7 @@ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | -// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); @@ -95,6 +95,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable { [PublicAPI] public ArchiWebHandler ArchiWebHandler { get; } + [JsonIgnore] + [PublicAPI] + public BotDatabase BotDatabase { get; } + [JsonProperty] [PublicAPI] public string BotName { get; } @@ -135,8 +139,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable { [PublicAPI] public SteamFriends SteamFriends { get; } - internal readonly BotDatabase BotDatabase; - internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked; internal bool IsAccountLimited => AccountFlags.HasFlag(EAccountFlags.LimitedUser) || AccountFlags.HasFlag(EAccountFlags.LimitedUserForce); internal bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown); diff --git a/ArchiSteamFarm/Steam/Exchange/Trading.cs b/ArchiSteamFarm/Steam/Exchange/Trading.cs index 5e84567b9..b93c75d08 100644 --- a/ArchiSteamFarm/Steam/Exchange/Trading.cs +++ b/ArchiSteamFarm/Steam/Exchange/Trading.cs @@ -4,7 +4,7 @@ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | -// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); @@ -414,7 +414,7 @@ public sealed class Trading : IDisposable { } private async Task ParseActiveTrades() { - HashSet? tradeOffers = await Bot.ArchiWebHandler.GetActiveTradeOffers().ConfigureAwait(false); + HashSet? tradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, true, false, true).ConfigureAwait(false); if ((tradeOffers == null) || (tradeOffers.Count == 0)) { return false; diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 3cfdf46b0..16b41604c 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -4,7 +4,7 @@ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | -// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); @@ -101,6 +101,17 @@ public sealed class ArchiWebHandler : IDisposable { WebBrowser.Dispose(); } + [PublicAPI] + public async Task CancelTradeOffer(ulong tradeID) { + if (tradeID == 0) { + throw new ArgumentOutOfRangeException(nameof(tradeID)); + } + + Uri request = new(SteamCommunityURL, $"/tradeoffer/{tradeID}/cancel"); + + return await UrlPostWithSession(request).ConfigureAwait(false); + } + [PublicAPI] public async Task GetAbsoluteProfileURL(bool waitForInitialization = true) { if (waitForInitialization && !Initialized) { @@ -353,6 +364,210 @@ public sealed class ArchiWebHandler : IDisposable { return result; } + [PublicAPI] + public async Task?> GetTradeOffers(bool? activeOnly = null, bool? receivedOffers = null, bool? sentOffers = null, bool? withDescriptions = null) { + (bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false); + + if (!success || string.IsNullOrEmpty(steamApiKey)) { + return null; + } + + Dictionary arguments = new(StringComparer.Ordinal) { + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + { "key", steamApiKey! } + }; + + if (activeOnly.HasValue) { + arguments["active_only"] = activeOnly.Value ? "true" : "false"; + + // This is ridiculous, active_only without historical cutoff is actually active right now + inactive ones that changed their status since our preview request, what the fuck + // We're going to make it work as everybody sane expects, by being active ONLY, as the name implies, not active + some shit nobody asked for + // https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#GetTradeOffers_.28v1.29 + if (activeOnly.Value) { + arguments["time_historical_cutoff"] = uint.MaxValue; + } + } + + if (receivedOffers.HasValue) { + arguments["get_received_offers"] = receivedOffers.Value ? "true" : "false"; + } + + if (sentOffers.HasValue) { + arguments["get_sent_offers"] = sentOffers.Value ? "true" : "false"; + } + + if (withDescriptions.HasValue) { + arguments["get_descriptions"] = withDescriptions.Value ? "true" : "false"; + } + + KeyValue? response = null; + + for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) { + if ((i > 0) && (WebLimiterDelay > 0)) { + await Task.Delay(WebLimiterDelay).ConfigureAwait(false); + } + + using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService); + + econService.Timeout = WebBrowser.Timeout; + + try { + response = await WebLimitRequest( + WebAPI.DefaultBaseAddress, + + // ReSharper disable once AccessToDisposedClosure + async () => await econService.CallAsync(HttpMethod.Get, "GetTradeOffers", args: arguments).ConfigureAwait(false) + ).ConfigureAwait(false); + } catch (TaskCanceledException e) { + Bot.ArchiLogger.LogGenericDebuggingException(e); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericWarningException(e); + } + } + + if (response == null) { + Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)); + + return null; + } + + Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions = new(); + + foreach (KeyValue description in response["descriptions"].Children) { + uint appID = description["appid"].AsUnsignedInteger(); + + if (appID == 0) { + Bot.ArchiLogger.LogNullError(appID); + + return null; + } + + ulong classID = description["classid"].AsUnsignedLong(); + + if (classID == 0) { + Bot.ArchiLogger.LogNullError(classID); + + return null; + } + + ulong instanceID = description["instanceid"].AsUnsignedLong(); + + (uint AppID, ulong ClassID, ulong InstanceID) key = (appID, classID, instanceID); + + if (descriptions.ContainsKey(key)) { + continue; + } + + InventoryResponse.Description parsedDescription = new() { + AppID = appID, + ClassID = classID, + InstanceID = instanceID, + Marketable = description["marketable"].AsBoolean(), + Tradable = true // We're parsing active trade offers, we can assume as much + }; + + List tags = description["tags"].Children; + + if (tags.Count > 0) { + HashSet parsedTags = new(tags.Count); + + foreach (KeyValue tag in tags) { + string? identifier = tag["category"].AsString(); + + if (string.IsNullOrEmpty(identifier)) { + Bot.ArchiLogger.LogNullError(identifier); + + return null; + } + + string? value = tag["internal_name"].AsString(); + + // Apparently, name can be empty, but not null + if (value == null) { + Bot.ArchiLogger.LogNullError(value); + + return null; + } + + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + parsedTags.Add(new Tag(identifier!, value)); + } + + parsedDescription.Tags = parsedTags.ToImmutableHashSet(); + } + + descriptions[key] = parsedDescription; + } + + IEnumerable trades = Enumerable.Empty(); + + if (receivedOffers.GetValueOrDefault(true)) { + trades = trades.Concat(response["trade_offers_received"].Children); + } + + if (sentOffers.GetValueOrDefault(true)) { + trades = trades.Concat(response["trade_offers_sent"].Children); + } + + HashSet result = new(); + + foreach (KeyValue trade in trades) { + ETradeOfferState state = trade["trade_offer_state"].AsEnum(); + + if (!Enum.IsDefined(state)) { + Bot.ArchiLogger.LogNullError(state); + + return null; + } + + if (activeOnly.HasValue && ((activeOnly.Value && (state != ETradeOfferState.Active)) || (!activeOnly.Value && (state == ETradeOfferState.Active)))) { + continue; + } + + ulong tradeOfferID = trade["tradeofferid"].AsUnsignedLong(); + + if (tradeOfferID == 0) { + Bot.ArchiLogger.LogNullError(tradeOfferID); + + return null; + } + + uint otherSteamID3 = trade["accountid_other"].AsUnsignedInteger(); + + if (otherSteamID3 == 0) { + Bot.ArchiLogger.LogNullError(otherSteamID3); + + return null; + } + + TradeOffer tradeOffer = new(tradeOfferID, otherSteamID3, state); + + List itemsToGive = trade["items_to_give"].Children; + + if (itemsToGive.Count > 0) { + if (!ParseItems(descriptions, itemsToGive, tradeOffer.ItemsToGive)) { + Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToGive))); + + return null; + } + } + + List itemsToReceive = trade["items_to_receive"].Children; + + if (itemsToReceive.Count > 0) { + if (!ParseItems(descriptions, itemsToReceive, tradeOffer.ItemsToReceive)) { + Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToReceive))); + + return null; + } + } + + result.Add(tradeOffer); + } + + return result; + } + [PublicAPI] public async Task HasValidApiKey() { (bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false); @@ -375,7 +590,7 @@ public sealed class ArchiWebHandler : IDisposable { } [PublicAPI] - public async Task<(bool Success, HashSet? MobileTradeOfferIDs)> SendTradeOffer(ulong steamID, IReadOnlyCollection? itemsToGive = null, IReadOnlyCollection? itemsToReceive = null, string? token = null, bool forcedSingleOffer = false, ushort itemsPerTrade = Trading.MaxItemsPerTrade) { + public async Task<(bool Success, HashSet? TradeOfferIDs, HashSet? MobileTradeOfferIDs)> SendTradeOffer(ulong steamID, IReadOnlyCollection? itemsToGive = null, IReadOnlyCollection? itemsToReceive = null, string? token = null, bool forcedSingleOffer = false, ushort itemsPerTrade = Trading.MaxItemsPerTrade) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { throw new ArgumentOutOfRangeException(nameof(steamID)); } @@ -432,6 +647,7 @@ public sealed class ArchiWebHandler : IDisposable { { "tradeoffermessage", $"Sent by {SharedInfo.PublicIdentifier}/{SharedInfo.Version}" } }; + HashSet tradeOfferIDs = new(trades.Count); HashSet mobileTradeOfferIDs = new(trades.Count); foreach (TradeOfferSendRequest trade in trades) { @@ -443,7 +659,7 @@ public sealed class ArchiWebHandler : IDisposable { response = await UrlPostToJsonObjectWithSession(request, data: data, referer: referer, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); if (response == null) { - return (false, mobileTradeOfferIDs); + return (false, tradeOfferIDs, mobileTradeOfferIDs); } if (response.StatusCode.IsServerErrorCode()) { @@ -458,26 +674,28 @@ public sealed class ArchiWebHandler : IDisposable { // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content!.ErrorText)); - return (false, mobileTradeOfferIDs); + return (false, tradeOfferIDs, mobileTradeOfferIDs); } } if (response?.Content == null) { - return (false, mobileTradeOfferIDs); + return (false, tradeOfferIDs, mobileTradeOfferIDs); } if (response.Content.TradeOfferID == 0) { Bot.ArchiLogger.LogNullError(response.Content.TradeOfferID); - return (false, mobileTradeOfferIDs); + return (false, tradeOfferIDs, mobileTradeOfferIDs); } + tradeOfferIDs.Add(response.Content.TradeOfferID); + if (response.Content.RequiresMobileConfirmation) { mobileTradeOfferIDs.Add(response.Content.TradeOfferID); } } - return (true, mobileTradeOfferIDs); + return (true, tradeOfferIDs, mobileTradeOfferIDs); } [PublicAPI] @@ -1465,182 +1683,6 @@ public sealed class ArchiWebHandler : IDisposable { return response?.Content?.Queue; } - internal async Task?> GetActiveTradeOffers() { - (bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false); - - if (!success || string.IsNullOrEmpty(steamApiKey)) { - return null; - } - - Dictionary arguments = new(5, StringComparer.Ordinal) { - { "active_only", 1 }, - { "get_descriptions", 1 }, - { "get_received_offers", 1 }, - - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - { "key", steamApiKey! }, - - { "time_historical_cutoff", uint.MaxValue } - }; - - KeyValue? response = null; - - for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) { - if ((i > 0) && (WebLimiterDelay > 0)) { - await Task.Delay(WebLimiterDelay).ConfigureAwait(false); - } - - using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService); - - econService.Timeout = WebBrowser.Timeout; - - try { - response = await WebLimitRequest( - WebAPI.DefaultBaseAddress, - - // ReSharper disable once AccessToDisposedClosure - async () => await econService.CallAsync(HttpMethod.Get, "GetTradeOffers", args: arguments).ConfigureAwait(false) - ).ConfigureAwait(false); - } catch (TaskCanceledException e) { - Bot.ArchiLogger.LogGenericDebuggingException(e); - } catch (Exception e) { - Bot.ArchiLogger.LogGenericWarningException(e); - } - } - - if (response == null) { - Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)); - - return null; - } - - Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions = new(); - - foreach (KeyValue description in response["descriptions"].Children) { - uint appID = description["appid"].AsUnsignedInteger(); - - if (appID == 0) { - Bot.ArchiLogger.LogNullError(appID); - - return null; - } - - ulong classID = description["classid"].AsUnsignedLong(); - - if (classID == 0) { - Bot.ArchiLogger.LogNullError(classID); - - return null; - } - - ulong instanceID = description["instanceid"].AsUnsignedLong(); - - (uint AppID, ulong ClassID, ulong InstanceID) key = (appID, classID, instanceID); - - if (descriptions.ContainsKey(key)) { - continue; - } - - InventoryResponse.Description parsedDescription = new() { - AppID = appID, - ClassID = classID, - InstanceID = instanceID, - Marketable = description["marketable"].AsBoolean(), - Tradable = true // We're parsing active trade offers, we can assume as much - }; - - List tags = description["tags"].Children; - - if (tags.Count > 0) { - HashSet parsedTags = new(tags.Count); - - foreach (KeyValue tag in tags) { - string? identifier = tag["category"].AsString(); - - if (string.IsNullOrEmpty(identifier)) { - Bot.ArchiLogger.LogNullError(identifier); - - return null; - } - - string? value = tag["internal_name"].AsString(); - - // Apparently, name can be empty, but not null - if (value == null) { - Bot.ArchiLogger.LogNullError(value); - - return null; - } - - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - parsedTags.Add(new Tag(identifier!, value)); - } - - parsedDescription.Tags = parsedTags.ToImmutableHashSet(); - } - - descriptions[key] = parsedDescription; - } - - HashSet result = new(); - - foreach (KeyValue trade in response["trade_offers_received"].Children) { - ETradeOfferState state = trade["trade_offer_state"].AsEnum(); - - if (!Enum.IsDefined(state)) { - Bot.ArchiLogger.LogNullError(state); - - return null; - } - - if (state != ETradeOfferState.Active) { - continue; - } - - ulong tradeOfferID = trade["tradeofferid"].AsUnsignedLong(); - - if (tradeOfferID == 0) { - Bot.ArchiLogger.LogNullError(tradeOfferID); - - return null; - } - - uint otherSteamID3 = trade["accountid_other"].AsUnsignedInteger(); - - if (otherSteamID3 == 0) { - Bot.ArchiLogger.LogNullError(otherSteamID3); - - return null; - } - - TradeOffer tradeOffer = new(tradeOfferID, otherSteamID3, state); - - List itemsToGive = trade["items_to_give"].Children; - - if (itemsToGive.Count > 0) { - if (!ParseItems(descriptions, itemsToGive, tradeOffer.ItemsToGive)) { - Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToGive))); - - return null; - } - } - - List itemsToReceive = trade["items_to_receive"].Children; - - if (itemsToReceive.Count > 0) { - if (!ParseItems(descriptions, itemsToReceive, tradeOffer.ItemsToReceive)) { - Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToReceive))); - - return null; - } - } - - result.Add(tradeOffer); - } - - return result; - } - internal async Task?> GetAppList() { KeyValue? response = null; diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index fd992b7bd..2c4540663 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -4,7 +4,7 @@ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | -// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); @@ -340,7 +340,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable { return (false, Strings.BotLootingFailed); } - (bool success, HashSet? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, items, token: tradeToken, itemsPerTrade: itemsPerTrade).ConfigureAwait(false); + (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); diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs index fe6e87892..df3f0c7ed 100644 --- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs +++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs @@ -4,7 +4,7 @@ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | -// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,15 +28,15 @@ using System.Linq; using System.Threading.Tasks; using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; -using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Security; +using ArchiSteamFarm.Storage; using JetBrains.Annotations; using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Storage; -internal sealed class BotDatabase : SerializableFile { +public sealed class BotDatabase : GenericDatabase { [JsonProperty(Required = Required.DisallowNull)] internal readonly ConcurrentHashSet FarmingBlacklistAppIDs = new(); diff --git a/ArchiSteamFarm/Storage/GenericDatabase.cs b/ArchiSteamFarm/Storage/GenericDatabase.cs new file mode 100644 index 000000000..96815b176 --- /dev/null +++ b/ArchiSteamFarm/Storage/GenericDatabase.cs @@ -0,0 +1,82 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// 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.Collections.Concurrent; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ArchiSteamFarm.Storage; + +public abstract class GenericDatabase : SerializableFile { + [JsonProperty(Required = Required.DisallowNull)] + private readonly ConcurrentDictionary KeyValueJsonStorage = new(); + + [PublicAPI] + public void DeleteFromJsonStorage(string key) { + if (string.IsNullOrEmpty(key)) { + throw new ArgumentNullException(nameof(key)); + } + + if (!KeyValueJsonStorage.TryRemove(key, out _)) { + return; + } + + Utilities.InBackground(Save); + } + + [PublicAPI] + public JToken? LoadFromJsonStorage(string key) { + if (string.IsNullOrEmpty(key)) { + throw new ArgumentNullException(nameof(key)); + } + + return KeyValueJsonStorage.TryGetValue(key, out JToken? value) ? value : null; + } + + [PublicAPI] + public void SaveToJsonStorage(string key, JToken value) { + if (string.IsNullOrEmpty(key)) { + throw new ArgumentNullException(nameof(key)); + } + + ArgumentNullException.ThrowIfNull(value); + + if (value.Type == JTokenType.Null) { + DeleteFromJsonStorage(key); + + return; + } + + if (KeyValueJsonStorage.TryGetValue(key, out JToken? currentValue) && JToken.DeepEquals(currentValue, value)) { + return; + } + + KeyValueJsonStorage[key] = value; + Utilities.InBackground(Save); + } + + [UsedImplicitly] + public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty; +} diff --git a/ArchiSteamFarm/Storage/GlobalDatabase.cs b/ArchiSteamFarm/Storage/GlobalDatabase.cs index 96636f755..2807806a5 100644 --- a/ArchiSteamFarm/Storage/GlobalDatabase.cs +++ b/ArchiSteamFarm/Storage/GlobalDatabase.cs @@ -4,7 +4,7 @@ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // | -// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,17 +29,15 @@ using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; -using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.SteamKit2; using JetBrains.Annotations; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ArchiSteamFarm.Storage; -public sealed class GlobalDatabase : SerializableFile { +public sealed class GlobalDatabase : GenericDatabase { [JsonIgnore] [PublicAPI] public IReadOnlyDictionary PackageAccessTokensReadOnly => PackagesAccessTokens; @@ -57,9 +55,6 @@ public sealed class GlobalDatabase : SerializableFile { [JsonProperty(Required = Required.DisallowNull)] internal readonly InMemoryServerListProvider ServerListProvider = new(); - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary KeyValueJsonStorage = new(); - [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentDictionary PackagesAccessTokens = new(); @@ -119,50 +114,6 @@ public sealed class GlobalDatabase : SerializableFile { ServerListProvider.ServerListUpdated += OnObjectModified; } - [PublicAPI] - public void DeleteFromJsonStorage(string key) { - if (string.IsNullOrEmpty(key)) { - throw new ArgumentNullException(nameof(key)); - } - - if (!KeyValueJsonStorage.TryRemove(key, out _)) { - return; - } - - Utilities.InBackground(Save); - } - - [PublicAPI] - public JToken? LoadFromJsonStorage(string key) { - if (string.IsNullOrEmpty(key)) { - throw new ArgumentNullException(nameof(key)); - } - - return KeyValueJsonStorage.TryGetValue(key, out JToken? value) ? value : null; - } - - [PublicAPI] - public void SaveToJsonStorage(string key, JToken value) { - if (string.IsNullOrEmpty(key)) { - throw new ArgumentNullException(nameof(key)); - } - - ArgumentNullException.ThrowIfNull(value); - - if (value.Type == JTokenType.Null) { - DeleteFromJsonStorage(key); - - return; - } - - if (KeyValueJsonStorage.TryGetValue(key, out JToken? currentValue) && JToken.DeepEquals(currentValue, value)) { - return; - } - - KeyValueJsonStorage[key] = value; - Utilities.InBackground(Save); - } - [UsedImplicitly] public bool ShouldSerializeBackingCellID() => BackingCellID != 0; @@ -175,9 +126,6 @@ public sealed class GlobalDatabase : SerializableFile { [UsedImplicitly] public bool ShouldSerializeCardCountsPerGame() => !CardCountsPerGame.IsEmpty; - [UsedImplicitly] - public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty; - [UsedImplicitly] public bool ShouldSerializePackagesAccessTokens() => !PackagesAccessTokens.IsEmpty;