diff --git a/ArchiSteamFarm.sln.DotSettings b/ArchiSteamFarm.sln.DotSettings index b084525c8..9a21d2744 100644 --- a/ArchiSteamFarm.sln.DotSettings +++ b/ArchiSteamFarm.sln.DotSettings @@ -90,10 +90,14 @@ END_OF_LINE True END_OF_LINE + 1 END_OF_LINE TOGETHER_SAME_LINE END_OF_LINE END_OF_LINE + 1 + 1 + False END_OF_LINE False False @@ -102,9 +106,216 @@ False LINE_BREAK True + True END_OF_LINE - 255 + 1000 False + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="ArchiPattern" Priority="150"> + <Entry DisplayName="Public (Events and Delegates)"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Or> + <Kind Is="Delegate" /> + <Kind Is="Event" /> + </Or> + </And> + </Entry.Match> + <Entry.SortBy> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Constants"> + <Entry.Match> + <Kind Is="Constant" /> + </Entry.Match> + <Entry.SortBy> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Static (Fields, Properties and Indexers)"> + <Entry.Match> + <Or> + <And> + <Kind Is="Field" /> + <Static /> + </And> + <And> + <Kind Is="Autoproperty" /> + <Static /> + </And> + <And> + <Kind Is="Property" /> + <Static /> + </And> + <And> + <Kind Is="Indexer" /> + <Static /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Access /> + <Readonly /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Non-static (Fields, Properties and Indexers)"> + <Entry.Match> + <And> + <Not> + <Static /> + </Not> + <Or> + <Kind Is="Field" /> + <Kind Is="Autoproperty" /> + <Kind Is="Property" /> + <Kind Is="Indexer" /> + </Or> + </And> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Constructors"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Interfaces"> + <Entry.Match> + <And> + <Kind Is="Member" /> + <ImplementsInterface /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Everything else"> + <Entry.SortBy> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Nested"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + <Entry.SortBy> + <Access /> + <Kind Is="Member" /> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="COM interfaces or structs"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" Inherited="True" /> + </And> + </HasMember> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <Or> + <Kind Is="Constructor" /> + <And> + <Kind Is="Method" /> + <ImplementsInterface Name="System.IDisposable" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Order="Constructor" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> +</Patterns> UseExplicitType UseExplicitType UseExplicitType @@ -129,5 +340,6 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="AaBb" /><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="AaBb" /><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> True + True True True \ No newline at end of file diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index 04a93e935..b2be71eaf 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -34,28 +34,6 @@ using ArchiSteamFarm.JSON; namespace ArchiSteamFarm { internal static class ASF { - internal sealed class BotConfigEventArgs : EventArgs { - internal readonly BotConfig BotConfig; - - internal BotConfigEventArgs(BotConfig botConfig = null) { - BotConfig = botConfig; - } - } - - internal enum EUserInputType : byte { - Unknown, - DeviceID, - Login, - Password, - PhoneNumber, - SMS, - SteamGuard, - SteamParentalPIN, - RevocationCode, - TwoFactorAuthentication, - WCFHostname - } - internal static readonly ArchiLogger ArchiLogger = new ArchiLogger(SharedInfo.ASF); private static readonly ConcurrentDictionary LastWriteTimes = new ConcurrentDictionary(); @@ -90,10 +68,7 @@ namespace ArchiSteamFarm { } if ((AutoUpdatesTimer == null) && Program.GlobalConfig.AutoUpdates) { - AutoUpdatesTimer = new Timer( - async e => await CheckForUpdate().ConfigureAwait(false), - null, - TimeSpan.FromDays(1), // Delay + AutoUpdatesTimer = new Timer(async e => await CheckForUpdate().ConfigureAwait(false), null, TimeSpan.FromDays(1), // Delay TimeSpan.FromDays(1) // Period ); @@ -258,9 +233,7 @@ namespace ArchiSteamFarm { return; } - FileSystemWatcher = new FileSystemWatcher(SharedInfo.ConfigDirectory, "*.json") { - NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite - }; + FileSystemWatcher = new FileSystemWatcher(SharedInfo.ConfigDirectory, "*.json") { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite }; FileSystemWatcher.Changed += OnChanged; FileSystemWatcher.Created += OnCreated; @@ -408,5 +381,27 @@ namespace ArchiSteamFarm { CreateBot(newBotName).Forget(); } + + internal sealed class BotConfigEventArgs : EventArgs { + internal readonly BotConfig BotConfig; + + internal BotConfigEventArgs(BotConfig botConfig = null) { + BotConfig = botConfig; + } + } + + internal enum EUserInputType : byte { + Unknown, + DeviceID, + Login, + Password, + PhoneNumber, + SMS, + SteamGuard, + SteamParentalPIN, + RevocationCode, + TwoFactorAuthentication, + WCFHostname + } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/App.config b/ArchiSteamFarm/App.config index fd9961fb2..efb4d02fd 100644 --- a/ArchiSteamFarm/App.config +++ b/ArchiSteamFarm/App.config @@ -1,6 +1,7 @@ - + + - - - - + + + + \ No newline at end of file diff --git a/ArchiSteamFarm/ArchiHandler.cs b/ArchiSteamFarm/ArchiHandler.cs index c2da0e33e..fea520c72 100644 --- a/ArchiSteamFarm/ArchiHandler.cs +++ b/ArchiSteamFarm/ArchiHandler.cs @@ -22,8 +22,6 @@ */ -using SteamKit2; -using SteamKit2.Internal; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -31,6 +29,8 @@ using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; +using SteamKit2; +using SteamKit2.Internal; namespace ArchiSteamFarm { internal sealed class ArchiHandler : ClientMsgHandler { @@ -44,187 +44,37 @@ namespace ArchiSteamFarm { ArchiLogger = archiLogger; } - /* - ____ _ _ _ _ - / ___| __ _ | || || |__ __ _ ___ | | __ ___ - | | / _` || || || '_ \ / _` | / __|| |/ // __| - | |___| (_| || || || |_) || (_| || (__ | < \__ \ - \____|\__,_||_||_||_.__/ \__,_| \___||_|\_\|___/ - - */ - - internal sealed class NotificationsCallback : CallbackMsg { - internal enum ENotification : byte { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - Unknown = 0, - Trading = 1, - // Only custom below, different than ones available as user_notification_type - Items = 254 + public override void HandleMsg(IPacketMsg packetMsg) { + if (packetMsg == null) { + ArchiLogger.LogNullError(nameof(packetMsg)); + return; } - internal readonly HashSet Notifications; - - internal NotificationsCallback(JobID jobID, CMsgClientUserNotifications msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - - if (msg.notifications.Count == 0) { - return; - } - - Notifications = new HashSet(msg.notifications.Select(notification => (ENotification) notification.user_notification_type)); - } - - internal NotificationsCallback(JobID jobID, CMsgClientItemAnnouncements msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - - if (msg.count_new_items > 0) { - Notifications = new HashSet { ENotification.Items }; - } + switch (packetMsg.MsgType) { + case EMsg.ClientFSOfflineMessageNotification: + HandleFSOfflineMessageNotification(packetMsg); + break; + case EMsg.ClientItemAnnouncements: + HandleItemAnnouncements(packetMsg); + break; + case EMsg.ClientPlayingSessionState: + HandlePlayingSessionState(packetMsg); + break; + case EMsg.ClientPurchaseResponse: + HandlePurchaseResponse(packetMsg); + break; + case EMsg.ClientRedeemGuestPassResponse: + HandleRedeemGuestPassResponse(packetMsg); + break; + case EMsg.ClientSharedLibraryLockStatus: + HandleSharedLibraryLockStatus(packetMsg); + break; + case EMsg.ClientUserNotifications: + HandleUserNotifications(packetMsg); + break; } } - internal sealed class OfflineMessageCallback : CallbackMsg { - internal readonly uint OfflineMessagesCount; - - internal OfflineMessageCallback(JobID jobID, CMsgClientOfflineMessageNotification msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - OfflineMessagesCount = msg.offline_messages; - } - } - - internal sealed class PlayingSessionStateCallback : CallbackMsg { - internal readonly bool PlayingBlocked; - - internal PlayingSessionStateCallback(JobID jobID, CMsgClientPlayingSessionState msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - PlayingBlocked = msg.playing_blocked; - } - } - - internal sealed class PurchaseResponseCallback : CallbackMsg { - internal enum EPurchaseResult : sbyte { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - Unknown = -2, - Timeout = -1, - OK = 0, - AlreadyOwned = 9, - RegionLocked = 13, - InvalidKey = 14, - DuplicatedKey = 15, - BaseGameRequired = 24, - SteamWalletCode = 50, - OnCooldown = 53 - } - - internal readonly Dictionary Items; - - internal EPurchaseResult PurchaseResult { get; set; } - - internal PurchaseResponseCallback(JobID jobID, CMsgClientPurchaseResponse msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - PurchaseResult = (EPurchaseResult) msg.purchase_result_details; - - if (msg.purchase_receipt_info == null) { - return; - } - - KeyValue receiptInfo = new KeyValue(); - using (MemoryStream ms = new MemoryStream(msg.purchase_receipt_info)) { - if (!receiptInfo.TryReadAsBinary(ms)) { - ASF.ArchiLogger.LogNullError(nameof(ms)); - return; - } - } - - List lineItems = receiptInfo["lineitems"].Children; - if (lineItems.Count == 0) { - return; - } - - Items = new Dictionary(lineItems.Count); - foreach (KeyValue lineItem in lineItems) { - uint packageID = lineItem["PackageID"].AsUnsignedInteger(); - if (packageID == 0) { - // Valid, coupons have PackageID of -1 (don't ask me why) - packageID = lineItem["ItemAppID"].AsUnsignedInteger(); - if (packageID == 0) { - ASF.ArchiLogger.LogNullError(nameof(packageID)); - return; - } - } - - string gameName = lineItem["ItemDescription"].Value; - if (string.IsNullOrEmpty(gameName)) { - ASF.ArchiLogger.LogNullError(nameof(gameName)); - return; - } - - gameName = WebUtility.HtmlDecode(gameName); // Apparently steam expects client to decode sent HTML - Items[packageID] = WebUtility.HtmlDecode(gameName); - } - } - } - - internal sealed class RedeemGuestPassResponseCallback : CallbackMsg { - internal readonly EResult Result; - - internal RedeemGuestPassResponseCallback(JobID jobID, CMsgClientRedeemGuestPassResponse msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - Result = (EResult) msg.eresult; - } - } - - internal sealed class SharedLibraryLockStatusCallback : CallbackMsg { - internal readonly ulong LibraryLockedBySteamID; - - internal SharedLibraryLockStatusCallback(JobID jobID, CMsgClientSharedLibraryLockStatus msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - - if (msg.own_library_locked_by == 0) { - return; - } - - LibraryLockedBySteamID = new SteamID(msg.own_library_locked_by, EUniverse.Public, EAccountType.Individual); - } - } - - /* - __ __ _ _ _ - | \/ | ___ | |_ | |__ ___ __| | ___ - | |\/| | / _ \| __|| '_ \ / _ \ / _` |/ __| - | | | || __/| |_ | | | || (_) || (_| |\__ \ - |_| |_| \___| \__||_| |_| \___/ \__,_||___/ - - */ - // TODO: Remove me once https://github.com/SteamRE/SteamKit/issues/305 is fixed internal void LogOnWithoutMachineID(SteamUser.LogOnDetails details) { if (details == null) { @@ -276,7 +126,6 @@ namespace ArchiSteamFarm { logon.Body.sha_sentryfile = details.SentryFileHash; logon.Body.eresult_sentryfile = (int) (details.SentryFileHash != null ? EResult.OK : EResult.FileNotFound); - Client.Send(logon); } @@ -301,19 +150,11 @@ namespace ArchiSteamFarm { ClientMsgProtobuf request = new ClientMsgProtobuf(EMsg.ClientGamesPlayed); if (!string.IsNullOrEmpty(gameName)) { - request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { - game_extra_info = gameName, - game_id = new GameID { - AppType = GameID.GameType.Shortcut, - ModID = uint.MaxValue - } - }); + request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { game_extra_info = gameName, game_id = new GameID { AppType = GameID.GameType.Shortcut, ModID = uint.MaxValue } }); } foreach (uint gameID in gameIDs.Where(gameID => gameID != 0)) { - request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { - game_id = new GameID(gameID) - }); + request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { game_id = new GameID(gameID) }); } Client.Send(request); @@ -329,9 +170,7 @@ namespace ArchiSteamFarm { return null; } - ClientMsgProtobuf request = new ClientMsgProtobuf(EMsg.ClientRedeemGuestPass) { - SourceJobID = Client.GetNextJobID() - }; + ClientMsgProtobuf request = new ClientMsgProtobuf(EMsg.ClientRedeemGuestPass) { SourceJobID = Client.GetNextJobID() }; request.Body.guest_pass_id = guestPassID; @@ -355,9 +194,7 @@ namespace ArchiSteamFarm { return null; } - ClientMsgProtobuf request = new ClientMsgProtobuf(EMsg.ClientRegisterKey) { - SourceJobID = Client.GetNextJobID() - }; + ClientMsgProtobuf request = new ClientMsgProtobuf(EMsg.ClientRegisterKey) { SourceJobID = Client.GetNextJobID() }; request.Body.key = key; @@ -371,46 +208,6 @@ namespace ArchiSteamFarm { } } - /* - _ _ _ _ - | | | | __ _ _ __ __| || | ___ _ __ ___ - | |_| | / _` || '_ \ / _` || | / _ \| '__|/ __| - | _ || (_| || | | || (_| || || __/| | \__ \ - |_| |_| \__,_||_| |_| \__,_||_| \___||_| |___/ - - */ - - public override void HandleMsg(IPacketMsg packetMsg) { - if (packetMsg == null) { - ArchiLogger.LogNullError(nameof(packetMsg)); - return; - } - - switch (packetMsg.MsgType) { - case EMsg.ClientFSOfflineMessageNotification: - HandleFSOfflineMessageNotification(packetMsg); - break; - case EMsg.ClientItemAnnouncements: - HandleItemAnnouncements(packetMsg); - break; - case EMsg.ClientPlayingSessionState: - HandlePlayingSessionState(packetMsg); - break; - case EMsg.ClientPurchaseResponse: - HandlePurchaseResponse(packetMsg); - break; - case EMsg.ClientRedeemGuestPassResponse: - HandleRedeemGuestPassResponse(packetMsg); - break; - case EMsg.ClientSharedLibraryLockStatus: - HandleSharedLibraryLockStatus(packetMsg); - break; - case EMsg.ClientUserNotifications: - HandleUserNotifications(packetMsg); - break; - } - } - private void HandleFSOfflineMessageNotification(IPacketMsg packetMsg) { if (packetMsg == null) { ArchiLogger.LogNullError(nameof(packetMsg)); @@ -480,5 +277,168 @@ namespace ArchiSteamFarm { ClientMsgProtobuf response = new ClientMsgProtobuf(packetMsg); Client.PostCallback(new NotificationsCallback(packetMsg.TargetJobID, response.Body)); } + + internal sealed class NotificationsCallback : CallbackMsg { + internal readonly HashSet Notifications; + + internal NotificationsCallback(JobID jobID, CMsgClientUserNotifications msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + + if (msg.notifications.Count == 0) { + return; + } + + Notifications = new HashSet(msg.notifications.Select(notification => (ENotification) notification.user_notification_type)); + } + + internal NotificationsCallback(JobID jobID, CMsgClientItemAnnouncements msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + + if (msg.count_new_items > 0) { + Notifications = new HashSet { ENotification.Items }; + } + } + + internal enum ENotification : byte { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + Unknown = 0, + Trading = 1, + // Only custom below, different than ones available as user_notification_type + Items = 254 + } + } + + internal sealed class OfflineMessageCallback : CallbackMsg { + internal readonly uint OfflineMessagesCount; + + internal OfflineMessageCallback(JobID jobID, CMsgClientOfflineMessageNotification msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + OfflineMessagesCount = msg.offline_messages; + } + } + + internal sealed class PlayingSessionStateCallback : CallbackMsg { + internal readonly bool PlayingBlocked; + + internal PlayingSessionStateCallback(JobID jobID, CMsgClientPlayingSessionState msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + PlayingBlocked = msg.playing_blocked; + } + } + + internal sealed class PurchaseResponseCallback : CallbackMsg { + internal readonly Dictionary Items; + + internal EPurchaseResult PurchaseResult { get; set; } + + internal PurchaseResponseCallback(JobID jobID, CMsgClientPurchaseResponse msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + PurchaseResult = (EPurchaseResult) msg.purchase_result_details; + + if (msg.purchase_receipt_info == null) { + return; + } + + KeyValue receiptInfo = new KeyValue(); + using (MemoryStream ms = new MemoryStream(msg.purchase_receipt_info)) { + if (!receiptInfo.TryReadAsBinary(ms)) { + ASF.ArchiLogger.LogNullError(nameof(ms)); + return; + } + } + + List lineItems = receiptInfo["lineitems"].Children; + if (lineItems.Count == 0) { + return; + } + + Items = new Dictionary(lineItems.Count); + foreach (KeyValue lineItem in lineItems) { + uint packageID = lineItem["PackageID"].AsUnsignedInteger(); + if (packageID == 0) { + // Valid, coupons have PackageID of -1 (don't ask me why) + packageID = lineItem["ItemAppID"].AsUnsignedInteger(); + if (packageID == 0) { + ASF.ArchiLogger.LogNullError(nameof(packageID)); + return; + } + } + + string gameName = lineItem["ItemDescription"].Value; + if (string.IsNullOrEmpty(gameName)) { + ASF.ArchiLogger.LogNullError(nameof(gameName)); + return; + } + + gameName = WebUtility.HtmlDecode(gameName); // Apparently steam expects client to decode sent HTML + Items[packageID] = WebUtility.HtmlDecode(gameName); + } + } + + internal enum EPurchaseResult : sbyte { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + Unknown = -2, + Timeout = -1, + OK = 0, + AlreadyOwned = 9, + RegionLocked = 13, + InvalidKey = 14, + DuplicatedKey = 15, + BaseGameRequired = 24, + SteamWalletCode = 50, + OnCooldown = 53 + } + } + + internal sealed class RedeemGuestPassResponseCallback : CallbackMsg { + internal readonly EResult Result; + + internal RedeemGuestPassResponseCallback(JobID jobID, CMsgClientRedeemGuestPassResponse msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + Result = (EResult) msg.eresult; + } + } + + internal sealed class SharedLibraryLockStatusCallback : CallbackMsg { + internal readonly ulong LibraryLockedBySteamID; + + internal SharedLibraryLockStatusCallback(JobID jobID, CMsgClientSharedLibraryLockStatus msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + + if (msg.own_library_locked_by == 0) { + return; + } + + LibraryLockedBySteamID = new SteamID(msg.own_library_locked_by, EUniverse.Public, EAccountType.Individual); + } + } } } \ No newline at end of file diff --git a/ArchiSteamFarm/ArchiLogger.cs b/ArchiSteamFarm/ArchiLogger.cs index 9ff07284f..56d88b3bc 100644 --- a/ArchiSteamFarm/ArchiLogger.cs +++ b/ArchiSteamFarm/ArchiLogger.cs @@ -40,24 +40,6 @@ namespace ArchiSteamFarm { Logger = LogManager.GetLogger(name); } - internal void LogGenericError(string message, [CallerMemberName] string previousMethodName = null) { - if (string.IsNullOrEmpty(message)) { - LogNullError(nameof(message)); - return; - } - - Logger.Error($"{previousMethodName}() {message}"); - } - - internal void LogGenericException(Exception exception, [CallerMemberName] string previousMethodName = null) { - if (exception == null) { - LogNullError(nameof(exception)); - return; - } - - Logger.Error(exception, $"{previousMethodName}()"); - } - [SuppressMessage("ReSharper", "LocalizableElement")] internal void LogFatalException(Exception exception, [CallerMemberName] string previousMethodName = null) { if (exception == null) { @@ -86,13 +68,31 @@ namespace ArchiSteamFarm { } } - internal void LogGenericWarning(string message, [CallerMemberName] string previousMethodName = null) { + internal void LogGenericDebug(string message, [CallerMemberName] string previousMethodName = null) { if (string.IsNullOrEmpty(message)) { LogNullError(nameof(message)); return; } - Logger.Warn($"{previousMethodName}() {message}"); + Logger.Debug($"{previousMethodName}() {message}"); + } + + internal void LogGenericError(string message, [CallerMemberName] string previousMethodName = null) { + if (string.IsNullOrEmpty(message)) { + LogNullError(nameof(message)); + return; + } + + Logger.Error($"{previousMethodName}() {message}"); + } + + internal void LogGenericException(Exception exception, [CallerMemberName] string previousMethodName = null) { + if (exception == null) { + LogNullError(nameof(exception)); + return; + } + + Logger.Error(exception, $"{previousMethodName}()"); } internal void LogGenericInfo(string message, [CallerMemberName] string previousMethodName = null) { @@ -104,24 +104,6 @@ namespace ArchiSteamFarm { Logger.Info($"{previousMethodName}() {message}"); } - [SuppressMessage("ReSharper", "ExplicitCallerInfoArgument")] - internal void LogNullError(string nullObjectName, [CallerMemberName] string previousMethodName = null) { - if (string.IsNullOrEmpty(nullObjectName)) { - return; - } - - LogGenericError(nullObjectName + " is null!", previousMethodName); - } - - internal void LogGenericDebug(string message, [CallerMemberName] string previousMethodName = null) { - if (string.IsNullOrEmpty(message)) { - LogNullError(nameof(message)); - return; - } - - Logger.Debug($"{previousMethodName}() {message}"); - } - internal void LogGenericTrace(string message, [CallerMemberName] string previousMethodName = null) { if (string.IsNullOrEmpty(message)) { LogNullError(nameof(message)); @@ -130,5 +112,23 @@ namespace ArchiSteamFarm { Logger.Trace($"{previousMethodName}() {message}"); } + + internal void LogGenericWarning(string message, [CallerMemberName] string previousMethodName = null) { + if (string.IsNullOrEmpty(message)) { + LogNullError(nameof(message)); + return; + } + + Logger.Warn($"{previousMethodName}() {message}"); + } + + [SuppressMessage("ReSharper", "ExplicitCallerInfoArgument")] + internal void LogNullError(string nullObjectName, [CallerMemberName] string previousMethodName = null) { + if (string.IsNullOrEmpty(nullObjectName)) { + return; + } + + LogGenericError(nullObjectName + " is null!", previousMethodName); + } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/ArchiServiceInstaller.cs b/ArchiSteamFarm/ArchiServiceInstaller.cs index 817193c37..aacbc6497 100644 --- a/ArchiSteamFarm/ArchiServiceInstaller.cs +++ b/ArchiSteamFarm/ArchiServiceInstaller.cs @@ -48,14 +48,11 @@ namespace ArchiSteamFarm { serviceInstaller.Installers.Clear(); - EventLogInstaller logInstaller = new EventLogInstaller { - Log = SharedInfo.EventLog, - Source = SharedInfo.EventLogSource - }; + EventLogInstaller logInstaller = new EventLogInstaller { Log = SharedInfo.EventLog, Source = SharedInfo.EventLogSource }; Installers.Add(serviceInstaller); Installers.Add(serviceProcessInstaller); Installers.Add(logInstaller); } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 6c733c9fe..8f5897fce 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -22,29 +22,29 @@ */ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using HtmlAgilityPack; -using SteamKit2; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Xml; -using System.Threading; using ArchiSteamFarm.JSON; +using HtmlAgilityPack; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SteamKit2; namespace ArchiSteamFarm { internal sealed class ArchiWebHandler : IDisposable { + 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 byte MinSessionTTL = GlobalConfig.DefaultHttpTimeout / 4; // Assume session is valid for at least that amount of seconds 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 static int Timeout = GlobalConfig.DefaultHttpTimeout*1000; // This must be int type private readonly Bot Bot; private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1); @@ -52,108 +52,8 @@ namespace ArchiSteamFarm { internal bool Ready { get; private set; } - private ulong SteamID; private DateTime LastSessionRefreshCheck = DateTime.MinValue; - - internal static void Init() { - Timeout = Program.GlobalConfig.HttpTimeout * 1000; - SteamCommunityURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamCommunityHost; - SteamStoreURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamStoreHost; - } - - private static uint GetAppIDFromMarketHashName(string hashName) { - if (string.IsNullOrEmpty(hashName)) { - ASF.ArchiLogger.LogNullError(nameof(hashName)); - return 0; - } - - int index = hashName.IndexOf('-'); - if (index <= 0) { - return 0; - } - - uint appID; - return uint.TryParse(hashName.Substring(0, index), out appID) ? appID : 0; - } - - private static Steam.Item.EType GetItemType(string name) { - if (string.IsNullOrEmpty(name)) { - ASF.ArchiLogger.LogNullError(nameof(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; - } - - if (name.EndsWith("Foil Trading Card", StringComparison.Ordinal)) { - return Steam.Item.EType.FoilTradingCard; - } - - if (name.EndsWith("Profile Background", StringComparison.Ordinal)) { - return Steam.Item.EType.ProfileBackground; - } - - return name.EndsWith("Trading Card", StringComparison.Ordinal) ? Steam.Item.EType.TradingCard : Steam.Item.EType.Unknown; - } - } - - private static bool ParseItems(Dictionary> descriptions, List input, HashSet output) { - if ((descriptions == null) || (input == null) || (input.Count == 0) || (output == null)) { - ASF.ArchiLogger.LogNullError(nameof(descriptions) + " || " + nameof(input) + " || " + nameof(output)); - return false; - } - - foreach (KeyValue item in input) { - uint appID = item["appid"].AsUnsignedInteger(); - if (appID == 0) { - ASF.ArchiLogger.LogNullError(nameof(appID)); - return false; - } - - ulong contextID = item["contextid"].AsUnsignedLong(); - if (contextID == 0) { - ASF.ArchiLogger.LogNullError(nameof(contextID)); - return false; - } - - ulong classID = item["classid"].AsUnsignedLong(); - if (classID == 0) { - ASF.ArchiLogger.LogNullError(nameof(classID)); - return false; - } - - uint amount = item["amount"].AsUnsignedInteger(); - if (amount == 0) { - ASF.ArchiLogger.LogNullError(nameof(amount)); - return false; - } - - uint realAppID = appID; - Steam.Item.EType type = Steam.Item.EType.Unknown; - - Tuple description; - if (descriptions.TryGetValue(classID, out description)) { - realAppID = description.Item1; - type = description.Item2; - } - - Steam.Item steamItem = new Steam.Item(appID, contextID, classID, amount, realAppID, type); - output.Add(steamItem); - } - - return true; - } + private ulong SteamID; internal ArchiWebHandler(Bot bot) { if (bot == null) { @@ -167,127 +67,28 @@ namespace ArchiSteamFarm { public void Dispose() => SessionSemaphore.Dispose(); - internal void OnDisconnected() => Ready = false; - - internal async Task Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin) { - if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce) || string.IsNullOrEmpty(parentalPin)) { - Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(universe) + " || " + nameof(webAPIUserNonce) + " || " + nameof(parentalPin)); - return false; + internal async Task AcceptTradeOffer(ulong tradeID) { + if (tradeID == 0) { + Bot.ArchiLogger.LogNullError(nameof(tradeID)); + return; } - SteamID = steamID; - - string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString())); - - // Generate an AES session key - byte[] sessionKey = SteamKit2.CryptoHelper.GenerateRandomBlock(32); - - // RSA encrypt it with the public key for the universe we're on - byte[] cryptedSessionKey; - using (RSACrypto rsa = new RSACrypto(KeyDictionary.GetPublicKey(universe))) { - cryptedSessionKey = rsa.Encrypt(sessionKey); - } - - // Copy our login key - byte[] loginKey = new byte[webAPIUserNonce.Length]; - Array.Copy(Encoding.ASCII.GetBytes(webAPIUserNonce), loginKey, webAPIUserNonce.Length); - - // AES encrypt the loginkey with our session key - byte[] cryptedLoginKey = SteamKit2.CryptoHelper.SymmetricEncrypt(loginKey, sessionKey); - - // Do the magic - Bot.ArchiLogger.LogGenericInfo("Logging in to ISteamUserAuth..."); - - KeyValue authResult; - using (dynamic iSteamUserAuth = WebAPI.GetInterface("ISteamUserAuth")) { - iSteamUserAuth.Timeout = Timeout; - - try { - authResult = iSteamUserAuth.AuthenticateUser( - steamid: steamID, - sessionkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedSessionKey, 0, cryptedSessionKey.Length)), - encrypted_loginkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedLoginKey, 0, cryptedLoginKey.Length)), - method: WebRequestMethods.Http.Post, - secure: !Program.GlobalConfig.ForceHttp - ); - } catch (Exception e) { - Bot.ArchiLogger.LogGenericException(e); - return false; - } - } - - if (authResult == null) { - Bot.ArchiLogger.LogNullError(nameof(authResult)); - return false; - } - - string steamLogin = authResult["token"].Value; - if (string.IsNullOrEmpty(steamLogin)) { - Bot.ArchiLogger.LogNullError(nameof(steamLogin)); - return false; - } - - string steamLoginSecure = authResult["tokensecure"].Value; - if (string.IsNullOrEmpty(steamLoginSecure)) { - Bot.ArchiLogger.LogNullError(nameof(steamLoginSecure)); - return false; - } - - WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamCommunityHost)); - WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamStoreHost)); - - WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamCommunityHost)); - WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamStoreHost)); - - WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamCommunityHost)); - WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamStoreHost)); - - Bot.ArchiLogger.LogGenericInfo("Success!"); - - // Unlock Steam Parental if needed - if (!parentalPin.Equals("0")) { - if (!await UnlockParentalAccount(parentalPin).ConfigureAwait(false)) { - return false; - } - } - - Ready = true; - LastSessionRefreshCheck = DateTime.Now; - return true; - } - - internal async Task> GetFamilySharingSteamIDs() { if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; + return; } - string request = SteamStoreURL + "/account/managedevices"; - HtmlDocument htmlDocument = await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); - - HtmlNodeCollection htmlNodes = htmlDocument?.DocumentNode.SelectNodes("(//table[@class='accountTable'])[last()]//a/@data-miniprofile"); - if (htmlNodes == null) { - return null; // OK, no authorized steamIDs + string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamCommunityURL, "sessionid"); + if (string.IsNullOrEmpty(sessionID)) { + Bot.ArchiLogger.LogNullError(nameof(sessionID)); + return; } - HashSet result = new HashSet(); + string referer = SteamCommunityURL + "/tradeoffer/" + tradeID; + string request = referer + "/accept"; - foreach (string miniProfile in htmlNodes.Select(htmlNode => htmlNode.GetAttributeValue("data-miniprofile", null))) { - if (string.IsNullOrEmpty(miniProfile)) { - Bot.ArchiLogger.LogNullError(nameof(miniProfile)); - return null; - } + Dictionary data = new Dictionary(3) { { "sessionid", sessionID }, { "serverid", "1" }, { "tradeofferid", tradeID.ToString() } }; - uint steamID3; - if (!uint.TryParse(miniProfile, out steamID3) || (steamID3 == 0)) { - Bot.ArchiLogger.LogNullError(nameof(steamID3)); - return null; - } - - ulong steamID = new SteamID(steamID3, EUniverse.Public, EAccountType.Individual); - result.Add(steamID); - } - - return result; + await WebBrowser.UrlPostRetry(request, data, referer).ConfigureAwait(false); } internal async Task AddFreeLicense(uint subID) { @@ -307,180 +108,25 @@ namespace ArchiSteamFarm { } string request = SteamStoreURL + "/checkout/addfreelicense"; - Dictionary data = new Dictionary(3) { - { "sessionid", sessionID }, - { "subid", subID.ToString() }, - { "action", "add_to_cart" } - }; + Dictionary data = new Dictionary(3) { { "sessionid", sessionID }, { "subid", subID.ToString() }, { "action", "add_to_cart" } }; HtmlDocument htmlDocument = await WebBrowser.UrlPostToHtmlDocumentRetry(request, data).ConfigureAwait(false); return htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='add_free_content_success_area']") != null; } - internal async Task JoinGroup(ulong groupID) { - if (groupID == 0) { - Bot.ArchiLogger.LogNullError(nameof(groupID)); - return false; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return false; - } - - string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamCommunityURL, "sessionid"); - if (string.IsNullOrEmpty(sessionID)) { - Bot.ArchiLogger.LogNullError(nameof(sessionID)); - return false; - } - - string request = SteamCommunityURL + "/gid/" + groupID; - Dictionary data = new Dictionary(2) { - { "sessionID", sessionID }, - { "action", "join" } - }; - - return await WebBrowser.UrlPostRetry(request, data).ConfigureAwait(false); - } - - internal async Task GetConfirmations(string deviceID, string confirmationHash, uint time) { - if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0)) { - Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time)); - return null; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; - } - - string request = SteamCommunityURL + "/mobileconf/conf?l=english&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf"; - return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); - } - - internal async Task GetConfirmationDetails(string deviceID, string confirmationHash, uint time, MobileAuthenticator.Confirmation confirmation) { - if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmation == null)) { - Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmation)); - return null; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; - } - - string request = SteamCommunityURL + "/mobileconf/details/" + confirmation.ID + "?l=english&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf"; - - Steam.ConfirmationDetails response = await WebBrowser.UrlGetToJsonResultRetry(request).ConfigureAwait(false); - if ((response == null) || !response.Success) { - return null; - } - - response.Confirmation = confirmation; - return response; - } - - internal async Task HandleConfirmation(string deviceID, string confirmationHash, uint time, uint confirmationID, ulong confirmationKey, bool accept) { - if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmationID == 0) || (confirmationKey == 0)) { - Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmationID) + " || " + nameof(confirmationKey)); - return null; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; - } - - string request = SteamCommunityURL + "/mobileconf/ajaxop?op=" + (accept ? "allow" : "cancel") + "&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf&cid=" + confirmationID + "&ck=" + confirmationKey; - - Steam.ConfirmationResponse response = await WebBrowser.UrlGetToJsonResultRetry(request).ConfigureAwait(false); - return response?.Success; - } - - internal async Task HandleConfirmations(string deviceID, string confirmationHash, uint time, HashSet confirmations, bool accept) { - if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmations == null) || (confirmations.Count == 0)) { - Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmations)); - return null; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; - } - - string request = SteamCommunityURL + "/mobileconf/multiajaxop"; - - List> data = new List>(7 + confirmations.Count * 2) { - new KeyValuePair("op", accept ? "allow" : "cancel"), - new KeyValuePair("p", deviceID), - new KeyValuePair("a", SteamID.ToString()), - new KeyValuePair("k", confirmationHash), - new KeyValuePair("t", time.ToString()), - new KeyValuePair("m", "android"), - new KeyValuePair("tag", "conf") - }; - - foreach (MobileAuthenticator.Confirmation confirmation in confirmations) { - data.Add(new KeyValuePair("cid[]", confirmation.ID.ToString())); - data.Add(new KeyValuePair("ck[]", confirmation.Key.ToString())); - } - - Steam.ConfirmationResponse response = await WebBrowser.UrlPostToJsonResultRetry(request, data).ConfigureAwait(false); - return response?.Success; - } - - internal async Task> GetOwnedGames() { - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; - } - - string request = SteamCommunityURL + "/my/games/?xml=1"; - - XmlDocument response = await WebBrowser.UrlGetToXMLRetry(request).ConfigureAwait(false); - - XmlNodeList xmlNodeList = response?.SelectNodes("gamesList/games/game"); - if ((xmlNodeList == null) || (xmlNodeList.Count == 0)) { - return null; - } - - Dictionary result = new Dictionary(xmlNodeList.Count); - foreach (XmlNode xmlNode in xmlNodeList) { - XmlNode appNode = xmlNode.SelectSingleNode("appID"); - if (appNode == null) { - Bot.ArchiLogger.LogNullError(nameof(appNode)); - return null; - } - - uint appID; - if (!uint.TryParse(appNode.InnerText, out appID)) { - Bot.ArchiLogger.LogNullError(nameof(appID)); - return null; - } - - XmlNode nameNode = xmlNode.SelectSingleNode("name"); - if (nameNode == null) { - Bot.ArchiLogger.LogNullError(nameof(nameNode)); - return null; - } - - result[appID] = nameNode.InnerText; - } - - return result; - } - - internal Dictionary GetOwnedGames(ulong steamID) { - if ((steamID == 0) || string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { - Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(Bot.BotConfig.SteamApiKey)); - return null; + internal void DeclineTradeOffer(ulong tradeID) { + if ((tradeID == 0) || string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { + Bot.ArchiLogger.LogNullError(nameof(tradeID) + " || " + nameof(Bot.BotConfig.SteamApiKey)); + return; } KeyValue response = null; for (byte i = 0; (i < WebBrowser.MaxRetries) && (response == null); i++) { - using (dynamic iPlayerService = WebAPI.GetInterface("IPlayerService", Bot.BotConfig.SteamApiKey)) { - iPlayerService.Timeout = Timeout; + using (dynamic iEconService = WebAPI.GetInterface("IEconService", Bot.BotConfig.SteamApiKey)) { + iEconService.Timeout = Timeout; try { - response = iPlayerService.GetOwnedGames( - steamid: steamID, - include_appinfo: 1, - secure: !Program.GlobalConfig.ForceHttp - ); + response = iEconService.DeclineTradeOffer(tradeofferid: tradeID.ToString(), method: WebRequestMethods.Http.Post, secure: !Program.GlobalConfig.ForceHttp); } catch (Exception e) { Bot.ArchiLogger.LogGenericException(e); } @@ -489,116 +135,7 @@ namespace ArchiSteamFarm { if (response == null) { Bot.ArchiLogger.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries"); - return null; } - - Dictionary result = new Dictionary(response["games"].Children.Count); - foreach (KeyValue game in response["games"].Children) { - uint appID = game["appid"].AsUnsignedInteger(); - if (appID == 0) { - Bot.ArchiLogger.LogNullError(nameof(appID)); - return null; - } - - result[appID] = game["name"].Value; - } - - return result; - } - - internal uint GetServerTime() { - KeyValue response = null; - for (byte i = 0; (i < WebBrowser.MaxRetries) && (response == null); i++) { - using (dynamic iTwoFactorService = WebAPI.GetInterface("ITwoFactorService")) { - iTwoFactorService.Timeout = Timeout; - - try { - response = iTwoFactorService.QueryTime( - method: WebRequestMethods.Http.Post, - secure: !Program.GlobalConfig.ForceHttp - ); - } catch (Exception e) { - Bot.ArchiLogger.LogGenericException(e); - } - } - } - - if (response != null) { - return response["server_time"].AsUnsignedInteger(); - } - - Bot.ArchiLogger.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries"); - return 0; - } - - internal async Task GetTradeHoldDuration(ulong tradeID) { - if (tradeID == 0) { - Bot.ArchiLogger.LogNullError(nameof(tradeID)); - return null; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; - } - - string request = SteamCommunityURL + "/tradeoffer/" + tradeID + "?l=english"; - - HtmlDocument htmlDocument = await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); - - HtmlNode htmlNode = htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='pagecontent']/script"); - if (htmlNode == null) { // Trade can be no longer valid - return null; - } - - string text = htmlNode.InnerText; - if (string.IsNullOrEmpty(text)) { - Bot.ArchiLogger.LogNullError(nameof(text)); - return null; - } - - int index = text.IndexOf("g_daysTheirEscrow = ", StringComparison.Ordinal); - if (index < 0) { - Bot.ArchiLogger.LogNullError(nameof(index)); - return null; - } - - index += 20; - text = text.Substring(index); - - index = text.IndexOf(';'); - if (index < 0) { - Bot.ArchiLogger.LogNullError(nameof(index)); - return null; - } - - text = text.Substring(0, index); - - byte holdDuration; - if (byte.TryParse(text, out holdDuration)) { - return holdDuration; - } - - Bot.ArchiLogger.LogNullError(nameof(holdDuration)); - return null; - } - - internal async Task RedeemWalletKey(string key) { - if (string.IsNullOrEmpty(key)) { - Bot.ArchiLogger.LogNullError(nameof(key)); - return ArchiHandler.PurchaseResponseCallback.EPurchaseResult.Unknown; - } - - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return ArchiHandler.PurchaseResponseCallback.EPurchaseResult.Timeout; - } - - string request = SteamStoreURL + "/account/validatewalletcode"; - Dictionary data = new Dictionary(1) { - { "wallet_code", key } - }; - - Steam.RedeemWalletResponse response = await WebBrowser.UrlPostToJsonResultRetry(request, data).ConfigureAwait(false); - return response?.PurchaseResult ?? ArchiHandler.PurchaseResponseCallback.EPurchaseResult.Timeout; } internal HashSet GetActiveTradeOffers() { @@ -613,12 +150,7 @@ namespace ArchiSteamFarm { iEconService.Timeout = Timeout; try { - response = iEconService.GetTradeOffers( - get_received_offers: 1, - active_only: 1, - get_descriptions: 1, - secure: !Program.GlobalConfig.ForceHttp - ); + response = iEconService.GetTradeOffers(get_received_offers: 1, active_only: 1, get_descriptions: 1, secure: !Program.GlobalConfig.ForceHttp); } catch (Exception e) { Bot.ArchiLogger.LogGenericException(e); } @@ -711,60 +243,101 @@ namespace ArchiSteamFarm { return result; } - internal async Task AcceptTradeOffer(ulong tradeID) { - if (tradeID == 0) { - Bot.ArchiLogger.LogNullError(nameof(tradeID)); - return; + internal async Task GetBadgePage(byte page) { + if (page == 0) { + Bot.ArchiLogger.LogNullError(nameof(page)); + return null; } if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return; + return null; } - string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamCommunityURL, "sessionid"); - if (string.IsNullOrEmpty(sessionID)) { - Bot.ArchiLogger.LogNullError(nameof(sessionID)); - return; - } - - string referer = SteamCommunityURL + "/tradeoffer/" + tradeID; - string request = referer + "/accept"; - - Dictionary data = new Dictionary(3) { - { "sessionid", sessionID }, - { "serverid", "1" }, - { "tradeofferid", tradeID.ToString() } - }; - - await WebBrowser.UrlPostRetry(request, data, referer).ConfigureAwait(false); + string request = SteamCommunityURL + "/my/badges?l=english&p=" + page; + return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); } - internal void DeclineTradeOffer(ulong tradeID) { - if ((tradeID == 0) || string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { - Bot.ArchiLogger.LogNullError(nameof(tradeID) + " || " + nameof(Bot.BotConfig.SteamApiKey)); - return; + internal async Task GetConfirmationDetails(string deviceID, string confirmationHash, uint time, MobileAuthenticator.Confirmation confirmation) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmation == null)) { + Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmation)); + return null; } - KeyValue response = null; - for (byte i = 0; (i < WebBrowser.MaxRetries) && (response == null); i++) { - using (dynamic iEconService = WebAPI.GetInterface("IEconService", Bot.BotConfig.SteamApiKey)) { - iEconService.Timeout = Timeout; + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } - try { - response = iEconService.DeclineTradeOffer( - tradeofferid: tradeID.ToString(), - method: WebRequestMethods.Http.Post, - secure: !Program.GlobalConfig.ForceHttp - ); - } catch (Exception e) { - Bot.ArchiLogger.LogGenericException(e); - } + string request = SteamCommunityURL + "/mobileconf/details/" + confirmation.ID + "?l=english&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf"; + + Steam.ConfirmationDetails response = await WebBrowser.UrlGetToJsonResultRetry(request).ConfigureAwait(false); + if ((response == null) || !response.Success) { + return null; + } + + response.Confirmation = confirmation; + return response; + } + + internal async Task GetConfirmations(string deviceID, string confirmationHash, uint time) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0)) { + Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time)); + return null; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/mobileconf/conf?l=english&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf"; + return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); + } + + internal async Task> GetFamilySharingSteamIDs() { + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamStoreURL + "/account/managedevices"; + HtmlDocument htmlDocument = await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); + + HtmlNodeCollection htmlNodes = htmlDocument?.DocumentNode.SelectNodes("(//table[@class='accountTable'])[last()]//a/@data-miniprofile"); + if (htmlNodes == null) { + return null; // OK, no authorized steamIDs + } + + HashSet result = new HashSet(); + + foreach (string miniProfile in htmlNodes.Select(htmlNode => htmlNode.GetAttributeValue("data-miniprofile", null))) { + if (string.IsNullOrEmpty(miniProfile)) { + Bot.ArchiLogger.LogNullError(nameof(miniProfile)); + return null; } + + uint steamID3; + if (!uint.TryParse(miniProfile, out steamID3) || (steamID3 == 0)) { + Bot.ArchiLogger.LogNullError(nameof(steamID3)); + return null; + } + + ulong steamID = new SteamID(steamID3, EUniverse.Public, EAccountType.Individual); + result.Add(steamID); } - if (response == null) { - Bot.ArchiLogger.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries"); + return result; + } + + internal async Task GetGameCardsPage(ulong appID) { + if (appID == 0) { + Bot.ArchiLogger.LogNullError(nameof(appID)); + return null; } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/my/gamecards/" + appID + "?l=english"; + return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); } internal async Task> GetMySteamInventory(bool tradable) { @@ -883,6 +456,333 @@ namespace ArchiSteamFarm { return result; } + internal async Task> GetOwnedGames() { + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/my/games/?xml=1"; + + XmlDocument response = await WebBrowser.UrlGetToXMLRetry(request).ConfigureAwait(false); + + XmlNodeList xmlNodeList = response?.SelectNodes("gamesList/games/game"); + if ((xmlNodeList == null) || (xmlNodeList.Count == 0)) { + return null; + } + + Dictionary result = new Dictionary(xmlNodeList.Count); + foreach (XmlNode xmlNode in xmlNodeList) { + XmlNode appNode = xmlNode.SelectSingleNode("appID"); + if (appNode == null) { + Bot.ArchiLogger.LogNullError(nameof(appNode)); + return null; + } + + uint appID; + if (!uint.TryParse(appNode.InnerText, out appID)) { + Bot.ArchiLogger.LogNullError(nameof(appID)); + return null; + } + + XmlNode nameNode = xmlNode.SelectSingleNode("name"); + if (nameNode == null) { + Bot.ArchiLogger.LogNullError(nameof(nameNode)); + return null; + } + + result[appID] = nameNode.InnerText; + } + + return result; + } + + internal Dictionary GetOwnedGames(ulong steamID) { + if ((steamID == 0) || string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { + Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(Bot.BotConfig.SteamApiKey)); + return null; + } + + KeyValue response = null; + for (byte i = 0; (i < WebBrowser.MaxRetries) && (response == null); i++) { + using (dynamic iPlayerService = WebAPI.GetInterface("IPlayerService", Bot.BotConfig.SteamApiKey)) { + iPlayerService.Timeout = Timeout; + + try { + response = iPlayerService.GetOwnedGames(steamid: steamID, include_appinfo: 1, secure: !Program.GlobalConfig.ForceHttp); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericException(e); + } + } + } + + if (response == null) { + Bot.ArchiLogger.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries"); + return null; + } + + Dictionary result = new Dictionary(response["games"].Children.Count); + foreach (KeyValue game in response["games"].Children) { + uint appID = game["appid"].AsUnsignedInteger(); + if (appID == 0) { + Bot.ArchiLogger.LogNullError(nameof(appID)); + return null; + } + + result[appID] = game["name"].Value; + } + + return result; + } + + internal uint GetServerTime() { + KeyValue response = null; + for (byte i = 0; (i < WebBrowser.MaxRetries) && (response == null); i++) { + using (dynamic iTwoFactorService = WebAPI.GetInterface("ITwoFactorService")) { + iTwoFactorService.Timeout = Timeout; + + try { + response = iTwoFactorService.QueryTime(method: WebRequestMethods.Http.Post, secure: !Program.GlobalConfig.ForceHttp); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericException(e); + } + } + } + + if (response != null) { + return response["server_time"].AsUnsignedInteger(); + } + + Bot.ArchiLogger.LogGenericWarning("Request failed even after " + WebBrowser.MaxRetries + " tries"); + return 0; + } + + internal async Task GetTradeHoldDuration(ulong tradeID) { + if (tradeID == 0) { + Bot.ArchiLogger.LogNullError(nameof(tradeID)); + return null; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/tradeoffer/" + tradeID + "?l=english"; + + HtmlDocument htmlDocument = await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); + + HtmlNode htmlNode = htmlDocument?.DocumentNode.SelectSingleNode("//div[@class='pagecontent']/script"); + if (htmlNode == null) { // Trade can be no longer valid + return null; + } + + string text = htmlNode.InnerText; + if (string.IsNullOrEmpty(text)) { + Bot.ArchiLogger.LogNullError(nameof(text)); + return null; + } + + int index = text.IndexOf("g_daysTheirEscrow = ", StringComparison.Ordinal); + if (index < 0) { + Bot.ArchiLogger.LogNullError(nameof(index)); + return null; + } + + index += 20; + text = text.Substring(index); + + index = text.IndexOf(';'); + if (index < 0) { + Bot.ArchiLogger.LogNullError(nameof(index)); + return null; + } + + text = text.Substring(0, index); + + byte holdDuration; + if (byte.TryParse(text, out holdDuration)) { + return holdDuration; + } + + Bot.ArchiLogger.LogNullError(nameof(holdDuration)); + return null; + } + + internal async Task HandleConfirmation(string deviceID, string confirmationHash, uint time, uint confirmationID, ulong confirmationKey, bool accept) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmationID == 0) || (confirmationKey == 0)) { + Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmationID) + " || " + nameof(confirmationKey)); + return null; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/mobileconf/ajaxop?op=" + (accept ? "allow" : "cancel") + "&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf&cid=" + confirmationID + "&ck=" + confirmationKey; + + Steam.ConfirmationResponse response = await WebBrowser.UrlGetToJsonResultRetry(request).ConfigureAwait(false); + return response?.Success; + } + + internal async Task HandleConfirmations(string deviceID, string confirmationHash, uint time, HashSet confirmations, bool accept) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmations == null) || (confirmations.Count == 0)) { + Bot.ArchiLogger.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmations)); + return null; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/mobileconf/multiajaxop"; + + List> data = new List>(7 + confirmations.Count*2) { new KeyValuePair("op", accept ? "allow" : "cancel"), new KeyValuePair("p", deviceID), new KeyValuePair("a", SteamID.ToString()), new KeyValuePair("k", confirmationHash), new KeyValuePair("t", time.ToString()), new KeyValuePair("m", "android"), new KeyValuePair("tag", "conf") }; + + foreach (MobileAuthenticator.Confirmation confirmation in confirmations) { + data.Add(new KeyValuePair("cid[]", confirmation.ID.ToString())); + data.Add(new KeyValuePair("ck[]", confirmation.Key.ToString())); + } + + Steam.ConfirmationResponse response = await WebBrowser.UrlPostToJsonResultRetry(request, data).ConfigureAwait(false); + return response?.Success; + } + + internal static void Init() { + Timeout = Program.GlobalConfig.HttpTimeout*1000; + SteamCommunityURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamCommunityHost; + SteamStoreURL = (Program.GlobalConfig.ForceHttp ? "http://" : "https://") + SteamStoreHost; + } + + internal async Task Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string parentalPin) { + if ((steamID == 0) || (universe == EUniverse.Invalid) || string.IsNullOrEmpty(webAPIUserNonce) || string.IsNullOrEmpty(parentalPin)) { + Bot.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(universe) + " || " + nameof(webAPIUserNonce) + " || " + nameof(parentalPin)); + return false; + } + + SteamID = steamID; + + string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString())); + + // Generate an AES session key + byte[] sessionKey = SteamKit2.CryptoHelper.GenerateRandomBlock(32); + + // RSA encrypt it with the public key for the universe we're on + byte[] cryptedSessionKey; + using (RSACrypto rsa = new RSACrypto(KeyDictionary.GetPublicKey(universe))) { + cryptedSessionKey = rsa.Encrypt(sessionKey); + } + + // Copy our login key + byte[] loginKey = new byte[webAPIUserNonce.Length]; + Array.Copy(Encoding.ASCII.GetBytes(webAPIUserNonce), loginKey, webAPIUserNonce.Length); + + // AES encrypt the loginkey with our session key + byte[] cryptedLoginKey = SteamKit2.CryptoHelper.SymmetricEncrypt(loginKey, sessionKey); + + // Do the magic + Bot.ArchiLogger.LogGenericInfo("Logging in to ISteamUserAuth..."); + + KeyValue authResult; + using (dynamic iSteamUserAuth = WebAPI.GetInterface("ISteamUserAuth")) { + iSteamUserAuth.Timeout = Timeout; + + try { + authResult = iSteamUserAuth.AuthenticateUser(steamid: steamID, sessionkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedSessionKey, 0, cryptedSessionKey.Length)), encrypted_loginkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedLoginKey, 0, cryptedLoginKey.Length)), method: WebRequestMethods.Http.Post, secure: !Program.GlobalConfig.ForceHttp); + } catch (Exception e) { + Bot.ArchiLogger.LogGenericException(e); + return false; + } + } + + if (authResult == null) { + Bot.ArchiLogger.LogNullError(nameof(authResult)); + return false; + } + + string steamLogin = authResult["token"].Value; + if (string.IsNullOrEmpty(steamLogin)) { + Bot.ArchiLogger.LogNullError(nameof(steamLogin)); + return false; + } + + string steamLoginSecure = authResult["tokensecure"].Value; + if (string.IsNullOrEmpty(steamLoginSecure)) { + Bot.ArchiLogger.LogNullError(nameof(steamLoginSecure)); + return false; + } + + WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamCommunityHost)); + WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamStoreHost)); + + WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamCommunityHost)); + WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", "." + SteamStoreHost)); + + WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamCommunityHost)); + WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", "." + SteamStoreHost)); + + Bot.ArchiLogger.LogGenericInfo("Success!"); + + // Unlock Steam Parental if needed + if (!parentalPin.Equals("0")) { + if (!await UnlockParentalAccount(parentalPin).ConfigureAwait(false)) { + return false; + } + } + + Ready = true; + LastSessionRefreshCheck = DateTime.Now; + return true; + } + + internal async Task JoinGroup(ulong groupID) { + if (groupID == 0) { + Bot.ArchiLogger.LogNullError(nameof(groupID)); + return false; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return false; + } + + string sessionID = WebBrowser.CookieContainer.GetCookieValue(SteamCommunityURL, "sessionid"); + if (string.IsNullOrEmpty(sessionID)) { + Bot.ArchiLogger.LogNullError(nameof(sessionID)); + return false; + } + + string request = SteamCommunityURL + "/gid/" + groupID; + Dictionary data = new Dictionary(2) { { "sessionID", sessionID }, { "action", "join" } }; + + return await WebBrowser.UrlPostRetry(request, data).ConfigureAwait(false); + } + + internal async Task MarkInventory() { + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return false; + } + + string request = SteamCommunityURL + "/my/inventory"; + return await WebBrowser.UrlHeadRetry(request).ConfigureAwait(false); + } + + internal void OnDisconnected() => Ready = false; + + internal async Task RedeemWalletKey(string key) { + if (string.IsNullOrEmpty(key)) { + Bot.ArchiLogger.LogNullError(nameof(key)); + return ArchiHandler.PurchaseResponseCallback.EPurchaseResult.Unknown; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return ArchiHandler.PurchaseResponseCallback.EPurchaseResult.Timeout; + } + + string request = SteamStoreURL + "/account/validatewalletcode"; + Dictionary data = new Dictionary(1) { { "wallet_code", key } }; + + Steam.RedeemWalletResponse response = await WebBrowser.UrlPostToJsonResultRetry(request, data).ConfigureAwait(false); + return response?.PurchaseResult ?? ArchiHandler.PurchaseResponseCallback.EPurchaseResult.Timeout; + } + internal async Task SendTradeOffer(HashSet inventory, ulong partnerID, string token = null) { if ((inventory == null) || (inventory.Count == 0) || (partnerID == 0)) { Bot.ArchiLogger.LogNullError(nameof(inventory) + " || " + nameof(inventory.Count) + " || " + nameof(partnerID)); @@ -920,14 +820,7 @@ namespace ArchiSteamFarm { string referer = SteamCommunityURL + "/tradeoffer/new"; string request = referer + "/send"; - foreach (Dictionary data in trades.Select(trade => 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}\"}}" } - })) { + foreach (Dictionary data in trades.Select(trade => 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}\"}}" } })) { if (!await WebBrowser.UrlPostRetry(request, data, referer).ConfigureAwait(false)) { return false; } @@ -936,41 +829,51 @@ namespace ArchiSteamFarm { return true; } - internal async Task GetBadgePage(byte page) { - if (page == 0) { - Bot.ArchiLogger.LogNullError(nameof(page)); - return null; + private static uint GetAppIDFromMarketHashName(string hashName) { + if (string.IsNullOrEmpty(hashName)) { + ASF.ArchiLogger.LogNullError(nameof(hashName)); + return 0; } - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; + int index = hashName.IndexOf('-'); + if (index <= 0) { + return 0; } - string request = SteamCommunityURL + "/my/badges?l=english&p=" + page; - return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); + uint appID; + return uint.TryParse(hashName.Substring(0, index), out appID) ? appID : 0; } - internal async Task GetGameCardsPage(ulong appID) { - if (appID == 0) { - Bot.ArchiLogger.LogNullError(nameof(appID)); - return null; + private static Steam.Item.EType GetItemType(string name) { + if (string.IsNullOrEmpty(name)) { + ASF.ArchiLogger.LogNullError(nameof(name)); + return Steam.Item.EType.Unknown; } - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return null; + 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; + } + + if (name.EndsWith("Foil Trading Card", StringComparison.Ordinal)) { + return Steam.Item.EType.FoilTradingCard; + } + + if (name.EndsWith("Profile Background", StringComparison.Ordinal)) { + return Steam.Item.EType.ProfileBackground; + } + + return name.EndsWith("Trading Card", StringComparison.Ordinal) ? Steam.Item.EType.TradingCard : Steam.Item.EType.Unknown; } - - string request = SteamCommunityURL + "/my/gamecards/" + appID + "?l=english"; - return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); - } - - internal async Task MarkInventory() { - if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { - return false; - } - - string request = SteamCommunityURL + "/my/inventory"; - return await WebBrowser.UrlHeadRetry(request).ConfigureAwait(false); } private async Task IsLoggedIn() { @@ -982,6 +885,53 @@ namespace ArchiSteamFarm { return !uri?.AbsolutePath.StartsWith("/login", StringComparison.Ordinal); } + private static bool ParseItems(Dictionary> descriptions, List input, HashSet output) { + if ((descriptions == null) || (input == null) || (input.Count == 0) || (output == null)) { + ASF.ArchiLogger.LogNullError(nameof(descriptions) + " || " + nameof(input) + " || " + nameof(output)); + return false; + } + + foreach (KeyValue item in input) { + uint appID = item["appid"].AsUnsignedInteger(); + if (appID == 0) { + ASF.ArchiLogger.LogNullError(nameof(appID)); + return false; + } + + ulong contextID = item["contextid"].AsUnsignedLong(); + if (contextID == 0) { + ASF.ArchiLogger.LogNullError(nameof(contextID)); + return false; + } + + ulong classID = item["classid"].AsUnsignedLong(); + if (classID == 0) { + ASF.ArchiLogger.LogNullError(nameof(classID)); + return false; + } + + uint amount = item["amount"].AsUnsignedInteger(); + if (amount == 0) { + ASF.ArchiLogger.LogNullError(nameof(amount)); + return false; + } + + uint realAppID = appID; + Steam.Item.EType type = Steam.Item.EType.Unknown; + + Tuple description; + if (descriptions.TryGetValue(classID, out description)) { + realAppID = description.Item1; + type = description.Item2; + } + + Steam.Item steamItem = new Steam.Item(appID, contextID, classID, amount, realAppID, type); + output.Add(steamItem); + } + + return true; + } + private async Task RefreshSessionIfNeeded() { if (DateTime.Now.Subtract(LastSessionRefreshCheck).TotalSeconds < MinSessionTTL) { return true; @@ -1016,9 +966,7 @@ namespace ArchiSteamFarm { Bot.ArchiLogger.LogGenericInfo("Unlocking parental account..."); string request = SteamCommunityURL + "/parental/ajaxunlock"; - Dictionary data = new Dictionary(1) { - { "pin", parentalPin } - }; + Dictionary data = new Dictionary(1) { { "pin", parentalPin } }; bool result = await WebBrowser.UrlPostRetry(request, data, SteamCommunityURL).ConfigureAwait(false); if (!result) { @@ -1030,4 +978,4 @@ namespace ArchiSteamFarm { return true; } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 389af288b..876f1b36c 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -22,9 +22,6 @@ */ -using Newtonsoft.Json; -using SteamKit2; -using SteamKit2.Internal; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -32,26 +29,18 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using ArchiSteamFarm.JSON; +using Newtonsoft.Json; +using SteamKit2; using SteamKit2.Discovery; +using SteamKit2.Internal; namespace ArchiSteamFarm { internal sealed class Bot : IDisposable { - [Flags] - private enum ERedeemFlags : byte { - None = 0, - Validate = 1, - ForceForwarding = 2, - SkipForwarding = 4, - ForceDistribution = 8, - SkipDistribution = 16, - SkipInitial = 32 - } - private const ushort CallbackSleep = 500; // In miliseconds private const byte FamilySharingInactivityMinutes = 5; private const uint LoginID = 0; // This must be the same for all ASF bots and all ASF processes @@ -62,104 +51,52 @@ namespace ArchiSteamFarm { private static readonly SemaphoreSlim GiftsSemaphore = new SemaphoreSlim(1); private static readonly SemaphoreSlim LoginSemaphore = new SemaphoreSlim(1); - internal readonly string BotName; internal readonly ArchiHandler ArchiHandler; internal readonly ArchiLogger ArchiLogger; internal readonly ArchiWebHandler ArchiWebHandler; - - private readonly string SentryFile; - private readonly BotDatabase BotDatabase; - private readonly CallbackManager CallbackManager; - - [JsonProperty] - private readonly CardsFarmer CardsFarmer; - - private readonly ConcurrentHashSet HandledGifts = new ConcurrentHashSet(); - private readonly ConcurrentHashSet SteamFamilySharingIDs = new ConcurrentHashSet(); - private readonly ConcurrentHashSet OwnedPackageIDs = new ConcurrentHashSet(); - private readonly SemaphoreSlim CallbackSemaphore = new SemaphoreSlim(1); - private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1); - private readonly SteamApps SteamApps; - private readonly SteamClient SteamClient; - private readonly SteamFriends SteamFriends; - private readonly SteamUser SteamUser; - private readonly Timer HeartBeatTimer; - private readonly Trading Trading; - - [JsonProperty] - internal bool KeepRunning { get; private set; } - - internal bool IsLimitedUser { get; private set; } - internal BotConfig BotConfig { get; private set; } + internal readonly string BotName; internal bool HasMobileAuthenticator => BotDatabase.MobileAuthenticator != null; internal bool IsConnectedAndLoggedOn => SteamClient.IsConnected && (SteamClient.SteamID != null); internal bool IsPlayingPossible => !PlayingBlocked && (LibraryLockedBySteamID == 0); internal ulong SteamID => SteamClient.SteamID; - private bool FirstTradeSent, PlayingBlocked, SkipFirstShutdown; - private byte HeartBeatFailures; + private readonly BotDatabase BotDatabase; + private readonly CallbackManager CallbackManager; + private readonly SemaphoreSlim CallbackSemaphore = new SemaphoreSlim(1); + + [JsonProperty] + private readonly CardsFarmer CardsFarmer; + + private readonly ConcurrentHashSet HandledGifts = new ConcurrentHashSet(); + private readonly Timer HeartBeatTimer; + private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1); + private readonly ConcurrentHashSet OwnedPackageIDs = new ConcurrentHashSet(); + + private readonly string SentryFile; + private readonly SteamApps SteamApps; + private readonly SteamClient SteamClient; + private readonly ConcurrentHashSet SteamFamilySharingIDs = new ConcurrentHashSet(); + private readonly SteamFriends SteamFriends; + private readonly SteamUser SteamUser; + private readonly Trading Trading; + + internal BotConfig BotConfig { get; private set; } + internal bool IsLimitedUser { get; private set; } + + [JsonProperty] + internal bool KeepRunning { get; private set; } + + private Timer AcceptConfirmationsTimer; private string AuthCode, TwoFactorCode; - private ulong LibraryLockedBySteamID; + private Timer FamilySharingInactivityTimer; + private bool FirstTradeSent; + private byte HeartBeatFailures; private EResult LastLogOnResult; - private Timer AcceptConfirmationsTimer, FamilySharingInactivityTimer, SendItemsTimer; - - internal static string GetAPIStatus() { - var response = new { - Bots - }; - - try { - return JsonConvert.SerializeObject(response); - } catch (JsonException e) { - ASF.ArchiLogger.LogGenericException(e); - return null; - } - } - - internal static void InitializeCMs(uint cellID, IServerListProvider serverListProvider) { - if (serverListProvider == null) { - ASF.ArchiLogger.LogNullError(nameof(serverListProvider)); - return; - } - - CMClient.Servers.CellID = cellID; - CMClient.Servers.ServerListProvider = serverListProvider; - } - - private static bool IsOwner(ulong steamID) { - if (steamID != 0) { - return (steamID == Program.GlobalConfig.SteamOwnerID) || (Debugging.IsDebugBuild && (steamID == SharedInfo.ArchiSteamID)); - } - - ASF.ArchiLogger.LogNullError(nameof(steamID)); - return false; - } - - private static bool IsValidCdKey(string key) { - if (!string.IsNullOrEmpty(key)) { - return Regex.IsMatch(key, @"^[0-9A-Z]{4,7}-[0-9A-Z]{4,7}-[0-9A-Z]{4,7}(?:(?:-[0-9A-Z]{4,7})?(?:-[0-9A-Z]{4,7}))?$", RegexOptions.IgnoreCase); - } - - ASF.ArchiLogger.LogNullError(nameof(key)); - return false; - } - - private static async Task LimitGiftsRequestsAsync() { - await GiftsSemaphore.WaitAsync().ConfigureAwait(false); - Task.Run(async () => { - await Task.Delay(Program.GlobalConfig.GiftsLimiterDelay * 1000).ConfigureAwait(false); - GiftsSemaphore.Release(); - }).Forget(); - } - - private static async Task LimitLoginRequestsAsync() { - await LoginSemaphore.WaitAsync().ConfigureAwait(false); - Task.Run(async () => { - await Task.Delay(Program.GlobalConfig.LoginLimiterDelay * 1000).ConfigureAwait(false); - LoginSemaphore.Release(); - }).Forget(); - } + private ulong LibraryLockedBySteamID; + private bool PlayingBlocked; + private Timer SendItemsTimer; + private bool SkipFirstShutdown; internal Bot(string botName) { if (string.IsNullOrEmpty(botName)) { @@ -262,97 +199,13 @@ namespace ArchiSteamFarm { Trading = new Trading(this); - HeartBeatTimer = new Timer( - async e => await HeartBeat().ConfigureAwait(false), - null, - TimeSpan.FromMinutes(1) + TimeSpan.FromMinutes(0.2 * Bots.Count), // Delay + HeartBeatTimer = new Timer(async e => await HeartBeat().ConfigureAwait(false), null, TimeSpan.FromMinutes(1) + TimeSpan.FromMinutes(0.2*Bots.Count), // Delay TimeSpan.FromMinutes(1) // Period ); Initialize().Forget(); } - internal async Task OnNewConfigLoaded(ASF.BotConfigEventArgs args) { - if (args == null) { - ArchiLogger.LogNullError(nameof(args)); - return; - } - - if (args.BotConfig == null) { - Destroy(); - return; - } - - if (args.BotConfig == BotConfig) { - return; - } - - await InitializationSemaphore.WaitAsync().ConfigureAwait(false); - - try { - if (args.BotConfig == BotConfig) { - return; - } - - Stop(false); - BotConfig = args.BotConfig; - - CardsFarmer.SetInitialState(BotConfig.Paused); - - if (BotConfig.AcceptConfirmationsPeriod > 0) { - TimeSpan delay = TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) + TimeSpan.FromMinutes(0.2 * Bots.Count); - TimeSpan period = TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod); - - if (AcceptConfirmationsTimer == null) { - AcceptConfirmationsTimer = new Timer( - async e => await AcceptConfirmations(true).ConfigureAwait(false), - null, - delay, // Delay - period // Period - ); - } else { - AcceptConfirmationsTimer.Change(delay, period); - } - } else { - AcceptConfirmationsTimer?.Change(Timeout.Infinite, Timeout.Infinite); - AcceptConfirmationsTimer?.Dispose(); - } - - if ((BotConfig.SendTradePeriod > 0) && (BotConfig.SteamMasterID != 0)) { - TimeSpan delay = TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromMinutes(Bots.Count); - TimeSpan period = TimeSpan.FromHours(BotConfig.SendTradePeriod); - - if (SendItemsTimer == null) { - SendItemsTimer = new Timer( - async e => await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false), - null, - delay, // Delay - period // Period - ); - } else { - SendItemsTimer.Change(delay, period); - } - } else { - SendItemsTimer?.Change(Timeout.Infinite, Timeout.Infinite); - SendItemsTimer?.Dispose(); - } - - await Initialize().ConfigureAwait(false); - } finally { - InitializationSemaphore.Release(); - } - } - - private async Task Initialize() { - if (!BotConfig.Enabled) { - ArchiLogger.LogGenericInfo("Not starting this instance because it's disabled in config file"); - return; - } - - // Start - await Start().ConfigureAwait(false); - } - public void Dispose() { // Those are objects that are always being created if constructor doesn't throw exception ArchiWebHandler.Dispose(); @@ -400,10 +253,7 @@ namespace ArchiSteamFarm { Steam.ConfirmationDetails[] detailsResults = await Task.WhenAll(confirmations.Select(BotDatabase.MobileAuthenticator.GetConfirmationDetails)).ConfigureAwait(false); - HashSet ignoredConfirmations = new HashSet(detailsResults.Where(details => (details != null) && ( - ((acceptedSteamID != 0) && (details.OtherSteamID64 != 0) && (acceptedSteamID != details.OtherSteamID64)) || - ((acceptedTradeIDs != null) && (details.TradeOfferID != 0) && !acceptedTradeIDs.Contains(details.TradeOfferID)) - )).Select(details => details.Confirmation)); + HashSet ignoredConfirmations = new HashSet(detailsResults.Where(details => (details != null) && (((acceptedSteamID != 0) && (details.OtherSteamID64 != 0) && (acceptedSteamID != details.OtherSteamID64)) || ((acceptedTradeIDs != null) && (details.TradeOfferID != 0) && !acceptedTradeIDs.Contains(details.TradeOfferID)))).Select(details => details.Confirmation)); if (ignoredConfirmations.Count > 0) { confirmations.ExceptWith(ignoredConfirmations); @@ -418,6 +268,119 @@ namespace ArchiSteamFarm { } } + internal static string GetAPIStatus() { + var response = new { Bots }; + + try { + return JsonConvert.SerializeObject(response); + } catch (JsonException e) { + ASF.ArchiLogger.LogGenericException(e); + return null; + } + } + + internal static void InitializeCMs(uint cellID, IServerListProvider serverListProvider) { + if (serverListProvider == null) { + ASF.ArchiLogger.LogNullError(nameof(serverListProvider)); + return; + } + + CMClient.Servers.CellID = cellID; + CMClient.Servers.ServerListProvider = serverListProvider; + } + + internal async Task LootIfNeeded() { + if (!BotConfig.SendOnFarmingFinished || (BotConfig.SteamMasterID == 0) || !IsConnectedAndLoggedOn || (BotConfig.SteamMasterID == SteamClient.SteamID)) { + return; + } + + await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false); + } + + internal async Task OnFarmingFinished(bool farmedSomething) { + OnFarmingStopped(); + + if (farmedSomething || !FirstTradeSent) { + FirstTradeSent = true; + await LootIfNeeded().ConfigureAwait(false); + } + + if (BotConfig.ShutdownOnFarmingFinished) { + if (SkipFirstShutdown) { + SkipFirstShutdown = false; + } else { + Stop(); + } + } + } + + internal void OnFarmingStopped() => ResetGamesPlayed(); + + internal async Task OnNewConfigLoaded(ASF.BotConfigEventArgs args) { + if (args == null) { + ArchiLogger.LogNullError(nameof(args)); + return; + } + + if (args.BotConfig == null) { + Destroy(); + return; + } + + if (args.BotConfig == BotConfig) { + return; + } + + await InitializationSemaphore.WaitAsync().ConfigureAwait(false); + + try { + if (args.BotConfig == BotConfig) { + return; + } + + Stop(false); + BotConfig = args.BotConfig; + + CardsFarmer.SetInitialState(BotConfig.Paused); + + if (BotConfig.AcceptConfirmationsPeriod > 0) { + TimeSpan delay = TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod) + TimeSpan.FromMinutes(0.2*Bots.Count); + TimeSpan period = TimeSpan.FromMinutes(BotConfig.AcceptConfirmationsPeriod); + + if (AcceptConfirmationsTimer == null) { + AcceptConfirmationsTimer = new Timer(async e => await AcceptConfirmations(true).ConfigureAwait(false), null, delay, // Delay + period // Period + ); + } else { + AcceptConfirmationsTimer.Change(delay, period); + } + } else { + AcceptConfirmationsTimer?.Change(Timeout.Infinite, Timeout.Infinite); + AcceptConfirmationsTimer?.Dispose(); + } + + if ((BotConfig.SendTradePeriod > 0) && (BotConfig.SteamMasterID != 0)) { + TimeSpan delay = TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromMinutes(Bots.Count); + TimeSpan period = TimeSpan.FromHours(BotConfig.SendTradePeriod); + + if (SendItemsTimer == null) { + SendItemsTimer = new Timer(async e => await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false), null, delay, // Delay + period // Period + ); + } else { + SendItemsTimer.Change(delay, period); + } + } else { + SendItemsTimer?.Change(Timeout.Infinite, Timeout.Infinite); + SendItemsTimer?.Dispose(); + } + + await Initialize().ConfigureAwait(false); + } finally { + InitializationSemaphore.Release(); + } + } + internal async Task RefreshSession() { if (!IsConnectedAndLoggedOn) { return false; @@ -446,50 +409,6 @@ namespace ArchiSteamFarm { return false; } - internal void Stop(bool withShutdownEvent = true) { - if (!KeepRunning) { - return; - } - - ArchiLogger.LogGenericInfo("Stopping..."); - KeepRunning = false; - - if (SteamClient.IsConnected) { - Disconnect(); - } - - if (withShutdownEvent) { - Events.OnBotShutdown(); - } - } - - internal async Task LootIfNeeded() { - if (!BotConfig.SendOnFarmingFinished || (BotConfig.SteamMasterID == 0) || !IsConnectedAndLoggedOn || (BotConfig.SteamMasterID == SteamClient.SteamID)) { - return; - } - - await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false); - } - - internal void OnFarmingStopped() => ResetGamesPlayed(); - - internal async Task OnFarmingFinished(bool farmedSomething) { - OnFarmingStopped(); - - if (farmedSomething || !FirstTradeSent) { - FirstTradeSent = true; - await LootIfNeeded().ConfigureAwait(false); - } - - if (BotConfig.ShutdownOnFarmingFinished) { - if (SkipFirstShutdown) { - SkipFirstShutdown = false; - } else { - Stop(); - } - } - } - internal async Task Response(ulong steamID, string message) { if ((steamID == 0) || string.IsNullOrEmpty(message)) { ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(message)); @@ -625,41 +544,41 @@ namespace ArchiSteamFarm { } } - private void Destroy(bool force = false) { - if (!force) { - Stop(); - } else { - // Stop() will most likely block due to fuckup, don't wait for it - Task.Run(() => Stop()).Forget(); - } - - Bot ignored; - Bots.TryRemove(BotName, out ignored); - } - - private async Task HeartBeat() { - if (!IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) { + internal void Stop(bool withShutdownEvent = true) { + if (!KeepRunning) { return; } - try { - await SteamApps.PICSGetProductInfo(0, null); - } catch { - if (!IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) { - return; - } + ArchiLogger.LogGenericInfo("Stopping..."); + KeepRunning = false; - if (++HeartBeatFailures >= 15) { - HeartBeatFailures = byte.MaxValue; - ArchiLogger.LogGenericError("HeartBeat failed to disconnect the client, abandoning this bot instance!"); - Destroy(true); - new Bot(BotName).Forget(); - return; - } - - ArchiLogger.LogGenericWarning("Connection to Steam Network lost, reconnecting..."); - Connect(true).Forget(); + if (SteamClient.IsConnected) { + Disconnect(); } + + if (withShutdownEvent) { + Events.OnBotShutdown(); + } + } + + private void CheckFamilySharingInactivity() { + if (!IsPlayingPossible) { + return; + } + + ArchiLogger.LogGenericInfo("Shared library has not been launched in given time period, farming process resumed!"); + CardsFarmer.Resume(false); + } + + private void CheckOccupationStatus() { + if (!IsPlayingPossible) { + ArchiLogger.LogGenericInfo("Account is currently being used, ASF will resume farming when it's free..."); + StopFamilySharingInactivityTimer(); + return; + } + + ArchiLogger.LogGenericInfo("Account is no longer occupied, farming process resumed!"); + CardsFarmer.Resume(false); } private async Task Connect(bool force = false) { @@ -686,81 +605,78 @@ namespace ArchiSteamFarm { } } + private void Destroy(bool force = false) { + if (!force) { + Stop(); + } else { + // Stop() will most likely block due to fuckup, don't wait for it + Task.Run(() => Stop()).Forget(); + } + + Bot ignored; + Bots.TryRemove(BotName, out ignored); + } + private void Disconnect() { lock (SteamClient) { SteamClient.Disconnect(); } } - private async Task Start() { - if (!KeepRunning) { - KeepRunning = true; - Task.Run(() => HandleCallbacks()).Forget(); - ArchiLogger.LogGenericInfo("Starting..."); - } + private void HandleCallbacks() { + TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep); + while (KeepRunning || SteamClient.IsConnected) { + if (!CallbackSemaphore.Wait(0)) { + return; + } - await Connect().ConfigureAwait(false); + try { + CallbackManager.RunWaitCallbacks(timeSpan); + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + } finally { + CallbackSemaphore.Release(); + } + } } - private bool IsMaster(ulong steamID) { - if (steamID != 0) { - return (steamID == BotConfig.SteamMasterID) || IsOwner(steamID); - } - - ArchiLogger.LogNullError(nameof(steamID)); - return false; - } - - private void CheckOccupationStatus() { - if (!IsPlayingPossible) { - ArchiLogger.LogGenericInfo("Account is currently being used, ASF will resume farming when it's free..."); - StopFamilySharingInactivityTimer(); + private async Task HandleMessage(ulong chatID, ulong steamID, string message) { + if ((chatID == 0) || (steamID == 0) || string.IsNullOrEmpty(message)) { + ArchiLogger.LogNullError(nameof(chatID) + " || " + nameof(steamID) + " || " + nameof(message)); return; } - ArchiLogger.LogGenericInfo("Account is no longer occupied, farming process resumed!"); - CardsFarmer.Resume(false); - } - - private void CheckFamilySharingInactivity() { - if (!IsPlayingPossible) { + string response = await Response(steamID, message).ConfigureAwait(false); + if (string.IsNullOrEmpty(response)) { // We respond with null when user is not authorized (and similar) return; } - ArchiLogger.LogGenericInfo("Shared library has not been launched in given time period, farming process resumed!"); - CardsFarmer.Resume(false); + SendMessage(chatID, response); } - private void StartFamilySharingInactivityTimer() { - if (FamilySharingInactivityTimer != null) { + private async Task HeartBeat() { + if (!IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) { return; } - FamilySharingInactivityTimer = new Timer( - e => CheckFamilySharingInactivity(), - null, - TimeSpan.FromMinutes(FamilySharingInactivityMinutes), // Delay - Timeout.InfiniteTimeSpan // Period - ); - } + try { + await SteamApps.PICSGetProductInfo(0, null); + } catch { + if (!IsConnectedAndLoggedOn || (HeartBeatFailures == byte.MaxValue)) { + return; + } - private void StopFamilySharingInactivityTimer() { - if (FamilySharingInactivityTimer == null) { - return; + if (++HeartBeatFailures >= 15) { + HeartBeatFailures = byte.MaxValue; + ArchiLogger.LogGenericError("HeartBeat failed to disconnect the client, abandoning this bot instance!"); + Destroy(true); + new Bot(BotName).Forget(); + return; + } + + ArchiLogger.LogGenericWarning("Connection to Steam Network lost, reconnecting..."); + Connect(true).Forget(); } - - FamilySharingInactivityTimer.Change(Timeout.Infinite, Timeout.Infinite); - FamilySharingInactivityTimer.Dispose(); - FamilySharingInactivityTimer = null; - } - - private async Task InitializeFamilySharing() { - HashSet steamIDs = await ArchiWebHandler.GetFamilySharingSteamIDs().ConfigureAwait(false); - if ((steamIDs == null) || (steamIDs.Count == 0)) { - return; - } - - SteamFamilySharingIDs.ReplaceIfNeededWith(steamIDs); } private void ImportAuthenticator(string maFilePath) { @@ -800,284 +716,668 @@ namespace ArchiSteamFarm { ArchiLogger.LogGenericInfo("Successfully finished importing mobile authenticator!"); } - private string ResponsePassword(ulong steamID) { - if (steamID == 0) { - ArchiLogger.LogNullError(nameof(steamID)); - return null; + private async Task Initialize() { + if (!BotConfig.Enabled) { + ArchiLogger.LogGenericInfo("Not starting this instance because it's disabled in config file"); + return; } - if (!IsMaster(steamID)) { - return null; - } - - if (string.IsNullOrEmpty(BotConfig.SteamPassword)) { - return "Can't encrypt null password!"; - } - - return Environment.NewLine + - "[" + CryptoHelper.ECryptoMethod.AES + "] password: " + CryptoHelper.Encrypt(CryptoHelper.ECryptoMethod.AES, BotConfig.SteamPassword) + Environment.NewLine + - "[" + CryptoHelper.ECryptoMethod.ProtectedDataForCurrentUser + "] password: " + CryptoHelper.Encrypt(CryptoHelper.ECryptoMethod.ProtectedDataForCurrentUser, BotConfig.SteamPassword); + // Start + await Start().ConfigureAwait(false); } - private static string ResponsePassword(ulong steamID, string botName) { - if ((steamID == 0) || string.IsNullOrEmpty(botName)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); - return null; + private async Task InitializeFamilySharing() { + HashSet steamIDs = await ArchiWebHandler.GetFamilySharingSteamIDs().ConfigureAwait(false); + if ((steamIDs == null) || (steamIDs.Count == 0)) { + return; } - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return bot.ResponsePassword(steamID); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; + SteamFamilySharingIDs.ReplaceIfNeededWith(steamIDs); } - private async Task ResponsePause(ulong steamID, bool sticky) { - if (steamID == 0) { - ArchiLogger.LogNullError(nameof(steamID)); - return null; + private bool InitializeLoginAndPassword(bool requiresPassword) { + if (string.IsNullOrEmpty(BotConfig.SteamLogin)) { + BotConfig.SteamLogin = Program.GetUserInput(ASF.EUserInputType.Login, BotName); + if (string.IsNullOrEmpty(BotConfig.SteamLogin)) { + return false; + } } - if (!IsMaster(steamID) && !SteamFamilySharingIDs.Contains(steamID)) { - return null; + if (!string.IsNullOrEmpty(BotConfig.SteamPassword) || (!requiresPassword && !string.IsNullOrEmpty(BotDatabase.LoginKey))) { + return true; } - if (!IsConnectedAndLoggedOn) { - return "This bot instance is not connected!"; - } - - if (CardsFarmer.Paused) { - return "Automatic farming is paused already!"; - } - - await CardsFarmer.Pause(sticky).ConfigureAwait(false); - - if (!SteamFamilySharingIDs.Contains(steamID)) { - return "Automatic farming is now paused!"; - } - - StartFamilySharingInactivityTimer(); - return "Automatic farming is now paused! You have " + FamilySharingInactivityMinutes + " minutes to start a game."; + BotConfig.SteamPassword = Program.GetUserInput(ASF.EUserInputType.Password, BotName); + return !string.IsNullOrEmpty(BotConfig.SteamPassword); } - private static async Task ResponsePause(ulong steamID, string botName, bool sticky) { - if ((steamID == 0) || string.IsNullOrEmpty(botName)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); - return null; + private bool IsMaster(ulong steamID) { + if (steamID != 0) { + return (steamID == BotConfig.SteamMasterID) || IsOwner(steamID); } - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return await bot.ResponsePause(steamID, sticky).ConfigureAwait(false); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; + ArchiLogger.LogNullError(nameof(steamID)); + return false; } - private string ResponseResume(ulong steamID) { - if (steamID == 0) { - ArchiLogger.LogNullError(nameof(steamID)); - return null; + private static bool IsOwner(ulong steamID) { + if (steamID != 0) { + return (steamID == Program.GlobalConfig.SteamOwnerID) || (Debugging.IsDebugBuild && (steamID == SharedInfo.ArchiSteamID)); } - if (!IsMaster(steamID) && !SteamFamilySharingIDs.Contains(steamID)) { - return null; - } - - if (!IsConnectedAndLoggedOn) { - return "This bot instance is not connected!"; - } - - if (!CardsFarmer.Paused) { - return "Automatic farming is resumed already!"; - } - - StopFamilySharingInactivityTimer(); - CardsFarmer.Resume(true); - return "Automatic farming is now resumed!"; + ASF.ArchiLogger.LogNullError(nameof(steamID)); + return false; } - private static string ResponseResume(ulong steamID, string botName) { - if ((steamID == 0) || string.IsNullOrEmpty(botName)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); - return null; + private static bool IsValidCdKey(string key) { + if (!string.IsNullOrEmpty(key)) { + return Regex.IsMatch(key, @"^[0-9A-Z]{4,7}-[0-9A-Z]{4,7}-[0-9A-Z]{4,7}(?:(?:-[0-9A-Z]{4,7})?(?:-[0-9A-Z]{4,7}))?$", RegexOptions.IgnoreCase); } - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return bot.ResponseResume(steamID); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; + ASF.ArchiLogger.LogNullError(nameof(key)); + return false; } - private string ResponseStatus(ulong steamID) { - if (steamID == 0) { - ArchiLogger.LogNullError(nameof(steamID)); - return null; + private void JoinMasterChat() { + if (!IsConnectedAndLoggedOn || (BotConfig.SteamMasterClanID == 0)) { + return; } - if (!IsMaster(steamID)) { - return null; + SteamFriends.JoinChat(BotConfig.SteamMasterClanID); + } + + private static async Task LimitGiftsRequestsAsync() { + await GiftsSemaphore.WaitAsync().ConfigureAwait(false); + Task.Run(async () => { + await Task.Delay(Program.GlobalConfig.GiftsLimiterDelay*1000).ConfigureAwait(false); + GiftsSemaphore.Release(); + }).Forget(); + } + + private static async Task LimitLoginRequestsAsync() { + await LoginSemaphore.WaitAsync().ConfigureAwait(false); + Task.Run(async () => { + await Task.Delay(Program.GlobalConfig.LoginLimiterDelay*1000).ConfigureAwait(false); + LoginSemaphore.Release(); + }).Forget(); + } + + private void OnAccountInfo(SteamUser.AccountInfoCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; } - if (!IsConnectedAndLoggedOn) { - if (KeepRunning) { - return "Bot " + BotName + " is not connected."; + if (!BotConfig.FarmOffline) { + SteamFriends.SetPersonaState(EPersonaState.Online); + } + } + + private void OnChatInvite(SteamFriends.ChatInviteCallback callback) { + if ((callback?.ChatRoomID == null) || (callback.PatronID == null)) { + ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.ChatRoomID) + " || " + nameof(callback.PatronID)); + return; + } + + if (!IsMaster(callback.PatronID)) { + return; + } + + SteamFriends.JoinChat(callback.ChatRoomID); + } + + private async void OnChatMsg(SteamFriends.ChatMsgCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if (callback.ChatMsgType != EChatEntryType.ChatMsg) { + return; + } + + if ((callback.ChatRoomID == null) || (callback.ChatterID == null) || string.IsNullOrEmpty(callback.Message)) { + ArchiLogger.LogNullError(nameof(callback.ChatRoomID) + " || " + nameof(callback.ChatterID) + " || " + nameof(callback.Message)); + return; + } + + ArchiLogger.LogGenericTrace(callback.ChatRoomID.ConvertToUInt64() + "/" + callback.ChatterID.ConvertToUInt64() + ": " + callback.Message); + + switch (callback.Message) { + case "!leave": + if (!IsMaster(callback.ChatterID)) { + break; + } + + SteamFriends.LeaveChat(callback.ChatRoomID); + break; + default: + await HandleMessage(callback.ChatRoomID, callback.ChatterID, callback.Message).ConfigureAwait(false); + break; + } + } + + private void OnConnected(SteamClient.ConnectedCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if (callback.Result != EResult.OK) { + ArchiLogger.LogGenericError("Unable to connect to Steam: " + callback.Result); + return; + } + + ArchiLogger.LogGenericInfo("Connected to Steam!"); + + if (!KeepRunning) { + ArchiLogger.LogGenericInfo("Disconnecting..."); + Disconnect(); + return; + } + + byte[] sentryFileHash = null; + if (File.Exists(SentryFile)) { + try { + byte[] sentryFileContent = File.ReadAllBytes(SentryFile); + sentryFileHash = SteamKit2.CryptoHelper.SHAHash(sentryFileContent); + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + } + } + + if (!InitializeLoginAndPassword(false)) { + Stop(); + return; + } + + ArchiLogger.LogGenericInfo("Logging in..."); + + string password = BotConfig.SteamPassword; + if (!string.IsNullOrEmpty(password)) { + // Steam silently ignores non-ASCII characters in password, we're going to do the same + // Don't ask me why, I know it's stupid + password = Regex.Replace(password, @"[^\u0000-\u007F]+", ""); + } + + // Decrypt login key if needed + string loginKey = BotDatabase.LoginKey; + if (!string.IsNullOrEmpty(loginKey) && (loginKey.Length > 19)) { + loginKey = CryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey); + } + + SteamUser.LogOnDetails logOnDetails = new SteamUser.LogOnDetails { Username = BotConfig.SteamLogin, Password = password, AuthCode = AuthCode, CellID = Program.GlobalDatabase.CellID, LoginID = LoginID, LoginKey = loginKey, TwoFactorCode = TwoFactorCode, SentryFileHash = sentryFileHash, ShouldRememberPassword = true }; + + try { + SteamUser.LogOn(logOnDetails); + } catch { + // TODO: Remove me once https://github.com/SteamRE/SteamKit/issues/305 is fixed + ArchiHandler.LogOnWithoutMachineID(logOnDetails); + } + } + + private async void OnDisconnected(SteamClient.DisconnectedCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + HeartBeatFailures = 0; + + EResult lastLogOnResult = LastLogOnResult; + LastLogOnResult = EResult.Invalid; + + ArchiLogger.LogGenericInfo("Disconnected from Steam!"); + + ArchiWebHandler.OnDisconnected(); + CardsFarmer.OnDisconnected(); + Trading.OnDisconnected(); + + FirstTradeSent = false; + HandledGifts.ClearAndTrim(); + + // If we initiated disconnect, do not attempt to reconnect + if (callback.UserInitiated) { + return; + } + + switch (lastLogOnResult) { + case EResult.Invalid: + // Invalid means that we didn't get OnLoggedOn() in the first place, so Steam is down + // Always reset one-time-only access tokens in this case, as OnLoggedOn() didn't do that for us + AuthCode = TwoFactorCode = null; + break; + case EResult.InvalidPassword: + // If we didn't use login key, it's nearly always rate limiting + if (string.IsNullOrEmpty(BotDatabase.LoginKey)) { + goto case EResult.RateLimitExceeded; + } + + BotDatabase.LoginKey = null; + ArchiLogger.LogGenericInfo("Removed expired login key"); + break; + case EResult.NoConnection: + case EResult.ServiceUnavailable: + case EResult.Timeout: + case EResult.TryAnotherCM: + await Task.Delay(5000).ConfigureAwait(false); + break; + case EResult.RateLimitExceeded: + ArchiLogger.LogGenericInfo("Will retry after 25 minutes..."); + await Task.Delay(25*60*1000).ConfigureAwait(false); // Captcha disappears after around 20 minutes, so we make it 25 + break; + } + + if (!KeepRunning || SteamClient.IsConnected) { + return; + } + + ArchiLogger.LogGenericInfo("Reconnecting..."); + await Connect().ConfigureAwait(false); + } + + private void OnFreeLicense(SteamApps.FreeLicenseCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + } + } + + private async void OnFriendMsg(SteamFriends.FriendMsgCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if (callback.EntryType != EChatEntryType.ChatMsg) { + return; + } + + if ((callback.Sender == null) || string.IsNullOrEmpty(callback.Message)) { + ArchiLogger.LogNullError(nameof(callback.Sender) + " || " + nameof(callback.Message)); + return; + } + + ArchiLogger.LogGenericTrace(callback.Sender.ConvertToUInt64() + ": " + callback.Message); + + await HandleMessage(callback.Sender, callback.Sender, callback.Message).ConfigureAwait(false); + } + + private async void OnFriendMsgHistory(SteamFriends.FriendMsgHistoryCallback callback) { + if ((callback?.Messages == null) || (callback.SteamID == null)) { + ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.Messages) + " || " + nameof(callback.SteamID)); + return; + } + + if ((callback.Messages.Count == 0) || !IsMaster(callback.SteamID)) { + return; + } + + // Get last message + SteamFriends.FriendMsgHistoryCallback.FriendMessage lastMessage = callback.Messages[callback.Messages.Count - 1]; + + // If message is read already, return + if (!lastMessage.Unread) { + return; + } + + // If message is too old, return + if (DateTime.UtcNow.Subtract(lastMessage.Timestamp).TotalHours > 1) { + return; + } + + // Handle the message + await HandleMessage(callback.SteamID, callback.SteamID, lastMessage.Message).ConfigureAwait(false); + } + + private void OnFriendsList(SteamFriends.FriendsListCallback callback) { + if (callback?.FriendList == null) { + ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.FriendList)); + return; + } + + foreach (SteamFriends.FriendsListCallback.Friend friend in callback.FriendList.Where(friend => friend.Relationship == EFriendRelationship.RequestRecipient)) { + if (IsMaster(friend.SteamID)) { + SteamFriends.AddFriend(friend.SteamID); + } else if (BotConfig.IsBotAccount) { + SteamFriends.RemoveFriend(friend.SteamID); + } + } + } + + private async void OnGuestPassList(SteamApps.GuestPassListCallback callback) { + if (callback?.GuestPasses == null) { + ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.GuestPasses)); + return; + } + + if ((callback.CountGuestPassesToRedeem == 0) || (callback.GuestPasses.Count == 0) || !BotConfig.AcceptGifts) { + return; + } + + foreach (ulong gid in callback.GuestPasses.Select(guestPass => guestPass["gid"].AsUnsignedLong()).Where(gid => (gid != 0) && !HandledGifts.Contains(gid))) { + HandledGifts.Add(gid); + + ArchiLogger.LogGenericInfo("Accepting gift: " + gid + "..."); + await LimitGiftsRequestsAsync().ConfigureAwait(false); + + ArchiHandler.RedeemGuestPassResponseCallback response = await ArchiHandler.RedeemGuestPass(gid).ConfigureAwait(false); + if (response != null) { + if (response.Result == EResult.OK) { + ArchiLogger.LogGenericInfo("Success!"); + } else { + ArchiLogger.LogGenericWarning("Failed with error: " + response.Result); + } + } else { + ArchiLogger.LogGenericWarning("Failed!"); + } + } + } + + private async void OnLicenseList(SteamApps.LicenseListCallback callback) { + if (callback?.LicenseList == null) { + ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LicenseList)); + return; + } + + OwnedPackageIDs.Clear(); + + foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList) { + OwnedPackageIDs.Add(license.PackageID); + } + + OwnedPackageIDs.TrimExcess(); + + await Task.Delay(1000).ConfigureAwait(false); // Wait a second for eventual PlayingSessionStateCallback + + if (!ArchiWebHandler.Ready) { + for (byte i = 0; (i < Program.GlobalConfig.HttpTimeout) && !ArchiWebHandler.Ready; i++) { + await Task.Delay(1000).ConfigureAwait(false); } - return "Bot " + BotName + " is not running."; + if (!ArchiWebHandler.Ready) { + return; + } } - if (PlayingBlocked) { - return "Bot " + BotName + " is currently being used."; + await CardsFarmer.OnNewGameAdded().ConfigureAwait(false); + } + + private void OnLoggedOff(SteamUser.LoggedOffCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; } - if (CardsFarmer.Paused) { - return "Bot " + BotName + " is paused or running in manual mode."; + ArchiLogger.LogGenericInfo("Logged off of Steam: " + callback.Result); + + switch (callback.Result) { + case EResult.LogonSessionReplaced: + ArchiLogger.LogGenericError("This account seems to be used in another ASF instance, which is undefined behaviour, refusing to keep it running!"); + Stop(); + break; + } + } + + private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; } - if (CardsFarmer.CurrentGamesFarming.Count == 0) { - return "Bot " + BotName + " is not farming anything."; + // Always reset one-time-only access tokens + AuthCode = TwoFactorCode = null; + + // Keep LastLogOnResult for OnDisconnected() + LastLogOnResult = callback.Result; + + switch (callback.Result) { + case EResult.AccountLogonDenied: + AuthCode = Program.GetUserInput(ASF.EUserInputType.SteamGuard, BotName); + if (string.IsNullOrEmpty(AuthCode)) { + Stop(); + } + + break; + case EResult.AccountLoginDeniedNeedTwoFactor: + if (BotDatabase.MobileAuthenticator == null) { + TwoFactorCode = Program.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName); + if (string.IsNullOrEmpty(TwoFactorCode)) { + Stop(); + } + } else { + ArchiLogger.LogGenericWarning("2FA code was invalid despite of using ASF 2FA. Invalid authenticator or bad timing?"); + } + + break; + case EResult.OK: + ArchiLogger.LogGenericInfo("Successfully logged on!"); + + // Old status for these doesn't matter, we'll update them if needed + LibraryLockedBySteamID = 0; + IsLimitedUser = PlayingBlocked = false; + + if (callback.AccountFlags.HasFlag(EAccountFlags.LimitedUser)) { + IsLimitedUser = true; + ArchiLogger.LogGenericWarning("This account is limited, farming process is permanently unavailable until the restriction is removed!"); + } + + if ((callback.CellID != 0) && (Program.GlobalDatabase.CellID != callback.CellID)) { + Program.GlobalDatabase.CellID = callback.CellID; + } + + if (BotDatabase.MobileAuthenticator == null) { + // Support and convert SDA files + string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, callback.ClientSteamID.ConvertToUInt64() + ".maFile"); + if (File.Exists(maFilePath)) { + ImportAuthenticator(maFilePath); + } + } + + if (string.IsNullOrEmpty(BotConfig.SteamParentalPIN)) { + BotConfig.SteamParentalPIN = Program.GetUserInput(ASF.EUserInputType.SteamParentalPIN, BotName); + if (string.IsNullOrEmpty(BotConfig.SteamParentalPIN)) { + Stop(); + return; + } + } + + if (!await ArchiWebHandler.Init(callback.ClientSteamID, SteamClient.ConnectedUniverse, callback.WebAPIUserNonce, BotConfig.SteamParentalPIN).ConfigureAwait(false)) { + if (!await RefreshSession().ConfigureAwait(false)) { + return; + } + } + + InitializeFamilySharing().Forget(); + + if (BotConfig.DismissInventoryNotifications) { + ArchiWebHandler.MarkInventory().Forget(); + } + + if (BotConfig.SteamMasterClanID != 0) { + Task.Run(async () => { + await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false); + JoinMasterChat(); + }).Forget(); + } + + if (Program.GlobalConfig.Statistics) { + ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).Forget(); + } + + Trading.CheckTrades().Forget(); + break; + case EResult.InvalidPassword: + case EResult.NoConnection: + case EResult.RateLimitExceeded: + case EResult.ServiceUnavailable: + case EResult.Timeout: + case EResult.TryAnotherCM: + case EResult.TwoFactorCodeMismatch: + ArchiLogger.LogGenericWarning("Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult); + break; + default: // Unexpected result, shutdown immediately + ArchiLogger.LogGenericError("Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult); + Stop(); + break; + } + } + + private void OnLoginKey(SteamUser.LoginKeyCallback callback) { + if (string.IsNullOrEmpty(callback?.LoginKey)) { + ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LoginKey)); + return; } - StringBuilder response = new StringBuilder("Bot " + BotName + " is farming "); + string loginKey = callback.LoginKey; + if (!string.IsNullOrEmpty(loginKey)) { + loginKey = CryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey); + } - if (CardsFarmer.CurrentGamesFarming.Count == 1) { - CardsFarmer.Game game = CardsFarmer.CurrentGamesFarming.First(); - response.Append("game " + game.AppID + " (" + game.GameName + ", " + game.CardsRemaining + " card drops remaining)"); + BotDatabase.LoginKey = loginKey; + SteamUser.AcceptNewLoginKey(callback); + } + + private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + int fileSize; + byte[] sentryHash; + + try { + using (FileStream fileStream = File.Open(SentryFile, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { + fileStream.Seek(callback.Offset, SeekOrigin.Begin); + fileStream.Write(callback.Data, 0, callback.BytesToWrite); + fileSize = (int) fileStream.Length; + + fileStream.Seek(0, SeekOrigin.Begin); + using (SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider()) { + sentryHash = sha.ComputeHash(fileStream); + } + } + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + return; + } + + // Inform the steam servers that we're accepting this sentry file + SteamUser.SendMachineAuthResponse(new SteamUser.MachineAuthDetails { JobID = callback.JobID, FileName = callback.FileName, BytesWritten = callback.BytesToWrite, FileSize = fileSize, Offset = callback.Offset, Result = EResult.OK, LastError = 0, OneTimePassword = callback.OneTimePassword, SentryFileHash = sentryHash }); + } + + private void OnNotifications(ArchiHandler.NotificationsCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if ((callback.Notifications == null) || (callback.Notifications.Count == 0)) { + return; + } + + foreach (ArchiHandler.NotificationsCallback.ENotification notification in callback.Notifications) { + switch (notification) { + case ArchiHandler.NotificationsCallback.ENotification.Items: + CardsFarmer.OnNewItemsNotification().Forget(); + if (BotConfig.DismissInventoryNotifications) { + ArchiWebHandler.MarkInventory().Forget(); + } + break; + case ArchiHandler.NotificationsCallback.ENotification.Trading: + Trading.CheckTrades().Forget(); + break; + } + } + } + + private void OnOfflineMessage(ArchiHandler.OfflineMessageCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if ((callback.OfflineMessagesCount == 0) || !BotConfig.HandleOfflineMessages) { + return; + } + + SteamFriends.RequestOfflineMessages(); + } + + private void OnPersonaState(SteamFriends.PersonaStateCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if (callback.FriendID == SteamClient.SteamID) { + Events.OnStateUpdated(this, callback); + } else if ((callback.FriendID == LibraryLockedBySteamID) && (callback.GameID == 0)) { + LibraryLockedBySteamID = 0; + CheckOccupationStatus(); + } + } + + private void OnPlayingSessionState(ArchiHandler.PlayingSessionStateCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + if (callback.PlayingBlocked == PlayingBlocked) { + return; // No status update, we're not interested + } + + PlayingBlocked = callback.PlayingBlocked; + CheckOccupationStatus(); + } + + private void OnPurchaseResponse(ArchiHandler.PurchaseResponseCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + } + } + + private void OnSharedLibraryLockStatus(ArchiHandler.SharedLibraryLockStatusCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); + return; + } + + // Ignore no status updates + if (LibraryLockedBySteamID == 0) { + if ((callback.LibraryLockedBySteamID == 0) || (callback.LibraryLockedBySteamID == SteamClient.SteamID)) { + return; + } + + LibraryLockedBySteamID = callback.LibraryLockedBySteamID; } else { - response.Append("appIDs " + string.Join(", ", CardsFarmer.CurrentGamesFarming.Select(game => game.AppID))); - } - - response.Append(" and has a total of " + CardsFarmer.GamesToFarm.Count + " games (" + CardsFarmer.GamesToFarm.Sum(game => game.CardsRemaining) + " cards) left to farm."); - return response.ToString(); - } - - private static string ResponseStatus(ulong steamID, string botName) { - if ((steamID == 0) || string.IsNullOrEmpty(botName)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); - return null; - } - - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return bot.ResponseStatus(steamID); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; - } - - private static string ResponseStatusAll(ulong steamID) { - if (steamID == 0) { - ASF.ArchiLogger.LogNullError(nameof(steamID)); - return null; - } - - if (!IsOwner(steamID)) { - return null; - } - - HashSet botsRunning = new HashSet(Bots.Where(bot => bot.Value.KeepRunning).OrderBy(bot => bot.Key).Select(bot => bot.Value)); - IEnumerable statuses = botsRunning.Select(bot => bot.ResponseStatus(steamID)); - - return Environment.NewLine + - string.Join(Environment.NewLine, statuses) + Environment.NewLine + - "There are " + botsRunning.Count + "/" + Bots.Count + " bots running, with total of " + botsRunning.Sum(bot => bot.CardsFarmer.GamesToFarm.Count) + " games (" + botsRunning.Sum(bot => bot.CardsFarmer.GamesToFarm.Sum(game => game.CardsRemaining)) + " cards) left to farm."; - } - - private async Task ResponseLoot(ulong steamID) { - if (steamID == 0) { - ArchiLogger.LogNullError(nameof(steamID)); - return null; - } - - if (!IsMaster(steamID)) { - return null; - } - - if (!IsConnectedAndLoggedOn) { - return "This bot instance is not connected!"; - } - - if (BotConfig.SteamMasterID == 0) { - return "Trade couldn't be send because SteamMasterID is not defined!"; - } - - if (BotConfig.SteamMasterID == SteamClient.SteamID) { - return "You can't loot yourself!"; - } - - await Trading.LimitInventoryRequestsAsync().ConfigureAwait(false); - - HashSet inventory = await ArchiWebHandler.GetMySteamInventory(true).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 - if (inventory.RemoveWhere(item => (item.Type != Steam.Item.EType.TradingCard) && ((item.Type != Steam.Item.EType.FoilTradingCard) || !BotConfig.IsBotAccount) && (item.Type != Steam.Item.EType.BoosterPack)) > 0) { - if (inventory.Count == 0) { - return "Nothing to send, inventory seems empty!"; + if ((callback.LibraryLockedBySteamID != 0) && (callback.LibraryLockedBySteamID != SteamClient.SteamID)) { + return; } + + if (SteamFriends.GetFriendGamePlayed(LibraryLockedBySteamID) != 0) { + return; + } + + LibraryLockedBySteamID = 0; } - if (!await ArchiWebHandler.SendTradeOffer(inventory, BotConfig.SteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) { - return "Trade offer failed due to error!"; - } - - await Task.Delay(1000).ConfigureAwait(false); // Sometimes we can be too fast for Steam servers to generate confirmations, wait a short moment - await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, BotConfig.SteamMasterID).ConfigureAwait(false); - return "Trade offer sent successfully!"; + CheckOccupationStatus(); } - private static async Task ResponseLoot(ulong steamID, string botName) { - if ((steamID == 0) || string.IsNullOrEmpty(botName)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); - return null; + private void OnWebAPIUserNonce(SteamUser.WebAPIUserNonceCallback callback) { + if (callback == null) { + ArchiLogger.LogNullError(nameof(callback)); } - - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return await bot.ResponseLoot(steamID).ConfigureAwait(false); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; } - private static async Task ResponseLootAll(ulong steamID) { - if (steamID == 0) { - ASF.ArchiLogger.LogNullError(nameof(steamID)); - return null; + private void ResetGamesPlayed() { + if (PlayingBlocked) { + return; } - if (!IsOwner(steamID)) { - return null; - } - - await Task.WhenAll(Bots.Values.Where(bot => bot.IsConnectedAndLoggedOn).Select(bot => bot.ResponseLoot(steamID))).ConfigureAwait(false); - return "Done!"; + ArchiHandler.PlayGames(BotConfig.GamesPlayedWhileIdle, BotConfig.CustomGamePlayedWhileIdle); } private async Task Response2FA(ulong steamID) { @@ -1159,6 +1459,74 @@ namespace ArchiSteamFarm { return null; } + private async Task ResponseAddLicense(ulong steamID, ICollection gameIDs) { + if ((steamID == 0) || (gameIDs == null) || (gameIDs.Count == 0)) { + ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(gameIDs) + " || " + nameof(gameIDs.Count)); + return null; + } + + if (!IsMaster(steamID)) { + return null; + } + + if (!IsConnectedAndLoggedOn) { + return "This bot instance is not connected!"; + } + + StringBuilder result = new StringBuilder(); + foreach (uint gameID in gameIDs) { + SteamApps.FreeLicenseCallback callback = await SteamApps.RequestFreeLicense(gameID); + if (callback == null) { + result.AppendLine(Environment.NewLine + "Result: Timeout!"); + break; + } + + if ((callback.GrantedApps.Count != 0) || (callback.GrantedPackages.Count != 0)) { + result.AppendLine(Environment.NewLine + "Result: " + callback.Result + " | Granted apps:" + (callback.GrantedApps.Count != 0 ? " " + string.Join(", ", callback.GrantedApps) : "") + (callback.GrantedPackages.Count != 0 ? " " + string.Join(", ", callback.GrantedPackages) : "")); + } else if (await ArchiWebHandler.AddFreeLicense(gameID).ConfigureAwait(false)) { + result.AppendLine(Environment.NewLine + "Result: " + EResult.OK + " | Granted apps: " + gameID); + } else { + result.AppendLine(Environment.NewLine + "Result: " + EResult.AccessDenied); + } + } + + return result.ToString(); + } + + private static async Task ResponseAddLicense(ulong steamID, string botName, string games) { + if ((steamID == 0) || string.IsNullOrEmpty(botName) || string.IsNullOrEmpty(games)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName) + " || " + nameof(games)); + return null; + } + + Bot bot; + if (!Bots.TryGetValue(botName, out bot)) { + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + string[] gameIDs = games.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + HashSet gamesToRedeem = new HashSet(); + foreach (string game in gameIDs.Where(game => !string.IsNullOrEmpty(game))) { + uint gameID; + if (!uint.TryParse(game, out gameID)) { + return "Couldn't parse games list!"; + } + + gamesToRedeem.Add(gameID); + } + + if (gamesToRedeem.Count == 0) { + return "List of games is empty!"; + } + + return await bot.ResponseAddLicense(steamID, gamesToRedeem).ConfigureAwait(false); + } + private static string ResponseAPI(ulong steamID) { if (steamID != 0) { return !IsOwner(steamID) ? null : GetAPIStatus(); @@ -1237,6 +1605,317 @@ namespace ArchiSteamFarm { return "https://github.com/" + SharedInfo.GithubRepo + "/wiki/Commands"; } + private async Task ResponseLoot(ulong steamID) { + if (steamID == 0) { + ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsMaster(steamID)) { + return null; + } + + if (!IsConnectedAndLoggedOn) { + return "This bot instance is not connected!"; + } + + if (BotConfig.SteamMasterID == 0) { + return "Trade couldn't be send because SteamMasterID is not defined!"; + } + + if (BotConfig.SteamMasterID == SteamClient.SteamID) { + return "You can't loot yourself!"; + } + + await Trading.LimitInventoryRequestsAsync().ConfigureAwait(false); + + HashSet inventory = await ArchiWebHandler.GetMySteamInventory(true).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 + if (inventory.RemoveWhere(item => (item.Type != Steam.Item.EType.TradingCard) && ((item.Type != Steam.Item.EType.FoilTradingCard) || !BotConfig.IsBotAccount) && (item.Type != Steam.Item.EType.BoosterPack)) > 0) { + if (inventory.Count == 0) { + return "Nothing to send, inventory seems empty!"; + } + } + + if (!await ArchiWebHandler.SendTradeOffer(inventory, BotConfig.SteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) { + return "Trade offer failed due to error!"; + } + + await Task.Delay(1000).ConfigureAwait(false); // Sometimes we can be too fast for Steam servers to generate confirmations, wait a short moment + await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, BotConfig.SteamMasterID).ConfigureAwait(false); + return "Trade offer sent successfully!"; + } + + private static async Task ResponseLoot(ulong steamID, string botName) { + if ((steamID == 0) || string.IsNullOrEmpty(botName)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); + return null; + } + + Bot bot; + if (Bots.TryGetValue(botName, out bot)) { + return await bot.ResponseLoot(steamID).ConfigureAwait(false); + } + + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + private static async Task ResponseLootAll(ulong steamID) { + if (steamID == 0) { + ASF.ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsOwner(steamID)) { + return null; + } + + await Task.WhenAll(Bots.Values.Where(bot => bot.IsConnectedAndLoggedOn).Select(bot => bot.ResponseLoot(steamID))).ConfigureAwait(false); + return "Done!"; + } + + private async Task ResponseOwns(ulong steamID, string query) { + if ((steamID == 0) || string.IsNullOrEmpty(query)) { + ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(query)); + return null; + } + + if (!IsMaster(steamID)) { + return null; + } + + if (!IsConnectedAndLoggedOn) { + return "This bot instance is not connected!"; + } + + Dictionary ownedGames; + if (!string.IsNullOrEmpty(BotConfig.SteamApiKey)) { + ownedGames = ArchiWebHandler.GetOwnedGames(SteamClient.SteamID); + } else { + ownedGames = await ArchiWebHandler.GetOwnedGames().ConfigureAwait(false); + } + + if ((ownedGames == null) || (ownedGames.Count == 0)) { + return Environment.NewLine + "<" + BotName + "> List of owned games is empty!"; + } + + StringBuilder response = new StringBuilder(); + + string[] games = query.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string game in games.Where(game => !string.IsNullOrEmpty(game))) { + // Check if this is appID + uint appID; + if (uint.TryParse(game, out appID)) { + string ownedName; + if (ownedGames.TryGetValue(appID, out ownedName)) { + response.Append(Environment.NewLine + "<" + BotName + "> Owned already: " + appID + " | " + ownedName); + } else { + response.Append(Environment.NewLine + "<" + BotName + "> Not owned yet: " + appID); + } + + continue; + } + + // This is a string, so check our entire library + foreach (KeyValuePair ownedGame in ownedGames.Where(ownedGame => ownedGame.Value.IndexOf(game, StringComparison.OrdinalIgnoreCase) >= 0)) { + response.Append(Environment.NewLine + "<" + BotName + "> Owned already: " + ownedGame.Key + " | " + ownedGame.Value); + } + } + + if (response.Length > 0) { + return response.ToString(); + } + + return Environment.NewLine + "<" + BotName + "> Not owned yet: " + query; + } + + private static async Task ResponseOwns(ulong steamID, string botName, string query) { + if ((steamID == 0) || string.IsNullOrEmpty(botName) || string.IsNullOrEmpty(query)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName) + " || " + nameof(query)); + return null; + } + + Bot bot; + if (Bots.TryGetValue(botName, out bot)) { + return await bot.ResponseOwns(steamID, query).ConfigureAwait(false); + } + + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + private static async Task ResponseOwnsAll(ulong steamID, string query) { + if ((steamID == 0) || string.IsNullOrEmpty(query)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(query)); + return null; + } + + if (!IsOwner(steamID)) { + return null; + } + + string[] responses = await Task.WhenAll(Bots.Where(bot => bot.Value.IsConnectedAndLoggedOn).OrderBy(bot => bot.Key).Select(bot => bot.Value.ResponseOwns(steamID, query))).ConfigureAwait(false); + + StringBuilder result = new StringBuilder(); + foreach (string response in responses.Where(response => !string.IsNullOrEmpty(response))) { + result.Append(response); + } + + return result.Length != 0 ? result.ToString() : null; + } + + private string ResponsePassword(ulong steamID) { + if (steamID == 0) { + ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsMaster(steamID)) { + return null; + } + + if (string.IsNullOrEmpty(BotConfig.SteamPassword)) { + return "Can't encrypt null password!"; + } + + return Environment.NewLine + "[" + CryptoHelper.ECryptoMethod.AES + "] password: " + CryptoHelper.Encrypt(CryptoHelper.ECryptoMethod.AES, BotConfig.SteamPassword) + Environment.NewLine + "[" + CryptoHelper.ECryptoMethod.ProtectedDataForCurrentUser + "] password: " + CryptoHelper.Encrypt(CryptoHelper.ECryptoMethod.ProtectedDataForCurrentUser, BotConfig.SteamPassword); + } + + private static string ResponsePassword(ulong steamID, string botName) { + if ((steamID == 0) || string.IsNullOrEmpty(botName)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); + return null; + } + + Bot bot; + if (Bots.TryGetValue(botName, out bot)) { + return bot.ResponsePassword(steamID); + } + + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + private async Task ResponsePause(ulong steamID, bool sticky) { + if (steamID == 0) { + ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsMaster(steamID) && !SteamFamilySharingIDs.Contains(steamID)) { + return null; + } + + if (!IsConnectedAndLoggedOn) { + return "This bot instance is not connected!"; + } + + if (CardsFarmer.Paused) { + return "Automatic farming is paused already!"; + } + + await CardsFarmer.Pause(sticky).ConfigureAwait(false); + + if (!SteamFamilySharingIDs.Contains(steamID)) { + return "Automatic farming is now paused!"; + } + + StartFamilySharingInactivityTimer(); + return "Automatic farming is now paused! You have " + FamilySharingInactivityMinutes + " minutes to start a game."; + } + + private static async Task ResponsePause(ulong steamID, string botName, bool sticky) { + if ((steamID == 0) || string.IsNullOrEmpty(botName)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); + return null; + } + + Bot bot; + if (Bots.TryGetValue(botName, out bot)) { + return await bot.ResponsePause(steamID, sticky).ConfigureAwait(false); + } + + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + private async Task ResponsePlay(ulong steamID, HashSet gameIDs) { + if ((steamID == 0) || (gameIDs == null) || (gameIDs.Count == 0)) { + ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(gameIDs) + " || " + nameof(gameIDs.Count)); + return null; + } + + if (!IsMaster(steamID)) { + return null; + } + + if (!IsConnectedAndLoggedOn) { + return "This bot instance is not connected!"; + } + + if (!CardsFarmer.Paused) { + await CardsFarmer.Pause(false).ConfigureAwait(false); + } + + ArchiHandler.PlayGames(gameIDs); + return "Done!"; + } + + private static async Task ResponsePlay(ulong steamID, string botName, string games) { + if ((steamID == 0) || string.IsNullOrEmpty(botName) || string.IsNullOrEmpty(games)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName) + " || " + nameof(games)); + return null; + } + + Bot bot; + if (!Bots.TryGetValue(botName, out bot)) { + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + string[] gameIDs = games.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + HashSet gamesToPlay = new HashSet(); + foreach (string game in gameIDs.Where(game => !string.IsNullOrEmpty(game))) { + uint gameID; + if (!uint.TryParse(game, out gameID)) { + return "Couldn't parse games list!"; + } + + gamesToPlay.Add(gameID); + + if (gamesToPlay.Count >= CardsFarmer.MaxGamesPlayedConcurrently) { + break; + } + } + + if (gamesToPlay.Count == 0) { + return "List of games is empty!"; + } + + return await bot.ResponsePlay(steamID, gamesToPlay).ConfigureAwait(false); + } + [SuppressMessage("ReSharper", "FunctionComplexityOverflow")] private async Task ResponseRedeem(ulong steamID, string message, ERedeemFlags redeemFlags = ERedeemFlags.None) { if ((steamID == 0) || string.IsNullOrEmpty(message)) { @@ -1416,191 +2095,13 @@ namespace ArchiSteamFarm { return "Done!"; } - private static string ResponseStartAll(ulong steamID) { + private string ResponseResume(ulong steamID) { if (steamID == 0) { - ASF.ArchiLogger.LogNullError(nameof(steamID)); + ArchiLogger.LogNullError(nameof(steamID)); return null; } - if (!IsOwner(steamID)) { - return null; - } - - foreach (Bot bot in Bots.Values.Where(bot => !bot.KeepRunning)) { - bot.ResponseStart(steamID); - } - - return "Done!"; - } - - private async Task ResponseAddLicense(ulong steamID, ICollection gameIDs) { - if ((steamID == 0) || (gameIDs == null) || (gameIDs.Count == 0)) { - ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(gameIDs) + " || " + nameof(gameIDs.Count)); - return null; - } - - if (!IsMaster(steamID)) { - return null; - } - - if (!IsConnectedAndLoggedOn) { - return "This bot instance is not connected!"; - } - - StringBuilder result = new StringBuilder(); - foreach (uint gameID in gameIDs) { - SteamApps.FreeLicenseCallback callback = await SteamApps.RequestFreeLicense(gameID); - if (callback == null) { - result.AppendLine(Environment.NewLine + "Result: Timeout!"); - break; - } - - if ((callback.GrantedApps.Count != 0) || (callback.GrantedPackages.Count != 0)) { - result.AppendLine(Environment.NewLine + "Result: " + callback.Result + " | Granted apps:" + (callback.GrantedApps.Count != 0 ? " " + string.Join(", ", callback.GrantedApps) : "") + (callback.GrantedPackages.Count != 0 ? " " + string.Join(", ", callback.GrantedPackages) : "")); - } else if (await ArchiWebHandler.AddFreeLicense(gameID).ConfigureAwait(false)) { - result.AppendLine(Environment.NewLine + "Result: " + EResult.OK + " | Granted apps: " + gameID); - } else { - result.AppendLine(Environment.NewLine + "Result: " + EResult.AccessDenied); - } - } - - return result.ToString(); - } - - private static async Task ResponseAddLicense(ulong steamID, string botName, string games) { - if ((steamID == 0) || string.IsNullOrEmpty(botName) || string.IsNullOrEmpty(games)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName) + " || " + nameof(games)); - return null; - } - - Bot bot; - if (!Bots.TryGetValue(botName, out bot)) { - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; - } - - string[] gameIDs = games.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - HashSet gamesToRedeem = new HashSet(); - foreach (string game in gameIDs.Where(game => !string.IsNullOrEmpty(game))) { - uint gameID; - if (!uint.TryParse(game, out gameID)) { - return "Couldn't parse games list!"; - } - - gamesToRedeem.Add(gameID); - } - - if (gamesToRedeem.Count == 0) { - return "List of games is empty!"; - } - - return await bot.ResponseAddLicense(steamID, gamesToRedeem).ConfigureAwait(false); - } - - private async Task ResponseOwns(ulong steamID, string query) { - if ((steamID == 0) || string.IsNullOrEmpty(query)) { - ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(query)); - return null; - } - - if (!IsMaster(steamID)) { - return null; - } - - if (!IsConnectedAndLoggedOn) { - return "This bot instance is not connected!"; - } - - Dictionary ownedGames; - if (!string.IsNullOrEmpty(BotConfig.SteamApiKey)) { - ownedGames = ArchiWebHandler.GetOwnedGames(SteamClient.SteamID); - } else { - ownedGames = await ArchiWebHandler.GetOwnedGames().ConfigureAwait(false); - } - - if ((ownedGames == null) || (ownedGames.Count == 0)) { - return Environment.NewLine + "<" + BotName + "> List of owned games is empty!"; - } - - StringBuilder response = new StringBuilder(); - - string[] games = query.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string game in games.Where(game => !string.IsNullOrEmpty(game))) { - // Check if this is appID - uint appID; - if (uint.TryParse(game, out appID)) { - string ownedName; - if (ownedGames.TryGetValue(appID, out ownedName)) { - response.Append(Environment.NewLine + "<" + BotName + "> Owned already: " + appID + " | " + ownedName); - } else { - response.Append(Environment.NewLine + "<" + BotName + "> Not owned yet: " + appID); - } - - continue; - } - - // This is a string, so check our entire library - foreach (KeyValuePair ownedGame in ownedGames.Where(ownedGame => ownedGame.Value.IndexOf(game, StringComparison.OrdinalIgnoreCase) >= 0)) { - response.Append(Environment.NewLine + "<" + BotName + "> Owned already: " + ownedGame.Key + " | " + ownedGame.Value); - } - } - - if (response.Length > 0) { - return response.ToString(); - } - - return Environment.NewLine + "<" + BotName + "> Not owned yet: " + query; - } - - private static async Task ResponseOwns(ulong steamID, string botName, string query) { - if ((steamID == 0) || string.IsNullOrEmpty(botName) || string.IsNullOrEmpty(query)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName) + " || " + nameof(query)); - return null; - } - - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return await bot.ResponseOwns(steamID, query).ConfigureAwait(false); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; - } - - private static async Task ResponseOwnsAll(ulong steamID, string query) { - if ((steamID == 0) || string.IsNullOrEmpty(query)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(query)); - return null; - } - - if (!IsOwner(steamID)) { - return null; - } - - string[] responses = await Task.WhenAll(Bots.Where(bot => bot.Value.IsConnectedAndLoggedOn).OrderBy(bot => bot.Key).Select(bot => bot.Value.ResponseOwns(steamID, query))).ConfigureAwait(false); - - StringBuilder result = new StringBuilder(); - foreach (string response in responses.Where(response => !string.IsNullOrEmpty(response))) { - result.Append(response); - } - - return result.Length != 0 ? result.ToString() : null; - } - - private async Task ResponsePlay(ulong steamID, HashSet gameIDs) { - if ((steamID == 0) || (gameIDs == null) || (gameIDs.Count == 0)) { - ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(gameIDs) + " || " + nameof(gameIDs.Count)); - return null; - } - - if (!IsMaster(steamID)) { + if (!IsMaster(steamID) && !SteamFamilySharingIDs.Contains(steamID)) { return null; } @@ -1609,49 +2110,30 @@ namespace ArchiSteamFarm { } if (!CardsFarmer.Paused) { - await CardsFarmer.Pause(false).ConfigureAwait(false); + return "Automatic farming is resumed already!"; } - ArchiHandler.PlayGames(gameIDs); - return "Done!"; + StopFamilySharingInactivityTimer(); + CardsFarmer.Resume(true); + return "Automatic farming is now resumed!"; } - private static async Task ResponsePlay(ulong steamID, string botName, string games) { - if ((steamID == 0) || string.IsNullOrEmpty(botName) || string.IsNullOrEmpty(games)) { - ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName) + " || " + nameof(games)); + private static string ResponseResume(ulong steamID, string botName) { + if ((steamID == 0) || string.IsNullOrEmpty(botName)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); return null; } Bot bot; - if (!Bots.TryGetValue(botName, out bot)) { - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; + if (Bots.TryGetValue(botName, out bot)) { + return bot.ResponseResume(steamID); } - string[] gameIDs = games.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - HashSet gamesToPlay = new HashSet(); - foreach (string game in gameIDs.Where(game => !string.IsNullOrEmpty(game))) { - uint gameID; - if (!uint.TryParse(game, out gameID)) { - return "Couldn't parse games list!"; - } - - gamesToPlay.Add(gameID); - - if (gamesToPlay.Count >= CardsFarmer.MaxGamesPlayedConcurrently) { - break; - } + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; } - if (gamesToPlay.Count == 0) { - return "List of games is empty!"; - } - - return await bot.ResponsePlay(steamID, gamesToPlay).ConfigureAwait(false); + return null; } private string ResponseStart(ulong steamID) { @@ -1691,6 +2173,100 @@ namespace ArchiSteamFarm { return null; } + private static string ResponseStartAll(ulong steamID) { + if (steamID == 0) { + ASF.ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsOwner(steamID)) { + return null; + } + + foreach (Bot bot in Bots.Values.Where(bot => !bot.KeepRunning)) { + bot.ResponseStart(steamID); + } + + return "Done!"; + } + + private string ResponseStatus(ulong steamID) { + if (steamID == 0) { + ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsMaster(steamID)) { + return null; + } + + if (!IsConnectedAndLoggedOn) { + if (KeepRunning) { + return "Bot " + BotName + " is not connected."; + } + + return "Bot " + BotName + " is not running."; + } + + if (PlayingBlocked) { + return "Bot " + BotName + " is currently being used."; + } + + if (CardsFarmer.Paused) { + return "Bot " + BotName + " is paused or running in manual mode."; + } + + if (CardsFarmer.CurrentGamesFarming.Count == 0) { + return "Bot " + BotName + " is not farming anything."; + } + + StringBuilder response = new StringBuilder("Bot " + BotName + " is farming "); + + if (CardsFarmer.CurrentGamesFarming.Count == 1) { + CardsFarmer.Game game = CardsFarmer.CurrentGamesFarming.First(); + response.Append("game " + game.AppID + " (" + game.GameName + ", " + game.CardsRemaining + " card drops remaining)"); + } else { + response.Append("appIDs " + string.Join(", ", CardsFarmer.CurrentGamesFarming.Select(game => game.AppID))); + } + + response.Append(" and has a total of " + CardsFarmer.GamesToFarm.Count + " games (" + CardsFarmer.GamesToFarm.Sum(game => game.CardsRemaining) + " cards) left to farm."); + return response.ToString(); + } + + private static string ResponseStatus(ulong steamID, string botName) { + if ((steamID == 0) || string.IsNullOrEmpty(botName)) { + ASF.ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(botName)); + return null; + } + + Bot bot; + if (Bots.TryGetValue(botName, out bot)) { + return bot.ResponseStatus(steamID); + } + + if (IsOwner(steamID)) { + return "Couldn't find any bot named " + botName + "!"; + } + + return null; + } + + private static string ResponseStatusAll(ulong steamID) { + if (steamID == 0) { + ASF.ArchiLogger.LogNullError(nameof(steamID)); + return null; + } + + if (!IsOwner(steamID)) { + return null; + } + + HashSet botsRunning = new HashSet(Bots.Where(bot => bot.Value.KeepRunning).OrderBy(bot => bot.Key).Select(bot => bot.Value)); + IEnumerable statuses = botsRunning.Select(bot => bot.ResponseStatus(steamID)); + + return Environment.NewLine + string.Join(Environment.NewLine, statuses) + Environment.NewLine + "There are " + botsRunning.Count + "/" + Bots.Count + " bots running, with total of " + botsRunning.Sum(bot => bot.CardsFarmer.GamesToFarm.Count) + " games (" + botsRunning.Sum(bot => bot.CardsFarmer.GamesToFarm.Sum(game => game.CardsRemaining)) + " cards) left to farm."; + } + private string ResponseStop(ulong steamID) { if (steamID == 0) { ArchiLogger.LogNullError(nameof(steamID)); @@ -1763,37 +2339,6 @@ namespace ArchiSteamFarm { return "ASF V" + SharedInfo.Version; } - private void HandleCallbacks() { - TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep); - while (KeepRunning || SteamClient.IsConnected) { - if (!CallbackSemaphore.Wait(0)) { - return; - } - - try { - CallbackManager.RunWaitCallbacks(timeSpan); - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - } finally { - CallbackSemaphore.Release(); - } - } - } - - private async Task HandleMessage(ulong chatID, ulong steamID, string message) { - if ((chatID == 0) || (steamID == 0) || string.IsNullOrEmpty(message)) { - ArchiLogger.LogNullError(nameof(chatID) + " || " + nameof(steamID) + " || " + nameof(message)); - return; - } - - string response = await Response(steamID, message).ConfigureAwait(false); - if (string.IsNullOrEmpty(response)) { // We respond with null when user is not authorized (and similar) - return; - } - - SendMessage(chatID, response); - } - private void SendMessage(ulong steamID, string message) { if ((steamID == 0) || string.IsNullOrEmpty(message)) { ArchiLogger.LogNullError(nameof(steamID) + " || " + nameof(message)); @@ -1839,626 +2384,45 @@ namespace ArchiSteamFarm { } } - private void JoinMasterChat() { - if (!IsConnectedAndLoggedOn || (BotConfig.SteamMasterClanID == 0)) { - return; - } - - SteamFriends.JoinChat(BotConfig.SteamMasterClanID); - } - - private bool InitializeLoginAndPassword(bool requiresPassword) { - if (string.IsNullOrEmpty(BotConfig.SteamLogin)) { - BotConfig.SteamLogin = Program.GetUserInput(ASF.EUserInputType.Login, BotName); - if (string.IsNullOrEmpty(BotConfig.SteamLogin)) { - return false; - } - } - - if (!string.IsNullOrEmpty(BotConfig.SteamPassword) || (!requiresPassword && !string.IsNullOrEmpty(BotDatabase.LoginKey))) { - return true; - } - - BotConfig.SteamPassword = Program.GetUserInput(ASF.EUserInputType.Password, BotName); - return !string.IsNullOrEmpty(BotConfig.SteamPassword); - } - - private void ResetGamesPlayed() { - if (PlayingBlocked) { - return; - } - - ArchiHandler.PlayGames(BotConfig.GamesPlayedWhileIdle, BotConfig.CustomGamePlayedWhileIdle); - } - - private void OnConnected(SteamClient.ConnectedCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if (callback.Result != EResult.OK) { - ArchiLogger.LogGenericError("Unable to connect to Steam: " + callback.Result); - return; - } - - ArchiLogger.LogGenericInfo("Connected to Steam!"); - + private async Task Start() { if (!KeepRunning) { - ArchiLogger.LogGenericInfo("Disconnecting..."); - Disconnect(); - return; + KeepRunning = true; + Task.Run(() => HandleCallbacks()).Forget(); + ArchiLogger.LogGenericInfo("Starting..."); } - byte[] sentryFileHash = null; - if (File.Exists(SentryFile)) { - try { - byte[] sentryFileContent = File.ReadAllBytes(SentryFile); - sentryFileHash = SteamKit2.CryptoHelper.SHAHash(sentryFileContent); - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - } - } - - if (!InitializeLoginAndPassword(false)) { - Stop(); - return; - } - - ArchiLogger.LogGenericInfo("Logging in..."); - - string password = BotConfig.SteamPassword; - if (!string.IsNullOrEmpty(password)) { - // Steam silently ignores non-ASCII characters in password, we're going to do the same - // Don't ask me why, I know it's stupid - password = Regex.Replace(password, @"[^\u0000-\u007F]+", ""); - } - - // Decrypt login key if needed - string loginKey = BotDatabase.LoginKey; - if (!string.IsNullOrEmpty(loginKey) && (loginKey.Length > 19)) { - loginKey = CryptoHelper.Decrypt(BotConfig.PasswordFormat, loginKey); - } - - SteamUser.LogOnDetails logOnDetails = new SteamUser.LogOnDetails { - Username = BotConfig.SteamLogin, - Password = password, - AuthCode = AuthCode, - CellID = Program.GlobalDatabase.CellID, - LoginID = LoginID, - LoginKey = loginKey, - TwoFactorCode = TwoFactorCode, - SentryFileHash = sentryFileHash, - ShouldRememberPassword = true - }; - - try { - SteamUser.LogOn(logOnDetails); - } catch { - // TODO: Remove me once https://github.com/SteamRE/SteamKit/issues/305 is fixed - ArchiHandler.LogOnWithoutMachineID(logOnDetails); - } - } - - private async void OnDisconnected(SteamClient.DisconnectedCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - HeartBeatFailures = 0; - - EResult lastLogOnResult = LastLogOnResult; - LastLogOnResult = EResult.Invalid; - - ArchiLogger.LogGenericInfo("Disconnected from Steam!"); - - ArchiWebHandler.OnDisconnected(); - CardsFarmer.OnDisconnected(); - Trading.OnDisconnected(); - - FirstTradeSent = false; - HandledGifts.ClearAndTrim(); - - // If we initiated disconnect, do not attempt to reconnect - if (callback.UserInitiated) { - return; - } - - switch (lastLogOnResult) { - case EResult.Invalid: - // Invalid means that we didn't get OnLoggedOn() in the first place, so Steam is down - // Always reset one-time-only access tokens in this case, as OnLoggedOn() didn't do that for us - AuthCode = TwoFactorCode = null; - break; - case EResult.InvalidPassword: - // If we didn't use login key, it's nearly always rate limiting - if (string.IsNullOrEmpty(BotDatabase.LoginKey)) { - goto case EResult.RateLimitExceeded; - } - - BotDatabase.LoginKey = null; - ArchiLogger.LogGenericInfo("Removed expired login key"); - break; - case EResult.NoConnection: - case EResult.ServiceUnavailable: - case EResult.Timeout: - case EResult.TryAnotherCM: - await Task.Delay(5000).ConfigureAwait(false); - break; - case EResult.RateLimitExceeded: - ArchiLogger.LogGenericInfo("Will retry after 25 minutes..."); - await Task.Delay(25 * 60 * 1000).ConfigureAwait(false); // Captcha disappears after around 20 minutes, so we make it 25 - break; - } - - if (!KeepRunning || SteamClient.IsConnected) { - return; - } - - ArchiLogger.LogGenericInfo("Reconnecting..."); await Connect().ConfigureAwait(false); } - private void OnFreeLicense(SteamApps.FreeLicenseCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); + private void StartFamilySharingInactivityTimer() { + if (FamilySharingInactivityTimer != null) { + return; } + + FamilySharingInactivityTimer = new Timer(e => CheckFamilySharingInactivity(), null, TimeSpan.FromMinutes(FamilySharingInactivityMinutes), // Delay + Timeout.InfiniteTimeSpan // Period + ); } - private async void OnGuestPassList(SteamApps.GuestPassListCallback callback) { - if (callback?.GuestPasses == null) { - ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.GuestPasses)); + private void StopFamilySharingInactivityTimer() { + if (FamilySharingInactivityTimer == null) { return; } - if ((callback.CountGuestPassesToRedeem == 0) || (callback.GuestPasses.Count == 0) || !BotConfig.AcceptGifts) { - return; - } - - foreach (ulong gid in callback.GuestPasses.Select(guestPass => guestPass["gid"].AsUnsignedLong()).Where(gid => (gid != 0) && !HandledGifts.Contains(gid))) { - HandledGifts.Add(gid); - - ArchiLogger.LogGenericInfo("Accepting gift: " + gid + "..."); - await LimitGiftsRequestsAsync().ConfigureAwait(false); - - ArchiHandler.RedeemGuestPassResponseCallback response = await ArchiHandler.RedeemGuestPass(gid).ConfigureAwait(false); - if (response != null) { - if (response.Result == EResult.OK) { - ArchiLogger.LogGenericInfo("Success!"); - } else { - ArchiLogger.LogGenericWarning("Failed with error: " + response.Result); - } - } else { - ArchiLogger.LogGenericWarning("Failed!"); - } - } + FamilySharingInactivityTimer.Change(Timeout.Infinite, Timeout.Infinite); + FamilySharingInactivityTimer.Dispose(); + FamilySharingInactivityTimer = null; } - private async void OnLicenseList(SteamApps.LicenseListCallback callback) { - if (callback?.LicenseList == null) { - ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LicenseList)); - return; - } - - OwnedPackageIDs.Clear(); - - foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList) { - OwnedPackageIDs.Add(license.PackageID); - } - - OwnedPackageIDs.TrimExcess(); - - await Task.Delay(1000).ConfigureAwait(false); // Wait a second for eventual PlayingSessionStateCallback - - if (!ArchiWebHandler.Ready) { - for (byte i = 0; (i < Program.GlobalConfig.HttpTimeout) && !ArchiWebHandler.Ready; i++) { - await Task.Delay(1000).ConfigureAwait(false); - } - - if (!ArchiWebHandler.Ready) { - return; - } - } - - await CardsFarmer.OnNewGameAdded().ConfigureAwait(false); - } - - private void OnChatInvite(SteamFriends.ChatInviteCallback callback) { - if ((callback?.ChatRoomID == null) || (callback.PatronID == null)) { - ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.ChatRoomID) + " || " + nameof(callback.PatronID)); - return; - } - - if (!IsMaster(callback.PatronID)) { - return; - } - - SteamFriends.JoinChat(callback.ChatRoomID); - } - - private async void OnChatMsg(SteamFriends.ChatMsgCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if (callback.ChatMsgType != EChatEntryType.ChatMsg) { - return; - } - - if ((callback.ChatRoomID == null) || (callback.ChatterID == null) || string.IsNullOrEmpty(callback.Message)) { - ArchiLogger.LogNullError(nameof(callback.ChatRoomID) + " || " + nameof(callback.ChatterID) + " || " + nameof(callback.Message)); - return; - } - - ArchiLogger.LogGenericTrace(callback.ChatRoomID.ConvertToUInt64() + "/" + callback.ChatterID.ConvertToUInt64() + ": " + callback.Message); - - switch (callback.Message) { - case "!leave": - if (!IsMaster(callback.ChatterID)) { - break; - } - - SteamFriends.LeaveChat(callback.ChatRoomID); - break; - default: - await HandleMessage(callback.ChatRoomID, callback.ChatterID, callback.Message).ConfigureAwait(false); - break; - } - } - - private void OnFriendsList(SteamFriends.FriendsListCallback callback) { - if (callback?.FriendList == null) { - ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.FriendList)); - return; - } - - foreach (SteamFriends.FriendsListCallback.Friend friend in callback.FriendList.Where(friend => friend.Relationship == EFriendRelationship.RequestRecipient)) { - if (IsMaster(friend.SteamID)) { - SteamFriends.AddFriend(friend.SteamID); - } else if (BotConfig.IsBotAccount) { - SteamFriends.RemoveFriend(friend.SteamID); - } - } - } - - private async void OnFriendMsg(SteamFriends.FriendMsgCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if (callback.EntryType != EChatEntryType.ChatMsg) { - return; - } - - if ((callback.Sender == null) || string.IsNullOrEmpty(callback.Message)) { - ArchiLogger.LogNullError(nameof(callback.Sender) + " || " + nameof(callback.Message)); - return; - } - - ArchiLogger.LogGenericTrace(callback.Sender.ConvertToUInt64() + ": " + callback.Message); - - await HandleMessage(callback.Sender, callback.Sender, callback.Message).ConfigureAwait(false); - } - - private async void OnFriendMsgHistory(SteamFriends.FriendMsgHistoryCallback callback) { - if ((callback?.Messages == null) || (callback.SteamID == null)) { - ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.Messages) + " || " + nameof(callback.SteamID)); - return; - } - - if ((callback.Messages.Count == 0) || !IsMaster(callback.SteamID)) { - return; - } - - // Get last message - SteamFriends.FriendMsgHistoryCallback.FriendMessage lastMessage = callback.Messages[callback.Messages.Count - 1]; - - // If message is read already, return - if (!lastMessage.Unread) { - return; - } - - // If message is too old, return - if (DateTime.UtcNow.Subtract(lastMessage.Timestamp).TotalHours > 1) { - return; - } - - // Handle the message - await HandleMessage(callback.SteamID, callback.SteamID, lastMessage.Message).ConfigureAwait(false); - } - - private void OnPersonaState(SteamFriends.PersonaStateCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if (callback.FriendID == SteamClient.SteamID) { - Events.OnStateUpdated(this, callback); - } else if ((callback.FriendID == LibraryLockedBySteamID) && (callback.GameID == 0)) { - LibraryLockedBySteamID = 0; - CheckOccupationStatus(); - } - } - - private void OnAccountInfo(SteamUser.AccountInfoCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if (!BotConfig.FarmOffline) { - SteamFriends.SetPersonaState(EPersonaState.Online); - } - } - - private void OnLoggedOff(SteamUser.LoggedOffCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - ArchiLogger.LogGenericInfo("Logged off of Steam: " + callback.Result); - - switch (callback.Result) { - case EResult.LogonSessionReplaced: - ArchiLogger.LogGenericError("This account seems to be used in another ASF instance, which is undefined behaviour, refusing to keep it running!"); - Stop(); - break; - } - } - - private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - // Always reset one-time-only access tokens - AuthCode = TwoFactorCode = null; - - // Keep LastLogOnResult for OnDisconnected() - LastLogOnResult = callback.Result; - - switch (callback.Result) { - case EResult.AccountLogonDenied: - AuthCode = Program.GetUserInput(ASF.EUserInputType.SteamGuard, BotName); - if (string.IsNullOrEmpty(AuthCode)) { - Stop(); - } - - break; - case EResult.AccountLoginDeniedNeedTwoFactor: - if (BotDatabase.MobileAuthenticator == null) { - TwoFactorCode = Program.GetUserInput(ASF.EUserInputType.TwoFactorAuthentication, BotName); - if (string.IsNullOrEmpty(TwoFactorCode)) { - Stop(); - } - } else { - ArchiLogger.LogGenericWarning("2FA code was invalid despite of using ASF 2FA. Invalid authenticator or bad timing?"); - } - - break; - case EResult.OK: - ArchiLogger.LogGenericInfo("Successfully logged on!"); - - // Old status for these doesn't matter, we'll update them if needed - LibraryLockedBySteamID = 0; - IsLimitedUser = PlayingBlocked = false; - - if (callback.AccountFlags.HasFlag(EAccountFlags.LimitedUser)) { - IsLimitedUser = true; - ArchiLogger.LogGenericWarning("This account is limited, farming process is permanently unavailable until the restriction is removed!"); - } - - if ((callback.CellID != 0) && (Program.GlobalDatabase.CellID != callback.CellID)) { - Program.GlobalDatabase.CellID = callback.CellID; - } - - if (BotDatabase.MobileAuthenticator == null) { - // Support and convert SDA files - string maFilePath = Path.Combine(SharedInfo.ConfigDirectory, callback.ClientSteamID.ConvertToUInt64() + ".maFile"); - if (File.Exists(maFilePath)) { - ImportAuthenticator(maFilePath); - } - } - - if (string.IsNullOrEmpty(BotConfig.SteamParentalPIN)) { - BotConfig.SteamParentalPIN = Program.GetUserInput(ASF.EUserInputType.SteamParentalPIN, BotName); - if (string.IsNullOrEmpty(BotConfig.SteamParentalPIN)) { - Stop(); - return; - } - } - - if (!await ArchiWebHandler.Init(callback.ClientSteamID, SteamClient.ConnectedUniverse, callback.WebAPIUserNonce, BotConfig.SteamParentalPIN).ConfigureAwait(false)) { - if (!await RefreshSession().ConfigureAwait(false)) { - return; - } - } - - InitializeFamilySharing().Forget(); - - if (BotConfig.DismissInventoryNotifications) { - ArchiWebHandler.MarkInventory().Forget(); - } - - if (BotConfig.SteamMasterClanID != 0) { - Task.Run(async () => { - await ArchiWebHandler.JoinGroup(BotConfig.SteamMasterClanID).ConfigureAwait(false); - JoinMasterChat(); - }).Forget(); - } - - if (Program.GlobalConfig.Statistics) { - ArchiWebHandler.JoinGroup(SharedInfo.ASFGroupSteamID).Forget(); - } - - Trading.CheckTrades().Forget(); - break; - case EResult.InvalidPassword: - case EResult.NoConnection: - case EResult.RateLimitExceeded: - case EResult.ServiceUnavailable: - case EResult.Timeout: - case EResult.TryAnotherCM: - case EResult.TwoFactorCodeMismatch: - ArchiLogger.LogGenericWarning("Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult); - break; - default: // Unexpected result, shutdown immediately - ArchiLogger.LogGenericError("Unable to login to Steam: " + callback.Result + " / " + callback.ExtendedResult); - Stop(); - break; - } - } - - private void OnLoginKey(SteamUser.LoginKeyCallback callback) { - if (string.IsNullOrEmpty(callback?.LoginKey)) { - ArchiLogger.LogNullError(nameof(callback) + " || " + nameof(callback.LoginKey)); - return; - } - - string loginKey = callback.LoginKey; - if (!string.IsNullOrEmpty(loginKey)) { - loginKey = CryptoHelper.Encrypt(BotConfig.PasswordFormat, loginKey); - } - - BotDatabase.LoginKey = loginKey; - SteamUser.AcceptNewLoginKey(callback); - } - - private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - int fileSize; - byte[] sentryHash; - - try { - using (FileStream fileStream = File.Open(SentryFile, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { - fileStream.Seek(callback.Offset, SeekOrigin.Begin); - fileStream.Write(callback.Data, 0, callback.BytesToWrite); - fileSize = (int) fileStream.Length; - - fileStream.Seek(0, SeekOrigin.Begin); - using (SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider()) { - sentryHash = sha.ComputeHash(fileStream); - } - } - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - return; - } - - // Inform the steam servers that we're accepting this sentry file - SteamUser.SendMachineAuthResponse(new SteamUser.MachineAuthDetails { - JobID = callback.JobID, - FileName = callback.FileName, - BytesWritten = callback.BytesToWrite, - FileSize = fileSize, - Offset = callback.Offset, - Result = EResult.OK, - LastError = 0, - OneTimePassword = callback.OneTimePassword, - SentryFileHash = sentryHash - }); - } - - private void OnWebAPIUserNonce(SteamUser.WebAPIUserNonceCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - } - } - - private void OnNotifications(ArchiHandler.NotificationsCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if ((callback.Notifications == null) || (callback.Notifications.Count == 0)) { - return; - } - - foreach (ArchiHandler.NotificationsCallback.ENotification notification in callback.Notifications) { - switch (notification) { - case ArchiHandler.NotificationsCallback.ENotification.Items: - CardsFarmer.OnNewItemsNotification().Forget(); - if (BotConfig.DismissInventoryNotifications) { - ArchiWebHandler.MarkInventory().Forget(); - } - break; - case ArchiHandler.NotificationsCallback.ENotification.Trading: - Trading.CheckTrades().Forget(); - break; - } - } - } - - private void OnOfflineMessage(ArchiHandler.OfflineMessageCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if ((callback.OfflineMessagesCount == 0) || !BotConfig.HandleOfflineMessages) { - return; - } - - SteamFriends.RequestOfflineMessages(); - } - - private void OnPlayingSessionState(ArchiHandler.PlayingSessionStateCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - if (callback.PlayingBlocked == PlayingBlocked) { - return; // No status update, we're not interested - } - - PlayingBlocked = callback.PlayingBlocked; - CheckOccupationStatus(); - } - - private void OnPurchaseResponse(ArchiHandler.PurchaseResponseCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - } - } - - private void OnSharedLibraryLockStatus(ArchiHandler.SharedLibraryLockStatusCallback callback) { - if (callback == null) { - ArchiLogger.LogNullError(nameof(callback)); - return; - } - - // Ignore no status updates - if (LibraryLockedBySteamID == 0) { - if ((callback.LibraryLockedBySteamID == 0) || (callback.LibraryLockedBySteamID == SteamClient.SteamID)) { - return; - } - - LibraryLockedBySteamID = callback.LibraryLockedBySteamID; - } else { - if ((callback.LibraryLockedBySteamID != 0) && (callback.LibraryLockedBySteamID != SteamClient.SteamID)) { - return; - } - - if (SteamFriends.GetFriendGamePlayed(LibraryLockedBySteamID) != 0) { - return; - } - - LibraryLockedBySteamID = 0; - } - - CheckOccupationStatus(); + [Flags] + private enum ERedeemFlags : byte { + None = 0, + Validate = 1, + ForceForwarding = 2, + SkipForwarding = 4, + ForceDistribution = 8, + SkipDistribution = 16, + SkipInitial = 32 } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index fcc774e8d..ee8fdee11 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -22,12 +22,12 @@ */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using Newtonsoft.Json; namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] @@ -35,98 +35,14 @@ namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ConvertToConstant.Local")] [SuppressMessage("ReSharper", "ConvertToConstant.Global")] internal sealed class BotConfig { - internal enum EFarmingOrder : byte { - Unordered, - AppIDsAscending, - AppIDsDescending, - CardDropsAscending, - CardDropsDescending, - HoursAscending, - HoursDescending, - NamesAscending, - NamesDescending - } - - [Flags] - internal enum ETradingPreferences : byte { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - None = 0, - AcceptDonations = 1, - SteamTradeMatcher = 2, - MatchEverything = 4 - } - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool Enabled = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool Paused = false; - - [JsonProperty] - internal string SteamLogin { get; set; } - - [JsonProperty] - internal string SteamPassword { get; set; } - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly CryptoHelper.ECryptoMethod PasswordFormat = CryptoHelper.ECryptoMethod.PlainText; - - [JsonProperty] - internal string SteamParentalPIN { get; set; } = "0"; - - [JsonProperty] - internal readonly string SteamApiKey = null; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ulong SteamMasterID = 0; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ulong SteamMasterClanID = 0; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool CardDropsRestricted = true; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool DismissInventoryNotifications = true; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly EFarmingOrder FarmingOrder = EFarmingOrder.Unordered; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool FarmOffline = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool HandleOfflineMessages = false; + internal readonly byte AcceptConfirmationsPeriod = 0; [JsonProperty(Required = Required.DisallowNull)] internal readonly bool AcceptGifts = false; [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool IsBotAccount = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool ForwardKeysToOtherBots = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool DistributeKeys = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool ShutdownOnFarmingFinished = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool SendOnFarmingFinished = false; - - [JsonProperty] - internal readonly string SteamTradeToken = null; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte SendTradePeriod = 0; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ETradingPreferences TradingPreferences = ETradingPreferences.AcceptDonations; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte AcceptConfirmationsPeriod = 0; + internal readonly bool CardDropsRestricted = true; [JsonProperty] internal readonly string CustomGamePlayedWhileFarming = null; @@ -134,9 +50,75 @@ namespace ArchiSteamFarm { [JsonProperty] internal readonly string CustomGamePlayedWhileIdle = null; + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool DismissInventoryNotifications = true; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool DistributeKeys = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool Enabled = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly EFarmingOrder FarmingOrder = EFarmingOrder.Unordered; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool FarmOffline = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool ForwardKeysToOtherBots = false; + [JsonProperty(Required = Required.DisallowNull)] internal readonly HashSet GamesPlayedWhileIdle = new HashSet(); + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool HandleOfflineMessages = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool IsBotAccount = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly CryptoHelper.ECryptoMethod PasswordFormat = CryptoHelper.ECryptoMethod.PlainText; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool Paused = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool SendOnFarmingFinished = false; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly byte SendTradePeriod = 0; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool ShutdownOnFarmingFinished = false; + + [JsonProperty] + internal readonly string SteamApiKey = null; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly ulong SteamMasterClanID = 0; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly ulong SteamMasterID = 0; + + [JsonProperty] + internal readonly string SteamTradeToken = null; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly ETradingPreferences TradingPreferences = ETradingPreferences.AcceptDonations; + + [JsonProperty] + internal string SteamLogin { get; set; } + + [JsonProperty] + internal string SteamParentalPIN { get; set; } = "0"; + + [JsonProperty] + internal string SteamPassword { get; set; } + + // This constructor is used only by deserializer + private BotConfig() { } + internal static BotConfig Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { ASF.ArchiLogger.LogNullError(nameof(filePath)); @@ -182,7 +164,25 @@ namespace ArchiSteamFarm { return botConfig; } - // This constructor is used only by deserializer - private BotConfig() { } + internal enum EFarmingOrder : byte { + Unordered, + AppIDsAscending, + AppIDsDescending, + CardDropsAscending, + CardDropsDescending, + HoursAscending, + HoursDescending, + NamesAscending, + NamesDescending + } + + [Flags] + internal enum ETradingPreferences : byte { + [SuppressMessage("ReSharper", "UnusedMember.Global")] + None = 0, + AcceptDonations = 1, + SteamTradeMatcher = 2, + MatchEverything = 4 + } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/BotDatabase.cs b/ArchiSteamFarm/BotDatabase.cs index a9c642a9a..1eec758f4 100644 --- a/ArchiSteamFarm/BotDatabase.cs +++ b/ArchiSteamFarm/BotDatabase.cs @@ -22,21 +22,18 @@ */ -using Newtonsoft.Json; using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; +using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class BotDatabase { - [JsonProperty] - private string _LoginKey; + private readonly object FileLock = new object(); internal string LoginKey { - get { - return _LoginKey; - } + get { return _LoginKey; } set { if (_LoginKey == value) { @@ -48,13 +45,8 @@ namespace ArchiSteamFarm { } } - [JsonProperty] - private MobileAuthenticator _MobileAuthenticator; - internal MobileAuthenticator MobileAuthenticator { - get { - return _MobileAuthenticator; - } + get { return _MobileAuthenticator; } set { if (_MobileAuthenticator == value) { @@ -66,10 +58,28 @@ namespace ArchiSteamFarm { } } - private readonly object FileLock = new object(); + [JsonProperty] + private string _LoginKey; + + [JsonProperty] + private MobileAuthenticator _MobileAuthenticator; private string FilePath; + // This constructor is used when creating new database + private BotDatabase(string filePath) { + if (string.IsNullOrEmpty(filePath)) { + throw new ArgumentNullException(nameof(filePath)); + } + + FilePath = filePath; + Save(); + } + + // This constructor is used only by deserializer + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private BotDatabase() { } + internal static BotDatabase Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { ASF.ArchiLogger.LogNullError(nameof(filePath)); @@ -98,20 +108,6 @@ namespace ArchiSteamFarm { return botDatabase; } - // This constructor is used when creating new database - private BotDatabase(string filePath) { - if (string.IsNullOrEmpty(filePath)) { - throw new ArgumentNullException(nameof(filePath)); - } - - FilePath = filePath; - Save(); - } - - // This constructor is used only by deserializer - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private BotDatabase() { } - internal void Save() { string json = JsonConvert.SerializeObject(this); if (string.IsNullOrEmpty(json)) { @@ -133,4 +129,4 @@ namespace ArchiSteamFarm { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index 189b4e1ab..0b86fdb70 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -22,7 +22,6 @@ */ -using HtmlAgilityPack; using System; using System.Collections.Generic; using System.Globalization; @@ -30,71 +29,30 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using HtmlAgilityPack; using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class CardsFarmer : IDisposable { - internal sealed class Game { - [JsonProperty] - internal readonly uint AppID; - - [JsonProperty] - internal readonly string GameName; - - [JsonProperty] - internal float HoursPlayed { get; set; } - - [JsonProperty] - internal ushort CardsRemaining { get; set; } - - //internal string HeaderURL => "https://steamcdn-a.akamaihd.net/steam/apps/" + AppID + "/header.jpg"; - - internal Game(uint appID, string gameName, float hoursPlayed, ushort cardsRemaining) { - if ((appID == 0) || string.IsNullOrEmpty(gameName) || (hoursPlayed < 0) || (cardsRemaining == 0)) { - throw new ArgumentOutOfRangeException(nameof(appID) + " || " + nameof(gameName) + " || " + nameof(hoursPlayed) + " || " + nameof(cardsRemaining)); - } - - AppID = appID; - GameName = gameName; - HoursPlayed = hoursPlayed; - CardsRemaining = cardsRemaining; - } - - public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) { - return false; - } - - if (ReferenceEquals(this, obj)) { - return true; - } - - Game game = obj as Game; - return (game != null) && Equals(game); - } - - public override int GetHashCode() => (int) AppID; - - private bool Equals(Game other) => AppID == other.AppID; - } - internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network - [JsonProperty] - internal readonly ConcurrentHashSet GamesToFarm = new ConcurrentHashSet(); - [JsonProperty] internal readonly ConcurrentHashSet CurrentGamesFarming = new ConcurrentHashSet(); - private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false); - private readonly SemaphoreSlim FarmingSemaphore = new SemaphoreSlim(1); + [JsonProperty] + internal readonly ConcurrentHashSet GamesToFarm = new ConcurrentHashSet(); + private readonly Bot Bot; + private readonly SemaphoreSlim FarmingSemaphore = new SemaphoreSlim(1); + private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false); private readonly Timer IdleFarmingTimer; [JsonProperty] internal bool Paused { get; private set; } - private bool KeepFarming, NowFarming, StickyPause; + private bool KeepFarming; + private bool NowFarming; + private bool StickyPause; internal CardsFarmer(Bot bot) { if (bot == null) { @@ -104,16 +62,50 @@ namespace ArchiSteamFarm { Bot = bot; if (Program.GlobalConfig.IdleFarmingPeriod > 0) { - IdleFarmingTimer = new Timer( - e => CheckGamesForFarming(), - null, - TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) + TimeSpan.FromMinutes(0.5 * Bot.Bots.Count), // Delay + IdleFarmingTimer = new Timer(e => CheckGamesForFarming(), null, TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) + TimeSpan.FromMinutes(0.5*Bot.Bots.Count), // Delay TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) // Period ); } } - internal void SetInitialState(bool paused) => StickyPause = Paused = paused; + public void Dispose() { + // Those are objects that are always being created if constructor doesn't throw exception + CurrentGamesFarming.Dispose(); + GamesToFarm.Dispose(); + FarmingSemaphore.Dispose(); + FarmResetEvent.Dispose(); + + // Those are objects that might be null and the check should be in-place + IdleFarmingTimer?.Dispose(); + } + + internal void OnDisconnected() => StopFarming().Forget(); + + internal async Task OnNewGameAdded() { + if (!NowFarming) { + // If we're not farming yet, obviously it's worth it to make a check + StartFarming().Forget(); + return; + } + + if (Bot.BotConfig.CardDropsRestricted && (GamesToFarm.Count > 0) && (GamesToFarm.Min(game => game.HoursPlayed) < 2)) { + // If we have Complex algorithm and some games to boost, it's also worth to make a check + // That's because we would check for new games after our current round anyway + await StopFarming().ConfigureAwait(false); + StartFarming().Forget(); + } + } + + internal async Task OnNewItemsNotification() { + if (NowFarming) { + FarmResetEvent.Set(); + return; + } + + // If we're not farming, and we got new items, it's likely to be a booster pack or likewise + // In this case, perform a loot if user wants to do so + await Bot.LootIfNeeded().ConfigureAwait(false); + } internal async Task Pause(bool sticky) { if (sticky) { @@ -142,6 +134,8 @@ namespace ArchiSteamFarm { } } + internal void SetInitialState(bool paused) => StickyPause = Paused = paused; + internal async Task StartFarming() { if (NowFarming || Paused || !Bot.IsPlayingPossible) { return; @@ -257,117 +251,12 @@ namespace ArchiSteamFarm { } } - internal void OnDisconnected() => StopFarming().Forget(); - - internal async Task OnNewItemsNotification() { - if (NowFarming) { - FarmResetEvent.Set(); + private void CheckGamesForFarming() { + if (NowFarming || Paused || !Bot.IsConnectedAndLoggedOn) { return; } - // If we're not farming, and we got new items, it's likely to be a booster pack or likewise - // In this case, perform a loot if user wants to do so - await Bot.LootIfNeeded().ConfigureAwait(false); - } - - internal async Task OnNewGameAdded() { - if (!NowFarming) { - // If we're not farming yet, obviously it's worth it to make a check - StartFarming().Forget(); - return; - } - - if (Bot.BotConfig.CardDropsRestricted && (GamesToFarm.Count > 0) && (GamesToFarm.Min(game => game.HoursPlayed) < 2)) { - // If we have Complex algorithm and some games to boost, it's also worth to make a check - // That's because we would check for new games after our current round anyway - await StopFarming().ConfigureAwait(false); - StartFarming().Forget(); - } - } - - private async Task IsAnythingToFarm() { - Bot.ArchiLogger.LogGenericInfo("Checking badges..."); - - // Find the number of badge pages - Bot.ArchiLogger.LogGenericInfo("Checking first page..."); - HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(1).ConfigureAwait(false); - if (htmlDocument == null) { - Bot.ArchiLogger.LogGenericWarning("Could not get badges information, will try again later!"); - return false; - } - - byte maxPages = 1; - - HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("(//a[@class='pagelink'])[last()]"); - if (htmlNode != null) { - string lastPage = htmlNode.InnerText; - if (string.IsNullOrEmpty(lastPage)) { - Bot.ArchiLogger.LogNullError(nameof(lastPage)); - return false; - } - - if (!byte.TryParse(lastPage, out maxPages) || (maxPages == 0)) { - Bot.ArchiLogger.LogNullError(nameof(maxPages)); - return false; - } - } - - GamesToFarm.ClearAndTrim(); - CheckPage(htmlDocument); - - if (maxPages == 1) { - SortGamesToFarm(); - return GamesToFarm.Count > 0; - } - - Bot.ArchiLogger.LogGenericInfo("Checking other pages..."); - - List tasks = new List(maxPages - 1); - for (byte page = 2; page <= maxPages; page++) { - byte currentPage = page; // We need a copy of variable being passed when in for loops, as loop will proceed before task is launched - tasks.Add(CheckPage(currentPage)); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - SortGamesToFarm(); - return GamesToFarm.Count > 0; - } - - private void SortGamesToFarm() { - IOrderedEnumerable gamesToFarm; - switch (Bot.BotConfig.FarmingOrder) { - case BotConfig.EFarmingOrder.Unordered: - return; - case BotConfig.EFarmingOrder.AppIDsAscending: - gamesToFarm = GamesToFarm.OrderBy(game => game.AppID); - break; - case BotConfig.EFarmingOrder.AppIDsDescending: - gamesToFarm = GamesToFarm.OrderByDescending(game => game.AppID); - break; - case BotConfig.EFarmingOrder.CardDropsAscending: - gamesToFarm = GamesToFarm.OrderBy(game => game.CardsRemaining); - break; - case BotConfig.EFarmingOrder.CardDropsDescending: - gamesToFarm = GamesToFarm.OrderByDescending(game => game.CardsRemaining); - break; - case BotConfig.EFarmingOrder.HoursAscending: - gamesToFarm = GamesToFarm.OrderBy(game => game.HoursPlayed); - break; - case BotConfig.EFarmingOrder.HoursDescending: - gamesToFarm = GamesToFarm.OrderByDescending(game => game.HoursPlayed); - break; - case BotConfig.EFarmingOrder.NamesAscending: - gamesToFarm = GamesToFarm.OrderBy(game => game.GameName); - break; - case BotConfig.EFarmingOrder.NamesDescending: - gamesToFarm = GamesToFarm.OrderByDescending(game => game.GameName); - break; - default: - Bot.ArchiLogger.LogGenericError("Unhandled case: " + Bot.BotConfig.FarmingOrder); - return; - } - - GamesToFarm.ReplaceWith(gamesToFarm.ToList()); // We must call ToList() here as we can't enumerate during replacing + StartFarming().Forget(); } private void CheckPage(HtmlDocument htmlDocument) { @@ -513,92 +402,6 @@ namespace ArchiSteamFarm { CheckPage(htmlDocument); } - private void CheckGamesForFarming() { - if (NowFarming || Paused || !Bot.IsConnectedAndLoggedOn) { - return; - } - - StartFarming().Forget(); - } - - private async Task ShouldFarm(Game game) { - if (game == null) { - Bot.ArchiLogger.LogNullError(nameof(game)); - return false; - } - - HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(game.AppID).ConfigureAwait(false); - if (htmlDocument == null) { - return null; - } - - HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']"); - if (htmlNode == null) { - Bot.ArchiLogger.LogNullError(nameof(htmlNode)); - return null; - } - - string progress = htmlNode.InnerText; - if (string.IsNullOrEmpty(progress)) { - Bot.ArchiLogger.LogNullError(nameof(progress)); - return null; - } - - ushort cardsRemaining = 0; - - Match match = Regex.Match(progress, @"\d+"); - if (match.Success) { - if (!ushort.TryParse(match.Value, out cardsRemaining)) { - Bot.ArchiLogger.LogNullError(nameof(cardsRemaining)); - return null; - } - } - - game.CardsRemaining = cardsRemaining; - - Bot.ArchiLogger.LogGenericInfo("Status for " + game.AppID + " (" + game.GameName + "): " + cardsRemaining + " cards remaining"); - return cardsRemaining > 0; - } - - private bool FarmMultiple(IEnumerable games) { - if (games == null) { - Bot.ArchiLogger.LogNullError(nameof(games)); - return false; - } - - CurrentGamesFarming.ReplaceWith(games); - - Bot.ArchiLogger.LogGenericInfo("Now farming: " + string.Join(", ", CurrentGamesFarming.Select(game => game.AppID))); - - bool result = FarmHours(CurrentGamesFarming); - CurrentGamesFarming.ClearAndTrim(); - return result; - } - - private async Task FarmSolo(Game game) { - if (game == null) { - Bot.ArchiLogger.LogNullError(nameof(game)); - return true; - } - - CurrentGamesFarming.Add(game); - - Bot.ArchiLogger.LogGenericInfo("Now farming: " + game.AppID + " (" + game.GameName + ")"); - - bool result = await Farm(game).ConfigureAwait(false); - CurrentGamesFarming.ClearAndTrim(); - - if (!result) { - return false; - } - - GamesToFarm.Remove(game); - - TimeSpan timeSpan = TimeSpan.FromHours(game.HoursPlayed); - Bot.ArchiLogger.LogGenericInfo("Done farming: " + game.AppID + " (" + game.GameName + ") after " + timeSpan.ToString(@"hh\:mm") + " hours of playtime!"); - return true; - } - private async Task Farm(Game game) { if (game == null) { Bot.ArchiLogger.LogNullError(nameof(game)); @@ -615,7 +418,7 @@ namespace ArchiSteamFarm { Bot.ArchiLogger.LogGenericInfo("Still farming: " + game.AppID + " (" + game.GameName + ")"); DateTime startFarmingPeriod = DateTime.Now; - if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) { + if (FarmResetEvent.Wait(60*1000*Program.GlobalConfig.FarmingDelay)) { FarmResetEvent.Reset(); success = KeepFarming; } @@ -658,7 +461,7 @@ namespace ArchiSteamFarm { Bot.ArchiLogger.LogGenericInfo("Still farming: " + string.Join(", ", games.Select(game => game.AppID))); DateTime startFarmingPeriod = DateTime.Now; - if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) { + if (FarmResetEvent.Wait(60*1000*Program.GlobalConfig.FarmingDelay)) { FarmResetEvent.Reset(); success = KeepFarming; } @@ -680,15 +483,211 @@ namespace ArchiSteamFarm { return success; } - public void Dispose() { - // Those are objects that are always being created if constructor doesn't throw exception - CurrentGamesFarming.Dispose(); - GamesToFarm.Dispose(); - FarmingSemaphore.Dispose(); - FarmResetEvent.Dispose(); + private bool FarmMultiple(IEnumerable games) { + if (games == null) { + Bot.ArchiLogger.LogNullError(nameof(games)); + return false; + } - // Those are objects that might be null and the check should be in-place - IdleFarmingTimer?.Dispose(); + CurrentGamesFarming.ReplaceWith(games); + + Bot.ArchiLogger.LogGenericInfo("Now farming: " + string.Join(", ", CurrentGamesFarming.Select(game => game.AppID))); + + bool result = FarmHours(CurrentGamesFarming); + CurrentGamesFarming.ClearAndTrim(); + return result; + } + + private async Task FarmSolo(Game game) { + if (game == null) { + Bot.ArchiLogger.LogNullError(nameof(game)); + return true; + } + + CurrentGamesFarming.Add(game); + + Bot.ArchiLogger.LogGenericInfo("Now farming: " + game.AppID + " (" + game.GameName + ")"); + + bool result = await Farm(game).ConfigureAwait(false); + CurrentGamesFarming.ClearAndTrim(); + + if (!result) { + return false; + } + + GamesToFarm.Remove(game); + + TimeSpan timeSpan = TimeSpan.FromHours(game.HoursPlayed); + Bot.ArchiLogger.LogGenericInfo("Done farming: " + game.AppID + " (" + game.GameName + ") after " + timeSpan.ToString(@"hh\:mm") + " hours of playtime!"); + return true; + } + + private async Task IsAnythingToFarm() { + Bot.ArchiLogger.LogGenericInfo("Checking badges..."); + + // Find the number of badge pages + Bot.ArchiLogger.LogGenericInfo("Checking first page..."); + HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(1).ConfigureAwait(false); + if (htmlDocument == null) { + Bot.ArchiLogger.LogGenericWarning("Could not get badges information, will try again later!"); + return false; + } + + byte maxPages = 1; + + HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("(//a[@class='pagelink'])[last()]"); + if (htmlNode != null) { + string lastPage = htmlNode.InnerText; + if (string.IsNullOrEmpty(lastPage)) { + Bot.ArchiLogger.LogNullError(nameof(lastPage)); + return false; + } + + if (!byte.TryParse(lastPage, out maxPages) || (maxPages == 0)) { + Bot.ArchiLogger.LogNullError(nameof(maxPages)); + return false; + } + } + + GamesToFarm.ClearAndTrim(); + CheckPage(htmlDocument); + + if (maxPages == 1) { + SortGamesToFarm(); + return GamesToFarm.Count > 0; + } + + Bot.ArchiLogger.LogGenericInfo("Checking other pages..."); + + List tasks = new List(maxPages - 1); + for (byte page = 2; page <= maxPages; page++) { + byte currentPage = page; // We need a copy of variable being passed when in for loops, as loop will proceed before task is launched + tasks.Add(CheckPage(currentPage)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + SortGamesToFarm(); + return GamesToFarm.Count > 0; + } + + private async Task ShouldFarm(Game game) { + if (game == null) { + Bot.ArchiLogger.LogNullError(nameof(game)); + return false; + } + + HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(game.AppID).ConfigureAwait(false); + if (htmlDocument == null) { + return null; + } + + HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']"); + if (htmlNode == null) { + Bot.ArchiLogger.LogNullError(nameof(htmlNode)); + return null; + } + + string progress = htmlNode.InnerText; + if (string.IsNullOrEmpty(progress)) { + Bot.ArchiLogger.LogNullError(nameof(progress)); + return null; + } + + ushort cardsRemaining = 0; + + Match match = Regex.Match(progress, @"\d+"); + if (match.Success) { + if (!ushort.TryParse(match.Value, out cardsRemaining)) { + Bot.ArchiLogger.LogNullError(nameof(cardsRemaining)); + return null; + } + } + + game.CardsRemaining = cardsRemaining; + + Bot.ArchiLogger.LogGenericInfo("Status for " + game.AppID + " (" + game.GameName + "): " + cardsRemaining + " cards remaining"); + return cardsRemaining > 0; + } + + private void SortGamesToFarm() { + IOrderedEnumerable gamesToFarm; + switch (Bot.BotConfig.FarmingOrder) { + case BotConfig.EFarmingOrder.Unordered: + return; + case BotConfig.EFarmingOrder.AppIDsAscending: + gamesToFarm = GamesToFarm.OrderBy(game => game.AppID); + break; + case BotConfig.EFarmingOrder.AppIDsDescending: + gamesToFarm = GamesToFarm.OrderByDescending(game => game.AppID); + break; + case BotConfig.EFarmingOrder.CardDropsAscending: + gamesToFarm = GamesToFarm.OrderBy(game => game.CardsRemaining); + break; + case BotConfig.EFarmingOrder.CardDropsDescending: + gamesToFarm = GamesToFarm.OrderByDescending(game => game.CardsRemaining); + break; + case BotConfig.EFarmingOrder.HoursAscending: + gamesToFarm = GamesToFarm.OrderBy(game => game.HoursPlayed); + break; + case BotConfig.EFarmingOrder.HoursDescending: + gamesToFarm = GamesToFarm.OrderByDescending(game => game.HoursPlayed); + break; + case BotConfig.EFarmingOrder.NamesAscending: + gamesToFarm = GamesToFarm.OrderBy(game => game.GameName); + break; + case BotConfig.EFarmingOrder.NamesDescending: + gamesToFarm = GamesToFarm.OrderByDescending(game => game.GameName); + break; + default: + Bot.ArchiLogger.LogGenericError("Unhandled case: " + Bot.BotConfig.FarmingOrder); + return; + } + + GamesToFarm.ReplaceWith(gamesToFarm.ToList()); // We must call ToList() here as we can't enumerate during replacing + } + + internal sealed class Game { + [JsonProperty] + internal readonly uint AppID; + + [JsonProperty] + internal readonly string GameName; + + [JsonProperty] + internal ushort CardsRemaining { get; set; } + + [JsonProperty] + internal float HoursPlayed { get; set; } + + //internal string HeaderURL => "https://steamcdn-a.akamaihd.net/steam/apps/" + AppID + "/header.jpg"; + + internal Game(uint appID, string gameName, float hoursPlayed, ushort cardsRemaining) { + if ((appID == 0) || string.IsNullOrEmpty(gameName) || (hoursPlayed < 0) || (cardsRemaining == 0)) { + throw new ArgumentOutOfRangeException(nameof(appID) + " || " + nameof(gameName) + " || " + nameof(hoursPlayed) + " || " + nameof(cardsRemaining)); + } + + AppID = appID; + GameName = gameName; + HoursPlayed = hoursPlayed; + CardsRemaining = cardsRemaining; + } + + public override bool Equals(object obj) { + if (ReferenceEquals(null, obj)) { + return false; + } + + if (ReferenceEquals(this, obj)) { + return true; + } + + Game game = obj as Game; + return (game != null) && Equals(game); + } + + public override int GetHashCode() => (int) AppID; + + private bool Equals(Game other) => AppID == other.AppID; } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/ConcurrentEnumerator.cs b/ArchiSteamFarm/ConcurrentEnumerator.cs index a9e0268bc..cd27b4ef0 100644 --- a/ArchiSteamFarm/ConcurrentEnumerator.cs +++ b/ArchiSteamFarm/ConcurrentEnumerator.cs @@ -31,11 +31,11 @@ namespace ArchiSteamFarm { internal sealed class ConcurrentEnumerator : IEnumerator { public T Current => Enumerator.Current; - object IEnumerator.Current => Current; - private readonly IEnumerator Enumerator; private readonly ReaderWriterLockSlim Lock; + object IEnumerator.Current => Current; + internal ConcurrentEnumerator(ICollection collection, ReaderWriterLockSlim rwLock) { if ((collection == null) || (rwLock == null)) { throw new ArgumentNullException(nameof(collection) + " || " + nameof(rwLock)); @@ -47,9 +47,9 @@ namespace ArchiSteamFarm { Enumerator = collection.GetEnumerator(); } + public void Dispose() => Lock?.ExitReadLock(); + public bool MoveNext() => Enumerator.MoveNext(); public void Reset() => Enumerator.Reset(); - - public void Dispose() => Lock?.ExitReadLock(); } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/ConcurrentHashSet.cs b/ArchiSteamFarm/ConcurrentHashSet.cs index 1b327e76b..bb45334f6 100644 --- a/ArchiSteamFarm/ConcurrentHashSet.cs +++ b/ArchiSteamFarm/ConcurrentHashSet.cs @@ -29,15 +29,6 @@ using System.Threading; namespace ArchiSteamFarm { internal sealed class ConcurrentHashSet : ICollection, IDisposable { - private readonly HashSet HashSet = new HashSet(); - private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); - - public bool IsReadOnly => false; - public IEnumerator GetEnumerator() => new ConcurrentEnumerator(HashSet, Lock); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - void ICollection.Add(T item) => Add(item); - public int Count { get { Lock.EnterReadLock(); @@ -50,6 +41,11 @@ namespace ArchiSteamFarm { } } + public bool IsReadOnly => false; + + private readonly HashSet HashSet = new HashSet(); + private readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); + public void Clear() { Lock.EnterWriteLock(); @@ -70,18 +66,6 @@ namespace ArchiSteamFarm { } } - public bool Remove(T item) { - Lock.EnterWriteLock(); - - try { - return HashSet.Remove(item); - } finally { - Lock.ExitWriteLock(); - } - } - - public void Dispose() => Lock.Dispose(); - public void CopyTo(T[] array, int arrayIndex) { Lock.EnterReadLock(); @@ -92,6 +76,23 @@ namespace ArchiSteamFarm { } } + public void Dispose() => Lock.Dispose(); + public IEnumerator GetEnumerator() => new ConcurrentEnumerator(HashSet, Lock); + + public bool Remove(T item) { + Lock.EnterWriteLock(); + + try { + return HashSet.Remove(item); + } finally { + Lock.ExitWriteLock(); + } + } + + void ICollection.Add(T item) => Add(item); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + internal void Add(T item) { Lock.EnterWriteLock(); @@ -102,6 +103,17 @@ namespace ArchiSteamFarm { } } + internal void ClearAndTrim() { + Lock.EnterWriteLock(); + + try { + HashSet.Clear(); + HashSet.TrimExcess(); + } finally { + Lock.ExitWriteLock(); + } + } + internal bool ReplaceIfNeededWith(HashSet items) { Lock.EnterUpgradeableReadLock(); @@ -133,17 +145,6 @@ namespace ArchiSteamFarm { } } - internal void ClearAndTrim() { - Lock.EnterWriteLock(); - - try { - HashSet.Clear(); - HashSet.TrimExcess(); - } finally { - Lock.ExitWriteLock(); - } - } - internal void TrimExcess() { Lock.EnterWriteLock(); @@ -154,4 +155,4 @@ namespace ArchiSteamFarm { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/CryptoHelper.cs b/ArchiSteamFarm/CryptoHelper.cs index ec32fa60b..5a2ca1fb9 100644 --- a/ArchiSteamFarm/CryptoHelper.cs +++ b/ArchiSteamFarm/CryptoHelper.cs @@ -28,21 +28,24 @@ using System.Text; namespace ArchiSteamFarm { internal static class CryptoHelper { - internal enum ECryptoMethod : byte { - PlainText, - AES, - ProtectedDataForCurrentUser - } - private static byte[] EncryptionKey = Encoding.UTF8.GetBytes("ArchiSteamFarm"); - internal static void SetEncryptionKey(string key) { - if (string.IsNullOrEmpty(key)) { - ASF.ArchiLogger.LogNullError(nameof(key)); - return; + internal static string Decrypt(ECryptoMethod cryptoMethod, string encrypted) { + if (string.IsNullOrEmpty(encrypted)) { + ASF.ArchiLogger.LogNullError(nameof(encrypted)); + return null; } - EncryptionKey = Encoding.UTF8.GetBytes(key); + switch (cryptoMethod) { + case ECryptoMethod.PlainText: + return encrypted; + case ECryptoMethod.AES: + return DecryptAES(encrypted); + case ECryptoMethod.ProtectedDataForCurrentUser: + return DecryptProtectedDataForCurrentUser(encrypted); + default: + return null; + } } internal static string Encrypt(ECryptoMethod cryptoMethod, string decrypted) { @@ -63,21 +66,50 @@ namespace ArchiSteamFarm { } } - internal static string Decrypt(ECryptoMethod cryptoMethod, string encrypted) { + internal static void SetEncryptionKey(string key) { + if (string.IsNullOrEmpty(key)) { + ASF.ArchiLogger.LogNullError(nameof(key)); + return; + } + + EncryptionKey = Encoding.UTF8.GetBytes(key); + } + + private static string DecryptAES(string encrypted) { if (string.IsNullOrEmpty(encrypted)) { ASF.ArchiLogger.LogNullError(nameof(encrypted)); return null; } - switch (cryptoMethod) { - case ECryptoMethod.PlainText: - return encrypted; - case ECryptoMethod.AES: - return DecryptAES(encrypted); - case ECryptoMethod.ProtectedDataForCurrentUser: - return DecryptProtectedDataForCurrentUser(encrypted); - default: - return null; + try { + byte[] key; + using (SHA256Cng sha256 = new SHA256Cng()) { + key = sha256.ComputeHash(EncryptionKey); + } + + byte[] decryptedData = Convert.FromBase64String(encrypted); + decryptedData = SteamKit2.CryptoHelper.SymmetricDecrypt(decryptedData, key); + return Encoding.UTF8.GetString(decryptedData); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + return null; + } + } + + private static string DecryptProtectedDataForCurrentUser(string encrypted) { + if (string.IsNullOrEmpty(encrypted)) { + ASF.ArchiLogger.LogNullError(nameof(encrypted)); + return null; + } + + try { + byte[] decryptedData = ProtectedData.Unprotect(Convert.FromBase64String(encrypted), EncryptionKey, // This is used as salt only and it's fine that it's known + DataProtectionScope.CurrentUser); + + return Encoding.UTF8.GetString(decryptedData); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + return null; } } @@ -102,27 +134,6 @@ namespace ArchiSteamFarm { } } - private static string DecryptAES(string encrypted) { - if (string.IsNullOrEmpty(encrypted)) { - ASF.ArchiLogger.LogNullError(nameof(encrypted)); - return null; - } - - try { - byte[] key; - using (SHA256Cng sha256 = new SHA256Cng()) { - key = sha256.ComputeHash(EncryptionKey); - } - - byte[] decryptedData = Convert.FromBase64String(encrypted); - decryptedData = SteamKit2.CryptoHelper.SymmetricDecrypt(decryptedData, key); - return Encoding.UTF8.GetString(decryptedData); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - return null; - } - } - private static string EncryptProtectedDataForCurrentUser(string decrypted) { if (string.IsNullOrEmpty(decrypted)) { ASF.ArchiLogger.LogNullError(nameof(decrypted)); @@ -130,11 +141,8 @@ namespace ArchiSteamFarm { } try { - byte[] encryptedData = ProtectedData.Protect( - Encoding.UTF8.GetBytes(decrypted), - EncryptionKey, // This is used as salt only and it's fine that it's known - DataProtectionScope.CurrentUser - ); + byte[] encryptedData = ProtectedData.Protect(Encoding.UTF8.GetBytes(decrypted), EncryptionKey, // This is used as salt only and it's fine that it's known + DataProtectionScope.CurrentUser); return Convert.ToBase64String(encryptedData); } catch (Exception e) { @@ -143,24 +151,10 @@ namespace ArchiSteamFarm { } } - private static string DecryptProtectedDataForCurrentUser(string encrypted) { - if (string.IsNullOrEmpty(encrypted)) { - ASF.ArchiLogger.LogNullError(nameof(encrypted)); - return null; - } - - try { - byte[] decryptedData = ProtectedData.Unprotect( - Convert.FromBase64String(encrypted), - EncryptionKey, // This is used as salt only and it's fine that it's known - DataProtectionScope.CurrentUser - ); - - return Encoding.UTF8.GetString(decryptedData); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - return null; - } + internal enum ECryptoMethod : byte { + PlainText, + AES, + ProtectedDataForCurrentUser } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Debugging.cs b/ArchiSteamFarm/Debugging.cs index af79f1d86..47b06f91d 100644 --- a/ArchiSteamFarm/Debugging.cs +++ b/ArchiSteamFarm/Debugging.cs @@ -22,8 +22,8 @@ */ -using SteamKit2; using System.Diagnostics.CodeAnalysis; +using SteamKit2; namespace ArchiSteamFarm { internal static class Debugging { @@ -46,4 +46,4 @@ namespace ArchiSteamFarm { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Events.cs b/ArchiSteamFarm/Events.cs index 0c4ce5359..085220e26 100644 --- a/ArchiSteamFarm/Events.cs +++ b/ArchiSteamFarm/Events.cs @@ -28,9 +28,6 @@ using SteamKit2; namespace ArchiSteamFarm { internal static class Events { - internal static void OnStateUpdated(Bot bot, SteamFriends.PersonaStateCallback callback) { - } - internal static async void OnBotShutdown() { if (Program.IsWCFRunning || Bot.Bots.Values.Any(bot => bot.KeepRunning)) { return; @@ -40,5 +37,7 @@ namespace ArchiSteamFarm { await Task.Delay(5000).ConfigureAwait(false); Program.Shutdown(); } + + internal static void OnStateUpdated(Bot bot, SteamFriends.PersonaStateCallback callback) { } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/GlobalConfig.cs b/ArchiSteamFarm/GlobalConfig.cs index 3c2ae80e1..b19d4c6bb 100644 --- a/ArchiSteamFarm/GlobalConfig.cs +++ b/ArchiSteamFarm/GlobalConfig.cs @@ -22,94 +22,90 @@ */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Sockets; +using Newtonsoft.Json; namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] [SuppressMessage("ReSharper", "ConvertToConstant.Global")] internal sealed class GlobalConfig { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - internal enum EUpdateChannel : byte { - None, - Stable, - Experimental - } - internal const byte DefaultHttpTimeout = 60; - private const byte DefaultMaxFarmingTime = 10; private const byte DefaultFarmingDelay = 15; - private const ushort DefaultWCFPort = 1242; + private const byte DefaultMaxFarmingTime = 10; private const ProtocolType DefaultSteamProtocol = ProtocolType.Tcp; + private const ushort DefaultWCFPort = 1242; // This is hardcoded blacklist which should not be possible to change internal static readonly HashSet GlobalBlacklist = new HashSet { 267420, 303700, 335590, 368020, 425280, 480730 }; [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool Debug = false; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool Headless = false; + internal readonly bool AutoRestart = true; [JsonProperty(Required = Required.DisallowNull)] internal readonly bool AutoUpdates = true; [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool AutoRestart = true; + internal readonly HashSet Blacklist = new HashSet(GlobalBlacklist); [JsonProperty(Required = Required.DisallowNull)] - internal readonly EUpdateChannel UpdateChannel = EUpdateChannel.Stable; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ProtocolType SteamProtocol = DefaultSteamProtocol; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly ulong SteamOwnerID = 0; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte MaxFarmingTime = DefaultMaxFarmingTime; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte IdleFarmingPeriod = 3; + internal readonly bool Debug = false; [JsonProperty(Required = Required.DisallowNull)] internal readonly byte FarmingDelay = DefaultFarmingDelay; [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte LoginLimiterDelay = 10; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte InventoryLimiterDelay = 3; + internal readonly bool ForceHttp = false; [JsonProperty(Required = Required.DisallowNull)] internal readonly byte GiftsLimiterDelay = 1; [JsonProperty(Required = Required.DisallowNull)] - internal readonly byte MaxTradeHoldDuration = 15; - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool ForceHttp = false; + internal readonly bool Headless = false; [JsonProperty(Required = Required.DisallowNull)] internal readonly byte HttpTimeout = DefaultHttpTimeout; - [JsonProperty] - internal string WCFHostname { get; set; } = "localhost"; + [JsonProperty(Required = Required.DisallowNull)] + internal readonly byte IdleFarmingPeriod = 3; [JsonProperty(Required = Required.DisallowNull)] - internal readonly ushort WCFPort = DefaultWCFPort; + internal readonly byte InventoryLimiterDelay = 3; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly byte LoginLimiterDelay = 10; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly byte MaxFarmingTime = DefaultMaxFarmingTime; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly byte MaxTradeHoldDuration = 15; [JsonProperty(Required = Required.DisallowNull)] internal readonly bool Statistics = true; [JsonProperty(Required = Required.DisallowNull)] - internal readonly HashSet Blacklist = new HashSet(GlobalBlacklist); + internal readonly ulong SteamOwnerID = 0; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly ProtocolType SteamProtocol = DefaultSteamProtocol; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly EUpdateChannel UpdateChannel = EUpdateChannel.Stable; + + [JsonProperty(Required = Required.DisallowNull)] + internal readonly ushort WCFPort = DefaultWCFPort; + + [JsonProperty] + internal string WCFHostname { get; set; } = "localhost"; + + // This constructor is used only by deserializer + private GlobalConfig() { } internal static GlobalConfig Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { @@ -171,7 +167,11 @@ namespace ArchiSteamFarm { return null; } - // This constructor is used only by deserializer - private GlobalConfig() { } + [SuppressMessage("ReSharper", "UnusedMember.Global")] + internal enum EUpdateChannel : byte { + None, + Stable, + Experimental + } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/GlobalDatabase.cs b/ArchiSteamFarm/GlobalDatabase.cs index 77d2b4790..80fe654fd 100644 --- a/ArchiSteamFarm/GlobalDatabase.cs +++ b/ArchiSteamFarm/GlobalDatabase.cs @@ -22,28 +22,23 @@ */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Threading; +using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class GlobalDatabase : IDisposable { - private static readonly JsonSerializerSettings CustomSerializerSettings = new JsonSerializerSettings { - Converters = new List(2) { - new IPAddressConverter(), - new IPEndPointConverter() - } - }; + private static readonly JsonSerializerSettings CustomSerializerSettings = new JsonSerializerSettings { Converters = new List(2) { new IPAddressConverter(), new IPEndPointConverter() } }; [JsonProperty(Required = Required.DisallowNull)] - private uint _CellID; + internal readonly InMemoryServerListProvider ServerListProvider = new InMemoryServerListProvider(); + + private readonly object FileLock = new object(); internal uint CellID { - get { - return _CellID; - } + get { return _CellID; } set { if ((value == 0) || (_CellID == value)) { return; @@ -55,12 +50,30 @@ namespace ArchiSteamFarm { } [JsonProperty(Required = Required.DisallowNull)] - internal readonly InMemoryServerListProvider ServerListProvider = new InMemoryServerListProvider(); - - private readonly object FileLock = new object(); + private uint _CellID; private string FilePath; + // This constructor is used when creating new database + private GlobalDatabase(string filePath) : this() { + if (string.IsNullOrEmpty(filePath)) { + throw new ArgumentNullException(nameof(filePath)); + } + + FilePath = filePath; + Save(); + } + + // This constructor is used only by deserializer + private GlobalDatabase() { + ServerListProvider.ServerListUpdated += OnServerListUpdated; + } + + public void Dispose() { + ServerListProvider.ServerListUpdated -= OnServerListUpdated; + ServerListProvider.Dispose(); + } + internal static GlobalDatabase Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { ASF.ArchiLogger.LogNullError(nameof(filePath)); @@ -89,26 +102,6 @@ namespace ArchiSteamFarm { return globalDatabase; } - public void Dispose() { - ServerListProvider.ServerListUpdated -= OnServerListUpdated; - ServerListProvider.Dispose(); - } - - // This constructor is used when creating new database - private GlobalDatabase(string filePath) : this() { - if (string.IsNullOrEmpty(filePath)) { - throw new ArgumentNullException(nameof(filePath)); - } - - FilePath = filePath; - Save(); - } - - // This constructor is used only by deserializer - private GlobalDatabase() { - ServerListProvider.ServerListUpdated += OnServerListUpdated; - } - private void OnServerListUpdated(object sender, EventArgs e) => Save(); private void Save() { @@ -132,4 +125,4 @@ namespace ArchiSteamFarm { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/IPAddressConverter.cs b/ArchiSteamFarm/IPAddressConverter.cs index 553ee0080..a35253c73 100644 --- a/ArchiSteamFarm/IPAddressConverter.cs +++ b/ArchiSteamFarm/IPAddressConverter.cs @@ -41,4 +41,4 @@ namespace ArchiSteamFarm { writer.WriteValue(ip.ToString()); } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/IPEndPointConverter.cs b/ArchiSteamFarm/IPEndPointConverter.cs index 041e57681..105a37947 100644 --- a/ArchiSteamFarm/IPEndPointConverter.cs +++ b/ArchiSteamFarm/IPEndPointConverter.cs @@ -48,4 +48,4 @@ namespace ArchiSteamFarm { writer.WriteEndObject(); } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/InMemoryServerListProvider.cs b/ArchiSteamFarm/InMemoryServerListProvider.cs index 6f9a101bd..ed911d624 100644 --- a/ArchiSteamFarm/InMemoryServerListProvider.cs +++ b/ArchiSteamFarm/InMemoryServerListProvider.cs @@ -34,8 +34,7 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] private readonly ConcurrentHashSet Servers = new ConcurrentHashSet(); - internal event EventHandler ServerListUpdated; - + public void Dispose() => Servers.Dispose(); public Task> FetchServerListAsync() => Task.FromResult>(Servers); public Task UpdateServerListAsync(IEnumerable endPoints) { @@ -54,6 +53,6 @@ namespace ArchiSteamFarm { return Task.Delay(0); } - public void Dispose() => Servers.Dispose(); + internal event EventHandler ServerListUpdated; } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/JSON/GitHub.cs b/ArchiSteamFarm/JSON/GitHub.cs index f18c63657..7838c110e 100644 --- a/ArchiSteamFarm/JSON/GitHub.cs +++ b/ArchiSteamFarm/JSON/GitHub.cs @@ -32,11 +32,11 @@ namespace ArchiSteamFarm.JSON { internal sealed class ReleaseResponse { #pragma warning disable 649 internal sealed class Asset { - [JsonProperty(PropertyName = "name", Required = Required.Always)] - internal readonly string Name; - [JsonProperty(PropertyName = "browser_download_url", Required = Required.Always)] internal readonly string DownloadURL; + + [JsonProperty(PropertyName = "name", Required = Required.Always)] + internal readonly string Name; } [JsonProperty(PropertyName = "tag_name", Required = Required.Always)] @@ -47,4 +47,4 @@ namespace ArchiSteamFarm.JSON { #pragma warning restore 649 } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/JSON/Steam.cs b/ArchiSteamFarm/JSON/Steam.cs index 45eb63ccb..b17a8c66e 100644 --- a/ArchiSteamFarm/JSON/Steam.cs +++ b/ArchiSteamFarm/JSON/Steam.cs @@ -33,137 +33,207 @@ using SteamKit2; namespace ArchiSteamFarm.JSON { internal static class Steam { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + internal sealed class ConfirmationDetails { +#pragma warning disable 649 + [JsonProperty(PropertyName = "success", Required = Required.Always)] + internal readonly bool Success; +#pragma warning restore 649 + internal ulong OtherSteamID64 { + get { + if (_OtherSteamID64 != 0) { + return _OtherSteamID64; + } + + if ((Type != EType.Trade) || (OtherSteamID3 == 0)) { + return 0; + } + + _OtherSteamID64 = new SteamID(OtherSteamID3, EUniverse.Public, EAccountType.Individual); + return _OtherSteamID64; + } + } + + internal ulong TradeOfferID { + get { + if (_TradeOfferID != 0) { + return _TradeOfferID; + } + + if ((Type != EType.Trade) || (HtmlDocument == null)) { + return 0; + } + + HtmlNode htmlNode = HtmlDocument.DocumentNode.SelectSingleNode("//div[@class='tradeoffer']"); + if (htmlNode == null) { + ASF.ArchiLogger.LogNullError(nameof(htmlNode)); + return 0; + } + + string id = htmlNode.GetAttributeValue("id", null); + if (string.IsNullOrEmpty(id)) { + ASF.ArchiLogger.LogNullError(nameof(id)); + return 0; + } + + int index = id.IndexOf('_'); + if (index < 0) { + ASF.ArchiLogger.LogNullError(nameof(index)); + return 0; + } + + index++; + if (id.Length <= index) { + ASF.ArchiLogger.LogNullError(nameof(id.Length)); + return 0; + } + + id = id.Substring(index); + if (ulong.TryParse(id, out _TradeOfferID) && (_TradeOfferID != 0)) { + return _TradeOfferID; + } + + ASF.ArchiLogger.LogNullError(nameof(_TradeOfferID)); + return 0; + } + } + +#pragma warning disable 649 + [JsonProperty(PropertyName = "html", Required = Required.DisallowNull)] + private readonly string HTML; +#pragma warning restore 649 + private HtmlDocument HtmlDocument { + get { + if (_HtmlDocument != null) { + return _HtmlDocument; + } + + if (string.IsNullOrEmpty(HTML)) { + return null; + } + + _HtmlDocument = new HtmlDocument(); + _HtmlDocument.LoadHtml(WebUtility.HtmlDecode(HTML)); + return _HtmlDocument; + } + } + + private uint OtherSteamID3 { + get { + if (_OtherSteamID3 != 0) { + return _OtherSteamID3; + } + + if ((Type != EType.Trade) || (HtmlDocument == null)) { + return 0; + } + + HtmlNode htmlNode = HtmlDocument.DocumentNode.SelectSingleNode("//a/@data-miniprofile"); + if (htmlNode == null) { + ASF.ArchiLogger.LogNullError(nameof(htmlNode)); + return 0; + } + + string miniProfile = htmlNode.GetAttributeValue("data-miniprofile", null); + if (string.IsNullOrEmpty(miniProfile)) { + ASF.ArchiLogger.LogNullError(nameof(miniProfile)); + return 0; + } + + if (uint.TryParse(miniProfile, out _OtherSteamID3) && (_OtherSteamID3 != 0)) { + return _OtherSteamID3; + } + + ASF.ArchiLogger.LogNullError(nameof(_OtherSteamID3)); + return 0; + } + } + + private EType Type { + get { + if (_Type != EType.Unknown) { + return _Type; + } + + if (HtmlDocument == null) { + return EType.Unknown; + } + + HtmlNode testNode = HtmlDocument.DocumentNode.SelectSingleNode("//div[@class='mobileconf_listing_prices']"); + if (testNode != null) { + _Type = EType.Market; + return _Type; + } + + testNode = HtmlDocument.DocumentNode.SelectSingleNode("//div[@class='mobileconf_trade_area']"); + if (testNode != null) { + _Type = EType.Trade; + return _Type; + } + + _Type = EType.Other; + return _Type; + } + } + + internal MobileAuthenticator.Confirmation Confirmation { + get { return _Confirmation; } + + set { + if (value == null) { + ASF.ArchiLogger.LogNullError(nameof(value)); + return; + } + + _Confirmation = value; + } + } + + private MobileAuthenticator.Confirmation _Confirmation; + private HtmlDocument _HtmlDocument; + private uint _OtherSteamID3; + private ulong _OtherSteamID64; + private ulong _TradeOfferID; + private EType _Type; + private ConfirmationDetails() { } // Deserialized from JSON + + internal enum EType : byte { + Unknown, + Trade, + Market, + Other + } + } + + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + internal sealed class ConfirmationResponse { // Deserialized from JSON +#pragma warning disable 649 + [JsonProperty(PropertyName = "success", Required = Required.Always)] + internal readonly bool Success; +#pragma warning restore 649 + + private ConfirmationResponse() { } + } + internal sealed class Item { // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_Asset | Deserialized from JSON (SteamCommunity) and constructed from code internal const ushort SteamAppID = 753; internal const byte SteamCommunityContextID = 6; - internal enum EType : byte { - Unknown, - - BoosterPack, - Coupon, - Gift, - SteamGems, - - Emoticon, - FoilTradingCard, - ProfileBackground, - TradingCard - } - + internal uint Amount { get; private set; } internal uint AppID { get; set; } - - [JsonProperty(PropertyName = "appid", Required = Required.DisallowNull)] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private string AppIDString { - get { - return AppID.ToString(); - } - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(nameof(value)); - return; - } - - uint appID; - if (!uint.TryParse(value, out appID) || (appID == 0)) { - ASF.ArchiLogger.LogNullError(nameof(appID)); - return; - } - - AppID = appID; - } - } - + internal ulong ClassID { get; private set; } internal ulong ContextID { get; set; } - - [JsonProperty(PropertyName = "contextid", Required = Required.DisallowNull)] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private string ContextIDString { - get { - return ContextID.ToString(); - } - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(nameof(value)); - return; - } - - ulong contextID; - if (!ulong.TryParse(value, out contextID) || (contextID == 0)) { - ASF.ArchiLogger.LogNullError(nameof(contextID)); - return; - } - - ContextID = contextID; - } - } + internal uint RealAppID { get; set; } + internal EType Type { get; set; } private ulong AssetID; - [JsonProperty(PropertyName = "assetid", Required = Required.DisallowNull)] - private string AssetIDString { - get { - return AssetID.ToString(); - } - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(nameof(value)); - return; - } - - ulong assetID; - if (!ulong.TryParse(value, out assetID) || (assetID == 0)) { - ASF.ArchiLogger.LogNullError(nameof(assetID)); - return; - } - - AssetID = assetID; - } - } - - [JsonProperty(PropertyName = "id", Required = Required.DisallowNull)] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private string ID { - get { return AssetIDString; } - set { AssetIDString = value; } - } - - internal ulong ClassID { get; private set; } - - [JsonProperty(PropertyName = "classid", Required = Required.DisallowNull)] - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private string ClassIDString { - get { - return ClassID.ToString(); - } - - set { - if (string.IsNullOrEmpty(value)) { - ASF.ArchiLogger.LogNullError(nameof(value)); - return; - } - - ulong classID; - if (!ulong.TryParse(value, out classID) || (classID == 0)) { - return; - } - - ClassID = classID; - } - } - - internal uint Amount { get; private set; } - [JsonProperty(PropertyName = "amount", Required = Required.Always)] [SuppressMessage("ReSharper", "UnusedMember.Local")] private string AmountString { - get { - return Amount.ToString(); - } + get { return Amount.ToString(); } set { if (string.IsNullOrEmpty(value)) { @@ -181,8 +251,94 @@ namespace ArchiSteamFarm.JSON { } } - internal uint RealAppID { get; set; } - internal EType Type { get; set; } + [JsonProperty(PropertyName = "appid", Required = Required.DisallowNull)] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private string AppIDString { + get { return AppID.ToString(); } + + set { + if (string.IsNullOrEmpty(value)) { + ASF.ArchiLogger.LogNullError(nameof(value)); + return; + } + + uint appID; + if (!uint.TryParse(value, out appID) || (appID == 0)) { + ASF.ArchiLogger.LogNullError(nameof(appID)); + return; + } + + AppID = appID; + } + } + + [JsonProperty(PropertyName = "assetid", Required = Required.DisallowNull)] + private string AssetIDString { + get { return AssetID.ToString(); } + + set { + if (string.IsNullOrEmpty(value)) { + ASF.ArchiLogger.LogNullError(nameof(value)); + return; + } + + ulong assetID; + if (!ulong.TryParse(value, out assetID) || (assetID == 0)) { + ASF.ArchiLogger.LogNullError(nameof(assetID)); + return; + } + + AssetID = assetID; + } + } + + [JsonProperty(PropertyName = "classid", Required = Required.DisallowNull)] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private string ClassIDString { + get { return ClassID.ToString(); } + + set { + if (string.IsNullOrEmpty(value)) { + ASF.ArchiLogger.LogNullError(nameof(value)); + return; + } + + ulong classID; + if (!ulong.TryParse(value, out classID) || (classID == 0)) { + return; + } + + ClassID = classID; + } + } + + [JsonProperty(PropertyName = "contextid", Required = Required.DisallowNull)] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private string ContextIDString { + get { return ContextID.ToString(); } + + set { + if (string.IsNullOrEmpty(value)) { + ASF.ArchiLogger.LogNullError(nameof(value)); + return; + } + + ulong contextID; + if (!ulong.TryParse(value, out contextID) || (contextID == 0)) { + ASF.ArchiLogger.LogNullError(nameof(contextID)); + return; + } + + ContextID = contextID; + } + } + + [JsonProperty(PropertyName = "id", Required = Required.DisallowNull)] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private string ID { + get { return AssetIDString; } + set { AssetIDString = value; } + } // This constructor is used for constructing items in trades being received internal Item(uint appID, ulong contextID, ulong classID, uint amount, uint realAppID, EType type) { @@ -200,33 +356,39 @@ namespace ArchiSteamFarm.JSON { [SuppressMessage("ReSharper", "UnusedMember.Local")] private Item() { } + + internal enum EType : byte { + Unknown, + + BoosterPack, + Coupon, + Gift, + SteamGems, + + Emoticon, + FoilTradingCard, + ProfileBackground, + TradingCard + } } - internal sealed class TradeOffer { // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer | Constructed from code - [SuppressMessage("ReSharper", "UnusedMember.Global")] - internal enum ETradeOfferState : byte { - Unknown, - Invalid, - Active, - Accepted, - Countered, - Expired, - Canceled, - Declined, - InvalidItems, - EmailPending, - EmailCanceled, - OnHold - } + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + internal sealed class RedeemWalletResponse { // Deserialized from JSON +#pragma warning disable 649 + [JsonProperty(PropertyName = "detail", Required = Required.Always)] + internal readonly ArchiHandler.PurchaseResponseCallback.EPurchaseResult PurchaseResult; +#pragma warning restore 649 - internal readonly ulong TradeOfferID; - internal readonly ETradeOfferState State; + private RedeemWalletResponse() { } + } + + internal sealed class TradeOffer { internal readonly HashSet ItemsToGive = new HashSet(); internal readonly HashSet ItemsToReceive = new HashSet(); + internal readonly ETradeOfferState State; + internal readonly ulong TradeOfferID; - private readonly uint OtherSteamID3; - - private ulong _OtherSteamID64; internal ulong OtherSteamID64 { get { if (_OtherSteamID64 != 0) { @@ -243,6 +405,10 @@ namespace ArchiSteamFarm.JSON { } } + private readonly uint OtherSteamID3; + + private ulong _OtherSteamID64; + internal TradeOffer(ulong tradeOfferID, uint otherSteamID3, ETradeOfferState state) { if ((tradeOfferID == 0) || (otherSteamID3 == 0) || (state == ETradeOfferState.Unknown)) { throw new ArgumentNullException(nameof(tradeOfferID) + " || " + nameof(otherSteamID3) + " || " + nameof(state)); @@ -253,8 +419,6 @@ namespace ArchiSteamFarm.JSON { State = state; } - internal bool IsSteamCardsRequest() => ItemsToGive.All(item => (item.AppID == Item.SteamAppID) && (item.ContextID == Item.SteamCommunityContextID) && (item.Type == Item.EType.TradingCard)); - internal bool IsFairTypesExchange() { Dictionary> itemsToGivePerGame = new Dictionary>(); foreach (Item item in ItemsToGive) { @@ -309,217 +473,38 @@ namespace ArchiSteamFarm.JSON { return true; } - } - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - internal sealed class RedeemWalletResponse { // Deserialized from JSON -#pragma warning disable 649 - [JsonProperty(PropertyName = "detail", Required = Required.Always)] - internal readonly ArchiHandler.PurchaseResponseCallback.EPurchaseResult PurchaseResult; -#pragma warning restore 649 + internal bool IsSteamCardsRequest() => ItemsToGive.All(item => (item.AppID == Item.SteamAppID) && (item.ContextID == Item.SteamCommunityContextID) && (item.Type == Item.EType.TradingCard)); // REF: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#CEcon_TradeOffer | Constructed from code - private RedeemWalletResponse() { } + [SuppressMessage("ReSharper", "UnusedMember.Global")] + internal enum ETradeOfferState : byte { + Unknown, + Invalid, + Active, + Accepted, + Countered, + Expired, + Canceled, + Declined, + InvalidItems, + EmailPending, + EmailCanceled, + OnHold + } } [SuppressMessage("ReSharper", "UnusedMember.Global")] - internal sealed class TradeOfferRequest { // Constructed from code - internal sealed class ItemList { - [JsonProperty(PropertyName = "assets", Required = Required.Always)] - internal readonly HashSet Assets = new HashSet(); - } - + internal sealed class TradeOfferRequest { [JsonProperty(PropertyName = "me", Required = Required.Always)] internal readonly ItemList ItemsToGive = new ItemList(); [JsonProperty(PropertyName = "them", Required = Required.Always)] - internal readonly ItemList ItemsToReceive = new ItemList(); - } + internal readonly ItemList ItemsToReceive = new ItemList(); // Constructed from code - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - internal sealed class ConfirmationResponse { // Deserialized from JSON -#pragma warning disable 649 - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success; -#pragma warning restore 649 - - private ConfirmationResponse() { } - } - - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - internal sealed class ConfirmationDetails { // Deserialized from JSON - internal enum EType : byte { - Unknown, - Trade, - Market, - Other + internal sealed class ItemList { + [JsonProperty(PropertyName = "assets", Required = Required.Always)] + internal readonly HashSet Assets = new HashSet(); } - - private MobileAuthenticator.Confirmation _Confirmation; - internal MobileAuthenticator.Confirmation Confirmation { - get { return _Confirmation; } - - set { - if (value == null) { - ASF.ArchiLogger.LogNullError(nameof(value)); - return; - } - - _Confirmation = value; - } - } - -#pragma warning disable 649 - [JsonProperty(PropertyName = "success", Required = Required.Always)] - internal readonly bool Success; -#pragma warning restore 649 - - private EType _Type; - private EType Type { - get { - if (_Type != EType.Unknown) { - return _Type; - } - - if (HtmlDocument == null) { - return EType.Unknown; - } - - HtmlNode testNode = HtmlDocument.DocumentNode.SelectSingleNode("//div[@class='mobileconf_listing_prices']"); - if (testNode != null) { - _Type = EType.Market; - return _Type; - } - - testNode = HtmlDocument.DocumentNode.SelectSingleNode("//div[@class='mobileconf_trade_area']"); - if (testNode != null) { - _Type = EType.Trade; - return _Type; - } - - _Type = EType.Other; - return _Type; - } - } - - private ulong _TradeOfferID; - internal ulong TradeOfferID { - get { - if (_TradeOfferID != 0) { - return _TradeOfferID; - } - - if ((Type != EType.Trade) || (HtmlDocument == null)) { - return 0; - } - - HtmlNode htmlNode = HtmlDocument.DocumentNode.SelectSingleNode("//div[@class='tradeoffer']"); - if (htmlNode == null) { - ASF.ArchiLogger.LogNullError(nameof(htmlNode)); - return 0; - } - - string id = htmlNode.GetAttributeValue("id", null); - if (string.IsNullOrEmpty(id)) { - ASF.ArchiLogger.LogNullError(nameof(id)); - return 0; - } - - int index = id.IndexOf('_'); - if (index < 0) { - ASF.ArchiLogger.LogNullError(nameof(index)); - return 0; - } - - index++; - if (id.Length <= index) { - ASF.ArchiLogger.LogNullError(nameof(id.Length)); - return 0; - } - - id = id.Substring(index); - if (ulong.TryParse(id, out _TradeOfferID) && (_TradeOfferID != 0)) { - return _TradeOfferID; - } - - ASF.ArchiLogger.LogNullError(nameof(_TradeOfferID)); - return 0; - } - } - - private ulong _OtherSteamID64; - internal ulong OtherSteamID64 { - get { - if (_OtherSteamID64 != 0) { - return _OtherSteamID64; - } - - if ((Type != EType.Trade) || (OtherSteamID3 == 0)) { - return 0; - } - - _OtherSteamID64 = new SteamID(OtherSteamID3, EUniverse.Public, EAccountType.Individual); - return _OtherSteamID64; - } - } - -#pragma warning disable 649 - [JsonProperty(PropertyName = "html", Required = Required.DisallowNull)] - private readonly string HTML; -#pragma warning restore 649 - - private uint _OtherSteamID3; - private uint OtherSteamID3 { - get { - if (_OtherSteamID3 != 0) { - return _OtherSteamID3; - } - - if ((Type != EType.Trade) || (HtmlDocument == null)) { - return 0; - } - - HtmlNode htmlNode = HtmlDocument.DocumentNode.SelectSingleNode("//a/@data-miniprofile"); - if (htmlNode == null) { - ASF.ArchiLogger.LogNullError(nameof(htmlNode)); - return 0; - } - - string miniProfile = htmlNode.GetAttributeValue("data-miniprofile", null); - if (string.IsNullOrEmpty(miniProfile)) { - ASF.ArchiLogger.LogNullError(nameof(miniProfile)); - return 0; - } - - if (uint.TryParse(miniProfile, out _OtherSteamID3) && (_OtherSteamID3 != 0)) { - return _OtherSteamID3; - } - - ASF.ArchiLogger.LogNullError(nameof(_OtherSteamID3)); - return 0; - } - } - - private HtmlDocument _HtmlDocument; - private HtmlDocument HtmlDocument { - get { - if (_HtmlDocument != null) { - return _HtmlDocument; - } - - if (string.IsNullOrEmpty(HTML)) { - return null; - } - - _HtmlDocument = new HtmlDocument(); - _HtmlDocument.LoadHtml(WebUtility.HtmlDecode(HTML)); - return _HtmlDocument; - } - } - - private ConfirmationDetails() { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Logging.cs b/ArchiSteamFarm/Logging.cs index f76f3390c..4edf0742a 100644 --- a/ArchiSteamFarm/Logging.cs +++ b/ArchiSteamFarm/Logging.cs @@ -29,9 +29,9 @@ using NLog.Targets; namespace ArchiSteamFarm { internal static class Logging { - private const string LayoutMessage = @"${logger}|${message}${onexception:inner= ${exception:format=toString,Data}}"; - private const string GeneralLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss}|${processname}-${processid}|${level:uppercase=true}|" + LayoutMessage; private const string EventLogLayout = LayoutMessage; + private const string GeneralLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss}|${processname}-${processid}|${level:uppercase=true}|" + LayoutMessage; + private const string LayoutMessage = @"${logger}|${message}${onexception:inner= ${exception:format=toString,Data}}"; private static readonly ConcurrentHashSet ConsoleLoggingRules = new ConcurrentHashSet(); @@ -47,29 +47,18 @@ namespace ArchiSteamFarm { LoggingConfiguration config = new LoggingConfiguration(); - ColoredConsoleTarget coloredConsoleTarget = new ColoredConsoleTarget("ColoredConsole") { - DetectConsoleAvailable = false, - Layout = GeneralLayout - }; + ColoredConsoleTarget coloredConsoleTarget = new ColoredConsoleTarget("ColoredConsole") { DetectConsoleAvailable = false, Layout = GeneralLayout }; config.AddTarget(coloredConsoleTarget); config.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, coloredConsoleTarget)); if (Program.IsRunningAsService) { - EventLogTarget eventLogTarget = new EventLogTarget("EventLog") { - Layout = EventLogLayout, - Log = SharedInfo.EventLog, - Source = SharedInfo.EventLogSource - }; + EventLogTarget eventLogTarget = new EventLogTarget("EventLog") { Layout = EventLogLayout, Log = SharedInfo.EventLog, Source = SharedInfo.EventLogSource }; config.AddTarget(eventLogTarget); config.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, eventLogTarget)); } else if (!Program.Mode.HasFlag(Program.EMode.Client) || Program.Mode.HasFlag(Program.EMode.Server)) { - FileTarget fileTarget = new FileTarget("File") { - DeleteOldFileOnStartup = true, - FileName = SharedInfo.LogFile, - Layout = GeneralLayout - }; + FileTarget fileTarget = new FileTarget("File") { DeleteOldFileOnStartup = true, FileName = SharedInfo.LogFile, Layout = GeneralLayout }; config.AddTarget(fileTarget); config.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, fileTarget)); @@ -79,20 +68,6 @@ namespace ArchiSteamFarm { InitConsoleLoggers(); } - internal static void OnUserInputStart() { - IsWaitingForUserInput = true; - - if (ConsoleLoggingRules.Count == 0) { - return; - } - - foreach (LoggingRule consoleLoggingRule in ConsoleLoggingRules) { - LogManager.Configuration.LoggingRules.Remove(consoleLoggingRule); - } - - LogManager.ReconfigExistingLoggers(); - } - internal static void OnUserInputEnd() { IsWaitingForUserInput = false; @@ -107,6 +82,20 @@ namespace ArchiSteamFarm { LogManager.ReconfigExistingLoggers(); } + internal static void OnUserInputStart() { + IsWaitingForUserInput = true; + + if (ConsoleLoggingRules.Count == 0) { + return; + } + + foreach (LoggingRule consoleLoggingRule in ConsoleLoggingRules) { + LogManager.Configuration.LoggingRules.Remove(consoleLoggingRule); + } + + LogManager.ReconfigExistingLoggers(); + } + private static void InitConsoleLoggers() { ConsoleLoggingRules.Clear(); foreach (LoggingRule loggingRule in LogManager.Configuration.LoggingRules.Where(loggingRule => loggingRule.Targets.Any(target => target is ColoredConsoleTarget || target is ConsoleTarget))) { @@ -127,4 +116,4 @@ namespace ArchiSteamFarm { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/MobileAuthenticator.cs b/ArchiSteamFarm/MobileAuthenticator.cs index 5646d7007..6d41b609e 100644 --- a/ArchiSteamFarm/MobileAuthenticator.cs +++ b/ArchiSteamFarm/MobileAuthenticator.cs @@ -37,61 +37,26 @@ namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] internal sealed class MobileAuthenticator : IDisposable { - internal sealed class Confirmation { - internal readonly uint ID; - internal readonly ulong Key; - internal readonly Steam.ConfirmationDetails.EType Type; - - internal Confirmation(uint id, ulong key, Steam.ConfirmationDetails.EType type) { - if ((id == 0) || (key == 0) || (type == Steam.ConfirmationDetails.EType.Unknown)) { - throw new ArgumentNullException(nameof(id) + " || " + nameof(key) + " || " + nameof(type)); - } - - ID = id; - Key = key; - Type = type; - } - } - private const byte CodeDigits = 5; private const byte CodeInterval = 30; - private static readonly char[] CodeCharacters = { - '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', - 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', - 'R', 'T', 'V', 'W', 'X', 'Y' - }; - + private static readonly char[] CodeCharacters = { '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y' }; private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1); private static short? SteamTimeDifference; + internal bool HasCorrectDeviceID => !string.IsNullOrEmpty(DeviceID) && !DeviceID.Equals("ERROR"); // "ERROR" is being used by SteamDesktopAuthenticator + private readonly SemaphoreSlim ConfirmationsSemaphore = new SemaphoreSlim(1); -#pragma warning disable 649 - [JsonProperty(PropertyName = "shared_secret", Required = Required.Always)] - private readonly string SharedSecret; - - [JsonProperty(PropertyName = "identity_secret", Required = Required.Always)] - private readonly string IdentitySecret; -#pragma warning restore 649 + private Bot Bot; [JsonProperty(PropertyName = "device_id")] private string DeviceID; - private Bot Bot; - - internal bool HasCorrectDeviceID => !string.IsNullOrEmpty(DeviceID) && !DeviceID.Equals("ERROR"); // "ERROR" is being used by SteamDesktopAuthenticator - private MobileAuthenticator() { } - internal void Init(Bot bot) { - if (bot == null) { - throw new ArgumentNullException(nameof(bot)); - } - - Bot = bot; - } + public void Dispose() => ConfirmationsSemaphore.Dispose(); internal void CorrectDeviceID(string deviceID) { if (string.IsNullOrEmpty(deviceID)) { @@ -102,56 +67,14 @@ namespace ArchiSteamFarm { DeviceID = deviceID; } - internal async Task HandleConfirmations(HashSet confirmations, bool accept) { - if ((confirmations == null) || (confirmations.Count == 0)) { - Bot.ArchiLogger.LogNullError(nameof(confirmations)); - return false; + internal async Task GenerateToken() { + uint time = await GetSteamTime().ConfigureAwait(false); + if (time != 0) { + return GenerateTokenForTime(time); } - if (!HasCorrectDeviceID) { - Bot.ArchiLogger.LogGenericWarning("Can't execute properly due to invalid DeviceID!"); - return false; - } - - await ConfirmationsSemaphore.WaitAsync().ConfigureAwait(false); - - try { - uint time = await GetSteamTime().ConfigureAwait(false); - if (time == 0) { - Bot.ArchiLogger.LogNullError(nameof(time)); - return false; - } - - string confirmationHash = GenerateConfirmationKey(time, "conf"); - if (string.IsNullOrEmpty(confirmationHash)) { - Bot.ArchiLogger.LogNullError(nameof(confirmationHash)); - return false; - } - - bool? result = await Bot.ArchiWebHandler.HandleConfirmations(DeviceID, confirmationHash, time, confirmations, accept).ConfigureAwait(false); - if (!result.HasValue) { // Request timed out - return false; - } - - if (result.Value) { // Request succeeded - return true; - } - - // Our multi request failed, this is almost always Steam fuckup that happens randomly - // In this case, we'll accept all pending confirmations one-by-one, synchronously (as Steam can't handle them in parallel) - // We totally ignore actual result returned by those calls, abort only if request timed out - - foreach (Confirmation confirmation in confirmations) { - bool? confirmationResult = await Bot.ArchiWebHandler.HandleConfirmation(DeviceID, confirmationHash, time, confirmation.ID, confirmation.Key, accept).ConfigureAwait(false); - if (!confirmationResult.HasValue) { - return false; - } - } - - return true; - } finally { - ConfirmationsSemaphore.Release(); - } + Bot.ArchiLogger.LogNullError(nameof(time)); + return null; } internal async Task GetConfirmationDetails(Confirmation confirmation) { @@ -185,16 +108,6 @@ namespace ArchiSteamFarm { return response; } - internal async Task GenerateToken() { - uint time = await GetSteamTime().ConfigureAwait(false); - if (time != 0) { - return GenerateTokenForTime(time); - } - - Bot.ArchiLogger.LogNullError(nameof(time)); - return null; - } - internal async Task> GetConfirmations() { if (!HasCorrectDeviceID) { Bot.ArchiLogger.LogGenericWarning("Can't execute properly due to invalid DeviceID!"); @@ -271,68 +184,64 @@ namespace ArchiSteamFarm { return result; } - private async Task GetSteamTime() { - if (SteamTimeDifference.HasValue) { - return (uint) (Utilities.GetUnixTime() + SteamTimeDifference.GetValueOrDefault()); + internal async Task HandleConfirmations(HashSet confirmations, bool accept) { + if ((confirmations == null) || (confirmations.Count == 0)) { + Bot.ArchiLogger.LogNullError(nameof(confirmations)); + return false; } - await TimeSemaphore.WaitAsync().ConfigureAwait(false); + if (!HasCorrectDeviceID) { + Bot.ArchiLogger.LogGenericWarning("Can't execute properly due to invalid DeviceID!"); + return false; + } + + await ConfirmationsSemaphore.WaitAsync().ConfigureAwait(false); try { - if (!SteamTimeDifference.HasValue) { - uint serverTime = Bot.ArchiWebHandler.GetServerTime(); - if (serverTime != 0) { - SteamTimeDifference = (short) (serverTime - Utilities.GetUnixTime()); + uint time = await GetSteamTime().ConfigureAwait(false); + if (time == 0) { + Bot.ArchiLogger.LogNullError(nameof(time)); + return false; + } + + string confirmationHash = GenerateConfirmationKey(time, "conf"); + if (string.IsNullOrEmpty(confirmationHash)) { + Bot.ArchiLogger.LogNullError(nameof(confirmationHash)); + return false; + } + + bool? result = await Bot.ArchiWebHandler.HandleConfirmations(DeviceID, confirmationHash, time, confirmations, accept).ConfigureAwait(false); + if (!result.HasValue) { // Request timed out + return false; + } + + if (result.Value) { // Request succeeded + return true; + } + + // Our multi request failed, this is almost always Steam fuckup that happens randomly + // In this case, we'll accept all pending confirmations one-by-one, synchronously (as Steam can't handle them in parallel) + // We totally ignore actual result returned by those calls, abort only if request timed out + + foreach (Confirmation confirmation in confirmations) { + bool? confirmationResult = await Bot.ArchiWebHandler.HandleConfirmation(DeviceID, confirmationHash, time, confirmation.ID, confirmation.Key, accept).ConfigureAwait(false); + if (!confirmationResult.HasValue) { + return false; } } - } finally { - TimeSemaphore.Release(); - } - return (uint) (Utilities.GetUnixTime() + SteamTimeDifference.GetValueOrDefault()); + return true; + } finally { + ConfirmationsSemaphore.Release(); + } } - private string GenerateTokenForTime(uint time) { - if (time == 0) { - Bot.ArchiLogger.LogNullError(nameof(time)); - return null; + internal void Init(Bot bot) { + if (bot == null) { + throw new ArgumentNullException(nameof(bot)); } - byte[] sharedSecret = Convert.FromBase64String(SharedSecret); - - byte[] timeArray = BitConverter.GetBytes((long) time / CodeInterval); - if (BitConverter.IsLittleEndian) { - Array.Reverse(timeArray); - } - - byte[] hash; - using (HMACSHA1 hmac = new HMACSHA1(sharedSecret)) { - hash = hmac.ComputeHash(timeArray); - } - - // The last 4 bits of the mac say where the code starts - int start = hash[hash.Length - 1] & 0x0f; - - // Extract those 4 bytes - byte[] bytes = new byte[4]; - - Array.Copy(hash, start, bytes, 0, 4); - - if (BitConverter.IsLittleEndian) { - Array.Reverse(bytes); - } - - uint fullCode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; - - // Build the alphanumeric code - StringBuilder code = new StringBuilder(); - - for (byte i = 0; i < CodeDigits; i++) { - code.Append(CodeCharacters[fullCode % CodeCharacters.Length]); - fullCode /= (uint) CodeCharacters.Length; - } - - return code.ToString(); + Bot = bot; } private string GenerateConfirmationKey(uint time, string tag = null) { @@ -368,6 +277,92 @@ namespace ArchiSteamFarm { return Convert.ToBase64String(hash); } - public void Dispose() => ConfirmationsSemaphore.Dispose(); + private string GenerateTokenForTime(uint time) { + if (time == 0) { + Bot.ArchiLogger.LogNullError(nameof(time)); + return null; + } + + byte[] sharedSecret = Convert.FromBase64String(SharedSecret); + + byte[] timeArray = BitConverter.GetBytes((long) time/CodeInterval); + if (BitConverter.IsLittleEndian) { + Array.Reverse(timeArray); + } + + byte[] hash; + using (HMACSHA1 hmac = new HMACSHA1(sharedSecret)) { + hash = hmac.ComputeHash(timeArray); + } + + // The last 4 bits of the mac say where the code starts + int start = hash[hash.Length - 1] & 0x0f; + + // Extract those 4 bytes + byte[] bytes = new byte[4]; + + Array.Copy(hash, start, bytes, 0, 4); + + if (BitConverter.IsLittleEndian) { + Array.Reverse(bytes); + } + + uint fullCode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; + + // Build the alphanumeric code + StringBuilder code = new StringBuilder(); + + for (byte i = 0; i < CodeDigits; i++) { + code.Append(CodeCharacters[fullCode%CodeCharacters.Length]); + fullCode /= (uint) CodeCharacters.Length; + } + + return code.ToString(); + } + + private async Task GetSteamTime() { + if (SteamTimeDifference.HasValue) { + return (uint) (Utilities.GetUnixTime() + SteamTimeDifference.GetValueOrDefault()); + } + + await TimeSemaphore.WaitAsync().ConfigureAwait(false); + + try { + if (!SteamTimeDifference.HasValue) { + uint serverTime = Bot.ArchiWebHandler.GetServerTime(); + if (serverTime != 0) { + SteamTimeDifference = (short) (serverTime - Utilities.GetUnixTime()); + } + } + } finally { + TimeSemaphore.Release(); + } + + return (uint) (Utilities.GetUnixTime() + SteamTimeDifference.GetValueOrDefault()); + } + + internal sealed class Confirmation { + internal readonly uint ID; + internal readonly ulong Key; + internal readonly Steam.ConfirmationDetails.EType Type; + + internal Confirmation(uint id, ulong key, Steam.ConfirmationDetails.EType type) { + if ((id == 0) || (key == 0) || (type == Steam.ConfirmationDetails.EType.Unknown)) { + throw new ArgumentNullException(nameof(id) + " || " + nameof(key) + " || " + nameof(type)); + } + + ID = id; + Key = key; + Type = type; + } + } + +#pragma warning disable 649 + [JsonProperty(PropertyName = "shared_secret", Required = Required.Always)] + private readonly string SharedSecret; + + [JsonProperty(PropertyName = "identity_secret", Required = Required.Always)] + private readonly string IdentitySecret; +#pragma warning restore 649 } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index 7d8124eca..a35d6e773 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -31,50 +31,28 @@ using System.Reflection; using System.ServiceProcess; using System.Threading; using System.Threading.Tasks; +using SteamKit2; namespace ArchiSteamFarm { internal static class Program { - [Flags] - internal enum EMode : byte { - Normal = 0, // Standard most common usage - Client = 1, // WCF client - Server = 2 // WCF server - } + internal static bool IsWCFRunning => WCF.IsServerRunning; + internal static GlobalConfig GlobalConfig { get; private set; } + internal static GlobalDatabase GlobalDatabase { get; private set; } + internal static bool IsRunningAsService { get; private set; } + internal static EMode Mode { get; private set; } = EMode.Normal; + internal static WebBrowser WebBrowser { get; private set; } private static readonly object ConsoleLock = new object(); private static readonly ManualResetEventSlim ShutdownResetEvent = new ManualResetEventSlim(false); private static readonly WCF WCF = new WCF(); - internal static bool IsRunningAsService { get; private set; } - internal static EMode Mode { get; private set; } = EMode.Normal; - internal static GlobalConfig GlobalConfig { get; private set; } - internal static GlobalDatabase GlobalDatabase { get; private set; } - internal static WebBrowser WebBrowser { get; private set; } - private static bool ShutdownSequenceInitialized; - internal static bool IsWCFRunning => WCF.IsServerRunning; - internal static void Exit(byte exitCode = 0) { Shutdown(); Environment.Exit(exitCode); } - internal static void Restart() { - if (!InitShutdownSequence()) { - return; - } - - try { - Process.Start(Assembly.GetEntryAssembly().Location, string.Join(" ", Environment.GetCommandLineArgs().Skip(1))); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } - - ShutdownResetEvent.Set(); - Environment.Exit(0); - } - internal static string GetUserInput(ASF.EUserInputType userInputType, string botName = SharedInfo.ASF, string extraInformation = null) { if (userInputType == ASF.EUserInputType.Unknown) { return null; @@ -137,6 +115,21 @@ namespace ArchiSteamFarm { return !string.IsNullOrEmpty(result) ? result.Trim() : null; } + internal static void Restart() { + if (!InitShutdownSequence()) { + return; + } + + try { + Process.Start(Assembly.GetEntryAssembly().Location, string.Join(" ", Environment.GetCommandLineArgs().Skip(1))); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + + ShutdownResetEvent.Set(); + Environment.Exit(0); + } + internal static void Shutdown() { if (!InitShutdownSequence()) { return; @@ -145,19 +138,72 @@ namespace ArchiSteamFarm { ShutdownResetEvent.Set(); } - private static bool InitShutdownSequence() { - if (ShutdownSequenceInitialized) { - return false; + private static void Init(string[] args) { + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler; + + string homeDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + if (!string.IsNullOrEmpty(homeDirectory)) { + Directory.SetCurrentDirectory(homeDirectory); + + // Allow loading configs from source tree if it's a debug build + if (Debugging.IsDebugBuild) { + // Common structure is bin/(x64/)Debug/ArchiSteamFarm.exe, so we allow up to 4 directories up + for (byte i = 0; i < 4; i++) { + Directory.SetCurrentDirectory(".."); + if (Directory.Exists(SharedInfo.ConfigDirectory)) { + break; + } + } + + // If config directory doesn't exist after our adjustment, abort all of that + if (!Directory.Exists(SharedInfo.ConfigDirectory)) { + Directory.SetCurrentDirectory(homeDirectory); + } + } } - ShutdownSequenceInitialized = true; - - WCF.StopServer(); - foreach (Bot bot in Bot.Bots.Values) { - bot.Stop(); + // Parse pre-init args + if (args != null) { + ParsePreInitArgs(args); } - return true; + Logging.InitLoggers(); + ASF.ArchiLogger.LogGenericInfo("ASF V" + SharedInfo.Version); + + if (!Runtime.IsRuntimeSupported) { + ASF.ArchiLogger.LogGenericError("ASF detected unsupported runtime version, program might NOT run correctly in current environment. You're running it at your own risk!"); + Thread.Sleep(10000); + } + + InitServices(); + + // If debugging is on, we prepare debug directory prior to running + if (GlobalConfig.Debug) { + if (Directory.Exists(SharedInfo.DebugDirectory)) { + Directory.Delete(SharedInfo.DebugDirectory, true); + Thread.Sleep(1000); // Dirty workaround giving Windows some time to sync + } + + Directory.CreateDirectory(SharedInfo.DebugDirectory); + + DebugLog.AddListener(new Debugging.DebugListener()); + DebugLog.Enabled = true; + } + + // Parse post-init args + if (args != null) { + ParsePostInitArgs(args); + } + + // If we ran ASF as a client, we're done by now + if (Mode.HasFlag(EMode.Client) && !Mode.HasFlag(EMode.Server)) { + Exit(); + } + + ASF.CheckForUpdate().Wait(); + ASF.InitBots(); + ASF.InitFileWatcher(); } private static void InitServices() { @@ -186,30 +232,36 @@ namespace ArchiSteamFarm { WebBrowser = new WebBrowser(ASF.ArchiLogger); } - private static void ParsePreInitArgs(IEnumerable args) { - if (args == null) { - ASF.ArchiLogger.LogNullError(nameof(args)); - return; + private static bool InitShutdownSequence() { + if (ShutdownSequenceInitialized) { + return false; } - foreach (string arg in args) { - switch (arg) { - case "": - break; - case "--client": - Mode |= EMode.Client; - break; - case "--server": - Mode |= EMode.Server; - break; - default: - if (arg.StartsWith("--", StringComparison.Ordinal)) { - if (arg.StartsWith("--path=", StringComparison.Ordinal) && (arg.Length > 7)) { - Directory.SetCurrentDirectory(arg.Substring(7)); - } - } + ShutdownSequenceInitialized = true; - break; + WCF.StopServer(); + foreach (Bot bot in Bot.Bots.Values) { + bot.Stop(); + } + + return true; + } + + private static void Main(string[] args) { + if (Runtime.IsUserInteractive) { + // App + Init(args); + + // Wait for signal to shutdown + ShutdownResetEvent.Wait(); + + // We got a signal to shutdown + Exit(); + } else { + // Service + IsRunningAsService = true; + using (Service service = new Service()) { + ServiceBase.Run(service); } } } @@ -253,6 +305,34 @@ namespace ArchiSteamFarm { } } + private static void ParsePreInitArgs(IEnumerable args) { + if (args == null) { + ASF.ArchiLogger.LogNullError(nameof(args)); + return; + } + + foreach (string arg in args) { + switch (arg) { + case "": + break; + case "--client": + Mode |= EMode.Client; + break; + case "--server": + Mode |= EMode.Server; + break; + default: + if (arg.StartsWith("--", StringComparison.Ordinal)) { + if (arg.StartsWith("--path=", StringComparison.Ordinal) && (arg.Length > 7)) { + Directory.SetCurrentDirectory(arg.Substring(7)); + } + } + + break; + } + } + } + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) { if (args?.ExceptionObject == null) { ASF.ArchiLogger.LogNullError(nameof(args) + " || " + nameof(args.ExceptionObject)); @@ -271,92 +351,11 @@ namespace ArchiSteamFarm { ASF.ArchiLogger.LogFatalException(args.Exception); } - private static void Init(string[] args) { - AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; - TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler; - - string homeDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - if (!string.IsNullOrEmpty(homeDirectory)) { - Directory.SetCurrentDirectory(homeDirectory); - - // Allow loading configs from source tree if it's a debug build - if (Debugging.IsDebugBuild) { - - // Common structure is bin/(x64/)Debug/ArchiSteamFarm.exe, so we allow up to 4 directories up - for (byte i = 0; i < 4; i++) { - Directory.SetCurrentDirectory(".."); - if (Directory.Exists(SharedInfo.ConfigDirectory)) { - break; - } - } - - // If config directory doesn't exist after our adjustment, abort all of that - if (!Directory.Exists(SharedInfo.ConfigDirectory)) { - Directory.SetCurrentDirectory(homeDirectory); - } - } - } - - // Parse pre-init args - if (args != null) { - ParsePreInitArgs(args); - } - - Logging.InitLoggers(); - ASF.ArchiLogger.LogGenericInfo("ASF V" + SharedInfo.Version); - - if (!Runtime.IsRuntimeSupported) { - ASF.ArchiLogger.LogGenericError("ASF detected unsupported runtime version, program might NOT run correctly in current environment. You're running it at your own risk!"); - Thread.Sleep(10000); - } - - InitServices(); - - // If debugging is on, we prepare debug directory prior to running - if (GlobalConfig.Debug) { - if (Directory.Exists(SharedInfo.DebugDirectory)) { - Directory.Delete(SharedInfo.DebugDirectory, true); - Thread.Sleep(1000); // Dirty workaround giving Windows some time to sync - } - - Directory.CreateDirectory(SharedInfo.DebugDirectory); - - SteamKit2.DebugLog.AddListener(new Debugging.DebugListener()); - SteamKit2.DebugLog.Enabled = true; - } - - // Parse post-init args - if (args != null) { - ParsePostInitArgs(args); - } - - // If we ran ASF as a client, we're done by now - if (Mode.HasFlag(EMode.Client) && !Mode.HasFlag(EMode.Server)) { - Exit(); - } - - ASF.CheckForUpdate().Wait(); - ASF.InitBots(); - ASF.InitFileWatcher(); - } - - private static void Main(string[] args) { - if (Runtime.IsUserInteractive) { - // App - Init(args); - - // Wait for signal to shutdown - ShutdownResetEvent.Wait(); - - // We got a signal to shutdown - Exit(); - } else { - // Service - IsRunningAsService = true; - using (Service service = new Service()) { - ServiceBase.Run(service); - } - } + [Flags] + internal enum EMode : byte { + Normal = 0, // Standard most common usage + Client = 1, // WCF client + Server = 2 // WCF server } private sealed class Service : ServiceBase { @@ -373,5 +372,4 @@ namespace ArchiSteamFarm { protected override void OnStop() => Shutdown(); } } - -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Properties/AssemblyInfo.cs b/ArchiSteamFarm/Properties/AssemblyInfo.cs index 5e5adcb67..33445158b 100644 --- a/ArchiSteamFarm/Properties/AssemblyInfo.cs +++ b/ArchiSteamFarm/Properties/AssemblyInfo.cs @@ -5,6 +5,7 @@ using ArchiSteamFarm; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. + [assembly: AssemblyTitle(SharedInfo.ServiceName)] [assembly: AssemblyDescription(SharedInfo.ServiceDescription)] [assembly: AssemblyConfiguration("")] @@ -17,9 +18,11 @@ using ArchiSteamFarm; // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. + [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM + [assembly: Guid("35af7887-08b9-40e8-a5ea-797d8b60b30c")] // Version information for an assembly consists of the following four values: @@ -32,5 +35,6 @@ using ArchiSteamFarm; // 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(SharedInfo.VersionNumber)] -[assembly: AssemblyFileVersion(SharedInfo.VersionNumber)] +[assembly: AssemblyFileVersion(SharedInfo.VersionNumber)] \ No newline at end of file diff --git a/ArchiSteamFarm/Runtime.cs b/ArchiSteamFarm/Runtime.cs index 283e66197..babf1579c 100644 --- a/ArchiSteamFarm/Runtime.cs +++ b/ArchiSteamFarm/Runtime.cs @@ -28,34 +28,8 @@ using Microsoft.Win32; namespace ArchiSteamFarm { internal static class Runtime { - private static readonly Type MonoRuntime = Type.GetType("Mono.Runtime"); - internal static bool IsRunningOnMono => MonoRuntime != null; - private static bool? _IsUserInteractive; - internal static bool IsUserInteractive { - get { - if (_IsUserInteractive.HasValue) { - return _IsUserInteractive.Value; - } - - if (Environment.UserInteractive) { - _IsUserInteractive = true; - } else if (!IsRunningOnMono) { - // If it's non-Mono, we can trust the result - _IsUserInteractive = false; - } else { - // In Mono, Environment.UserInteractive is always false - // There is really no reliable way for now, so assume always being interactive - // Maybe in future I find out some awful hack or workaround that could be at least semi-reliable - _IsUserInteractive = true; - } - - return _IsUserInteractive.Value; - } - } - - private static bool? _IsRuntimeSupported; internal static bool IsRuntimeSupported { get { if (_IsRuntimeSupported.HasValue) { @@ -102,6 +76,69 @@ namespace ArchiSteamFarm { } } + internal static bool IsUserInteractive { + get { + if (_IsUserInteractive.HasValue) { + return _IsUserInteractive.Value; + } + + if (Environment.UserInteractive) { + _IsUserInteractive = true; + } else if (!IsRunningOnMono) { + // If it's non-Mono, we can trust the result + _IsUserInteractive = false; + } else { + // In Mono, Environment.UserInteractive is always false + // There is really no reliable way for now, so assume always being interactive + // Maybe in future I find out some awful hack or workaround that could be at least semi-reliable + _IsUserInteractive = true; + } + + return _IsUserInteractive.Value; + } + } + + private static readonly Type MonoRuntime = Type.GetType("Mono.Runtime"); + + private static bool? _IsRuntimeSupported; + + private static bool? _IsUserInteractive; + + private static Version GetMonoVersion() { + if (MonoRuntime == null) { + ASF.ArchiLogger.LogNullError(nameof(MonoRuntime)); + return null; + } + + MethodInfo displayName = MonoRuntime.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + if (displayName == null) { + ASF.ArchiLogger.LogNullError(nameof(displayName)); + return null; + } + + string versionString = (string) displayName.Invoke(null, null); + if (string.IsNullOrEmpty(versionString)) { + ASF.ArchiLogger.LogNullError(nameof(versionString)); + return null; + } + + int index = versionString.IndexOf(' '); + if (index <= 0) { + ASF.ArchiLogger.LogNullError(nameof(index)); + return null; + } + + versionString = versionString.Substring(0, index); + + Version version; + if (Version.TryParse(versionString, out version)) { + return version; + } + + ASF.ArchiLogger.LogNullError(nameof(version)); + return null; + } + private static Version GetNetVersion() { uint release; using (RegistryKey registryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\\")) { @@ -144,40 +181,5 @@ namespace ArchiSteamFarm { return release >= 378389 ? new Version(4, 5) : null; } - - private static Version GetMonoVersion() { - if (MonoRuntime == null) { - ASF.ArchiLogger.LogNullError(nameof(MonoRuntime)); - return null; - } - - MethodInfo displayName = MonoRuntime.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); - if (displayName == null) { - ASF.ArchiLogger.LogNullError(nameof(displayName)); - return null; - } - - string versionString = (string) displayName.Invoke(null, null); - if (string.IsNullOrEmpty(versionString)) { - ASF.ArchiLogger.LogNullError(nameof(versionString)); - return null; - } - - int index = versionString.IndexOf(' '); - if (index <= 0) { - ASF.ArchiLogger.LogNullError(nameof(index)); - return null; - } - - versionString = versionString.Substring(0, index); - - Version version; - if (Version.TryParse(versionString, out version)) { - return version; - } - - ASF.ArchiLogger.LogNullError(nameof(version)); - return null; - } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/SharedInfo.cs b/ArchiSteamFarm/SharedInfo.cs index c512b1813..82425dc5e 100644 --- a/ArchiSteamFarm/SharedInfo.cs +++ b/ArchiSteamFarm/SharedInfo.cs @@ -27,30 +27,24 @@ using System.Reflection; namespace ArchiSteamFarm { internal static class SharedInfo { - internal const string VersionNumber = "2.1.6.9"; - internal const string Copyright = "Copyright © ArchiSteamFarm 2015-2016"; - - internal const string GithubRepo = "JustArchi/ArchiSteamFarm"; - - internal const string ServiceName = "ArchiSteamFarm"; - internal const string ServiceDescription = "ASF is an application that allows you to farm steam cards using multiple steam accounts simultaneously."; - - internal const string EventLog = ServiceName; - internal const string EventLogSource = EventLog + "Logger"; - + internal const ulong ArchiSteamID = 76561198006963719; internal const string ASF = "ASF"; internal const string ASFDirectory = "ArchiSteamFarm"; - internal const string ConfigDirectory = "config"; - internal const string DebugDirectory = "debug"; - internal const string LogFile = "log.txt"; - - internal const ulong ArchiSteamID = 76561198006963719; internal const ulong ASFGroupSteamID = 103582791440160998; - + internal const string ConfigDirectory = "config"; + internal const string Copyright = "Copyright © ArchiSteamFarm 2015-2016"; + internal const string DebugDirectory = "debug"; + internal const string EventLog = ServiceName; + internal const string EventLogSource = EventLog + "Logger"; internal const string GithubReleaseURL = "https://api.github.com/repos/" + GithubRepo + "/releases"; // GitHub API is HTTPS only + internal const string GithubRepo = "JustArchi/ArchiSteamFarm"; internal const string GlobalConfigFileName = ASF + ".json"; internal const string GlobalDatabaseFileName = ASF + ".db"; + internal const string LogFile = "log.txt"; + internal const string ServiceDescription = "ASF is an application that allows you to farm steam cards using multiple steam accounts simultaneously."; + internal const string ServiceName = "ArchiSteamFarm"; + internal const string VersionNumber = "2.1.6.9"; internal static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 79a84ec7f..46a8573b8 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -31,28 +31,6 @@ using ArchiSteamFarm.JSON; namespace ArchiSteamFarm { internal sealed class Trading : IDisposable { - private sealed class ParseTradeResult { - internal enum EResult : byte { - Unknown, - AcceptedWithItemLose, - AcceptedWithoutItemLose, - RejectedTemporarily, - RejectedPermanently - } - - internal readonly ulong TradeID; - internal readonly EResult Result; - - internal ParseTradeResult(ulong tradeID, EResult result) { - if ((tradeID == 0) || (result == EResult.Unknown)) { - throw new ArgumentNullException(nameof(tradeID) + " || " + nameof(result)); - } - - TradeID = tradeID; - Result = result; - } - } - internal const byte MaxItemsPerTrade = 150; // This is due to limit on POST size in WebBrowser internal const byte MaxTradesPerAccount = 5; // This is limit introduced by Valve @@ -64,14 +42,6 @@ namespace ArchiSteamFarm { private bool ParsingScheduled; - internal static async Task LimitInventoryRequestsAsync() { - await InventorySemaphore.WaitAsync().ConfigureAwait(false); - Task.Run(async () => { - await Task.Delay(Program.GlobalConfig.InventoryLimiterDelay * 1000).ConfigureAwait(false); - InventorySemaphore.Release(); - }).Forget(); - } - internal Trading(Bot bot) { if (bot == null) { throw new ArgumentNullException(nameof(bot)); @@ -85,8 +55,6 @@ namespace ArchiSteamFarm { TradesSemaphore.Dispose(); } - internal void OnDisconnected() => IgnoredTrades.ClearAndTrim(); - internal async Task CheckTrades() { // We aim to have a maximum of 2 tasks, one already parsing, and one waiting in the queue // This way we can call this function as many times as needed e.g. because of Steam events @@ -111,6 +79,16 @@ namespace ArchiSteamFarm { } } + internal static async Task LimitInventoryRequestsAsync() { + await InventorySemaphore.WaitAsync().ConfigureAwait(false); + Task.Run(async () => { + await Task.Delay(Program.GlobalConfig.InventoryLimiterDelay*1000).ConfigureAwait(false); + InventorySemaphore.Release(); + }).Forget(); + } + + internal void OnDisconnected() => IgnoredTrades.ClearAndTrim(); + private async Task ParseActiveTrades() { if (string.IsNullOrEmpty(Bot.BotConfig.SteamApiKey)) { return; @@ -321,5 +299,28 @@ namespace ArchiSteamFarm { // If not, we assume that the trade might be good for us in the future, unless we're bot account where we assume that inventory doesn't change return new ParseTradeResult(tradeOffer.TradeOfferID, difference > 0 ? ParseTradeResult.EResult.AcceptedWithItemLose : (Bot.BotConfig.IsBotAccount ? ParseTradeResult.EResult.RejectedPermanently : ParseTradeResult.EResult.RejectedTemporarily)); } + + private sealed class ParseTradeResult { + internal readonly EResult Result; + + internal readonly ulong TradeID; + + internal ParseTradeResult(ulong tradeID, EResult result) { + if ((tradeID == 0) || (result == EResult.Unknown)) { + throw new ArgumentNullException(nameof(tradeID) + " || " + nameof(result)); + } + + TradeID = tradeID; + Result = result; + } + + internal enum EResult : byte { + Unknown, + AcceptedWithItemLose, + AcceptedWithoutItemLose, + RejectedTemporarily, + RejectedPermanently + } + } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/Utilities.cs b/ArchiSteamFarm/Utilities.cs index 31d09fa78..8abb4ce1f 100644 --- a/ArchiSteamFarm/Utilities.cs +++ b/ArchiSteamFarm/Utilities.cs @@ -55,4 +55,4 @@ namespace ArchiSteamFarm { internal static uint GetUnixTime() => (uint) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/WCF.cs b/ArchiSteamFarm/WCF.cs index 6124c8d44..00417686b 100644 --- a/ArchiSteamFarm/WCF.cs +++ b/ArchiSteamFarm/WCF.cs @@ -41,22 +41,18 @@ namespace ArchiSteamFarm { internal sealed class WCF : IWCF, IDisposable { private static string URL = "http://localhost:1242/ASF"; - private ServiceHost ServiceHost; - private Client Client; - internal bool IsServerRunning => ServiceHost != null; - internal static void Init() { - if (string.IsNullOrEmpty(Program.GlobalConfig.WCFHostname)) { - Program.GlobalConfig.WCFHostname = Program.GetUserInput(ASF.EUserInputType.WCFHostname); - if (string.IsNullOrEmpty(Program.GlobalConfig.WCFHostname)) { - return; - } - } + private Client Client; + private ServiceHost ServiceHost; - URL = "http://" + Program.GlobalConfig.WCFHostname + ":" + Program.GlobalConfig.WCFPort + "/ASF"; + public void Dispose() { + StopClient(); + StopServer(); } + public string GetStatus() => Program.GlobalConfig.SteamOwnerID == 0 ? "{}" : Bot.GetAPIStatus(); + public string HandleCommand(string input) { if (string.IsNullOrEmpty(input)) { ASF.ArchiLogger.LogNullError(nameof(input)); @@ -79,11 +75,28 @@ namespace ArchiSteamFarm { return output; } - public string GetStatus() => Program.GlobalConfig.SteamOwnerID == 0 ? "{}" : Bot.GetAPIStatus(); + internal static void Init() { + if (string.IsNullOrEmpty(Program.GlobalConfig.WCFHostname)) { + Program.GlobalConfig.WCFHostname = Program.GetUserInput(ASF.EUserInputType.WCFHostname); + if (string.IsNullOrEmpty(Program.GlobalConfig.WCFHostname)) { + return; + } + } - public void Dispose() { - StopClient(); - StopServer(); + URL = "http://" + Program.GlobalConfig.WCFHostname + ":" + Program.GlobalConfig.WCFPort + "/ASF"; + } + + internal string SendCommand(string input) { + if (string.IsNullOrEmpty(input)) { + ASF.ArchiLogger.LogNullError(nameof(input)); + return null; + } + + if (Client == null) { + Client = new Client(new BasicHttpBinding(), new EndpointAddress(URL)); + } + + return Client.HandleCommand(input); } internal void StartServer() { @@ -96,9 +109,7 @@ namespace ArchiSteamFarm { try { ServiceHost = new ServiceHost(typeof(WCF), new Uri(URL)); - ServiceHost.Description.Behaviors.Add(new ServiceMetadataBehavior { - HttpGetEnabled = true - }); + ServiceHost.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true }); ServiceHost.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexHttpBinding(), "mex"); ServiceHost.AddServiceEndpoint(typeof(IWCF), new BasicHttpBinding(), string.Empty); @@ -129,19 +140,6 @@ namespace ArchiSteamFarm { ServiceHost = null; } - internal string SendCommand(string input) { - if (string.IsNullOrEmpty(input)) { - ASF.ArchiLogger.LogNullError(nameof(input)); - return null; - } - - if (Client == null) { - Client = new Client(new BasicHttpBinding(), new EndpointAddress(URL)); - } - - return Client.HandleCommand(input); - } - private void StopClient() { if (Client == null) { return; @@ -172,4 +170,4 @@ namespace ArchiSteamFarm { } } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/WebBrowser.cs b/ArchiSteamFarm/WebBrowser.cs index ea7d658a0..eb627d216 100644 --- a/ArchiSteamFarm/WebBrowser.cs +++ b/ArchiSteamFarm/WebBrowser.cs @@ -22,15 +22,15 @@ */ -using HtmlAgilityPack; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Xml; +using HtmlAgilityPack; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace ArchiSteamFarm { internal sealed class WebBrowser { @@ -44,12 +44,27 @@ namespace ArchiSteamFarm { private readonly ArchiLogger ArchiLogger; private readonly HttpClient HttpClient; + internal WebBrowser(ArchiLogger archiLogger) { + if (archiLogger == null) { + throw new ArgumentNullException(nameof(archiLogger)); + } + + ArchiLogger = archiLogger; + + HttpClientHandler httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, CookieContainer = CookieContainer }; + + HttpClient = new HttpClient(httpClientHandler) { Timeout = TimeSpan.FromSeconds(Program.GlobalConfig.HttpTimeout) }; + + // Most web services expect that UserAgent is set, so we declare it globally + HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/" + SharedInfo.Version); + } + internal static void Init() { // Set max connection limit from default of 2 to desired value ServicePointManager.DefaultConnectionLimit = MaxConnections; // Set max idle time from default of 100 seconds (100 * 1000) to desired value - ServicePointManager.MaxServicePointIdleTime = MaxIdleTime * 1000; + ServicePointManager.MaxServicePointIdleTime = MaxIdleTime*1000; // Don't use Expect100Continue, we're sure about our POSTs, save some TCP packets ServicePointManager.Expect100Continue = false; @@ -66,68 +81,6 @@ namespace ArchiSteamFarm { #endif } -#if !__MonoCS__ - private static void InitNonMonoBehaviour() => ServicePointManager.ReusePort = true; -#endif - - internal WebBrowser(ArchiLogger archiLogger) { - if (archiLogger == null) { - throw new ArgumentNullException(nameof(archiLogger)); - } - - ArchiLogger = archiLogger; - - HttpClientHandler httpClientHandler = new HttpClientHandler { - AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - CookieContainer = CookieContainer - }; - - HttpClient = new HttpClient(httpClientHandler) { - Timeout = TimeSpan.FromSeconds(Program.GlobalConfig.HttpTimeout) - }; - - // Most web services expect that UserAgent is set, so we declare it globally - HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/" + SharedInfo.Version); - } - - internal async Task UrlHeadRetry(string request, string referer = null) { - if (string.IsNullOrEmpty(request)) { - ArchiLogger.LogNullError(nameof(request)); - return false; - } - - bool result = false; - for (byte i = 0; (i < MaxRetries) && !result; i++) { - result = await UrlHead(request, referer).ConfigureAwait(false); - } - - if (result) { - return true; - } - - ArchiLogger.LogGenericWarning("Request failed even after " + MaxRetries + " tries"); - return false; - } - - internal async Task UrlHeadToUriRetry(string request, string referer = null) { - if (string.IsNullOrEmpty(request)) { - ArchiLogger.LogNullError(nameof(request)); - return null; - } - - Uri result = null; - for (byte i = 0; (i < MaxRetries) && (result == null); i++) { - result = await UrlHeadToUri(request, referer).ConfigureAwait(false); - } - - if (result != null) { - return result; - } - - ArchiLogger.LogGenericWarning("Request failed even after " + MaxRetries + " tries"); - return null; - } - internal async Task UrlGetToBytesRetry(string request, string referer = null) { if (string.IsNullOrEmpty(request)) { ArchiLogger.LogNullError(nameof(request)); @@ -223,6 +176,44 @@ namespace ArchiSteamFarm { return null; } + internal async Task UrlHeadRetry(string request, string referer = null) { + if (string.IsNullOrEmpty(request)) { + ArchiLogger.LogNullError(nameof(request)); + return false; + } + + bool result = false; + for (byte i = 0; (i < MaxRetries) && !result; i++) { + result = await UrlHead(request, referer).ConfigureAwait(false); + } + + if (result) { + return true; + } + + ArchiLogger.LogGenericWarning("Request failed even after " + MaxRetries + " tries"); + return false; + } + + internal async Task UrlHeadToUriRetry(string request, string referer = null) { + if (string.IsNullOrEmpty(request)) { + ArchiLogger.LogNullError(nameof(request)); + return null; + } + + Uri result = null; + for (byte i = 0; (i < MaxRetries) && (result == null); i++) { + result = await UrlHeadToUri(request, referer).ConfigureAwait(false); + } + + if (result != null) { + return result; + } + + ArchiLogger.LogGenericWarning("Request failed even after " + MaxRetries + " tries"); + return null; + } + internal async Task UrlPostRetry(string request, ICollection> data = null, string referer = null) { if (string.IsNullOrEmpty(request)) { ArchiLogger.LogNullError(nameof(request)); @@ -277,6 +268,10 @@ namespace ArchiSteamFarm { } } +#if !__MonoCS__ + private static void InitNonMonoBehaviour() => ServicePointManager.ReusePort = true; +#endif + private async Task UrlGetToBytes(string request, string referer = null) { if (string.IsNullOrEmpty(request)) { ArchiLogger.LogNullError(nameof(request)); @@ -537,4 +532,4 @@ namespace ArchiSteamFarm { return null; } } -} +} \ No newline at end of file diff --git a/ArchiSteamFarm/config/ASF.json b/ArchiSteamFarm/config/ASF.json index 3354c3d6d..46ea6d8d1 100644 --- a/ArchiSteamFarm/config/ASF.json +++ b/ArchiSteamFarm/config/ASF.json @@ -1,29 +1,29 @@ { - "Debug": false, - "Headless": false, - "AutoUpdates": true, - "AutoRestart": true, - "UpdateChannel": 1, - "SteamProtocol": 6, - "SteamOwnerID": 0, - "MaxFarmingTime": 10, - "IdleFarmingPeriod": 3, - "FarmingDelay": 15, - "LoginLimiterDelay": 10, - "InventoryLimiterDelay": 3, - "GiftsLimiterDelay": 1, - "MaxTradeHoldDuration": 15, - "ForceHttp": false, - "HttpTimeout": 60, - "WCFHostname": "localhost", - "WCFPort": 1242, - "Statistics": true, - "Blacklist": [ - 267420, - 303700, - 335590, - 368020, - 425280, - 480730 - ] + "Debug": false, + "Headless": false, + "AutoUpdates": true, + "AutoRestart": true, + "UpdateChannel": 1, + "SteamProtocol": 6, + "SteamOwnerID": 0, + "MaxFarmingTime": 10, + "IdleFarmingPeriod": 3, + "FarmingDelay": 15, + "LoginLimiterDelay": 10, + "InventoryLimiterDelay": 3, + "GiftsLimiterDelay": 1, + "MaxTradeHoldDuration": 15, + "ForceHttp": false, + "HttpTimeout": 60, + "WCFHostname": "localhost", + "WCFPort": 1242, + "Statistics": true, + "Blacklist": [ + 267420, + 303700, + 335590, + 368020, + 425280, + 480730 + ] } \ No newline at end of file diff --git a/ArchiSteamFarm/config/example.json b/ArchiSteamFarm/config/example.json index 0f9ce2679..63617b797 100644 --- a/ArchiSteamFarm/config/example.json +++ b/ArchiSteamFarm/config/example.json @@ -1,29 +1,29 @@ { - "Enabled": false, - "Paused": false, - "SteamLogin": null, - "SteamPassword": null, - "PasswordFormat": 0, - "SteamParentalPIN": "0", - "SteamApiKey": null, - "SteamMasterID": 0, - "SteamMasterClanID": 0, - "CardDropsRestricted": true, - "DismissInventoryNotifications": true, - "FarmingOrder": 0, - "FarmOffline": false, - "HandleOfflineMessages": false, - "AcceptGifts": false, - "IsBotAccount": false, - "ForwardKeysToOtherBots": false, - "DistributeKeys": false, - "ShutdownOnFarmingFinished": false, - "SendOnFarmingFinished": false, - "SteamTradeToken": null, - "SendTradePeriod": 0, - "TradingPreferences": 1, - "AcceptConfirmationsPeriod": 0, - "CustomGamePlayedWhileFarming": null, - "CustomGamePlayedWhileIdle": null, - "GamesPlayedWhileIdle": [ ] + "Enabled": false, + "Paused": false, + "SteamLogin": null, + "SteamPassword": null, + "PasswordFormat": 0, + "SteamParentalPIN": "0", + "SteamApiKey": null, + "SteamMasterID": 0, + "SteamMasterClanID": 0, + "CardDropsRestricted": true, + "DismissInventoryNotifications": true, + "FarmingOrder": 0, + "FarmOffline": false, + "HandleOfflineMessages": false, + "AcceptGifts": false, + "IsBotAccount": false, + "ForwardKeysToOtherBots": false, + "DistributeKeys": false, + "ShutdownOnFarmingFinished": false, + "SendOnFarmingFinished": false, + "SteamTradeToken": null, + "SendTradePeriod": 0, + "TradingPreferences": 1, + "AcceptConfirmationsPeriod": 0, + "CustomGamePlayedWhileFarming": null, + "CustomGamePlayedWhileIdle": null, + "GamesPlayedWhileIdle": [] } \ No newline at end of file diff --git a/ArchiSteamFarm/config/minimal.json b/ArchiSteamFarm/config/minimal.json index a667c1b78..a75051327 100644 --- a/ArchiSteamFarm/config/minimal.json +++ b/ArchiSteamFarm/config/minimal.json @@ -1,5 +1,5 @@ { - "Enabled": false, - "SteamLogin": null, - "SteamPassword": null + "Enabled": false, + "SteamLogin": null, + "SteamPassword": null } \ No newline at end of file diff --git a/ArchiSteamFarm/packages.config b/ArchiSteamFarm/packages.config index c0225b2ce..d9ec4890a 100644 --- a/ArchiSteamFarm/packages.config +++ b/ArchiSteamFarm/packages.config @@ -1,4 +1,5 @@  + diff --git a/ConfigGenerator/ASFConfig.cs b/ConfigGenerator/ASFConfig.cs index 76198336e..7a27f6413 100644 --- a/ConfigGenerator/ASFConfig.cs +++ b/ConfigGenerator/ASFConfig.cs @@ -22,20 +22,20 @@ */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using ArchiSteamFarm; +using Newtonsoft.Json; namespace ConfigGenerator { internal abstract class ASFConfig { internal static readonly HashSet ASFConfigs = new HashSet(); - internal string FilePath { get; set; } - private readonly object FileLock = new object(); + internal string FilePath { get; set; } + protected ASFConfig() { ASFConfigs.Add(this); } @@ -48,16 +48,6 @@ namespace ConfigGenerator { FilePath = filePath; } - internal void Save() { - lock (FileLock) { - try { - File.WriteAllText(FilePath, JsonConvert.SerializeObject(this, Formatting.Indented)); - } catch (Exception e) { - Logging.LogGenericException(e); - } - } - } - internal void Remove() { string queryPath = Path.GetFileNameWithoutExtension(FilePath); lock (FileLock) { @@ -92,5 +82,15 @@ namespace ConfigGenerator { FilePath = Path.Combine(SharedInfo.ConfigDirectory, botName + ".json"); } } + + internal void Save() { + lock (FileLock) { + try { + File.WriteAllText(FilePath, JsonConvert.SerializeObject(this, Formatting.Indented)); + } catch (Exception e) { + Logging.LogGenericException(e); + } + } + } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/App.config b/ConfigGenerator/App.config index bae5d6d81..efb4d02fd 100644 --- a/ConfigGenerator/App.config +++ b/ConfigGenerator/App.config @@ -1,6 +1,7 @@ - + + - - - - + + + + \ No newline at end of file diff --git a/ConfigGenerator/BotConfig.cs b/ConfigGenerator/BotConfig.cs index d98b9aeab..f71014f40 100644 --- a/ConfigGenerator/BotConfig.cs +++ b/ConfigGenerator/BotConfig.cs @@ -22,12 +22,13 @@ */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Drawing.Design; using System.IO; +using Newtonsoft.Json; namespace ConfigGenerator { [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] @@ -35,120 +36,16 @@ namespace ConfigGenerator { [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")] internal sealed class BotConfig : ASFConfig { - internal enum ECryptoMethod : byte { - PlainText, - AES, - ProtectedDataForCurrentUser - } - - internal enum EFarmingOrder : byte { - Unordered, - AppIDsAscending, - AppIDsDescending, - CardDropsAscending, - CardDropsDescending, - HoursAscending, - HoursDescending, - NamesAscending, - NamesDescending - } - - [Flags] - internal enum ETradingPreferences : byte { - None = 0, - AcceptDonations = 1, - SteamTradeMatcher = 2, - MatchEverything = 4 - } - - [Category("\t\tCore")] - [JsonProperty(Required = Required.DisallowNull)] - public bool Enabled { get; set; } = false; - [Category("\tAdvanced")] [JsonProperty(Required = Required.DisallowNull)] - public bool Paused { get; set; } = false; - - [Category("\t\tCore")] - [JsonProperty] - public string SteamLogin { get; set; } = null; - - [Category("\t\tCore")] - [JsonProperty] - [PasswordPropertyText(true)] - public string SteamPassword { get; set; } = null; - - [Category("\tAccess")] - [JsonProperty(Required = Required.DisallowNull)] - public ECryptoMethod PasswordFormat { get; set; } = ECryptoMethod.PlainText; - - [Category("\tAccess")] - [JsonProperty] - public string SteamParentalPIN { get; set; } = "0"; - - [Category("\tAccess")] - [JsonProperty] - public string SteamApiKey { get; set; } = null; - - [Category("\tAccess")] - [JsonProperty(Required = Required.DisallowNull)] - public ulong SteamMasterID { get; set; } = 0; - - [Category("\tAccess")] - [JsonProperty(Required = Required.DisallowNull)] - public ulong SteamMasterClanID { get; set; } = 0; - - [Category("\tPerformance")] - [JsonProperty(Required = Required.DisallowNull)] - public bool CardDropsRestricted { get; set; } = true; - - [JsonProperty(Required = Required.DisallowNull)] - public bool DismissInventoryNotifications { get; set; } = true; - - [JsonProperty(Required = Required.DisallowNull)] - public EFarmingOrder FarmingOrder { get; set; } = EFarmingOrder.Unordered; - - [JsonProperty(Required = Required.DisallowNull)] - public bool FarmOffline { get; set; } = false; - - [Category("\tAdvanced")] - [JsonProperty(Required = Required.DisallowNull)] - public bool HandleOfflineMessages { get; set; } = false; + public byte AcceptConfirmationsPeriod { get; set; } = 0; [JsonProperty(Required = Required.DisallowNull)] public bool AcceptGifts { get; set; } = false; - [Category("\tAdvanced")] + [Category("\tPerformance")] [JsonProperty(Required = Required.DisallowNull)] - public bool IsBotAccount { get; set; } = false; - - [JsonProperty(Required = Required.DisallowNull)] - public bool ForwardKeysToOtherBots { get; set; } = false; - - [JsonProperty(Required = Required.DisallowNull)] - public bool DistributeKeys { get; set; } = false; - - [JsonProperty(Required = Required.DisallowNull)] - public bool ShutdownOnFarmingFinished { get; set; } = false; - - [JsonProperty(Required = Required.DisallowNull)] - public bool SendOnFarmingFinished { get; set; } = false; - - [Category("\tAccess")] - [JsonProperty] - public string SteamTradeToken { get; set; } = null; - - [JsonProperty(Required = Required.DisallowNull)] - public byte SendTradePeriod { get; set; } = 0; - - [Category("\tAdvanced")] - [Editor(typeof(FlagEnumUIEditor), typeof(System.Drawing.Design.UITypeEditor))] - [JsonProperty(Required = Required.DisallowNull)] - public ETradingPreferences TradingPreferences { get; set; } = ETradingPreferences.AcceptDonations; - - [Category("\tAdvanced")] - [JsonProperty(Required = Required.DisallowNull)] - public byte AcceptConfirmationsPeriod { get; set; } = 0; + public bool CardDropsRestricted { get; set; } = true; [JsonProperty] public string CustomGamePlayedWhileFarming { get; set; } = null; @@ -156,9 +53,98 @@ namespace ConfigGenerator { [JsonProperty] public string CustomGamePlayedWhileIdle { get; set; } = null; + [JsonProperty(Required = Required.DisallowNull)] + public bool DismissInventoryNotifications { get; set; } = true; + + [JsonProperty(Required = Required.DisallowNull)] + public bool DistributeKeys { get; set; } = false; + + [Category("\t\tCore")] + [JsonProperty(Required = Required.DisallowNull)] + public bool Enabled { get; set; } = false; + + [JsonProperty(Required = Required.DisallowNull)] + public EFarmingOrder FarmingOrder { get; set; } = EFarmingOrder.Unordered; + + [JsonProperty(Required = Required.DisallowNull)] + public bool FarmOffline { get; set; } = false; + + [JsonProperty(Required = Required.DisallowNull)] + public bool ForwardKeysToOtherBots { get; set; } = false; + [JsonProperty(Required = Required.DisallowNull)] public List GamesPlayedWhileIdle { get; set; } = new List(); + [Category("\tAdvanced")] + [JsonProperty(Required = Required.DisallowNull)] + public bool HandleOfflineMessages { get; set; } = false; + + [Category("\tAdvanced")] + [JsonProperty(Required = Required.DisallowNull)] + public bool IsBotAccount { get; set; } = false; + + [Category("\tAccess")] + [JsonProperty(Required = Required.DisallowNull)] + public ECryptoMethod PasswordFormat { get; set; } = ECryptoMethod.PlainText; + + [Category("\tAdvanced")] + [JsonProperty(Required = Required.DisallowNull)] + public bool Paused { get; set; } = false; + + [JsonProperty(Required = Required.DisallowNull)] + public bool SendOnFarmingFinished { get; set; } = false; + + [JsonProperty(Required = Required.DisallowNull)] + public byte SendTradePeriod { get; set; } = 0; + + [JsonProperty(Required = Required.DisallowNull)] + public bool ShutdownOnFarmingFinished { get; set; } = false; + + [Category("\tAccess")] + [JsonProperty] + public string SteamApiKey { get; set; } = null; + + [Category("\t\tCore")] + [JsonProperty] + public string SteamLogin { get; set; } = null; + + [Category("\tAccess")] + [JsonProperty(Required = Required.DisallowNull)] + public ulong SteamMasterClanID { get; set; } = 0; + + [Category("\tAccess")] + [JsonProperty(Required = Required.DisallowNull)] + public ulong SteamMasterID { get; set; } = 0; + + [Category("\tAccess")] + [JsonProperty] + public string SteamParentalPIN { get; set; } = "0"; + + [Category("\t\tCore")] + [JsonProperty] + [PasswordPropertyText(true)] + public string SteamPassword { get; set; } = null; + + [Category("\tAccess")] + [JsonProperty] + public string SteamTradeToken { get; set; } = null; + + [Category("\tAdvanced")] + [Editor(typeof(FlagEnumUiEditor), typeof(UITypeEditor))] + [JsonProperty(Required = Required.DisallowNull)] + public ETradingPreferences TradingPreferences { get; set; } = ETradingPreferences.AcceptDonations; + + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private BotConfig() { } + + private BotConfig(string filePath) : base(filePath) { + if (string.IsNullOrEmpty(filePath)) { + throw new ArgumentNullException(nameof(filePath)); + } + + Save(); + } + internal static BotConfig Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { Logging.LogNullError(nameof(filePath)); @@ -187,15 +173,30 @@ namespace ConfigGenerator { return botConfig; } - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private BotConfig() { } + internal enum ECryptoMethod : byte { + PlainText, + AES, + ProtectedDataForCurrentUser + } - private BotConfig(string filePath) : base(filePath) { - if (string.IsNullOrEmpty(filePath)) { - throw new ArgumentNullException(nameof(filePath)); - } + internal enum EFarmingOrder : byte { + Unordered, + AppIDsAscending, + AppIDsDescending, + CardDropsAscending, + CardDropsDescending, + HoursAscending, + HoursDescending, + NamesAscending, + NamesDescending + } - Save(); + [Flags] + internal enum ETradingPreferences : byte { + None = 0, + AcceptDonations = 1, + SteamTradeMatcher = 2, + MatchEverything = 4 } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/ConfigPage.cs b/ConfigGenerator/ConfigPage.cs index 43c4ee902..3b9eebf81 100644 --- a/ConfigGenerator/ConfigPage.cs +++ b/ConfigGenerator/ConfigPage.cs @@ -45,4 +45,4 @@ namespace ConfigGenerator { internal void RefreshText() => Text = Path.GetFileNameWithoutExtension(ASFConfig.FilePath); } -} +} \ No newline at end of file diff --git a/ConfigGenerator/Debugging.cs b/ConfigGenerator/Debugging.cs index bb56ec613..b32c9c97c 100644 --- a/ConfigGenerator/Debugging.cs +++ b/ConfigGenerator/Debugging.cs @@ -34,4 +34,4 @@ namespace ConfigGenerator { internal static readonly bool IsDebugBuild = false; #endif } -} +} \ No newline at end of file diff --git a/ConfigGenerator/DialogBox.cs b/ConfigGenerator/DialogBox.cs index 82ba258fc..5e249d761 100644 --- a/ConfigGenerator/DialogBox.cs +++ b/ConfigGenerator/DialogBox.cs @@ -36,43 +36,11 @@ namespace ConfigGenerator { return DialogResult.Abort; } - TextBox textBox = new TextBox { - Anchor = AnchorStyles.Right, - Bounds = new Rectangle(12, 36, 372, 20), - Width = 1000 - }; - - Button buttonOk = new Button { - Anchor = AnchorStyles.Bottom | AnchorStyles.Right, - Bounds = new Rectangle(228, 72, 75, 23), - DialogResult = DialogResult.OK, - Text = Resources.OK - }; - - Button buttonCancel = new Button { - Anchor = AnchorStyles.Bottom | AnchorStyles.Right, - Bounds = new Rectangle(309, 72, 75, 23), - DialogResult = DialogResult.Cancel, - Text = Resources.Cancel - }; - - Label label = new Label { - AutoSize = true, - Bounds = new Rectangle(9, 20, 372, 13), - Text = promptText - }; - - Form form = new Form { - AcceptButton = buttonOk, - CancelButton = buttonCancel, - ClientSize = new Size(Math.Max(300, label.Right + 10), 107), - Controls = { label, textBox, buttonOk, buttonCancel }, - FormBorderStyle = FormBorderStyle.FixedDialog, - MinimizeBox = false, - MaximizeBox = false, - StartPosition = FormStartPosition.CenterScreen, - Text = title - }; + TextBox textBox = new TextBox { Anchor = AnchorStyles.Right, Bounds = new Rectangle(12, 36, 372, 20), Width = 1000 }; + Button buttonOk = new Button { Anchor = AnchorStyles.Bottom | AnchorStyles.Right, Bounds = new Rectangle(228, 72, 75, 23), DialogResult = DialogResult.OK, Text = Resources.OK }; + Button buttonCancel = new Button { Anchor = AnchorStyles.Bottom | AnchorStyles.Right, Bounds = new Rectangle(309, 72, 75, 23), DialogResult = DialogResult.Cancel, Text = Resources.Cancel }; + Label label = new Label { AutoSize = true, Bounds = new Rectangle(9, 20, 372, 13), Text = promptText }; + Form form = new Form { AcceptButton = buttonOk, CancelButton = buttonCancel, ClientSize = new Size(Math.Max(300, label.Right + 10), 107), Controls = { label, textBox, buttonOk, buttonCancel }, FormBorderStyle = FormBorderStyle.FixedDialog, MinimizeBox = false, MaximizeBox = false, StartPosition = FormStartPosition.CenterScreen, Text = title }; DialogResult dialogResult = form.ShowDialog(); value = textBox.Text; @@ -85,40 +53,13 @@ namespace ConfigGenerator { return DialogResult.Abort; } - Button buttonYes = new Button { - Anchor = AnchorStyles.Bottom | AnchorStyles.Right, - Bounds = new Rectangle(228, 72, 75, 23), - DialogResult = DialogResult.Yes, - Text = Resources.Yes - }; - - Button buttonNo = new Button { - Anchor = AnchorStyles.Bottom | AnchorStyles.Right, - Bounds = new Rectangle(309, 72, 75, 23), - DialogResult = DialogResult.No, - Text = Resources.No - }; - - Label label = new Label { - AutoSize = true, - Bounds = new Rectangle(9, 20, 372, 13), - Text = promptText - }; - - Form form = new Form { - AcceptButton = buttonYes, - CancelButton = buttonNo, - ClientSize = new Size(Math.Max(300, label.Right + 10), 107), - Controls = { label, buttonYes, buttonNo }, - FormBorderStyle = FormBorderStyle.FixedDialog, - MinimizeBox = false, - MaximizeBox = false, - StartPosition = FormStartPosition.CenterScreen, - Text = title - }; + Button buttonYes = new Button { Anchor = AnchorStyles.Bottom | AnchorStyles.Right, Bounds = new Rectangle(228, 72, 75, 23), DialogResult = DialogResult.Yes, Text = Resources.Yes }; + Button buttonNo = new Button { Anchor = AnchorStyles.Bottom | AnchorStyles.Right, Bounds = new Rectangle(309, 72, 75, 23), DialogResult = DialogResult.No, Text = Resources.No }; + Label label = new Label { AutoSize = true, Bounds = new Rectangle(9, 20, 372, 13), Text = promptText }; + Form form = new Form { AcceptButton = buttonYes, CancelButton = buttonNo, ClientSize = new Size(Math.Max(300, label.Right + 10), 107), Controls = { label, buttonYes, buttonNo }, FormBorderStyle = FormBorderStyle.FixedDialog, MinimizeBox = false, MaximizeBox = false, StartPosition = FormStartPosition.CenterScreen, Text = title }; DialogResult dialogResult = form.ShowDialog(); return dialogResult; } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/EnhancedPropertyGrid.cs b/ConfigGenerator/EnhancedPropertyGrid.cs index d345194a1..b7d5fe8d5 100644 --- a/ConfigGenerator/EnhancedPropertyGrid.cs +++ b/ConfigGenerator/EnhancedPropertyGrid.cs @@ -43,6 +43,16 @@ namespace ConfigGenerator { ToolbarVisible = false; } + protected override void OnGotFocus(EventArgs args) { + if (args == null) { + Logging.LogNullError(nameof(args)); + return; + } + + base.OnGotFocus(args); + ASFConfig.Save(); + } + protected override void OnPropertyValueChanged(PropertyValueChangedEventArgs args) { if (args == null) { Logging.LogNullError(nameof(args)); @@ -74,15 +84,5 @@ namespace ConfigGenerator { Tutorial.OnAction(Tutorial.EPhase.GlobalConfigReady); } } - - protected override void OnGotFocus(EventArgs args) { - if (args == null) { - Logging.LogNullError(nameof(args)); - return; - } - - base.OnGotFocus(args); - ASFConfig.Save(); - } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/FlagEnumEditor.cs b/ConfigGenerator/FlagEnumEditor.cs index e2096f2b5..93a8ddd75 100644 --- a/ConfigGenerator/FlagEnumEditor.cs +++ b/ConfigGenerator/FlagEnumEditor.cs @@ -7,28 +7,31 @@ using System.Windows.Forms.Design; namespace ConfigGenerator { internal sealed class FlagCheckedListBox : CheckedListBox { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + internal Enum EnumValue { + get { + object e = Enum.ToObject(EnumType, GetCurrentValue()); + return (Enum) e; + } + + set { + Items.Clear(); + _EnumValue = value; // Store the current enum value + EnumType = value.GetType(); // Store enum type + FillEnumMembers(); // Add items for enum members + ApplyEnumValue(); // Check/uncheck items depending on enum value + } + } + + private Enum _EnumValue; + private Type EnumType; + private bool IsUpdatingCheckStates; internal FlagCheckedListBox() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); } - #region Component Designer generated code - private void InitializeComponent() { - // - // FlaggedCheckedListBox - // - CheckOnClick = true; - - } - #endregion - - // Adds an integer value and its associated description - private void Add(int v, string c) { - FlagCheckedListBoxItem item = new FlagCheckedListBoxItem(v, c); - Items.Add(item); - } - protected override void OnItemCheck(ItemCheckEventArgs e) { base.OnItemCheck(e); @@ -42,9 +45,44 @@ namespace ConfigGenerator { UpdateCheckedItems(item, e.NewValue); } + // Adds an integer value and its associated description + private void Add(int v, string c) { + FlagCheckedListBoxItem item = new FlagCheckedListBoxItem(v, c); + Items.Add(item); + } + + // Checks/unchecks items based on the current value of the enum variable + private void ApplyEnumValue() { + int intVal = (int) Convert.ChangeType(_EnumValue, typeof(int)); + UpdateCheckedItems(intVal); + } + + // Adds items to the checklistbox based on the members of the enum + private void FillEnumMembers() { + foreach (string name in Enum.GetNames(EnumType)) { + object val = Enum.Parse(EnumType, name); + int intVal = (int) Convert.ChangeType(val, typeof(int)); + + Add(intVal, name); + } + } + + // Gets the current bit value corresponding to all checked items + private int GetCurrentValue() => (from object t in Items select t as FlagCheckedListBoxItem).Where((item, i) => (item != null) && GetItemChecked(i)).Aggregate(0, (current, item) => current | item.Value); + + #region Component Designer generated code + + private void InitializeComponent() { + // + // FlaggedCheckedListBox + // + CheckOnClick = true; + } + + #endregion + // Checks/Unchecks items depending on the give bitvalue private void UpdateCheckedItems(int value) { - IsUpdatingCheckStates = true; // Iterate over all items @@ -57,7 +95,6 @@ namespace ConfigGenerator { if (item.Value == 0) { SetItemChecked(i, value == 0); } else { - // If the bit for the current item is on in the bitvalue, check it if (((item.Value & value) == item.Value) && (item.Value != 0)) { SetItemChecked(i, true); @@ -70,20 +107,17 @@ namespace ConfigGenerator { } IsUpdatingCheckStates = false; - } // Updates items in the checklistbox // composite = The item that was checked/unchecked // cs = The check state of that item private void UpdateCheckedItems(FlagCheckedListBoxItem composite, CheckState cs) { - // If the value of the item is 0, call directly. if (composite.Value == 0) { UpdateCheckedItems(0); } - // Get the total value of all checked items int sum = (from object t in Items select t as FlagCheckedListBoxItem).Where((item, i) => (item != null) && GetItemChecked(i)).Aggregate(0, (current, item) => current | item.Value); @@ -98,53 +132,7 @@ namespace ConfigGenerator { // Update all items in the checklistbox based on the final bit value UpdateCheckedItems(sum); - } - - private bool IsUpdatingCheckStates; - - // Gets the current bit value corresponding to all checked items - private int GetCurrentValue() => (from object t in Items select t as FlagCheckedListBoxItem).Where((item, i) => (item != null) && GetItemChecked(i)).Aggregate(0, (current, item) => current | item.Value); - - private Type EnumType; - private Enum _EnumValue; - - // Adds items to the checklistbox based on the members of the enum - private void FillEnumMembers() { - foreach (string name in Enum.GetNames(EnumType)) { - object val = Enum.Parse(EnumType, name); - int intVal = (int) Convert.ChangeType(val, typeof(int)); - - Add(intVal, name); - } - } - - // Checks/unchecks items based on the current value of the enum variable - private void ApplyEnumValue() { - int intVal = (int) Convert.ChangeType(_EnumValue, typeof(int)); - UpdateCheckedItems(intVal); - - } - - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - internal Enum EnumValue { - get { - object e = Enum.ToObject(EnumType, GetCurrentValue()); - return (Enum) e; - } - - set { - - Items.Clear(); - _EnumValue = value; // Store the current enum value - EnumType = value.GetType(); // Store enum type - FillEnumMembers(); // Add items for enum members - ApplyEnumValue(); // Check/uncheck items depending on enum value - - } - } - - } // Represents an item in the checklistbox @@ -161,13 +149,12 @@ namespace ConfigGenerator { public override string ToString() => Caption; } - // UITypeEditor for flag enums - internal sealed class FlagEnumUIEditor : UITypeEditor { + internal sealed class FlagEnumUiEditor : UITypeEditor { // The checklistbox private readonly FlagCheckedListBox FlagEnumCb; - internal FlagEnumUIEditor() { + internal FlagEnumUiEditor() { FlagEnumCb = new FlagCheckedListBox { BorderStyle = BorderStyle.None }; } @@ -190,4 +177,4 @@ namespace ConfigGenerator { public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) => UITypeEditorEditStyle.DropDown; } -} +} \ No newline at end of file diff --git a/ConfigGenerator/FodyWeavers.xml b/ConfigGenerator/FodyWeavers.xml index 5df39cbf9..c4626742c 100644 --- a/ConfigGenerator/FodyWeavers.xml +++ b/ConfigGenerator/FodyWeavers.xml @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/ConfigGenerator/GlobalConfig.cs b/ConfigGenerator/GlobalConfig.cs index 998c5da10..f009d9a54 100644 --- a/ConfigGenerator/GlobalConfig.cs +++ b/ConfigGenerator/GlobalConfig.cs @@ -22,13 +22,13 @@ */ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Sockets; +using Newtonsoft.Json; namespace ConfigGenerator { [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] @@ -36,52 +36,49 @@ namespace ConfigGenerator { [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")] internal sealed class GlobalConfig : ASFConfig { - internal enum EUpdateChannel : byte { - None, - Stable, - Experimental - } - - private const byte DefaultMaxFarmingTime = 10; private const byte DefaultFarmingDelay = 15; private const byte DefaultHttpTimeout = 60; - private const ushort DefaultWCFPort = 1242; + private const byte DefaultMaxFarmingTime = 10; private const ProtocolType DefaultSteamProtocol = ProtocolType.Tcp; + private const ushort DefaultWCFPort = 1242; // This is hardcoded blacklist which should not be possible to change private static readonly HashSet GlobalBlacklist = new HashSet { 267420, 303700, 335590, 368020, 425280, 480730 }; - [Category("\tDebugging")] - [JsonProperty(Required = Required.DisallowNull)] - public bool Debug { get; set; } = false; - - [Category("\tAdvanced")] - [JsonProperty(Required = Required.DisallowNull)] - public bool Headless { get; set; } = false; - - [Category("\tUpdates")] - [JsonProperty(Required = Required.DisallowNull)] - public bool AutoUpdates { get; set; } = true; - [Category("\tUpdates")] [JsonProperty(Required = Required.DisallowNull)] public bool AutoRestart { get; set; } = true; [Category("\tUpdates")] [JsonProperty(Required = Required.DisallowNull)] - public EUpdateChannel UpdateChannel { get; set; } = EUpdateChannel.Stable; + public bool AutoUpdates { get; set; } = true; - [Category("\tAdvanced")] [JsonProperty(Required = Required.DisallowNull)] - public ProtocolType SteamProtocol { get; set; } = DefaultSteamProtocol; + public List Blacklist { get; set; } = new List(); - [Category("\tAccess")] + [Category("\tDebugging")] [JsonProperty(Required = Required.DisallowNull)] - public ulong SteamOwnerID { get; set; } = 0; + public bool Debug { get; set; } = false; [Category("\tPerformance")] [JsonProperty(Required = Required.DisallowNull)] - public byte MaxFarmingTime { get; set; } = DefaultMaxFarmingTime; + public byte FarmingDelay { get; set; } = DefaultFarmingDelay; + + [Category("\tDebugging")] + [JsonProperty(Required = Required.DisallowNull)] + public bool ForceHttp { get; set; } = false; + + [Category("\tPerformance")] + [JsonProperty(Required = Required.DisallowNull)] + public byte GiftsLimiterDelay { get; set; } = 1; + + [Category("\tAdvanced")] + [JsonProperty(Required = Required.DisallowNull)] + public bool Headless { get; set; } = false; + + [Category("\tDebugging")] + [JsonProperty(Required = Required.DisallowNull)] + public byte HttpTimeout { get; set; } = DefaultHttpTimeout; [Category("\tPerformance")] [JsonProperty(Required = Required.DisallowNull)] @@ -89,7 +86,7 @@ namespace ConfigGenerator { [Category("\tPerformance")] [JsonProperty(Required = Required.DisallowNull)] - public byte FarmingDelay { get; set; } = DefaultFarmingDelay; + public byte InventoryLimiterDelay { get; set; } = 3; [Category("\tPerformance")] [JsonProperty(Required = Required.DisallowNull)] @@ -97,22 +94,25 @@ namespace ConfigGenerator { [Category("\tPerformance")] [JsonProperty(Required = Required.DisallowNull)] - public byte InventoryLimiterDelay { get; set; } = 3; - - [Category("\tPerformance")] - [JsonProperty(Required = Required.DisallowNull)] - public byte GiftsLimiterDelay { get; set; } = 1; + public byte MaxFarmingTime { get; set; } = DefaultMaxFarmingTime; [JsonProperty(Required = Required.DisallowNull)] public byte MaxTradeHoldDuration { get; set; } = 15; - [Category("\tDebugging")] [JsonProperty(Required = Required.DisallowNull)] - public bool ForceHttp { get; set; } = false; + public bool Statistics { get; set; } = true; - [Category("\tDebugging")] + [Category("\tAccess")] [JsonProperty(Required = Required.DisallowNull)] - public byte HttpTimeout { get; set; } = DefaultHttpTimeout; + public ulong SteamOwnerID { get; set; } = 0; + + [Category("\tAdvanced")] + [JsonProperty(Required = Required.DisallowNull)] + public ProtocolType SteamProtocol { get; set; } = DefaultSteamProtocol; + + [Category("\tUpdates")] + [JsonProperty(Required = Required.DisallowNull)] + public EUpdateChannel UpdateChannel { get; set; } = EUpdateChannel.Stable; [Category("\tAccess")] [JsonProperty] @@ -122,11 +122,17 @@ namespace ConfigGenerator { [JsonProperty(Required = Required.DisallowNull)] public ushort WCFPort { get; set; } = DefaultWCFPort; - [JsonProperty(Required = Required.DisallowNull)] - public bool Statistics { get; set; } = true; + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private GlobalConfig() { } - [JsonProperty(Required = Required.DisallowNull)] - public List Blacklist { get; set; } = new List(); + private GlobalConfig(string filePath) : base(filePath) { + if (string.IsNullOrEmpty(filePath)) { + throw new ArgumentNullException(nameof(filePath)); + } + + Blacklist.AddRange(GlobalBlacklist); + Save(); + } internal static GlobalConfig Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { @@ -192,16 +198,10 @@ namespace ConfigGenerator { return globalConfig; } - [SuppressMessage("ReSharper", "UnusedMember.Local")] - private GlobalConfig() { } - - private GlobalConfig(string filePath) : base(filePath) { - if (string.IsNullOrEmpty(filePath)) { - throw new ArgumentNullException(nameof(filePath)); - } - - Blacklist.AddRange(GlobalBlacklist); - Save(); + internal enum EUpdateChannel : byte { + None, + Stable, + Experimental } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/Logging.cs b/ConfigGenerator/Logging.cs index b1894ff71..89a479ec7 100644 --- a/ConfigGenerator/Logging.cs +++ b/ConfigGenerator/Logging.cs @@ -30,15 +30,6 @@ using ConfigGenerator.Properties; namespace ConfigGenerator { internal static class Logging { - internal static void LogGenericInfoWithoutStacktrace(string message) { - if (string.IsNullOrEmpty(message)) { - LogNullError(nameof(message)); - return; - } - - MessageBox.Show(message, Resources.Information, MessageBoxButtons.OK, MessageBoxIcon.Information); - } - internal static void LogGenericErrorWithoutStacktrace(string message) { if (string.IsNullOrEmpty(message)) { LogNullError(nameof(message)); @@ -66,6 +57,15 @@ namespace ConfigGenerator { } } + internal static void LogGenericInfoWithoutStacktrace(string message) { + if (string.IsNullOrEmpty(message)) { + LogNullError(nameof(message)); + return; + } + + MessageBox.Show(message, Resources.Information, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + internal static void LogGenericWarning(string message, [CallerMemberName] string previousMethodName = null) { if (string.IsNullOrEmpty(message)) { LogNullError(nameof(message)); @@ -97,4 +97,4 @@ namespace ConfigGenerator { LogGenericErrorWithoutStacktrace(previousMethodName + @"() " + message); } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/MainForm.cs b/ConfigGenerator/MainForm.cs index 980f1d64b..10ca57257 100644 --- a/ConfigGenerator/MainForm.cs +++ b/ConfigGenerator/MainForm.cs @@ -46,6 +46,18 @@ namespace ConfigGenerator { InitializeComponent(); } + private void MainForm_HelpButtonClicked(object sender, CancelEventArgs args) { + if ((sender == null) || (args == null)) { + Logging.LogNullError(nameof(sender) + " || " + nameof(args)); + return; + } + + args.Cancel = true; + Tutorial.OnAction(Tutorial.EPhase.Help); + Process.Start("https://github.com/" + SharedInfo.GithubRepo + "/wiki/Configuration"); + Tutorial.OnAction(Tutorial.EPhase.HelpFinished); + } + private void MainForm_Load(object sender, EventArgs args) { if ((sender == null) || (args == null)) { Logging.LogNullError(nameof(sender) + " || " + nameof(args)); @@ -73,6 +85,24 @@ namespace ConfigGenerator { Tutorial.OnAction(Tutorial.EPhase.Start); } + private void MainForm_Shown(object sender, EventArgs args) { + if ((sender == null) || (args == null)) { + Logging.LogNullError(nameof(sender) + " || " + nameof(args)); + return; + } + + Tutorial.OnAction(Tutorial.EPhase.Shown); + } + + private void MainTab_Deselecting(object sender, TabControlCancelEventArgs args) { + if ((sender == null) || (args == null)) { + Logging.LogNullError(nameof(sender) + " || " + nameof(args)); + return; + } + + OldTab = args.TabPage; + } + private void MainTab_Selected(object sender, TabControlEventArgs args) { if ((sender == null) || (args == null)) { Logging.LogNullError(nameof(sender) + " || " + nameof(args)); @@ -183,35 +213,5 @@ namespace ConfigGenerator { Tutorial.OnAction(Tutorial.EPhase.GlobalConfigOpened); } } - - private void MainTab_Deselecting(object sender, TabControlCancelEventArgs args) { - if ((sender == null) || (args == null)) { - Logging.LogNullError(nameof(sender) + " || " + nameof(args)); - return; - } - - OldTab = args.TabPage; - } - - private void MainForm_Shown(object sender, EventArgs args) { - if ((sender == null) || (args == null)) { - Logging.LogNullError(nameof(sender) + " || " + nameof(args)); - return; - } - - Tutorial.OnAction(Tutorial.EPhase.Shown); - } - - private void MainForm_HelpButtonClicked(object sender, CancelEventArgs args) { - if ((sender == null) || (args == null)) { - Logging.LogNullError(nameof(sender) + " || " + nameof(args)); - return; - } - - args.Cancel = true; - Tutorial.OnAction(Tutorial.EPhase.Help); - Process.Start("https://github.com/" + SharedInfo.GithubRepo + "/wiki/Configuration"); - Tutorial.OnAction(Tutorial.EPhase.HelpFinished); - } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/Program.cs b/ConfigGenerator/Program.cs index 819d780d9..9ffc7fd54 100644 --- a/ConfigGenerator/Program.cs +++ b/ConfigGenerator/Program.cs @@ -34,17 +34,6 @@ namespace ConfigGenerator { internal static class Program { private const string ASFExecutableFile = SharedInfo.ASF + ".exe"; - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main() { - Init(); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new MainForm()); - } - private static void Init() { AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler; @@ -55,7 +44,6 @@ namespace ConfigGenerator { // Allow loading configs from source tree if it's a debug build if (Debugging.IsDebugBuild) { - // Common structure is bin/(x64/)Debug/ArchiSteamFarm.exe, so we allow up to 4 directories up for (byte i = 0; i < 4; i++) { Directory.SetCurrentDirectory(".."); @@ -92,17 +80,23 @@ namespace ConfigGenerator { return; } - Logging.LogGenericErrorWithoutStacktrace( - "Version of ASF and ConfigGenerator doesn't match!" + Environment.NewLine + - "ASF version: " + asfVersion + " | ConfigGenerator version: " + cgVersion + Environment.NewLine + - Environment.NewLine + - "Please use ConfigGenerator from the same ASF release, I'll redirect you to appropriate ASF release..." - ); + Logging.LogGenericErrorWithoutStacktrace("Version of ASF and ConfigGenerator doesn't match!" + Environment.NewLine + "ASF version: " + asfVersion + " | ConfigGenerator version: " + cgVersion + Environment.NewLine + Environment.NewLine + "Please use ConfigGenerator from the same ASF release, I'll redirect you to appropriate ASF release..."); Process.Start("https://github.com/" + SharedInfo.GithubRepo + "/releases/tag/" + asfVersion); Environment.Exit(1); } + /// + /// The main entry point for the application. + /// + [STAThread] + private static void Main() { + Init(); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) { if (args?.ExceptionObject == null) { Logging.LogNullError(nameof(args) + " || " + nameof(args.ExceptionObject)); @@ -121,4 +115,4 @@ namespace ConfigGenerator { Logging.LogGenericException(args.Exception); } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/Properties/AssemblyInfo.cs b/ConfigGenerator/Properties/AssemblyInfo.cs index f83116994..235a684f7 100644 --- a/ConfigGenerator/Properties/AssemblyInfo.cs +++ b/ConfigGenerator/Properties/AssemblyInfo.cs @@ -5,6 +5,7 @@ using ArchiSteamFarm; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. + [assembly: AssemblyTitle(SharedInfo.ServiceName + "-ConfigGenerator")] [assembly: AssemblyDescription(SharedInfo.ServiceDescription)] [assembly: AssemblyConfiguration("")] @@ -17,9 +18,11 @@ using ArchiSteamFarm; // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. + [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM + [assembly: Guid("c3f6fe68-5e75-415e-bea1-1e7c16d6a433")] // Version information for an assembly consists of the following four values: @@ -32,5 +35,6 @@ using ArchiSteamFarm; // 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(SharedInfo.VersionNumber)] -[assembly: AssemblyFileVersion(SharedInfo.VersionNumber)] +[assembly: AssemblyFileVersion(SharedInfo.VersionNumber)] \ No newline at end of file diff --git a/ConfigGenerator/Properties/Settings.settings b/ConfigGenerator/Properties/Settings.settings index 39645652a..e04fc6310 100644 --- a/ConfigGenerator/Properties/Settings.settings +++ b/ConfigGenerator/Properties/Settings.settings @@ -1,7 +1,8 @@  + - + \ No newline at end of file diff --git a/ConfigGenerator/RunTime.cs b/ConfigGenerator/RunTime.cs index e464f48a0..06553095b 100644 --- a/ConfigGenerator/RunTime.cs +++ b/ConfigGenerator/RunTime.cs @@ -26,8 +26,7 @@ using System; namespace ConfigGenerator { internal static class Runtime { - private static readonly Type MonoRuntime = Type.GetType("Mono.Runtime"); - internal static bool IsRunningOnMono => MonoRuntime != null; + private static readonly Type MonoRuntime = Type.GetType("Mono.Runtime"); } -} +} \ No newline at end of file diff --git a/ConfigGenerator/Tutorial.cs b/ConfigGenerator/Tutorial.cs index 97dc5fd7d..5c1008c66 100644 --- a/ConfigGenerator/Tutorial.cs +++ b/ConfigGenerator/Tutorial.cs @@ -24,20 +24,6 @@ namespace ConfigGenerator { internal static class Tutorial { - internal enum EPhase : byte { - Unknown, - Start, - Shown, - Help, - HelpFinished, - BotNickname, - BotNicknameFinished, - BotEnabled, - BotReady, - GlobalConfigOpened, - GlobalConfigReady - } - internal static bool Enabled { private get; set; } = true; private static EPhase NextPhase = EPhase.Start; @@ -99,5 +85,19 @@ namespace ConfigGenerator { NextPhase++; } + + internal enum EPhase : byte { + Unknown, + Start, + Shown, + Help, + HelpFinished, + BotNickname, + BotNicknameFinished, + BotEnabled, + BotReady, + GlobalConfigOpened, + GlobalConfigReady + } } -} +} \ No newline at end of file diff --git a/ConfigGenerator/packages.config b/ConfigGenerator/packages.config index a82f17503..699908060 100644 --- a/ConfigGenerator/packages.config +++ b/ConfigGenerator/packages.config @@ -1,4 +1,5 @@  + diff --git a/GUI/App.config b/GUI/App.config index 731f6de6c..efb4d02fd 100644 --- a/GUI/App.config +++ b/GUI/App.config @@ -1,6 +1,7 @@ - + + - - - + + + \ No newline at end of file diff --git a/GUI/BotStatusForm.cs b/GUI/BotStatusForm.cs index a15e8c6b2..93529d4b5 100644 --- a/GUI/BotStatusForm.cs +++ b/GUI/BotStatusForm.cs @@ -43,4 +43,4 @@ namespace GUI { MainForm.UpdateBotAvatar(Bot.BotName, AvatarPictureBox.Image); } } -} +} \ No newline at end of file diff --git a/GUI/Events.cs b/GUI/Events.cs index f2be04e33..f0b691e12 100644 --- a/GUI/Events.cs +++ b/GUI/Events.cs @@ -26,6 +26,7 @@ using GUI; using SteamKit2; // ReSharper disable once CheckNamespace + namespace ArchiSteamFarm { internal static class Events { internal static void OnBotShutdown() { } @@ -49,4 +50,4 @@ namespace ArchiSteamFarm { form.OnStateUpdated(callback); } } -} +} \ No newline at end of file diff --git a/GUI/FodyWeavers.xml b/GUI/FodyWeavers.xml index 5df39cbf9..c4626742c 100644 --- a/GUI/FodyWeavers.xml +++ b/GUI/FodyWeavers.xml @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/GUI/Logging.cs b/GUI/Logging.cs index ab5958926..7a3af88eb 100644 --- a/GUI/Logging.cs +++ b/GUI/Logging.cs @@ -29,6 +29,7 @@ using NLog.Targets; using NLog.Windows.Forms; // ReSharper disable once CheckNamespace + namespace ArchiSteamFarm { internal static class Logging { private const string GeneralLayout = @"${date:format=yyyy-MM-dd HH\:mm\:ss} | ${level:uppercase=true} | ${logger} | ${message}${onexception:inner= | ${exception:format=toString,Data}}"; @@ -46,10 +47,7 @@ namespace ArchiSteamFarm { } } - MessageBoxTarget messageBoxTarget = new MessageBoxTarget { - Name = "MessageBox", - Layout = GeneralLayout - }; + MessageBoxTarget messageBoxTarget = new MessageBoxTarget { Name = "MessageBox", Layout = GeneralLayout }; LogManager.Configuration.AddTarget(messageBoxTarget); LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Fatal, messageBoxTarget)); @@ -61,11 +59,7 @@ namespace ArchiSteamFarm { return; } - FileTarget fileTarget = new FileTarget("File") { - DeleteOldFileOnStartup = true, - FileName = SharedInfo.LogFile, - Layout = GeneralLayout - }; + FileTarget fileTarget = new FileTarget("File") { DeleteOldFileOnStartup = true, FileName = SharedInfo.LogFile, Layout = GeneralLayout }; LogManager.Configuration.AddTarget(fileTarget); LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, fileTarget)); @@ -74,23 +68,15 @@ namespace ArchiSteamFarm { } internal static void InitFormLogger() { - RichTextBoxTarget formControlTarget = new RichTextBoxTarget { - AutoScroll = true, - ControlName = "LogTextBox", - FormName = "MainForm", - Layout = GeneralLayout, - MaxLines = byte.MaxValue, - Name = "RichTextBox" - }; + RichTextBoxTarget formControlTarget = new RichTextBoxTarget { AutoScroll = true, ControlName = "LogTextBox", FormName = "MainForm", Layout = GeneralLayout, MaxLines = byte.MaxValue, Name = "RichTextBox" }; formControlTarget.RowColoringRules.Add(new RichTextBoxRowColoringRule("level >= LogLevel.Error", "Red", "Black")); formControlTarget.RowColoringRules.Add(new RichTextBoxRowColoringRule("level >= LogLevel.Warn", "Yellow", "Black")); - LogManager.Configuration.AddTarget(formControlTarget); LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, formControlTarget)); LogManager.ReconfigExistingLoggers(); } } -} +} \ No newline at end of file diff --git a/GUI/MainForm.cs b/GUI/MainForm.cs index 56619f6eb..319d5a20f 100644 --- a/GUI/MainForm.cs +++ b/GUI/MainForm.cs @@ -47,44 +47,20 @@ namespace GUI { })); } - private static Bitmap ResizeImage(Image image, int width, int height) { - if ((image == null) || (width <= 0) || (height <= 0)) { - ASF.ArchiLogger.LogNullError(nameof(image) + " || " + nameof(width) + " || " + nameof(height)); - return null; + private void BotListView_SelectedIndexChanged(object sender, EventArgs e) { + if (!string.IsNullOrEmpty(PreviouslySelectedBotName)) { + BotStatusForm.BotForms[PreviouslySelectedBotName].Visible = false; } - Rectangle destRect = new Rectangle(0, 0, width, height); - Bitmap destImage = new Bitmap(width, height); - - destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); - - using (Graphics graphics = Graphics.FromImage(destImage)) { - graphics.CompositingMode = CompositingMode.SourceCopy; - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - - using (ImageAttributes wrapMode = new ImageAttributes()) { - wrapMode.SetWrapMode(WrapMode.TileFlipXY); - graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); - } + if (BotListView.SelectedItems.Count == 0) { + return; } - return destImage; + PreviouslySelectedBotName = BotListView.SelectedItems[0].Text; + BotStatusForm.BotForms[PreviouslySelectedBotName].Visible = true; } - private void MainForm_Resize(object sender, EventArgs e) { - switch (WindowState) { - case FormWindowState.Minimized: - MinimizeIcon.Visible = true; - MinimizeIcon.ShowBalloonTip(5000); - break; - case FormWindowState.Normal: - MinimizeIcon.Visible = false; - break; - } - } + private void MainForm_FormClosed(object sender, FormClosedEventArgs e) => Program.InitShutdownSequence(); private async void MainForm_Load(object sender, EventArgs e) { Logging.InitFormLogger(); @@ -124,10 +100,7 @@ namespace GUI { botStatusForm.TopLevel = false; BotStatusPanel.Controls.Add(botStatusForm); - ListViewItem botListViewItem = new ListViewItem { - ImageIndex = BotIndexes[botName], - Text = botName - }; + ListViewItem botListViewItem = new ListViewItem { ImageIndex = BotIndexes[botName], Text = botName }; BotListView.Items.Add(botListViewItem); } @@ -138,24 +111,48 @@ namespace GUI { } } + private void MainForm_Resize(object sender, EventArgs e) { + switch (WindowState) { + case FormWindowState.Minimized: + MinimizeIcon.Visible = true; + MinimizeIcon.ShowBalloonTip(5000); + break; + case FormWindowState.Normal: + MinimizeIcon.Visible = false; + break; + } + } + private void MinimizeIcon_DoubleClick(object sender, EventArgs e) { Show(); WindowState = FormWindowState.Normal; } - private void MainForm_FormClosed(object sender, FormClosedEventArgs e) => Program.InitShutdownSequence(); - - private void BotListView_SelectedIndexChanged(object sender, EventArgs e) { - if (!string.IsNullOrEmpty(PreviouslySelectedBotName)) { - BotStatusForm.BotForms[PreviouslySelectedBotName].Visible = false; + private static Bitmap ResizeImage(Image image, int width, int height) { + if ((image == null) || (width <= 0) || (height <= 0)) { + ASF.ArchiLogger.LogNullError(nameof(image) + " || " + nameof(width) + " || " + nameof(height)); + return null; } - if (BotListView.SelectedItems.Count == 0) { - return; + Rectangle destRect = new Rectangle(0, 0, width, height); + Bitmap destImage = new Bitmap(width, height); + + destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); + + using (Graphics graphics = Graphics.FromImage(destImage)) { + graphics.CompositingMode = CompositingMode.SourceCopy; + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + + using (ImageAttributes wrapMode = new ImageAttributes()) { + wrapMode.SetWrapMode(WrapMode.TileFlipXY); + graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); + } } - PreviouslySelectedBotName = BotListView.SelectedItems[0].Text; - BotStatusForm.BotForms[PreviouslySelectedBotName].Visible = true; + return destImage; } } -} +} \ No newline at end of file diff --git a/GUI/Program.cs b/GUI/Program.cs index 4e4c9ca17..061385432 100644 --- a/GUI/Program.cs +++ b/GUI/Program.cs @@ -7,21 +7,29 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using GUI; +using SteamKit2; // ReSharper disable once CheckNamespace + namespace ArchiSteamFarm { internal static class Program { internal static GlobalConfig GlobalConfig { get; private set; } internal static GlobalDatabase GlobalDatabase { get; private set; } internal static WebBrowser WebBrowser { get; private set; } + internal static void Exit(int exitCode = 0) { + InitShutdownSequence(); + Environment.Exit(exitCode); + } + internal static string GetUserInput(ASF.EUserInputType userInputType, string botName = SharedInfo.ASF, string extraInformation = null) { return null; // TODO } - internal static void Exit(int exitCode = 0) { - InitShutdownSequence(); - Environment.Exit(exitCode); + internal static void InitShutdownSequence() { + foreach (Bot bot in Bot.Bots.Values.Where(bot => bot.KeepRunning)) { + bot.Stop(); + } } internal static void Restart() { @@ -36,53 +44,6 @@ namespace ArchiSteamFarm { Environment.Exit(0); } - internal static void InitShutdownSequence() { - foreach (Bot bot in Bot.Bots.Values.Where(bot => bot.KeepRunning)) { - bot.Stop(); - } - } - - private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) { - if (args?.ExceptionObject == null) { - ASF.ArchiLogger.LogNullError(nameof(args) + " || " + nameof(args.ExceptionObject)); - return; - } - - ASF.ArchiLogger.LogFatalException((Exception) args.ExceptionObject); - } - - private static void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs args) { - if (args?.Exception == null) { - ASF.ArchiLogger.LogNullError(nameof(args) + " || " + nameof(args.Exception)); - return; - } - - ASF.ArchiLogger.LogFatalException(args.Exception); - } - - private static void InitServices() { - string globalConfigFile = Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalConfigFileName); - - GlobalConfig = GlobalConfig.Load(globalConfigFile); - if (GlobalConfig == null) { - ASF.ArchiLogger.LogGenericError("Global config could not be loaded, please make sure that " + globalConfigFile + " exists and is valid!"); - Exit(1); - } - - string globalDatabaseFile = Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalDatabaseFileName); - - GlobalDatabase = GlobalDatabase.Load(globalDatabaseFile); - if (GlobalDatabase == null) { - ASF.ArchiLogger.LogGenericError("Global database could not be loaded, if issue persists, please remove " + globalDatabaseFile + " in order to recreate database!"); - Exit(1); - } - - ArchiWebHandler.Init(); - WebBrowser.Init(); - - WebBrowser = new WebBrowser(ASF.ArchiLogger); - } - private static void Init() { AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler; @@ -99,7 +60,6 @@ namespace ArchiSteamFarm { // Allow loading configs from source tree if it's a debug build if (Debugging.IsDebugBuild) { - // Common structure is bin/(x64/)Debug/ArchiSteamFarm.exe, so we allow up to 4 directories up for (byte i = 0; i < 4; i++) { Directory.SetCurrentDirectory(".."); @@ -129,15 +89,38 @@ namespace ArchiSteamFarm { Directory.CreateDirectory(SharedInfo.DebugDirectory); - SteamKit2.DebugLog.AddListener(new Debugging.DebugListener()); - SteamKit2.DebugLog.Enabled = true; + DebugLog.AddListener(new Debugging.DebugListener()); + DebugLog.Enabled = true; } Logging.InitEnhancedLoggers(); } + private static void InitServices() { + string globalConfigFile = Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalConfigFileName); + + GlobalConfig = GlobalConfig.Load(globalConfigFile); + if (GlobalConfig == null) { + ASF.ArchiLogger.LogGenericError("Global config could not be loaded, please make sure that " + globalConfigFile + " exists and is valid!"); + Exit(1); + } + + string globalDatabaseFile = Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalDatabaseFileName); + + GlobalDatabase = GlobalDatabase.Load(globalDatabaseFile); + if (GlobalDatabase == null) { + ASF.ArchiLogger.LogGenericError("Global database could not be loaded, if issue persists, please remove " + globalDatabaseFile + " in order to recreate database!"); + Exit(1); + } + + ArchiWebHandler.Init(); + WebBrowser.Init(); + + WebBrowser = new WebBrowser(ASF.ArchiLogger); + } + /// - /// The main entry point for the application. + /// The main entry point for the application. /// [STAThread] private static void Main() { @@ -146,5 +129,23 @@ namespace ArchiSteamFarm { Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } + + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) { + if (args?.ExceptionObject == null) { + ASF.ArchiLogger.LogNullError(nameof(args) + " || " + nameof(args.ExceptionObject)); + return; + } + + ASF.ArchiLogger.LogFatalException((Exception) args.ExceptionObject); + } + + private static void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs args) { + if (args?.Exception == null) { + ASF.ArchiLogger.LogNullError(nameof(args) + " || " + nameof(args.Exception)); + return; + } + + ASF.ArchiLogger.LogFatalException(args.Exception); + } } -} +} \ No newline at end of file diff --git a/GUI/Properties/AssemblyInfo.cs b/GUI/Properties/AssemblyInfo.cs index 783210ed9..60cd4e468 100644 --- a/GUI/Properties/AssemblyInfo.cs +++ b/GUI/Properties/AssemblyInfo.cs @@ -5,6 +5,7 @@ using ArchiSteamFarm; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. + [assembly: AssemblyTitle(SharedInfo.ServiceName + "-GUI")] [assembly: AssemblyDescription(SharedInfo.ServiceDescription)] [assembly: AssemblyConfiguration("")] @@ -17,9 +18,11 @@ using ArchiSteamFarm; // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. + [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM + [assembly: Guid("13949b41-787c-4558-90ae-a9f9e7f86b1f")] // Version information for an assembly consists of the following four values: @@ -32,5 +35,6 @@ using ArchiSteamFarm; // 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(SharedInfo.VersionNumber)] -[assembly: AssemblyFileVersion(SharedInfo.VersionNumber)] +[assembly: AssemblyFileVersion(SharedInfo.VersionNumber)] \ No newline at end of file diff --git a/GUI/Properties/Settings.settings b/GUI/Properties/Settings.settings index 39645652a..e04fc6310 100644 --- a/GUI/Properties/Settings.settings +++ b/GUI/Properties/Settings.settings @@ -1,7 +1,8 @@  + - + \ No newline at end of file diff --git a/GUI/packages.config b/GUI/packages.config index 7412e17dd..574119999 100644 --- a/GUI/packages.config +++ b/GUI/packages.config @@ -1,4 +1,5 @@  +