2024-03-16 23:06:13 +00:00
|
|
|
// ----------------------------------------------------------------------------------------------
|
2022-12-15 17:46:37 +00:00
|
|
|
// _ _ _ ____ _ _____
|
|
|
|
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
|
|
|
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
|
|
|
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
|
|
|
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
2024-03-16 23:06:13 +00:00
|
|
|
// ----------------------------------------------------------------------------------------------
|
2024-03-17 01:35:40 +00:00
|
|
|
// |
|
2024-01-08 10:33:28 +00:00
|
|
|
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
|
2022-12-15 17:46:37 +00:00
|
|
|
// Contact: JustArchi@JustArchi.net
|
2024-03-17 01:35:40 +00:00
|
|
|
// |
|
2022-12-15 17:46:37 +00:00
|
|
|
// 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
|
2024-03-17 01:35:40 +00:00
|
|
|
// |
|
2022-12-15 17:46:37 +00:00
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
2024-03-17 01:35:40 +00:00
|
|
|
// |
|
2022-12-15 17:46:37 +00:00
|
|
|
// 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.Collections.Generic;
|
|
|
|
using System.Collections.Immutable;
|
2023-11-28 23:08:16 +00:00
|
|
|
using System.Linq;
|
2022-12-15 17:46:37 +00:00
|
|
|
using System.Net;
|
2023-11-28 23:08:16 +00:00
|
|
|
using System.Text;
|
|
|
|
using System.Threading;
|
2022-12-15 17:46:37 +00:00
|
|
|
using System.Threading.Tasks;
|
|
|
|
using ArchiSteamFarm.Core;
|
|
|
|
using ArchiSteamFarm.IPC.Responses;
|
2022-12-15 18:16:28 +00:00
|
|
|
using ArchiSteamFarm.OfficialPlugins.ItemsMatcher.Data;
|
2022-12-15 17:46:37 +00:00
|
|
|
using ArchiSteamFarm.Steam;
|
|
|
|
using ArchiSteamFarm.Steam.Data;
|
|
|
|
using ArchiSteamFarm.Steam.Storage;
|
|
|
|
using ArchiSteamFarm.Storage;
|
|
|
|
using ArchiSteamFarm.Web;
|
|
|
|
using ArchiSteamFarm.Web.Responses;
|
2023-01-14 22:41:25 +00:00
|
|
|
using SteamKit2;
|
2022-12-15 17:46:37 +00:00
|
|
|
|
|
|
|
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
|
|
|
|
|
|
|
internal static class Backend {
|
2024-03-17 01:29:04 +00:00
|
|
|
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceDiffForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, IReadOnlyCollection<AssetForListing> inventoryRemoved, string? previousInventoryChecksum, string? nickname = null, string? avatarHash = null) {
|
2023-11-28 23:08:16 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
|
|
|
|
2023-01-14 22:41:25 +00:00
|
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(steamID));
|
|
|
|
}
|
|
|
|
|
2023-11-28 23:08:16 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(inventory);
|
|
|
|
ArgumentException.ThrowIfNullOrEmpty(inventoryChecksum);
|
|
|
|
|
|
|
|
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
|
|
|
}
|
|
|
|
|
|
|
|
ArgumentOutOfRangeException.ThrowIfZero(totalInventoryCount);
|
|
|
|
ArgumentException.ThrowIfNullOrEmpty(tradeToken);
|
|
|
|
|
|
|
|
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(tradeToken));
|
|
|
|
}
|
|
|
|
|
|
|
|
ArgumentNullException.ThrowIfNull(inventoryRemoved);
|
|
|
|
ArgumentException.ThrowIfNullOrEmpty(previousInventoryChecksum);
|
|
|
|
|
2023-12-02 18:36:34 +00:00
|
|
|
Uri request = new(ArchiNet.URL, "/Api/Listing/AnnounceDiff/v2");
|
2023-11-28 23:08:16 +00:00
|
|
|
|
|
|
|
AnnouncementDiffRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, inventory, inventoryChecksum, acceptedMatchableTypes, totalInventoryCount, matchEverything, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, tradeToken, inventoryRemoved, previousInventoryChecksum, nickname, avatarHash);
|
|
|
|
|
2023-12-02 18:36:34 +00:00
|
|
|
return await webBrowser.UrlPostToJsonObject<GenericResponse<BackgroundTaskResponse>, AnnouncementDiffRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
|
2023-11-28 23:08:16 +00:00
|
|
|
}
|
|
|
|
|
2024-03-17 01:29:04 +00:00
|
|
|
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> AnnounceForListing(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<AssetForListing> inventory, string inventoryChecksum, IReadOnlyCollection<EAssetType> acceptedMatchableTypes, uint totalInventoryCount, bool matchEverything, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
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.
2022-12-23 17:21:43 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
2023-11-28 23:08:16 +00:00
|
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(steamID));
|
|
|
|
}
|
|
|
|
|
2022-12-15 17:46:37 +00:00
|
|
|
if ((inventory == null) || (inventory.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(inventory));
|
|
|
|
}
|
|
|
|
|
2023-11-28 23:08:16 +00:00
|
|
|
ArgumentException.ThrowIfNullOrEmpty(inventoryChecksum);
|
|
|
|
|
2022-12-15 17:46:37 +00:00
|
|
|
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
|
|
|
}
|
|
|
|
|
2023-11-14 18:12:33 +00:00
|
|
|
ArgumentOutOfRangeException.ThrowIfZero(totalInventoryCount);
|
|
|
|
ArgumentException.ThrowIfNullOrEmpty(tradeToken);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
|
|
|
if (tradeToken.Length != BotConfig.SteamTradeTokenLength) {
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(tradeToken));
|
|
|
|
}
|
|
|
|
|
2023-12-02 18:36:34 +00:00
|
|
|
Uri request = new(ArchiNet.URL, "/Api/Listing/Announce/v5");
|
2022-12-15 17:46:37 +00:00
|
|
|
|
2023-11-28 23:08:16 +00:00
|
|
|
AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, inventory, inventoryChecksum, acceptedMatchableTypes, totalInventoryCount, matchEverything, ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, tradeToken, nickname, avatarHash);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
2023-12-02 18:36:34 +00:00
|
|
|
return await webBrowser.UrlPostToJsonObject<GenericResponse<BackgroundTaskResponse>, AnnouncementRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
|
2022-12-15 17:46:37 +00:00
|
|
|
}
|
|
|
|
|
2023-11-28 23:08:16 +00:00
|
|
|
internal static string GenerateChecksumFor(IList<AssetForListing> assetsForListings) {
|
|
|
|
if ((assetsForListings == null) || (assetsForListings.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(assetsForListings));
|
|
|
|
}
|
|
|
|
|
|
|
|
string text = string.Join('|', assetsForListings.Select(static asset => asset.BackendHashCode));
|
|
|
|
byte[] bytes = Encoding.UTF8.GetBytes(text);
|
|
|
|
|
|
|
|
return Utilities.GenerateChecksumFor(bytes);
|
|
|
|
}
|
|
|
|
|
2024-02-04 21:28:59 +00:00
|
|
|
internal static async Task<HttpStatusCode?> GetLicenseStatus(Guid licenseID, WebBrowser webBrowser) {
|
|
|
|
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
|
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
|
|
|
|
|
|
|
Uri request = new(ArchiNet.URL, "/Api/Licenses/Status");
|
|
|
|
|
|
|
|
Dictionary<string, string> headers = new(1, StringComparer.Ordinal) {
|
|
|
|
{ "X-License-Key", licenseID.ToString("N") }
|
|
|
|
};
|
|
|
|
|
|
|
|
ObjectResponse<GenericResponse>? response = await webBrowser.UrlGetToJsonObject<GenericResponse>(request, headers, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
|
|
|
|
|
|
|
return response?.StatusCode;
|
|
|
|
}
|
|
|
|
|
2024-03-17 01:29:04 +00:00
|
|
|
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, WebBrowser webBrowser, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<EAssetType> acceptedMatchableTypes) {
|
2023-11-14 18:12:33 +00:00
|
|
|
ArgumentOutOfRangeException.ThrowIfEqual(licenseID, Guid.Empty);
|
2022-12-15 17:46:37 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(bot);
|
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.
2022-12-23 17:21:43 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
|
|
|
if ((inventory == null) || (inventory.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(inventory));
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((acceptedMatchableTypes == null) || (acceptedMatchableTypes.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(acceptedMatchableTypes));
|
|
|
|
}
|
|
|
|
|
2023-01-11 17:40:46 +00:00
|
|
|
Uri request = new(ArchiNet.URL, "/Api/Listing/Inventories/v2");
|
2022-12-15 17:46:37 +00:00
|
|
|
|
|
|
|
Dictionary<string, string> headers = new(1, StringComparer.Ordinal) {
|
|
|
|
{ "X-License-Key", licenseID.ToString("N") }
|
|
|
|
};
|
|
|
|
|
2022-12-23 14:31:14 +00:00
|
|
|
InventoriesRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, inventory, acceptedMatchableTypes);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
2023-01-15 20:26:03 +00:00
|
|
|
ObjectResponse<GenericResponse<ImmutableHashSet<ListedUser>>>? response = await webBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<ListedUser>>, InventoriesRequest>(request, headers, data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
|
|
|
if (response == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2024-03-17 01:54:28 +00:00
|
|
|
return (response.StatusCode, response.Content?.Result ?? []);
|
2022-12-15 17:46:37 +00:00
|
|
|
}
|
|
|
|
|
2024-03-17 01:29:04 +00:00
|
|
|
internal static async Task<ObjectResponse<GenericResponse<ImmutableHashSet<SetPart>>>?> GetSetParts(WebBrowser webBrowser, ulong steamID, IReadOnlyCollection<EAssetType> matchableTypes, IReadOnlyCollection<uint> realAppIDs, CancellationToken cancellationToken = default) {
|
2023-11-28 23:08:16 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
|
|
|
|
|
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(steamID));
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((matchableTypes == null) || (matchableTypes.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(matchableTypes));
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((realAppIDs == null) || (realAppIDs.Count == 0)) {
|
|
|
|
throw new ArgumentNullException(nameof(realAppIDs));
|
|
|
|
}
|
|
|
|
|
|
|
|
Uri request = new(ArchiNet.URL, "/Api/SetParts/Request");
|
|
|
|
|
|
|
|
SetPartsRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), steamID, matchableTypes, realAppIDs);
|
|
|
|
|
|
|
|
return await webBrowser.UrlPostToJsonObject<GenericResponse<ImmutableHashSet<SetPart>>, SetPartsRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors | WebBrowser.ERequestOptions.CompressRequest, cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
|
|
}
|
|
|
|
|
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.
2022-12-23 17:21:43 +00:00
|
|
|
internal static async Task<BasicResponse?> HeartBeatForListing(Bot bot, WebBrowser webBrowser) {
|
2022-12-15 17:46:37 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(bot);
|
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.
2022-12-23 17:21:43 +00:00
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
2022-12-15 17:46:37 +00:00
|
|
|
|
|
|
|
Uri request = new(ArchiNet.URL, "/Api/Listing/HeartBeat");
|
|
|
|
|
|
|
|
HeartBeatRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID);
|
|
|
|
|
2023-01-15 20:26:03 +00:00
|
|
|
return await webBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.CompressRequest).ConfigureAwait(false);
|
2022-12-15 17:46:37 +00:00
|
|
|
}
|
2023-12-02 18:36:34 +00:00
|
|
|
|
|
|
|
internal static async Task<ObjectResponse<GenericResponse<BackgroundTaskResponse>>?> PollResult(WebBrowser webBrowser, ulong steamID, Guid requestID) {
|
|
|
|
ArgumentNullException.ThrowIfNull(webBrowser);
|
|
|
|
|
|
|
|
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(steamID));
|
|
|
|
}
|
|
|
|
|
|
|
|
ArgumentOutOfRangeException.ThrowIfEqual(requestID, Guid.Empty);
|
|
|
|
|
2024-09-16 15:58:35 +00:00
|
|
|
if (BuildInfo.IsCustomBuild) {
|
2023-12-02 18:36:34 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
Uri request = new(ArchiNet.URL, $"/Api/Listing/PollResult/{steamID}/{requestID:N}");
|
|
|
|
|
|
|
|
return await webBrowser.UrlGetToJsonObject<GenericResponse<BackgroundTaskResponse>>(request, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
|
|
|
}
|
2022-12-15 17:46:37 +00:00
|
|
|
}
|