From 4eae3ebf4d537fc92b76384dc0af8d6f9a835259 Mon Sep 17 00:00:00 2001 From: Archi Date: Fri, 23 Dec 2022 18:21:43 +0100 Subject: [PATCH] Use custom WebBrowser for items matcher Now this is dictated by at least several reasons: - Firstly, we must have a WebBrowser per bot, and not per ASF instance, as we preserve ASF STM cookies that are on per-bot basis, which are required e.g. for Announce - At the same time we shouldn't use Bot's one, because there are settings like WebProxy that shouldn't be used in regards to our own server - We also require higher timeout than default one, especially for Announce, but also Inventories - Best we can do is optimize that to not create a WebBrowser for bots that are neither configured for public listing, nor match actively. Since those settings need to be explicitly turned on, we shouldn't be duplicating WebBrowser per each bot instance, but rather only few selected bots configured to participate. --- .../Backend.cs | 15 +++++--- .../RemoteCommunication.cs | 38 +++++++++++++++++-- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs index eb74cc701..0c9722f44 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/Backend.cs @@ -37,8 +37,9 @@ using ArchiSteamFarm.Web.Responses; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; internal static class Backend { - internal static async Task AnnounceForListing(Bot bot, IReadOnlyList inventory, IReadOnlyCollection acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) { + internal static async Task AnnounceForListing(Bot bot, WebBrowser webBrowser, IReadOnlyList inventory, IReadOnlyCollection acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) { ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(webBrowser); if ((inventory == null) || (inventory.Count == 0)) { throw new ArgumentNullException(nameof(inventory)); @@ -60,15 +61,16 @@ internal static class Backend { AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, tradeToken, inventory, acceptedMatchableTypes, bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything), ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, nickname, avatarHash); - return await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); + return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); } - internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, IReadOnlyCollection inventory, IReadOnlyCollection acceptedMatchableTypes) { + internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection inventory, IReadOnlyCollection acceptedMatchableTypes) { if (licenseID == Guid.Empty) { throw new ArgumentOutOfRangeException(nameof(licenseID)); } ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(webBrowser); if ((inventory == null) || (inventory.Count == 0)) { throw new ArgumentNullException(nameof(inventory)); @@ -86,7 +88,7 @@ internal static class Backend { InventoriesRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, inventory, acceptedMatchableTypes); - ObjectResponse>>? response = await bot.ArchiWebHandler.WebBrowser.UrlPostToJsonObject>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); + ObjectResponse>>? response = await webBrowser.UrlPostToJsonObject>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); if (response == null) { return null; @@ -95,13 +97,14 @@ internal static class Backend { return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet.Empty); } - internal static async Task HeartBeatForListing(Bot bot) { + internal static async Task HeartBeatForListing(Bot bot, WebBrowser webBrowser) { ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(webBrowser); Uri request = new(ArchiNet.URL, "/Api/Listing/HeartBeat"); HeartBeatRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID); - return await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); + return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false); } } diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index 882d3d808..568c5218d 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -39,6 +39,7 @@ using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; +using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; @@ -62,6 +63,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1); private readonly Timer? MatchActivelyTimer; private readonly SemaphoreSlim RequestsSemaphore = new(1, 1); + private readonly WebBrowser? WebBrowser; private DateTime LastAnnouncement; private DateTime LastHeartBeat; @@ -75,6 +77,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { Bot = bot; + if (!Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing) && !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively)) { + return; + } + + WebBrowser = new WebBrowser(bot.ArchiLogger, extendedTimeout: true); + if (Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) { HeartBeatTimer = new Timer( HeartBeat, @@ -105,7 +113,15 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { // Those are objects that might be null and the check should be in-place HeartBeatTimer?.Dispose(); - MatchActivelyTimer?.Dispose(); + + if (MatchActivelyTimer != null) { + // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that + lock (MatchActivelySemaphore) { + MatchActivelyTimer.Dispose(); + } + } + + WebBrowser?.Dispose(); } public async ValueTask DisposeAsync() { @@ -124,6 +140,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { MatchActivelyTimer.Dispose(); } } + + WebBrowser?.Dispose(); } internal void OnNewItemsNotification() => ShouldSendAnnouncementEarlier = true; @@ -133,6 +151,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { return; } + if (WebBrowser == null) { + throw new InvalidOperationException(nameof(WebBrowser)); + } + if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) { return; } @@ -232,7 +254,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, inventory.Count)); // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - BasicResponse? response = await Backend.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false); + BasicResponse? response = await Backend.AnnounceForListing(Bot, WebBrowser, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false); if (response == null) { // This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check @@ -308,6 +330,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { } private async void HeartBeat(object? state = null) { + if (WebBrowser == null) { + throw new InvalidOperationException(nameof(WebBrowser)); + } + if (!Bot.IsConnectedAndLoggedOn || (Bot.HeartBeatFailures > 0)) { return; } @@ -327,7 +353,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { } try { - BasicResponse? response = await Backend.HeartBeatForListing(Bot).ConfigureAwait(false); + BasicResponse? response = await Backend.HeartBeatForListing(Bot, WebBrowser).ConfigureAwait(false); if (response == null) { // This is actually a network failure, we should keep sending heartbeats for now @@ -416,6 +442,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { } private async void MatchActively(object? state = null) { + if (WebBrowser == null) { + throw new InvalidOperationException(nameof(WebBrowser)); + } + if (ASF.GlobalConfig == null) { throw new InvalidOperationException(nameof(ASF.GlobalConfig)); } @@ -481,7 +511,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable { } // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - (HttpStatusCode StatusCode, ImmutableHashSet Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, ourInventory, acceptedMatchableTypes).ConfigureAwait(false); + (HttpStatusCode StatusCode, ImmutableHashSet Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, ourInventory, acceptedMatchableTypes).ConfigureAwait(false); if (response == null) { Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(response)));