Compare commits

...

45 Commits

Author SHA1 Message Date
JustArchi
f3da5d6afc Fix small regression caused by d0cc10f3c6 2016-04-23 16:21:12 +02:00
JustArchi
84f33fcef4 Bump 2016-04-23 16:12:57 +02:00
JustArchi
22f0d423a3 Closes #205 2016-04-23 14:47:39 +02:00
JustArchi
f1d7609796 Send trades also when we didn't farm anything
Previously I avoided that because of !loot looting entire steam EQ, but now when we loot only cards and boosters, that shouldn't be as annoying as before
2016-04-23 02:15:01 +02:00
JustArchi
77386ecae5 Misc 2016-04-22 17:51:13 +02:00
JustArchi
4e86d21ef8 Misc 2016-04-22 17:50:01 +02:00
JustArchi
044fc87691 Closes #201 2016-04-21 20:14:15 +02:00
JustArchi
79fad62a4d Bump 2016-04-21 19:26:47 +02:00
JustArchi
6622d3b147 Misc 2016-04-21 14:48:24 +02:00
JustArchi
6b6d5429ad Fix 1+1 -> 0+2, #84 2016-04-21 04:50:57 +02:00
JustArchi
d59d0a8415 Misc 2016-04-21 02:37:40 +02:00
JustArchi
e3100d3938 Implement super-smart STM calculations, #84
It's excellent now, also fixed inventory not returning all of my 6k items (sigh)
2016-04-21 02:32:36 +02:00
JustArchi
42c020e552 Add AppVeyor config 2016-04-21 01:09:52 +02:00
JustArchi
554273833b Make travis less annoying 2016-04-21 01:02:07 +02:00
JustArchi
093a29df62 Loot only steam cards and boosters, closes #111 2016-04-21 00:56:15 +02:00
JustArchi
31aa6b2e4a Remove my debug 2016-04-21 00:51:55 +02:00
JustArchi
9f7ecdf054 Add support for inventory descriptions, #111 2016-04-21 00:51:22 +02:00
JustArchi
7c4c74bf84 Bump 2016-04-20 23:57:06 +02:00
JustArchi
b8f03abd8b Fix fuckups 2016-04-20 23:48:33 +02:00
JustArchi
47f846540b Closes #200, thanks to @GUiHKX 2016-04-20 23:44:20 +02:00
JustArchi
bc14713079 Bump 2016-04-20 23:22:16 +02:00
JustArchi
770a8fee66 But keep trading only of cards + foils 2016-04-20 23:16:15 +02:00
JustArchi
1df9af08e6 Bugfixes + other types for STM 2016-04-20 23:02:02 +02:00
JustArchi
27464f6120 Add recent trades optimization 2016-04-20 22:19:31 +02:00
JustArchi
dfd45c6e25 Bump 2016-04-20 21:40:08 +02:00
JustArchi
adc1759cee Misc 2016-04-20 21:34:40 +02:00
JustArchi
88369ec71a Put massive amount of work into STM integration, #84 2016-04-20 21:27:57 +02:00
JustArchi
a5d8ae53dd Bump 2016-04-19 19:54:39 +02:00
JustArchi
cd7b65868a Misc 2016-04-19 12:24:07 +02:00
JustArchi
7575704a01 Misc 2016-04-19 12:23:24 +02:00
JustArchi
b6ce8f435c Use more optimized Slim manual reset events 2016-04-18 18:43:58 +02:00
JustArchi
565acca9fb Add AutoRestart property 2016-04-18 18:38:48 +02:00
JustArchi
610954ba73 Alter logic of key distribution, closes #199 2016-04-18 18:01:49 +02:00
JustArchi
74a748b03f Bump 2016-04-17 00:47:30 +02:00
JustArchi
585a075ec9 Closes #198
It was possible that we initiated a loop for bot that was connected, and it got disconnected shortly after, which could result in infinite loop if DistributeKeys was disabled (and nothing would change that bot to other one)
2016-04-17 00:36:38 +02:00
JustArchi
891d40afe1 Fix potential bug found by zinnerz
We might !stop account waiting in invalid password or game playing condition, which will then initiate connect without checking if it's still valid to do so
2016-04-16 19:24:04 +02:00
JustArchi
387f0dd1c7 Bump 2016-04-15 21:33:58 +02:00
JustArchi
8b4d3c219c Remove GUI app from final zip until I'm happy with the way how it works 2016-04-15 21:29:27 +02:00
JustArchi
365877ec89 Misc 2016-04-15 15:08:50 +02:00
JustArchi
f03a43d573 Misc 2016-04-15 00:32:55 +02:00
JustArchi
d15a9cbfca Misc 2016-04-15 00:18:24 +02:00
JustArchi
acfad624fb Bump 2016-04-14 22:44:35 +02:00
JustArchi
03a7d5f4ac Fix my tests 2016-04-14 22:25:10 +02:00
JustArchi
0f96d84d36 Fix disposed streams for ASF update 2016-04-14 22:23:37 +02:00
JustArchi
0edc9f4ff6 Bump 2016-04-14 21:30:17 +02:00
20 changed files with 826 additions and 284 deletions

View File

@@ -1,4 +1,3 @@
sudo: false
language: csharp
solution: ArchiSteamFarm.sln
@@ -8,3 +7,6 @@ git:
mono:
- weekly
- latest
notifications:
email: false

View File

@@ -67,7 +67,7 @@ namespace ArchiSteamFarm {
}
}
internal readonly List<Notification> Notifications;
internal readonly HashSet<Notification> Notifications;
internal NotificationsCallback(JobID jobID, CMsgClientUserNotifications msg) {
JobID = jobID;
@@ -76,7 +76,7 @@ namespace ArchiSteamFarm {
return;
}
Notifications = new List<Notification>(msg.notifications.Count);
Notifications = new HashSet<Notification>();
foreach (var notification in msg.notifications) {
Notifications.Add(new Notification((Notification.ENotificationType) notification.user_notification_type));
}
@@ -90,7 +90,7 @@ namespace ArchiSteamFarm {
}
if (msg.count_new_items > 0) {
Notifications = new List<Notification>(1) {
Notifications = new HashSet<Notification>() {
new Notification(Notification.ENotificationType.Items)
};
}
@@ -99,7 +99,6 @@ namespace ArchiSteamFarm {
internal sealed class OfflineMessageCallback : CallbackMsg {
internal readonly uint OfflineMessages;
internal readonly List<uint> Users;
internal OfflineMessageCallback(JobID jobID, CMsgClientOfflineMessageNotification msg) {
JobID = jobID;
@@ -109,7 +108,6 @@ namespace ArchiSteamFarm {
}
OfflineMessages = msg.offline_messages;
Users = msg.friends_with_offline_messages;
}
}
@@ -155,8 +153,8 @@ namespace ArchiSteamFarm {
foreach (KeyValue lineItem in lineItems) {
uint appID = (uint) lineItem["PackageID"].AsUnsignedLong();
string gameName = lineItem["ItemDescription"].AsString();
gameName = WebUtility.UrlDecode(gameName); // Apparently steam expects client to decode sent HTML
string gameName = lineItem["ItemDescription"].Value;
gameName = WebUtility.HtmlDecode(gameName); // Apparently steam expects client to decode sent HTML
Items.Add(appID, gameName);
}
}

View File

@@ -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;
@@ -54,6 +53,53 @@ namespace ArchiSteamFarm {
SteamCommunityURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamCommunity;
}
private static uint GetAppIDFromMarketHashName(string hashName) {
if (string.IsNullOrEmpty(hashName)) {
return 0;
}
int index = hashName.IndexOf('-');
if (index < 1) {
return 0;
}
uint appID;
if (!uint.TryParse(hashName.Substring(0, index), out appID)) {
return 0;
}
return appID;
}
private static Steam.Item.EType GetItemType(string name) {
if (string.IsNullOrEmpty(name)) {
return Steam.Item.EType.Unknown;
}
switch (name) {
case "Booster Pack":
return Steam.Item.EType.BoosterPack;
case "Coupon":
return Steam.Item.EType.Coupon;
case "Gift":
return Steam.Item.EType.Gift;
case "Steam Gems":
return Steam.Item.EType.SteamGems;
default:
if (name.EndsWith("Emoticon", StringComparison.Ordinal)) {
return Steam.Item.EType.Emoticon;
} else if (name.EndsWith("Foil Trading Card", StringComparison.Ordinal)) {
return Steam.Item.EType.FoilTradingCard;
} else if (name.EndsWith("Profile Background", StringComparison.Ordinal)) {
return Steam.Item.EType.ProfileBackground;
} else if (name.EndsWith("Trading Card", StringComparison.Ordinal)) {
return Steam.Item.EType.TradingCard;
} else {
return Steam.Item.EType.Unknown;
}
}
}
internal ArchiWebHandler(Bot bot) {
if (bot == null) {
throw new ArgumentNullException("bot");
@@ -133,9 +179,11 @@ namespace ArchiSteamFarm {
}
internal async Task<bool?> IsLoggedIn() {
string request = SteamCommunityURL + "/my/profile";
HtmlDocument htmlDocument = null;
for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) {
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(SteamCommunityURL + "/my/profile").ConfigureAwait(false);
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
}
if (htmlDocument == null) {
@@ -148,15 +196,13 @@ namespace ArchiSteamFarm {
}
internal async Task<bool> RefreshSessionIfNeeded() {
DateTime now = DateTime.Now;
if (now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
if (DateTime.Now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
return true;
}
await SessionSemaphore.WaitAsync().ConfigureAwait(false);
now = DateTime.Now;
if (now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
if (DateTime.Now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) {
SessionSemaphore.Release();
return true;
}
@@ -166,8 +212,7 @@ namespace ArchiSteamFarm {
bool? isLoggedIn = await IsLoggedIn().ConfigureAwait(false);
if (isLoggedIn.GetValueOrDefault(true)) {
result = true;
now = DateTime.Now;
LastSessionRefreshCheck = now;
LastSessionRefreshCheck = DateTime.Now;
} else {
Logging.LogGenericInfo("Refreshing our session!", Bot.BotName);
result = await Bot.RefreshSession().ConfigureAwait(false);
@@ -222,7 +267,7 @@ namespace ArchiSteamFarm {
return result;
}
internal List<Steam.TradeOffer> GetTradeOffers() {
internal HashSet<Steam.TradeOffer> GetTradeOffers() {
if (string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) {
return null;
}
@@ -236,6 +281,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 +295,87 @@ namespace ArchiSteamFarm {
return null;
}
List<Steam.TradeOffer> result = new List<Steam.TradeOffer>();
Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>> descriptionMap = new Dictionary<Tuple<ulong, ulong>, Tuple<uint, 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<ulong, ulong> key = new Tuple<ulong, ulong>(classID, instanceID);
if (descriptionMap.ContainsKey(key)) {
continue;
}
uint appID = 0;
Steam.Item.EType type = Steam.Item.EType.Unknown;
string hashName = description["market_hash_name"].Value;
if (!string.IsNullOrEmpty(hashName)) {
appID = GetAppIDFromMarketHashName(hashName);
}
string descriptionType = description["type"].Value;
if (!string.IsNullOrEmpty(descriptionType)) {
type = GetItemType(descriptionType);
}
descriptionMap[key] = new Tuple<uint, Steam.Item.EType>(appID, type);
}
HashSet<Steam.TradeOffer> result = new HashSet<Steam.TradeOffer>();
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<Steam.TradeOffer.ETradeOfferState>()
TradeOfferID = trade["tradeofferid"].AsUnsignedLong(),
OtherSteamID3 = (uint) trade["accountid_other"].AsUnsignedLong(),
State = trade["trade_offer_state"].AsEnum<Steam.TradeOffer.ETradeOfferState>()
};
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 = (uint) item["amount"].AsUnsignedLong()
};
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(steamItem.ClassID, steamItem.InstanceID);
Tuple<uint, Steam.Item.EType> description;
if (descriptionMap.TryGetValue(key, out description)) {
steamItem.RealAppID = description.Item1;
steamItem.Type = description.Item2;
}
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 = (uint) item["amount"].AsUnsignedLong()
};
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(steamItem.ClassID, steamItem.InstanceID);
Tuple<uint, Steam.Item.EType> description;
if (descriptionMap.TryGetValue(key, out description)) {
steamItem.RealAppID = description.Item1;
steamItem.Type = description.Item2;
}
tradeOffer.ItemsToReceive.Add(steamItem);
}
result.Add(tradeOffer);
}
@@ -300,8 +400,8 @@ namespace ArchiSteamFarm {
string request = SteamCommunityURL + "/gid/" + clanID;
Dictionary<string, string> data = new Dictionary<string, string>(2) {
{"sessionID", sessionID},
{"action", "join"}
{ "sessionID", sessionID },
{ "action", "join" }
};
bool result = false;
@@ -336,9 +436,9 @@ namespace ArchiSteamFarm {
string request = referer + "/accept";
Dictionary<string, string> data = new Dictionary<string, string>(3) {
{"sessionid", sessionID},
{"serverid", "1"},
{"tradeofferid", tradeID.ToString()}
{ "sessionid", sessionID },
{ "serverid", "1" },
{ "tradeofferid", tradeID.ToString() }
};
bool result = false;
@@ -354,40 +454,120 @@ namespace ArchiSteamFarm {
return true;
}
internal async Task<List<Steam.Item>> GetMyTradableInventory() {
internal async Task<HashSet<Steam.Item>> 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);
}
HashSet<Steam.Item> result = new HashSet<Steam.Item>();
if (jObject == null) {
Logging.LogGenericWTF("Request failed even after " + WebBrowser.MaxRetries + " tries", Bot.BotName);
return null;
}
ushort nextPage = 0;
while (true) {
string request = SteamCommunityURL + "/my/inventory/json/" + Steam.Item.SteamAppID + "/" + Steam.Item.SteamContextID + "?trading=1&start=" + nextPage;
IEnumerable<JToken> jTokens = jObject.SelectTokens("$.rgInventory.*");
if (jTokens == null) {
Logging.LogNullError("jTokens", Bot.BotName);
return null;
}
JObject jObject = null;
for (byte i = 0; i < WebBrowser.MaxRetries && jObject == null; i++) {
jObject = await WebBrowser.UrlGetToJObject(request).ConfigureAwait(false);
}
List<Steam.Item> result = new List<Steam.Item>();
foreach (JToken jToken in jTokens) {
try {
result.Add(JsonConvert.DeserializeObject<Steam.Item>(jToken.ToString()));
} catch (Exception e) {
Logging.LogGenericException(e, Bot.BotName);
if (jObject == null) {
Logging.LogGenericWTF("Request failed even after " + WebBrowser.MaxRetries + " tries", Bot.BotName);
return null;
}
IEnumerable<JToken> descriptions = jObject.SelectTokens("$.rgDescriptions.*");
if (descriptions == null) {
return null;
}
Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>> descriptionMap = new Dictionary<Tuple<ulong, ulong>, Tuple<uint, Steam.Item.EType>>();
foreach (JToken description in descriptions) {
string classIDString = description["classid"].ToString();
if (string.IsNullOrEmpty(classIDString)) {
continue;
}
ulong classID;
if (!ulong.TryParse(classIDString, out classID) || classID == 0) {
continue;
}
string instanceIDString = description["instanceid"].ToString();
if (string.IsNullOrEmpty(instanceIDString)) {
continue;
}
ulong instanceID;
if (!ulong.TryParse(instanceIDString, out instanceID)) {
continue;
}
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(classID, instanceID);
if (descriptionMap.ContainsKey(key)) {
continue;
}
uint appID = 0;
Steam.Item.EType type = Steam.Item.EType.Unknown;
string hashName = description["market_hash_name"].ToString();
if (!string.IsNullOrEmpty(hashName)) {
appID = GetAppIDFromMarketHashName(hashName);
}
string descriptionType = description["type"].ToString();
if (!string.IsNullOrEmpty(descriptionType)) {
type = GetItemType(descriptionType);
}
descriptionMap[key] = new Tuple<uint, Steam.Item.EType>(appID, type);
}
IEnumerable<JToken> items = jObject.SelectTokens("$.rgInventory.*");
if (descriptions == null) {
return null;
}
foreach (JToken item in items) {
Steam.Item steamItem;
try {
steamItem = JsonConvert.DeserializeObject<Steam.Item>(item.ToString());
} catch (JsonException e) {
Logging.LogGenericException(e, Bot.BotName);
continue;
}
if (steamItem == null) {
continue;
}
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(steamItem.ClassID, steamItem.InstanceID);
Tuple<uint, Steam.Item.EType> description;
if (descriptionMap.TryGetValue(key, out description)) {
steamItem.RealAppID = description.Item1;
steamItem.Type = description.Item2;
}
result.Add(steamItem);
}
bool more;
if (!bool.TryParse(jObject["more"].ToString(), out more) || !more) {
break;
}
if (!ushort.TryParse(jObject["more_start"].ToString(), out nextPage)) {
break;
}
}
return result;
}
internal async Task<bool> SendTradeOffer(List<Steam.Item> inventory, ulong partnerID, string token = null) {
internal async Task<bool> SendTradeOffer(HashSet<Steam.Item> inventory, ulong partnerID, string token = null) {
if (inventory == null || inventory.Count == 0 || partnerID == 0) {
return false;
}
@@ -402,26 +582,30 @@ namespace ArchiSteamFarm {
return false;
}
List<Steam.TradeOfferRequest> trades = new List<Steam.TradeOfferRequest>(1 + inventory.Count / Trading.MaxItemsPerTrade);
HashSet<Steam.TradeOfferRequest> trades = new HashSet<Steam.TradeOfferRequest>();
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
singleTrade.ItemsToGive.Assets.Add(new Steam.Item() {
AppID = Steam.Item.SteamAppID,
ContextID = Steam.Item.SteamContextID,
Amount = item.Amount,
AssetID = item.AssetID
});
itemID++;
}
string referer = SteamCommunityURL + "/tradeoffer/new";
@@ -429,12 +613,12 @@ namespace ArchiSteamFarm {
foreach (Steam.TradeOfferRequest trade in trades) {
Dictionary<string, string> data = new Dictionary<string, string>(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;
@@ -460,9 +644,11 @@ namespace ArchiSteamFarm {
return null;
}
string request = SteamCommunityURL + "/my/badges?p=" + page;
HtmlDocument htmlDocument = null;
for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) {
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(SteamCommunityURL + "/my/badges?p=" + page).ConfigureAwait(false);
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
}
if (htmlDocument == null) {
@@ -482,9 +668,11 @@ namespace ArchiSteamFarm {
return null;
}
string request = SteamCommunityURL + "/my/gamecards/" + appID;
HtmlDocument htmlDocument = null;
for (byte i = 0; i < WebBrowser.MaxRetries && htmlDocument == null; i++) {
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(SteamCommunityURL + "/my/gamecards/" + appID).ConfigureAwait(false);
htmlDocument = await WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
}
if (htmlDocument == null) {
@@ -500,9 +688,11 @@ namespace ArchiSteamFarm {
return false;
}
string request = SteamCommunityURL + "/my/inventory";
bool result = false;
for (byte i = 0; i < WebBrowser.MaxRetries && !result; i++) {
result = await WebBrowser.UrlGet(SteamCommunityURL + "/my/inventory").ConfigureAwait(false);
result = await WebBrowser.UrlGet(request).ConfigureAwait(false);
}
if (!result) {

View File

@@ -33,6 +33,7 @@ using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
using System.Text.RegularExpressions;
namespace ArchiSteamFarm {
internal sealed class Bot {
@@ -100,9 +101,8 @@ namespace ArchiSteamFarm {
}
// Steam keys are offered in many formats: https://support.steampowered.com/kb_article.php?ref=7480-WUSF-3601
// It's pointless to implement them all, so we'll just do a simple check if key is supposed to be valid
// Every valid key, apart from Prey one has at least two dashes
return Utilities.GetCharCountInString(key, '-') >= 2;
// This regex should catch all of them, we can always further extend it in future
return Regex.IsMatch(key, @"[0-9A-Z]{4,5}-[0-9A-Z]{4,5}-[0-9A-Z]{4,5}-?(?:(?:[0-9A-Z]{4,5}-?)?(?:[0-9A-Z]{4,5}))?");
}
internal Bot(string botName) {
@@ -295,8 +295,8 @@ namespace ArchiSteamFarm {
return true;
}
internal async Task OnFarmingFinished(bool farmedSomething) {
if (farmedSomething && BotConfig.SendOnFarmingFinished) {
internal async Task OnFarmingFinished() {
if (BotConfig.SendOnFarmingFinished) {
await ResponseSendTrade(BotConfig.SteamMasterID).ConfigureAwait(false);
}
@@ -310,11 +310,11 @@ namespace ArchiSteamFarm {
return null;
}
if (!message.StartsWith("!")) {
if (message[0] != '!') {
return await ResponseRedeem(steamID, message.Replace(",", Environment.NewLine), true).ConfigureAwait(false);
}
if (!message.Contains(" ")) {
if (message.IndexOf(' ') < 0) {
switch (message) {
case "!2fa":
return Response2FA(steamID);
@@ -350,7 +350,7 @@ namespace ArchiSteamFarm {
return ResponseUnknown(steamID);
}
} else {
string[] args = message.Split(' ');
string[] args = message.Split((char[]) null, StringSplitOptions.RemoveEmptyEntries);
switch (args[0]) {
case "!2fa":
return Response2FA(steamID, args[1]);
@@ -410,8 +410,8 @@ namespace ArchiSteamFarm {
Task.Run(() => HandleCallbacks()).Forget();
}
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
if (TwoFactorCode == null) {
// 2FA tokens are expiring soon, use limiter only when user is providing one
if (TwoFactorCode == null || BotDatabase.SteamGuardAccount != null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
}
@@ -463,6 +463,7 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo("ASF requires a few more steps to complete authenticator import...", BotName);
if (!InitializeLoginAndPassword()) {
BotDatabase.SteamGuardAccount = null;
return;
}
@@ -473,10 +474,12 @@ namespace ArchiSteamFarm {
case LoginResult.Need2FA:
userLogin.TwoFactorCode = Program.GetUserInput(Program.EUserInputType.TwoFactorAuthentication, BotName);
if (string.IsNullOrEmpty(userLogin.TwoFactorCode)) {
BotDatabase.SteamGuardAccount = null;
return;
}
break;
default:
BotDatabase.SteamGuardAccount = null;
Logging.LogGenericError("Unhandled situation: " + loginResult, BotName);
return;
}
@@ -588,12 +591,18 @@ namespace ArchiSteamFarm {
}
await Trading.LimitInventoryRequestsAsync().ConfigureAwait(false);
List<Steam.Item> inventory = await ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false);
HashSet<Steam.Item> inventory = await ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false);
if (inventory == null || inventory.Count == 0) {
return "Nothing to send, inventory seems empty!";
}
// Remove from our pending inventory all items that are not steam cards and boosters
inventory.RemoveWhere(item => item.Type != Steam.Item.EType.TradingCard && item.Type != Steam.Item.EType.FoilTradingCard && item.Type != Steam.Item.EType.BoosterPack);
if (inventory.Count == 0) {
return "Nothing to send, inventory seems empty!";
}
if (await ArchiWebHandler.SendTradeOffer(inventory, BotConfig.SteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) {
await AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false);
return "Trade offer sent successfully!";
@@ -774,12 +783,16 @@ namespace ArchiSteamFarm {
while (!string.IsNullOrEmpty(key) && currentBot != null) {
if (validate && !IsValidCdKey(key)) {
key = reader.ReadLine(); // Next key
continue; // Without changing the bot
continue; // Keep current bot
}
if (currentBot.SteamClient.IsConnected) {
if (!currentBot.SteamClient.IsConnected) {
currentBot = null; // Either bot will be changed, or loop aborted
} else {
ArchiHandler.PurchaseResponseCallback result = await currentBot.ArchiHandler.RedeemKey(key).ConfigureAwait(false);
if (result != null) {
if (result == null) {
currentBot = null; // Either bot will be changed, or loop aborted
} else {
switch (result.PurchaseResult) {
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.DuplicatedKey:
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.InvalidKey:
@@ -787,7 +800,12 @@ namespace ArchiSteamFarm {
response.Append(Environment.NewLine + "<" + currentBot.BotName + "> Key: " + key + " | Status: " + result.PurchaseResult + " | Items: " + string.Join("", result.Items));
key = reader.ReadLine(); // Next key
break; // Next bot (if needed)
if (result.PurchaseResult == ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OK) {
break; // Next bot (if needed)
} else {
continue; // Keep current bot
}
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.AlreadyOwned:
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.BaseGameRequired:
case ArchiHandler.PurchaseResponseCallback.EPurchaseResult.OnCooldown:
@@ -1226,14 +1244,27 @@ namespace ArchiSteamFarm {
string sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName);
if (string.IsNullOrEmpty(sms)) {
Logging.LogGenericWarning("Aborted!", BotName);
DelinkMobileAuthenticator();
return;
}
AuthenticatorLinker.FinalizeResult finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(sms);
if (finalizeResult != AuthenticatorLinker.FinalizeResult.Success) {
Logging.LogGenericError("Unhandled situation: " + finalizeResult, BotName);
DelinkMobileAuthenticator();
return;
AuthenticatorLinker.FinalizeResult finalizeResult;
while ((finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(sms)) != AuthenticatorLinker.FinalizeResult.Success) {
switch (finalizeResult) {
case AuthenticatorLinker.FinalizeResult.BadSMSCode:
sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName);
if (string.IsNullOrEmpty(sms)) {
Logging.LogGenericWarning("Aborted!", BotName);
DelinkMobileAuthenticator();
return;
}
break;
default:
Logging.LogGenericError("Unhandled situation: " + finalizeResult, BotName);
DelinkMobileAuthenticator();
return;
}
}
// Ensure that we also save changes made by finalization step (if any)
@@ -1248,13 +1279,13 @@ namespace ArchiSteamFarm {
return false;
}
bool result = BotDatabase.SteamGuardAccount.DeactivateAuthenticator();
if (result) {
// Try to deactivate authenticator, and assume we're safe to remove if it wasn't fully enrolled yet (even if request fails)
if (BotDatabase.SteamGuardAccount.DeactivateAuthenticator() || !BotDatabase.SteamGuardAccount.FullyEnrolled) {
BotDatabase.SteamGuardAccount = null;
return true;
}
return result;
return false;
}
private void JoinMasterChat() {
@@ -1364,10 +1395,6 @@ namespace ArchiSteamFarm {
return;
}
if (!KeepRunning) {
return;
}
if (InvalidPassword) {
InvalidPassword = false;
if (!string.IsNullOrEmpty(BotDatabase.LoginKey)) { // InvalidPassword means usually that login key has expired, if we used it
@@ -1389,10 +1416,14 @@ namespace ArchiSteamFarm {
await Utilities.SleepAsync(Program.GlobalConfig.AccountPlayingDelay * 60 * 1000).ConfigureAwait(false);
}
if (!KeepRunning || SteamClient.IsConnected) {
return;
}
Logging.LogGenericInfo("Reconnecting...", BotName);
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
if (TwoFactorCode == null) {
// 2FA tokens are expiring soon, use limiter only when user is providing one
if (TwoFactorCode == null || BotDatabase.SteamGuardAccount != null) {
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
}
@@ -1714,7 +1745,7 @@ namespace ArchiSteamFarm {
}
private void OnOfflineMessage(ArchiHandler.OfflineMessageCallback callback) {
if (callback == null) {
if (callback == null || callback.OfflineMessages == 0) {
return;
}

View File

@@ -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;

View File

@@ -37,7 +37,7 @@ namespace ArchiSteamFarm {
internal readonly ConcurrentDictionary<uint, float> GamesToFarm = new ConcurrentDictionary<uint, float>();
internal readonly HashSet<uint> CurrentGamesFarming = new HashSet<uint>();
private readonly ManualResetEvent FarmResetEvent = new ManualResetEvent(false);
private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false);
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
private readonly Bot Bot;
private readonly Timer Timer;
@@ -94,7 +94,7 @@ namespace ArchiSteamFarm {
if (!await IsAnythingToFarm().ConfigureAwait(false)) {
Semaphore.Release(); // We have nothing to do, don't forget to release semaphore
Logging.LogGenericInfo("We don't have anything to farm on this account!", Bot.BotName);
await Bot.OnFarmingFinished(false).ConfigureAwait(false);
await Bot.OnFarmingFinished().ConfigureAwait(false);
return;
}
@@ -102,8 +102,6 @@ namespace ArchiSteamFarm {
NowFarming = true;
Semaphore.Release(); // From this point we allow other calls to shut us down
bool farmedSomething = false;
do {
// Now the algorithm used for farming depends on whether account is restricted or not
if (Bot.BotConfig.CardDropsRestricted) { // If we have restricted card drops, we use complex algorithm
@@ -114,7 +112,6 @@ namespace ArchiSteamFarm {
while (gamesToFarmSolo.Count > 0) {
uint appID = gamesToFarmSolo.First();
if (await FarmSolo(appID).ConfigureAwait(false)) {
farmedSomething = true;
gamesToFarmSolo.Remove(appID);
gamesToFarmSolo.TrimExcess();
} else {
@@ -135,9 +132,7 @@ namespace ArchiSteamFarm {
Logging.LogGenericInfo("Chosen farming algorithm: Simple", Bot.BotName);
while (GamesToFarm.Count > 0) {
uint appID = GamesToFarm.Keys.FirstOrDefault();
if (await FarmSolo(appID).ConfigureAwait(false)) {
farmedSomething = true;
} else {
if (!await FarmSolo(appID).ConfigureAwait(false)) {
NowFarming = false;
return;
}
@@ -150,7 +145,7 @@ namespace ArchiSteamFarm {
NowFarming = false;
Logging.LogGenericInfo("Farming finished!", Bot.BotName);
await Bot.OnFarmingFinished(farmedSomething).ConfigureAwait(false);
await Bot.OnFarmingFinished().ConfigureAwait(false);
}
internal async Task StopFarming() {
@@ -421,7 +416,7 @@ namespace ArchiSteamFarm {
bool? keepFarming = await ShouldFarm(appID).ConfigureAwait(false);
for (ushort farmingTime = 0; farmingTime <= 60 * Program.GlobalConfig.MaxFarmingTime && keepFarming.GetValueOrDefault(true); farmingTime += Program.GlobalConfig.FarmingDelay) {
if (FarmResetEvent.WaitOne(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
success = false;
break;
}
@@ -452,7 +447,7 @@ namespace ArchiSteamFarm {
bool success = true;
while (maxHour < 2) {
if (FarmResetEvent.WaitOne(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
success = false;
break;
}

View File

@@ -55,6 +55,9 @@ namespace ArchiSteamFarm {
[JsonProperty(Required = Required.DisallowNull)]
internal bool AutoUpdates { get; private set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
internal bool AutoRestart { get; private set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
internal EUpdateChannel UpdateChannel { get; private set; } = EUpdateChannel.Stable;

View File

@@ -28,40 +28,161 @@ 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; }
internal enum EType : byte {
Unknown,
[JsonProperty(Required = Required.DisallowNull)]
internal string assetid { get; set; }
BoosterPack,
Coupon,
Gift,
SteamGems,
[JsonProperty(Required = Required.DisallowNull)]
internal string id {
get { return assetid; }
set { assetid = value; }
Emoticon,
FoilTradingCard,
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;
}
AppID = result;
}
}
internal ulong ContextID;
[JsonProperty(PropertyName = "contextid", Required = Required.DisallowNull)]
internal string ContextIDString {
get {
return ContextID.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
ulong result;
if (!ulong.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;
}
ulong result;
if (!ulong.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;
}
ulong result;
if (!ulong.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;
}
ulong result;
if (!ulong.TryParse(value, out result)) {
return;
}
InstanceID = result;
}
}
internal uint Amount;
[JsonProperty(PropertyName = "amount", Required = Required.Always)]
internal string AmountString {
get {
return Amount.ToString();
}
set {
if (string.IsNullOrEmpty(value)) {
return;
}
uint result;
if (!uint.TryParse(value, out result)) {
return;
}
Amount = result;
}
}
internal uint RealAppID { get; set; }
internal EType Type { get; set; }
}
internal sealed class ItemList {
[JsonProperty(Required = Required.Always)]
internal List<Steam.Item> assets { get; } = new List<Steam.Item>();
}
internal sealed class TradeOffer {
// REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer
internal sealed class TradeOffer { // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer
internal enum ETradeOfferState : byte {
Unknown,
Invalid,
@@ -77,46 +198,148 @@ 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<Steam.Item> items_to_give { get; } = new List<Steam.Item>();
[JsonProperty(Required = Required.Always)]
internal List<Steam.Item> items_to_receive { get; } = new List<Steam.Item>();
// 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<Item> ItemsToGive { get; } = new HashSet<Item>();
[JsonProperty(PropertyName = "items_to_receive", Required = Required.Always)]
internal HashSet<Item> ItemsToReceive { get; } = new HashSet<Item>();
// 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() {
foreach (Item item in ItemsToGive) {
if (item.AppID != Item.SteamAppID || item.ContextID != Item.SteamContextID || (item.Type != Item.EType.FoilTradingCard && 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.FoilTradingCard && item.Type != Item.EType.TradingCard)) {
return false;
}
}
return true;
}
internal bool IsPotentiallyDupesTrade() {
Dictionary<uint, Dictionary<Item.EType, uint>> ItemsToGivePerGame = new Dictionary<uint, Dictionary<Item.EType, uint>>();
foreach (Item item in ItemsToGive) {
Dictionary<Item.EType, uint> ItemsPerType;
if (!ItemsToGivePerGame.TryGetValue(item.RealAppID, out ItemsPerType)) {
ItemsPerType = new Dictionary<Item.EType, uint>();
ItemsPerType[item.Type] = item.Amount;
ItemsToGivePerGame[item.RealAppID] = ItemsPerType;
} else {
uint amount;
if (ItemsPerType.TryGetValue(item.Type, out amount)) {
ItemsPerType[item.Type] = amount + item.Amount;
} else {
ItemsPerType[item.Type] = item.Amount;
}
}
}
Dictionary<uint, Dictionary<Item.EType, uint>> ItemsToReceivePerGame = new Dictionary<uint, Dictionary<Item.EType, uint>>();
foreach (Item item in ItemsToReceive) {
Dictionary<Item.EType, uint> ItemsPerType;
if (!ItemsToReceivePerGame.TryGetValue(item.RealAppID, out ItemsPerType)) {
ItemsPerType = new Dictionary<Item.EType, uint>();
ItemsPerType[item.Type] = item.Amount;
ItemsToReceivePerGame[item.RealAppID] = ItemsPerType;
} else {
uint amount;
if (ItemsPerType.TryGetValue(item.Type, out amount)) {
ItemsPerType[item.Type] = amount + item.Amount;
} else {
ItemsPerType[item.Type] = item.Amount;
}
}
}
// Ensure that amount per type and per game matches
foreach (KeyValuePair<uint, Dictionary<Item.EType, uint>> ItemsPerGame in ItemsToGivePerGame) {
Dictionary<Item.EType, uint> otherItemsPerType;
if (!ItemsToReceivePerGame.TryGetValue(ItemsPerGame.Key, out otherItemsPerType)) {
return false;
}
foreach (KeyValuePair<Item.EType, uint> ItemsPerType in ItemsPerGame.Value) {
uint otherAmount;
if (!otherItemsPerType.TryGetValue(ItemsPerType.Key, out otherAmount)) {
return false;
}
if (ItemsPerType.Value != otherAmount) {
return false;
}
}
}
return true;
}
}
internal sealed class TradeOfferRequest {
[JsonProperty(Required = Required.Always)]
internal bool newversion { get; } = true;
internal sealed class ItemList {
[JsonProperty(PropertyName = "assets", Required = Required.Always)]
internal HashSet<Item> Assets { get; } = new HashSet<Item>();
}
[JsonProperty(Required = Required.Always)]
internal int version { get; } = 2;
[JsonProperty(PropertyName = "newversion", Required = Required.Always)]
internal bool NewVersion { get; } = true;
[JsonProperty(Required = Required.Always)]
internal Steam.ItemList me { get; } = new Steam.ItemList();
[JsonProperty(PropertyName = "version", Required = Required.Always)]
internal byte Version { get; } = 2;
[JsonProperty(Required = Required.Always)]
internal Steam.ItemList them { get; } = new Steam.ItemList();
[JsonProperty(PropertyName = "me", Required = Required.Always)]
internal ItemList ItemsToGive { get; } = new ItemList();
[JsonProperty(PropertyName = "them", Required = Required.Always)]
internal ItemList ItemsToReceive { get; } = new ItemList();
}
}
}

View File

@@ -65,13 +65,12 @@ namespace ArchiSteamFarm {
private const string GithubReleaseURL = "https://api.github.com/repos/" + GithubRepo + "/releases"; // GitHub API is HTTPS only
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
internal static readonly Version Version = Assembly.GetName().Version;
internal static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version;
private static readonly object ConsoleLock = new object();
private static readonly SemaphoreSlim SteamSemaphore = new SemaphoreSlim(1);
private static readonly ManualResetEvent ShutdownResetEvent = new ManualResetEvent(false);
private static readonly string ExecutableFile = Assembly.Location;
private static readonly ManualResetEventSlim ShutdownResetEvent = new ManualResetEventSlim(false);
private static readonly string ExecutableFile = Assembly.GetEntryAssembly().Location;
private static readonly string ExecutableName = Path.GetFileName(ExecutableFile);
private static readonly string ExecutableDirectory = Path.GetDirectoryName(ExecutableFile);
private static readonly WCF WCF = new WCF();
@@ -187,7 +186,7 @@ namespace ArchiSteamFarm {
GitHub.Asset binaryAsset = null;
foreach (var asset in releaseResponse.Assets) {
if (string.IsNullOrEmpty(asset.Name) || !asset.Name.Equals(ExecutableName)) {
if (string.IsNullOrEmpty(asset.Name) || !asset.Name.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase)) {
continue;
}
@@ -205,21 +204,22 @@ namespace ArchiSteamFarm {
return;
}
Logging.LogGenericInfo("Downloading new version...");
Stream newExe = await WebBrowser.UrlGetToStream(binaryAsset.DownloadURL).ConfigureAwait(false);
if (newExe == null) {
Logging.LogGenericWarning("Could not download new version!");
byte[] result = null;
for (byte i = 0; i < WebBrowser.MaxRetries && result == null; i++) {
Logging.LogGenericInfo("Downloading new version...");
result = await WebBrowser.UrlGetToBytes(binaryAsset.DownloadURL).ConfigureAwait(false);
}
if (result == null) {
Logging.LogGenericWTF("Request failed even after " + WebBrowser.MaxRetries + " tries");
return;
}
// We start deep update logic here
string newExeFile = ExecutableFile + ".new";
// Firstly we create new exec
try {
using (FileStream fileStream = File.Open(newExeFile, FileMode.Create)) {
await newExe.CopyToAsync(fileStream).ConfigureAwait(false);
}
File.WriteAllBytes(newExeFile, result);
} catch (Exception e) {
Logging.LogGenericException(e);
return;
@@ -250,10 +250,17 @@ namespace ArchiSteamFarm {
return;
}
Logging.LogGenericInfo("Update process is finished! ASF will now restart itself...");
await Utilities.SleepAsync(5000);
Logging.LogGenericInfo("Update process finished!");
Restart();
if (GlobalConfig.AutoRestart) {
Logging.LogGenericInfo("Restarting...");
await Utilities.SleepAsync(5000).ConfigureAwait(false);
Restart();
} else {
Logging.LogGenericInfo("Exiting...");
await Utilities.SleepAsync(5000).ConfigureAwait(false);
Exit();
}
}
internal static void Exit(int exitCode = 0) {
@@ -515,7 +522,7 @@ namespace ArchiSteamFarm {
Init(args);
// Wait for signal to shutdown
ShutdownResetEvent.WaitOne();
ShutdownResetEvent.Wait();
// We got a signal to shutdown
Exit();

View File

@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("2.0.3.0")]
[assembly: AssemblyFileVersion("2.0.3.0")]
[assembly: AssemblyVersion("2.0.4.0")]
[assembly: AssemblyFileVersion("2.0.4.0")]

View File

@@ -37,6 +37,7 @@ namespace ArchiSteamFarm {
private readonly Bot Bot;
private readonly SemaphoreSlim TradesSemaphore = new SemaphoreSlim(1);
private readonly HashSet<ulong> RecentlyParsedTrades = new HashSet<ulong>();
private byte ParsingTasks;
@@ -79,43 +80,62 @@ namespace ArchiSteamFarm {
TradesSemaphore.Release();
}
private async Task ForgetRecentTrade(ulong tradeID) {
await Utilities.SleepAsync(24 * 60 * 60 * 1000).ConfigureAwait(false);
lock (RecentlyParsedTrades) {
RecentlyParsedTrades.Remove(tradeID);
RecentlyParsedTrades.TrimExcess();
}
}
private async Task ParseActiveTrades() {
List<Steam.TradeOffer> tradeOffers = Bot.ArchiWebHandler.GetTradeOffers();
if (tradeOffers == null) {
HashSet<Steam.TradeOffer> tradeOffers = Bot.ArchiWebHandler.GetTradeOffers();
if (tradeOffers == null || tradeOffers.Count == 0) {
return;
}
lock (RecentlyParsedTrades) {
tradeOffers.RemoveWhere(trade => RecentlyParsedTrades.Contains(trade.TradeOfferID));
}
if (tradeOffers.Count == 0) {
return;
}
foreach (Steam.TradeOffer tradeOffer in tradeOffers) {
lock (RecentlyParsedTrades) {
RecentlyParsedTrades.Add(tradeOffer.TradeOfferID);
}
ForgetRecentTrade(tradeOffer.TradeOfferID).Forget();
}
await tradeOffers.ForEachAsync(ParseTrade).ConfigureAwait(false);
await Bot.AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false);
}
private async Task ParseTrade(Steam.TradeOffer tradeOffer) {
if (tradeOffer == null || tradeOffer.trade_offer_state != Steam.TradeOffer.ETradeOfferState.Active) {
if (tradeOffer == null || tradeOffer.State != Steam.TradeOffer.ETradeOfferState.Active) {
return;
}
ulong tradeID;
if (!ulong.TryParse(tradeOffer.tradeofferid, out tradeID)) {
return;
}
if (ShouldAcceptTrade(tradeOffer)) {
Logging.LogGenericInfo("Accepting trade: " + tradeID, Bot.BotName);
await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
if (await ShouldAcceptTrade(tradeOffer).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);
}
}
private bool ShouldAcceptTrade(Steam.TradeOffer tradeOffer) {
private async Task<bool> ShouldAcceptTrade(Steam.TradeOffer tradeOffer) {
if (tradeOffer == null) {
return false;
}
// 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 +143,84 @@ 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;
}
// At this point we're sure that STM trade is valid
// Now check if it's worth for us to do the trade
HashSet<Steam.Item> inventory = await Bot.ArchiWebHandler.GetMyTradableInventory().ConfigureAwait(false);
if (inventory == null || inventory.Count == 0) {
return true; // OK, assume that this trade is valid, we can't check our EQ
}
// Get appIDs we're interested in
HashSet<uint> appIDs = new HashSet<uint>();
foreach (Steam.Item item in tradeOffer.ItemsToGive) {
appIDs.Add(item.RealAppID);
}
// Now remove from our inventory all items we're NOT interested in
inventory.RemoveWhere(item => !appIDs.Contains(item.RealAppID));
// If for some reason Valve is talking crap and we can't find mentioned items, assume OK
if (inventory.Count == 0) {
return true;
}
// Now let's create a map which maps items to their amount in our EQ
Dictionary<Tuple<ulong, ulong>, uint> amountMap = new Dictionary<Tuple<ulong, ulong>, uint>();
foreach (Steam.Item item in inventory) {
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(item.ClassID, item.InstanceID);
uint amount;
if (amountMap.TryGetValue(key, out amount)) {
amountMap[key] = amount + item.Amount;
} else {
amountMap[key] = item.Amount;
}
}
// Calculate our value of items to give
uint itemsToGiveDupesValue = 0;
foreach (Steam.Item item in tradeOffer.ItemsToGive) {
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(item.ClassID, item.InstanceID);
uint amount;
if (!amountMap.TryGetValue(key, out amount)) {
continue;
}
itemsToGiveDupesValue += amount;
}
// Calculate our value of items to receive
uint itemsToReceiveDupesValue = 0;
foreach (Steam.Item item in tradeOffer.ItemsToReceive) {
Tuple<ulong, ulong> key = new Tuple<ulong, ulong>(item.ClassID, item.InstanceID);
uint amount;
if (!amountMap.TryGetValue(key, out amount)) {
continue;
}
itemsToReceiveDupesValue += amount;
}
// Trade is worth for us if we're in total trading more of our dupes for less of our dupes (or at least same amount)
// Which means that itemsToGiveDupesValue should be greater than itemsToReceiveDupesValue
return itemsToGiveDupesValue > itemsToReceiveDupesValue;
}
}
}

View File

@@ -33,6 +33,10 @@ namespace ArchiSteamFarm {
internal static void Forget(this Task task) { }
internal static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
if (action == null) {
return Task.FromResult(true);
}
return Task.WhenAll(sequence.Select(action));
}
@@ -64,20 +68,5 @@ namespace ArchiSteamFarm {
return Task.Delay(miliseconds);
}
internal static uint GetCharCountInString(string s, char c) {
if (string.IsNullOrEmpty(s)) {
return 0;
}
uint count = 0;
foreach (char singleChar in s) {
if (singleChar == c) {
count++;
}
}
return count;
}
}
}

View File

@@ -88,13 +88,9 @@ namespace ArchiSteamFarm {
return false;
}
HttpResponseMessage response = await UrlGetToResponse(request, referer).ConfigureAwait(false);
if (response == null) {
return false;
using (HttpResponseMessage response = await UrlGetToResponse(request, referer).ConfigureAwait(false)) {
return response != null;
}
response.Dispose();
return true;
}
internal async Task<bool> UrlPost(string request, Dictionary<string, string> data = null, string referer = null) {
@@ -102,29 +98,9 @@ namespace ArchiSteamFarm {
return false;
}
HttpResponseMessage response = await UrlPostToResponse(request, data, referer).ConfigureAwait(false);
if (response == null) {
return false;
using (HttpResponseMessage response = await UrlPostToResponse(request, data, referer).ConfigureAwait(false)) {
return response != null;
}
response.Dispose();
return true;
}
internal async Task<HttpResponseMessage> UrlGetToResponse(string request, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
return await UrlRequest(request, HttpMethod.Get, null, referer).ConfigureAwait(false);
}
internal async Task<HttpResponseMessage> UrlPostToResponse(string request, Dictionary<string, string> data = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
return await UrlRequest(request, HttpMethod.Post, data, referer).ConfigureAwait(false);
}
internal async Task<string> UrlGetToContent(string request, string referer = null) {
@@ -132,31 +108,27 @@ namespace ArchiSteamFarm {
return null;
}
HttpResponseMessage httpResponse = await UrlGetToResponse(request, referer).ConfigureAwait(false);
if (httpResponse == null) {
return null;
using (HttpResponseMessage httpResponse = await UrlGetToResponse(request, referer).ConfigureAwait(false)) {
if (httpResponse == null) {
return null;
}
return await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
}
string result = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
httpResponse.Dispose();
return result;
}
internal async Task<Stream> UrlGetToStream(string request, string referer = null) {
internal async Task<byte[]> UrlGetToBytes(string request, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
HttpResponseMessage httpResponse = await UrlGetToResponse(request, referer).ConfigureAwait(false);
if (httpResponse == null) {
return null;
using (HttpResponseMessage httpResponse = await UrlGetToResponse(request, referer).ConfigureAwait(false)) {
if (httpResponse == null) {
return null;
}
return await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
Stream result = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
httpResponse.Dispose();
return result;
}
internal async Task<HtmlDocument> UrlGetToHtmlDocument(string request, string referer = null) {
@@ -169,10 +141,8 @@ namespace ArchiSteamFarm {
return null;
}
content = WebUtility.HtmlDecode(content);
HtmlDocument htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(content);
htmlDocument.LoadHtml(WebUtility.HtmlDecode(content));
return htmlDocument;
}
@@ -220,12 +190,28 @@ namespace ArchiSteamFarm {
return xmlDocument;
}
private async Task<HttpResponseMessage> UrlGetToResponse(string request, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
return await UrlRequest(request, HttpMethod.Get, null, referer).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> UrlPostToResponse(string request, Dictionary<string, string> data = null, string referer = null) {
if (string.IsNullOrEmpty(request)) {
return null;
}
return await UrlRequest(request, HttpMethod.Post, data, referer).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> UrlRequest(string request, HttpMethod httpMethod, Dictionary<string, string> data = null, string referer = null) {
if (string.IsNullOrEmpty(request) || httpMethod == null) {
return null;
}
if (request.StartsWith("https://") && Program.GlobalConfig.ForceHttp) {
if (request.StartsWith("https://", StringComparison.Ordinal) && Program.GlobalConfig.ForceHttp) {
return null;
}
@@ -251,15 +237,17 @@ namespace ArchiSteamFarm {
}
}
if (responseMessage == null || !responseMessage.IsSuccessStatusCode) {
if (responseMessage == null) {
return null;
}
if (!responseMessage.IsSuccessStatusCode) {
if (Debugging.IsDebugBuild || Program.GlobalConfig.Debug) {
Logging.LogGenericError("Request: " + request + " failed!", Identifier);
if (responseMessage != null) {
Logging.LogGenericError("Status code: " + responseMessage.StatusCode, Identifier);
Logging.LogGenericError("Content: " + Environment.NewLine + await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false), Identifier);
responseMessage.Dispose();
}
Logging.LogGenericError("Status code: " + responseMessage.StatusCode, Identifier);
Logging.LogGenericError("Content: " + Environment.NewLine + await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false), Identifier);
}
responseMessage.Dispose();
return null;
}

View File

@@ -2,6 +2,7 @@
"Debug": false,
"Headless": false,
"AutoUpdates": true,
"AutoRestart": true,
"UpdateChannel": 1,
"SteamProtocol": 6,
"SteamOwnerID": 0,

View File

@@ -12,6 +12,7 @@
"FarmOffline": false,
"HandleOfflineMessages": false,
"AcceptGifts": false,
"SteamTradeMatcher": false,
"ForwardKeysToOtherBots": false,
"DistributeKeys": false,
"UseAsfAsMobileAuthenticator": false,

View File

@@ -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;

View File

@@ -54,6 +54,9 @@ namespace ConfigGenerator {
[JsonProperty(Required = Required.DisallowNull)]
public bool AutoUpdates { get; set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
public bool AutoRestart { get; set; } = true;
[JsonProperty(Required = Required.DisallowNull)]
public EUpdateChannel UpdateChannel { get; set; } = EUpdateChannel.Stable;

View File

@@ -36,7 +36,7 @@ namespace ConfigGenerator {
private const string ASFDirectory = "ArchiSteamFarm";
private static readonly string ExecutableDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
private static readonly string ExecutableDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
/// <summary>
/// The main entry point for the application.

View File

@@ -135,6 +135,7 @@
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<!--
<PostBuildEvent Condition=" '$(OS)' != 'Unix' AND '$(ConfigurationName)' == 'Release' ">
"$(SolutionDir)tools\ILRepack\ILRepack.exe" /ndebug /internalize /parallel /targetplatform:v4 /wildcards /out:"$(SolutionDir)out\ASF-GUI.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll"
del "$(SolutionDir)out\ASF-GUI.exe.config"
@@ -143,6 +144,7 @@
mono -O=all "$(SolutionDir)tools/ILRepack/ILRepack.exe" /ndebug /internalize /parallel /targetplatform:v4 /wildcards /out:"$(SolutionDir)out/ASF-GUI.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll"
rm "$(SolutionDir)out/ASF-GUI.exe.config"
</PostBuildEvent>
-->
</PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

9
appveyor.yml Normal file
View File

@@ -0,0 +1,9 @@
version: 1.0.{build}-{branch}
image: Visual Studio 2015
configuration: Release
platform: Any CPU
clone_depth: 10
build:
project: ArchiSteamFarm.sln
parallel: true
verbosity: minimal