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)));