From 6b0bf0f9c17f7a69c7dc2a39b7986e9a19cd85cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Wed, 21 Feb 2024 03:09:36 +0100 Subject: [PATCH] Closes #3061 (#3145) * Good start * Misc * Make ApiAuthenticationMiddleware use new json * Remove first newtonsoft dependency * Pull latest ASFB json enhancements * Start reimplementing newtonsoft! * One thing at a time * Keep doing all kind of breaking changes which need to be tested later * Add back ShouldSerialize() support * Misc * Eradicate remaining parts of newtonsoft * WIP * Workaround STJ stupidity in regards to derived types STJ can't serialize derived type properties by default, so we'll use another approach in our serializable file function * Make CI happy * Bunch of further fixes * Fix AddFreeLicense() after rewrite * Add full support for JsonDisallowNullAttribute * Optimize our json utilities even further * Misc * Add support for fields in disallow null * Misc optimization * Fix deserialization of GlobalCache in STD * Fix non-public [JsonExtensionData] * Fix IM missing method exception, correct db storage helpers * Fix saving into generic databases Thanks STJ * Make Save() function abstract to force inheritors to implement it properly * Correct ShouldSerializeAdditionalProperties to be a method * Misc cleanup * Code review * Allow JSON comments in configs, among other * Allow trailing commas in configs Users very often add them accidentally, no reason to throw on them * Fix confirmation ID Probably needs further fixes, will need to check later * Correct confirmations deserialization * Use JsonNumberHandling * Misc * Misc * [JsonDisallowNull] corrections * Forbid [JsonDisallowNull] on non-nullable structs * Not really but okay * Add and use ToJson() helpers * Misc * Misc --- ...eamFarm.CustomPlugins.ExamplePlugin.csproj | 1 - .../ExamplePlugin.cs | 25 ++- .../MeowResponse.cs | 8 +- .../PeriodicGCPlugin.cs | 6 + ...mFarm.CustomPlugins.SignInWithSteam.csproj | 1 - .../Data/SignInWithSteamRequest.cs | 7 +- .../Data/SignInWithSteamResponse.cs | 7 +- .../SignInWithSteamPlugin.cs | 6 + ...amFarm.OfficialPlugins.ItemsMatcher.csproj | 1 - .../BotCache.cs | 24 ++- .../Data/AnnouncementDiffRequest.cs | 12 +- .../Data/AnnouncementRequest.cs | 55 +++--- .../Data/AssetForListing.cs | 16 +- .../Data/AssetForMatching.cs | 38 ++-- .../Data/AssetInInventory.cs | 8 +- .../Data/BackgroundTaskResponse.cs | 16 +- .../Data/HeartBeatRequest.cs | 12 +- .../Data/InventoriesRequest.cs | 22 ++- .../Data/ListedUser.cs | 58 +++--- .../Data/SetPart.cs | 34 ++-- .../Data/SetPartsRequest.cs | 22 ++- .../ItemsMatcherPlugin.cs | 13 +- .../RemoteCommunication.cs | 20 +- ...OfficialPlugins.MobileAuthenticator.csproj | 1 - .../Commands.cs | 8 +- .../MaFileData.cs | 95 +++++---- .../MaFileSessionData.cs | 7 +- .../MobileAuthenticatorPlugin.cs | 9 +- ...rm.OfficialPlugins.SteamTokenDumper.csproj | 1 - .../Data/SubmitRequest.cs | 32 +-- .../Data/SubmitResponse.cs | 17 +- .../Data/SubmitResponseData.cs | 48 +++-- .../GlobalCache.cs | 42 ++-- .../GlobalConfigExtension.cs | 10 +- .../SteamTokenDumperConfig.cs | 24 ++- .../SteamTokenDumperPlugin.cs | 28 ++- ArchiSteamFarm/ArchiSteamFarm.csproj | 2 - .../Collections/ConcurrentHashSet.cs | 2 +- ArchiSteamFarm/Collections/ConcurrentList.cs | 2 +- .../ObservableConcurrentDictionary.cs | 3 +- .../Helpers/Json/JsonDisallowNullAttribute.cs | 30 +++ ArchiSteamFarm/Helpers/Json/JsonUtilities.cs | 186 ++++++++++++++++++ ArchiSteamFarm/Helpers/SerializableFile.cs | 50 +++-- ArchiSteamFarm/IPC/ArchiKestrel.cs | 8 +- .../IPC/Controllers/Api/ASFController.cs | 6 +- .../IPC/Controllers/Api/BotController.cs | 6 +- .../IPC/Controllers/Api/NLogController.cs | 6 +- .../IPC/Controllers/Api/StorageController.cs | 17 +- .../IPC/Controllers/Api/TypeController.cs | 34 ++-- .../ApiAuthenticationMiddleware.cs | 4 +- .../Integration/ReadOnlyFixesSchemaFilter.cs | 40 ++++ .../SwaggerSteamIdentifierAttribute.cs | 8 +- .../SwaggerValidValuesAttribute.cs | 4 +- .../IPC/Requests/ASFEncryptRequest.cs | 12 +- ArchiSteamFarm/IPC/Requests/ASFHashRequest.cs | 12 +- ArchiSteamFarm/IPC/Requests/ASFRequest.cs | 7 +- .../BotGamesToRedeemInBackgroundRequest.cs | 7 +- .../IPC/Requests/BotInputRequest.cs | 15 +- .../IPC/Requests/BotPauseRequest.cs | 10 +- .../IPC/Requests/BotRedeemRequest.cs | 7 +- .../IPC/Requests/BotRenameRequest.cs | 7 +- ArchiSteamFarm/IPC/Requests/BotRequest.cs | 7 +- ArchiSteamFarm/IPC/Requests/CommandRequest.cs | 7 +- ...actorAuthenticationConfirmationsRequest.cs | 29 +-- ArchiSteamFarm/IPC/Requests/UpdateRequest.cs | 6 +- ArchiSteamFarm/IPC/Responses/ASFResponse.cs | 37 ++-- .../GamesToRedeemInBackgroundResponse.cs | 10 +- .../IPC/Responses/GenericResponse.cs | 15 +- .../IPC/Responses/GitHubReleaseResponse.cs | 22 ++- ArchiSteamFarm/IPC/Responses/LogResponse.cs | 12 +- .../IPC/Responses/StatusCodeResponse.cs | 12 +- .../IPC/Responses/TypeProperties.cs | 14 +- ArchiSteamFarm/IPC/Responses/TypeResponse.cs | 12 +- ArchiSteamFarm/IPC/Startup.cs | 17 +- ArchiSteamFarm/IPC/WebUtilities.cs | 27 --- ArchiSteamFarm/Plugins/Interfaces/IASF.cs | 6 +- .../Plugins/Interfaces/IBotModules.cs | 6 +- ArchiSteamFarm/Plugins/Interfaces/IPlugin.cs | 9 +- .../Plugins/Interfaces/IWebInterface.cs | 4 +- ArchiSteamFarm/Plugins/PluginsCore.cs | 6 +- ArchiSteamFarm/Program.cs | 6 +- ArchiSteamFarm/Steam/Bot.cs | 80 +++++--- ArchiSteamFarm/Steam/Cards/CardsFarmer.cs | 18 +- ArchiSteamFarm/Steam/Cards/Game.cs | 15 +- ArchiSteamFarm/Steam/Data/Asset.cs | 156 ++++----------- ArchiSteamFarm/Steam/Data/BooleanResponse.cs | 8 +- .../Steam/Data/BoosterCreatorEntry.cs | 14 +- ArchiSteamFarm/Steam/Data/Confirmation.cs | 29 ++- .../Steam/Data/ConfirmationsResponse.cs | 8 +- .../Steam/Data/InventoryResponse.cs | 158 +++++++-------- .../Steam/Data/NewDiscoveryQueueResponse.cs | 8 +- .../Steam/Data/OptionalResultResponse.cs | 7 +- .../Steam/Data/RedeemWalletResponse.cs | 12 +- ArchiSteamFarm/Steam/Data/ResultResponse.cs | 8 +- ArchiSteamFarm/Steam/Data/Tag.cs | 14 +- .../Steam/Data/TradeOfferAcceptResponse.cs | 12 +- .../Steam/Data/TradeOfferSendRequest.cs | 20 +- .../Steam/Data/TradeOfferSendResponse.cs | 41 ++-- ArchiSteamFarm/Steam/Data/UserPrivacy.cs | 60 +++--- .../Steam/Integration/ArchiWebHandler.cs | 54 ++--- .../CMsgs/CMsgClientAcknowledgeClanInvite.cs | 4 +- .../Steam/Security/MobileAuthenticator.cs | 16 +- .../SteamKit2/InMemoryServerListProvider.cs | 10 +- .../Steam/SteamKit2/ServerRecordEndPoint.cs | 17 +- ArchiSteamFarm/Steam/Storage/BotConfig.cs | 148 +++++++------- ArchiSteamFarm/Steam/Storage/BotDatabase.cs | 100 +++++++--- ArchiSteamFarm/Storage/CrashFile.cs | 15 +- ArchiSteamFarm/Storage/GenericDatabase.cs | 55 +++--- ArchiSteamFarm/Storage/GlobalConfig.cs | 159 ++++++++------- ArchiSteamFarm/Storage/GlobalDatabase.cs | 87 +++++--- ArchiSteamFarm/Storage/PackageData.cs | 20 +- ArchiSteamFarm/Web/GitHub.cs | 64 +++--- ArchiSteamFarm/Web/WebBrowser.cs | 30 +-- Directory.Packages.props | 2 - 114 files changed, 1717 insertions(+), 1215 deletions(-) create mode 100644 ArchiSteamFarm/Helpers/Json/JsonDisallowNullAttribute.cs create mode 100644 ArchiSteamFarm/Helpers/Json/JsonUtilities.cs create mode 100644 ArchiSteamFarm/IPC/Integration/ReadOnlyFixesSchemaFilter.cs diff --git a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj index e815540bf7c0f..ffdea400a02b7 100644 --- a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj +++ b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ArchiSteamFarm.CustomPlugins.ExamplePlugin.csproj @@ -6,7 +6,6 @@ - diff --git a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs index 4da40b3615ecf..263ad94cfa7fe 100644 --- a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs +++ b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs @@ -21,16 +21,17 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Plugins.Interfaces; using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin; @@ -45,31 +46,35 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin; internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer { // This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class // Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place + [JsonInclude] + [Required] public string Name => nameof(ExamplePlugin); // This will be displayed to the user and written in the log file, typically you should point it to the version of your library, but alternatively you can do some more advanced logic if you'd like to // Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place + [JsonInclude] + [Required] public Version Version => typeof(ExamplePlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); // Plugins can expose custom properties for our GET /Api/Plugins API call, simply annotate them with [JsonProperty] (or keep public) - [JsonProperty] - public bool CustomIsEnabledField { get; private set; } = true; + [JsonInclude] + [Required] + public bool CustomIsEnabledField { get; private init; } = true; // This method, apart from being called before any bot initialization takes place, allows you to read custom global config properties that are not recognized by ASF // Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime // Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately // In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible - public Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) { + public Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) { if (additionalConfigProperties == null) { return Task.CompletedTask; } - foreach ((string configProperty, JToken configValue) in additionalConfigProperties) { + foreach ((string configProperty, JsonElement configValue) in additionalConfigProperties) { // It's a good idea to prefix your custom properties with the name of your plugin, so there will be no possible conflict of ASF or other plugins using the same name, neither now or in the future switch (configProperty) { - case $"{nameof(ExamplePlugin)}TestProperty" when configValue.Type == JTokenType.Boolean: - bool exampleBooleanValue = configValue.Value(); - ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of: {exampleBooleanValue}"); + case $"{nameof(ExamplePlugin)}TestProperty" when configValue.ValueKind == JsonValueKind.True: + ASF.ArchiLogger.LogGenericInfo($"{nameof(ExamplePlugin)}TestProperty boolean property has been found with a value of true"); break; } @@ -135,7 +140,7 @@ public Task OnBotInit(Bot bot) { // Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately // Also keep in mind that this function can be called multiple times, e.g. when user edits their bot configs during runtime // Take a look at OnASFInit() for example parsing code - public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { + public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { // For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules() // Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise bot.ArchiLogger.LogGenericInfo("Pausing this bot as asked from the plugin"); diff --git a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs index f9f9e0793fa0e..7b76a33c39778 100644 --- a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs +++ b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/MeowResponse.cs @@ -21,15 +21,17 @@ using System; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin; #pragma warning disable CA1812 // False positive, the class is used during json deserialization [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class MeowResponse { - [JsonProperty("url", Required = Required.Always)] - internal readonly Uri URL = null!; + [JsonInclude] + [JsonPropertyName("url")] + [JsonRequired] + internal Uri URL { get; private init; } = null!; [JsonConstructor] private MeowResponse() { } diff --git a/ArchiSteamFarm.CustomPlugins.PeriodicGC/PeriodicGCPlugin.cs b/ArchiSteamFarm.CustomPlugins.PeriodicGC/PeriodicGCPlugin.cs index bba3cb69d3683..86e7b5b4fb617 100644 --- a/ArchiSteamFarm.CustomPlugins.PeriodicGC/PeriodicGCPlugin.cs +++ b/ArchiSteamFarm.CustomPlugins.PeriodicGC/PeriodicGCPlugin.cs @@ -20,8 +20,10 @@ // limitations under the License. using System; +using System.ComponentModel.DataAnnotations; using System.Composition; using System.Runtime; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; @@ -38,8 +40,12 @@ internal sealed class PeriodicGCPlugin : IPlugin { private static readonly object LockObject = new(); private static readonly Timer PeriodicGCTimer = new(PerformGC); + [JsonInclude] + [Required] public string Name => nameof(PeriodicGCPlugin); + [JsonInclude] + [Required] public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); public Task OnLoaded() { diff --git a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/ArchiSteamFarm.CustomPlugins.SignInWithSteam.csproj b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/ArchiSteamFarm.CustomPlugins.SignInWithSteam.csproj index 842515d4a33da..bdf2d49b3e338 100644 --- a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/ArchiSteamFarm.CustomPlugins.SignInWithSteam.csproj +++ b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/ArchiSteamFarm.CustomPlugins.SignInWithSteam.csproj @@ -7,7 +7,6 @@ - diff --git a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamRequest.cs b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamRequest.cs index d836697192742..095ebaf360edd 100644 --- a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamRequest.cs +++ b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamRequest.cs @@ -20,11 +20,12 @@ // limitations under the License. using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data; public sealed class SignInWithSteamRequest { - [JsonProperty(Required = Required.Always)] - public Uri RedirectURL { get; private set; } = null!; + [JsonInclude] + [JsonRequired] + public Uri RedirectURL { get; private init; } = null!; } diff --git a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamResponse.cs b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamResponse.cs index 9215975051320..ba9e2ea1318c3 100644 --- a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamResponse.cs +++ b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/Data/SignInWithSteamResponse.cs @@ -20,13 +20,14 @@ // limitations under the License. using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam.Data; public sealed class SignInWithSteamResponse { - [JsonProperty(Required = Required.Always)] - public Uri ReturnURL { get; private set; } + [JsonInclude] + [JsonRequired] + public Uri ReturnURL { get; private init; } internal SignInWithSteamResponse(Uri returnURL) { ArgumentNullException.ThrowIfNull(returnURL); diff --git a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/SignInWithSteamPlugin.cs b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/SignInWithSteamPlugin.cs index 921df52aecd54..2319c810a1a49 100644 --- a/ArchiSteamFarm.CustomPlugins.SignInWithSteam/SignInWithSteamPlugin.cs +++ b/ArchiSteamFarm.CustomPlugins.SignInWithSteam/SignInWithSteamPlugin.cs @@ -20,7 +20,9 @@ // limitations under the License. using System; +using System.ComponentModel.DataAnnotations; using System.Composition; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Plugins.Interfaces; @@ -31,8 +33,12 @@ namespace ArchiSteamFarm.CustomPlugins.SignInWithSteam; [Export(typeof(IPlugin))] [UsedImplicitly] internal sealed class SignInWithSteamPlugin : IPlugin { + [JsonInclude] + [Required] public string Name => nameof(SignInWithSteamPlugin); + [JsonInclude] + [Required] public Version Version => typeof(SignInWithSteamPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); public Task OnLoaded() { diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ArchiSteamFarm.OfficialPlugins.ItemsMatcher.csproj b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ArchiSteamFarm.OfficialPlugins.ItemsMatcher.csproj index c8601cfc58d2f..f43fdb6131439 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ArchiSteamFarm.OfficialPlugins.ItemsMatcher.csproj +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ArchiSteamFarm.OfficialPlugins.ItemsMatcher.csproj @@ -6,7 +6,6 @@ - diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/BotCache.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/BotCache.cs index 6b909d79e58e6..bbb688b0705b5 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/BotCache.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/BotCache.cs @@ -22,20 +22,22 @@ using System; using System.Globalization; using System.IO; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; internal sealed class BotCache : SerializableFile { - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentList LastAnnouncedAssetsForListing = []; + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentList LastAnnouncedAssetsForListing { get; private init; } = []; internal string? LastAnnouncedTradeToken { get => BackingLastAnnouncedTradeToken; @@ -76,14 +78,14 @@ internal DateTime? LastRequestAt { } } - [JsonProperty] - private string? BackingLastAnnouncedTradeToken; + [JsonInclude] + private string? BackingLastAnnouncedTradeToken { get; set; } - [JsonProperty] - private string? BackingLastInventoryChecksumBeforeDeduplication; + [JsonInclude] + private string? BackingLastInventoryChecksumBeforeDeduplication { get; set; } - [JsonProperty] - private DateTime? BackingLastRequestAt; + [JsonInclude] + private DateTime? BackingLastRequestAt { get; set; } private BotCache(string filePath) : this() { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -116,6 +118,8 @@ protected override void Dispose(bool disposing) { base.Dispose(disposing); } + protected override Task Save() => Save(this); + internal static async Task CreateOrLoad(string filePath) { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -134,7 +138,7 @@ internal static async Task CreateOrLoad(string filePath) { return new BotCache(filePath); } - botCache = JsonConvert.DeserializeObject(json); + botCache = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementDiffRequest.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementDiffRequest.cs index e118021251576..9de335f41895a 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementDiffRequest.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementDiffRequest.cs @@ -22,19 +22,21 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Storage; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal sealed class AnnouncementDiffRequest : AnnouncementRequest { - [JsonProperty(Required = Required.Always)] - private readonly ImmutableHashSet InventoryRemoved; + [JsonInclude] + [JsonRequired] + private ImmutableHashSet InventoryRemoved { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly string PreviousInventoryChecksum; + [JsonInclude] + [JsonRequired] + private string PreviousInventoryChecksum { get; init; } internal AnnouncementDiffRequest(Guid guid, ulong steamID, IReadOnlyCollection inventory, string inventoryChecksum, IReadOnlyCollection matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, IReadOnlyCollection inventoryRemoved, string previousInventoryChecksum, string? nickname = null, string? avatarHash = null) : base(guid, steamID, inventory, inventoryChecksum, matchableTypes, totalInventoryCount, matchEverything, maxTradeHoldDuration, tradeToken, nickname, avatarHash) { ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementRequest.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementRequest.cs index 619aa85c30e03..c448af64915fc 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementRequest.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AnnouncementRequest.cs @@ -22,47 +22,56 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Storage; using JetBrains.Annotations; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal class AnnouncementRequest { - [JsonProperty] - private readonly string? AvatarHash; + [JsonInclude] + private string? AvatarHash { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly Guid Guid; + [JsonInclude] + [JsonRequired] + private Guid Guid { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly ImmutableHashSet Inventory; + [JsonInclude] + [JsonRequired] + private ImmutableHashSet Inventory { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly string InventoryChecksum; + [JsonInclude] + [JsonRequired] + private string InventoryChecksum { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly ImmutableHashSet MatchableTypes; + [JsonInclude] + [JsonRequired] + private ImmutableHashSet MatchableTypes { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly bool MatchEverything; + [JsonInclude] + [JsonRequired] + private bool MatchEverything { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly byte MaxTradeHoldDuration; + [JsonInclude] + [JsonRequired] + private byte MaxTradeHoldDuration { get; init; } - [JsonProperty] - private readonly string? Nickname; + [JsonInclude] + private string? Nickname { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly ulong SteamID; + [JsonInclude] + [JsonRequired] + private ulong SteamID { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly uint TotalInventoryCount; + [JsonInclude] + [JsonRequired] + private uint TotalInventoryCount { get; init; } - [JsonProperty(Required = Required.Always)] - private readonly string TradeToken; + [JsonInclude] + [JsonRequired] + private string TradeToken { get; init; } internal AnnouncementRequest(Guid guid, ulong steamID, IReadOnlyCollection inventory, string inventoryChecksum, IReadOnlyCollection matchableTypes, uint totalInventoryCount, bool matchEverything, byte maxTradeHoldDuration, string tradeToken, string? nickname = null, string? avatarHash = null) { ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForListing.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForListing.cs index 84ea0b8919a5a..ef2cb8b45f634 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForListing.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForListing.cs @@ -20,19 +20,23 @@ // limitations under the License. using System; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal sealed class AssetForListing : AssetInInventory { - [JsonProperty("i", Required = Required.Always)] - internal readonly uint Index; + internal string BackendHashCode => $"{Index}-{PreviousAssetID}-{AssetID}-{ClassID}-{Rarity}-{RealAppID}-{Tradable}-{Type}-{Amount}"; - [JsonProperty("l", Required = Required.Always)] - internal readonly ulong PreviousAssetID; + [JsonInclude] + [JsonPropertyName("i")] + [JsonRequired] + internal uint Index { get; private init; } - internal string BackendHashCode => Index + "-" + PreviousAssetID + "-" + AssetID + "-" + ClassID + "-" + Rarity + "-" + RealAppID + "-" + Tradable + "-" + Type + "-" + Amount; + [JsonInclude] + [JsonPropertyName("l")] + [JsonRequired] + internal ulong PreviousAssetID { get; private init; } internal AssetForListing(Asset asset, uint index, ulong previousAssetID) : base(asset) { ArgumentNullException.ThrowIfNull(asset); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForMatching.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForMatching.cs index 577d2bdb187e6..950832c253302 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForMatching.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetForMatching.cs @@ -20,29 +20,41 @@ // limitations under the License. using System; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal class AssetForMatching { - [JsonProperty("c", Required = Required.Always)] - internal readonly ulong ClassID; + [JsonInclude] + [JsonPropertyName("a")] + [JsonRequired] + internal uint Amount { get; set; } - [JsonProperty("r", Required = Required.Always)] - internal readonly Asset.ERarity Rarity; + [JsonInclude] + [JsonPropertyName("c")] + [JsonRequired] + internal ulong ClassID { get; private init; } - [JsonProperty("e", Required = Required.Always)] - internal readonly uint RealAppID; + [JsonInclude] + [JsonPropertyName("r")] + [JsonRequired] + internal Asset.ERarity Rarity { get; private init; } - [JsonProperty("t", Required = Required.Always)] - internal readonly bool Tradable; + [JsonInclude] + [JsonPropertyName("e")] + [JsonRequired] + internal uint RealAppID { get; private init; } - [JsonProperty("p", Required = Required.Always)] - internal readonly Asset.EType Type; + [JsonInclude] + [JsonPropertyName("t")] + [JsonRequired] + internal bool Tradable { get; private init; } - [JsonProperty("a", Required = Required.Always)] - internal uint Amount { get; set; } + [JsonInclude] + [JsonPropertyName("p")] + [JsonRequired] + internal Asset.EType Type { get; private init; } [JsonConstructor] protected AssetForMatching() { } diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetInInventory.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetInInventory.cs index b8acc0594e977..a0404f51a1ab0 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetInInventory.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/AssetInInventory.cs @@ -20,14 +20,16 @@ // limitations under the License. using System; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal class AssetInInventory : AssetForMatching { - [JsonProperty("d", Required = Required.Always)] - internal readonly ulong AssetID; + [JsonInclude] + [JsonPropertyName("d")] + [JsonRequired] + internal ulong AssetID { get; private init; } [JsonConstructor] protected AssetInInventory() { } diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/BackgroundTaskResponse.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/BackgroundTaskResponse.cs index def540eb78d93..55db8d97ab9b3 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/BackgroundTaskResponse.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/BackgroundTaskResponse.cs @@ -21,22 +21,20 @@ using System; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; #pragma warning disable CA1812 // False positive, the class is used during json deserialization [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class BackgroundTaskResponse { -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly bool Finished; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal bool Finished { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly Guid RequestID; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal Guid RequestID { get; private init; } [JsonConstructor] private BackgroundTaskResponse() { } diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/HeartBeatRequest.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/HeartBeatRequest.cs index 68debbca3e831..b917483417f0a 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/HeartBeatRequest.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/HeartBeatRequest.cs @@ -20,17 +20,19 @@ // limitations under the License. using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal sealed class HeartBeatRequest { - [JsonProperty(Required = Required.Always)] - internal readonly Guid Guid; + [JsonInclude] + [JsonRequired] + internal Guid Guid { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ulong SteamID; + [JsonInclude] + [JsonRequired] + internal ulong SteamID { get; private init; } internal HeartBeatRequest(Guid guid, ulong steamID) { ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/InventoriesRequest.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/InventoriesRequest.cs index c0048eae351dc..a08f9be29e4fe 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/InventoriesRequest.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/InventoriesRequest.cs @@ -23,24 +23,28 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal sealed class InventoriesRequest { - [JsonProperty(Required = Required.Always)] - internal readonly Guid Guid; + [JsonInclude] + [JsonRequired] + internal Guid Guid { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ImmutableHashSet Inventory; + [JsonInclude] + [JsonRequired] + internal ImmutableHashSet Inventory { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ImmutableHashSet MatchableTypes; + [JsonInclude] + [JsonRequired] + internal ImmutableHashSet MatchableTypes { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ulong SteamID; + [JsonInclude] + [JsonRequired] + internal ulong SteamID { get; private init; } internal InventoriesRequest(Guid guid, ulong steamID, IReadOnlyCollection inventory, IReadOnlyCollection matchableTypes) { ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/ListedUser.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/ListedUser.cs index 24a45cc0f7402..67eaeb238549c 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/ListedUser.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/ListedUser.cs @@ -21,52 +21,48 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; #pragma warning disable CA1812 // False positive, the class is used during json deserialization [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class ListedUser { - [JsonProperty(Required = Required.Always)] - internal readonly ImmutableHashSet Assets = ImmutableHashSet.Empty; + [JsonInclude] + [JsonRequired] + internal ImmutableHashSet Assets { get; private init; } = ImmutableHashSet.Empty; - [JsonProperty(Required = Required.Always)] - internal readonly ImmutableHashSet MatchableTypes = ImmutableHashSet.Empty; + [JsonInclude] + [JsonRequired] + internal ImmutableHashSet MatchableTypes { get; private init; } = ImmutableHashSet.Empty; -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly bool MatchEverything; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal bool MatchEverything { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly byte MaxTradeHoldDuration; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal byte MaxTradeHoldDuration { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.AllowNull)] - internal readonly string? Nickname; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + internal string? Nickname { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly ulong SteamID; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal ulong SteamID { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly uint TotalGamesCount; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal uint TotalGamesCount { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty(Required = Required.Always)] - internal readonly uint TotalInventoryCount; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonRequired] + internal uint TotalInventoryCount { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly string TradeToken = ""; + [JsonInclude] + [JsonRequired] + internal string TradeToken { get; private init; } = ""; [JsonConstructor] private ListedUser() { } diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPart.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPart.cs index 70433175fa4dc..4856b30f1262d 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPart.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPart.cs @@ -20,33 +20,33 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; #pragma warning disable CA1812 // False positive, the class is used during json deserialization [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class SetPart { -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty("c", Required = Required.Always)] - internal readonly ulong ClassID; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonPropertyName("c")] + [JsonRequired] + internal ulong ClassID { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty("r", Required = Required.Always)] - internal readonly Asset.ERarity Rarity; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonPropertyName("r")] + [JsonRequired] + internal Asset.ERarity Rarity { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty("e", Required = Required.Always)] - internal readonly uint RealAppID; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonPropertyName("e")] + [JsonRequired] + internal uint RealAppID { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty("p", Required = Required.Always)] - internal readonly Asset.EType Type; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonPropertyName("p")] + [JsonRequired] + internal Asset.EType Type { get; private init; } [JsonConstructor] private SetPart() { } diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPartsRequest.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPartsRequest.cs index 2c6f96bc889ec..bb75e23f86ebb 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPartsRequest.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Data/SetPartsRequest.cs @@ -22,24 +22,28 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data; internal sealed class SetPartsRequest { - [JsonProperty(Required = Required.Always)] - internal readonly Guid Guid; + [JsonInclude] + [JsonRequired] + internal Guid Guid { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ImmutableHashSet MatchableTypes; + [JsonInclude] + [JsonRequired] + internal ImmutableHashSet MatchableTypes { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ImmutableHashSet RealAppIDs; + [JsonInclude] + [JsonRequired] + internal ImmutableHashSet RealAppIDs { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ulong SteamID; + [JsonInclude] + [JsonRequired] + internal ulong SteamID { get; private init; } internal SetPartsRequest(Guid guid, ulong steamID, IReadOnlyCollection matchableTypes, IReadOnlyCollection realAppIDs) { ArgumentOutOfRangeException.ThrowIfEqual(guid, Guid.Empty); diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ItemsMatcherPlugin.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ItemsMatcherPlugin.cs index ff345cfb70a50..fca96c2dda6ea 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ItemsMatcherPlugin.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/ItemsMatcherPlugin.cs @@ -23,8 +23,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Composition; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Localization; @@ -33,8 +36,6 @@ using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Exchange; using ArchiSteamFarm.Steam.Integration.Callbacks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; @@ -43,10 +44,12 @@ namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; internal sealed class ItemsMatcherPlugin : OfficialPlugin, IBot, IBotCommand2, IBotIdentity, IBotModules, IBotTradeOfferResults, IBotUserNotifications { internal static readonly ConcurrentDictionary RemoteCommunications = new(); - [JsonProperty] + [JsonInclude] + [Required] public override string Name => nameof(ItemsMatcherPlugin); - [JsonProperty] + [JsonInclude] + [Required] public override Version Version => typeof(ItemsMatcherPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) { @@ -83,7 +86,7 @@ public Task OnBotInit(Bot bot) { return Task.CompletedTask; } - public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { + public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { ArgumentNullException.ThrowIfNull(bot); if (RemoteCommunications.TryRemove(bot, out RemoteCommunication? remoteCommunication)) { diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index 6d07da6c9104f..027b34f7598de 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -28,6 +28,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; @@ -43,7 +44,6 @@ using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; -using Newtonsoft.Json.Linq; using SteamKit2; using SteamKit2.Internal; @@ -1190,11 +1190,19 @@ private async Task MatchActively(IReadOnlyCollection listedUse // 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); + JsonElement matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey); - if (matchActivelyTradeOfferIDsToken != null) { + if (matchActivelyTradeOfferIDsToken.ValueKind == JsonValueKind.Array) { try { - matchActivelyTradeOfferIDs = matchActivelyTradeOfferIDsToken.ToObject>(); + matchActivelyTradeOfferIDs = new HashSet(matchActivelyTradeOfferIDsToken.GetArrayLength()); + + foreach (JsonElement tradeIDElement in matchActivelyTradeOfferIDsToken.EnumerateArray()) { + if (!tradeIDElement.TryGetUInt64(out ulong tradeID)) { + continue; + } + + matchActivelyTradeOfferIDs.Add(tradeID); + } } catch (Exception e) { Bot.ArchiLogger.LogGenericWarningException(e); } @@ -1223,7 +1231,7 @@ private async Task MatchActively(IReadOnlyCollection listedUse matchActivelyTradeOfferIDs = activeTradeOfferIDs; if (matchActivelyTradeOfferIDs.Count > 0) { - Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs)); + Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, matchActivelyTradeOfferIDs); } else { Bot.BotDatabase.DeleteFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey); } @@ -1427,7 +1435,7 @@ private async Task MatchActively(IReadOnlyCollection listedUse if (tradeOfferIDs?.Count > 0) { matchActivelyTradeOfferIDs.UnionWith(tradeOfferIDs); - Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs)); + Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, matchActivelyTradeOfferIDs); } if (mobileTradeOfferIDs?.Count > 0) { diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj index d6a3bbc92dc95..264fdafa8405f 100644 --- a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj @@ -6,7 +6,6 @@ - diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs index f8ceeae8b7b9b..2a1d0394a6757 100644 --- a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs @@ -27,9 +27,9 @@ using System.Linq; using System.Threading.Tasks; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; -using Newtonsoft.Json; using SteamKit2; using SteamKit2.Internal; @@ -128,7 +128,7 @@ internal static class Commands { return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); } - Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject(json); + Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject(); if (mobileAuthenticator == null) { return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); @@ -261,7 +261,7 @@ internal static class Commands { return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); } - Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject(json); + Steam.Security.MobileAuthenticator? mobileAuthenticator = json.ToJsonObject(); if (mobileAuthenticator == null) { return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); @@ -360,7 +360,7 @@ internal static class Commands { MaFileData maFileData = new(response, bot.SteamID, deviceID); string maFilePendingPath = $"{bot.GetFilePath(Bot.EFileType.MobileAuthenticator)}.PENDING"; - string json = JsonConvert.SerializeObject(maFileData, Formatting.Indented); + string json = maFileData.ToJsonText(true); try { await File.WriteAllTextAsync(maFilePendingPath, json).ConfigureAwait(false); diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs index e3e62b46d12ef..65e307a994332 100644 --- a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs @@ -20,48 +20,71 @@ // limitations under the License. using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using SteamKit2; using SteamKit2.Internal; namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; internal sealed class MaFileData { - [JsonProperty("account_name", Required = Required.Always)] - internal readonly string AccountName; - - [JsonProperty("device_id", Required = Required.Always)] - internal readonly string DeviceID; - - [JsonProperty("identity_secret", Required = Required.Always)] - internal readonly string IdentitySecret; - - [JsonProperty("revocation_code", Required = Required.Always)] - internal readonly string RevocationCode; - - [JsonProperty("secret_1", Required = Required.Always)] - internal readonly string Secret1; - - [JsonProperty("serial_number", Required = Required.Always)] - internal readonly ulong SerialNumber; - - [JsonProperty("server_time", Required = Required.Always)] - internal readonly ulong ServerTime; - - [JsonProperty(Required = Required.Always)] - internal readonly MaFileSessionData Session; - - [JsonProperty("shared_secret", Required = Required.Always)] - internal readonly string SharedSecret; - - [JsonProperty("status", Required = Required.Always)] - internal readonly int Status; - - [JsonProperty("token_gid", Required = Required.Always)] - internal readonly string TokenGid; - - [JsonProperty("uri", Required = Required.Always)] - internal readonly string Uri; + [JsonInclude] + [JsonPropertyName("account_name")] + [JsonRequired] + internal string AccountName { get; private init; } + + [JsonInclude] + [JsonPropertyName("device_id")] + [JsonRequired] + internal string DeviceID { get; private init; } + + [JsonInclude] + [JsonPropertyName("identity_secret")] + [JsonRequired] + internal string IdentitySecret { get; private init; } + + [JsonInclude] + [JsonPropertyName("revocation_code")] + [JsonRequired] + internal string RevocationCode { get; private init; } + + [JsonInclude] + [JsonPropertyName("secret_1")] + [JsonRequired] + internal string Secret1 { get; private init; } + + [JsonInclude] + [JsonPropertyName("serial_number")] + [JsonRequired] + internal ulong SerialNumber { get; private init; } + + [JsonInclude] + [JsonPropertyName("server_time")] + [JsonRequired] + internal ulong ServerTime { get; private init; } + + [JsonInclude] + [JsonRequired] + internal MaFileSessionData Session { get; private init; } + + [JsonInclude] + [JsonPropertyName("shared_secret")] + [JsonRequired] + internal string SharedSecret { get; private init; } + + [JsonInclude] + [JsonPropertyName("status")] + [JsonRequired] + internal int Status { get; private init; } + + [JsonInclude] + [JsonPropertyName("token_gid")] + [JsonRequired] + internal string TokenGid { get; private init; } + + [JsonInclude] + [JsonPropertyName("uri")] + [JsonRequired] + internal string Uri { get; private init; } internal MaFileData(CTwoFactor_AddAuthenticator_Response data, ulong steamID, string deviceID) { ArgumentNullException.ThrowIfNull(data); diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileSessionData.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileSessionData.cs index 0744097946580..c3c0e1cfa26b7 100644 --- a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileSessionData.cs +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileSessionData.cs @@ -20,14 +20,15 @@ // limitations under the License. using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; internal sealed class MaFileSessionData { - [JsonProperty(Required = Required.Always)] - internal readonly ulong SteamID; + [JsonInclude] + [JsonRequired] + internal ulong SteamID { get; private init; } internal MaFileSessionData(ulong steamID) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs index cd5bb0d90d983..afaf9aa6b8abb 100644 --- a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs @@ -22,15 +22,16 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Composition; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization; using ArchiSteamFarm.Plugins; using ArchiSteamFarm.Plugins.Interfaces; using ArchiSteamFarm.Steam; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; @@ -38,10 +39,12 @@ namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; [Export(typeof(IPlugin))] [SuppressMessage("ReSharper", "MemberCanBeFileLocal")] internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient { - [JsonProperty] + [JsonInclude] + [Required] public override string Name => nameof(MobileAuthenticatorPlugin); - [JsonProperty] + [JsonInclude] + [Required] public override Version Version => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) { diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.csproj b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.csproj index fc7c1085d7f8c..32d8ba527d46f 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.csproj +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.csproj @@ -6,7 +6,6 @@ - diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitRequest.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitRequest.cs index 2bea64b75fb67..53aa1df13d411 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitRequest.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitRequest.cs @@ -23,34 +23,44 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.Text.Json.Serialization; using ArchiSteamFarm.Core; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data; internal sealed class SubmitRequest { - [JsonProperty("guid", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("guid")] private static string Guid => ASF.GlobalDatabase?.Identifier.ToString("N") ?? throw new InvalidOperationException(nameof(ASF.GlobalDatabase.Identifier)); - [JsonProperty("token", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("token")] private static string Token => SharedInfo.Token; - [JsonProperty("v", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("v")] private static byte Version => SharedInfo.ApiVersion; - [JsonProperty("apps", Required = Required.Always)] - private readonly ImmutableDictionary Apps; + [JsonInclude] + [JsonPropertyName("apps")] + [JsonRequired] + private ImmutableDictionary Apps { get; init; } - [JsonProperty("depots", Required = Required.Always)] - private readonly ImmutableDictionary Depots; + [JsonInclude] + [JsonPropertyName("depots")] + [JsonRequired] + private ImmutableDictionary Depots { get; init; } private readonly ulong SteamID; - [JsonProperty("subs", Required = Required.Always)] - private readonly ImmutableDictionary Subs; + [JsonInclude] + [JsonPropertyName("subs")] + [JsonRequired] + private ImmutableDictionary Subs { get; init; } - [JsonProperty("steamid", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("steamid")] private string SteamIDText => new SteamID(SteamID).Render(); internal SubmitRequest(ulong steamID, IReadOnlyCollection> apps, IReadOnlyCollection> accessTokens, IReadOnlyCollection> depots) { diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponse.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponse.cs index 4ac2686860a39..9291494ba6242 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponse.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponse.cs @@ -20,22 +20,21 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data; #pragma warning disable CA1812 // False positive, the class is used during json deserialization [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class SubmitResponse { -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty("data", Required = Required.DisallowNull)] - internal readonly SubmitResponseData? Data; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonPropertyName("data")] + internal SubmitResponseData? Data { get; private init; } -#pragma warning disable CS0649 // False positive, the field is used during json deserialization - [JsonProperty("success", Required = Required.Always)] - internal readonly bool Success; -#pragma warning restore CS0649 // False positive, the field is used during json deserialization + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + internal bool Success { get; private init; } [JsonConstructor] private SubmitResponse() { } diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponseData.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponseData.cs index fa3f7c62f23f5..fa85733d6dde8 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponseData.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Data/SubmitResponseData.cs @@ -20,28 +20,40 @@ // limitations under the License. using System.Collections.Immutable; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data; #pragma warning disable CA1812 // False positive, the class is used during json deserialization internal sealed class SubmitResponseData { - [JsonProperty("new_apps", Required = Required.Always)] - internal readonly ImmutableHashSet NewApps = ImmutableHashSet.Empty; - - [JsonProperty("new_depots", Required = Required.Always)] - internal readonly ImmutableHashSet NewDepots = ImmutableHashSet.Empty; - - [JsonProperty("new_subs", Required = Required.Always)] - internal readonly ImmutableHashSet NewPackages = ImmutableHashSet.Empty; - - [JsonProperty("verified_apps", Required = Required.Always)] - internal readonly ImmutableHashSet VerifiedApps = ImmutableHashSet.Empty; - - [JsonProperty("verified_depots", Required = Required.Always)] - internal readonly ImmutableHashSet VerifiedDepots = ImmutableHashSet.Empty; - - [JsonProperty("verified_subs", Required = Required.Always)] - internal readonly ImmutableHashSet VerifiedPackages = ImmutableHashSet.Empty; + [JsonInclude] + [JsonPropertyName("new_apps")] + [JsonRequired] + internal ImmutableHashSet NewApps { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("new_depots")] + [JsonRequired] + internal ImmutableHashSet NewDepots { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("new_subs")] + [JsonRequired] + internal ImmutableHashSet NewPackages { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("verified_apps")] + [JsonRequired] + internal ImmutableHashSet VerifiedApps { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("verified_depots")] + [JsonRequired] + internal ImmutableHashSet VerifiedDepots { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("verified_subs")] + [JsonRequired] + internal ImmutableHashSet VerifiedPackages { get; private init; } = ImmutableHashSet.Empty; } #pragma warning restore CA1812 // False positive, the class is used during json deserialization diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalCache.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalCache.cs index 10f1a3b439eed..2933b3f876a2b 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalCache.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalCache.cs @@ -26,14 +26,15 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization; using ArchiSteamFarm.Web.Responses; using JetBrains.Annotations; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper; @@ -43,27 +44,34 @@ internal sealed class GlobalCache : SerializableFile { private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache"); - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary AppChangeNumbers = new(); + [JsonInclude] + internal uint LastChangeNumber { get; private set; } - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary AppTokens = new(); + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary AppChangeNumbers { get; init; } = new(); - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary DepotKeys = new(); + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary AppTokens { get; init; } = new(); - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary SubmittedApps = new(); + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary DepotKeys { get; init; } = new(); - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary SubmittedDepots = new(); + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary SubmittedApps { get; init; } = new(); - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary SubmittedPackages = new(); + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary SubmittedDepots { get; init; } = new(); - [JsonProperty(Required = Required.DisallowNull)] - internal uint LastChangeNumber { get; private set; } + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary SubmittedPackages { get; init; } = new(); + [JsonConstructor] internal GlobalCache() => FilePath = SharedFilePath; [UsedImplicitly] @@ -87,6 +95,8 @@ internal sealed class GlobalCache : SerializableFile { [UsedImplicitly] public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages.IsEmpty; + protected override Task Save() => Save(this); + internal ulong GetAppToken(uint appID) => AppTokens[appID]; internal Dictionary GetAppTokensForSubmission() => AppTokens.Where(appToken => (SteamTokenDumperPlugin.Config?.SecretAppIDs.Contains(appToken.Key) != true) && (appToken.Value > 0) && (!SubmittedApps.TryGetValue(appToken.Key, out ulong token) || (appToken.Value != token))).ToDictionary(static appToken => appToken.Key, static appToken => appToken.Value); @@ -118,7 +128,7 @@ internal Dictionary GetPackageTokensForSubmission() { return null; } - globalCache = JsonConvert.DeserializeObject(json); + globalCache = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalConfigExtension.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalConfigExtension.cs index 1826ab8451532..87550494d7e0f 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalConfigExtension.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/GlobalConfigExtension.cs @@ -19,16 +19,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper; public sealed class GlobalConfigExtension { - [JsonProperty] - public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private set; } + [JsonInclude] + public SteamTokenDumperConfig? SteamTokenDumperPlugin { get; private init; } - [JsonProperty(Required = Required.DisallowNull)] - public bool SteamTokenDumperPluginEnabled { get; private set; } + [JsonInclude] + public bool SteamTokenDumperPluginEnabled { get; private init; } [JsonConstructor] internal GlobalConfigExtension() { } diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperConfig.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperConfig.cs index 50ec96503e332..74482afcfef97 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperConfig.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperConfig.cs @@ -20,29 +20,33 @@ // limitations under the License. using System.Collections.Immutable; +using System.Text.Json.Serialization; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC.Integration; -using Newtonsoft.Json; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper; public sealed class SteamTokenDumperConfig { - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] public bool Enabled { get; internal set; } - [JsonProperty(Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] [SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)] - public ImmutableHashSet SecretAppIDs { get; private set; } = ImmutableHashSet.Empty; + public ImmutableHashSet SecretAppIDs { get; private init; } = ImmutableHashSet.Empty; - [JsonProperty(Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] [SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)] - public ImmutableHashSet SecretDepotIDs { get; private set; } = ImmutableHashSet.Empty; + public ImmutableHashSet SecretDepotIDs { get; private init; } = ImmutableHashSet.Empty; - [JsonProperty(Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] [SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)] - public ImmutableHashSet SecretPackageIDs { get; private set; } = ImmutableHashSet.Empty; + public ImmutableHashSet SecretPackageIDs { get; private init; } = ImmutableHashSet.Empty; - [JsonProperty(Required = Required.DisallowNull)] - public bool SkipAutoGrantPackages { get; private set; } = true; + [JsonInclude] + public bool SkipAutoGrantPackages { get; private init; } = true; [JsonConstructor] internal SteamTokenDumperConfig() { } diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs index 0ef21a6dddbf0..5718339c6bfa8 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs @@ -24,14 +24,18 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Composition; using System.Globalization; using System.Linq; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data; using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization; using ArchiSteamFarm.Plugins; @@ -41,8 +45,6 @@ using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper; @@ -51,7 +53,7 @@ namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper; internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotCommand2, IBotSteamClient, ISteamPICSChanges { private const ushort DepotsRateLimitingDelay = 500; - [JsonProperty] + [JsonInclude] internal static SteamTokenDumperConfig? Config { get; private set; } private static readonly ConcurrentDictionary BotSubscriptions = new(); @@ -62,15 +64,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC private static GlobalCache? GlobalCache; private static DateTimeOffset LastUploadAt = DateTimeOffset.MinValue; - [JsonProperty] + [JsonInclude] + [Required] public override string Name => nameof(SteamTokenDumperPlugin); - [JsonProperty] + [JsonInclude] + [Required] public override Version Version => typeof(SteamTokenDumperPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); public Task GetPreferredChangeNumberToStartFrom() => Task.FromResult(GlobalCache?.LastChangeNumber ?? 0); - public async Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) { + public async Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) { if (!SharedInfo.HasValidToken) { ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledMissingBuildToken, nameof(SteamTokenDumperPlugin))); @@ -81,15 +85,19 @@ public async Task OnASFInit(IReadOnlyDictionary? additionalConfi SteamTokenDumperConfig? config = null; if (additionalConfigProperties != null) { - foreach ((string configProperty, JToken configValue) in additionalConfigProperties) { + foreach ((string configProperty, JsonElement configValue) in additionalConfigProperties) { try { switch (configProperty) { case nameof(GlobalConfigExtension.SteamTokenDumperPlugin): - config = configValue.ToObject(); + config = configValue.ToJsonObject(); break; - case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled): - isEnabled = configValue.Value(); + case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled) when configValue.ValueKind == JsonValueKind.False: + isEnabled = false; + + break; + case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled) when configValue.ValueKind == JsonValueKind.True: + isEnabled = true; break; } diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index 9a1202ff2e995..14d70d26e32db 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -17,13 +17,11 @@ - - diff --git a/ArchiSteamFarm/Collections/ConcurrentHashSet.cs b/ArchiSteamFarm/Collections/ConcurrentHashSet.cs index 85abae05ef6a0..700e59665ab6a 100644 --- a/ArchiSteamFarm/Collections/ConcurrentHashSet.cs +++ b/ArchiSteamFarm/Collections/ConcurrentHashSet.cs @@ -28,7 +28,7 @@ namespace ArchiSteamFarm.Collections; -public sealed class ConcurrentHashSet : IReadOnlyCollection, ISet where T : notnull { +public sealed class ConcurrentHashSet : IReadOnlySet, ISet where T : notnull { [PublicAPI] public event EventHandler? OnModified; diff --git a/ArchiSteamFarm/Collections/ConcurrentList.cs b/ArchiSteamFarm/Collections/ConcurrentList.cs index baaa87452f5e2..6b40ed07e5665 100644 --- a/ArchiSteamFarm/Collections/ConcurrentList.cs +++ b/ArchiSteamFarm/Collections/ConcurrentList.cs @@ -27,7 +27,7 @@ namespace ArchiSteamFarm.Collections; -internal sealed class ConcurrentList : IList, IReadOnlyList { +public sealed class ConcurrentList : IList, IReadOnlyList { [PublicAPI] public event EventHandler? OnModified; diff --git a/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs b/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs index a201ba17c7cd9..f99151eeed45c 100644 --- a/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs +++ b/ArchiSteamFarm/Collections/ObservableConcurrentDictionary.cs @@ -24,7 +24,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Collections; @@ -42,7 +41,6 @@ public sealed class ObservableConcurrentDictionary : IDictionary Keys => BackingDictionary.Keys; - [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentDictionary BackingDictionary = new(); int ICollection>.Count => BackingDictionary.Count; @@ -54,6 +52,7 @@ public sealed class ObservableConcurrentDictionary : IDictionary BackingDictionary[key]; + set { if (BackingDictionary.TryGetValue(key, out TValue? savedValue) && EqualityComparer.Default.Equals(savedValue, value)) { return; diff --git a/ArchiSteamFarm/Helpers/Json/JsonDisallowNullAttribute.cs b/ArchiSteamFarm/Helpers/Json/JsonDisallowNullAttribute.cs new file mode 100644 index 0000000000000..ab5065ba0931d --- /dev/null +++ b/ArchiSteamFarm/Helpers/Json/JsonDisallowNullAttribute.cs @@ -0,0 +1,30 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Ł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.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Helpers.Json; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +[PublicAPI] +public sealed class JsonDisallowNullAttribute : JsonAttribute; diff --git a/ArchiSteamFarm/Helpers/Json/JsonUtilities.cs b/ArchiSteamFarm/Helpers/Json/JsonUtilities.cs new file mode 100644 index 0000000000000..4c34ec1f0af80 --- /dev/null +++ b/ArchiSteamFarm/Helpers/Json/JsonUtilities.cs @@ -0,0 +1,186 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Ł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.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading; +using System.Threading.Tasks; +using ArchiSteamFarm.Localization; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Helpers.Json; + +public static class JsonUtilities { + [PublicAPI] + public static readonly JsonSerializerOptions DefaultJsonSerialierOptions = CreateDefaultJsonSerializerOptions(); + + [PublicAPI] + public static readonly JsonSerializerOptions IndentedJsonSerialierOptions = CreateDefaultJsonSerializerOptions(true); + + [PublicAPI] + public static JsonElement ToJsonElement(this T obj, bool writeIndented = false) where T : notnull { + ArgumentNullException.ThrowIfNull(obj); + + return JsonSerializer.SerializeToElement(obj, writeIndented ? IndentedJsonSerialierOptions : DefaultJsonSerialierOptions); + } + + [PublicAPI] + public static T? ToJsonObject(this JsonElement jsonElement, CancellationToken cancellationToken = default) => jsonElement.Deserialize(DefaultJsonSerialierOptions); + + [PublicAPI] + public static async ValueTask ToJsonObject(this Stream stream, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(stream); + + return await JsonSerializer.DeserializeAsync(stream, DefaultJsonSerialierOptions, cancellationToken).ConfigureAwait(false); + } + + [PublicAPI] + public static T? ToJsonObject([StringSyntax(StringSyntaxAttribute.Json)] this string json) { + ArgumentException.ThrowIfNullOrEmpty(json); + + return JsonSerializer.Deserialize(json, DefaultJsonSerialierOptions); + } + + [PublicAPI] + public static string ToJsonText(this T obj, bool writeIndented = false) => JsonSerializer.Serialize(obj, writeIndented ? IndentedJsonSerialierOptions : DefaultJsonSerialierOptions); + + private static void ApplyCustomModifiers(JsonTypeInfo jsonTypeInfo) { + ArgumentNullException.ThrowIfNull(jsonTypeInfo); + + bool potentialDisallowedNullsPossible = false; + + foreach (JsonPropertyInfo property in jsonTypeInfo.Properties) { + // All our modifications require a valid Get method on a property + if (property.Get == null) { + continue; + } + + // The object should be validated against potential nulls if at least one property has [JsonDisallowNull] declared, avoid performance penalty otherwise + if (property.AttributeProvider?.IsDefined(typeof(JsonDisallowNullAttribute), false) == true) { + if (property.PropertyType.IsValueType && (Nullable.GetUnderlyingType(property.PropertyType) == null)) { + // We should have no [JsonDisallowNull] declared on non-nullable types, this requires developer correction + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(JsonDisallowNullAttribute), $"{property.Name} ({jsonTypeInfo.Type})")); + } + + potentialDisallowedNullsPossible = true; + } + + // The property should be checked against ShouldSerialize if there is a valid method to invoke, avoid performance penalty otherwise + MethodInfo? shouldSerializeMethod = GetShouldSerializeMethod(jsonTypeInfo.Type, property); + + if (shouldSerializeMethod != null) { + property.ShouldSerialize = (parent, _) => ShouldSerialize(shouldSerializeMethod, parent); + } + } + + if (potentialDisallowedNullsPossible) { + jsonTypeInfo.OnDeserialized = OnPotentialDisallowedNullsDeserialized; + } + } + + private static JsonSerializerOptions CreateDefaultJsonSerializerOptions(bool writeIndented = false) => + new() { + AllowTrailingCommas = true, + PropertyNamingPolicy = null, + ReadCommentHandling = JsonCommentHandling.Skip, + TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { ApplyCustomModifiers } }, + WriteIndented = writeIndented + }; + + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2070", Justification = "We don't care about trimmed methods, it's not like we can make it work differently anyway")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075", Justification = "We don't care about trimmed properties, it's not like we can make it work differently anyway")] + private static MethodInfo? GetShouldSerializeMethod([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Type parent, JsonPropertyInfo property) { + ArgumentNullException.ThrowIfNull(parent); + ArgumentNullException.ThrowIfNull(property); + + // Handle most common case where ShouldSerializeXYZ() matches property name + MethodInfo? result = parent.GetMethod($"ShouldSerialize{property.Name}", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null); + + if (result?.ReturnType == typeof(bool)) { + // Method exists and returns a boolean, that's what we'd like to hear + return result; + } + + // Handle less common case where ShouldSerializeXYZ() matches original member name + PropertyInfo? memberNameProperty = property.GetType().GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + if (memberNameProperty == null) { + // Should never happen, investigate if it does + throw new InvalidOperationException(nameof(memberNameProperty)); + } + + object? memberNameResult = memberNameProperty.GetValue(property); + + if (memberNameResult is not string memberName) { + // Should never happen, investigate if it does + throw new InvalidOperationException(nameof(memberName)); + } + + if (string.IsNullOrEmpty(memberName) || (memberName == property.Name)) { + // We don't have anything to work with further, there is no ShouldSerialize() method + return null; + } + + result = parent.GetMethod($"ShouldSerialize{memberName}", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null); + + // Use alternative method if it exists and returns a boolean + return result?.ReturnType == typeof(bool) ? result : null; + } + + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075", Justification = "We don't care about trimmed properties, it's not like we can make it work differently anyway")] + private static void OnPotentialDisallowedNullsDeserialized(object obj) { + ArgumentNullException.ThrowIfNull(obj); + + Type type = obj.GetType(); + + foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Where(field => field.IsDefined(typeof(JsonDisallowNullAttribute), false) && (field.GetValue(obj) == null))) { + throw new JsonException($"Required field {field.Name} expects a non-null value."); + } + + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Where(property => (property.GetMethod != null) && property.IsDefined(typeof(JsonDisallowNullAttribute), false) && (property.GetValue(obj) == null))) { + throw new JsonException($"Required property {property.Name} expects a non-null value."); + } + } + + private static bool ShouldSerialize(MethodInfo shouldSerializeMethod, object parent) { + ArgumentNullException.ThrowIfNull(shouldSerializeMethod); + ArgumentNullException.ThrowIfNull(parent); + + if (shouldSerializeMethod.ReturnType != typeof(bool)) { + throw new InvalidOperationException(nameof(shouldSerializeMethod)); + } + + object? shouldSerialize = shouldSerializeMethod.Invoke(parent, null); + + if (shouldSerialize is not bool result) { + // Should not happen, we've already determined we have a method that returns a boolean + throw new InvalidOperationException(nameof(shouldSerialize)); + } + + return result; + } +} diff --git a/ArchiSteamFarm/Helpers/SerializableFile.cs b/ArchiSteamFarm/Helpers/SerializableFile.cs index 4a2dc99e68246..0894790dc54f9 100644 --- a/ArchiSteamFarm/Helpers/SerializableFile.cs +++ b/ArchiSteamFarm/Helpers/SerializableFile.cs @@ -24,7 +24,8 @@ using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; -using Newtonsoft.Json; +using ArchiSteamFarm.Helpers.Json; +using JetBrains.Annotations; namespace ArchiSteamFarm.Helpers; @@ -49,47 +50,60 @@ protected virtual void Dispose(bool disposing) { } } - protected async Task Save() { - if (string.IsNullOrEmpty(FilePath)) { + /// + /// Implementing this method in your target class is crucial for providing supported functionality. + /// In order to do so, it's enough to call static function from the parent class, providing this as input parameter. + /// Afterwards, simply call your function whenever you need to save changes. + /// This approach will allow JSON serializer used in the to properly discover all of the properties used in your class. + /// Unfortunately, due to STJ's limitations, called by some "security", it's not possible for base class to resolve your properties automatically otherwise. + /// + /// protected override Task Save() => Save(this); + [UsedImplicitly] + protected abstract Task Save(); + + protected static async Task Save(T serializableFile) where T : SerializableFile { + ArgumentNullException.ThrowIfNull(serializableFile); + + if (string.IsNullOrEmpty(serializableFile.FilePath)) { throw new InvalidOperationException(nameof(FilePath)); } - if (ReadOnly) { + if (serializableFile.ReadOnly) { return; } // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that - lock (FileSemaphore) { - if (SavingScheduled) { + lock (serializableFile.FileSemaphore) { + if (serializableFile.SavingScheduled) { return; } - SavingScheduled = true; + serializableFile.SavingScheduled = true; } - await FileSemaphore.WaitAsync().ConfigureAwait(false); + await serializableFile.FileSemaphore.WaitAsync().ConfigureAwait(false); try { // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that - lock (FileSemaphore) { - SavingScheduled = false; + lock (serializableFile.FileSemaphore) { + serializableFile.SavingScheduled = false; } - if (ReadOnly) { + if (serializableFile.ReadOnly) { return; } - string json = JsonConvert.SerializeObject(this, Debugging.IsUserDebugging ? Formatting.Indented : Formatting.None); + string json = serializableFile.ToJsonText(Debugging.IsUserDebugging); if (string.IsNullOrEmpty(json)) { throw new InvalidOperationException(nameof(json)); } // We always want to write entire content to temporary file first, in order to never load corrupted data, also when target file doesn't exist - string newFilePath = $"{FilePath}.new"; + string newFilePath = $"{serializableFile.FilePath}.new"; - if (File.Exists(FilePath)) { - string currentJson = await File.ReadAllTextAsync(FilePath).ConfigureAwait(false); + if (File.Exists(serializableFile.FilePath)) { + string currentJson = await File.ReadAllTextAsync(serializableFile.FilePath).ConfigureAwait(false); if (json == currentJson) { return; @@ -97,16 +111,16 @@ protected async Task Save() { await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - File.Replace(newFilePath, FilePath, null); + File.Replace(newFilePath, serializableFile.FilePath, null); } else { await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - File.Move(newFilePath, FilePath); + File.Move(newFilePath, serializableFile.FilePath); } } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); } finally { - FileSemaphore.Release(); + serializableFile.FileSemaphore.Release(); } } diff --git a/ArchiSteamFarm/IPC/ArchiKestrel.cs b/ArchiSteamFarm/IPC/ArchiKestrel.cs index 03fc7420880aa..1bc14c8dca236 100644 --- a/ArchiSteamFarm/IPC/ArchiKestrel.cs +++ b/ArchiSteamFarm/IPC/ArchiKestrel.cs @@ -21,8 +21,10 @@ using System; using System.IO; +using System.Text.Json.Nodes; using System.Threading.Tasks; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC.Controllers.Api; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; @@ -31,8 +33,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NLog.Web; namespace ArchiSteamFarm.IPC; @@ -83,9 +83,9 @@ internal static async Task Start() { string json = await File.ReadAllTextAsync(customConfigPath).ConfigureAwait(false); if (!string.IsNullOrEmpty(json)) { - JObject jObject = JObject.Parse(json); + JsonNode? jsonNode = JsonNode.Parse(json); - ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jObject.ToString(Formatting.Indented)}"); + ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jsonNode?.ToJsonText(true) ?? "null"}"); } } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs index 474a978562b90..e45cbe23fb127 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs @@ -24,6 +24,7 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.IPC.Requests; @@ -32,7 +33,6 @@ using ArchiSteamFarm.Steam.Interaction; using ArchiSteamFarm.Storage; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -124,9 +124,9 @@ public async Task> ASFPost([FromBody] ASFRequest r } if (ASF.GlobalConfig.AdditionalProperties is { Count: > 0 }) { - request.GlobalConfig.AdditionalProperties ??= new Dictionary(ASF.GlobalConfig.AdditionalProperties.Count, ASF.GlobalConfig.AdditionalProperties.Comparer); + request.GlobalConfig.AdditionalProperties ??= new Dictionary(ASF.GlobalConfig.AdditionalProperties.Count, ASF.GlobalConfig.AdditionalProperties.Comparer); - foreach ((string key, JToken value) in ASF.GlobalConfig.AdditionalProperties.Where(property => !request.GlobalConfig.AdditionalProperties.ContainsKey(property.Key))) { + foreach ((string key, JsonElement value) in ASF.GlobalConfig.AdditionalProperties.Where(property => !request.GlobalConfig.AdditionalProperties.ContainsKey(property.Key))) { request.GlobalConfig.AdditionalProperties.Add(key, value); } diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index 100ecec81ea32..717c2b5ade03b 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -25,6 +25,7 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.IPC.Requests; @@ -33,7 +34,6 @@ using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.Storage; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; using SteamKit2.Internal; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -127,9 +127,9 @@ public async Task> BotPost(string botNames, [FromB } if (bot.BotConfig.AdditionalProperties?.Count > 0) { - request.BotConfig.AdditionalProperties ??= new Dictionary(bot.BotConfig.AdditionalProperties.Count, bot.BotConfig.AdditionalProperties.Comparer); + request.BotConfig.AdditionalProperties ??= new Dictionary(bot.BotConfig.AdditionalProperties.Count, bot.BotConfig.AdditionalProperties.Comparer); - foreach ((string key, JToken value) in bot.BotConfig.AdditionalProperties.Where(property => !request.BotConfig.AdditionalProperties.ContainsKey(property.Key))) { + foreach ((string key, JsonElement value) in bot.BotConfig.AdditionalProperties.Where(property => !request.BotConfig.AdditionalProperties.ContainsKey(property.Key))) { request.BotConfig.AdditionalProperties.Add(key, value); } diff --git a/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs b/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs index 229849b89e7c8..e7e7e52b8760b 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/NLogController.cs @@ -30,13 +30,13 @@ using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; using ArchiSteamFarm.NLog.Targets; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -156,7 +156,7 @@ internal static async void OnNewHistoryEntry(object? sender, HistoryTarget.NewHi return; } - string json = JsonConvert.SerializeObject(new GenericResponse(newHistoryEntryArgs.Message)); + string json = new GenericResponse(newHistoryEntryArgs.Message).ToJsonText(); await Task.WhenAll(ActiveLogWebSockets.Where(static kv => kv.Key.State == WebSocketState.Open).Select(kv => PostLoggedJsonUpdate(kv.Key, json, kv.Value.Semaphore, kv.Value.CancellationToken))).ConfigureAwait(false); } @@ -206,7 +206,7 @@ private static async Task PostLoggedMessageUpdate(WebSocket webSocket, string lo return; } - string response = JsonConvert.SerializeObject(new GenericResponse(loggedMessage)); + string response = new GenericResponse(loggedMessage).ToJsonText(); await PostLoggedJsonUpdate(webSocket, response, sendSemaphore, cancellationToken).ConfigureAwait(false); } diff --git a/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs b/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs index 05e8f5fdd0428..5664224f5e870 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs @@ -21,10 +21,10 @@ using System; using System.Net; +using System.Text.Json; using ArchiSteamFarm.Core; using ArchiSteamFarm.IPC.Responses; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -51,7 +51,7 @@ public ActionResult StorageDelete(string key) { /// Loads entry under specified key from ASF's persistent KeyValue JSON storage. /// [HttpGet] - [ProducesResponseType>((int) HttpStatusCode.OK)] + [ProducesResponseType>((int) HttpStatusCode.OK)] public ActionResult StorageGet(string key) { ArgumentException.ThrowIfNullOrEmpty(key); @@ -59,9 +59,9 @@ public ActionResult StorageGet(string key) { throw new InvalidOperationException(nameof(ASF.GlobalDatabase)); } - JToken? value = ASF.GlobalDatabase.LoadFromJsonStorage(key); + JsonElement value = ASF.GlobalDatabase.LoadFromJsonStorage(key); - return Ok(new GenericResponse(true, value)); + return Ok(new GenericResponse(true, value.ValueKind != JsonValueKind.Undefined ? value : null)); } /// @@ -70,15 +70,18 @@ public ActionResult StorageGet(string key) { [Consumes("application/json")] [HttpPost] [ProducesResponseType((int) HttpStatusCode.OK)] - public ActionResult StoragePost(string key, [FromBody] JToken value) { + public ActionResult StoragePost(string key, [FromBody] JsonElement value) { ArgumentException.ThrowIfNullOrEmpty(key); - ArgumentNullException.ThrowIfNull(value); + + if (value.ValueKind == JsonValueKind.Undefined) { + throw new ArgumentOutOfRangeException(nameof(value)); + } if (ASF.GlobalDatabase == null) { throw new InvalidOperationException(nameof(ASF.GlobalDatabase)); } - if (value.Type == JTokenType.Null) { + if (value.ValueKind == JsonValueKind.Null) { ASF.GlobalDatabase.DeleteFromJsonStorage(key); } else { ASF.GlobalDatabase.SaveToJsonStorage(key, value); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs b/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs index 7aa9dca01e0aa..eaebb2eb886d0 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs @@ -26,11 +26,11 @@ using System.Linq; using System.Net; using System.Reflection; +using System.Text.Json.Serialization; using ArchiSteamFarm.Core; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -63,27 +63,35 @@ public ActionResult TypeGet(string type) { if (targetType.IsClass) { foreach (FieldInfo field in targetType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(static field => !field.IsPrivate)) { - JsonPropertyAttribute? jsonProperty = field.GetCustomAttribute(); + if (!field.IsDefined(typeof(JsonIncludeAttribute), false) || field.IsDefined(typeof(JsonExtensionDataAttribute), false)) { + continue; + } - if (jsonProperty != null) { - string? unifiedName = field.FieldType.GetUnifiedName(); + string? unifiedName = field.FieldType.GetUnifiedName(); - if (!string.IsNullOrEmpty(unifiedName)) { - body[jsonProperty.PropertyName ?? field.Name] = unifiedName; - } + if (string.IsNullOrEmpty(unifiedName)) { + continue; } + + JsonPropertyNameAttribute? jsonPropertyName = field.GetCustomAttribute(); + + body[jsonPropertyName?.Name ?? field.Name] = unifiedName; } foreach (PropertyInfo property in targetType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(static property => property is { CanRead: true, GetMethod.IsPrivate: false })) { - JsonPropertyAttribute? jsonProperty = property.GetCustomAttribute(); + if (!property.IsDefined(typeof(JsonIncludeAttribute), false) || property.IsDefined(typeof(JsonExtensionDataAttribute), false)) { + continue; + } - if (jsonProperty != null) { - string? unifiedName = property.PropertyType.GetUnifiedName(); + string? unifiedName = property.PropertyType.GetUnifiedName(); - if (!string.IsNullOrEmpty(unifiedName)) { - body[jsonProperty.PropertyName ?? property.Name] = unifiedName; - } + if (string.IsNullOrEmpty(unifiedName)) { + continue; } + + JsonPropertyNameAttribute? jsonPropertyName = property.GetCustomAttribute(); + + body[jsonPropertyName?.Name ?? property.Name] = unifiedName; } } else if (targetType.IsEnum) { Type enumType = Enum.GetUnderlyingType(targetType); diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index ba36ab6efcff2..9e6af20f6409b 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -68,7 +68,7 @@ public ApiAuthenticationMiddleware(RequestDelegate next, IOptions jsonOptions) { + public async Task InvokeAsync(HttpContext context, IOptions jsonOptions) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(jsonOptions); @@ -84,7 +84,7 @@ public async Task InvokeAsync(HttpContext context, IOptions(false, statusCodeResponse), jsonOptions.Value.SerializerSettings).ConfigureAwait(false); + await context.Response.WriteAsJsonAsync(new GenericResponse(false, statusCodeResponse), jsonOptions.Value.JsonSerializerOptions).ConfigureAwait(false); } internal static void ClearFailedAuthorizations(object? state = null) => FailedAuthorizations.Clear(); diff --git a/ArchiSteamFarm/IPC/Integration/ReadOnlyFixesSchemaFilter.cs b/ArchiSteamFarm/IPC/Integration/ReadOnlyFixesSchemaFilter.cs new file mode 100644 index 0000000000000..daa3413a8089a --- /dev/null +++ b/ArchiSteamFarm/IPC/Integration/ReadOnlyFixesSchemaFilter.cs @@ -0,0 +1,40 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Ł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.Reflection; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace ArchiSteamFarm.IPC.Integration; + +[UsedImplicitly] +internal sealed class ReadOnlyFixesSchemaFilter : ISchemaFilter { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) { + ArgumentNullException.ThrowIfNull(schema); + ArgumentNullException.ThrowIfNull(context); + + if (schema.ReadOnly && context.MemberInfo is PropertyInfo { CanWrite: true }) { + schema.ReadOnly = false; + } + } +} diff --git a/ArchiSteamFarm/IPC/Integration/SwaggerSteamIdentifierAttribute.cs b/ArchiSteamFarm/IPC/Integration/SwaggerSteamIdentifierAttribute.cs index 44c7a383678d4..3b3707a52d8b6 100644 --- a/ArchiSteamFarm/IPC/Integration/SwaggerSteamIdentifierAttribute.cs +++ b/ArchiSteamFarm/IPC/Integration/SwaggerSteamIdentifierAttribute.cs @@ -28,10 +28,10 @@ namespace ArchiSteamFarm.IPC.Integration; [PublicAPI] public sealed class SwaggerSteamIdentifierAttribute : CustomSwaggerAttribute { - public EAccountType AccountType { get; set; } = EAccountType.Individual; - public uint MaximumAccountID { get; set; } = uint.MaxValue; - public uint MinimumAccountID { get; set; } = 1; - public EUniverse Universe { get; set; } = EUniverse.Public; + public EAccountType AccountType { get; init; } = EAccountType.Individual; + public uint MaximumAccountID { get; init; } = uint.MaxValue; + public uint MinimumAccountID { get; init; } = 1; + public EUniverse Universe { get; init; } = EUniverse.Public; public override void Apply(OpenApiSchema schema) { ArgumentNullException.ThrowIfNull(schema); diff --git a/ArchiSteamFarm/IPC/Integration/SwaggerValidValuesAttribute.cs b/ArchiSteamFarm/IPC/Integration/SwaggerValidValuesAttribute.cs index 5a062f2ccb3c3..43b4f664efde7 100644 --- a/ArchiSteamFarm/IPC/Integration/SwaggerValidValuesAttribute.cs +++ b/ArchiSteamFarm/IPC/Integration/SwaggerValidValuesAttribute.cs @@ -30,8 +30,8 @@ namespace ArchiSteamFarm.IPC.Integration; [PublicAPI] public sealed class SwaggerValidValuesAttribute : CustomSwaggerAttribute { - public int[]? ValidIntValues { get; set; } - public string[]? ValidStringValues { get; set; } + public int[]? ValidIntValues { get; init; } + public string[]? ValidStringValues { get; init; } public override void Apply(OpenApiSchema schema) { ArgumentNullException.ThrowIfNull(schema); diff --git a/ArchiSteamFarm/IPC/Requests/ASFEncryptRequest.cs b/ArchiSteamFarm/IPC/Requests/ASFEncryptRequest.cs index 1eb6e8e8adfc1..cc9506f268ade 100644 --- a/ArchiSteamFarm/IPC/Requests/ASFEncryptRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/ASFEncryptRequest.cs @@ -21,8 +21,8 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Helpers; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -31,16 +31,18 @@ public sealed class ASFEncryptRequest { /// /// Encryption method used for encrypting this string. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public ArchiCryptoHelper.ECryptoMethod CryptoMethod { get; private set; } + public ArchiCryptoHelper.ECryptoMethod CryptoMethod { get; private init; } /// /// String to encrypt with provided . /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string StringToEncrypt { get; private set; } = ""; + public string StringToEncrypt { get; private init; } = ""; [JsonConstructor] private ASFEncryptRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/ASFHashRequest.cs b/ArchiSteamFarm/IPC/Requests/ASFHashRequest.cs index 8496af8a5bd27..bf43e25d9f397 100644 --- a/ArchiSteamFarm/IPC/Requests/ASFHashRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/ASFHashRequest.cs @@ -21,8 +21,8 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Helpers; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -31,16 +31,18 @@ public sealed class ASFHashRequest { /// /// Hashing method used for hashing this string. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public ArchiCryptoHelper.EHashingMethod HashingMethod { get; private set; } + public ArchiCryptoHelper.EHashingMethod HashingMethod { get; private init; } /// /// String to hash with provided . /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string StringToHash { get; private set; } = ""; + public string StringToHash { get; private init; } = ""; [JsonConstructor] private ASFHashRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/ASFRequest.cs b/ArchiSteamFarm/IPC/Requests/ASFRequest.cs index ca66e7a591ce7..b0d072aa574f7 100644 --- a/ArchiSteamFarm/IPC/Requests/ASFRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/ASFRequest.cs @@ -21,8 +21,8 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Storage; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -31,9 +31,10 @@ public sealed class ASFRequest { /// /// ASF's global config structure. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public GlobalConfig GlobalConfig { get; private set; } = new(); + public GlobalConfig GlobalConfig { get; private init; } = new(); [JsonConstructor] private ASFRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/BotGamesToRedeemInBackgroundRequest.cs b/ArchiSteamFarm/IPC/Requests/BotGamesToRedeemInBackgroundRequest.cs index 896213a5fc82e..f04ccb04fde4c 100644 --- a/ArchiSteamFarm/IPC/Requests/BotGamesToRedeemInBackgroundRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/BotGamesToRedeemInBackgroundRequest.cs @@ -22,7 +22,7 @@ using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Requests; @@ -35,9 +35,10 @@ public sealed class BotGamesToRedeemInBackgroundRequest { /// Key in the map must be a valid and unique Steam cd-key. /// Value in the map must be a non-null and non-empty name of the key (e.g. game's name, but can be anything). /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public OrderedDictionary GamesToRedeemInBackground { get; private set; } = new(); + public OrderedDictionary GamesToRedeemInBackground { get; private init; } = new(); [JsonConstructor] private BotGamesToRedeemInBackgroundRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/BotInputRequest.cs b/ArchiSteamFarm/IPC/Requests/BotInputRequest.cs index 87838580249ef..30c953e2de28d 100644 --- a/ArchiSteamFarm/IPC/Requests/BotInputRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/BotInputRequest.cs @@ -19,9 +19,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Core; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -30,14 +31,18 @@ public sealed class BotInputRequest { /// /// Specifies the type of the input. /// - [JsonProperty(Required = Required.Always)] - public ASF.EUserInputType Type { get; private set; } + [JsonInclude] + [JsonRequired] + [Required] + public ASF.EUserInputType Type { get; private init; } /// /// Specifies the value for given input type (declared in ) /// - [JsonProperty(Required = Required.Always)] - public string Value { get; private set; } = ""; + [JsonInclude] + [JsonRequired] + [Required] + public string Value { get; private init; } = ""; [JsonConstructor] private BotInputRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/BotPauseRequest.cs b/ArchiSteamFarm/IPC/Requests/BotPauseRequest.cs index d62d635d85ff4..de3de13a816a7 100644 --- a/ArchiSteamFarm/IPC/Requests/BotPauseRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/BotPauseRequest.cs @@ -20,7 +20,7 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Requests; @@ -29,14 +29,14 @@ public sealed class BotPauseRequest { /// /// Specifies if pause is permanent or temporary (default). /// - [JsonProperty(Required = Required.DisallowNull)] - public bool Permanent { get; private set; } + [JsonInclude] + public bool Permanent { get; private init; } /// /// Specifies automatic resume action in given seconds. Default value of 0 disables automatic resume. /// - [JsonProperty(Required = Required.DisallowNull)] - public ushort ResumeInSeconds { get; private set; } + [JsonInclude] + public ushort ResumeInSeconds { get; private init; } [JsonConstructor] private BotPauseRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/BotRedeemRequest.cs b/ArchiSteamFarm/IPC/Requests/BotRedeemRequest.cs index 1584587cc1ac2..b97c2c054427e 100644 --- a/ArchiSteamFarm/IPC/Requests/BotRedeemRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/BotRedeemRequest.cs @@ -22,7 +22,7 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Requests; @@ -31,9 +31,10 @@ public sealed class BotRedeemRequest { /// /// A collection (set) of keys to redeem. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public ImmutableHashSet KeysToRedeem { get; private set; } = ImmutableHashSet.Empty; + public ImmutableHashSet KeysToRedeem { get; private init; } = ImmutableHashSet.Empty; [JsonConstructor] private BotRedeemRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/BotRenameRequest.cs b/ArchiSteamFarm/IPC/Requests/BotRenameRequest.cs index cf8891686e326..e4bda406a9854 100644 --- a/ArchiSteamFarm/IPC/Requests/BotRenameRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/BotRenameRequest.cs @@ -21,7 +21,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Requests; @@ -30,9 +30,10 @@ public sealed class BotRenameRequest { /// /// Specifies the new name for the bot. The new name can't be "ASF", neither the one used by any existing bot. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string NewName { get; private set; } = ""; + public string NewName { get; private init; } = ""; [JsonConstructor] private BotRenameRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/BotRequest.cs b/ArchiSteamFarm/IPC/Requests/BotRequest.cs index abefd5811b769..a5790909ae182 100644 --- a/ArchiSteamFarm/IPC/Requests/BotRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/BotRequest.cs @@ -21,8 +21,8 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Storage; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -31,9 +31,10 @@ public sealed class BotRequest { /// /// ASF's bot config structure. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public BotConfig BotConfig { get; private set; } = new(); + public BotConfig BotConfig { get; private init; } = new(); [JsonConstructor] private BotRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/CommandRequest.cs b/ArchiSteamFarm/IPC/Requests/CommandRequest.cs index d0d3af287c9a2..e58f680c29fc6 100644 --- a/ArchiSteamFarm/IPC/Requests/CommandRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/CommandRequest.cs @@ -21,7 +21,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Requests; @@ -30,9 +30,10 @@ public sealed class CommandRequest { /// /// Specifies the command that will be executed by ASF. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string Command { get; private set; } = ""; + public string Command { get; private init; } = ""; [JsonConstructor] private CommandRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs b/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs index 5ac25c02a18a3..7badf0328bd13 100644 --- a/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/TwoFactorAuthenticationConfirmationsRequest.cs @@ -22,13 +22,15 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -37,29 +39,34 @@ public sealed class TwoFactorAuthenticationConfirmationsRequest { /// /// Specifies the target action, whether we should accept the confirmations (true), or decline them (false). /// - [JsonProperty(Required = Required.Always)] - public bool Accept { get; private set; } + [JsonInclude] + [JsonRequired] + [Required] + public bool Accept { get; private init; } /// /// Specifies IDs of the confirmations that we're supposed to handle. CreatorID of the confirmation is equal to ID of the object that triggered it - e.g. ID of the trade offer, or ID of the market listing. If not provided, or empty array, all confirmation IDs are considered for an action. /// - [JsonProperty(Required = Required.DisallowNull)] - public ImmutableHashSet AcceptedCreatorIDs { get; private set; } = ImmutableHashSet.Empty; + [JsonDisallowNull] + [JsonInclude] + public ImmutableHashSet AcceptedCreatorIDs { get; private init; } = ImmutableHashSet.Empty; /// /// Specifies the type of confirmations to handle. If not provided, all confirmation types are considered for an action. /// - [JsonProperty] - public Confirmation.EConfirmationType? AcceptedType { get; private set; } + [JsonInclude] + public Confirmation.EConfirmationType? AcceptedType { get; private init; } /// /// A helper property which works the same as but with values written as strings - for javascript compatibility purposes. Use either this one, or , not both. /// - [JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(AcceptedCreatorIDs)}", Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(AcceptedCreatorIDs)}")] public ImmutableHashSet SAcceptedCreatorIDs { get => AcceptedCreatorIDs.Select(static creatorID => creatorID.ToString(CultureInfo.InvariantCulture)).ToImmutableHashSet(StringComparer.Ordinal); - private set { + private init { ArgumentNullException.ThrowIfNull(value); HashSet acceptedCreatorIDs = []; @@ -81,8 +88,8 @@ private set { /// /// Specifies whether we should wait for the confirmations to arrive, in case they're not available immediately. This option makes sense only if is specified as well, and in this case ASF will add a few more tries if needed to ensure that all specified IDs are handled. Useful if confirmations are generated with a delay on Steam network side, which happens fairly often. /// - [JsonProperty(Required = Required.DisallowNull)] - public bool WaitIfNeeded { get; private set; } + [JsonInclude] + public bool WaitIfNeeded { get; private init; } [JsonConstructor] private TwoFactorAuthenticationConfirmationsRequest() { } diff --git a/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs b/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs index 5a1e7213447a8..a9c28d1ebc44c 100644 --- a/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs @@ -20,8 +20,8 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Storage; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Requests; @@ -30,8 +30,8 @@ public sealed class UpdateRequest { /// /// Target update channel. Not required, will default to UpdateChannel in GlobalConfig if not provided. /// - [JsonProperty(Required = Required.DisallowNull)] - public GlobalConfig.EUpdateChannel? Channel { get; private set; } + [JsonInclude] + public GlobalConfig.EUpdateChannel? Channel { get; private init; } [JsonConstructor] private UpdateRequest() { } diff --git a/ArchiSteamFarm/IPC/Responses/ASFResponse.cs b/ArchiSteamFarm/IPC/Responses/ASFResponse.cs index 92fb6a46b1dd5..0b00dda7fa62e 100644 --- a/ArchiSteamFarm/IPC/Responses/ASFResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/ASFResponse.cs @@ -21,8 +21,8 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using ArchiSteamFarm.Storage; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Responses; @@ -30,51 +30,58 @@ public sealed class ASFResponse { /// /// ASF's build variant. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string BuildVariant { get; private set; } + public string BuildVariant { get; private init; } /// /// A value specifying whether this variant of ASF is capable of auto-update. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public bool CanUpdate { get; private set; } + public bool CanUpdate { get; private init; } /// /// Currently loaded ASF's global config. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public GlobalConfig GlobalConfig { get; private set; } + public GlobalConfig GlobalConfig { get; private init; } /// /// Current amount of managed memory being used by the process, in kilobytes. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public uint MemoryUsage { get; private set; } + public uint MemoryUsage { get; private init; } /// /// Start date of the process. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public DateTime ProcessStartTime { get; private set; } + public DateTime ProcessStartTime { get; private init; } /// /// Boolean value specifying whether ASF has been started with a --service parameter. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public bool Service { get; private set; } + public bool Service { get; private init; } /// /// ASF version of currently running binary. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public Version Version { get; private set; } + public Version Version { get; private init; } internal ASFResponse(string buildVariant, bool canUpdate, GlobalConfig globalConfig, uint memoryUsage, DateTime processStartTime, Version version) { ArgumentException.ThrowIfNullOrEmpty(buildVariant); diff --git a/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs b/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs index 7daf278f002ef..4028f4f6981be 100644 --- a/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs @@ -20,7 +20,7 @@ // limitations under the License. using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Responses; @@ -28,14 +28,14 @@ public sealed class GamesToRedeemInBackgroundResponse { /// /// Keys that were redeemed and not used during the process, if available. /// - [JsonProperty] - public Dictionary? UnusedKeys { get; private set; } + [JsonInclude] + public Dictionary? UnusedKeys { get; private init; } /// /// Keys that were redeemed and used during the process, if available. /// - [JsonProperty] - public Dictionary? UsedKeys { get; private set; } + [JsonInclude] + public Dictionary? UsedKeys { get; private init; } internal GamesToRedeemInBackgroundResponse(Dictionary? unusedKeys = null, Dictionary? usedKeys = null) { UnusedKeys = unusedKeys; diff --git a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs index 74e0450e9b04d..cc6765df0ee66 100644 --- a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs @@ -20,8 +20,8 @@ // limitations under the License. using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using ArchiSteamFarm.Localization; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Responses; @@ -32,8 +32,8 @@ public sealed class GenericResponse : GenericResponse { /// /// The type of the result depends on the API endpoint that you've called. /// - [JsonProperty] - public T? Result { get; private set; } + [JsonInclude] + public T? Result { get; private init; } public GenericResponse(T? result) : base(result is not null) => Result = result; public GenericResponse(bool success, string? message) : base(success, message) { } @@ -51,15 +51,16 @@ public class GenericResponse { /// /// This property will provide exact reason for majority of expected failures. /// - [JsonProperty] - public string? Message { get; private set; } + [JsonInclude] + public string? Message { get; private init; } /// /// Boolean type that specifies if the request has succeeded. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public bool Success { get; private set; } + public bool Success { get; private init; } public GenericResponse(bool success, string? message = null) { Success = success; diff --git a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs index cc2a6aaf0022a..9c658b623cdbb 100644 --- a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs @@ -21,8 +21,8 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using ArchiSteamFarm.Web; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC.Responses; @@ -30,30 +30,34 @@ public sealed class GitHubReleaseResponse { /// /// Changelog of the release rendered in HTML. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string ChangelogHTML { get; private set; } + public string ChangelogHTML { get; private init; } /// /// Date of the release. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public DateTime ReleasedAt { get; private set; } + public DateTime ReleasedAt { get; private init; } /// /// Boolean value that specifies whether the build is stable or not (pre-release). /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public bool Stable { get; private set; } + public bool Stable { get; private init; } /// /// Version of the release. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public string Version { get; private set; } + public string Version { get; private init; } internal GitHubReleaseResponse(GitHub.ReleaseResponse releaseResponse) { ArgumentNullException.ThrowIfNull(releaseResponse); diff --git a/ArchiSteamFarm/IPC/Responses/LogResponse.cs b/ArchiSteamFarm/IPC/Responses/LogResponse.cs index 7707c08c37962..8ddd061d9d0c4 100644 --- a/ArchiSteamFarm/IPC/Responses/LogResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/LogResponse.cs @@ -22,7 +22,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Responses; @@ -30,16 +30,18 @@ public sealed class LogResponse { /// /// Content of the log file which consists of lines read from it - in chronological order. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public IReadOnlyList Content { get; private set; } + public IReadOnlyList Content { get; private init; } /// /// Total number of lines of the log file returned, can be used as an index for future requests. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public int TotalLines { get; private set; } + public int TotalLines { get; private init; } internal LogResponse(int totalLines, IReadOnlyList content) { ArgumentOutOfRangeException.ThrowIfNegative(totalLines); diff --git a/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs b/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs index 977ee7127a405..4e5829fb10894 100644 --- a/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs @@ -21,7 +21,7 @@ using System.ComponentModel.DataAnnotations; using System.Net; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Responses; @@ -29,16 +29,18 @@ public sealed class StatusCodeResponse { /// /// Value indicating whether the status is permanent. If yes, retrying the request with exactly the same payload doesn't make sense due to a permanent problem (e.g. ASF misconfiguration). /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public bool Permanent { get; private set; } + public bool Permanent { get; private init; } /// /// Status code transmitted in addition to the one in HTTP spec. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public HttpStatusCode StatusCode { get; private set; } + public HttpStatusCode StatusCode { get; private init; } internal StatusCodeResponse(HttpStatusCode statusCode, bool permanent) { StatusCode = statusCode; diff --git a/ArchiSteamFarm/IPC/Responses/TypeProperties.cs b/ArchiSteamFarm/IPC/Responses/TypeProperties.cs index fb98e743587fb..aa16b8978d360 100644 --- a/ArchiSteamFarm/IPC/Responses/TypeProperties.cs +++ b/ArchiSteamFarm/IPC/Responses/TypeProperties.cs @@ -21,7 +21,7 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Responses; @@ -32,8 +32,8 @@ public sealed class TypeProperties { /// /// This can be used for determining how the body of the response should be interpreted. /// - [JsonProperty] - public string? BaseType { get; private set; } + [JsonInclude] + public string? BaseType { get; private init; } /// /// Custom attributes of given type, if available. @@ -41,8 +41,8 @@ public sealed class TypeProperties { /// /// This can be used for determining main enum type if is . /// - [JsonProperty] - public HashSet? CustomAttributes { get; private set; } + [JsonInclude] + public HashSet? CustomAttributes { get; private init; } /// /// Underlying type of given type, if available. @@ -50,8 +50,8 @@ public sealed class TypeProperties { /// /// This can be used for determining underlying enum type if is . /// - [JsonProperty] - public string? UnderlyingType { get; private set; } + [JsonInclude] + public string? UnderlyingType { get; private init; } internal TypeProperties(string? baseType = null, HashSet? customAttributes = null, string? underlyingType = null) { BaseType = baseType; diff --git a/ArchiSteamFarm/IPC/Responses/TypeResponse.cs b/ArchiSteamFarm/IPC/Responses/TypeResponse.cs index 91063582f4ce0..e3f5447b7efe9 100644 --- a/ArchiSteamFarm/IPC/Responses/TypeResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/TypeResponse.cs @@ -22,7 +22,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.IPC.Responses; @@ -35,16 +35,18 @@ public sealed class TypeResponse { /// For enums, keys are friendly names while values are underlying values of those names. /// For objects, keys are non-private fields and properties, while values are underlying types of those. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public Dictionary Body { get; private set; } + public Dictionary Body { get; private init; } /// /// Metadata of given type. /// - [JsonProperty(Required = Required.Always)] + [JsonInclude] + [JsonRequired] [Required] - public TypeProperties Properties { get; private set; } + public TypeProperties Properties { get; private init; } internal TypeResponse(Dictionary body, TypeProperties properties) { ArgumentNullException.ThrowIfNull(body); diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index 9ca530820313b..4dc1ec1925bee 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -28,6 +28,7 @@ using System.Net; using System.Reflection; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC.Integration; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Plugins; @@ -45,8 +46,6 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace ArchiSteamFarm.IPC; @@ -284,6 +283,7 @@ public void ConfigureServices(IServiceCollection services) { options.SchemaFilter(); options.SchemaFilter(); + options.SchemaFilter(); options.SwaggerDoc( SharedInfo.ASF, new OpenApiInfo { @@ -310,9 +310,6 @@ public void ConfigureServices(IServiceCollection services) { } ); - // Add support for Newtonsoft.Json in swagger, this one must be executed after AddSwaggerGen() - services.AddSwaggerGenNewtonsoftSupport(); - // We need MVC for /Api, but we're going to use only a small subset of all available features IMvcBuilder mvc = services.AddControllers(); @@ -329,14 +326,10 @@ public void ConfigureServices(IServiceCollection services) { mvc.AddControllersAsServices(); - mvc.AddNewtonsoftJson( + mvc.AddJsonOptions( static options => { - // Fix default contract resolver to use original names and not a camel case - options.SerializerSettings.ContractResolver = new DefaultContractResolver(); - - if (Debugging.IsUserDebugging) { - options.SerializerSettings.Formatting = Formatting.Indented; - } + options.JsonSerializerOptions.PropertyNamingPolicy = JsonUtilities.DefaultJsonSerialierOptions.PropertyNamingPolicy; + options.JsonSerializerOptions.TypeInfoResolver = JsonUtilities.DefaultJsonSerialierOptions.TypeInfoResolver; } ); } diff --git a/ArchiSteamFarm/IPC/WebUtilities.cs b/ArchiSteamFarm/IPC/WebUtilities.cs index 649583ce94ea5..539f6c7320a92 100644 --- a/ArchiSteamFarm/IPC/WebUtilities.cs +++ b/ArchiSteamFarm/IPC/WebUtilities.cs @@ -21,12 +21,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; namespace ArchiSteamFarm.IPC; @@ -56,26 +51,4 @@ internal static class WebUtilities { return Type.GetType($"{typeText},{typeText[..index]}"); } - - internal static async Task WriteJsonAsync(this HttpResponse response, TValue? value, JsonSerializerSettings? jsonSerializerSettings = null) { - ArgumentNullException.ThrowIfNull(response); - - JsonSerializer serializer = JsonSerializer.CreateDefault(jsonSerializerSettings); - - response.ContentType = "application/json; charset=utf-8"; - - StreamWriter streamWriter = new(response.Body, Encoding.UTF8); - - await using (streamWriter.ConfigureAwait(false)) { -#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose - JsonTextWriter jsonWriter = new(streamWriter) { - CloseOutput = false - }; -#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose - - await using (jsonWriter.ConfigureAwait(false)) { - serializer.Serialize(jsonWriter, value); - } - } - } } diff --git a/ArchiSteamFarm/Plugins/Interfaces/IASF.cs b/ArchiSteamFarm/Plugins/Interfaces/IASF.cs index e797acae53cd2..1b695787448c7 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IASF.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IASF.cs @@ -20,10 +20,10 @@ // limitations under the License. using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -33,5 +33,5 @@ public interface IASF : IPlugin { /// ASF will call this method right after global config initialization. /// /// Extra config properties made out of . Can be null if no extra properties are found. - Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null); + Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null); } diff --git a/ArchiSteamFarm/Plugins/Interfaces/IBotModules.cs b/ArchiSteamFarm/Plugins/Interfaces/IBotModules.cs index 83afc58b2ab4d..10bf8ec96855d 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IBotModules.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IBotModules.cs @@ -20,11 +20,11 @@ // limitations under the License. using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Steam; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -35,5 +35,5 @@ public interface IBotModules : IPlugin { /// /// Bot object related to this callback. /// Extra config properties made out of . Can be null if no extra properties are found. - Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null); + Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null); } diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPlugin.cs b/ArchiSteamFarm/Plugins/Interfaces/IPlugin.cs index cc3c5d9e75077..0cc8536db0e48 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPlugin.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPlugin.cs @@ -20,9 +20,10 @@ // limitations under the License. using System; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using System.Threading.Tasks; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -32,7 +33,8 @@ public interface IPlugin { /// ASF will use this property as general plugin identifier for the user. /// /// String that will be used as the name of this plugin. - [JsonProperty] + [JsonInclude] + [Required] string Name { get; } /// @@ -40,7 +42,8 @@ public interface IPlugin { /// You have a freedom in deciding what versioning you want to use, this is for identification purposes only. /// /// Version that will be shown to the user when plugin is loaded. - [JsonProperty] + [JsonInclude] + [Required] Version Version { get; } /// diff --git a/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs b/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs index 646a793b1724e..d9ec22fd10a25 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs @@ -19,7 +19,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -35,6 +35,6 @@ public interface IWebInterface : IPlugin { /// /// Specifies web path (address) under which ASF should host your static WWW files in directory. Default value of "/" allows you to override default ASF files and gives you full flexibility in your directory. However, you can instead host your files under some other fixed location specified here, such as "/MyPlugin". /// - [JsonProperty] + [JsonInclude] string WebPath => "/"; } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index d9f81316c299c..dc037bb85aea7 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -33,6 +33,7 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; @@ -43,7 +44,6 @@ using ArchiSteamFarm.Steam.Exchange; using ArchiSteamFarm.Steam.Integration.Callbacks; using JetBrains.Annotations; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.Plugins; @@ -286,7 +286,7 @@ internal static async Task InitPlugins() { return assemblies; } - internal static async Task OnASFInitModules(IReadOnlyDictionary? additionalConfigProperties = null) { + internal static async Task OnASFInitModules(IReadOnlyDictionary? additionalConfigProperties = null) { if (ActivePlugins.Count == 0) { return; } @@ -436,7 +436,7 @@ internal static async Task OnBotInit(Bot bot) { } } - internal static async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { + internal static async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) { ArgumentNullException.ThrowIfNull(bot); if (ActivePlugins.Count == 0) { diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index b6f20eac981c5..9f9def7245b5b 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -33,6 +33,7 @@ using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; @@ -40,7 +41,6 @@ using ArchiSteamFarm.Steam; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; -using Newtonsoft.Json; using NLog; using SteamKit2; @@ -353,7 +353,7 @@ private static async Task InitGlobalConfigAndLanguage() { } if (globalConfig.Debug) { - ASF.ArchiLogger.LogGenericDebug($"{globalConfigFile}: {JsonConvert.SerializeObject(globalConfig, Formatting.Indented)}"); + ASF.ArchiLogger.LogGenericDebug($"{globalConfigFile}: {globalConfig.ToJsonText(true)}"); } if (!string.IsNullOrEmpty(globalConfig.CurrentCulture)) { @@ -414,7 +414,7 @@ private static async Task InitGlobalDatabaseAndServices() { // If debugging is on, we prepare debug directory prior to running if (Debugging.IsUserDebugging) { if (Debugging.IsDebugConfigured) { - ASF.ArchiLogger.LogGenericDebug($"{globalDatabaseFile}: {JsonConvert.SerializeObject(ASF.GlobalDatabase, Formatting.Indented)}"); + ASF.ArchiLogger.LogGenericDebug($"{globalDatabaseFile}: {globalDatabase.ToJsonText(true)}"); } Logging.EnableTraceLogging(); diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index a15743d9d8be9..003bd669981ae 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -27,10 +27,12 @@ using System.Collections.Immutable; using System.Collections.Specialized; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -38,6 +40,7 @@ using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; using ArchiSteamFarm.Plugins; @@ -53,7 +56,6 @@ using ArchiSteamFarm.Web; using JetBrains.Annotations; using Microsoft.IdentityModel.JsonWebTokens; -using Newtonsoft.Json; using SteamKit2; using SteamKit2.Authentication; using SteamKit2.Internal; @@ -101,24 +103,28 @@ public sealed class Bot : IAsyncDisposable, IDisposable { [PublicAPI] public BotDatabase BotDatabase { get; } - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public string BotName { get; } - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public CardsFarmer CardsFarmer { get; } [JsonIgnore] [PublicAPI] public Commands Commands { get; } - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public uint GamesToRedeemInBackgroundCount => BotDatabase.GamesToRedeemInBackgroundCount; - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public bool HasMobileAuthenticator => BotDatabase.MobileAuthenticator != null; [JsonIgnore] @@ -129,18 +135,26 @@ public sealed class Bot : IAsyncDisposable, IDisposable { [PublicAPI] public bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown); - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public bool IsConnectedAndLoggedOn => SteamClient.SteamID != null; - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public bool IsPlayingPossible => !PlayingBlocked && !LibraryLocked; - [JsonProperty] + [JsonInclude] [PublicAPI] public string? PublicIP => SteamClient.PublicIP?.ToString(); + [JsonInclude] + [JsonPropertyName($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")] + [PublicAPI] + [Required] + public string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture); + [JsonIgnore] [PublicAPI] public SteamApps SteamApps { get; } @@ -186,9 +200,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } } - [JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamID)}")] - private string SSteamID => SteamID.ToString(CultureInfo.InvariantCulture); - [JsonIgnore] [PublicAPI] public string? AccessToken { @@ -217,19 +228,29 @@ private set { } } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public EAccountFlags AccountFlags { get; private set; } - [JsonProperty] + [JsonInclude] [PublicAPI] + public string? AvatarHash { get; private set; } + + [JsonInclude] + [JsonRequired] + [PublicAPI] + [Required] public BotConfig BotConfig { get; private set; } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public bool KeepRunning { get; private set; } - [JsonProperty] + [JsonInclude] [PublicAPI] public string? Nickname { get; private set; } @@ -237,24 +258,34 @@ private set { [PublicAPI] public FrozenDictionary OwnedPackageIDs { get; private set; } = FrozenDictionary.Empty; - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public ASF.EUserInputType RequiredInput { get; private set; } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public ulong SteamID { get; private set; } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public long WalletBalance { get; private set; } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public long WalletBalanceDelayed { get; private set; } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public ECurrencyCode WalletCurrency { get; private set; } internal byte HeartBeatFailures { get; private set; } @@ -264,9 +295,6 @@ private set { private DateTime? AccessTokenValidUntil; private string? AuthCode; - [JsonProperty] - private string? AvatarHash; - private string? BackingAccessToken; private Timer? ConnectionFailureTimer; private bool FirstTradeSent; @@ -1660,7 +1688,7 @@ internal static async Task RegisterBot(string botName) { } if (Debugging.IsDebugConfigured) { - ASF.ArchiLogger.LogGenericDebug($"{configFilePath}: {JsonConvert.SerializeObject(botConfig, Formatting.Indented)}"); + ASF.ArchiLogger.LogGenericDebug($"{configFilePath}: {botConfig.ToJsonText(true)}"); } if (!string.IsNullOrEmpty(latestJson)) { @@ -1688,7 +1716,7 @@ internal static async Task RegisterBot(string botName) { } if (Debugging.IsDebugConfigured) { - ASF.ArchiLogger.LogGenericDebug($"{databaseFilePath}: {JsonConvert.SerializeObject(botDatabase, Formatting.Indented)}"); + ASF.ArchiLogger.LogGenericDebug($"{databaseFilePath}: {botDatabase.ToJsonText(true)}"); } botDatabase.PerformMaintenance(); @@ -2280,7 +2308,7 @@ private async Task ImportAuthenticatorFromFile(string maFilePath) { return; } - MobileAuthenticator? authenticator = JsonConvert.DeserializeObject(json); + MobileAuthenticator? authenticator = json.ToJsonObject(); if (authenticator == null) { ArchiLogger.LogNullError(authenticator); diff --git a/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs b/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs index 2912ad664f1cd..0c827abc7f785 100644 --- a/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs +++ b/ArchiSteamFarm/Steam/Cards/CardsFarmer.cs @@ -24,8 +24,10 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -42,7 +44,6 @@ using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; using JetBrains.Annotations; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.Steam.Cards; @@ -63,16 +64,21 @@ public sealed class CardsFarmer : IAsyncDisposable, IDisposable { // Games that were confirmed to show false status on general badges page private static readonly FrozenSet UntrustedAppIDs = new HashSet(3) { 440, 570, 730 }.ToFrozenSet(); - [JsonProperty(nameof(CurrentGamesFarming))] + [JsonInclude] + [JsonPropertyName(nameof(CurrentGamesFarming))] [PublicAPI] + [Required] public IReadOnlyCollection CurrentGamesFarmingReadOnly => CurrentGamesFarming; - [JsonProperty(nameof(GamesToFarm))] + [JsonInclude] + [JsonPropertyName(nameof(GamesToFarm))] [PublicAPI] + [Required] public IReadOnlyCollection GamesToFarmReadOnly => GamesToFarm; - [JsonProperty] + [JsonInclude] [PublicAPI] + [Required] public TimeSpan TimeRemaining { get { if (GamesToFarm.Count == 0) { @@ -136,8 +142,10 @@ private IEnumerable> SourcesOfIgnoredAppIDs } } - [JsonProperty] + [JsonInclude] + [JsonRequired] [PublicAPI] + [Required] public bool Paused { get; private set; } internal bool NowFarming { get; private set; } diff --git a/ArchiSteamFarm/Steam/Cards/Game.cs b/ArchiSteamFarm/Steam/Cards/Game.cs index f0d23367e3eeb..29b82067caa2a 100644 --- a/ArchiSteamFarm/Steam/Cards/Game.cs +++ b/ArchiSteamFarm/Steam/Cards/Game.cs @@ -20,23 +20,28 @@ // limitations under the License. using System; -using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Cards; public sealed class Game : IEquatable { - [JsonProperty] + [JsonInclude] + [Required] public uint AppID { get; } - [JsonProperty] + [JsonInclude] + [Required] public string GameName { get; } internal readonly byte BadgeLevel; - [JsonProperty] + [JsonInclude] + [Required] public ushort CardsRemaining { get; internal set; } - [JsonProperty] + [JsonInclude] + [Required] public float HoursPlayed { get; internal set; } internal uint PlayableAppID { get; set; } diff --git a/ArchiSteamFarm/Steam/Data/Asset.cs b/ArchiSteamFarm/Steam/Data/Asset.cs index 318f6b66e3ad0..8bec38e0af526 100644 --- a/ArchiSteamFarm/Steam/Data/Asset.cs +++ b/ArchiSteamFarm/Steam/Data/Asset.cs @@ -22,11 +22,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; -using ArchiSteamFarm.Core; +using System.Text.Json; +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ArchiSteamFarm.Steam.Data; @@ -43,34 +41,46 @@ public sealed class Asset { [JsonIgnore] [PublicAPI] - public IReadOnlyDictionary? AdditionalPropertiesReadOnly => AdditionalProperties; + public IReadOnlyDictionary? AdditionalPropertiesReadOnly => AdditionalProperties; [JsonIgnore] [PublicAPI] public bool IsSteamPointsShopItem => !Tradable && (InstanceID == SteamPointsShopInstanceID); - [JsonIgnore] + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("amount")] + [JsonRequired] [PublicAPI] public uint Amount { get; internal set; } - [JsonProperty("appid", Required = Required.DisallowNull)] - public uint AppID { get; private set; } + [JsonInclude] + [JsonPropertyName("appid")] + public uint AppID { get; private init; } - [JsonIgnore] + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("assetid")] [PublicAPI] - public ulong AssetID { get; private set; } + public ulong AssetID { get; private init; } - [JsonIgnore] + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("classid")] [PublicAPI] - public ulong ClassID { get; private set; } + public ulong ClassID { get; private init; } - [JsonIgnore] + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("contextid")] [PublicAPI] - public ulong ContextID { get; private set; } + public ulong ContextID { get; private init; } - [JsonIgnore] + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("instanceid")] [PublicAPI] - public ulong InstanceID { get; private set; } + public ulong InstanceID { get; private init; } [JsonIgnore] [PublicAPI] @@ -96,109 +106,16 @@ public sealed class Asset { [PublicAPI] public EType Type { get; internal set; } - [JsonExtensionData(WriteData = false)] - internal Dictionary? AdditionalProperties { private get; set; } - - [JsonProperty("amount", Required = Required.Always)] - private string AmountText { - get => Amount.ToString(CultureInfo.InvariantCulture); - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!uint.TryParse(value, out uint amount) || (amount == 0)) { - ASF.ArchiLogger.LogNullError(amount); - - return; - } - - Amount = amount; - } - } - - [JsonProperty("assetid", Required = Required.DisallowNull)] - private string AssetIDText { - get => AssetID.ToString(CultureInfo.InvariantCulture); - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!ulong.TryParse(value, out ulong assetID) || (assetID == 0)) { - ASF.ArchiLogger.LogNullError(assetID); - - return; - } - - AssetID = assetID; - } - } - - [JsonProperty("classid", Required = Required.DisallowNull)] - private string ClassIDText { - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!ulong.TryParse(value, out ulong classID) || (classID == 0)) { - return; - } + [JsonExtensionData] + [JsonInclude] + internal Dictionary? AdditionalProperties { get; set; } - ClassID = classID; - } - } - - [JsonProperty("contextid", Required = Required.DisallowNull)] - private string ContextIDText { - get => ContextID.ToString(CultureInfo.InvariantCulture); - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!ulong.TryParse(value, out ulong contextID) || (contextID == 0)) { - ASF.ArchiLogger.LogNullError(contextID); - - return; - } - - ContextID = contextID; - } - } - - [JsonProperty("id", Required = Required.DisallowNull)] - private string IDText { - set => AssetIDText = value; - } - - [JsonProperty("instanceid", Required = Required.DisallowNull)] - private string InstanceIDText { - set { - if (string.IsNullOrEmpty(value)) { - return; - } - - if (!ulong.TryParse(value, out ulong instanceID)) { - ASF.ArchiLogger.LogNullError(instanceID); - - return; - } - - InstanceID = instanceID; - } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("id")] + private ulong ID { + get => AssetID; + init => AssetID = value; } // Constructed from trades being received or plugins @@ -228,6 +145,9 @@ public Asset(uint appID, ulong contextID, ulong classID, uint amount, ulong inst [JsonConstructor] private Asset() { } + [UsedImplicitly] + public static bool ShouldSerializeAdditionalProperties() => false; + internal Asset CreateShallowCopy() => (Asset) MemberwiseClone(); public enum ERarity : byte { diff --git a/ArchiSteamFarm/Steam/Data/BooleanResponse.cs b/ArchiSteamFarm/Steam/Data/BooleanResponse.cs index b77818e53bbe0..d66b18b79178c 100644 --- a/ArchiSteamFarm/Steam/Data/BooleanResponse.cs +++ b/ArchiSteamFarm/Steam/Data/BooleanResponse.cs @@ -20,8 +20,8 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Data; @@ -30,8 +30,10 @@ namespace ArchiSteamFarm.Steam.Data; public class BooleanResponse { // You say it works in a RESTFUL way // Then your errors come back as 200 OK - [JsonProperty("success", Required = Required.Always)] - public bool Success { get; private set; } + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + public bool Success { get; private init; } [JsonConstructor] protected BooleanResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/BoosterCreatorEntry.cs b/ArchiSteamFarm/Steam/Data/BoosterCreatorEntry.cs index fa68f6b224b3a..2e355cd13cdcd 100644 --- a/ArchiSteamFarm/Steam/Data/BoosterCreatorEntry.cs +++ b/ArchiSteamFarm/Steam/Data/BoosterCreatorEntry.cs @@ -20,17 +20,21 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class BoosterCreatorEntry { - [JsonProperty("appid", Required = Required.Always)] - public uint AppID { get; private set; } + [JsonInclude] + [JsonPropertyName("appid")] + [JsonRequired] + public uint AppID { get; private init; } - [JsonProperty("name", Required = Required.Always)] - public string Name { get; private set; } = ""; + [JsonInclude] + [JsonPropertyName("name")] + [JsonRequired] + public string Name { get; private init; } = ""; [JsonConstructor] private BoosterCreatorEntry() { } diff --git a/ArchiSteamFarm/Steam/Data/Confirmation.cs b/ArchiSteamFarm/Steam/Data/Confirmation.cs index cdcc8c000514a..ff366b2d6de06 100644 --- a/ArchiSteamFarm/Steam/Data/Confirmation.cs +++ b/ArchiSteamFarm/Steam/Data/Confirmation.cs @@ -20,25 +20,36 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Data; [PublicAPI] [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class Confirmation { - [JsonProperty(PropertyName = "nonce", Required = Required.Always)] - internal readonly ulong Nonce; + [JsonInclude] + [JsonPropertyName("type")] + [JsonRequired] + public EConfirmationType ConfirmationType { get; private init; } - [JsonProperty(PropertyName = "type", Required = Required.Always)] - public EConfirmationType ConfirmationType { get; private set; } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("creator_id")] + [JsonRequired] + public ulong CreatorID { get; private init; } - [JsonProperty(PropertyName = "creator_id", Required = Required.Always)] - public ulong CreatorID { get; private set; } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("id")] + [JsonRequired] + public ulong ID { get; private init; } - [JsonProperty(PropertyName = "id", Required = Required.Always)] - public ulong ID { get; private set; } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("nonce")] + [JsonRequired] + internal ulong Nonce { get; private init; } [JsonConstructor] private Confirmation() { } diff --git a/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs b/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs index 5186f163f04e6..af52dd7affe62 100644 --- a/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs +++ b/ArchiSteamFarm/Steam/Data/ConfirmationsResponse.cs @@ -20,13 +20,15 @@ // limitations under the License. using System.Collections.Immutable; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Data; internal sealed class ConfirmationsResponse : BooleanResponse { - [JsonProperty("conf", Required = Required.Always)] - internal readonly ImmutableHashSet Confirmations = ImmutableHashSet.Empty; + [JsonInclude] + [JsonPropertyName("conf")] + [JsonRequired] + internal ImmutableHashSet Confirmations { get; private init; } = ImmutableHashSet.Empty; [JsonConstructor] private ConfirmationsResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/InventoryResponse.cs b/ArchiSteamFarm/Steam/Data/InventoryResponse.cs index 1b528d3e3a78d..b4bfa9fc47fb1 100644 --- a/ArchiSteamFarm/Steam/Data/InventoryResponse.cs +++ b/ArchiSteamFarm/Steam/Data/InventoryResponse.cs @@ -24,80 +24,68 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Integration; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class InventoryResponse : OptionalResultResponse { - [JsonProperty("assets", Required = Required.DisallowNull)] - internal readonly ImmutableList Assets = ImmutableList.Empty; + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName("assets")] + internal ImmutableList Assets { get; private init; } = ImmutableList.Empty; - [JsonProperty("descriptions", Required = Required.DisallowNull)] - internal readonly ImmutableHashSet Descriptions = ImmutableHashSet.Empty; + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName("descriptions")] + internal ImmutableHashSet Descriptions { get; private init; } = ImmutableHashSet.Empty; - [JsonProperty("total_inventory_count", Required = Required.DisallowNull)] - internal readonly uint TotalInventoryCount; + internal EResult? ErrorCode { get; private init; } + internal string? ErrorText { get; private init; } - internal EResult? ErrorCode { get; private set; } - internal string? ErrorText { get; private set; } - internal ulong LastAssetID { get; private set; } - internal bool MoreItems { get; private set; } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("last_assetid")] + internal ulong LastAssetID { get; private init; } - [JsonProperty("error", Required = Required.DisallowNull)] + internal bool MoreItems { get; private init; } + + [JsonInclude] + [JsonPropertyName("total_inventory_count")] + internal uint TotalInventoryCount { get; private init; } + + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName("error")] private string Error { - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); + get => ErrorText ?? ""; - return; - } + init { + ArgumentException.ThrowIfNullOrEmpty(value); ErrorCode = SteamUtilities.InterpretError(value); ErrorText = value; } } - [JsonProperty("last_assetid", Required = Required.DisallowNull)] - private string LastAssetIDText { - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!ulong.TryParse(value, out ulong lastAssetID) || (lastAssetID == 0)) { - ASF.ArchiLogger.LogNullError(lastAssetID); - - return; - } - - LastAssetID = lastAssetID; - } - } - - [JsonProperty("more_items", Required = Required.DisallowNull)] + [JsonInclude] + [JsonPropertyName("more_items")] private byte MoreItemsNumber { - set => MoreItems = value > 0; + get => MoreItems ? (byte) 1 : (byte) 0; + init => MoreItems = value > 0; } [JsonConstructor] private InventoryResponse() { } internal sealed class Description { - [JsonProperty("appid", Required = Required.Always)] - internal readonly uint AppID; - - [JsonProperty("tags", Required = Required.DisallowNull)] - internal readonly ImmutableHashSet Tags = ImmutableHashSet.Empty; - internal Asset.ERarity Rarity { get { foreach (Tag tag in Tags) { @@ -217,62 +205,49 @@ internal Asset.EType Type { } } - [JsonExtensionData(WriteData = false)] - internal Dictionary? AdditionalProperties { - get; - [UsedImplicitly] - set; - } - - internal ulong ClassID { get; private set; } - internal ulong InstanceID { get; private set; } - internal bool Marketable { get; private set; } - internal bool Tradable { get; private set; } + [JsonExtensionData] + [JsonInclude] + internal Dictionary? AdditionalProperties { get; private init; } - [JsonProperty("classid", Required = Required.Always)] - private string ClassIDText { - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!ulong.TryParse(value, out ulong classID) || (classID == 0)) { - ASF.ArchiLogger.LogNullError(classID); - - return; - } + [JsonInclude] + [JsonPropertyName("appid")] + [JsonRequired] + internal uint AppID { get; private init; } - ClassID = classID; - } - } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("classid")] + [JsonRequired] + internal ulong ClassID { get; private init; } - [JsonProperty("instanceid", Required = Required.DisallowNull)] - private string InstanceIDText { - set { - if (string.IsNullOrEmpty(value)) { - return; - } + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("instanceid")] + internal ulong InstanceID { get; private init; } - if (!ulong.TryParse(value, out ulong instanceID)) { - ASF.ArchiLogger.LogNullError(instanceID); + internal bool Marketable { get; private init; } - return; - } + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName("tags")] + internal ImmutableHashSet Tags { get; private init; } = ImmutableHashSet.Empty; - InstanceID = instanceID; - } - } + internal bool Tradable { get; private init; } - [JsonProperty("marketable", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("marketable")] + [JsonRequired] private byte MarketableNumber { - set => Marketable = value > 0; + get => Marketable ? (byte) 1 : (byte) 0; + init => Marketable = value > 0; } - [JsonProperty("tradable", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("tradable")] + [JsonRequired] private byte TradableNumber { - set => Tradable = value > 0; + get => Tradable ? (byte) 1 : (byte) 0; + init => Tradable = value > 0; } // Constructed from trades being received/sent @@ -293,5 +268,8 @@ internal Description(uint appID, ulong classID, ulong instanceID, bool marketabl [JsonConstructor] private Description() { } + + [UsedImplicitly] + public static bool ShouldSerializeAdditionalProperties() => false; } } diff --git a/ArchiSteamFarm/Steam/Data/NewDiscoveryQueueResponse.cs b/ArchiSteamFarm/Steam/Data/NewDiscoveryQueueResponse.cs index d624ba8eb122c..7662563c4a474 100644 --- a/ArchiSteamFarm/Steam/Data/NewDiscoveryQueueResponse.cs +++ b/ArchiSteamFarm/Steam/Data/NewDiscoveryQueueResponse.cs @@ -21,14 +21,16 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class NewDiscoveryQueueResponse { - [JsonProperty("queue", Required = Required.Always)] - internal readonly ImmutableHashSet Queue = ImmutableHashSet.Empty; + [JsonInclude] + [JsonPropertyName("queue")] + [JsonRequired] + internal ImmutableHashSet Queue { get; private init; } = ImmutableHashSet.Empty; [JsonConstructor] private NewDiscoveryQueueResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/OptionalResultResponse.cs b/ArchiSteamFarm/Steam/Data/OptionalResultResponse.cs index dfe1c49812b9e..fdb9cf5ba1408 100644 --- a/ArchiSteamFarm/Steam/Data/OptionalResultResponse.cs +++ b/ArchiSteamFarm/Steam/Data/OptionalResultResponse.cs @@ -19,16 +19,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.Steam.Data; [PublicAPI] public class OptionalResultResponse { - [JsonProperty("success", Required = Required.DisallowNull)] - public EResult? Result { get; private set; } + [JsonInclude] + [JsonPropertyName("success")] + public EResult? Result { get; private init; } [JsonConstructor] protected OptionalResultResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/RedeemWalletResponse.cs b/ArchiSteamFarm/Steam/Data/RedeemWalletResponse.cs index 20b75cce32a5f..014aaf6000945 100644 --- a/ArchiSteamFarm/Steam/Data/RedeemWalletResponse.cs +++ b/ArchiSteamFarm/Steam/Data/RedeemWalletResponse.cs @@ -20,18 +20,20 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using SteamKit2; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class RedeemWalletResponse : ResultResponse { - [JsonProperty("formattednewwalletbalance", Required = Required.DisallowNull)] - internal readonly string? BalanceText; + [JsonInclude] + [JsonPropertyName("formattednewwalletbalance")] + internal string? BalanceText { get; private init; } - [JsonProperty("detail", Required = Required.DisallowNull)] - internal readonly EPurchaseResultDetail PurchaseResultDetail; + [JsonInclude] + [JsonPropertyName("detail")] + internal EPurchaseResultDetail PurchaseResultDetail { get; private init; } [JsonConstructor] private RedeemWalletResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/ResultResponse.cs b/ArchiSteamFarm/Steam/Data/ResultResponse.cs index 89f29b2baa738..f08fe730c10bc 100644 --- a/ArchiSteamFarm/Steam/Data/ResultResponse.cs +++ b/ArchiSteamFarm/Steam/Data/ResultResponse.cs @@ -19,16 +19,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; using SteamKit2; namespace ArchiSteamFarm.Steam.Data; [PublicAPI] public class ResultResponse { - [JsonProperty("success", Required = Required.Always)] - public EResult Result { get; private set; } + [JsonInclude] + [JsonPropertyName("success")] + [JsonRequired] + public EResult Result { get; private init; } [JsonConstructor] protected ResultResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/Tag.cs b/ArchiSteamFarm/Steam/Data/Tag.cs index 486f970b9883c..aca6f524b67e2 100644 --- a/ArchiSteamFarm/Steam/Data/Tag.cs +++ b/ArchiSteamFarm/Steam/Data/Tag.cs @@ -20,19 +20,23 @@ // limitations under the License. using System; +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Data; public sealed class Tag { - [JsonProperty("category", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("category")] + [JsonRequired] [PublicAPI] - public string Identifier { get; private set; } = ""; + public string Identifier { get; private init; } = ""; - [JsonProperty("internal_name", Required = Required.Always)] + [JsonInclude] + [JsonPropertyName("internal_name")] + [JsonRequired] [PublicAPI] - public string Value { get; private set; } = ""; + public string Value { get; private init; } = ""; internal Tag(string identifier, string value) { ArgumentException.ThrowIfNullOrEmpty(identifier); diff --git a/ArchiSteamFarm/Steam/Data/TradeOfferAcceptResponse.cs b/ArchiSteamFarm/Steam/Data/TradeOfferAcceptResponse.cs index 7be903f744009..6626cdb04ec9d 100644 --- a/ArchiSteamFarm/Steam/Data/TradeOfferAcceptResponse.cs +++ b/ArchiSteamFarm/Steam/Data/TradeOfferAcceptResponse.cs @@ -20,17 +20,19 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class TradeOfferAcceptResponse { - [JsonProperty("strError", Required = Required.DisallowNull)] - internal readonly string ErrorText = ""; + [JsonInclude] + [JsonPropertyName("strError")] + internal string? ErrorText { get; private init; } - [JsonProperty("needs_mobile_confirmation", Required = Required.DisallowNull)] - internal readonly bool RequiresMobileConfirmation; + [JsonInclude] + [JsonPropertyName("needs_mobile_confirmation")] + internal bool RequiresMobileConfirmation { get; private init; } [JsonConstructor] private TradeOfferAcceptResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/TradeOfferSendRequest.cs b/ArchiSteamFarm/Steam/Data/TradeOfferSendRequest.cs index 1de47cd3e733b..c7dcde0653d6c 100644 --- a/ArchiSteamFarm/Steam/Data/TradeOfferSendRequest.cs +++ b/ArchiSteamFarm/Steam/Data/TradeOfferSendRequest.cs @@ -20,19 +20,25 @@ // limitations under the License. using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Data; internal sealed class TradeOfferSendRequest { - [JsonProperty("me", Required = Required.Always)] - internal readonly ItemList ItemsToGive = new(); + [JsonInclude] + [JsonPropertyName("me")] + [JsonRequired] + internal ItemList ItemsToGive { get; private init; } = new(); - [JsonProperty("them", Required = Required.Always)] - internal readonly ItemList ItemsToReceive = new(); + [JsonInclude] + [JsonPropertyName("them")] + [JsonRequired] + internal ItemList ItemsToReceive { get; private init; } = new(); internal sealed class ItemList { - [JsonProperty("assets", Required = Required.Always)] - internal readonly HashSet Assets = []; + [JsonInclude] + [JsonPropertyName("assets")] + [JsonRequired] + internal HashSet Assets { get; private init; } = []; } } diff --git a/ArchiSteamFarm/Steam/Data/TradeOfferSendResponse.cs b/ArchiSteamFarm/Steam/Data/TradeOfferSendResponse.cs index af349ff548167..2a157c8efc12c 100644 --- a/ArchiSteamFarm/Steam/Data/TradeOfferSendResponse.cs +++ b/ArchiSteamFarm/Steam/Data/TradeOfferSendResponse.cs @@ -20,39 +20,24 @@ // limitations under the License. using System.Diagnostics.CodeAnalysis; -using ArchiSteamFarm.Core; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class TradeOfferSendResponse { - [JsonProperty("strError", Required = Required.DisallowNull)] - internal readonly string ErrorText = ""; - - [JsonProperty("needs_mobile_confirmation", Required = Required.DisallowNull)] - internal readonly bool RequiresMobileConfirmation; - - internal ulong TradeOfferID { get; private set; } - - [JsonProperty("tradeofferid", Required = Required.DisallowNull)] - private string TradeOfferIDText { - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(value); - - return; - } - - if (!ulong.TryParse(value, out ulong tradeOfferID) || (tradeOfferID == 0)) { - ASF.ArchiLogger.LogNullError(tradeOfferID); - - return; - } - - TradeOfferID = tradeOfferID; - } - } + [JsonInclude] + [JsonPropertyName("strError")] + internal string? ErrorText { get; private init; } + + [JsonInclude] + [JsonPropertyName("needs_mobile_confirmation")] + internal bool RequiresMobileConfirmation { get; private init; } + + [JsonInclude] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + [JsonPropertyName("tradeofferid")] + internal ulong TradeOfferID { get; private init; } [JsonConstructor] private TradeOfferSendResponse() { } diff --git a/ArchiSteamFarm/Steam/Data/UserPrivacy.cs b/ArchiSteamFarm/Steam/Data/UserPrivacy.cs index 9451c821ba6f6..7af2b8d238b06 100644 --- a/ArchiSteamFarm/Steam/Data/UserPrivacy.cs +++ b/ArchiSteamFarm/Steam/Data/UserPrivacy.cs @@ -22,18 +22,22 @@ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using ArchiSteamFarm.Steam.Integration; -using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class UserPrivacy { - [JsonProperty("eCommentPermission", Required = Required.Always)] - internal readonly ECommentPermission CommentPermission; + [JsonInclude] + [JsonPropertyName("eCommentPermission")] + [JsonRequired] + internal ECommentPermission CommentPermission { get; private init; } - [JsonProperty("PrivacySettings", Required = Required.Always)] - internal readonly PrivacySettings Settings = new(); + [JsonInclude] + [JsonPropertyName("PrivacySettings")] + [JsonRequired] + internal PrivacySettings Settings { get; private init; } = new(); // Constructed from privacy change request internal UserPrivacy(PrivacySettings settings, ECommentPermission commentPermission) { @@ -51,23 +55,35 @@ internal UserPrivacy(PrivacySettings settings, ECommentPermission commentPermiss private UserPrivacy() { } internal sealed class PrivacySettings { - [JsonProperty("PrivacyFriendsList", Required = Required.Always)] - internal readonly ArchiHandler.EPrivacySetting FriendsList; - - [JsonProperty("PrivacyInventory", Required = Required.Always)] - internal readonly ArchiHandler.EPrivacySetting Inventory; - - [JsonProperty("PrivacyInventoryGifts", Required = Required.Always)] - internal readonly ArchiHandler.EPrivacySetting InventoryGifts; - - [JsonProperty("PrivacyOwnedGames", Required = Required.Always)] - internal readonly ArchiHandler.EPrivacySetting OwnedGames; - - [JsonProperty("PrivacyPlaytime", Required = Required.Always)] - internal readonly ArchiHandler.EPrivacySetting Playtime; - - [JsonProperty("PrivacyProfile", Required = Required.Always)] - internal readonly ArchiHandler.EPrivacySetting Profile; + [JsonInclude] + [JsonPropertyName("PrivacyFriendsList")] + [JsonRequired] + internal ArchiHandler.EPrivacySetting FriendsList { get; private init; } + + [JsonInclude] + [JsonPropertyName("PrivacyInventory")] + [JsonRequired] + internal ArchiHandler.EPrivacySetting Inventory { get; private init; } + + [JsonInclude] + [JsonPropertyName("PrivacyInventoryGifts")] + [JsonRequired] + internal ArchiHandler.EPrivacySetting InventoryGifts { get; private init; } + + [JsonInclude] + [JsonPropertyName("PrivacyOwnedGames")] + [JsonRequired] + internal ArchiHandler.EPrivacySetting OwnedGames { get; private init; } + + [JsonInclude] + [JsonPropertyName("PrivacyPlaytime")] + [JsonRequired] + internal ArchiHandler.EPrivacySetting Playtime { get; private init; } + + [JsonInclude] + [JsonPropertyName("PrivacyProfile")] + [JsonRequired] + internal ArchiHandler.EPrivacySetting Profile { get; private init; } // Constructed from privacy change request internal PrivacySettings(ArchiHandler.EPrivacySetting profile, ArchiHandler.EPrivacySetting ownedGames, ArchiHandler.EPrivacySetting playtime, ArchiHandler.EPrivacySetting friendsList, ArchiHandler.EPrivacySetting inventory, ArchiHandler.EPrivacySetting inventoryGifts) { diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index bfb124749244a..54e966fa743b2 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -30,11 +30,13 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using AngleSharp.Dom; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; @@ -42,8 +44,6 @@ using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.Steam.Integration; @@ -167,7 +167,7 @@ public async Task CancelTradeOffer(ulong tradeID) { string json = scriptNode.TextContent[startIndex..(endIndex + 1)]; try { - result = JsonConvert.DeserializeObject>(json); + result = json.ToJsonObject>(); } catch (Exception e) { Bot.ArchiLogger.LogGenericException(e); @@ -713,7 +713,7 @@ public async Task JoinGroup(ulong groupID) { Dictionary data = new(6, StringComparer.Ordinal) { { "partner", steamID.ToString(CultureInfo.InvariantCulture) }, { "serverid", "1" }, - { "trade_offer_create_params", !string.IsNullOrEmpty(token) ? new JObject { { "trade_offer_access_token", token } }.ToString(Formatting.None) : "" }, + { "trade_offer_create_params", !string.IsNullOrEmpty(token) ? new JsonObject { { "trade_offer_access_token", token } }.ToJsonText() : "" }, { "tradeoffermessage", $"Sent by {SharedInfo.PublicIdentifier}/{SharedInfo.Version}" } }; @@ -721,7 +721,7 @@ public async Task JoinGroup(ulong groupID) { HashSet mobileTradeOfferIDs = new(trades.Count); foreach (TradeOfferSendRequest trade in trades) { - data["json_tradeoffer"] = JsonConvert.SerializeObject(trade); + data["json_tradeoffer"] = trade.ToJsonText(); ObjectResponse? response = null; @@ -1578,7 +1578,7 @@ internal async Task AcceptDigitalGiftCard(ulong giftCardID) { { "ajax", "true" } }; - ObjectResponse? response = await UrlPostToJsonObjectWithSession(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.ReturnServerErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); + ObjectResponse? response = await UrlPostToJsonObjectWithSession(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.ReturnServerErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); if (response == null) { return (EResult.Fail, EPurchaseResultDetail.Timeout); @@ -1594,31 +1594,37 @@ internal async Task AcceptDigitalGiftCard(ulong giftCardID) { // There is not much we can do apart from trying to extract the result and returning it along with the OK and non-OK response, it's also why it doesn't make any sense to strong-type it EResult result = response.StatusCode.IsSuccessCode() ? EResult.OK : EResult.Fail; - if (response.Content is not JObject jObject) { + if (response.Content is not JsonObject jsonObject) { // Who knows what piece of crap that is? return (result, EPurchaseResultDetail.NoDetail); } - byte? numberResult = jObject["purchaseresultdetail"]?.Value(); + try { + byte? numberResult = jsonObject["purchaseresultdetail"]?.GetValue(); - if (numberResult.HasValue) { - return (result, (EPurchaseResultDetail) numberResult.Value); - } + if (numberResult.HasValue) { + return (result, (EPurchaseResultDetail) numberResult.Value); + } - // Attempt to do limited parsing from error message, if it exists that is - string? errorMessage = jObject["error"]?.Value(); + // Attempt to do limited parsing from error message, if it exists that is + string? errorMessage = jsonObject["error"]?.GetValue(); - switch (errorMessage) { - case null: - case "": - // Thanks Steam, very useful - return (result, EPurchaseResultDetail.NoDetail); - case "You got rate limited, try again in an hour.": - return (result, EPurchaseResultDetail.RateLimited); - default: - Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(errorMessage), errorMessage)); + switch (errorMessage) { + case null: + case "": + // Thanks Steam, very useful + return (result, EPurchaseResultDetail.NoDetail); + case "You got rate limited, try again in an hour.": + return (result, EPurchaseResultDetail.RateLimited); + default: + Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(errorMessage), errorMessage)); + + return (result, EPurchaseResultDetail.ContactSupport); + } + } catch (Exception e) { + Bot.ArchiLogger.LogGenericException(e); - return (result, EPurchaseResultDetail.ContactSupport); + return (result, EPurchaseResultDetail.ContactSupport); } case HttpStatusCode.Unauthorized: // Let's convert this into something reasonable @@ -1647,7 +1653,7 @@ internal async Task ChangePrivacySettings(UserPrivacy userPrivacy) { // Extra entry for sessionID Dictionary data = new(3, StringComparer.Ordinal) { { "eCommentPermission", ((byte) userPrivacy.CommentPermission).ToString(CultureInfo.InvariantCulture) }, - { "Privacy", JsonConvert.SerializeObject(userPrivacy.Settings) } + { "Privacy", userPrivacy.Settings.ToJsonText() } }; ObjectResponse? response = await UrlPostToJsonObjectWithSession(request, data: data).ConfigureAwait(false); diff --git a/ArchiSteamFarm/Steam/Integration/CMsgs/CMsgClientAcknowledgeClanInvite.cs b/ArchiSteamFarm/Steam/Integration/CMsgs/CMsgClientAcknowledgeClanInvite.cs index 5eded80dd6d8e..daf21968d6a59 100644 --- a/ArchiSteamFarm/Steam/Integration/CMsgs/CMsgClientAcknowledgeClanInvite.cs +++ b/ArchiSteamFarm/Steam/Integration/CMsgs/CMsgClientAcknowledgeClanInvite.cs @@ -28,8 +28,8 @@ namespace ArchiSteamFarm.Steam.Integration.CMsgs; internal sealed class CMsgClientAcknowledgeClanInvite : ISteamSerializableMessage { - internal bool AcceptInvite { private get; set; } - internal ulong ClanID { private get; set; } + internal bool AcceptInvite { get; set; } + internal ulong ClanID { get; set; } void ISteamSerializable.Deserialize(Stream stream) { ArgumentNullException.ThrowIfNull(stream); diff --git a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs index 2caae78bf619b..e1f7291cd3254 100644 --- a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs +++ b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs @@ -27,6 +27,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; @@ -34,7 +35,6 @@ using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Storage; -using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Security; @@ -56,13 +56,17 @@ public sealed class MobileAuthenticator : IDisposable { private readonly ArchiCacheable CachedDeviceID; - [JsonProperty("identity_secret", Required = Required.Always)] - private readonly string IdentitySecret = ""; + private Bot? Bot; - [JsonProperty("shared_secret", Required = Required.Always)] - private readonly string SharedSecret = ""; + [JsonInclude] + [JsonPropertyName("identity_secret")] + [JsonRequired] + private string IdentitySecret { get; init; } = ""; - private Bot? Bot; + [JsonInclude] + [JsonPropertyName("shared_secret")] + [JsonRequired] + private string SharedSecret { get; init; } = ""; [JsonConstructor] private MobileAuthenticator() => CachedDeviceID = new ArchiCacheable(ResolveDeviceID); diff --git a/ArchiSteamFarm/Steam/SteamKit2/InMemoryServerListProvider.cs b/ArchiSteamFarm/Steam/SteamKit2/InMemoryServerListProvider.cs index 4f5a6703d8369..dfcd124acb755 100644 --- a/ArchiSteamFarm/Steam/SteamKit2/InMemoryServerListProvider.cs +++ b/ArchiSteamFarm/Steam/SteamKit2/InMemoryServerListProvider.cs @@ -22,16 +22,19 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Collections; -using Newtonsoft.Json; +using ArchiSteamFarm.Helpers.Json; +using JetBrains.Annotations; using SteamKit2.Discovery; namespace ArchiSteamFarm.Steam.SteamKit2; internal sealed class InMemoryServerListProvider : IServerListProvider { - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentHashSet ServerRecords = []; + [JsonDisallowNull] + [JsonInclude] + private ConcurrentHashSet ServerRecords { get; init; } = []; public Task> FetchServerListAsync() => Task.FromResult(ServerRecords.Where(static server => !string.IsNullOrEmpty(server.Host) && server is { Port: > 0, ProtocolTypes: > 0 }).Select(static server => ServerRecord.CreateServer(server.Host, server.Port, server.ProtocolTypes))); @@ -47,6 +50,7 @@ public Task UpdateServerListAsync(IEnumerable endpoints) { return Task.CompletedTask; } + [UsedImplicitly] public bool ShouldSerializeServerRecords() => ServerRecords.Count > 0; internal event EventHandler? ServerListUpdated; diff --git a/ArchiSteamFarm/Steam/SteamKit2/ServerRecordEndPoint.cs b/ArchiSteamFarm/Steam/SteamKit2/ServerRecordEndPoint.cs index 7313c5bbd7b0c..3a52909319372 100644 --- a/ArchiSteamFarm/Steam/SteamKit2/ServerRecordEndPoint.cs +++ b/ArchiSteamFarm/Steam/SteamKit2/ServerRecordEndPoint.cs @@ -21,20 +21,23 @@ using System; using System.ComponentModel; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using SteamKit2; namespace ArchiSteamFarm.Steam.SteamKit2; internal sealed class ServerRecordEndPoint : IEquatable { - [JsonProperty(Required = Required.Always)] - internal readonly string Host = ""; + [JsonInclude] + [JsonRequired] + internal string Host { get; private init; } = ""; - [JsonProperty(Required = Required.Always)] - internal readonly ushort Port; + [JsonInclude] + [JsonRequired] + internal ushort Port { get; private init; } - [JsonProperty(Required = Required.Always)] - internal readonly ProtocolTypes ProtocolTypes; + [JsonInclude] + [JsonRequired] + internal ProtocolTypes ProtocolTypes { get; private init; } internal ServerRecordEndPoint(string host, ushort port, ProtocolTypes protocolTypes) { ArgumentException.ThrowIfNullOrEmpty(host); diff --git a/ArchiSteamFarm/Steam/Storage/BotConfig.cs b/ArchiSteamFarm/Steam/Storage/BotConfig.cs index c14772df1c06e..4c1bc71df59bc 100644 --- a/ArchiSteamFarm/Steam/Storage/BotConfig.cs +++ b/ArchiSteamFarm/Steam/Storage/BotConfig.cs @@ -28,16 +28,17 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC.Integration; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Integration; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.Steam.Storage; @@ -134,67 +135,72 @@ public sealed class BotConfig { [PublicAPI] public static readonly ImmutableHashSet DefaultTransferableTypes = ImmutableHashSet.Create(Asset.EType.BoosterPack, Asset.EType.FoilTradingCard, Asset.EType.TradingCard); - [JsonProperty(Required = Required.DisallowNull)] - public bool AcceptGifts { get; private set; } = DefaultAcceptGifts; + [JsonInclude] + public bool AcceptGifts { get; private init; } = DefaultAcceptGifts; - [JsonProperty(Required = Required.DisallowNull)] - public EBotBehaviour BotBehaviour { get; private set; } = DefaultBotBehaviour; + [JsonInclude] + public EBotBehaviour BotBehaviour { get; private init; } = DefaultBotBehaviour; - [JsonProperty(Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] [SwaggerValidValues(ValidIntValues = [(int) Asset.EType.FoilTradingCard, (int) Asset.EType.TradingCard])] - public ImmutableHashSet CompleteTypesToSend { get; private set; } = DefaultCompleteTypesToSend; + public ImmutableHashSet CompleteTypesToSend { get; private init; } = DefaultCompleteTypesToSend; - [JsonProperty] - public string? CustomGamePlayedWhileFarming { get; private set; } = DefaultCustomGamePlayedWhileFarming; + [JsonInclude] + public string? CustomGamePlayedWhileFarming { get; private init; } = DefaultCustomGamePlayedWhileFarming; - [JsonProperty] - public string? CustomGamePlayedWhileIdle { get; private set; } = DefaultCustomGamePlayedWhileIdle; + [JsonInclude] + public string? CustomGamePlayedWhileIdle { get; private init; } = DefaultCustomGamePlayedWhileIdle; - [JsonProperty(Required = Required.DisallowNull)] - public bool Enabled { get; private set; } = DefaultEnabled; + [JsonInclude] + public bool Enabled { get; private init; } = DefaultEnabled; - [JsonProperty(Required = Required.DisallowNull)] - public ImmutableList FarmingOrders { get; private set; } = DefaultFarmingOrders; + [JsonDisallowNull] + [JsonInclude] + public ImmutableList FarmingOrders { get; private init; } = DefaultFarmingOrders; - [JsonProperty(Required = Required.DisallowNull)] - public EFarmingPreferences FarmingPreferences { get; private set; } = DefaultFarmingPreferences; + [JsonInclude] + public EFarmingPreferences FarmingPreferences { get; private init; } = DefaultFarmingPreferences; - [JsonProperty(Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] [MaxLength(ArchiHandler.MaxGamesPlayedConcurrently)] [SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "This is optional, supportive attribute, we don't care if it gets trimmed or not")] - public ImmutableList GamesPlayedWhileIdle { get; private set; } = DefaultGamesPlayedWhileIdle; + public ImmutableList GamesPlayedWhileIdle { get; private init; } = DefaultGamesPlayedWhileIdle; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte HoursUntilCardDrops { get; private set; } = DefaultHoursUntilCardDrops; + public byte HoursUntilCardDrops { get; private init; } = DefaultHoursUntilCardDrops; - [JsonProperty(Required = Required.DisallowNull)] - public ImmutableHashSet LootableTypes { get; private set; } = DefaultLootableTypes; + [JsonDisallowNull] + [JsonInclude] + public ImmutableHashSet LootableTypes { get; private init; } = DefaultLootableTypes; - [JsonProperty(Required = Required.DisallowNull)] - public ImmutableHashSet MatchableTypes { get; private set; } = DefaultMatchableTypes; + [JsonDisallowNull] + [JsonInclude] + public ImmutableHashSet MatchableTypes { get; private init; } = DefaultMatchableTypes; - [JsonProperty(Required = Required.DisallowNull)] - public EPersonaStateFlag OnlineFlags { get; private set; } = DefaultOnlineFlags; + [JsonInclude] + public EPersonaStateFlag OnlineFlags { get; private init; } = DefaultOnlineFlags; - [JsonProperty(Required = Required.DisallowNull)] - public EPersonaState OnlineStatus { get; private set; } = DefaultOnlineStatus; + [JsonInclude] + public EPersonaState OnlineStatus { get; private init; } = DefaultOnlineStatus; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] public ArchiCryptoHelper.ECryptoMethod PasswordFormat { get; internal set; } = DefaultPasswordFormat; - [JsonProperty(Required = Required.DisallowNull)] - public ERedeemingPreferences RedeemingPreferences { get; private set; } = DefaultRedeemingPreferences; + [JsonInclude] + public ERedeemingPreferences RedeemingPreferences { get; private init; } = DefaultRedeemingPreferences; - [JsonProperty(Required = Required.DisallowNull)] - public ERemoteCommunication RemoteCommunication { get; private set; } = DefaultRemoteCommunication; + [JsonInclude] + public ERemoteCommunication RemoteCommunication { get; private init; } = DefaultRemoteCommunication; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte SendTradePeriod { get; private set; } = DefaultSendTradePeriod; + public byte SendTradePeriod { get; private init; } = DefaultSendTradePeriod; - [JsonProperty] + [JsonInclude] public string? SteamLogin { get => BackingSteamLogin; @@ -204,12 +210,12 @@ internal set { } } - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [SwaggerSteamIdentifier(AccountType = EAccountType.Clan)] [SwaggerValidValues(ValidIntValues = [0])] - public ulong SteamMasterClanID { get; private set; } = DefaultSteamMasterClanID; + public ulong SteamMasterClanID { get; private init; } = DefaultSteamMasterClanID; - [JsonProperty] + [JsonInclude] [MaxLength(SteamParentalCodeLength)] [MinLength(SteamParentalCodeLength)] [SwaggerValidValues(ValidStringValues = ["0"])] @@ -223,7 +229,7 @@ internal set { } } - [JsonProperty] + [JsonInclude] public string? SteamPassword { get => BackingSteamPassword; @@ -233,37 +239,36 @@ internal set { } } - [JsonProperty] + [JsonInclude] [MaxLength(SteamTradeTokenLength)] [MinLength(SteamTradeTokenLength)] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "This is optional, supportive attribute, we don't care if it gets trimmed or not")] - public string? SteamTradeToken { get; private set; } = DefaultSteamTradeToken; + public string? SteamTradeToken { get; private init; } = DefaultSteamTradeToken; - [JsonProperty(Required = Required.DisallowNull)] - public ImmutableDictionary SteamUserPermissions { get; private set; } = DefaultSteamUserPermissions; + [JsonDisallowNull] + [JsonInclude] + public ImmutableDictionary SteamUserPermissions { get; private init; } = DefaultSteamUserPermissions; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte TradeCheckPeriod { get; private set; } = DefaultTradeCheckPeriod; + public byte TradeCheckPeriod { get; private init; } = DefaultTradeCheckPeriod; - [JsonProperty(Required = Required.DisallowNull)] - public ETradingPreferences TradingPreferences { get; private set; } = DefaultTradingPreferences; + [JsonInclude] + public ETradingPreferences TradingPreferences { get; private init; } = DefaultTradingPreferences; - [JsonProperty(Required = Required.DisallowNull)] - public ImmutableHashSet TransferableTypes { get; private set; } = DefaultTransferableTypes; + [JsonDisallowNull] + [JsonInclude] + public ImmutableHashSet TransferableTypes { get; private init; } = DefaultTransferableTypes; - [JsonProperty(Required = Required.DisallowNull)] - public bool UseLoginKeys { get; private set; } = DefaultUseLoginKeys; + [JsonInclude] + public bool UseLoginKeys { get; private init; } = DefaultUseLoginKeys; - [JsonProperty(Required = Required.DisallowNull)] - public ArchiHandler.EUserInterfaceMode UserInterfaceMode { get; private set; } = DefaultUserInterfaceMode; + [JsonInclude] + public ArchiHandler.EUserInterfaceMode UserInterfaceMode { get; private init; } = DefaultUserInterfaceMode; [JsonExtensionData] - internal Dictionary? AdditionalProperties { - get; - [UsedImplicitly] - set; - } + [JsonInclude] + internal Dictionary? AdditionalProperties { get; set; } internal bool IsSteamLoginSet { get; set; } internal bool IsSteamParentalCodeSet { get; set; } @@ -274,18 +279,17 @@ internal Dictionary? AdditionalProperties { private string? BackingSteamParentalCode = DefaultSteamParentalCode; private string? BackingSteamPassword = DefaultSteamPassword; - [JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamMasterClanID)}", Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamMasterClanID)}")] private string SSteamMasterClanID { get => SteamMasterClanID.ToString(CultureInfo.InvariantCulture); - set { - if (string.IsNullOrEmpty(value) || !ulong.TryParse(value, out ulong result)) { - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(SSteamMasterClanID))); - - return; - } + init { + ArgumentException.ThrowIfNullOrEmpty(value); - SteamMasterClanID = result; + // We intend to throw exception back to caller here + SteamMasterClanID = ulong.Parse(value, CultureInfo.InvariantCulture); } } @@ -387,7 +391,7 @@ public static async Task Write(string filePath, BotConfig botConfig) { ArgumentException.ThrowIfNullOrEmpty(filePath); ArgumentNullException.ThrowIfNull(botConfig); - string json = JsonConvert.SerializeObject(botConfig, Formatting.Indented); + string json = botConfig.ToJsonText(true); return await SerializableFile.Write(filePath, json).ConfigureAwait(false); } @@ -533,7 +537,7 @@ public static async Task Write(string filePath, BotConfig botConfig) { return (null, null); } - botConfig = JsonConvert.DeserializeObject(json); + botConfig = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); @@ -596,7 +600,7 @@ public static async Task Write(string filePath, BotConfig botConfig) { } botConfig.Saving = true; - string latestJson = JsonConvert.SerializeObject(botConfig, Formatting.Indented); + string latestJson = botConfig.ToJsonText(true); botConfig.Saving = false; return (botConfig, json != latestJson ? latestJson : null); diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs index e8d891bd16606..043c791b6969b 100644 --- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs +++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs @@ -25,36 +25,20 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Storage; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Steam.Storage; public sealed class BotDatabase : GenericDatabase { - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentHashSet FarmingBlacklistAppIDs = []; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentHashSet FarmingPriorityQueueAppIDs = []; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ObservableConcurrentDictionary FarmingRiskyIgnoredAppIDs = new(); - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentHashSet FarmingRiskyPrioritizedAppIDs = []; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentHashSet MatchActivelyBlacklistAppIDs = []; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentHashSet TradingBlacklistSteamIDs = []; - internal uint GamesToRedeemInBackgroundCount { get { lock (GamesToRedeemInBackground) { @@ -65,9 +49,6 @@ internal uint GamesToRedeemInBackgroundCount { internal bool HasGamesToRedeemInBackground => GamesToRedeemInBackgroundCount > 0; - [JsonProperty(Required = Required.DisallowNull)] - private readonly OrderedDictionary GamesToRedeemInBackground = new(); - internal string? AccessToken { get => BackingAccessToken; @@ -81,6 +62,26 @@ internal string? AccessToken { } } + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentHashSet FarmingBlacklistAppIDs { get; private init; } = []; + + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentHashSet FarmingPriorityQueueAppIDs { get; private init; } = []; + + [JsonDisallowNull] + [JsonInclude] + internal ObservableConcurrentDictionary FarmingRiskyIgnoredAppIDs { get; private init; } = new(); + + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentHashSet FarmingRiskyPrioritizedAppIDs { get; private init; } = []; + + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentHashSet MatchActivelyBlacklistAppIDs { get; private init; } = []; + internal MobileAuthenticator? MobileAuthenticator { get => BackingMobileAuthenticator; @@ -120,17 +121,26 @@ internal string? SteamGuardData { } } - [JsonProperty] - private string? BackingAccessToken; + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentHashSet TradingBlacklistSteamIDs { get; private init; } = []; - [JsonProperty($"_{nameof(MobileAuthenticator)}")] - private MobileAuthenticator? BackingMobileAuthenticator; + [JsonInclude] + private string? BackingAccessToken { get; set; } - [JsonProperty] - private string? BackingRefreshToken; + [JsonInclude] + [JsonPropertyName($"_{nameof(MobileAuthenticator)}")] + private MobileAuthenticator? BackingMobileAuthenticator { get; set; } - [JsonProperty] - private string? BackingSteamGuardData; + [JsonInclude] + private string? BackingRefreshToken { get; set; } + + [JsonInclude] + private string? BackingSteamGuardData { get; set; } + + [JsonDisallowNull] + [JsonInclude] + private OrderedDictionary GamesToRedeemInBackground { get; init; } = new(); private BotDatabase(string filePath) : this() { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -148,6 +158,32 @@ private BotDatabase() { TradingBlacklistSteamIDs.OnModified += OnObjectModified; } + [PublicAPI] + public void DeleteFromJsonStorage(string key) { + ArgumentException.ThrowIfNullOrEmpty(key); + + DeleteFromJsonStorage(this, key); + } + + [PublicAPI] + public void SaveToJsonStorage(string key, T value) where T : notnull { + ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(value); + + SaveToJsonStorage(this, key, value); + } + + [PublicAPI] + public void SaveToJsonStorage(string key, JsonElement value) { + ArgumentException.ThrowIfNullOrEmpty(key); + + if (value.ValueKind == JsonValueKind.Undefined) { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + SaveToJsonStorage(this, key, value); + } + [UsedImplicitly] public bool ShouldSerializeBackingAccessToken() => !string.IsNullOrEmpty(BackingAccessToken); @@ -199,6 +235,8 @@ protected override void Dispose(bool disposing) { base.Dispose(disposing); } + protected override Task Save() => Save(this); + internal void AddGamesToRedeemInBackground(IOrderedDictionary games) { if ((games == null) || (games.Count == 0)) { throw new ArgumentNullException(nameof(games)); @@ -236,7 +274,7 @@ internal void AddGamesToRedeemInBackground(IOrderedDictionary games) { return null; } - botDatabase = JsonConvert.DeserializeObject(json); + botDatabase = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/ArchiSteamFarm/Storage/CrashFile.cs b/ArchiSteamFarm/Storage/CrashFile.cs index b13b23d509e44..ae28b756f97d5 100644 --- a/ArchiSteamFarm/Storage/CrashFile.cs +++ b/ArchiSteamFarm/Storage/CrashFile.cs @@ -22,12 +22,13 @@ using System; using System.Globalization; 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 JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Storage; @@ -58,11 +59,11 @@ internal byte StartupCount { } } - [JsonProperty(Required = Required.DisallowNull)] - private DateTime BackingLastStartup; + [JsonInclude] + private DateTime BackingLastStartup { get; set; } - [JsonProperty(Required = Required.DisallowNull)] - private byte BackingStartupCount; + [JsonInclude] + private byte BackingStartupCount { get; set; } private CrashFile(string filePath) : this() { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -79,6 +80,8 @@ private CrashFile() { } [UsedImplicitly] public bool ShouldSerializeBackingStartupCount() => BackingStartupCount > 0; + protected override Task Save() => Save(this); + internal static async Task CreateOrLoad(string filePath) { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -97,7 +100,7 @@ internal static async Task CreateOrLoad(string filePath) { return new CrashFile(filePath); } - crashFile = JsonConvert.DeserializeObject(json); + crashFile = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/ArchiSteamFarm/Storage/GenericDatabase.cs b/ArchiSteamFarm/Storage/GenericDatabase.cs index e89a6ab56fa18..d88ccdaa497c3 100644 --- a/ArchiSteamFarm/Storage/GenericDatabase.cs +++ b/ArchiSteamFarm/Storage/GenericDatabase.cs @@ -22,55 +22,64 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; 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(); + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary KeyValueJsonStorage { get; init; } = new(); [PublicAPI] - public void DeleteFromJsonStorage(string key) { + public JsonElement LoadFromJsonStorage(string key) { ArgumentException.ThrowIfNullOrEmpty(key); - if (!KeyValueJsonStorage.TryRemove(key, out _)) { + return KeyValueJsonStorage.GetValueOrDefault(key); + } + + [UsedImplicitly] + public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty; + + protected static void DeleteFromJsonStorage(T genericDatabase, string key) where T : GenericDatabase { + ArgumentNullException.ThrowIfNull(genericDatabase); + ArgumentException.ThrowIfNullOrEmpty(key); + + if (!genericDatabase.KeyValueJsonStorage.TryRemove(key, out _)) { return; } - Utilities.InBackground(Save); + Utilities.InBackground(() => Save(genericDatabase)); } - [PublicAPI] - public JToken? LoadFromJsonStorage(string key) { + protected static void SaveToJsonStorage(TDatabase genericDatabase, string key, TValue value) where TDatabase : GenericDatabase where TValue : notnull { + ArgumentNullException.ThrowIfNull(genericDatabase); ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(value); - return KeyValueJsonStorage.GetValueOrDefault(key); + JsonElement jsonElement = value.ToJsonElement(); + + SaveToJsonStorage(genericDatabase, key, jsonElement); } - [PublicAPI] - public void SaveToJsonStorage(string key, JToken value) { + protected static void SaveToJsonStorage(T genericDatabase, string key, JsonElement value) where T : GenericDatabase { + ArgumentNullException.ThrowIfNull(genericDatabase); ArgumentException.ThrowIfNullOrEmpty(key); - ArgumentNullException.ThrowIfNull(value); - - if (value.Type == JTokenType.Null) { - DeleteFromJsonStorage(key); - return; + if (value.ValueKind == JsonValueKind.Undefined) { + throw new ArgumentOutOfRangeException(nameof(value)); } - if (KeyValueJsonStorage.TryGetValue(key, out JToken? currentValue) && JToken.DeepEquals(currentValue, value)) { + if (genericDatabase.KeyValueJsonStorage.TryGetValue(key, out JsonElement currentValue) && currentValue.Equals(value)) { return; } - KeyValueJsonStorage[key] = value; - Utilities.InBackground(Save); + genericDatabase.KeyValueJsonStorage[key] = value; + Utilities.InBackground(() => Save(genericDatabase)); } - - [UsedImplicitly] - public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty; } diff --git a/ArchiSteamFarm/Storage/GlobalConfig.cs b/ArchiSteamFarm/Storage/GlobalConfig.cs index b793d647554d0..e898a8faeb37c 100644 --- a/ArchiSteamFarm/Storage/GlobalConfig.cs +++ b/ArchiSteamFarm/Storage/GlobalConfig.cs @@ -28,15 +28,16 @@ using System.Globalization; using System.IO; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.IPC.Integration; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Integration; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SteamKit2; namespace ArchiSteamFarm.Storage; @@ -188,59 +189,60 @@ public WebProxy? WebProxy { } } - [JsonProperty(Required = Required.DisallowNull)] - public bool AutoRestart { get; private set; } = DefaultAutoRestart; + [JsonInclude] + public bool AutoRestart { get; private init; } = DefaultAutoRestart; - [JsonProperty(Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] [SwaggerItemsMinMax(MinimumUint = 1, MaximumUint = uint.MaxValue)] - public ImmutableHashSet Blacklist { get; private set; } = DefaultBlacklist; + public ImmutableHashSet Blacklist { get; private init; } = DefaultBlacklist; - [JsonProperty] - public string? CommandPrefix { get; private set; } = DefaultCommandPrefix; + [JsonInclude] + public string? CommandPrefix { get; private init; } = DefaultCommandPrefix; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte ConfirmationsLimiterDelay { get; private set; } = DefaultConfirmationsLimiterDelay; + public byte ConfirmationsLimiterDelay { get; private init; } = DefaultConfirmationsLimiterDelay; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(1, byte.MaxValue)] - public byte ConnectionTimeout { get; private set; } = DefaultConnectionTimeout; + public byte ConnectionTimeout { get; private init; } = DefaultConnectionTimeout; - [JsonProperty] - public string? CurrentCulture { get; private set; } = DefaultCurrentCulture; + [JsonInclude] + public string? CurrentCulture { get; private init; } = DefaultCurrentCulture; - [JsonProperty(Required = Required.DisallowNull)] - public bool Debug { get; private set; } = DefaultDebug; + [JsonInclude] + public bool Debug { get; private init; } = DefaultDebug; - [JsonProperty] - public string? DefaultBot { get; private set; } = DefaultDefaultBot; + [JsonInclude] + public string? DefaultBot { get; private init; } = DefaultDefaultBot; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(1, byte.MaxValue)] - public byte FarmingDelay { get; private set; } = DefaultFarmingDelay; + public byte FarmingDelay { get; private init; } = DefaultFarmingDelay; - [JsonProperty(Required = Required.DisallowNull)] - public bool FilterBadBots { get; private set; } = DefaultFilterBadBots; + [JsonInclude] + public bool FilterBadBots { get; private init; } = DefaultFilterBadBots; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte GiftsLimiterDelay { get; private set; } = DefaultGiftsLimiterDelay; + public byte GiftsLimiterDelay { get; private init; } = DefaultGiftsLimiterDelay; - [JsonProperty(Required = Required.DisallowNull)] - public bool Headless { get; private set; } = DefaultHeadless; + [JsonInclude] + public bool Headless { get; private init; } = DefaultHeadless; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte IdleFarmingPeriod { get; private set; } = DefaultIdleFarmingPeriod; + public byte IdleFarmingPeriod { get; private init; } = DefaultIdleFarmingPeriod; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte InventoryLimiterDelay { get; private set; } = DefaultInventoryLimiterDelay; + public byte InventoryLimiterDelay { get; private init; } = DefaultInventoryLimiterDelay; - [JsonProperty(Required = Required.DisallowNull)] - public bool IPC { get; private set; } = DefaultIPC; + [JsonInclude] + public bool IPC { get; private init; } = DefaultIPC; - [JsonProperty] + [JsonInclude] public string? IPCPassword { get => BackingIPCPassword; @@ -250,74 +252,72 @@ internal set { } } - [JsonProperty(Required = Required.DisallowNull)] - public ArchiCryptoHelper.EHashingMethod IPCPasswordFormat { get; private set; } = DefaultIPCPasswordFormat; + [JsonInclude] + public ArchiCryptoHelper.EHashingMethod IPCPasswordFormat { get; private init; } = DefaultIPCPasswordFormat; - [JsonProperty] - public Guid? LicenseID { get; private set; } = DefaultLicenseID; + [JsonInclude] + public Guid? LicenseID { get; private init; } = DefaultLicenseID; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte LoginLimiterDelay { get; private set; } = DefaultLoginLimiterDelay; + public byte LoginLimiterDelay { get; private init; } = DefaultLoginLimiterDelay; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(1, byte.MaxValue)] - public byte MaxFarmingTime { get; private set; } = DefaultMaxFarmingTime; + public byte MaxFarmingTime { get; private init; } = DefaultMaxFarmingTime; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte MaxTradeHoldDuration { get; private set; } = DefaultMaxTradeHoldDuration; + public byte MaxTradeHoldDuration { get; private init; } = DefaultMaxTradeHoldDuration; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte MinFarmingDelayAfterBlock { get; private set; } = DefaultMinFarmingDelayAfterBlock; + public byte MinFarmingDelayAfterBlock { get; private init; } = DefaultMinFarmingDelayAfterBlock; - [JsonProperty(Required = Required.DisallowNull)] - public EOptimizationMode OptimizationMode { get; private set; } = DefaultOptimizationMode; + [JsonInclude] + public EOptimizationMode OptimizationMode { get; private init; } = DefaultOptimizationMode; - [JsonProperty] + [JsonInclude] [MaxLength(SteamChatMessage.MaxMessagePrefixBytes / SteamChatMessage.ReservedEscapeMessageBytes)] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "This is optional, supportive attribute, we don't care if it gets trimmed or not")] - public string? SteamMessagePrefix { get; private set; } = DefaultSteamMessagePrefix; + public string? SteamMessagePrefix { get; private init; } = DefaultSteamMessagePrefix; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [SwaggerSteamIdentifier] [SwaggerValidValues(ValidIntValues = [0])] - public ulong SteamOwnerID { get; private set; } = DefaultSteamOwnerID; + public ulong SteamOwnerID { get; private init; } = DefaultSteamOwnerID; - [JsonProperty(Required = Required.DisallowNull)] - public ProtocolTypes SteamProtocols { get; private set; } = DefaultSteamProtocols; + [JsonInclude] + public ProtocolTypes SteamProtocols { get; private init; } = DefaultSteamProtocols; - [JsonProperty(Required = Required.DisallowNull)] - public EUpdateChannel UpdateChannel { get; private set; } = DefaultUpdateChannel; + [JsonInclude] + public EUpdateChannel UpdateChannel { get; private init; } = DefaultUpdateChannel; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(byte.MinValue, byte.MaxValue)] - public byte UpdatePeriod { get; private set; } = DefaultUpdatePeriod; + public byte UpdatePeriod { get; private init; } = DefaultUpdatePeriod; - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [Range(ushort.MinValue, ushort.MaxValue)] - public ushort WebLimiterDelay { get; private set; } = DefaultWebLimiterDelay; + public ushort WebLimiterDelay { get; private init; } = DefaultWebLimiterDelay; - [JsonProperty(nameof(WebProxy))] - public string? WebProxyText { get; private set; } = DefaultWebProxyText; + [JsonInclude] + [JsonPropertyName(nameof(WebProxy))] + public string? WebProxyText { get; private init; } = DefaultWebProxyText; - [JsonProperty] - public string? WebProxyUsername { get; private set; } = DefaultWebProxyUsername; + [JsonInclude] + public string? WebProxyUsername { get; private init; } = DefaultWebProxyUsername; [JsonExtensionData] - internal Dictionary? AdditionalProperties { - get; - [UsedImplicitly] - set; - } + [JsonInclude] + internal Dictionary? AdditionalProperties { get; set; } internal bool IsIPCPasswordSet { get; private set; } internal bool IsWebProxyPasswordSet { get; private set; } internal bool Saving { get; set; } - [JsonProperty] + [JsonInclude] internal string? WebProxyPassword { get => BackingWebProxyPassword; @@ -331,18 +331,17 @@ internal string? WebProxyPassword { private WebProxy? BackingWebProxy; private string? BackingWebProxyPassword = DefaultWebProxyPassword; - [JsonProperty($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamOwnerID)}", Required = Required.DisallowNull)] + [JsonDisallowNull] + [JsonInclude] + [JsonPropertyName($"{SharedInfo.UlongCompatibilityStringPrefix}{nameof(SteamOwnerID)}")] private string SSteamOwnerID { get => SteamOwnerID.ToString(CultureInfo.InvariantCulture); - set { - if (string.IsNullOrEmpty(value) || !ulong.TryParse(value, out ulong result)) { - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(SSteamOwnerID))); - - return; - } + init { + ArgumentException.ThrowIfNullOrEmpty(value); - SteamOwnerID = result; + // We intend to throw exception back to caller here + SteamOwnerID = ulong.Parse(value, CultureInfo.InvariantCulture); } } @@ -507,7 +506,7 @@ internal GlobalConfig() { } return (null, null); } - globalConfig = JsonConvert.DeserializeObject(json); + globalConfig = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); @@ -557,7 +556,7 @@ internal GlobalConfig() { } } globalConfig.Saving = true; - string latestJson = JsonConvert.SerializeObject(globalConfig, Formatting.Indented); + string latestJson = globalConfig.ToJsonText(true); globalConfig.Saving = false; return (globalConfig, json != latestJson ? latestJson : null); @@ -567,7 +566,7 @@ internal static async Task Write(string filePath, GlobalConfig globalConfi ArgumentException.ThrowIfNullOrEmpty(filePath); ArgumentNullException.ThrowIfNull(globalConfig); - string json = JsonConvert.SerializeObject(globalConfig, Formatting.Indented); + string json = globalConfig.ToJsonText(true); return await SerializableFile.Write(filePath, json).ConfigureAwait(false); } diff --git a/ArchiSteamFarm/Storage/GlobalDatabase.cs b/ArchiSteamFarm/Storage/GlobalDatabase.cs index 08a0fa3c17aef..eb838edb2bc85 100644 --- a/ArchiSteamFarm/Storage/GlobalDatabase.cs +++ b/ArchiSteamFarm/Storage/GlobalDatabase.cs @@ -25,15 +25,17 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Collections; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; using ArchiSteamFarm.Steam.SteamKit2; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Storage; @@ -46,26 +48,19 @@ public sealed class GlobalDatabase : GenericDatabase { [PublicAPI] public IReadOnlyDictionary PackagesDataReadOnly => PackagesData; - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ConcurrentHashSet CachedBadBots = []; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ObservableConcurrentDictionary CardCountsPerGame = new(); - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly InMemoryServerListProvider ServerListProvider = new(); - - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary PackagesAccessTokens = new(); - - [JsonProperty(Required = Required.DisallowNull)] - private readonly ConcurrentDictionary PackagesData = new(); - private readonly SemaphoreSlim PackagesRefreshSemaphore = new(1, 1); - [JsonProperty(Required = Required.DisallowNull)] + [JsonInclude] [PublicAPI] - public Guid Identifier { get; private set; } = Guid.NewGuid(); + public Guid Identifier { get; private init; } = Guid.NewGuid(); + + [JsonDisallowNull] + [JsonInclude] + internal ConcurrentHashSet CachedBadBots { get; private init; } = []; + + [JsonDisallowNull] + [JsonInclude] + internal ObservableConcurrentDictionary CardCountsPerGame { get; private init; } = new(); internal uint CellID { get => BackingCellID; @@ -93,11 +88,25 @@ internal uint LastChangeNumber { } } - [JsonProperty($"_{nameof(CellID)}", Required = Required.DisallowNull)] - private uint BackingCellID; + [JsonDisallowNull] + [JsonInclude] + internal InMemoryServerListProvider ServerListProvider { get; private init; } = new(); - [JsonProperty($"_{nameof(LastChangeNumber)}", Required = Required.DisallowNull)] - private uint BackingLastChangeNumber; + [JsonInclude] + [JsonPropertyName($"_{nameof(CellID)}")] + private uint BackingCellID { get; set; } + + [JsonInclude] + [JsonPropertyName($"_{nameof(LastChangeNumber)}")] + private uint BackingLastChangeNumber { get; set; } + + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary PackagesAccessTokens { get; init; } = new(); + + [JsonDisallowNull] + [JsonInclude] + private ConcurrentDictionary PackagesData { get; init; } = new(); private GlobalDatabase(string filePath) : this() { ArgumentException.ThrowIfNullOrEmpty(filePath); @@ -112,11 +121,37 @@ private GlobalDatabase() { ServerListProvider.ServerListUpdated += OnObjectModified; } + [PublicAPI] + public void DeleteFromJsonStorage(string key) { + ArgumentException.ThrowIfNullOrEmpty(key); + + DeleteFromJsonStorage(this, key); + } + + [PublicAPI] + public void SaveToJsonStorage(string key, T value) where T : notnull { + ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(value); + + SaveToJsonStorage(this, key, value); + } + + [PublicAPI] + public void SaveToJsonStorage(string key, JsonElement value) { + ArgumentException.ThrowIfNullOrEmpty(key); + + if (value.ValueKind == JsonValueKind.Undefined) { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + SaveToJsonStorage(this, key, value); + } + [UsedImplicitly] public bool ShouldSerializeBackingCellID() => BackingCellID != 0; [UsedImplicitly] - public bool ShouldSerializeBackingLastChangeNumber() => LastChangeNumber != 0; + public bool ShouldSerializeBackingLastChangeNumber() => BackingLastChangeNumber != 0; [UsedImplicitly] public bool ShouldSerializeCachedBadBots() => CachedBadBots.Count > 0; @@ -148,13 +183,15 @@ protected override void Dispose(bool disposing) { base.Dispose(disposing); } + protected override Task Save() => Save(this); + internal static async Task CreateOrLoad(string filePath) { ArgumentException.ThrowIfNullOrEmpty(filePath); if (!File.Exists(filePath)) { GlobalDatabase result = new(filePath); - Utilities.InBackground(result.Save); + Utilities.InBackground(() => Save(result)); return result; } @@ -170,7 +207,7 @@ protected override void Dispose(bool disposing) { return null; } - globalDatabase = JsonConvert.DeserializeObject(json); + globalDatabase = json.ToJsonObject(); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); diff --git a/ArchiSteamFarm/Storage/PackageData.cs b/ArchiSteamFarm/Storage/PackageData.cs index f269006e1f5f3..18042a2e9bc4e 100644 --- a/ArchiSteamFarm/Storage/PackageData.cs +++ b/ArchiSteamFarm/Storage/PackageData.cs @@ -21,23 +21,25 @@ using System; using System.Collections.Immutable; +using System.Text.Json.Serialization; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Storage; public sealed class PackageData { - [JsonProperty] - public ImmutableHashSet? AppIDs { get; private set; } + [JsonInclude] + public ImmutableHashSet? AppIDs { get; private init; } - [JsonProperty(Required = Required.Always)] - public uint ChangeNumber { get; private set; } + [JsonInclude] + [JsonRequired] + public uint ChangeNumber { get; private init; } - [JsonProperty] - public ImmutableHashSet? ProhibitRunInCountries { get; private set; } + [JsonInclude] + public ImmutableHashSet? ProhibitRunInCountries { get; private init; } - [JsonProperty(Required = Required.Always)] - public DateTime ValidUntil { get; private set; } + [JsonInclude] + [JsonRequired] + public DateTime ValidUntil { get; private init; } internal PackageData(uint changeNumber, DateTime validUntil, ImmutableHashSet? appIDs = null, ImmutableHashSet? prohibitRunInCountries = null) { ArgumentOutOfRangeException.ThrowIfZero(changeNumber); diff --git a/ArchiSteamFarm/Web/GitHub.cs b/ArchiSteamFarm/Web/GitHub.cs index b6b1c42364a28..7225c7b9a93a6 100644 --- a/ArchiSteamFarm/Web/GitHub.cs +++ b/ArchiSteamFarm/Web/GitHub.cs @@ -27,6 +27,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AngleSharp.Dom; @@ -36,7 +37,6 @@ using Markdig.Renderers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -using Newtonsoft.Json; namespace ArchiSteamFarm.Web; @@ -193,18 +193,6 @@ private static MarkdownDocument ExtractChangelogFromBody(string markdownText) { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class ReleaseResponse { - [JsonProperty("assets", Required = Required.Always)] - internal readonly ImmutableHashSet Assets = ImmutableHashSet.Empty; - - [JsonProperty("prerelease", Required = Required.Always)] - internal readonly bool IsPreRelease; - - [JsonProperty("published_at", Required = Required.Always)] - internal readonly DateTime PublishedAt; - - [JsonProperty("tag_name", Required = Required.Always)] - internal readonly string Tag = ""; - internal string? ChangelogHTML { get { if (BackingChangelogHTML != null) { @@ -255,9 +243,6 @@ internal string? ChangelogPlainText { } } - [JsonProperty("body", Required = Required.Always)] - private readonly string? MarkdownBody = ""; - private MarkdownDocument? Changelog { get { if (BackingChangelog != null) { @@ -274,22 +259,53 @@ private MarkdownDocument? Changelog { } } + [JsonInclude] + [JsonPropertyName("assets")] + [JsonRequired] + internal ImmutableHashSet Assets { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("prerelease")] + [JsonRequired] + internal bool IsPreRelease { get; private init; } + + [JsonInclude] + [JsonPropertyName("published_at")] + [JsonRequired] + internal DateTime PublishedAt { get; private init; } + + [JsonInclude] + [JsonPropertyName("tag_name")] + [JsonRequired] + internal string Tag { get; private init; } = ""; + private MarkdownDocument? BackingChangelog; private string? BackingChangelogHTML; private string? BackingChangelogPlainText; + [JsonInclude] + [JsonPropertyName("body")] + [JsonRequired] + private string? MarkdownBody { get; init; } = ""; + [JsonConstructor] private ReleaseResponse() { } internal sealed class Asset { - [JsonProperty("browser_download_url", Required = Required.Always)] - internal readonly Uri? DownloadURL; - - [JsonProperty("name", Required = Required.Always)] - internal readonly string? Name; - - [JsonProperty("size", Required = Required.Always)] - internal readonly uint Size; + [JsonInclude] + [JsonPropertyName("browser_download_url")] + [JsonRequired] + internal Uri? DownloadURL { get; private init; } + + [JsonInclude] + [JsonPropertyName("name")] + [JsonRequired] + internal string? Name { get; private init; } + + [JsonInclude] + [JsonPropertyName("size")] + [JsonRequired] + internal uint Size { get; private init; } [JsonConstructor] private Asset() { } diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs index 87c3d5b1b2bab..f0fa3ebf85555 100644 --- a/ArchiSteamFarm/Web/WebBrowser.cs +++ b/ArchiSteamFarm/Web/WebBrowser.cs @@ -28,16 +28,16 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Localization; using ArchiSteamFarm.NLog; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web.Responses; using JetBrains.Annotations; -using Newtonsoft.Json; namespace ArchiSteamFarm.Web; @@ -323,17 +323,7 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) { T? obj; try { - using StreamReader streamReader = new(response.Content); - -#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose - JsonTextReader jsonReader = new(streamReader); -#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose - - await using (jsonReader.ConfigureAwait(false)) { - JsonSerializer serializer = new(); - - obj = serializer.Deserialize(jsonReader); - } + obj = await response.Content.ToJsonObject(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception e) { @@ -606,17 +596,7 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) { TResult? obj; try { - using StreamReader streamReader = new(response.Content); - -#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose - JsonTextReader jsonReader = new(streamReader); -#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose - - await using (jsonReader.ConfigureAwait(false)) { - JsonSerializer serializer = new(); - - obj = serializer.Deserialize(jsonReader); - } + obj = await response.Content.ToJsonObject(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception e) { @@ -762,7 +742,7 @@ internal static void Init() { break; default: - requestMessage.Content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); + requestMessage.Content = JsonContent.Create(data, options: JsonUtilities.DefaultJsonSerialierOptions); break; } diff --git a/Directory.Packages.props b/Directory.Packages.props index e4b9bd9f23c86..0a42fcd64ed31 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,13 +10,11 @@ - -