From 88369ec71ab20eaf4d7929d089bb0441ce4e49bf Mon Sep 17 00:00:00 2001 From: JustArchi Date: Wed, 20 Apr 2016 21:27:57 +0200 Subject: [PATCH] Put massive amount of work into STM integration, #84 --- ArchiSteamFarm/ArchiWebHandler.cs | 169 ++++++++++++----- ArchiSteamFarm/Bot.cs | 2 +- ArchiSteamFarm/BotConfig.cs | 3 + ArchiSteamFarm/JSON/Steam.cs | 291 ++++++++++++++++++++++++----- ArchiSteamFarm/Trading.cs | 42 +++-- ArchiSteamFarm/config/example.json | 1 + ConfigGenerator/BotConfig.cs | 3 + 7 files changed, 403 insertions(+), 108 deletions(-) diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 078dcf20c..99f302b3c 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -28,7 +28,6 @@ using SteamKit2; using System; using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Xml; @@ -222,7 +221,7 @@ namespace ArchiSteamFarm { return result; } - internal List GetTradeOffers() { + internal HashSet GetTradeOffers() { if (string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { return null; } @@ -236,6 +235,7 @@ namespace ArchiSteamFarm { response = iEconService.GetTradeOffers( get_received_offers: 1, active_only: 1, + get_descriptions: 1, secure: !Program.GlobalConfig.ForceHttp ); } catch (Exception e) { @@ -249,33 +249,108 @@ namespace ArchiSteamFarm { return null; } - List result = new List(); + Dictionary, uint> appIDMap = new Dictionary, uint>(); + Dictionary, Steam.Item.EType> typeMap = new Dictionary, Steam.Item.EType>(); + foreach (KeyValue description in response["descriptions"].Children) { + ulong classID = description["classid"].AsUnsignedLong(); + if (classID == 0) { + continue; + } + + ulong instanceID = description["instanceid"].AsUnsignedLong(); + + Tuple key = new Tuple(classID, instanceID); + + if (!appIDMap.ContainsKey(key)) { + string hashName = description["market_hash_name"].Value; + if (!string.IsNullOrEmpty(hashName)) { + int index = hashName.IndexOf('-'); + if (index < 1) { + continue; + } + + uint appID; + if (!uint.TryParse(hashName.Substring(0, index), out appID)) { + continue; + } + + appIDMap[key] = appID; + } + } + + if (!typeMap.ContainsKey(key)) { + string type = description["type"].Value; + if (!string.IsNullOrEmpty(type)) { + if (type.EndsWith("Trading Card", StringComparison.Ordinal)) { + typeMap[key] = Steam.Item.EType.TradingCard; + } else if (type.EndsWith("Profile Background", StringComparison.Ordinal)) { + typeMap[key] = Steam.Item.EType.ProfileBackground; + } else { + typeMap[key] = Steam.Item.EType.Unknown; + } + } + } + } + + HashSet result = new HashSet(); foreach (KeyValue trade in response["trade_offers_received"].Children) { + // TODO: Correct some of these when SK2 with https://github.com/SteamRE/SteamKit/pull/255 gets released Steam.TradeOffer tradeOffer = new Steam.TradeOffer { - tradeofferid = trade["tradeofferid"].AsString(), - accountid_other = (uint) trade["accountid_other"].AsUnsignedLong(), // TODO: Correct this when SK2 with https://github.com/SteamRE/SteamKit/pull/255 gets released - trade_offer_state = trade["trade_offer_state"].AsEnum() + TradeOfferID = trade["tradeofferid"].AsUnsignedLong(), + OtherSteamID3 = (uint) trade["accountid_other"].AsUnsignedLong(), + State = trade["trade_offer_state"].AsEnum() }; + foreach (KeyValue item in trade["items_to_give"].Children) { - tradeOffer.items_to_give.Add(new Steam.Item { - appid = item["appid"].AsString(), - contextid = item["contextid"].AsString(), - assetid = item["assetid"].AsString(), - classid = item["classid"].AsString(), - instanceid = item["instanceid"].AsString(), - amount = item["amount"].AsString(), - }); + Steam.Item steamItem = new Steam.Item { + AppID = (uint) item["appid"].AsUnsignedLong(), + ContextID = item["contextid"].AsUnsignedLong(), + AssetID = item["assetid"].AsUnsignedLong(), + ClassID = item["classid"].AsUnsignedLong(), + InstanceID = item["instanceid"].AsUnsignedLong(), + Amount = (byte) item["amount"].AsUnsignedLong() + }; + + Tuple key = new Tuple(steamItem.ClassID, steamItem.InstanceID); + + uint realAppID; + if (appIDMap.TryGetValue(key, out realAppID)) { + steamItem.RealAppID = realAppID; + } + + Steam.Item.EType type; + if (typeMap.TryGetValue(key, out type)) { + steamItem.Type = type; + } + + tradeOffer.ItemsToGive.Add(steamItem); } + foreach (KeyValue item in trade["items_to_receive"].Children) { - tradeOffer.items_to_receive.Add(new Steam.Item { - appid = item["appid"].AsString(), - contextid = item["contextid"].AsString(), - assetid = item["assetid"].AsString(), - classid = item["classid"].AsString(), - instanceid = item["instanceid"].AsString(), - amount = item["amount"].AsString(), - }); + Steam.Item steamItem = new Steam.Item { + AppID = (uint) item["appid"].AsUnsignedLong(), + ContextID = item["contextid"].AsUnsignedLong(), + AssetID = item["assetid"].AsUnsignedLong(), + ClassID = item["classid"].AsUnsignedLong(), + InstanceID = item["instanceid"].AsUnsignedLong(), + Amount = (byte) item["amount"].AsUnsignedLong() + }; + + Tuple key = new Tuple(steamItem.ClassID, steamItem.InstanceID); + + uint realAppID; + if (appIDMap.TryGetValue(key, out realAppID)) { + steamItem.RealAppID = realAppID; + } + + Steam.Item.EType type; + if (typeMap.TryGetValue(key, out type)) { + steamItem.Type = type; + } + + tradeOffer.ItemsToReceive.Add(steamItem); } + result.Add(tradeOffer); } @@ -300,8 +375,8 @@ namespace ArchiSteamFarm { string request = SteamCommunityURL + "/gid/" + clanID; Dictionary data = new Dictionary(2) { - {"sessionID", sessionID}, - {"action", "join"} + { "sessionID", sessionID }, + { "action", "join" } }; bool result = false; @@ -336,9 +411,9 @@ namespace ArchiSteamFarm { string request = referer + "/accept"; Dictionary data = new Dictionary(3) { - {"sessionid", sessionID}, - {"serverid", "1"}, - {"tradeofferid", tradeID.ToString()} + { "sessionid", sessionID }, + { "serverid", "1" }, + { "tradeofferid", tradeID.ToString() } }; bool result = false; @@ -354,14 +429,14 @@ namespace ArchiSteamFarm { return true; } - internal async Task> GetMyTradableInventory() { + internal async Task> GetMyTradableInventory() { if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { return null; } JObject jObject = null; for (byte i = 0; i < WebBrowser.MaxRetries && jObject == null; i++) { - jObject = await WebBrowser.UrlGetToJObject(SteamCommunityURL + "/my/inventory/json/753/6?trading=1").ConfigureAwait(false); + jObject = await WebBrowser.UrlGetToJObject(SteamCommunityURL + "/my/inventory/json/" + Steam.Item.SteamAppID + "/" + Steam.Item.SteamContextID + "?trading=1").ConfigureAwait(false); } if (jObject == null) { @@ -375,7 +450,7 @@ namespace ArchiSteamFarm { return null; } - List result = new List(); + HashSet result = new HashSet(); foreach (JToken jToken in jTokens) { try { result.Add(JsonConvert.DeserializeObject(jToken.ToString())); @@ -387,7 +462,7 @@ namespace ArchiSteamFarm { return result; } - internal async Task SendTradeOffer(List inventory, ulong partnerID, string token = null) { + internal async Task SendTradeOffer(HashSet inventory, ulong partnerID, string token = null) { if (inventory == null || inventory.Count == 0 || partnerID == 0) { return false; } @@ -402,26 +477,30 @@ namespace ArchiSteamFarm { return false; } - List trades = new List(1 + inventory.Count / Trading.MaxItemsPerTrade); + HashSet trades = new HashSet(); Steam.TradeOfferRequest singleTrade = null; - for (ushort i = 0; i < inventory.Count; i++) { - if (i % Trading.MaxItemsPerTrade == 0) { + + byte itemID = 0; + foreach (Steam.Item item in inventory) { + if (itemID % Trading.MaxItemsPerTrade == 0) { if (trades.Count >= Trading.MaxTradesPerAccount) { break; } singleTrade = new Steam.TradeOfferRequest(); trades.Add(singleTrade); + itemID = 0; } - Steam.Item item = inventory[i]; singleTrade.me.assets.Add(new Steam.Item() { - appid = "753", - contextid = "6", - amount = item.amount, - assetid = item.id + AppID = Steam.Item.SteamAppID, + ContextID = Steam.Item.SteamContextID, + Amount = item.Amount, + AssetID = item.AssetID }); + + itemID++; } string referer = SteamCommunityURL + "/tradeoffer/new"; @@ -429,12 +508,12 @@ namespace ArchiSteamFarm { foreach (Steam.TradeOfferRequest trade in trades) { Dictionary data = new Dictionary(6) { - {"sessionid", sessionID}, - {"serverid", "1"}, - {"partner", partnerID.ToString()}, - {"tradeoffermessage", "Sent by ASF"}, - {"json_tradeoffer", JsonConvert.SerializeObject(trade)}, - {"trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : $"{{\"trade_offer_access_token\":\"{token}\"}}"} + { "sessionid", sessionID }, + { "serverid", "1" }, + { "partner", partnerID.ToString() }, + { "tradeoffermessage", "Sent by ASF" }, + { "json_tradeoffer", JsonConvert.SerializeObject(trade) }, + { "trade_offer_create_params", string.IsNullOrEmpty(token) ? "" : $"{{\"trade_offer_access_token\":\"{token}\"}}" } }; bool result = false; diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 6d8792a7c..e52d028d7 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -588,7 +588,7 @@ namespace ArchiSteamFarm { } await Trading.LimitInventoryRequestsAsync().ConfigureAwait(false); - List inventory = await ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false); + HashSet inventory = await ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false); if (inventory == null || inventory.Count == 0) { return "Nothing to send, inventory seems empty!"; diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index 1343d8ded..2fc3838bf 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -68,6 +68,9 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] internal bool AcceptGifts { get; private set; } = false; + [JsonProperty(Required = Required.DisallowNull)] + internal bool SteamTradeMatcher { get; private set; } = false; + [JsonProperty(Required = Required.DisallowNull)] internal bool ForwardKeysToOtherBots { get; private set; } = false; diff --git a/ArchiSteamFarm/JSON/Steam.cs b/ArchiSteamFarm/JSON/Steam.cs index 282f25aa6..052d6218d 100644 --- a/ArchiSteamFarm/JSON/Steam.cs +++ b/ArchiSteamFarm/JSON/Steam.cs @@ -28,36 +28,150 @@ using System.Collections.Generic; namespace ArchiSteamFarm { internal static class Steam { - internal sealed class Item { - // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset - [JsonProperty(Required = Required.DisallowNull)] - internal string appid { get; set; } + internal sealed class Item { // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset + internal const ushort SteamAppID = 753; + internal const byte SteamContextID = 6; - [JsonProperty(Required = Required.DisallowNull)] - internal string contextid { get; set; } - - [JsonProperty(Required = Required.DisallowNull)] - internal string assetid { get; set; } - - [JsonProperty(Required = Required.DisallowNull)] - internal string id { - get { return assetid; } - set { assetid = value; } + internal enum EType : byte { + Unknown, + ProfileBackground, + TradingCard } - [JsonProperty(Required = Required.AllowNull)] - internal string classid { get; set; } + internal uint AppID; - [JsonProperty(Required = Required.AllowNull)] - internal string instanceid { get; set; } + [JsonProperty(PropertyName = "appid", Required = Required.DisallowNull)] + internal string AppIDString { + get { + return AppID.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; + } - [JsonProperty(Required = Required.Always)] - internal string amount { get; set; } - } + uint result; + if (!uint.TryParse(value, out result)) { + return; + } - internal sealed class ItemList { - [JsonProperty(Required = Required.Always)] - internal List assets { get; } = new List(); + AppID = result; + } + } + + internal ulong ContextID; + + [JsonProperty(PropertyName = "contextid", Required = Required.DisallowNull)] + internal string ContextIDString { + get { + return ContextID.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; + } + + uint result; + if (!uint.TryParse(value, out result)) { + return; + } + + ContextID = result; + } + } + + internal ulong AssetID; + + [JsonProperty(PropertyName = "assetid", Required = Required.DisallowNull)] + internal string AssetIDString { + get { + return AssetID.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; + } + + uint result; + if (!uint.TryParse(value, out result)) { + return; + } + + AssetID = result; + } + } + + [JsonProperty(PropertyName = "id", Required = Required.DisallowNull)] + internal string id { + get { return AssetIDString; } + set { AssetIDString = value; } + } + + internal ulong ClassID; + + [JsonProperty(PropertyName = "classid", Required = Required.DisallowNull)] + internal string ClassIDString { + get { + return ClassID.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; + } + + uint result; + if (!uint.TryParse(value, out result)) { + return; + } + + ClassID = result; + } + } + + internal ulong InstanceID; + + [JsonProperty(PropertyName = "instanceid", Required = Required.DisallowNull)] + internal string InstanceIDString { + get { + return InstanceID.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; + } + + uint result; + if (!uint.TryParse(value, out result)) { + return; + } + + InstanceID = result; + } + } + + internal byte Amount; + + [JsonProperty(PropertyName = "amount", Required = Required.Always)] + internal string AmountString { + get { + return Amount.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; + } + + byte result; + if (!byte.TryParse(value, out result)) { + return; + } + + Amount = result; + } + } + + internal uint RealAppID { get; set; } + internal EType Type { get; set; } } internal sealed class TradeOffer { @@ -77,35 +191,120 @@ namespace ArchiSteamFarm { OnHold } - [JsonProperty(Required = Required.Always)] - internal string tradeofferid { get; set; } + internal ulong TradeOfferID; - [JsonProperty(Required = Required.Always)] - internal uint accountid_other { get; set; } - - [JsonProperty(Required = Required.Always)] - internal ETradeOfferState trade_offer_state { get; set; } - - [JsonProperty(Required = Required.Always)] - internal List items_to_give { get; } = new List(); - - [JsonProperty(Required = Required.Always)] - internal List items_to_receive { get; } = new List(); - - // Extra - private ulong _OtherSteamID64 = 0; - internal ulong OtherSteamID64 { + [JsonProperty(PropertyName = "tradeofferid", Required = Required.Always)] + internal string TradeOfferIDString { get { - if (_OtherSteamID64 == 0 && accountid_other != 0) { - _OtherSteamID64 = new SteamID(accountid_other, EUniverse.Public, EAccountType.Individual).ConvertToUInt64(); + return TradeOfferID.ToString(); + } + set { + if (string.IsNullOrEmpty(value)) { + return; } - return _OtherSteamID64; + ulong result; + if (!ulong.TryParse(value, out result)) { + return; + } + + TradeOfferID = result; + } + } + + [JsonProperty(PropertyName = "accountid_other", Required = Required.Always)] + internal uint OtherSteamID3 { get; set; } + + [JsonProperty(PropertyName = "trade_offer_state", Required = Required.Always)] + internal ETradeOfferState State { get; set; } + + [JsonProperty(PropertyName = "items_to_give", Required = Required.Always)] + internal HashSet ItemsToGive { get; } = new HashSet(); + + [JsonProperty(PropertyName = "items_to_receive", Required = Required.Always)] + internal HashSet ItemsToReceive { get; } = new HashSet(); + + // Extra + internal ulong OtherSteamID64 { + get { + if (OtherSteamID3 == 0) { + return 0; + } + + return new SteamID(OtherSteamID3, EUniverse.Public, EAccountType.Individual); + } + set { + if (value == 0) { + return; + } + + OtherSteamID3 = new SteamID(value).AccountID; + } + } + + internal bool IsSteamCardsOnlyTrade { + get { + foreach (Item item in ItemsToGive) { + if (item.AppID != Item.SteamAppID || item.ContextID != Item.SteamContextID || item.Type != Item.EType.TradingCard) { + return false; + } + } + + foreach (Item item in ItemsToReceive) { + if (item.AppID != Item.SteamAppID || item.ContextID != Item.SteamContextID || item.Type != Item.EType.TradingCard) { + return false; + } + } + + return true; + } + } + + internal bool IsPotentiallyDupesTrade { + get { + Dictionary ItemsToGivePerGameAmount = new Dictionary(); + foreach (Item item in ItemsToGive) { + byte amount; + if (ItemsToGivePerGameAmount.TryGetValue(item.RealAppID, out amount)) { + ItemsToGivePerGameAmount[item.RealAppID] = (byte) (amount + item.Amount); + } else { + ItemsToGivePerGameAmount[item.RealAppID] = item.Amount; + } + } + + Dictionary ItemsToReceivePerGameAmount = new Dictionary(); + foreach (Item item in ItemsToReceive) { + byte amount; + if (ItemsToReceivePerGameAmount.TryGetValue(item.RealAppID, out amount)) { + ItemsToReceivePerGameAmount[item.RealAppID] = (byte) (amount + item.Amount); + } else { + ItemsToReceivePerGameAmount[item.RealAppID] = item.Amount; + } + } + + // Ensure that amounts are exactly the same + foreach (KeyValuePair item in ItemsToGivePerGameAmount) { + byte otherValue; + if (!ItemsToReceivePerGameAmount.TryGetValue(item.Key, out otherValue)) { + return false; + } + + if (item.Value != otherValue) { + return false; + } + } + + return true; } } } internal sealed class TradeOfferRequest { + internal sealed class ItemList { + [JsonProperty(Required = Required.Always)] + internal HashSet assets { get; } = new HashSet(); + } + [JsonProperty(Required = Required.Always)] internal bool newversion { get; } = true; @@ -113,10 +312,10 @@ namespace ArchiSteamFarm { internal int version { get; } = 2; [JsonProperty(Required = Required.Always)] - internal Steam.ItemList me { get; } = new Steam.ItemList(); + internal ItemList me { get; } = new ItemList(); [JsonProperty(Required = Required.Always)] - internal Steam.ItemList them { get; } = new Steam.ItemList(); + internal ItemList them { get; } = new ItemList(); } } } diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 56960eef5..1ca1aae06 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -80,8 +80,8 @@ namespace ArchiSteamFarm { } private async Task ParseActiveTrades() { - List tradeOffers = Bot.ArchiWebHandler.GetTradeOffers(); - if (tradeOffers == null) { + HashSet tradeOffers = Bot.ArchiWebHandler.GetTradeOffers(); + if (tradeOffers == null || tradeOffers.Count == 0) { return; } @@ -90,20 +90,15 @@ namespace ArchiSteamFarm { } private async Task ParseTrade(Steam.TradeOffer tradeOffer) { - if (tradeOffer == null || tradeOffer.trade_offer_state != Steam.TradeOffer.ETradeOfferState.Active) { - return; - } - - ulong tradeID; - if (!ulong.TryParse(tradeOffer.tradeofferid, out tradeID)) { + if (tradeOffer == null || tradeOffer.State != Steam.TradeOffer.ETradeOfferState.Active) { return; } if (ShouldAcceptTrade(tradeOffer)) { - Logging.LogGenericInfo("Accepting trade: " + tradeID, Bot.BotName); - await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false); + Logging.LogGenericInfo("Accepting trade: " + tradeOffer.TradeOfferID, Bot.BotName); + await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false); } else { - Logging.LogGenericInfo("Ignoring trade: " + tradeID, Bot.BotName); + Logging.LogGenericInfo("Ignoring trade: " + tradeOffer.TradeOfferID, Bot.BotName); } } @@ -113,9 +108,9 @@ namespace ArchiSteamFarm { } // Always accept trades when we're not losing anything - if (tradeOffer.items_to_give.Count == 0) { + if (tradeOffer.ItemsToGive.Count == 0) { // Unless it's steam fuckup and we're dealing with broken trade - return tradeOffer.items_to_receive.Count > 0; + return tradeOffer.ItemsToReceive.Count > 0; } // Always accept trades from SteamMasterID @@ -123,10 +118,25 @@ namespace ArchiSteamFarm { return true; } - // TODO: Add optional SteamTradeMatcher integration here + // If we don't have SteamTradeMatcher enabled, this is the end for us + if (!Bot.BotConfig.SteamTradeMatcher) { + return false; + } - // If no rule above matched this trade, reject it - return false; + // Rule 1 - We always trade the same amount of items + if (tradeOffer.ItemsToGive.Count != tradeOffer.ItemsToReceive.Count) { + return false; + } + + // Rule 2 - We always trade steam cards and only for the same set + if (!tradeOffer.IsSteamCardsOnlyTrade || !tradeOffer.IsPotentiallyDupesTrade) { + return false; + } + + // This STM trade SHOULD be fine + // Potential TODO: Ensure that our inventory in fact has proper amount of both received and given cards + // This way we could calculate amounts before and after trade, ensuring that we're in fact trading dupes and not 1 + 2 -> 0 + 3 + return true; } } } diff --git a/ArchiSteamFarm/config/example.json b/ArchiSteamFarm/config/example.json index f60532a95..ecf28878c 100644 --- a/ArchiSteamFarm/config/example.json +++ b/ArchiSteamFarm/config/example.json @@ -12,6 +12,7 @@ "FarmOffline": false, "HandleOfflineMessages": false, "AcceptGifts": false, + "SteamTradeMatcher": false, "ForwardKeysToOtherBots": false, "DistributeKeys": false, "UseAsfAsMobileAuthenticator": false, diff --git a/ConfigGenerator/BotConfig.cs b/ConfigGenerator/BotConfig.cs index 4327af510..d0951a507 100644 --- a/ConfigGenerator/BotConfig.cs +++ b/ConfigGenerator/BotConfig.cs @@ -68,6 +68,9 @@ namespace ConfigGenerator { [JsonProperty(Required = Required.DisallowNull)] public bool AcceptGifts { get; set; } = false; + [JsonProperty(Required = Required.DisallowNull)] + public bool SteamTradeMatcher { get; set; } = false; + [JsonProperty(Required = Required.DisallowNull)] public bool ForwardKeysToOtherBots { get; set; } = false;