Inventory fetching through CM (#3155)

* New inventory fetching

* use new method everywhere

* Store description in the asset, add protobuf body as a backing field for InventoryDescription, add properties to description

* parse trade offers as json, stub descriptions, fix build

* formatting, misc fixes

* fix pragma comments

* fix passing tradable property

* fix convesion of assets, add compatibility method

* fix fetching tradeoffers

* use 40k as default count per request

* throw an exception instead of silencing the error
This commit is contained in:
Vita Chumakova 2024-03-17 02:57:25 +04:00 committed by GitHub
parent aedede3ba4
commit 184232995d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 835 additions and 511 deletions

View file

@ -40,5 +40,5 @@ internal class AssetInInventory : AssetForMatching {
AssetID = asset.AssetID; AssetID = asset.AssetID;
} }
internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, tradable: Tradable, assetID: AssetID, realAppID: RealAppID, type: Type, rarity: Rarity); internal Asset ToAsset() => new(Asset.SteamAppID, Asset.SteamCommunityContextID, ClassID, Amount, new InventoryDescription { ProtobufBody = { tradable = Tradable } }, assetID: AssetID, realAppID: RealAppID, type: Type, rarity: Rarity);
} }

View file

@ -250,7 +250,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
List<Asset> inventory; List<Asset> inventory;
try { try {
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().ToListAsync().ConfigureAwait(false); inventory = await Bot.ArchiHandler.GetMyInventoryAsync().ToListAsync().ConfigureAwait(false);
} catch (HttpRequestException e) { } catch (HttpRequestException e) {
// 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
ShouldSendHeartBeats = false; ShouldSendHeartBeats = false;
@ -937,7 +937,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
HashSet<Asset> assetsForMatching; HashSet<Asset> assetsForMatching;
try { try {
assetsForMatching = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false); assetsForMatching = await Bot.ArchiHandler.GetMyInventoryAsync().Where(item => item is { AssetID: > 0, Amount: > 0, ClassID: > 0, RealAppID: > 0, Type: > Asset.EType.Unknown, Rarity: > Asset.ERarity.Unknown, IsSteamPointsShopItem: false } && acceptedMatchableTypes.Contains(item.Type) && !Bot.BotDatabase.MatchActivelyBlacklistAppIDs.Contains(item.RealAppID)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) { } catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e); Bot.ArchiLogger.LogGenericWarningException(e);
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(assetsForMatching))); Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(assetsForMatching)));
@ -1504,10 +1504,10 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
// However, since this is only an assumption, we must mark newly acquired items as untradable so we're sure that they're not considered for trading, only for matching // However, since this is only an assumption, we must mark newly acquired items as untradable so we're sure that they're not considered for trading, only for matching
foreach (Asset itemToReceive in itemsToReceive) { foreach (Asset itemToReceive in itemsToReceive) {
if (ourInventory.TryGetValue(itemToReceive.AssetID, out Asset? item)) { if (ourInventory.TryGetValue(itemToReceive.AssetID, out Asset? item)) {
item.Tradable = false; item.Description.ProtobufBody.tradable = false;
item.Amount += itemToReceive.Amount; item.Amount += itemToReceive.Amount;
} else { } else {
itemToReceive.Tradable = false; itemToReceive.Description.ProtobufBody.tradable = false;
ourInventory[itemToReceive.AssetID] = itemToReceive; ourInventory[itemToReceive.AssetID] = itemToReceive;
} }

View file

@ -489,7 +489,7 @@ public sealed class Bot {
Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality))); Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
} }
private static Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity); private static Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(), realAppID, type, rarity);
private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems); private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems);

View file

@ -426,5 +426,5 @@ public sealed class Trading {
Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive)); Assert.IsTrue(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
} }
private static Asset CreateItem(ulong classID, uint amount = 1, uint realAppID = Asset.SteamAppID, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity); private static Asset CreateItem(ulong classID, uint amount = 1, uint realAppID = Asset.SteamAppID, Asset.EType type = Asset.EType.TradingCard, Asset.ERarity rarity = Asset.ERarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(), realAppID, type, rarity);
} }

View file

@ -0,0 +1,44 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Ł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;
using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Helpers.Json;
[PublicAPI]
public sealed class BooleanNumberConverter : 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,
_ => throw new JsonException()
};
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) {
ArgumentNullException.ThrowIfNull(writer);
writer.WriteNumberValue(value ? 1 : 0);
}
}

View file

@ -3640,8 +3640,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
HashSet<Asset> inventory; HashSet<Asset> inventory;
try { try {
inventory = await ArchiWebHandler.GetInventoryAsync() inventory = await ArchiHandler.GetMyInventoryAsync(tradableOnly: true)
.Where(item => item.Tradable && appIDs.Contains(item.RealAppID) && BotConfig.CompleteTypesToSend.Contains(item.Type)) .Where(item => appIDs.Contains(item.RealAppID) && BotConfig.CompleteTypesToSend.Contains(item.Type))
.ToHashSetAsync() .ToHashSetAsync()
.ConfigureAwait(false); .ConfigureAwait(false);
} catch (HttpRequestException e) { } catch (HttpRequestException e) {

View file

@ -0,0 +1,31 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Ł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.Text.Json.Serialization;
namespace ArchiSteamFarm.Steam.Data;
public class APIWrappedResponse<T> where T : class {
[JsonInclude]
[JsonPropertyName("response")]
[JsonRequired]
public T Response { get; private init; } = null!;
}

View file

@ -20,9 +20,7 @@
// limitations under the License. // limitations under the License.
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -41,11 +39,40 @@ public sealed class Asset {
[JsonIgnore] [JsonIgnore]
[PublicAPI] [PublicAPI]
public IReadOnlyDictionary<string, JsonElement>? AdditionalPropertiesReadOnly => AdditionalProperties; public bool IsSteamPointsShopItem => !Tradable && (InstanceID == SteamPointsShopInstanceID);
[JsonIgnore] [JsonIgnore]
[PublicAPI] [PublicAPI]
public bool IsSteamPointsShopItem => !Tradable && (InstanceID == SteamPointsShopInstanceID); public bool Marketable => Description.Marketable;
[JsonIgnore]
[PublicAPI]
public ERarity Rarity => OverriddenRarity ?? Description.Rarity;
[JsonIgnore]
[PublicAPI]
public uint RealAppID => OverriddenRealAppID ?? Description.RealAppID;
[JsonIgnore]
[PublicAPI]
public ImmutableHashSet<Tag> Tags => Description.Tags;
[JsonIgnore]
[PublicAPI]
public bool Tradable => Description.Tradable;
[JsonIgnore]
[PublicAPI]
public EType Type => OverriddenType ?? Description.Type;
[JsonIgnore]
private ERarity? OverriddenRarity { get; }
[JsonIgnore]
private uint? OverriddenRealAppID { get; }
[JsonIgnore]
private EType? OverriddenType { get; }
[JsonInclude] [JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
@ -76,40 +103,15 @@ public sealed class Asset {
[PublicAPI] [PublicAPI]
public ulong ContextID { get; private init; } public ulong ContextID { get; private init; }
[PublicAPI]
public InventoryDescription Description { get; internal set; } = null!;
[JsonInclude] [JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("instanceid")] [JsonPropertyName("instanceid")]
[PublicAPI] [PublicAPI]
public ulong InstanceID { get; private init; } public ulong InstanceID { get; private init; }
[JsonIgnore]
[PublicAPI]
public bool Marketable { get; internal set; }
[JsonIgnore]
[PublicAPI]
public ERarity Rarity { get; internal set; }
[JsonIgnore]
[PublicAPI]
public uint RealAppID { get; internal set; }
[JsonIgnore]
[PublicAPI]
public ImmutableHashSet<Tag>? Tags { get; internal set; }
[JsonIgnore]
[PublicAPI]
public bool Tradable { get; internal set; }
[JsonIgnore]
[PublicAPI]
public EType Type { get; internal set; }
[JsonExtensionData]
[JsonInclude]
internal Dictionary<string, JsonElement>? AdditionalProperties { get; set; }
[JsonInclude] [JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("id")] [JsonPropertyName("id")]
@ -118,8 +120,15 @@ public sealed class Asset {
init => AssetID = value; init => AssetID = value;
} }
// Constructed from trades being received or plugins internal Asset(uint appID, ulong contextID, ulong classID, uint amount, InventoryDescription description, uint realAppID, EType? type, ERarity? rarity, ulong assetID = 0, ulong instanceID = 0) : this(appID, contextID, classID, amount, description, assetID, instanceID) {
public Asset(uint appID, ulong contextID, ulong classID, uint amount, ulong instanceID = 0, ulong assetID = 0, bool marketable = true, bool tradable = true, ImmutableHashSet<Tag>? tags = null, uint realAppID = 0, EType type = EType.Unknown, ERarity rarity = ERarity.Unknown) { ArgumentOutOfRangeException.ThrowIfZero(realAppID);
OverriddenRealAppID = realAppID;
OverriddenType = type;
OverriddenRarity = rarity;
}
internal Asset(uint appID, ulong contextID, ulong classID, uint amount, InventoryDescription description, ulong assetID = 0, ulong instanceID = 0) {
ArgumentOutOfRangeException.ThrowIfZero(appID); ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(contextID); ArgumentOutOfRangeException.ThrowIfZero(contextID);
ArgumentOutOfRangeException.ThrowIfZero(classID); ArgumentOutOfRangeException.ThrowIfZero(classID);
@ -129,17 +138,9 @@ public sealed class Asset {
ContextID = contextID; ContextID = contextID;
ClassID = classID; ClassID = classID;
Amount = amount; Amount = amount;
Description = description;
InstanceID = instanceID; InstanceID = instanceID;
AssetID = assetID; AssetID = assetID;
Marketable = marketable;
Tradable = tradable;
RealAppID = realAppID;
Type = type;
Rarity = rarity;
if (tags?.Count > 0) {
Tags = tags;
}
} }
[JsonConstructor] [JsonConstructor]

View file

@ -0,0 +1,364 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Ł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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.Localization;
using JetBrains.Annotations;
using SteamKit2.Internal;
namespace ArchiSteamFarm.Steam.Data;
[PublicAPI]
public sealed class InventoryDescription {
[JsonIgnore]
public CEconItem_Description ProtobufBody { get; } = new();
internal Asset.ERarity Rarity {
get {
foreach (Tag tag in Tags) {
switch (tag.Identifier) {
case "droprate":
switch (tag.Value) {
case "droprate_0":
return Asset.ERarity.Common;
case "droprate_1":
return Asset.ERarity.Uncommon;
case "droprate_2":
return Asset.ERarity.Rare;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
break;
}
break;
}
}
return Asset.ERarity.Unknown;
}
}
internal uint RealAppID {
get {
foreach (Tag tag in Tags) {
switch (tag.Identifier) {
case "Game":
if (string.IsNullOrEmpty(tag.Value) || (tag.Value.Length <= 4) || !tag.Value.StartsWith("app_", StringComparison.Ordinal)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
break;
}
string appIDText = tag.Value[4..];
if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) {
ASF.ArchiLogger.LogNullError(appID);
break;
}
return appID;
}
}
return 0;
}
}
internal Asset.EType Type {
get {
Asset.EType type = Asset.EType.Unknown;
foreach (Tag tag in Tags) {
switch (tag.Identifier) {
case "cardborder":
switch (tag.Value) {
case "cardborder_0":
return Asset.EType.TradingCard;
case "cardborder_1":
return Asset.EType.FoilTradingCard;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
return Asset.EType.Unknown;
}
case "item_class":
switch (tag.Value) {
case "item_class_2":
if (type == Asset.EType.Unknown) {
// This is a fallback in case we'd have no cardborder available to interpret
type = Asset.EType.TradingCard;
}
continue;
case "item_class_3":
return Asset.EType.ProfileBackground;
case "item_class_4":
return Asset.EType.Emoticon;
case "item_class_5":
return Asset.EType.BoosterPack;
case "item_class_6":
return Asset.EType.Consumable;
case "item_class_7":
return Asset.EType.SteamGems;
case "item_class_8":
return Asset.EType.ProfileModifier;
case "item_class_10":
return Asset.EType.SaleItem;
case "item_class_11":
return Asset.EType.Sticker;
case "item_class_12":
return Asset.EType.ChatEffect;
case "item_class_13":
return Asset.EType.MiniProfileBackground;
case "item_class_14":
return Asset.EType.AvatarProfileFrame;
case "item_class_15":
return Asset.EType.AnimatedAvatar;
case "item_class_16":
return Asset.EType.KeyboardSkin;
case "item_class_17":
return Asset.EType.StartupVideo;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
return Asset.EType.Unknown;
}
}
}
return type;
}
}
[JsonInclude]
[JsonPropertyName("appid")]
[JsonRequired]
public uint AppID {
get => (uint) ProtobufBody.appid;
private init => ProtobufBody.appid = (int) value;
}
[JsonInclude]
[JsonPropertyName("background_color")]
public string BackgroundColor {
get => ProtobufBody.background_color;
private init => ProtobufBody.background_color = value;
}
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("classid")]
[JsonRequired]
public ulong ClassID {
get => ProtobufBody.classid;
private init => ProtobufBody.classid = value;
}
[JsonInclude]
[JsonPropertyName("commodity")]
[JsonConverter(typeof(BooleanNumberConverter))]
public bool Commodity {
get => ProtobufBody.commodity;
private init => ProtobufBody.commodity = value;
}
[JsonInclude]
[JsonPropertyName("currency")]
[JsonConverter(typeof(BooleanNumberConverter))]
public bool Currency {
get => ProtobufBody.currency;
private init => ProtobufBody.currency = value;
}
[JsonInclude]
[JsonPropertyName("descriptions")]
public ImmutableHashSet<ItemDescription> Descriptions {
get => ProtobufBody.descriptions.Select(static description => new ItemDescription(description.type, description.value, description.color, description.label)).ToImmutableHashSet();
private init {
ProtobufBody.descriptions.Clear();
foreach (ItemDescription description in value) {
ProtobufBody.descriptions.Add(
new CEconItem_DescriptionLine {
color = description.Color,
label = description.Label,
type = description.Type,
value = description.Value
}
);
}
}
}
[JsonInclude]
[JsonPropertyName("icon_url")]
#pragma warning disable CA1056 // this is a JSON/Protobuf field, and even then it doesn't contain full URL
public string IconURL {
#pragma warning restore CA1056 // this is a JSON/Protobuf field, and even then it doesn't contain full URL
get => ProtobufBody.icon_url;
private init => ProtobufBody.icon_url = value;
}
[JsonInclude]
[JsonPropertyName("icon_url_large")]
#pragma warning disable CA1056 // this is a JSON/Protobuf field, and even then it doesn't contain full URL
public string IconURLLarge {
#pragma warning restore CA1056 // this is a JSON/Protobuf field, and even then it doesn't contain full URL
get => ProtobufBody.icon_url_large;
private init => ProtobufBody.icon_url_large = value;
}
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("instanceid")]
public ulong InstanceID {
get => ProtobufBody.instanceid;
private init => ProtobufBody.instanceid = value;
}
[JsonInclude]
[JsonPropertyName("marketable")]
[JsonRequired]
[JsonConverter(typeof(BooleanNumberConverter))]
public bool Marketable {
get => ProtobufBody.marketable;
private init => ProtobufBody.marketable = value;
}
[JsonInclude]
[JsonPropertyName("market_fee_app")]
public uint MarketFeeApp {
get => (uint) ProtobufBody.market_fee_app;
private init => ProtobufBody.market_fee_app = (int) value;
}
[JsonInclude]
[JsonPropertyName("market_hash_name")]
public string MarketHashName {
get => ProtobufBody.market_hash_name;
private init => ProtobufBody.market_hash_name = value;
}
[JsonInclude]
[JsonPropertyName("market_name")]
public string MarketName {
get => ProtobufBody.market_name;
private init => ProtobufBody.market_name = value;
}
[JsonInclude]
[JsonPropertyName("name")]
public string Name {
get => ProtobufBody.name;
private init => ProtobufBody.name = value;
}
[JsonInclude]
[JsonPropertyName("owner_actions")]
public ImmutableHashSet<ItemAction> OwnerActions {
get => ProtobufBody.owner_actions.Select(static action => new ItemAction(action.link, action.name)).ToImmutableHashSet();
private init {
ProtobufBody.owner_actions.Clear();
foreach (ItemAction action in value) {
ProtobufBody.owner_actions.Add(
new CEconItem_Action {
link = action.Link,
name = action.Name
}
);
}
}
}
[JsonInclude]
[JsonPropertyName("type")]
public string TypeText {
get => ProtobufBody.type;
private init => ProtobufBody.type = value;
}
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("tags")]
internal ImmutableHashSet<Tag> Tags {
get => ProtobufBody.tags.Select(static x => new Tag(x.category, x.internal_name, x.localized_category_name, x.localized_tag_name)).ToImmutableHashSet();
private init {
ProtobufBody.tags.Clear();
foreach (Tag tag in value) {
ProtobufBody.tags.Add(
new CEconItem_Tag {
appid = AppID,
category = tag.Identifier,
color = tag.Color,
internal_name = tag.Value,
localized_category_name = tag.LocalizedIdentifier,
localized_tag_name = tag.LocalizedValue
}
);
}
}
}
[JsonInclude]
[JsonPropertyName("tradable")]
[JsonRequired]
[JsonConverter(typeof(BooleanNumberConverter))]
internal bool Tradable {
get => ProtobufBody.tradable;
private init => ProtobufBody.tradable = value;
}
// For stubs and deserialization
[JsonConstructor]
internal InventoryDescription() { }
// Constructed from trades being received/sent
internal InventoryDescription(uint appID, ulong classID, ulong instanceID, bool marketable, bool tradable, IReadOnlyCollection<Tag>? tags = null) {
ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(classID);
AppID = appID;
ClassID = classID;
InstanceID = instanceID;
Marketable = marketable;
Tradable = tradable;
if (tags?.Count > 0) {
Tags = tags.ToImmutableHashSet();
}
}
internal InventoryDescription(CEconItem_Description description) => ProtobufBody = description;
[UsedImplicitly]
public static bool ShouldSerializeAdditionalProperties() => false;
}

View file

@ -20,17 +20,11 @@
// limitations under the License. // limitations under the License.
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Helpers.Json;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Steam.Integration;
using JetBrains.Annotations;
using SteamKit2; using SteamKit2;
namespace ArchiSteamFarm.Steam.Data; namespace ArchiSteamFarm.Steam.Data;
@ -45,7 +39,7 @@ internal sealed class InventoryResponse : OptionalResultResponse {
[JsonDisallowNull] [JsonDisallowNull]
[JsonInclude] [JsonInclude]
[JsonPropertyName("descriptions")] [JsonPropertyName("descriptions")]
internal ImmutableHashSet<Description> Descriptions { get; private init; } = ImmutableHashSet<Description>.Empty; internal ImmutableHashSet<InventoryDescription> Descriptions { get; private init; } = ImmutableHashSet<InventoryDescription>.Empty;
internal EResult? ErrorCode { get; private init; } internal EResult? ErrorCode { get; private init; }
internal string? ErrorText { get; private init; } internal string? ErrorText { get; private init; }
@ -55,6 +49,9 @@ internal sealed class InventoryResponse : OptionalResultResponse {
[JsonPropertyName("last_assetid")] [JsonPropertyName("last_assetid")]
internal ulong LastAssetID { get; private init; } internal ulong LastAssetID { get; private init; }
[JsonInclude]
[JsonPropertyName("more_items")]
[JsonConverter(typeof(BooleanNumberConverter))]
internal bool MoreItems { get; private init; } internal bool MoreItems { get; private init; }
[JsonInclude] [JsonInclude]
@ -75,201 +72,6 @@ internal sealed class InventoryResponse : OptionalResultResponse {
} }
} }
[JsonInclude]
[JsonPropertyName("more_items")]
private byte MoreItemsNumber {
get => MoreItems ? (byte) 1 : (byte) 0;
init => MoreItems = value > 0;
}
[JsonConstructor] [JsonConstructor]
private InventoryResponse() { } private InventoryResponse() { }
internal sealed class Description {
internal Asset.ERarity Rarity {
get {
foreach (Tag tag in Tags) {
switch (tag.Identifier) {
case "droprate":
switch (tag.Value) {
case "droprate_0":
return Asset.ERarity.Common;
case "droprate_1":
return Asset.ERarity.Uncommon;
case "droprate_2":
return Asset.ERarity.Rare;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
break;
}
break;
}
}
return Asset.ERarity.Unknown;
}
}
internal uint RealAppID {
get {
foreach (Tag tag in Tags) {
switch (tag.Identifier) {
case "Game":
if (string.IsNullOrEmpty(tag.Value) || (tag.Value.Length <= 4) || !tag.Value.StartsWith("app_", StringComparison.Ordinal)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
break;
}
string appIDText = tag.Value[4..];
if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) {
ASF.ArchiLogger.LogNullError(appID);
break;
}
return appID;
}
}
return 0;
}
}
internal Asset.EType Type {
get {
Asset.EType type = Asset.EType.Unknown;
foreach (Tag tag in Tags) {
switch (tag.Identifier) {
case "cardborder":
switch (tag.Value) {
case "cardborder_0":
return Asset.EType.TradingCard;
case "cardborder_1":
return Asset.EType.FoilTradingCard;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
return Asset.EType.Unknown;
}
case "item_class":
switch (tag.Value) {
case "item_class_2":
if (type == Asset.EType.Unknown) {
// This is a fallback in case we'd have no cardborder available to interpret
type = Asset.EType.TradingCard;
}
continue;
case "item_class_3":
return Asset.EType.ProfileBackground;
case "item_class_4":
return Asset.EType.Emoticon;
case "item_class_5":
return Asset.EType.BoosterPack;
case "item_class_6":
return Asset.EType.Consumable;
case "item_class_7":
return Asset.EType.SteamGems;
case "item_class_8":
return Asset.EType.ProfileModifier;
case "item_class_10":
return Asset.EType.SaleItem;
case "item_class_11":
return Asset.EType.Sticker;
case "item_class_12":
return Asset.EType.ChatEffect;
case "item_class_13":
return Asset.EType.MiniProfileBackground;
case "item_class_14":
return Asset.EType.AvatarProfileFrame;
case "item_class_15":
return Asset.EType.AnimatedAvatar;
case "item_class_16":
return Asset.EType.KeyboardSkin;
case "item_class_17":
return Asset.EType.StartupVideo;
default:
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(tag.Value), tag.Value));
return Asset.EType.Unknown;
}
}
}
return type;
}
}
[JsonExtensionData]
[JsonInclude]
internal Dictionary<string, JsonElement>? AdditionalProperties { get; private init; }
[JsonInclude]
[JsonPropertyName("appid")]
[JsonRequired]
internal uint AppID { get; private init; }
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("classid")]
[JsonRequired]
internal ulong ClassID { get; private init; }
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("instanceid")]
internal ulong InstanceID { get; private init; }
internal bool Marketable { get; private init; }
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("tags")]
internal ImmutableHashSet<Tag> Tags { get; private init; } = ImmutableHashSet<Tag>.Empty;
internal bool Tradable { get; private init; }
[JsonInclude]
[JsonPropertyName("marketable")]
[JsonRequired]
private byte MarketableNumber {
get => Marketable ? (byte) 1 : (byte) 0;
init => Marketable = value > 0;
}
[JsonInclude]
[JsonPropertyName("tradable")]
[JsonRequired]
private byte TradableNumber {
get => Tradable ? (byte) 1 : (byte) 0;
init => Tradable = value > 0;
}
// Constructed from trades being received/sent
internal Description(uint appID, ulong classID, ulong instanceID, bool marketable, IReadOnlyCollection<Tag>? tags = null) {
ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(classID);
AppID = appID;
ClassID = classID;
InstanceID = instanceID;
Marketable = marketable;
Tradable = true;
if (tags?.Count > 0) {
Tags = tags.ToImmutableHashSet();
}
}
[JsonConstructor]
private Description() { }
[UsedImplicitly]
public static bool ShouldSerializeAdditionalProperties() => false;
}
} }

View file

@ -0,0 +1,45 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Ł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.Text.Json.Serialization;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Steam.Data;
public class ItemAction {
[JsonInclude]
[JsonPropertyName("link")]
[PublicAPI]
public string Link { get; private init; } = null!;
[JsonInclude]
[JsonPropertyName("name")]
[PublicAPI]
public string Name { get; private init; } = null!;
internal ItemAction(string link, string name) {
Link = link;
Name = name;
}
[JsonConstructor]
private ItemAction() { }
}

View file

@ -0,0 +1,60 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Ł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.Text.Json.Serialization;
using ArchiSteamFarm.Helpers.Json;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Steam.Data;
public class ItemDescription {
[JsonInclude]
[JsonPropertyName("color")]
[PublicAPI]
public string? Color { get; private init; }
[JsonInclude]
[JsonPropertyName("label")]
[PublicAPI]
public string? Label { get; private init; }
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("type")]
[PublicAPI]
public string Type { get; private init; } = null!;
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("value")]
[PublicAPI]
public string Value { get; private init; } = null!;
internal ItemDescription(string type, string value, string color, string label) {
Type = type;
Value = value;
Color = color;
Label = label;
}
[JsonConstructor]
private ItemDescription() { }
}

View file

@ -32,18 +32,38 @@ public sealed class Tag {
[PublicAPI] [PublicAPI]
public string Identifier { get; private init; } = ""; public string Identifier { get; private init; } = "";
[JsonInclude]
[JsonPropertyName("localized_category_name")]
[JsonRequired]
[PublicAPI]
public string LocalizedIdentifier { get; private init; } = "";
[JsonInclude]
[JsonPropertyName("localized_tag_name")]
[JsonRequired]
[PublicAPI]
public string LocalizedValue { get; private init; } = "";
[JsonInclude] [JsonInclude]
[JsonPropertyName("internal_name")] [JsonPropertyName("internal_name")]
[JsonRequired] [JsonRequired]
[PublicAPI] [PublicAPI]
public string Value { get; private init; } = ""; public string Value { get; private init; } = "";
internal Tag(string identifier, string value) { [JsonInclude]
[JsonPropertyName("color")]
[PublicAPI]
public string? Color { get; private init; }
internal Tag(string identifier, string value, string localizedIdentifier, string localizedValue, string? color = null) {
ArgumentException.ThrowIfNullOrEmpty(identifier); ArgumentException.ThrowIfNullOrEmpty(identifier);
ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(value);
Identifier = identifier; Identifier = identifier;
Value = value; Value = value;
LocalizedIdentifier = localizedIdentifier;
LocalizedValue = localizedValue;
Color = color;
} }
[JsonConstructor] [JsonConstructor]

View file

@ -21,14 +21,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Helpers.Json;
using JetBrains.Annotations; using JetBrains.Annotations;
using SteamKit2; using SteamKit2;
namespace ArchiSteamFarm.Steam.Data; namespace ArchiSteamFarm.Steam.Data;
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
public sealed class TradeOffer { public sealed class TradeOffer {
[PublicAPI] [PublicAPI]
public IReadOnlyCollection<Asset> ItemsToGiveReadOnly => ItemsToGive; public IReadOnlyCollection<Asset> ItemsToGiveReadOnly => ItemsToGive;
@ -36,31 +39,37 @@ public sealed class TradeOffer {
[PublicAPI] [PublicAPI]
public IReadOnlyCollection<Asset> ItemsToReceiveReadOnly => ItemsToReceive; public IReadOnlyCollection<Asset> ItemsToReceiveReadOnly => ItemsToReceive;
internal readonly HashSet<Asset> ItemsToGive = [];
internal readonly HashSet<Asset> ItemsToReceive = [];
[PublicAPI] [PublicAPI]
public ulong OtherSteamID64 { get; private set; } public ulong OtherSteamID64 { get; private set; }
[JsonInclude]
[JsonPropertyName("trade_offer_state")]
[PublicAPI] [PublicAPI]
public ETradeOfferState State { get; private set; } public ETradeOfferState State { get; private set; }
[JsonInclude]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
[JsonPropertyName("tradeofferid")]
[PublicAPI] [PublicAPI]
public ulong TradeOfferID { get; private set; } public ulong TradeOfferID { get; private set; }
// Constructed from trades being received [JsonDisallowNull]
internal TradeOffer(ulong tradeOfferID, uint otherSteamID3, ETradeOfferState state) { [JsonInclude]
ArgumentOutOfRangeException.ThrowIfZero(tradeOfferID); [JsonPropertyName("items_to_give")]
ArgumentOutOfRangeException.ThrowIfZero(otherSteamID3); internal HashSet<Asset> ItemsToGive { get; private init; } = [];
if (!Enum.IsDefined(state)) { [JsonDisallowNull]
throw new InvalidEnumArgumentException(nameof(state), (int) state, typeof(ETradeOfferState)); [JsonInclude]
} [JsonPropertyName("items_to_receive")]
internal HashSet<Asset> ItemsToReceive { get; private init; } = [];
TradeOfferID = tradeOfferID; [JsonInclude]
OtherSteamID64 = new SteamID(otherSteamID3, EUniverse.Public, EAccountType.Individual); [JsonPropertyName("accountid_other")]
State = state; [JsonRequired]
} private uint OtherSteamID3 { init => OtherSteamID64 = new SteamID(value, EUniverse.Public, EAccountType.Individual); }
[JsonConstructor]
private TradeOffer() { }
[PublicAPI] [PublicAPI]
public bool IsValidSteamItemsRequest(IReadOnlyCollection<Asset.EType> acceptedTypes) { public bool IsValidSteamItemsRequest(IReadOnlyCollection<Asset.EType> acceptedTypes) {

View file

@ -0,0 +1,43 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2024 Ł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.Immutable;
using System.Text.Json.Serialization;
using ArchiSteamFarm.Helpers.Json;
namespace ArchiSteamFarm.Steam.Data;
public class TradeOffersResponse {
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("descriptions")]
public ImmutableHashSet<InventoryDescription> Descriptions { get; private init; } = [];
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("trade_offers_received")]
public ImmutableHashSet<TradeOffer> TradeOffersReceived { get; private init; } = [];
[JsonDisallowNull]
[JsonInclude]
[JsonPropertyName("trade_offers_sent")]
public ImmutableHashSet<TradeOffer> TradeOffersSent { get; private init; } = [];
}

View file

@ -639,7 +639,7 @@ public sealed class Trading : IDisposable {
HashSet<Asset> inventory; HashSet<Asset> inventory;
try { try {
inventory = await Bot.ArchiWebHandler.GetInventoryAsync().Where(item => !item.IsSteamPointsShopItem && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity))).ToHashSetAsync().ConfigureAwait(false); inventory = await Bot.ArchiHandler.GetMyInventoryAsync().Where(item => !item.IsSteamPointsShopItem && wantedSets.Contains((item.RealAppID, item.Type, item.Rarity))).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) { } catch (HttpRequestException e) {
// If we can't check our inventory when not using MatchEverything, this is a temporary failure, try again later // If we can't check our inventory when not using MatchEverything, this is a temporary failure, try again later
Bot.ArchiLogger.LogGenericWarningException(e); Bot.ArchiLogger.LogGenericWarningException(e);

View file

@ -22,10 +22,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ArchiSteamFarm.Core; using ArchiSteamFarm.Core;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog; using ArchiSteamFarm.NLog;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Integration.Callbacks; using ArchiSteamFarm.Steam.Integration.Callbacks;
using ArchiSteamFarm.Steam.Integration.CMsgs; using ArchiSteamFarm.Steam.Integration.CMsgs;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -151,6 +154,99 @@ public sealed class ArchiHandler : ClientMsgHandler {
return response.Result == EResult.OK ? response.GetDeserializedResponse<CCredentials_LastCredentialChangeTime_Response>() : null; return response.Result == EResult.OK ? response.GetDeserializedResponse<CCredentials_LastCredentialChangeTime_Response>() : null;
} }
[PublicAPI]
public async IAsyncEnumerable<Asset> GetMyInventoryAsync(uint appID = Asset.SteamAppID, ulong contextID = Asset.SteamCommunityContextID, bool tradableOnly = false, bool marketableOnly = false, ushort itemsCountPerRequest = 40000) {
ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(contextID);
if (Client.SteamID == null) {
throw new InvalidOperationException(nameof(Client.SteamID));
}
SteamID steamID = Client.SteamID;
// We need to store asset IDs to make sure we won't get duplicate items
HashSet<ulong>? assetIDs = null;
ulong startAssetID = 0;
while (true) {
ulong currentStartAssetID = startAssetID;
CEcon_GetInventoryItemsWithDescriptions_Request request = new() {
appid = appID,
contextid = contextID,
filters = new CEcon_GetInventoryItemsWithDescriptions_Request.FilterOptions {
tradable_only = tradableOnly,
marketable_only = marketableOnly
},
get_descriptions = true,
steamid = steamID.ConvertToUInt64(),
start_assetid = currentStartAssetID,
count = itemsCountPerRequest
};
SteamUnifiedMessages.ServiceMethodResponse genericResponse = await UnifiedEconService
.SendMessage(x => x.GetInventoryItemsWithDescriptions(request))
.ToLongRunningTask()
.ConfigureAwait(false);
CEcon_GetInventoryItemsWithDescriptions_Response response = genericResponse.GetDeserializedResponse<CEcon_GetInventoryItemsWithDescriptions_Response>();
if ((response.total_inventory_count == 0) || (response.assets.Count == 0)) {
// Empty inventory
yield break;
}
if (response.descriptions.Count == 0) {
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(response.descriptions)));
}
if (response.total_inventory_count > Array.MaxLength) {
throw new InvalidOperationException(nameof(response.total_inventory_count));
}
assetIDs ??= new HashSet<ulong>((int) response.total_inventory_count);
if ((response.assets.Count == 0) || (response.descriptions.Count == 0)) {
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, $"{nameof(response.assets)} || {nameof(response.descriptions)}"));
}
List<InventoryDescription> convertedDescriptions = response.descriptions.Select(static description => new InventoryDescription(description)).ToList();
Dictionary<(ulong ClassID, ulong InstanceID), InventoryDescription> descriptions = new();
foreach (InventoryDescription description in convertedDescriptions) {
if (description.ClassID == 0) {
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(description.ClassID)));
}
(ulong ClassID, ulong InstanceID) key = (description.ClassID, description.InstanceID);
descriptions.TryAdd(key, description);
}
foreach (CEcon_Asset? asset in response.assets) {
if (!descriptions.TryGetValue((asset.classid, asset.instanceid), out InventoryDescription? description) || !assetIDs.Add(asset.assetid)) {
continue;
}
Asset convertedAsset = new(asset.appid, asset.contextid, asset.classid, (uint) asset.amount, description, asset.assetid, asset.instanceid);
yield return convertedAsset;
}
if (!response.more_items) {
yield break;
}
if (response.last_assetid == 0) {
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(response.last_assetid)));
}
startAssetID = response.last_assetid;
}
}
[PublicAPI] [PublicAPI]
public async Task<Dictionary<uint, string>?> GetOwnedGames(ulong steamID) { public async Task<Dictionary<uint, string>?> GetOwnedGames(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {

View file

@ -33,6 +33,7 @@ using System.Text;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using AngleSharp.Dom; using AngleSharp.Dom;
using ArchiSteamFarm.Core; using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Helpers;
@ -230,32 +231,32 @@ public sealed class ArchiWebHandler : IDisposable {
} }
[PublicAPI] [PublicAPI]
public async IAsyncEnumerable<Asset> GetInventoryAsync(ulong steamID = 0, uint appID = Asset.SteamAppID, ulong contextID = Asset.SteamCommunityContextID) { [Obsolete($"Use ArchiHandler.{nameof(ArchiHandler.GetMyInventoryAsync)} for getting bot's own inventory or ArchiWebHandler.${nameof(GetForeignInventoryAsync)} in other cases instead.")]
public IAsyncEnumerable<Asset> GetInventoryAsync(ulong steamID = 0, uint appID = Asset.SteamAppID, ulong contextID = Asset.SteamCommunityContextID) {
if ((steamID == 0) || (steamID == Bot.SteamID)) {
return Bot.ArchiHandler.GetMyInventoryAsync(appID, contextID);
}
return GetForeignInventoryAsync(steamID, appID, contextID);
}
[PublicAPI]
public async IAsyncEnumerable<Asset> GetForeignInventoryAsync(ulong steamID, uint appID = Asset.SteamAppID, ulong contextID = Asset.SteamCommunityContextID) {
ArgumentOutOfRangeException.ThrowIfZero(appID); ArgumentOutOfRangeException.ThrowIfZero(appID);
ArgumentOutOfRangeException.ThrowIfZero(contextID); ArgumentOutOfRangeException.ThrowIfZero(contextID);
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if (steamID == Bot.SteamID) {
throw new NotSupportedException();
}
if (ASF.InventorySemaphore == null) { if (ASF.InventorySemaphore == null) {
throw new InvalidOperationException(nameof(ASF.InventorySemaphore)); throw new InvalidOperationException(nameof(ASF.InventorySemaphore));
} }
if (steamID == 0) {
if (!Initialized) {
byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout;
for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) {
await Task.Delay(1000).ConfigureAwait(false);
}
if (!Initialized) {
throw new HttpRequestException(Strings.WarningFailed);
}
}
steamID = Bot.SteamID;
} else if (!new SteamID(steamID).IsIndividualAccount) {
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(steamID)));
}
ulong startAssetID = 0; ulong startAssetID = 0;
// We need to store asset IDs to make sure we won't get duplicate items // We need to store asset IDs to make sure we won't get duplicate items
@ -343,9 +344,9 @@ public sealed class ArchiWebHandler : IDisposable {
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, $"{nameof(response.Content.Assets)} || {nameof(response.Content.Descriptions)}")); throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, $"{nameof(response.Content.Assets)} || {nameof(response.Content.Descriptions)}"));
} }
Dictionary<(ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions = new(); Dictionary<(ulong ClassID, ulong InstanceID), InventoryDescription> descriptions = new();
foreach (InventoryResponse.Description description in response.Content.Descriptions) { foreach (InventoryDescription description in response.Content.Descriptions) {
if (description.ClassID == 0) { if (description.ClassID == 0) {
throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(description.ClassID))); throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(description.ClassID)));
} }
@ -356,20 +357,11 @@ public sealed class ArchiWebHandler : IDisposable {
} }
foreach (Asset asset in response.Content.Assets) { foreach (Asset asset in response.Content.Assets) {
if (!descriptions.TryGetValue((asset.ClassID, asset.InstanceID), out InventoryResponse.Description? description) || !assetIDs.Add(asset.AssetID)) { if (!descriptions.TryGetValue((asset.ClassID, asset.InstanceID), out InventoryDescription? description) || !assetIDs.Add(asset.AssetID)) {
continue; continue;
} }
asset.Marketable = description.Marketable; asset.Description = description;
asset.Tradable = description.Tradable;
asset.Tags = description.Tags;
asset.RealAppID = description.RealAppID;
asset.Type = description.Type;
asset.Rarity = description.Rarity;
if (description.AdditionalProperties != null) {
asset.AdditionalProperties = description.AdditionalProperties;
}
yield return asset; yield return asset;
} }
@ -457,7 +449,7 @@ public sealed class ArchiWebHandler : IDisposable {
return null; return null;
} }
Dictionary<string, object?> arguments = new(StringComparer.Ordinal) { Dictionary<string, object> arguments = new(StringComparer.Ordinal) {
{ "access_token", accessToken } { "access_token", accessToken }
}; };
@ -484,30 +476,11 @@ public sealed class ArchiWebHandler : IDisposable {
arguments["get_descriptions"] = withDescriptions.Value ? "true" : "false"; arguments["get_descriptions"] = withDescriptions.Value ? "true" : "false";
} }
KeyValue? response = null; string queryString = string.Join('&', arguments.Select(static argument => $"{argument.Key}={HttpUtility.UrlEncode(argument.Value.ToString())}"));
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) { string request = $"/{EconService}/GetTradeOffers/v1/?" + queryString;
if ((i > 0) && (WebLimiterDelay > 0)) {
await Task.Delay(WebLimiterDelay).ConfigureAwait(false);
}
using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService); TradeOffersResponse? response = (await WebLimitRequest(WebAPI.DefaultBaseAddress, async () => await WebBrowser.UrlGetToJsonObject<APIWrappedResponse<TradeOffersResponse>>(new Uri(WebAPI.DefaultBaseAddress, request)).ConfigureAwait(false)).ConfigureAwait(false))?.Content?.Response;
econService.Timeout = WebBrowser.Timeout;
try {
response = await WebLimitRequest(
WebAPI.DefaultBaseAddress,
// ReSharper disable once AccessToDisposedClosure
async () => await econService.CallAsync(HttpMethod.Get, "GetTradeOffers", args: arguments).ConfigureAwait(false)
).ConfigureAwait(false);
} catch (TaskCanceledException e) {
Bot.ArchiLogger.LogGenericDebuggingException(e);
} catch (Exception e) {
Bot.ArchiLogger.LogGenericWarningException(e);
}
}
if (response == null) { if (response == null) {
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)); Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
@ -515,130 +488,27 @@ public sealed class ArchiWebHandler : IDisposable {
return null; return null;
} }
Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions = new(); IEnumerable<TradeOffer> trades = Enumerable.Empty<TradeOffer>();
foreach (KeyValue description in response["descriptions"].Children) {
uint appID = description["appid"].AsUnsignedInteger();
if (appID == 0) {
Bot.ArchiLogger.LogNullError(appID);
return null;
}
ulong classID = description["classid"].AsUnsignedLong();
if (classID == 0) {
Bot.ArchiLogger.LogNullError(classID);
return null;
}
ulong instanceID = description["instanceid"].AsUnsignedLong();
(uint AppID, ulong ClassID, ulong InstanceID) key = (appID, classID, instanceID);
if (descriptions.ContainsKey(key)) {
continue;
}
bool marketable = description["marketable"].AsBoolean();
List<KeyValue> tags = description["tags"].Children;
HashSet<Tag>? parsedTags = null;
if (tags.Count > 0) {
parsedTags = new HashSet<Tag>(tags.Count);
foreach (KeyValue tag in tags) {
string? identifier = tag["category"].AsString();
if (string.IsNullOrEmpty(identifier)) {
Bot.ArchiLogger.LogNullError(identifier);
return null;
}
string? value = tag["internal_name"].AsString();
// Apparently, name can be empty, but not null
if (value == null) {
Bot.ArchiLogger.LogNullError(value);
return null;
}
parsedTags.Add(new Tag(identifier, value));
}
}
InventoryResponse.Description parsedDescription = new(appID, classID, instanceID, marketable, parsedTags);
descriptions[key] = parsedDescription;
}
IEnumerable<KeyValue> trades = Enumerable.Empty<KeyValue>();
if (receivedOffers.GetValueOrDefault(true)) { if (receivedOffers.GetValueOrDefault(true)) {
trades = trades.Concat(response["trade_offers_received"].Children); trades = trades.Concat(response.TradeOffersReceived);
} }
if (sentOffers.GetValueOrDefault(true)) { if (sentOffers.GetValueOrDefault(true)) {
trades = trades.Concat(response["trade_offers_sent"].Children); trades = trades.Concat(response.TradeOffersSent);
} }
Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryDescription> descriptions = response.Descriptions.ToDictionary(static description => (description.AppID, description.ClassID, description.InstanceID), static description => description);
HashSet<TradeOffer> result = []; HashSet<TradeOffer> result = [];
foreach (KeyValue trade in trades) { foreach (TradeOffer tradeOffer in trades.Where(tradeOffer => !activeOnly.HasValue || ((!activeOnly.Value || (tradeOffer.State == ETradeOfferState.Active)) && (activeOnly.Value || (tradeOffer.State != ETradeOfferState.Active))))) {
ETradeOfferState state = trade["trade_offer_state"].AsEnum<ETradeOfferState>(); if (tradeOffer.ItemsToGive.Count > 0) {
SetDescriptionsToAssets(tradeOffer.ItemsToGive, descriptions);
if (!Enum.IsDefined(state)) {
Bot.ArchiLogger.LogNullError(state);
return null;
} }
if (activeOnly.HasValue && ((activeOnly.Value && (state != ETradeOfferState.Active)) || (!activeOnly.Value && (state == ETradeOfferState.Active)))) { if (tradeOffer.ItemsToReceive.Count > 0) {
continue; SetDescriptionsToAssets(tradeOffer.ItemsToReceive, descriptions);
}
ulong tradeOfferID = trade["tradeofferid"].AsUnsignedLong();
if (tradeOfferID == 0) {
Bot.ArchiLogger.LogNullError(tradeOfferID);
return null;
}
uint otherSteamID3 = trade["accountid_other"].AsUnsignedInteger();
if (otherSteamID3 == 0) {
Bot.ArchiLogger.LogNullError(otherSteamID3);
return null;
}
TradeOffer tradeOffer = new(tradeOfferID, otherSteamID3, state);
List<KeyValue> itemsToGive = trade["items_to_give"].Children;
if (itemsToGive.Count > 0) {
if (!ParseItems(descriptions, itemsToGive, tradeOffer.ItemsToGive)) {
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToGive)));
return null;
}
}
List<KeyValue> itemsToReceive = trade["items_to_receive"].Children;
if (itemsToReceive.Count > 0) {
if (!ParseItems(descriptions, itemsToReceive, tradeOffer.ItemsToReceive)) {
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToReceive)));
return null;
}
} }
result.Add(tradeOffer); result.Add(tradeOffer);
@ -2389,77 +2259,6 @@ public sealed class ArchiWebHandler : IDisposable {
return uri.AbsolutePath.StartsWith("/login", StringComparison.OrdinalIgnoreCase) || uri.Host.Equals("lostauth", StringComparison.OrdinalIgnoreCase); return uri.AbsolutePath.StartsWith("/login", StringComparison.OrdinalIgnoreCase) || uri.Host.Equals("lostauth", StringComparison.OrdinalIgnoreCase);
} }
private static bool ParseItems([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions, IReadOnlyCollection<KeyValue> input, ICollection<Asset> output) {
ArgumentNullException.ThrowIfNull(descriptions);
if ((input == null) || (input.Count == 0)) {
throw new ArgumentNullException(nameof(input));
}
ArgumentNullException.ThrowIfNull(output);
foreach (KeyValue item in input) {
uint appID = item["appid"].AsUnsignedInteger();
if (appID == 0) {
ASF.ArchiLogger.LogNullError(appID);
return false;
}
ulong contextID = item["contextid"].AsUnsignedLong();
if (contextID == 0) {
ASF.ArchiLogger.LogNullError(contextID);
return false;
}
ulong classID = item["classid"].AsUnsignedLong();
if (classID == 0) {
ASF.ArchiLogger.LogNullError(classID);
return false;
}
ulong instanceID = item["instanceid"].AsUnsignedLong();
(uint AppID, ulong ClassID, ulong InstanceID) key = (appID, classID, instanceID);
uint amount = item["amount"].AsUnsignedInteger();
if (amount == 0) {
ASF.ArchiLogger.LogNullError(amount);
return false;
}
ulong assetID = item["assetid"].AsUnsignedLong();
bool marketable = true;
bool tradable = true;
ImmutableHashSet<Tag>? tags = null;
uint realAppID = 0;
Asset.EType type = Asset.EType.Unknown;
Asset.ERarity rarity = Asset.ERarity.Unknown;
if (descriptions.TryGetValue(key, out InventoryResponse.Description? description)) {
marketable = description.Marketable;
tradable = description.Tradable;
tags = description.Tags;
realAppID = description.RealAppID;
type = description.Type;
rarity = description.Rarity;
}
Asset steamAsset = new(appID, contextID, classID, amount, instanceID, assetID, marketable, tradable, tags, realAppID, type, rarity);
output.Add(steamAsset);
}
return true;
}
private async Task<bool> RefreshSession() { private async Task<bool> RefreshSession() {
if (!Bot.IsConnectedAndLoggedOn) { if (!Bot.IsConnectedAndLoggedOn) {
return false; return false;
@ -2569,6 +2368,16 @@ public sealed class ArchiWebHandler : IDisposable {
return (true, result.ToFrozenSet()); return (true, result.ToFrozenSet());
} }
private static void SetDescriptionsToAssets(IEnumerable<Asset> assets, [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryDescription> descriptions) {
foreach (Asset asset in assets) {
if (!descriptions.TryGetValue((asset.AppID, asset.ClassID, asset.InstanceID), out InventoryDescription? description)) {
description = new InventoryDescription(asset.AppID, asset.ClassID, asset.InstanceID, true, true);
}
asset.Description = description;
}
}
private async Task<bool> UnlockParentalAccount(string parentalCode) { private async Task<bool> UnlockParentalAccount(string parentalCode) {
ArgumentException.ThrowIfNullOrEmpty(parentalCode); ArgumentException.ThrowIfNullOrEmpty(parentalCode);

View file

@ -429,7 +429,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable {
TradingScheduled = false; TradingScheduled = false;
} }
inventory = await Bot.ArchiWebHandler.GetInventoryAsync(appID: appID, contextID: contextID).Where(item => item.Tradable && filterFunction(item)).ToHashSetAsync().ConfigureAwait(false); inventory = await Bot.ArchiHandler.GetMyInventoryAsync(appID, contextID, true).Where(item => filterFunction(item)).ToHashSetAsync().ConfigureAwait(false);
} catch (HttpRequestException e) { } catch (HttpRequestException e) {
Bot.ArchiLogger.LogGenericWarningException(e); Bot.ArchiLogger.LogGenericWarningException(e);

View file

@ -3107,7 +3107,7 @@ public sealed class Commands {
// It'd also make sense to run all of this in parallel, but it seems that Steam has a lot of problems with inventory-related parallel requests | https://steamcommunity.com/groups/archiasf/discussions/1/3559414588264550284/ // It'd also make sense to run all of this in parallel, but it seems that Steam has a lot of problems with inventory-related parallel requests | https://steamcommunity.com/groups/archiasf/discussions/1/3559414588264550284/
try { try {
await foreach (Asset item in Bot.ArchiWebHandler.GetInventoryAsync().Where(static item => item.Type == Asset.EType.BoosterPack).ConfigureAwait(false)) { await foreach (Asset item in Bot.ArchiHandler.GetMyInventoryAsync().Where(static item => item.Type == Asset.EType.BoosterPack).ConfigureAwait(false)) {
if (!await Bot.ArchiWebHandler.UnpackBooster(item.RealAppID, item.AssetID).ConfigureAwait(false)) { if (!await Bot.ArchiWebHandler.UnpackBooster(item.RealAppID, item.AssetID).ConfigureAwait(false)) {
completeSuccess = false; completeSuccess = false;
} }