diff --git a/ArchiSteamFarm b/ArchiSteamFarm index 716b253..e4c20df 160000 --- a/ArchiSteamFarm +++ b/ArchiSteamFarm @@ -1 +1 @@ -Subproject commit 716b253a044c9560bea1e9a77cb34afede93c6a3 +Subproject commit e4c20df4a896209636078c7acf33fbcaa8ad35bc diff --git a/BoosterManager/AdapterBridge.cs b/BoosterManager/AdapterBridge.cs new file mode 100644 index 0000000..857a75c --- /dev/null +++ b/BoosterManager/AdapterBridge.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; +using ArchiSteamFarm.Core; + +// ASFEnhanced Adapter https://github.com/chr233/ASFEnhanceAdapterDemoPlugin + +namespace BoosterManager; +internal static class AdapterBridge { + public static bool InitAdapter(string pluginName, string pluginId, string? cmdPrefix, string? repoName, MethodInfo? cmdHandler) { + try { + var adapterEndpoint = Assembly.Load("ASFEnhance").GetType("ASFEnhance._Adapter_.Endpoint"); + var registerModule = adapterEndpoint?.GetMethod("RegisterModule", BindingFlags.Static | BindingFlags.Public); + var pluinVersion = Assembly.GetExecutingAssembly().GetName().Version; + + if (registerModule != null && adapterEndpoint != null) { + var result = registerModule?.Invoke(null, new object?[] { pluginName, pluginId, cmdPrefix, repoName, pluinVersion, cmdHandler }); + + if (result is string str) { + if (str == pluginName) { + return true; + } else { + ASF.ArchiLogger.LogGenericWarning(str); + } + } + } + } catch (Exception ex) { + ASF.ArchiLogger.LogGenericException(ex, "Community with ASFEnhance failed"); + } + + return false; + } +} diff --git a/BoosterManager/BoosterManager.cs b/BoosterManager/BoosterManager.cs index 66ac1be..be42569 100644 --- a/BoosterManager/BoosterManager.cs +++ b/BoosterManager/BoosterManager.cs @@ -6,67 +6,86 @@ using ArchiSteamFarm.Core; using ArchiSteamFarm.Steam; using ArchiSteamFarm.Plugins.Interfaces; -using Newtonsoft.Json.Linq; using ArchiSteamFarm.Steam.Exchange; +using System.Text.Json; +using ArchiSteamFarm.Helpers.Json; +using System.Reflection; namespace BoosterManager { [Export(typeof(IPlugin))] public sealed class BoosterManager : IASF, IBotModules, IBotCommand2, IBotTradeOfferResults { public string Name => nameof(BoosterManager); public Version Version => typeof(BoosterManager).Assembly.GetName().Version ?? new Version("0"); + private bool ASFEnhanceEnabled = false; public Task OnLoaded() { ASF.ArchiLogger.LogGenericInfo("BoosterManager ASF Plugin by Citrinate"); + + // ASFEnhanced Adapter https://github.com/chr233/ASFEnhanceAdapterDemoPlugin + var flag = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + var handler = typeof(Commands).GetMethod(nameof(Commands.Response), flag); + const string pluginId = nameof(BoosterManager); + const string cmdPrefix = "BOOSTERMANAGER"; + const string repoName = "Citrinate/BoosterManager"; + var registered = AdapterBridge.InitAdapter(Name, pluginId, cmdPrefix, repoName, handler); + ASFEnhanceEnabled = registered; + return Task.CompletedTask; } - public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) => await Commands.Response(bot, access, steamID, message, args).ConfigureAwait(false); + public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) { + if (ASFEnhanceEnabled) { + return null; + } + + return await Commands.Response(bot, access, steamID, message, args).ConfigureAwait(false); + } - public Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) { + public Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) { if (additionalConfigProperties == null) { return Task.FromResult(0); } - foreach (KeyValuePair configProperty in additionalConfigProperties) { + foreach (KeyValuePair configProperty in additionalConfigProperties) { switch (configProperty.Key) { - case "AllowCraftUntradableBoosters" when configProperty.Value.Type == JTokenType.Boolean: { + case "AllowCraftUntradableBoosters" when (configProperty.Value.ValueKind == JsonValueKind.True || configProperty.Value.ValueKind == JsonValueKind.False): { ASF.ArchiLogger.LogGenericInfo("Allow Craft Untradable Boosters : " + configProperty.Value); - BoosterHandler.AllowCraftUntradableBoosters = configProperty.Value.ToObject(); + BoosterHandler.AllowCraftUntradableBoosters = configProperty.Value.GetBoolean(); break; } - case "BoosterDelayBetweenBots" when configProperty.Value.Type == JTokenType.Integer: { + case "BoosterDelayBetweenBots" when configProperty.Value.ValueKind == JsonValueKind.Number: { ASF.ArchiLogger.LogGenericInfo("Booster Delay Between Bots : " + configProperty.Value); - BoosterHandler.UpdateBotDelays((int)configProperty.Value.ToObject()); + BoosterHandler.UpdateBotDelays(configProperty.Value.GetInt32()); break; } - case "BoosterDataAPI" when configProperty.Value.Type == JTokenType.String: { + case "BoosterDataAPI" when configProperty.Value.ValueKind == JsonValueKind.String: { ASF.ArchiLogger.LogGenericInfo("Booster Data API : " + configProperty.Value); - DataHandler.BoosterDataAPI = new Uri(configProperty.Value.ToObject()!); + DataHandler.BoosterDataAPI = new Uri(configProperty.Value.GetString()!); break; } - case "InventoryHistoryAPI" when configProperty.Value.Type == JTokenType.String: { + case "InventoryHistoryAPI" when configProperty.Value.ValueKind == JsonValueKind.String: { ASF.ArchiLogger.LogGenericInfo("Inventory History API : " + configProperty.Value); - DataHandler.InventoryHistoryAPI = new Uri(configProperty.Value.ToObject()!); + DataHandler.InventoryHistoryAPI = new Uri(configProperty.Value.GetString()!); break; } - case "MarketListingsAPI" when configProperty.Value.Type == JTokenType.String: { + case "MarketListingsAPI" when configProperty.Value.ValueKind == JsonValueKind.String: { ASF.ArchiLogger.LogGenericInfo("Market Listings API : " + configProperty.Value); - DataHandler.MarketListingsAPI = new Uri(configProperty.Value.ToObject()!); + DataHandler.MarketListingsAPI = new Uri(configProperty.Value.GetString()!); break; } - case "MarketHistoryAPI" when configProperty.Value.Type == JTokenType.String: { + case "MarketHistoryAPI" when configProperty.Value.ValueKind == JsonValueKind.String: { ASF.ArchiLogger.LogGenericInfo("Market History API : " + configProperty.Value); - DataHandler.MarketHistoryAPI = new Uri(configProperty.Value.ToObject()!); + DataHandler.MarketHistoryAPI = new Uri(configProperty.Value.GetString()!); break; } - case "LogDataPageDelay" or "MarketHistoryDelay" when configProperty.Value.Type == JTokenType.Integer: { + case "LogDataPageDelay" or "MarketHistoryDelay" when configProperty.Value.ValueKind == JsonValueKind.Number: { ASF.ArchiLogger.LogGenericInfo("Log Data Page Delay : " + configProperty.Value); - DataHandler.LogDataPageDelay = configProperty.Value.ToObject(); + DataHandler.LogDataPageDelay = configProperty.Value.GetUInt32(); break; } - case "InventoryHistoryAppFilter" when configProperty.Value.Type == JTokenType.Array && configProperty.Value.Any(): { + case "InventoryHistoryAppFilter" when configProperty.Value.ValueKind == JsonValueKind.Array && configProperty.Value.GetArrayLength() > 0: { ASF.ArchiLogger.LogGenericInfo("Inventory History App Filter : " + string.Join(",", configProperty.Value)); - List? appIDs = configProperty.Value.ToObject>(); + List? appIDs = configProperty.Value.ToJsonObject>(); if (appIDs == null) { ASF.ArchiLogger.LogNullError(appIDs); } else { @@ -80,18 +99,18 @@ public Task OnASFInit(IReadOnlyDictionary? additionalConfigPrope return Task.FromResult(0); } - public Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { + public Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { BoosterHandler.AddHandler(bot); if (additionalConfigProperties == null) { return Task.FromResult(0); } - foreach (KeyValuePair configProperty in additionalConfigProperties) { + foreach (KeyValuePair configProperty in additionalConfigProperties) { switch (configProperty.Key) { - case "GamesToBooster" when configProperty.Value.Type == JTokenType.Array && configProperty.Value.Any(): { + case "GamesToBooster" when configProperty.Value.ValueKind == JsonValueKind.Array && configProperty.Value.GetArrayLength() > 0: { bot.ArchiLogger.LogGenericInfo("Games To Booster : " + string.Join(",", configProperty.Value)); - HashSet? gameIDs = configProperty.Value.ToObject>(); + HashSet? gameIDs = configProperty.Value.ToJsonObject>(); if (gameIDs == null) { bot.ArchiLogger.LogNullError(gameIDs); } else { diff --git a/BoosterManager/BoosterManager.csproj b/BoosterManager/BoosterManager.csproj index 3543e8f..4b1f4a0 100644 --- a/BoosterManager/BoosterManager.csproj +++ b/BoosterManager/BoosterManager.csproj @@ -2,7 +2,7 @@ Citrinate - 2.8.2 + 2.8.3 enable latest net8.0 diff --git a/BoosterManager/Boosters/BoosterDatabase.cs b/BoosterManager/Boosters/BoosterDatabase.cs index 969eedf..7b3df05 100644 --- a/BoosterManager/Boosters/BoosterDatabase.cs +++ b/BoosterManager/Boosters/BoosterDatabase.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Text.Json.Serialization; +using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; -using Newtonsoft.Json; namespace BoosterManager { internal sealed class BoosterDatabase : SerializableFile { - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary BoosterLastCrafts = new(); + [JsonInclude] + private ConcurrentDictionary BoosterLastCrafts { get; init; } = new(); [JsonConstructor] private BoosterDatabase() { } @@ -22,6 +24,8 @@ private BoosterDatabase(string filePath) : this() { FilePath = filePath; } + protected override Task Save() => Save(this); + internal static BoosterDatabase? CreateOrLoad(string filePath) { if (string.IsNullOrEmpty(filePath)) { throw new ArgumentNullException(nameof(filePath)); @@ -42,7 +46,7 @@ private BoosterDatabase(string filePath) : this() { return null; } - boosterDatabase = JsonConvert.DeserializeObject(json); + boosterDatabase = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/BoosterManager/Boosters/BoosterLastCraft.cs b/BoosterManager/Boosters/BoosterLastCraft.cs index 0dbb145..0104e4f 100644 --- a/BoosterManager/Boosters/BoosterLastCraft.cs +++ b/BoosterManager/Boosters/BoosterLastCraft.cs @@ -1,13 +1,15 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BoosterManager { internal sealed class BoosterLastCraft { - [JsonProperty(Required = Required.Always)] - internal DateTime CraftTime; + [JsonInclude] + [JsonRequired] + internal DateTime CraftTime { get; set; } - [JsonProperty(Required = Required.Always)] - internal int BoosterDelay; + [JsonInclude] + [JsonRequired] + internal int BoosterDelay { get; set; } [JsonConstructor] private BoosterLastCraft() { } diff --git a/BoosterManager/Boosters/BoosterPageResponse.cs b/BoosterManager/Boosters/BoosterPageResponse.cs index 4605cef..edcf62b 100644 --- a/BoosterManager/Boosters/BoosterPageResponse.cs +++ b/BoosterManager/Boosters/BoosterPageResponse.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Text.RegularExpressions; using AngleSharp.Dom; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; -using Newtonsoft.Json; namespace BoosterManager { internal sealed class BoosterPageResponse { @@ -37,7 +37,7 @@ internal BoosterPageResponse(Bot bot, IDocument? boosterPage) { IEnumerable? enumerableBoosters; try { - enumerableBoosters = JsonConvert.DeserializeObject>(info.Value, new Steam.BoosterInfoDateConverter()); + enumerableBoosters = JsonSerializer.Deserialize>(info.Value, new JsonSerializerOptions { NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString }); } catch (JsonException ex) { Bot.ArchiLogger.LogGenericError(ex.Message); diff --git a/BoosterManager/Commands.cs b/BoosterManager/Commands.cs index 755231c..fd4de0e 100644 --- a/BoosterManager/Commands.cs +++ b/BoosterManager/Commands.cs @@ -7,7 +7,6 @@ using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using System.ComponentModel; -using System.Collections.Immutable; using System.Reflection; namespace BoosterManager { @@ -24,6 +23,9 @@ internal static class Commands { switch (args.Length) { case 1: switch (args[0].ToUpperInvariant()) { + case "BOOSTERMANAGER" when access >= EAccess.FamilySharing: + return String.Format("{0} {1}", nameof(BoosterManager), (typeof(BoosterManager).Assembly.GetName().Version ?? new Version("0")).ToString()); + case "BDROP" or "BDROPS": return await ResponseBoosterDrops(bot, access).ConfigureAwait(false); diff --git a/BoosterManager/Data/ItemIdentifier.cs b/BoosterManager/Data/ItemIdentifier.cs index e9b884e..3508c9b 100644 --- a/BoosterManager/Data/ItemIdentifier.cs +++ b/BoosterManager/Data/ItemIdentifier.cs @@ -92,10 +92,10 @@ internal bool IsItemMatch(Asset item) { } if (TextID != null) { - string? name = item.AdditionalPropertiesReadOnly?["name"].ToObject(); - string? marketName = item.AdditionalPropertiesReadOnly?["market_name"].ToObject(); - string? marketHashName = item.AdditionalPropertiesReadOnly?["market_hash_name"].ToObject(); - string? type = item.AdditionalPropertiesReadOnly?["type"].ToObject(); + string? name = item.AdditionalPropertiesReadOnly?["name"].GetString(); + string? marketName = item.AdditionalPropertiesReadOnly?["market_name"].GetString(); + string? marketHashName = item.AdditionalPropertiesReadOnly?["market_hash_name"].GetString(); + string? type = item.AdditionalPropertiesReadOnly?["type"].GetString(); if ((name == null || !name.Contains(TextID)) && (marketName == null || !marketName.Contains(TextID)) diff --git a/BoosterManager/Data/ItemListing.cs b/BoosterManager/Data/ItemListing.cs index f9ca089..ffe8afc 100644 --- a/BoosterManager/Data/ItemListing.cs +++ b/BoosterManager/Data/ItemListing.cs @@ -1,6 +1,7 @@ using System; +using System.Text.Json.Nodes; using ArchiSteamFarm.Core; -using Newtonsoft.Json.Linq; +using ArchiSteamFarm.Helpers.Json; namespace BoosterManager { internal sealed class ItemListing { @@ -12,10 +13,10 @@ internal sealed class ItemListing { internal ulong ContextID; internal ulong ClassID; - internal ItemListing(JObject listing) { + internal ItemListing(JsonObject listing) { string? name = listing["asset"]?["name"]?.ToString(); if (name == null) { - ASF.ArchiLogger.LogNullError(name); + ASF.ArchiLogger.LogNullError(name); throw new InvalidOperationException(); } @@ -37,19 +38,19 @@ internal ItemListing(JObject listing) { throw new InvalidOperationException(); } - uint? appID = listing["asset"]?["appid"]?.ToObject(); + uint? appID = listing["asset"]?["appid"]?.ToString().ToJsonObject(); if (appID == null) { ASF.ArchiLogger.LogNullError(appID); throw new InvalidOperationException(); } - ulong? contextID = listing["asset"]?["contextid"]?.ToObject(); + ulong? contextID = listing["asset"]?["contextid"]?.ToString().ToJsonObject(); if (contextID == null) { ASF.ArchiLogger.LogNullError(contextID); throw new InvalidOperationException(); } - ulong? classID = listing["asset"]?["classid"]?.ToObject(); + ulong? classID = listing["asset"]?["classid"]?.ToString().ToJsonObject(); if (classID == null) { ASF.ArchiLogger.LogNullError(classID); throw new InvalidOperationException(); diff --git a/BoosterManager/Data/SteamData.cs b/BoosterManager/Data/SteamData.cs index 71dc0b7..b497091 100644 --- a/BoosterManager/Data/SteamData.cs +++ b/BoosterManager/Data/SteamData.cs @@ -1,23 +1,28 @@ using System; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam; -using Newtonsoft.Json; namespace BoosterManager { internal sealed class SteamData { - [JsonProperty(PropertyName = "steamid")] - public ulong SteamID; + [JsonInclude] + [JsonPropertyName("steamid")] + public ulong SteamID { get; private init; } - [JsonProperty(PropertyName = "source")] - public string Source; + [JsonInclude] + [JsonPropertyName("source")] + public string Source { get; private init; } - [JsonProperty(PropertyName = "page")] - public uint? Page; + [JsonInclude] + [JsonPropertyName("page")] + public uint? Page { get; private init; } - [JsonProperty(PropertyName = "cursor")] - public Steam.InventoryHistoryCursor? Cursor; + [JsonInclude] + [JsonPropertyName("cursor")] + public Steam.InventoryHistoryCursor? Cursor { get; private init; } - [JsonProperty(PropertyName = "data")] - public T Data; + [JsonInclude] + [JsonPropertyName("data")] + public T Data { get; private init; } internal SteamData(Bot bot, T steamData, Uri source, uint? page, Steam.InventoryHistoryCursor? cursor) { SteamID = bot.SteamID; diff --git a/BoosterManager/Data/SteamDataResponse.cs b/BoosterManager/Data/SteamDataResponse.cs index 919f9d8..e08b6be 100644 --- a/BoosterManager/Data/SteamDataResponse.cs +++ b/BoosterManager/Data/SteamDataResponse.cs @@ -1,24 +1,31 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BoosterManager { internal sealed class SteamDataResponse { - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success = false; + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + internal bool Success { get; private init; } = false; - [JsonProperty(PropertyName = "message", Required = Required.Default)] - internal readonly string? Message = null; + [JsonInclude] + [JsonPropertyName("message")] + internal string? Message { get; private init; } = null; - [JsonProperty(PropertyName = "show_message", Required = Required.DisallowNull)] - internal readonly bool ShowMessage = true; + [JsonInclude] + [JsonPropertyName("show_message")] + internal bool ShowMessage { get; private init; } = true; - [JsonProperty(PropertyName = "get_next_page", Required = Required.DisallowNull)] - internal readonly bool GetNextPage = false; + [JsonInclude] + [JsonPropertyName("get_next_page")] + internal bool GetNextPage { get; private init; } = false; - [JsonProperty(PropertyName = "next_page", Required = Required.Default)] - internal readonly uint? NextPage = null; + [JsonInclude] + [JsonPropertyName("next_page")] + internal uint? NextPage { get; private init; } = null; - [JsonProperty(PropertyName = "next_cursor", Required = Required.Default)] - internal readonly Steam.InventoryHistoryCursor? NextCursor = null; + [JsonInclude] + [JsonPropertyName("next_cursor")] + internal Steam.InventoryHistoryCursor? NextCursor { get; private init; } = null; [JsonConstructor] internal SteamDataResponse() { } diff --git a/BoosterManager/Docs/InventoryHistory.md b/BoosterManager/Docs/InventoryHistory.md index 2e85df8..3f4baa6 100644 --- a/BoosterManager/Docs/InventoryHistory.md +++ b/BoosterManager/Docs/InventoryHistory.md @@ -110,7 +110,7 @@ Be aware that each of these descriptions describes a unique type of event. For Event descriptions for Valve games can be found in the following files as strings starting with `ItemHistory_Action` - Artifact Classic: [game/dcg/resource/dcg_common_english.txt](https://github.com/SteamDatabase/GameTracking-Artifact/blob/master/game/dcg/resource/dcg_common_english.txt) -- Counter-Strike: Global Offensive: [csgo/resource/csgo_english.txt](https://github.com/SteamDatabase/GameTracking-CS2/blob/master/game/csgo/resource/csgo_english.txt) +- Counter-Strike 2: [csgo/resource/csgo_english.txt](https://github.com/SteamDatabase/GameTracking-CS2/blob/master/game/csgo/pak01_dir/resource/csgo_english.txt) - Dota 2: [game/dota/pak01_dir/resource/localization/dota_english.txt](https://github.com/SteamDatabase/GameTracking-Dota2/blob/6abdd9d13de2f4330ca748082467b9ff6e6cd928/game/dota/pak01_dir/resource/localization/dota_english.txt) - Portal 2: [portal2/portal2/resource/portal2_english.txt](https://github.com/SteamDatabase/GameTracking/blob/master/portal2/portal2/resource/portal2_english.txt) - Team Fortress 2: [tf/resource/tf_english.txt](https://github.com/SteamDatabase/GameTracking-TF2/blob/master/tf/resource/tf_english.txt) @@ -122,18 +122,18 @@ The Inventory History API provides no way to fetch specific pages, instead we sp As an example, assuming you know there should be history on your account between `4/30/21` and `1/5/21`, your history might look like this: ``` -… → 5/2/21 → 5/1/21 → 4/30/21 → 1/5/21 → 1/4/21 → … +… → 5/2/21 → 5/1/21 → 4/30/21 → … Missing … → 1/5/21 → 1/4/21 → … ``` This bug can be addressed by searching for history within the gap. You can use the in-browser "Jump to date" feature, or try setting the `start_time` parameter yourself. It may take several attempts to find a value for `start_time` that causes the missing history to re-appear. -If you search at date `x` and find missing history there, then history older than `x` should also re-appear, but history newer than `x` might not. Looking at the previous example and assuming there's history between `4/30/21` and `3/14/21`; if `x = 3/14/21` then the gap may shrink, but not disappear, and some of the missing history may also show up right before `3/14/21`: +If you search at date `x` and find missing history there, then history older than `x` should also re-appear, but history newer than `x` might not. Looking at the previous example and assuming there's history between `4/30/21` and `3/14/21`; if `x = 3/14/21` then the gap may shrink, but not disappear: ``` -… → 5/2/21 → 5/1/21 → 4/30/21 → 3/15/21 → 3/14/21 → 3/13/21 → … → 1/5/21 → 1/4/21 → … +… → 5/2/21 → 5/1/21 → 4/30/21 → … Still Missing … → 3/15/21 → 3/14/21 → 3/13/21 → … Not Missing Anymore … → 1/6/21 → 1/5/21 → … ``` -For this reason it's better to start your search right where the gap begins and proceed gradually. Setting the `start_time` parameter yourself allows you to move in increments of 1 second. The "Jump to date" feature moves in increments of 24 hours. You can also use the `cursor[time]` and `cursor[time_frac]` parameters to move in increments of 1 millisecond. +For this reason it's better to start your search right where the gap begins and proceed gradually. The "Jump to date" feature moves in increments of 24 hours. Setting the `start_time` parameter yourself allows you to move in increments of 1 second. You can also use the `cursor[time]` and `cursor[time_frac]` parameters to move in increments of 1 millisecond. Not all gaps are as large as in the examples above. It's very common to have lots of small gaps when numerous events share the same time (ex: confirming multiple market listings at once). Here the gaps length can be shorter than a second, and may skip as few as 1 event. These gaps can be addressed in the same way as large gaps, but because of how small they are, they're very hard to identify and correct for. diff --git a/BoosterManager/Docs/ItemIDs.md b/BoosterManager/Docs/ItemIDs.md new file mode 100644 index 0000000..5da3419 --- /dev/null +++ b/BoosterManager/Docs/ItemIDs.md @@ -0,0 +1,67 @@ +# How to find Steam Item IDs + +## AppID + +An AppID refers to a specific Steam App. This can be found in the url of an App's inventory page. + +--- + +#### Example + +`https://steamcommunity.com/id/████/inventory/#753` + +AppID of `753` + +--- + +## ContextID + +Apps can have multiple inventories, and the ContextID refers to the inventory an item exists in. The ContextID can be found in the url you'd get by right clicking any item in your inventory, and selecting "Copy link address". + +--- + +#### Example + +`https://steamcommunity.com/id/████/inventory/#753_6_22000101010` + +AppID of `753` + +ContextID of `6` + +AssetID of `22000101010` + +--- + +## ClassID + +The ClassID refers to all copies of an item. This can be found in the source code of an item's market listing page. Alternatively, or if an item doesn't have a market listing page, this can be found at `https://steamcommunity.com/my/inventory/json/AppID/ContextID`. There also exists another location which can be used similarly at `https://steamcommunity.com/inventory/SteamID/AppID/ContextID`, which has the query parameters `count` and `start_assetid`. + +--- + +#### Example using a Market Listing Page + +The source code for [:TheMessenger:](https://steamcommunity.com/market/listings/753/764790-%3ATheMessenger%3A) contains the text: + +```javascript +var g_rgAssets = {"753":{"6":{"28191259516":{"currency":0,"appid":753,"contextid":"6","id":"28191259516","classid":"2994832731","instanceid":"0" +``` + +AppID of `753` + +ContextID of `6` + +ClassID of `2994832731` + +--- + +#### Example using JSON inventory + +It will be necessary to first get the AppID, ContextID, and AssetID of the item. The AssetID refers to a specific copy of an item, and all three can be found using the above method to find the ContextID. + +When navigating to https://steamcommunity.com/my/inventory/json/753/6 and searching for the AssetID of `22000101010`, we may find something like this: + +```json +{"22000101010":{"id":"22000101010","classid":"2994832731","instanceid":"0","amount":"1","hide_in_china":0,"pos":42}, +``` + +ClassID of `2994832731` diff --git a/BoosterManager/Handlers/GemHandler.cs b/BoosterManager/Handlers/GemHandler.cs index 976f01a..e6a3034 100644 --- a/BoosterManager/Handlers/GemHandler.cs +++ b/BoosterManager/Handlers/GemHandler.cs @@ -54,7 +54,7 @@ internal static async Task UnpackGems(Bot bot) { foreach (Asset sack in sacks) { Steam.ExchangeGooResponse? response = await WebRequest.UnpackGems(bot, sack.AssetID, sack.Amount); - if (response == null || !response.Success) { + if (response == null || response.Success != 1) { return Commands.FormatBotResponse(bot, Strings.WarningFailed); } } diff --git a/BoosterManager/Handlers/MarketHandler.cs b/BoosterManager/Handlers/MarketHandler.cs index 616bc86..997d77b 100644 --- a/BoosterManager/Handlers/MarketHandler.cs +++ b/BoosterManager/Handlers/MarketHandler.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Threading.Tasks; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json.Linq; namespace BoosterManager { internal static class MarketHandler { @@ -34,15 +35,15 @@ internal static async Task GetValue(Bot bot, uint subtractFrom = 0) { } private static async Task GetMarketListingsValue(Bot bot) { - Dictionary? listings = await GetFullMarketListings(bot).ConfigureAwait(false); + Dictionary? listings = await GetFullMarketListings(bot).ConfigureAwait(false); if (listings == null) { return null; } uint listingsValue = 0; - foreach (JObject listing in listings.Values) { - uint? price = listing["price"]?.ToObject(); + foreach (JsonObject listing in listings.Values) { + uint? price = listing["price"]?.ToString().ToJsonObject(); if (price == null) { bot.ArchiLogger.LogNullError(price); @@ -106,8 +107,8 @@ internal static async Task FindAndRemoveListings(Bot bot, List?> GetFullMarketListings(Bot bot, uint delayInMilliseconds = 5000) { - Dictionary? listings = null; + private static async Task?> GetFullMarketListings(Bot bot, uint delayInMilliseconds = 5000) { + Dictionary? listings = null; uint totalListings = 0; uint listingsCollected = 0; do { @@ -128,27 +129,27 @@ internal static async Task FindAndRemoveListings(Bot bot, List((int)totalListings); + listings = new Dictionary((int)totalListings); } - foreach (JObject listing in marketListings.Listings) { - ulong? listingid = listing["listingid"]?.ToObject(); + foreach (JsonNode? listing in marketListings.Listings) { + if (listing == null) { + bot.ArchiLogger.LogNullError(listing); + + return null; + } + + ulong? listingid = listing["listingid"]?.ToString().ToJsonObject(); if (listingid == null) { bot.ArchiLogger.LogNullError(listingid); return null; } - if (listings.TryAdd(listingid.Value, listing)) { + if (listings.TryAdd(listingid.Value, listing.AsObject())) { listingsCollected++; } } @@ -158,14 +159,14 @@ internal static async Task FindAndRemoveListings(Bot bot, List>?> GetListingIDsFromIdentifiers(Bot bot, List itemIdentifiers) { - Dictionary? listings = await GetFullMarketListings(bot).ConfigureAwait(false); + Dictionary? listings = await GetFullMarketListings(bot).ConfigureAwait(false); if (listings == null) { return null; } Dictionary> filteredListings = new Dictionary>(); - foreach ((ulong listingID, JObject listing) in listings) { + foreach ((ulong listingID, JsonObject listing) in listings) { ItemListing item; try { item = new ItemListing(listing); diff --git a/BoosterManager/IPC/Api/BoosterManagerController.cs b/BoosterManager/IPC/Api/BoosterManagerController.cs index dacdb1c..83a27ef 100644 --- a/BoosterManager/IPC/Api/BoosterManagerController.cs +++ b/BoosterManager/IPC/Api/BoosterManagerController.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using ArchiSteamFarm.IPC.Controllers.Api; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; using Swashbuckle.AspNetCore.Annotations; namespace BoosterManager { @@ -150,7 +150,7 @@ public async Task> InventoryHistory(string botName /// [HttpGet("{botName:required}/GetBadgeInfo/{appID:required}")] [SwaggerOperation (Summary = "Retrieves badge info for given bot.")] - [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] public async Task> GetBadgeInfo(string botName, uint appID, uint border = 0) { if (string.IsNullOrEmpty(botName)) { @@ -166,12 +166,12 @@ public async Task> GetBadgeInfo(string botName, ui return BadRequest(new GenericResponse(false, Strings.BotNotConnected)); } - JToken? badgeInfo = await WebRequest.GetBadgeInfo(bot, appID, border).ConfigureAwait(false); + JsonDocument? badgeInfo = await WebRequest.GetBadgeInfo(bot, appID, border).ConfigureAwait(false); if (badgeInfo == null) { return BadRequest(new GenericResponse(false, "Failed to fetch badge info")); } - return Ok(new GenericResponse(true, badgeInfo)); + return Ok(new GenericResponse(true, badgeInfo)); } /// @@ -179,7 +179,7 @@ public async Task> GetBadgeInfo(string botName, ui /// [HttpGet("{botNames:required}/GetPriceHistory/{appID:required}/{hashName:required}")] [SwaggerOperation (Summary = "Retrieves price history for market items.")] - [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] public async Task> GetPriceHistory(string botNames, uint appID, string hashName) { if (string.IsNullOrEmpty(botNames)) { @@ -200,12 +200,12 @@ public async Task> GetPriceHistory(string botNames return BadRequest(new GenericResponse(false, Strings.BotNotConnected)); } - JToken? priceHistory = await WebRequest.GetPriceHistory(bot, appID, hashName).ConfigureAwait(false); + JsonDocument? priceHistory = await WebRequest.GetPriceHistory(bot, appID, hashName).ConfigureAwait(false); if (priceHistory == null) { return BadRequest(new GenericResponse(false, "Failed to fetch price history")); } - return Ok(new GenericResponse(true, priceHistory)); + return Ok(new GenericResponse(true, priceHistory)); } } } \ No newline at end of file diff --git a/BoosterManager/Json.cs b/BoosterManager/Json.cs index e4eedd8..27c3e0c 100644 --- a/BoosterManager/Json.cs +++ b/BoosterManager/Json.cs @@ -1,137 +1,202 @@ using System; using System.Collections.Generic; using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using SteamKit2; #pragma warning disable 649 namespace BoosterManager { internal static class Steam { internal class EResultResponse { - [JsonProperty(PropertyName = "success", Required = Required.Always)] - public readonly EResult Result; + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + public EResult Result { get; private init; } [JsonConstructor] public EResultResponse() { } } internal sealed class BoosterInfo { - [JsonProperty(PropertyName = "appid", Required = Required.Always)] - internal readonly uint AppID; - - [JsonProperty(PropertyName = "name", Required = Required.Always)] - internal readonly string Name = ""; - - [JsonProperty(PropertyName = "series", Required = Required.Always)] - internal readonly uint Series; - - [JsonProperty(PropertyName = "price", Required = Required.Always)] - internal readonly uint Price; - - [JsonProperty(PropertyName = "unavailable", Required = Required.DisallowNull)] - internal readonly bool Unavailable; - - [JsonProperty(PropertyName = "available_at_time", Required = Required.Default)] - internal readonly DateTime? AvailableAtTime; + [JsonInclude] + [JsonPropertyName("appid")] + [JsonRequired] + internal uint AppID { get; private init; } + + [JsonInclude] + [JsonPropertyName("name")] + [JsonRequired] + internal string Name { get; private init; } = ""; + + [JsonInclude] + [JsonPropertyName("series")] + [JsonRequired] + internal uint Series { get; private init; } + + [JsonInclude] + [JsonPropertyName("price")] + [JsonRequired] + internal uint Price { get; private init; } + + [JsonInclude] + [JsonPropertyName("unavailable")] + internal bool Unavailable { get; private init; } + + [JsonInclude] + [JsonPropertyName("available_at_time")] + [JsonConverter(typeof(BoosterInfoDateConverter))] + internal DateTime? AvailableAtTime { get; private init; } [JsonConstructor] public BoosterInfo() { } } internal sealed class BoostersResponse { - [JsonProperty(PropertyName = "goo_amount", Required = Required.Always)] - internal readonly uint GooAmount; - - [JsonProperty(PropertyName = "tradable_goo_amount", Required = Required.Always)] - internal readonly uint TradableGooAmount; - - [JsonProperty(PropertyName = "untradable_goo_amount", Required = Required.Always)] - internal readonly uint UntradableGooAmount; - - [JsonProperty(PropertyName = "purchase_result", Required = Required.DisallowNull)] - internal readonly EResultResponse Result = new(); - - [JsonProperty(PropertyName = "purchase_eresult", Required = Required.DisallowNull)] - internal readonly EResult PurchaseEResult; + [JsonInclude] + [JsonPropertyName("goo_amount")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + [JsonRequired] + internal uint GooAmount { get; private init; } + + [JsonInclude] + [JsonPropertyName("tradable_goo_amount")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + [JsonRequired] + internal uint TradableGooAmount { get; private init; } + + [JsonInclude] + [JsonPropertyName("untradable_goo_amount")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + [JsonRequired] + internal uint UntradableGooAmount { get; private init; } + + [JsonInclude] + [JsonPropertyName("purchase_result")] + internal EResultResponse Result { get; private init; } = new(); + + [JsonInclude] + [JsonPropertyName("purchase_eresult")] + internal EResult PurchaseEResult { get; private init; } [JsonConstructor] private BoostersResponse() { } } internal sealed class MarketListingsResponse { - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success; - - [JsonProperty(PropertyName = "pagesize", Required = Required.Always)] - internal readonly int PageSize; - - [JsonProperty(PropertyName = "total_count", Required = Required.Always)] - internal readonly uint TotalCount; - - [JsonProperty(PropertyName = "assets", Required = Required.Always)] - internal readonly JToken? Assets; - - [JsonProperty(PropertyName = "start", Required = Required.Always)] - internal readonly uint Start; - - [JsonProperty(PropertyName = "num_active_listings", Required = Required.Always)] - internal readonly uint NumActiveListings; - - [JsonProperty(PropertyName = "listings", Required = Required.AllowNull)] - internal readonly JArray? Listings; - - [JsonProperty(PropertyName = "listings_on_hold", Required = Required.Always)] - internal readonly JArray? ListingsOnHold = new(); - - [JsonProperty(PropertyName = "listings_to_confirm", Required = Required.Always)] - internal readonly JArray ListingsToConfirm = new(); - - [JsonProperty(PropertyName = "buy_orders", Required = Required.Always)] - internal readonly JArray BuyOrders = new(); + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + internal bool Success { get; private init; } + + [JsonInclude] + [JsonPropertyName("pagesize")] + [JsonRequired] + internal int PageSize { get; private init; } + + [JsonInclude] + [JsonPropertyName("total_count")] + [JsonRequired] + internal uint TotalCount { get; private init; } + + [JsonInclude] + [JsonPropertyName("assets")] + [JsonRequired] + internal JsonElement? Assets { get; private init; } + + [JsonInclude] + [JsonPropertyName("start")] + [JsonRequired] + internal uint Start { get; private init; } + + [JsonInclude] + [JsonPropertyName("num_active_listings")] + [JsonRequired] + internal uint NumActiveListings { get; private init; } + + [JsonInclude] + [JsonPropertyName("listings")] + [JsonRequired] + internal JsonArray? Listings { get; private init; } + + [JsonInclude] + [JsonPropertyName("listings_on_hold")] + [JsonRequired] + internal JsonArray? ListingsOnHold { get; private init; } = new(); + + [JsonInclude] + [JsonPropertyName("listings_to_confirm")] + [JsonRequired] + internal JsonArray ListingsToConfirm { get; private init; } = new(); + + [JsonInclude] + [JsonPropertyName("buy_orders")] + [JsonRequired] + internal JsonArray BuyOrders { get; private init; } = new(); [JsonConstructor] private MarketListingsResponse() { } } internal sealed class MarketHistoryResponse { - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success; - - [JsonProperty(PropertyName = "pagesize", Required = Required.Always)] - internal readonly uint PageSize; - - [JsonProperty(PropertyName = "total_count", Required = Required.Always)] - internal readonly uint? TotalCount; - - [JsonProperty(PropertyName = "start", Required = Required.Always)] - internal readonly uint Start; - - [JsonProperty(PropertyName = "assets", Required = Required.Always)] - internal readonly JToken? Assets; - - [JsonProperty(PropertyName = "events", Required = Required.Default)] - internal readonly JArray? Events = new(); - - [JsonProperty(PropertyName = "purchases", Required = Required.Default)] - internal readonly JToken? Purchases; - - [JsonProperty(PropertyName = "listings", Required = Required.Always)] - internal readonly JToken? Listings; + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + internal bool Success { get; private init; } + + [JsonInclude] + [JsonPropertyName("pagesize")] + [JsonRequired] + internal uint PageSize { get; private init; } + + [JsonInclude] + [JsonPropertyName("total_count")] + [JsonRequired] + internal uint? TotalCount { get; private init; } + + [JsonInclude] + [JsonPropertyName("start")] + [JsonRequired] + internal uint Start { get; private init; } + + [JsonInclude] + [JsonPropertyName("assets")] + [JsonRequired] + internal JsonElement? Assets { get; private init; } + + [JsonInclude] + [JsonPropertyName("events")] + internal JsonArray? Events { get; private init; } + + [JsonInclude] + [JsonPropertyName("purchases")] + internal JsonElement? Purchases { get; private init; } + + [JsonInclude] + [JsonPropertyName("listings")] + [JsonRequired] + internal JsonElement? Listings { get; private init; } [JsonConstructor] private MarketHistoryResponse() { } } internal sealed class InventoryHistoryCursor { - [JsonProperty(PropertyName = "time", Required = Required.Always)] - internal readonly uint Time; + [JsonInclude] + [JsonPropertyName("time")] + [JsonRequired] + internal uint Time { get; private init; } - [JsonProperty(PropertyName = "time_frac", Required = Required.Always)] - internal readonly uint TimeFrac; + [JsonInclude] + [JsonPropertyName("time_frac")] + [JsonRequired] + internal uint TimeFrac { get; private init; } - [JsonProperty(PropertyName = "s", Required = Required.Always)] - internal readonly string S = ""; + [JsonInclude] + [JsonPropertyName("s")] + [JsonRequired] + internal string S { get; private init; } = ""; [JsonConstructor] internal InventoryHistoryCursor() { } @@ -144,41 +209,52 @@ internal InventoryHistoryCursor(uint time, uint timeFrac, string s) { } internal sealed class InventoryHistoryResponse { - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success; + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + internal bool Success { get; private init; } - [JsonProperty(PropertyName = "error", Required = Required.Default)] - internal readonly string? Error = ""; + [JsonInclude] + [JsonPropertyName("error")] + internal string? Error { get; private init; } = ""; - [JsonProperty(PropertyName = "html", Required = Required.Default)] - internal readonly string Html = ""; + [JsonInclude] + [JsonPropertyName("html")] + internal string Html { get; private init; } = ""; - [JsonProperty(PropertyName = "num", Required = Required.Default)] - internal readonly uint Num = 0; + [JsonInclude] + [JsonPropertyName("num")] + internal uint Num { get; private init; } = 0; - [JsonProperty(PropertyName = "descriptions", Required = Required.Default)] - internal readonly JToken? Descriptions; + [JsonInclude] + [JsonPropertyName("descriptions")] + internal JsonElement? Descriptions { get; private init; } - [JsonProperty(PropertyName = "apps", Required = Required.Default)] - internal readonly JArray Apps = new(); + [JsonInclude] + [JsonPropertyName("apps")] + internal JsonArray Apps { get; private init; } = new(); - [JsonProperty(PropertyName = "cursor", Required = Required.Default)] - internal readonly InventoryHistoryCursor? Cursor; + [JsonInclude] + [JsonPropertyName("cursor")] + internal InventoryHistoryCursor? Cursor { get; private init; } [JsonConstructor] private InventoryHistoryResponse() { } } internal sealed class ExchangeGooResponse { - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success; + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + internal int Success { get; private init; } [JsonConstructor] private ExchangeGooResponse() { } } // https://stackoverflow.com/a/51319347 - internal sealed class BoosterInfoDateConverter : JsonConverter { + // internal sealed class BoosterInfoDateConverter : JsonConverter { + internal sealed class BoosterInfoDateConverter : JsonConverter { private List DateTimeFormats = new List() { "MMM d @ h:mmtt", "MMM d, yyyy @ h:mmtt", @@ -186,30 +262,24 @@ internal sealed class BoosterInfoDateConverter : JsonConverter { "d MMM, yyyy @ h:mmtt" }; - public override bool CanConvert(Type objectType) { - return objectType == typeof(DateTime?); - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - if (reader.Value == null) { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + string? dateString = reader.GetString(); + if (dateString == null) { throw new JsonException("Unable to parse null as a date."); } - string dateString = (string)reader.Value; + DateTime date; foreach (string format in DateTimeFormats) { if (DateTime.TryParseExact(dateString, format, new CultureInfo("en-US"), DateTimeStyles.None, out date)) { return date; } } + throw new JsonException("Unable to parse \"" + dateString + "\" as a date."); } - - public override bool CanWrite { - get { return false; } - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { + writer.WriteStringValue(value.ToString("s", System.Globalization.CultureInfo.InvariantCulture)); } } } diff --git a/BoosterManager/WebRequest.cs b/BoosterManager/WebRequest.cs index b39e305..55a0733 100644 --- a/BoosterManager/WebRequest.cs +++ b/BoosterManager/WebRequest.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.Net; +using System.Text.Json; using System.Threading.Tasks; +using ArchiSteamFarm.Core; using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; -using Newtonsoft.Json.Linq; namespace BoosterManager { internal static class WebRequest { @@ -19,7 +20,9 @@ internal static class WebRequest { BoosterPageResponse boosterPageResponse = new BoosterPageResponse(bot, boosterPage?.Content); return (boosterPageResponse, request); - } catch (Exception) { + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + return (null, request); } } @@ -120,15 +123,15 @@ internal static async Task SendSteamData(Uri request, Bot return response.Content; } - internal static async Task GetBadgeInfo(Bot bot, uint appID, uint border = 0) { + internal static async Task GetBadgeInfo(Bot bot, uint appID, uint border = 0) { Uri request = new(ArchiWebHandler.SteamCommunityURL, String.Format("/profiles/{0}/ajaxgetbadgeinfo/{1}?border={2}", bot.SteamID, appID, border)); - ObjectResponse? badgeInfoResponse = await bot.ArchiWebHandler.UrlGetToJsonObjectWithSession(request).ConfigureAwait(false); + ObjectResponse? badgeInfoResponse = await bot.ArchiWebHandler.UrlGetToJsonObjectWithSession(request).ConfigureAwait(false); return badgeInfoResponse?.Content; } - internal static async Task GetPriceHistory(Bot bot, uint appID, string hashName) { + internal static async Task GetPriceHistory(Bot bot, uint appID, string hashName) { Uri request = new(ArchiWebHandler.SteamCommunityURL, String.Format("/market/pricehistory/?appid={0}&market_hash_name={1}", appID, Uri.EscapeDataString(hashName))); - ObjectResponse? priceHistoryResponse = await bot.ArchiWebHandler.UrlGetToJsonObjectWithSession(request).ConfigureAwait(false); + ObjectResponse? priceHistoryResponse = await bot.ArchiWebHandler.UrlGetToJsonObjectWithSession(request).ConfigureAwait(false); return priceHistoryResponse?.Content; } } diff --git a/README.md b/README.md index f2bcfb0..9f9bb83 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,9 @@ Format | Example | `AppID::ContextID`|The identifier `753::6` will match with all Steam Community items `AppID::ContextID::ClassID`|The identifier `753::6::667933237` will match all "Sack of Gems" items +> **Note** +> Information on how to determine an item's `AppID`, `ContextID`, and `ClassID` may be found [here](https://github.com/Citrinate/BoosterManager/blob/master/BoosterManager/Docs/ItemIDs.md). + --- ### Command Aliases