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.
This commit is contained in:
Archi 2022-12-23 18:21:43 +01:00
parent 1daa6728f6
commit 4eae3ebf4d
No known key found for this signature in database
GPG key ID: 6B138B4C64555AEA
2 changed files with 43 additions and 10 deletions

View file

@ -37,8 +37,9 @@ using ArchiSteamFarm.Web.Responses;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
internal static class Backend { internal static class Backend {
internal static async Task<BasicResponse?> AnnounceForListing(Bot bot, IReadOnlyList<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) { internal static async Task<BasicResponse?> AnnounceForListing(Bot bot, WebBrowser webBrowser, IReadOnlyList<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
ArgumentNullException.ThrowIfNull(bot); ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(webBrowser);
if ((inventory == null) || (inventory.Count == 0)) { if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory)); 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); 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<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) { internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes) {
if (licenseID == Guid.Empty) { if (licenseID == Guid.Empty) {
throw new ArgumentOutOfRangeException(nameof(licenseID)); throw new ArgumentOutOfRangeException(nameof(licenseID));
} }
ArgumentNullException.ThrowIfNull(bot); ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(webBrowser);
if ((inventory == null) || (inventory.Count == 0)) { if ((inventory == null) || (inventory.Count == 0)) {
throw new ArgumentNullException(nameof(inventory)); 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); InventoriesRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, inventory, acceptedMatchableTypes);
ObjectResponse<GenericResponse<ImmutableHashSet<ListedUser>>>? response = await bot.ArchiWebHandler.WebBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<ListedUser>>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); ObjectResponse<GenericResponse<ImmutableHashSet<ListedUser>>>? response = await webBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<ListedUser>>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
if (response == null) { if (response == null) {
return null; return null;
@ -95,13 +97,14 @@ internal static class Backend {
return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet<ListedUser>.Empty); return (response.StatusCode, response.Content?.Result ?? ImmutableHashSet<ListedUser>.Empty);
} }
internal static async Task<BasicResponse?> HeartBeatForListing(Bot bot) { internal static async Task<BasicResponse?> HeartBeatForListing(Bot bot, WebBrowser webBrowser) {
ArgumentNullException.ThrowIfNull(bot); ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(webBrowser);
Uri request = new(ArchiNet.URL, "/Api/Listing/HeartBeat"); Uri request = new(ArchiNet.URL, "/Api/Listing/HeartBeat");
HeartBeatRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID); 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);
} }
} }

View file

@ -39,6 +39,7 @@ using ArchiSteamFarm.Steam.Integration;
using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Security;
using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Steam.Storage;
using ArchiSteamFarm.Storage; using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web;
using ArchiSteamFarm.Web.Responses; using ArchiSteamFarm.Web.Responses;
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher; namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
@ -62,6 +63,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1); private readonly SemaphoreSlim MatchActivelySemaphore = new(1, 1);
private readonly Timer? MatchActivelyTimer; private readonly Timer? MatchActivelyTimer;
private readonly SemaphoreSlim RequestsSemaphore = new(1, 1); private readonly SemaphoreSlim RequestsSemaphore = new(1, 1);
private readonly WebBrowser? WebBrowser;
private DateTime LastAnnouncement; private DateTime LastAnnouncement;
private DateTime LastHeartBeat; private DateTime LastHeartBeat;
@ -75,6 +77,12 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
Bot = bot; 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)) { if (Bot.BotConfig.RemoteCommunication.HasFlag(BotConfig.ERemoteCommunication.PublicListing)) {
HeartBeatTimer = new Timer( HeartBeatTimer = new Timer(
HeartBeat, 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 // Those are objects that might be null and the check should be in-place
HeartBeatTimer?.Dispose(); 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() { public async ValueTask DisposeAsync() {
@ -124,6 +140,8 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
MatchActivelyTimer.Dispose(); MatchActivelyTimer.Dispose();
} }
} }
WebBrowser?.Dispose();
} }
internal void OnNewItemsNotification() => ShouldSendAnnouncementEarlier = true; internal void OnNewItemsNotification() => ShouldSendAnnouncementEarlier = true;
@ -133,6 +151,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
return; return;
} }
if (WebBrowser == null) {
throw new InvalidOperationException(nameof(WebBrowser));
}
if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) { if ((DateTime.UtcNow < LastAnnouncement.AddMinutes(ShouldSendAnnouncementEarlier ? MinAnnouncementTTL : MaxAnnouncementTTL)) && ShouldSendHeartBeats) {
return; 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)); Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, inventory.Count));
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework // 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) { if (response == null) {
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check // 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) { private async void HeartBeat(object? state = null) {
if (WebBrowser == null) {
throw new InvalidOperationException(nameof(WebBrowser));
}
if (!Bot.IsConnectedAndLoggedOn || (Bot.HeartBeatFailures > 0)) { if (!Bot.IsConnectedAndLoggedOn || (Bot.HeartBeatFailures > 0)) {
return; return;
} }
@ -327,7 +353,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
} }
try { try {
BasicResponse? response = await Backend.HeartBeatForListing(Bot).ConfigureAwait(false); BasicResponse? response = await Backend.HeartBeatForListing(Bot, WebBrowser).ConfigureAwait(false);
if (response == null) { if (response == null) {
// This is actually a network failure, we should keep sending heartbeats for now // 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) { private async void MatchActively(object? state = null) {
if (WebBrowser == null) {
throw new InvalidOperationException(nameof(WebBrowser));
}
if (ASF.GlobalConfig == null) { if (ASF.GlobalConfig == null) {
throw new InvalidOperationException(nameof(ASF.GlobalConfig)); throw new InvalidOperationException(nameof(ASF.GlobalConfig));
} }
@ -481,7 +511,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
} }
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, ourInventory, acceptedMatchableTypes).ConfigureAwait(false); (HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)? response = await Backend.GetListedUsersForMatching(ASF.GlobalConfig.LicenseID.Value, Bot, WebBrowser, ourInventory, acceptedMatchableTypes).ConfigureAwait(false);
if (response == null) { if (response == null) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(response))); Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(response)));