diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs
index d30272e86..357a415c1 100644
--- a/ArchiSteamFarm/ASF.cs
+++ b/ArchiSteamFarm/ASF.cs
@@ -195,18 +195,6 @@ namespace ArchiSteamFarm {
await RestartOrExit().ConfigureAwait(false);
}
- private static async Task RestartOrExit() {
- if (Program.GlobalConfig.AutoRestart) {
- ArchiLogger.LogGenericInfo("Restarting...");
- await Task.Delay(5000).ConfigureAwait(false);
- Program.Restart();
- } else {
- ArchiLogger.LogGenericInfo("Exiting...");
- await Task.Delay(5000).ConfigureAwait(false);
- Program.Exit();
- }
- }
-
internal static void InitBots() {
if (Bot.Bots.Count != 0) {
return;
@@ -387,6 +375,18 @@ namespace ArchiSteamFarm {
CreateBot(newBotName).Forget();
}
+ private static async Task RestartOrExit() {
+ if (Program.GlobalConfig.AutoRestart) {
+ ArchiLogger.LogGenericInfo("Restarting...");
+ await Task.Delay(5000).ConfigureAwait(false);
+ Program.Restart();
+ } else {
+ ArchiLogger.LogGenericInfo("Exiting...");
+ await Task.Delay(5000).ConfigureAwait(false);
+ Program.Exit();
+ }
+ }
+
internal sealed class BotConfigEventArgs : EventArgs {
internal readonly BotConfig BotConfig;
diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj
index 288d68bb0..91945e4da 100644
--- a/ArchiSteamFarm/ArchiSteamFarm.csproj
+++ b/ArchiSteamFarm/ArchiSteamFarm.csproj
@@ -138,6 +138,7 @@
+
diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs
index da9073d18..976d48d8e 100644
--- a/ArchiSteamFarm/ArchiWebHandler.cs
+++ b/ArchiSteamFarm/ArchiWebHandler.cs
@@ -42,9 +42,9 @@ namespace ArchiSteamFarm {
private const byte MinSessionTTL = GlobalConfig.DefaultHttpTimeout / 4; // Assume session is valid for at least that amount of seconds
private const string SteamCommunityHost = "steamcommunity.com";
private const string SteamStoreHost = "store.steampowered.com";
+ private const string SteamStoreURL = "http://" + SteamStoreHost;
private static string SteamCommunityURL = "https://" + SteamCommunityHost;
- private static string SteamStoreURL = "https://" + SteamStoreHost;
private static int Timeout = GlobalConfig.DefaultHttpTimeout * 1000; // This must be int type
private readonly Bot Bot;
@@ -123,6 +123,31 @@ namespace ArchiSteamFarm {
return htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='add_free_content_success_area']") != null;
}
+ internal async Task ClearFromDiscoveryQueue(uint appID) {
+ if (appID == 0) {
+ Bot.ArchiLogger.LogNullError(nameof(appID));
+ return false;
+ }
+
+ if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
+ return false;
+ }
+
+ string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamStoreURL, "sessionid");
+ if (string.IsNullOrEmpty(sessionID)) {
+ Bot.ArchiLogger.LogNullError(nameof(sessionID));
+ return false;
+ }
+
+ string request = SteamStoreURL + "/app/" + appID;
+ Dictionary data = new Dictionary(2) {
+ { "sessionid", sessionID },
+ { "appid_to_clear_from_queue", appID.ToString() }
+ };
+
+ return await WebBrowser.UrlPostRetry(request, data).ConfigureAwait(false);
+ }
+
internal void DeclineTradeOffer(ulong tradeID) {
if ((tradeID == 0) || string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) {
Bot.ArchiLogger.LogNullError(nameof(tradeID) + " || " + nameof(Bot.BotConfig.SteamApiKey));
@@ -147,6 +172,27 @@ namespace ArchiSteamFarm {
}
}
+ internal async Task> GenerateNewDiscoveryQueue() {
+ if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
+ return null;
+ }
+
+ string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamStoreURL, "sessionid");
+ if (string.IsNullOrEmpty(sessionID)) {
+ Bot.ArchiLogger.LogNullError(nameof(sessionID));
+ return null;
+ }
+
+ string request = SteamStoreURL + "/explore/generatenewdiscoveryqueue";
+ Dictionary data = new Dictionary(2) {
+ { "sessionid", sessionID },
+ { "queuetype", "0" }
+ };
+
+ Steam.NewDiscoveryQueueResponse output = await WebBrowser.UrlPostToJsonResultRetry(request, data).ConfigureAwait(false);
+ return output?.Queue;
+ }
+
internal HashSet GetActiveTradeOffers() {
if (string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) {
Bot.ArchiLogger.LogNullError(nameof(Bot.BotConfig.SteamApiKey));
@@ -301,6 +347,15 @@ namespace ArchiSteamFarm {
return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false);
}
+ internal async Task GetDiscoveryQueuePage() {
+ if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
+ return null;
+ }
+
+ string request = SteamStoreURL + "/explore?l=english";
+ return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false);
+ }
+
internal async Task> GetFamilySharingSteamIDs() {
if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
return null;
@@ -565,6 +620,15 @@ namespace ArchiSteamFarm {
return 0;
}
+ internal async Task GetSteamAwardsPage() {
+ if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
+ return null;
+ }
+
+ string request = SteamStoreURL + "/SteamAwards?l=english";
+ return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false);
+ }
+
internal async Task GetTradeHoldDuration(ulong tradeID) {
if (tradeID == 0) {
Bot.ArchiLogger.LogNullError(nameof(tradeID));
@@ -666,7 +730,7 @@ namespace ArchiSteamFarm {
internal static void Init() {
Timeout = Program.GlobalConfig.HttpTimeout * 1000;
SteamCommunityURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamCommunityHost;
- SteamStoreURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamStoreHost;
+ //SteamStoreURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamStoreHost;
}
internal async Task Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin) {
@@ -858,6 +922,32 @@ namespace ArchiSteamFarm {
return true;
}
+ internal async Task SteamAwardsVote(byte voteID, uint appID) {
+ if ((voteID == 0) || (appID == 0)) {
+ Bot.ArchiLogger.LogNullError(nameof(voteID) + " || " + nameof(appID));
+ return false;
+ }
+
+ if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) {
+ return false;
+ }
+
+ string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamStoreURL, "sessionid");
+ if (string.IsNullOrEmpty(sessionID)) {
+ Bot.ArchiLogger.LogNullError(nameof(sessionID));
+ return false;
+ }
+
+ string request = SteamStoreURL + "/salevote";
+ Dictionary data = new Dictionary(3) {
+ { "sessionid", sessionID },
+ { "voteid", voteID.ToString() },
+ { "appid", appID.ToString() }
+ };
+
+ return await WebBrowser.UrlPostRetry(request, data).ConfigureAwait(false);
+ }
+
private static uint GetAppIDFromMarketHashName(string hashName) {
if (string.IsNullOrEmpty(hashName)) {
ASF.ArchiLogger.LogNullError(nameof(hashName));
diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs
index 899c61c8b..6b2d581f0 100755
--- a/ArchiSteamFarm/Bot.cs
+++ b/ArchiSteamFarm/Bot.cs
@@ -82,6 +82,7 @@ namespace ArchiSteamFarm {
private readonly SteamClient SteamClient;
private readonly ConcurrentHashSet SteamFamilySharingIDs = new ConcurrentHashSet();
private readonly SteamFriends SteamFriends;
+ private readonly SteamSaleEvent SteamSaleEvent;
private readonly SteamUser SteamUser;
private readonly Trading Trading;
@@ -206,6 +207,7 @@ namespace ArchiSteamFarm {
CardsFarmer = new CardsFarmer(this);
CardsFarmer.SetInitialState(BotConfig.Paused);
+ SteamSaleEvent = new SteamSaleEvent(this);
Trading = new Trading(this);
if (Program.GlobalConfig.Statistics) {
@@ -232,6 +234,7 @@ namespace ArchiSteamFarm {
InitializationSemaphore.Dispose();
SteamFamilySharingIDs.Dispose();
OwnedPackageIDs.Dispose();
+ SteamSaleEvent.Dispose();
Trading.Dispose();
// Those are objects that might be null and the check should be in-place
diff --git a/ArchiSteamFarm/JSON/Steam.cs b/ArchiSteamFarm/JSON/Steam.cs
index bd880476e..bc2631305 100644
--- a/ArchiSteamFarm/JSON/Steam.cs
+++ b/ArchiSteamFarm/JSON/Steam.cs
@@ -372,6 +372,17 @@ namespace ArchiSteamFarm.JSON {
}
}
+ [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
+ [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
+ internal sealed class NewDiscoveryQueueResponse { // Deserialized from JSON
+#pragma warning disable 649
+ [JsonProperty(PropertyName = "queue", Required = Required.Always)]
+ internal readonly HashSet Queue;
+#pragma warning restore 649
+
+ private NewDiscoveryQueueResponse() { }
+ }
+
[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")]
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
internal sealed class RedeemWalletResponse { // Deserialized from JSON
diff --git a/ArchiSteamFarm/SteamSaleEvent.cs b/ArchiSteamFarm/SteamSaleEvent.cs
new file mode 100644
index 000000000..4df3a2098
--- /dev/null
+++ b/ArchiSteamFarm/SteamSaleEvent.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using HtmlAgilityPack;
+
+namespace ArchiSteamFarm {
+ internal sealed class SteamSaleEvent : IDisposable {
+ private static readonly DateTime SaleEndingDateUtc = new DateTime(2017, 1, 2, 18, 0, 0, DateTimeKind.Utc);
+
+ private readonly Bot Bot;
+ private readonly Timer SteamAwardsTimer;
+ private readonly Timer SteamDiscoveryQueueTimer;
+
+ internal SteamSaleEvent(Bot bot) {
+ if (bot == null) {
+ throw new ArgumentNullException(nameof(bot));
+ }
+
+ Bot = bot;
+
+ if (DateTime.UtcNow >= SaleEndingDateUtc) {
+ return;
+ }
+
+ SteamAwardsTimer = new Timer(
+ async e => await VoteForSteamAwards().ConfigureAwait(false),
+ null,
+ TimeSpan.FromMinutes(1 + 0.2 * Bot.Bots.Count), // Delay
+ TimeSpan.FromHours(6) // Period
+ );
+
+ SteamDiscoveryQueueTimer = new Timer(
+ async e => await ExploreDiscoveryQueue().ConfigureAwait(false),
+ null,
+ TimeSpan.FromMinutes(1 + 0.2 * Bot.Bots.Count), // Delay
+ TimeSpan.FromHours(6) // Period
+ );
+ }
+
+ public void Dispose() {
+ SteamAwardsTimer?.Dispose();
+ SteamDiscoveryQueueTimer?.Dispose();
+ }
+
+ private async Task ExploreDiscoveryQueue() {
+ if (DateTime.UtcNow >= SaleEndingDateUtc) {
+ return;
+ }
+
+ if (!Bot.ArchiWebHandler.Ready) {
+ return;
+ }
+
+ Bot.ArchiLogger.LogGenericDebug("Started!");
+
+ for (byte i = 0; (i < 3) && !(await IsDiscoveryQueueEmpty().ConfigureAwait(false)).GetValueOrDefault(); i++) {
+ Bot.ArchiLogger.LogGenericDebug("Getting new queue...");
+ HashSet queue = await Bot.ArchiWebHandler.GenerateNewDiscoveryQueue().ConfigureAwait(false);
+ if (queue == null) {
+ Bot.ArchiLogger.LogGenericWarning("Aborting due to error!");
+ break;
+ }
+
+ Bot.ArchiLogger.LogGenericDebug("We got new queue, clearing...");
+ foreach (uint queuedAppID in queue) {
+ Bot.ArchiLogger.LogGenericDebug("Clearing " + queuedAppID + "...");
+ if (await Bot.ArchiWebHandler.ClearFromDiscoveryQueue(queuedAppID).ConfigureAwait(false)) {
+ continue;
+ }
+
+ Bot.ArchiLogger.LogGenericWarning("Aborting due to error!");
+ i = byte.MaxValue;
+ break;
+ }
+ }
+
+ Bot.ArchiLogger.LogGenericDebug("Done!");
+ }
+
+ private async Task IsDiscoveryQueueEmpty() {
+ if (!Bot.ArchiWebHandler.Ready) {
+ return null;
+ }
+
+ Bot.ArchiLogger.LogGenericDebug("Checking if discovery queue is empty...");
+ HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetDiscoveryQueuePage().ConfigureAwait(false);
+ if (htmlDocument == null) {
+ Bot.ArchiLogger.LogGenericDebug("Could not get discovery queue page, returning null");
+ return null;
+ }
+
+ HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//div[@class='subtext']");
+ if (htmlNode == null) {
+ Bot.ArchiLogger.LogNullError(nameof(htmlNode));
+ return null;
+ }
+
+ string text = htmlNode.InnerText;
+ if (!string.IsNullOrEmpty(text)) {
+ // It'd make more sense to check "Come back tomorrow", but it might not cover out-of-the-event queue
+ Bot.ArchiLogger.LogGenericDebug("Our text is: " + text);
+ return !text.StartsWith("You can get ", StringComparison.Ordinal);
+ }
+
+ Bot.ArchiLogger.LogNullError(nameof(text));
+ return null;
+ }
+
+ private async Task VoteForSteamAwards() {
+ if (DateTime.UtcNow >= SaleEndingDateUtc) {
+ return;
+ }
+
+ if (!Bot.ArchiWebHandler.Ready) {
+ return;
+ }
+
+ Bot.ArchiLogger.LogGenericDebug("Getting SteamAwards page...");
+ HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetSteamAwardsPage().ConfigureAwait(false);
+ HtmlNode voteNode = htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='vote_nomination ']");
+ if (voteNode == null) {
+ // Event ended, error or likewise
+ Bot.ArchiLogger.LogGenericDebug("Could not get SteamAwards page, returning");
+ return;
+ }
+
+ HtmlNode myVoteNode = htmlDocument.DocumentNode.SelectSingleNode("//div[@class='vote_nomination your_vote']");
+ if (myVoteNode != null) {
+ // Already voted
+ Bot.ArchiLogger.LogGenericDebug("We voted already, nothing to do");
+ return;
+ }
+
+ HtmlNode nominationsNode = htmlDocument.DocumentNode.SelectSingleNode("//div[@class='vote_nominations store_horizontal_autoslider']");
+ if (nominationsNode == null) {
+ Bot.ArchiLogger.LogNullError(nameof(nominationsNode));
+ return;
+ }
+
+ string voteIDText = nominationsNode.GetAttributeValue("data-voteid", null);
+ if (string.IsNullOrEmpty(voteIDText)) {
+ Bot.ArchiLogger.LogNullError(nameof(voteIDText));
+ return;
+ }
+
+ byte voteID;
+ if (!byte.TryParse(voteIDText, out voteID) || (voteID == 0)) {
+ Bot.ArchiLogger.LogNullError(nameof(voteID));
+ return;
+ }
+
+ string appIDText = voteNode.GetAttributeValue("data-vote-appid", null);
+ if (string.IsNullOrEmpty(appIDText)) {
+ Bot.ArchiLogger.LogNullError(nameof(appIDText));
+ return;
+ }
+
+ uint appID;
+ if (!uint.TryParse(appIDText, out appID) || (appID == 0)) {
+ Bot.ArchiLogger.LogNullError(nameof(appID));
+ return;
+ }
+
+ Bot.ArchiLogger.LogGenericDebug("Voting...");
+ await Bot.ArchiWebHandler.SteamAwardsVote(voteID, appID).ConfigureAwait(false);
+ Bot.ArchiLogger.LogGenericDebug("Done!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/GUI/GUI.csproj b/GUI/GUI.csproj
index db6599e1b..f41146719 100644
--- a/GUI/GUI.csproj
+++ b/GUI/GUI.csproj
@@ -151,6 +151,9 @@
Statistics.cs
+
+ SteamSaleEvent.cs
+
Trading.cs