From 62ce58e14888e3fa157daa5eb9ccdef6b841b523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Sun, 9 Feb 2025 21:14:05 +0100 Subject: [PATCH] Closes #3378 --- .../IPC/Controllers/Api/BotController.cs | 52 +++++++++++++++++++ .../IPC/Responses/BotInventoryResponse.cs | 45 ++++++++++++++++ ArchiSteamFarm/Steam/Interaction/Actions.cs | 28 ++++++++++ 3 files changed, 125 insertions(+) create mode 100644 ArchiSteamFarm/IPC/Responses/BotInventoryResponse.cs diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index db5ddbbc5..b52d50dad 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -33,6 +33,7 @@ using ArchiSteamFarm.IPC.Requests; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; +using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Storage; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -277,6 +278,57 @@ public sealed class BotController : ArchiController { return Ok(results.All(static result => result) ? new GenericResponse(true) : new GenericResponse(false, Strings.WarningFailed)); } + [EndpointSummary("Fetches inventory of given bots")] + [HttpGet("{botNames:required}/Inventory/{appID}/{contextID}")] + [ProducesResponseType>>((int) HttpStatusCode.OK)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + public async Task> InventoryGet(string botNames, uint appID, ulong contextID) { + ArgumentException.ThrowIfNullOrEmpty(botNames); + + if (appID == 0) { + return BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(appID)))); + } + + if (contextID == 0) { + return BadRequest(new GenericResponse(false, Strings.FormatErrorIsInvalid(nameof(contextID)))); + } + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames))); + } + + IList<(HashSet? Result, string Message)> results = await Utilities.InParallel(bots.Select(bot => bot.Actions.GetInventory(appID, contextID))).ConfigureAwait(false); + + Dictionary result = new(bots.Count, Bot.BotsComparer); + + foreach (Bot bot in bots) { + (HashSet? inventory, _) = results[result.Count]; + + if (inventory == null) { + result[bot.BotName] = new BotInventoryResponse(); + + continue; + } + + HashSet assets = new(inventory.Count); + HashSet descriptions = []; + + foreach (Asset asset in inventory) { + assets.Add(asset.Body); + + if (asset.Description != null) { + descriptions.Add(asset.Description.Body); + } + } + + result[bot.BotName] = new BotInventoryResponse(assets, descriptions); + } + + return Ok(new GenericResponse>(result)); + } + [EndpointSummary("Pauses given bots")] [HttpPost("{botNames:required}/Pause")] [ProducesResponseType((int) HttpStatusCode.OK)] diff --git a/ArchiSteamFarm/IPC/Responses/BotInventoryResponse.cs b/ArchiSteamFarm/IPC/Responses/BotInventoryResponse.cs new file mode 100644 index 000000000..951ada4fa --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/BotInventoryResponse.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------------------------- +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// ---------------------------------------------------------------------------------------------- +// | +// Copyright 2015-2025 Ɓ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.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Text.Json.Serialization; +using SteamKit2.Internal; + +namespace ArchiSteamFarm.IPC.Responses; + +public sealed class BotInventoryResponse { + [Description("Inventory assetsr")] + [JsonInclude] + public ImmutableHashSet? Assets { get; private init; } + + [Description("Descriptions of the inventory assets")] + [JsonInclude] + public ImmutableHashSet? Descriptions { get; private init; } + + internal BotInventoryResponse(IEnumerable? assets = null, IEnumerable? descriptions = null) { + Assets = assets?.ToImmutableHashSet(); + Descriptions = descriptions?.ToImmutableHashSet(); + } +} diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 2b9484288..cdb673c00 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -36,6 +36,7 @@ using ArchiSteamFarm.Plugins; using ArchiSteamFarm.Plugins.Interfaces; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; +using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; @@ -172,6 +173,33 @@ public sealed class Actions : IAsyncDisposable, IDisposable { return (steamOwnerID > 0) && new SteamID(steamOwnerID).IsIndividualAccount ? steamOwnerID : 0; } + /// This action should be used if you require full inventory exclusively, otherwise consider calling instead. + [PublicAPI] + public async Task<(HashSet? Result, string Message)> GetInventory(uint appID = Asset.SteamAppID, ulong contextID = Asset.SteamCommunityContextID, Func? filterFunction = null) { + ArgumentOutOfRangeException.ThrowIfZero(appID); + ArgumentOutOfRangeException.ThrowIfZero(contextID); + + if (!Bot.IsConnectedAndLoggedOn) { + return (null, Strings.BotNotConnected); + } + + filterFunction ??= static _ => true; + + using (await GetTradingLock().ConfigureAwait(false)) { + try { + return (await Bot.ArchiHandler.GetMyInventoryAsync(appID, contextID).Where(item => filterFunction(item)).ToHashSetAsync().ConfigureAwait(false), Strings.Success); + } catch (TimeoutException e) { + Bot.ArchiLogger.LogGenericWarningException(e); + + return (null, Strings.FormatWarningFailedWithError(e.Message)); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericException(e); + + return (null, Strings.FormatWarningFailedWithError(e.Message)); + } + } + } + [PublicAPI] public async Task?> GetRewardItems(IReadOnlyCollection definitionIDs) { if ((definitionIDs == null) || (definitionIDs.Count == 0)) {