Skip to content

Commit

Permalink
Add new ASF API endpoint for inventory summary, add inventory command
Browse files Browse the repository at this point in the history
Wow, new features in ASF?!
  • Loading branch information
JustArchi committed Feb 16, 2025
1 parent 90db25e commit 3f079a8
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 8 deletions.
47 changes: 47 additions & 0 deletions ArchiSteamFarm/Helpers/Json/BooleanNormalizationConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Łukasz "JustArchi" Domeradzki
// Contact: [email protected]
// |
// 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;
using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace ArchiSteamFarm.Helpers.Json;

[PublicAPI]
public sealed class BooleanNormalizationConverter : JsonConverter<bool> {
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType switch {
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.Number => reader.GetByte() == 1,
JsonTokenType.String => reader.GetString() == "1",
_ => throw new JsonException()
};

public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) {
ArgumentNullException.ThrowIfNull(writer);

writer.WriteBooleanValue(value);
}
}
1 change: 1 addition & 0 deletions ArchiSteamFarm/Helpers/Json/BooleanNumberConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

namespace ArchiSteamFarm.Helpers.Json;

[Obsolete($"Use {nameof(BooleanNormalizationConverter)} instead if you want to always serialize as booleans, or roll out your own solution that would preserve original type. This helper class will be removed in the next ASF version, as we switched to the other converter instead.")]
[PublicAPI]
public sealed class BooleanNumberConverter : JsonConverter<bool> {
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
Expand Down
27 changes: 26 additions & 1 deletion ArchiSteamFarm/IPC/Controllers/Api/BotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -278,7 +279,31 @@ public async Task<ActionResult<GenericResponse>> InputPost(string botNames, [Fro
return Ok(results.All(static result => result) ? new GenericResponse(true) : new GenericResponse(false, Strings.WarningFailed));
}

[EndpointSummary("Fetches inventory of given bots")]
[EndpointSummary("Fetches general inventory information of given bots")]
[HttpGet("{botNames:required}/Inventory")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, ImmutableDictionary<uint, InventoryAppData>>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
public async Task<ActionResult<GenericResponse>> InventoryInfoGet(string botNames) {
ArgumentException.ThrowIfNullOrEmpty(botNames);

HashSet<Bot>? bots = Bot.GetBots(botNames);

if ((bots == null) || (bots.Count == 0)) {
return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames)));
}

IList<ImmutableDictionary<uint, InventoryAppData>?> results = await Utilities.InParallel(bots.Select(static bot => bot.ArchiWebHandler.GetInventoryContextData())).ConfigureAwait(false);

Dictionary<string, ImmutableDictionary<uint, InventoryAppData>?> result = new(bots.Count, Bot.BotsComparer);

foreach (Bot bot in bots) {
result[bot.BotName] = results[result.Count];
}

return Ok(new GenericResponse<IReadOnlyDictionary<string, ImmutableDictionary<uint, InventoryAppData>?>>(result));
}

[EndpointSummary("Fetches specific inventory of given bots")]
[HttpGet("{botNames:required}/Inventory/{appID}/{contextID}")]
[ProducesResponseType<GenericResponse<IReadOnlyDictionary<string, BotInventoryResponse>>>((int) HttpStatusCode.OK)]
[ProducesResponseType<GenericResponse>((int) HttpStatusCode.BadRequest)]
Expand Down
4 changes: 4 additions & 0 deletions ArchiSteamFarm/Localization/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@ Process uptime: {1}</value>
<value>Bot has level {0}.</value>
<comment>{0} will be replaced by bot's level</comment>
</data>
<data name="BotInventory" xml:space="preserve">
<value>{0}/{1} ({2}/{3}): {4} assets</value>
<comment>{0} will be replaced by appID (number), {1} will be replaced by contextID (number), {2} will be replaced by app's name (string), {3} will be replaced by name of the context (string), {4} will be replaced by number of assets in the specified inventory (number).</comment>
</data>
<data name="ActivelyMatchingItems" xml:space="preserve">
<value>Matching Steam items, round #{0}...</value>
<comment>{0} will be replaced by round number</comment>
Expand Down
91 changes: 91 additions & 0 deletions ArchiSteamFarm/Steam/Data/InventoryAppData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Łukasz "JustArchi" Domeradzki
// Contact: [email protected]
// |
// 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;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Helpers.Json;

namespace ArchiSteamFarm.Steam.Data;

public sealed class InventoryAppData {
[JsonInclude]
[JsonPropertyName("appid")]
[JsonRequired]
public uint AppID { get; private init; }

[JsonInclude]
[JsonPropertyName("asset_count")]
[JsonRequired]
public uint AssetsCount { get; private init; }

[JsonInclude]
[JsonPropertyName("rgContexts")]
[JsonRequired]
public ImmutableDictionary<ulong, InventoryContextData> Contexts { get; private init; } = ImmutableDictionary<ulong, InventoryContextData>.Empty;

[JsonInclude]
[JsonPropertyName("icon")]
[JsonRequired]
public Uri Icon { get; private init; } = null!;

[JsonInclude]
[JsonPropertyName("inventory_logo")]
public Uri? InventoryLogo { get; private init; }

[JsonInclude]
[JsonPropertyName("link")]
[JsonRequired]
public Uri Link { get; private init; } = null!;

// This seems to be rendered as number always, but who knows, better treat it like other brain damages in this response
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("load_failed")]
[JsonRequired]
public bool LoadFailed { get; private init; }

[JsonInclude]
[JsonPropertyName("name")]
[JsonRequired]
public string Name { get; private init; } = "";

// Steam renders this sometimes as a number, sometimes as a boolean, because fuck you, that's why
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("owner_only")]
[JsonRequired]
public bool OwnerOnly { get; private init; }

// Steam renders this sometimes as a string, sometimes as a boolean, because fuck you, that's why
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("store_vetted")]
[JsonRequired]
public bool StoreVetted { get; private init; }

[JsonInclude]
[JsonPropertyName("trade_permissions")]
[JsonRequired]
public string TradePermissions { get; private init; } = "";
}
44 changes: 44 additions & 0 deletions ArchiSteamFarm/Steam/Data/InventoryContextData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Łukasz "JustArchi" Domeradzki
// Contact: [email protected]
// |
// 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.Text.Json.Serialization;

namespace ArchiSteamFarm.Steam.Data;

public sealed class InventoryContextData {
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("id")]
[JsonRequired]
public ulong ID { get; private init; }

[JsonInclude]
[JsonPropertyName("name")]
[JsonRequired]
public string Name { get; private init; } = "";

[JsonInclude]
[JsonPropertyName("asset_count")]
[JsonRequired]
public uint AssetsCount { get; private init; }
}
8 changes: 4 additions & 4 deletions ArchiSteamFarm/Steam/Data/InventoryDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ public ulong ClassID {
private init => Body.classid = value;
}

[JsonConverter(typeof(BooleanNumberConverter))]
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("commodity")]
public bool Commodity {
get => Body.commodity;
private init => Body.commodity = value;
}

[JsonConverter(typeof(BooleanNumberConverter))]
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("currency")]
public bool Currency {
Expand Down Expand Up @@ -127,7 +127,7 @@ public ulong InstanceID {
private init => Body.instanceid = value;
}

[JsonConverter(typeof(BooleanNumberConverter))]
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("marketable")]
[JsonRequired]
Expand Down Expand Up @@ -340,7 +340,7 @@ private init {
}
}

[JsonConverter(typeof(BooleanNumberConverter))]
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("tradable")]
[JsonRequired]
Expand Down
2 changes: 1 addition & 1 deletion ArchiSteamFarm/Steam/Data/InventoryResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ internal EResult? ErrorCode {
[JsonPropertyName("last_assetid")]
internal ulong LastAssetID { get; private init; }

[JsonConverter(typeof(BooleanNumberConverter))]
[JsonConverter(typeof(BooleanNormalizationConverter))]
[JsonInclude]
[JsonPropertyName("more_items")]
internal bool MoreItems { get; private init; }
Expand Down
53 changes: 53 additions & 0 deletions ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,59 @@ public async IAsyncEnumerable<Asset> GetInventoryAsync(ulong steamID = 0, uint a
}
}

[PublicAPI]
public async Task<ImmutableDictionary<uint, InventoryAppData>?> GetInventoryContextData() {
Uri request = new(SteamCommunityURL, "/my/inventory?l=english");

using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);

INode? htmlNode = response?.Content?.SelectSingleNode("//div[@role='main']/script[contains(., 'g_rgAppContextData')]");

if (htmlNode == null) {
return null;
}

string text = htmlNode.TextContent;

if (string.IsNullOrEmpty(text)) {
Bot.ArchiLogger.LogNullError(text);

return null;
}

const string appContextDataVariableName = "g_rgAppContextData = {";

int startIndex = text.IndexOf(appContextDataVariableName, StringComparison.Ordinal);

if (startIndex < 0) {
Bot.ArchiLogger.LogNullError(startIndex);

return null;
}

startIndex += appContextDataVariableName.Length - 1;

int endIndex = text.IndexOf("};", startIndex, StringComparison.Ordinal);

if (endIndex < 0) {
Bot.ArchiLogger.LogNullError(endIndex);

return null;
}

endIndex++;

text = text[startIndex..endIndex];

try {
return text.ToJsonObject<ImmutableDictionary<uint, InventoryAppData>>();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);

return null;
}
}

[PublicAPI]
public async Task<HashSet<TradeOffer>?> GetTradeOffers(bool? activeOffers = null, bool? receivedOffers = null, bool? sentOffers = null, bool? withDescriptions = null) {
if ((receivedOffers == false) && (sentOffers == false)) {
Expand Down
Loading

0 comments on commit 3f079a8

Please sign in to comment.