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