diff --git a/ArchiSteamFarm/ASF.cs b/ArchiSteamFarm/ASF.cs index 8f6056b46..de98668d4 100644 --- a/ArchiSteamFarm/ASF.cs +++ b/ArchiSteamFarm/ASF.cs @@ -133,7 +133,7 @@ namespace ArchiSteamFarm { InitEvents(); } - internal static void InitGlobalConfig(GlobalConfig globalConfig) { + internal static async Task InitGlobalConfig(GlobalConfig globalConfig) { if (globalConfig == null) { throw new ArgumentNullException(nameof(globalConfig)); } @@ -159,18 +159,18 @@ namespace ArchiSteamFarm { networkGroupText = "-" + BitConverter.ToString(hashingAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(globalConfig.WebProxyText!))).Replace("-", ""); } - ConfirmationsSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(ConfirmationsSemaphore) + networkGroupText); - GiftsSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(GiftsSemaphore) + networkGroupText); - InventorySemaphore ??= OS.CreateCrossProcessSemaphore(nameof(InventorySemaphore) + networkGroupText); - LoginRateLimitingSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(LoginRateLimitingSemaphore) + networkGroupText); - LoginSemaphore ??= OS.CreateCrossProcessSemaphore(nameof(LoginSemaphore) + networkGroupText); + ConfirmationsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(ConfirmationsSemaphore) + networkGroupText).ConfigureAwait(false); + GiftsSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(GiftsSemaphore) + networkGroupText).ConfigureAwait(false); + InventorySemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(InventorySemaphore) + networkGroupText).ConfigureAwait(false); + LoginRateLimitingSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginRateLimitingSemaphore) + networkGroupText).ConfigureAwait(false); + LoginSemaphore ??= await PluginsCore.GetCrossProcessSemaphore(nameof(LoginSemaphore) + networkGroupText).ConfigureAwait(false); WebLimitingSemaphores ??= new Dictionary(4, StringComparer.OrdinalIgnoreCase) { - { nameof(ArchiWebHandler), (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { ArchiWebHandler.SteamCommunityURL, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(ArchiWebHandler.SteamCommunityURL)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { ArchiWebHandler.SteamHelpURL, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(ArchiWebHandler.SteamHelpURL)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { ArchiWebHandler.SteamStoreURL, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(ArchiWebHandler.SteamStoreURL)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, - { WebAPI.DefaultBaseAddress.Host, (OS.CreateCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(WebAPI)), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) } + { nameof(ArchiWebHandler), (await PluginsCore.GetCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText).ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { ArchiWebHandler.SteamCommunityURL, (await PluginsCore.GetCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(ArchiWebHandler.SteamCommunityURL)).ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { ArchiWebHandler.SteamHelpURL, (await PluginsCore.GetCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(ArchiWebHandler.SteamHelpURL)).ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { ArchiWebHandler.SteamStoreURL, (await PluginsCore.GetCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(ArchiWebHandler.SteamStoreURL)).ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) }, + { WebAPI.DefaultBaseAddress.Host, (await PluginsCore.GetCrossProcessSemaphore(nameof(ArchiWebHandler) + networkGroupText + "-" + nameof(WebAPI)).ConfigureAwait(false), new SemaphoreSlim(WebBrowser.MaxConnections, WebBrowser.MaxConnections)) } }.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase); } diff --git a/ArchiSteamFarm/ArchiHandler.cs b/ArchiSteamFarm/ArchiHandler.cs index c1f6de732..e5e91bd6d 100644 --- a/ArchiSteamFarm/ArchiHandler.cs +++ b/ArchiSteamFarm/ArchiHandler.cs @@ -63,6 +63,35 @@ namespace ArchiSteamFarm { UnifiedTwoFactorService = steamUnifiedMessages.CreateService(); } + [PublicAPI] + public async Task AddFriend(ulong steamID) { + if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected) { + return false; + } + + CPlayer_AddFriend_Request request = new() { steamid = steamID }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedPlayerService.SendMessage(x => x.AddFriend(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return false; + } + + return response.Result == EResult.OK; + } + public override void HandleMsg(IPacketMsg packetMsg) { if (packetMsg == null) { throw new ArgumentNullException(nameof(packetMsg)); @@ -118,6 +147,35 @@ namespace ArchiSteamFarm { } } + [PublicAPI] + public async Task RemoveFriend(ulong steamID) { + if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected) { + return false; + } + + CPlayer_RemoveFriend_Request request = new() { steamid = steamID }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedPlayerService.SendMessage(x => x.RemoveFriend(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return false; + } + + return response.Result == EResult.OK; + } + internal void AckChatMessage(ulong chatGroupID, ulong chatID, uint timestamp) { if (chatGroupID == 0) { throw new ArgumentOutOfRangeException(nameof(chatGroupID)); @@ -196,34 +254,6 @@ namespace ArchiSteamFarm { Client.Send(request); } - internal async Task AddFriend(ulong steamID) { - if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { - throw new ArgumentOutOfRangeException(nameof(steamID)); - } - - if (Client == null) { - throw new InvalidOperationException(nameof(Client)); - } - - if (!Client.IsConnected) { - return false; - } - - CPlayer_AddFriend_Request request = new() { steamid = steamID }; - - SteamUnifiedMessages.ServiceMethodResponse response; - - try { - response = await UnifiedPlayerService.SendMessage(x => x.AddFriend(request)).ToLongRunningTask().ConfigureAwait(false); - } catch (Exception e) { - ArchiLogger.LogGenericWarningException(e); - - return false; - } - - return response.Result == EResult.OK; - } - internal async Task GetClanChatGroupID(ulong steamID) { if ((steamID == 0) || !new SteamID(steamID).IsClanAccount) { throw new ArgumentOutOfRangeException(nameof(steamID)); @@ -554,34 +584,6 @@ namespace ArchiSteamFarm { } } - internal async Task RemoveFriend(ulong steamID) { - if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { - throw new ArgumentOutOfRangeException(nameof(steamID)); - } - - if (Client == null) { - throw new InvalidOperationException(nameof(Client)); - } - - if (!Client.IsConnected) { - return false; - } - - CPlayer_RemoveFriend_Request request = new() { steamid = steamID }; - - SteamUnifiedMessages.ServiceMethodResponse response; - - try { - response = await UnifiedPlayerService.SendMessage(x => x.RemoveFriend(request)).ToLongRunningTask().ConfigureAwait(false); - } catch (Exception e) { - ArchiLogger.LogGenericWarningException(e); - - return false; - } - - return response.Result == EResult.OK; - } - internal void RequestItemAnnouncements() { if (Client == null) { throw new InvalidOperationException(nameof(Client)); diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 2bed1379d..812044505 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -366,6 +366,20 @@ namespace ArchiSteamFarm { return success ? !string.IsNullOrEmpty(steamApiKey) : null; } + [PublicAPI] + public async Task JoinGroup(ulong groupID) { + if ((groupID == 0) || !new SteamID(groupID).IsClanAccount) { + throw new ArgumentOutOfRangeException(nameof(groupID)); + } + + string request = "/gid/" + groupID; + + // Extra entry for sessionID + Dictionary data = new(2, StringComparer.Ordinal) { { "action", "join" } }; + + return await UrlPostWithSession(SteamCommunityURL, request, data: data, session: ESession.CamelCase).ConfigureAwait(false); + } + [PublicAPI] public async Task<(bool Success, HashSet? MobileTradeOfferIDs)> SendTradeOffer(ulong steamID, IReadOnlyCollection? itemsToGive = null, IReadOnlyCollection? itemsToReceive = null, string? token = null, bool forcedSingleOffer = false, ushort itemsPerTrade = Trading.MaxItemsPerTrade) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { @@ -2207,19 +2221,6 @@ namespace ArchiSteamFarm { return true; } - internal async Task JoinGroup(ulong groupID) { - if ((groupID == 0) || !new SteamID(groupID).IsClanAccount) { - throw new ArgumentOutOfRangeException(nameof(groupID)); - } - - string request = "/gid/" + groupID; - - // Extra entry for sessionID - Dictionary data = new(2, StringComparer.Ordinal) { { "action", "join" } }; - - return await UrlPostWithSession(SteamCommunityURL, request, data: data, session: ESession.CamelCase).ConfigureAwait(false); - } - internal async Task MarkInventory() { if (ASF.InventorySemaphore == null) { throw new InvalidOperationException(nameof(ASF.InventorySemaphore)); diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 608e4d9e1..d8c6751f0 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -74,6 +74,9 @@ namespace ArchiSteamFarm { [PublicAPI] public Actions Actions { get; } + [PublicAPI] + public ArchiHandler ArchiHandler { get; } + [JsonIgnore] [PublicAPI] public ArchiLogger ArchiLogger { get; } @@ -126,7 +129,6 @@ namespace ArchiSteamFarm { [PublicAPI] public SteamFriends SteamFriends { get; } - internal readonly ArchiHandler ArchiHandler; internal readonly BotDatabase BotDatabase; internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked; @@ -456,6 +458,30 @@ namespace ArchiSteamFarm { return result; } + [PublicAPI] + public static string GetFilePath(string botName, EFileType fileType) { + if (string.IsNullOrEmpty(botName)) { + throw new ArgumentNullException(nameof(botName)); + } + + if (!Enum.IsDefined(typeof(EFileType), fileType)) { + throw new InvalidEnumArgumentException(nameof(fileType), (int) fileType, typeof(EFileType)); + } + + string botPath = Path.Combine(SharedInfo.ConfigDirectory, botName); + + return fileType switch { + EFileType.Config => botPath + SharedInfo.JsonConfigExtension, + EFileType.Database => botPath + SharedInfo.DatabaseExtension, + EFileType.KeysToRedeem => botPath + SharedInfo.KeysExtension, + EFileType.KeysToRedeemUnused => botPath + SharedInfo.KeysExtension + SharedInfo.KeysUnusedExtension, + EFileType.KeysToRedeemUsed => botPath + SharedInfo.KeysExtension + SharedInfo.KeysUsedExtension, + EFileType.MobileAuthenticator => botPath + SharedInfo.MobileAuthenticatorExtension, + EFileType.SentryFile => botPath + SharedInfo.SentryHashExtension, + _ => throw new ArgumentOutOfRangeException(nameof(fileType)) + }; + } + [PublicAPI] public async Task GetTradeHoldDuration(ulong steamID, ulong tradeID) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { @@ -515,6 +541,106 @@ namespace ArchiSteamFarm { }; } + [PublicAPI] + public async Task SendMessage(ulong steamID, string message) { + if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + if (string.IsNullOrEmpty(message)) { + throw new ArgumentNullException(nameof(message)); + } + + if (!IsConnectedAndLoggedOn) { + return false; + } + + ArchiLogger.LogChatMessage(true, message, steamID: steamID); + + string? steamMessagePrefix = ASF.GlobalConfig != null ? ASF.GlobalConfig.SteamMessagePrefix : GlobalConfig.DefaultSteamMessagePrefix; + ushort maxMessageLength = (ushort) (MaxMessageLength - ReservedMessageLength - (steamMessagePrefix?.Length ?? 0)); + + // We must escape our message prior to sending it + message = Escape(message); + + int i = 0; + + while (i < message.Length) { + int partLength; + bool copyNewline = false; + + // ReSharper disable ArrangeMissingParentheses - conflict with Roslyn + if (message.Length - i > maxMessageLength) { + int lastNewLine = message.LastIndexOf(Environment.NewLine, i + maxMessageLength - Environment.NewLine.Length, maxMessageLength - Environment.NewLine.Length, StringComparison.Ordinal); + + if (lastNewLine > i) { + partLength = lastNewLine - i + Environment.NewLine.Length; + copyNewline = true; + } else { + partLength = maxMessageLength; + } + } else { + partLength = message.Length - i; + } + + // If our message is of max length and ends with a single '\' then we can't split it here, it escapes the next character + if ((partLength >= maxMessageLength) && (message[i + partLength - 1] == '\\') && (message[i + partLength - 2] != '\\')) { + // Instead, we'll cut this message one char short and include the rest in next iteration + partLength--; + } + + // ReSharper restore ArrangeMissingParentheses + string messagePart = message.Substring(i, partLength); + + messagePart = steamMessagePrefix + (i > 0 ? "…" : "") + messagePart + (maxMessageLength < message.Length - i ? "…" : ""); + + await MessagingSemaphore.WaitAsync().ConfigureAwait(false); + + try { + bool sent = false; + + for (byte j = 0; (j < WebBrowser.MaxTries) && !sent && IsConnectedAndLoggedOn; j++) { + // We add a one-second delay here to avoid Steam screwup in form of a ghost notification + // The exact cause is unknown, but the theory is that Steam is confused when dealing with more than 1 message per second from the same user + await Task.Delay(1000).ConfigureAwait(false); + + EResult result = await ArchiHandler.SendMessage(steamID, messagePart).ConfigureAwait(false); + + switch (result) { + case EResult.Busy: + case EResult.Fail: + case EResult.RateLimitExceeded: + case EResult.ServiceUnavailable: + case EResult.Timeout: + await Task.Delay(5000).ConfigureAwait(false); + + continue; + case EResult.OK: + sent = true; + + break; + default: + ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(result), result)); + + return false; + } + } + + if (!sent) { + ArchiLogger.LogGenericWarning(Strings.WarningFailed); + + return false; + } + } finally { + MessagingSemaphore.Release(); + } + + i += partLength - (copyNewline ? Environment.NewLine.Length : 0); + } + + return true; + } + [PublicAPI] public bool SetUserInput(ASF.EUserInputType inputType, string inputValue) { if ((inputType == ASF.EUserInputType.None) || !Enum.IsDefined(typeof(ASF.EUserInputType), inputType)) { @@ -592,7 +718,8 @@ namespace ArchiSteamFarm { } } - internal async Task DeleteAllRelatedFiles() { + [PublicAPI] + public async Task DeleteAllRelatedFiles() { await BotDatabase.MakeReadOnly().ConfigureAwait(false); foreach (string filePath in RelatedFiles.Select(file => file.FilePath).Where(File.Exists)) { @@ -827,29 +954,6 @@ namespace ArchiSteamFarm { return ((productInfoResultSet.Complete && !productInfoResultSet.Failed) || optimisticDiscovery ? appID : 0, DateTime.MinValue, true); } - internal static string GetFilePath(string botName, EFileType fileType) { - if (string.IsNullOrEmpty(botName)) { - throw new ArgumentNullException(nameof(botName)); - } - - if (!Enum.IsDefined(typeof(EFileType), fileType)) { - throw new InvalidEnumArgumentException(nameof(fileType), (int) fileType, typeof(EFileType)); - } - - string botPath = Path.Combine(SharedInfo.ConfigDirectory, botName); - - return fileType switch { - EFileType.Config => botPath + SharedInfo.JsonConfigExtension, - EFileType.Database => botPath + SharedInfo.DatabaseExtension, - EFileType.KeysToRedeem => botPath + SharedInfo.KeysExtension, - EFileType.KeysToRedeemUnused => botPath + SharedInfo.KeysExtension + SharedInfo.KeysUnusedExtension, - EFileType.KeysToRedeemUsed => botPath + SharedInfo.KeysExtension + SharedInfo.KeysUsedExtension, - EFileType.MobileAuthenticator => botPath + SharedInfo.MobileAuthenticatorExtension, - EFileType.SentryFile => botPath + SharedInfo.SentryHashExtension, - _ => throw new ArgumentOutOfRangeException(nameof(fileType)) - }; - } - internal async Task?> GetMarketableAppIDs() => await ArchiWebHandler.GetAppList().ConfigureAwait(false); internal async Task? AppIDs)>?> GetPackagesData(IReadOnlyCollection packageIDs) { @@ -1297,106 +1401,8 @@ namespace ArchiSteamFarm { SteamFriends.RequestFriendInfo(SteamID, EClientPersonaStateFlag.PlayerName | EClientPersonaStateFlag.Presence); } - internal async Task SendMessage(ulong steamID, string message) { - if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { - throw new ArgumentOutOfRangeException(nameof(steamID)); - } - - if (string.IsNullOrEmpty(message)) { - throw new ArgumentNullException(nameof(message)); - } - - if (!IsConnectedAndLoggedOn) { - return false; - } - - ArchiLogger.LogChatMessage(true, message, steamID: steamID); - - string? steamMessagePrefix = ASF.GlobalConfig != null ? ASF.GlobalConfig.SteamMessagePrefix : GlobalConfig.DefaultSteamMessagePrefix; - ushort maxMessageLength = (ushort) (MaxMessageLength - ReservedMessageLength - (steamMessagePrefix?.Length ?? 0)); - - // We must escape our message prior to sending it - message = Escape(message); - - int i = 0; - - while (i < message.Length) { - int partLength; - bool copyNewline = false; - - // ReSharper disable ArrangeMissingParentheses - conflict with Roslyn - if (message.Length - i > maxMessageLength) { - int lastNewLine = message.LastIndexOf(Environment.NewLine, i + maxMessageLength - Environment.NewLine.Length, maxMessageLength - Environment.NewLine.Length, StringComparison.Ordinal); - - if (lastNewLine > i) { - partLength = lastNewLine - i + Environment.NewLine.Length; - copyNewline = true; - } else { - partLength = maxMessageLength; - } - } else { - partLength = message.Length - i; - } - - // If our message is of max length and ends with a single '\' then we can't split it here, it escapes the next character - if ((partLength >= maxMessageLength) && (message[i + partLength - 1] == '\\') && (message[i + partLength - 2] != '\\')) { - // Instead, we'll cut this message one char short and include the rest in next iteration - partLength--; - } - - // ReSharper restore ArrangeMissingParentheses - string messagePart = message.Substring(i, partLength); - - messagePart = steamMessagePrefix + (i > 0 ? "…" : "") + messagePart + (maxMessageLength < message.Length - i ? "…" : ""); - - await MessagingSemaphore.WaitAsync().ConfigureAwait(false); - - try { - bool sent = false; - - for (byte j = 0; (j < WebBrowser.MaxTries) && !sent && IsConnectedAndLoggedOn; j++) { - // We add a one-second delay here to avoid Steam screwup in form of a ghost notification - // The exact cause is unknown, but the theory is that Steam is confused when dealing with more than 1 message per second from the same user - await Task.Delay(1000).ConfigureAwait(false); - - EResult result = await ArchiHandler.SendMessage(steamID, messagePart).ConfigureAwait(false); - - switch (result) { - case EResult.Busy: - case EResult.Fail: - case EResult.RateLimitExceeded: - case EResult.ServiceUnavailable: - case EResult.Timeout: - await Task.Delay(5000).ConfigureAwait(false); - - continue; - case EResult.OK: - sent = true; - - break; - default: - ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(result), result)); - - return false; - } - } - - if (!sent) { - ArchiLogger.LogGenericWarning(Strings.WarningFailed); - - return false; - } - } finally { - MessagingSemaphore.Release(); - } - - i += partLength - (copyNewline ? Environment.NewLine.Length : 0); - } - - return true; - } - - internal async Task SendMessage(ulong chatGroupID, ulong chatID, string message) { + [PublicAPI] + public async Task SendMessage(ulong chatGroupID, ulong chatID, string message) { if (chatGroupID == 0) { throw new ArgumentOutOfRangeException(nameof(chatGroupID)); } @@ -3468,7 +3474,7 @@ namespace ArchiSteamFarm { return (true, steamParentalCode); } - internal enum EFileType : byte { + public enum EFileType : byte { Config, Database, KeysToRedeem, diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index f0769f873..ef42f75eb 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -200,9 +200,39 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] public bool ShutdownOnFarmingFinished { get; private set; } = DefaultShutdownOnFarmingFinished; + [JsonProperty] + public string? SteamLogin { + internal get => BackingSteamLogin; + + set { + IsSteamLoginSet = true; + BackingSteamLogin = value; + } + } + [JsonProperty(Required = Required.DisallowNull)] public ulong SteamMasterClanID { get; private set; } = DefaultSteamMasterClanID; + [JsonProperty] + public string? SteamParentalCode { + internal get => BackingSteamParentalCode; + + set { + IsSteamParentalCodeSet = true; + BackingSteamParentalCode = value; + } + } + + [JsonProperty] + public string? SteamPassword { + internal get => BackingSteamPassword; + + set { + IsSteamPasswordSet = true; + BackingSteamPassword = value; + } + } + [JsonProperty] public string? SteamTradeToken { get; private set; } = DefaultSteamTradeToken; @@ -262,36 +292,6 @@ namespace ArchiSteamFarm { internal bool ShouldSerializeHelperProperties { private get; set; } = true; internal bool ShouldSerializeSensitiveDetails { private get; set; } - [JsonProperty] - internal string? SteamLogin { - get => BackingSteamLogin; - - set { - IsSteamLoginSet = true; - BackingSteamLogin = value; - } - } - - [JsonProperty] - internal string? SteamParentalCode { - get => BackingSteamParentalCode; - - set { - IsSteamParentalCodeSet = true; - BackingSteamParentalCode = value; - } - } - - [JsonProperty] - internal string? SteamPassword { - get => BackingSteamPassword; - - set { - IsSteamPasswordSet = true; - BackingSteamPassword = value; - } - } - private string? BackingSteamLogin = DefaultSteamLogin; private string? BackingSteamParentalCode = DefaultSteamParentalCode; private string? BackingSteamPassword = DefaultSteamPassword; @@ -314,6 +314,40 @@ namespace ArchiSteamFarm { [JsonConstructor] internal BotConfig() { } + [PublicAPI] + public static async Task Write(string filePath, BotConfig botConfig) { + if (string.IsNullOrEmpty(filePath)) { + throw new ArgumentNullException(nameof(filePath)); + } + + if (botConfig == null) { + throw new ArgumentNullException(nameof(botConfig)); + } + + string json = JsonConvert.SerializeObject(botConfig, Formatting.Indented); + string newFilePath = filePath + ".new"; + + await WriteSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await RuntimeCompatibility.File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); + + if (File.Exists(filePath)) { + File.Replace(newFilePath, filePath, null); + } else { + File.Move(newFilePath, filePath); + } + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + + return false; + } finally { + WriteSemaphore.Release(); + } + + return true; + } + internal (bool Valid, string? ErrorMessage) CheckValidation() { if (BotBehaviour > EBotBehaviour.All) { return (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(BotBehaviour), BotBehaviour)); @@ -423,39 +457,6 @@ namespace ArchiSteamFarm { return botConfig; } - internal static async Task Write(string filePath, BotConfig botConfig) { - if (string.IsNullOrEmpty(filePath)) { - throw new ArgumentNullException(nameof(filePath)); - } - - if (botConfig == null) { - throw new ArgumentNullException(nameof(botConfig)); - } - - string json = JsonConvert.SerializeObject(botConfig, Formatting.Indented); - string newFilePath = filePath + ".new"; - - await WriteSemaphore.WaitAsync().ConfigureAwait(false); - - try { - await RuntimeCompatibility.File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); - - if (File.Exists(filePath)) { - File.Replace(newFilePath, filePath, null); - } else { - File.Move(newFilePath, filePath); - } - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - - return false; - } finally { - WriteSemaphore.Release(); - } - - return true; - } - public enum EAccess : byte { None, FamilySharing, diff --git a/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs b/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs index e8b02539a..849dba3cc 100644 --- a/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs +++ b/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs @@ -20,9 +20,11 @@ // limitations under the License. using System.Threading.Tasks; +using JetBrains.Annotations; namespace ArchiSteamFarm.Helpers { - internal interface ICrossProcessSemaphore { + [PublicAPI] + public interface ICrossProcessSemaphore { internal void Release(); internal Task WaitAsync(); internal Task WaitAsync(int millisecondsTimeout); diff --git a/ArchiSteamFarm/OS.cs b/ArchiSteamFarm/OS.cs index 88ddbbd7c..dde2f63af 100644 --- a/ArchiSteamFarm/OS.cs +++ b/ArchiSteamFarm/OS.cs @@ -29,7 +29,6 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; namespace ArchiSteamFarm { @@ -64,14 +63,12 @@ namespace ArchiSteamFarm { } } - internal static ICrossProcessSemaphore CreateCrossProcessSemaphore(string objectName) { + internal static string GetOsResourceName(string objectName) { if (string.IsNullOrEmpty(objectName)) { throw new ArgumentNullException(nameof(objectName)); } - string resourceName = GetOsResourceName(objectName); - - return new CrossProcessFileBasedSemaphore(resourceName); + return SharedInfo.AssemblyName + "-" + objectName; } internal static void Init(GlobalConfig.EOptimizationMode optimizationMode) { @@ -195,14 +192,6 @@ namespace ArchiSteamFarm { #endif } - private static string GetOsResourceName(string objectName) { - if (string.IsNullOrEmpty(objectName)) { - throw new ArgumentNullException(nameof(objectName)); - } - - return SharedInfo.AssemblyName + "-" + objectName; - } - private static void WindowsDisableQuickEditMode() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; diff --git a/ArchiSteamFarm/Plugins/ICrossProcessSemaphoreProvider.cs b/ArchiSteamFarm/Plugins/ICrossProcessSemaphoreProvider.cs new file mode 100644 index 000000000..eb29e2fe0 --- /dev/null +++ b/ArchiSteamFarm/Plugins/ICrossProcessSemaphoreProvider.cs @@ -0,0 +1,36 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2020 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Threading.Tasks; +using ArchiSteamFarm.Helpers; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Plugins { + [PublicAPI] + public interface ICrossProcessSemaphoreProvider : IPlugin { + /// + /// ASF will call this method when initializing instance of for its internal limiters. + /// + /// Unique resource name provided by ASF for identification purposes. + /// Concrete implementation of providing required functionality. It's allowed to return null if you want to use ASF's default implementation for specified resource instead. + Task GetCrossProcessSemaphore(string resourceName); + } +} diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index b83aadbd5..b77ba5c33 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -30,6 +30,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Json; using ArchiSteamFarm.Localization; using Newtonsoft.Json.Linq; @@ -86,6 +87,30 @@ namespace ArchiSteamFarm.Plugins { return changeNumberToStartFrom == uint.MaxValue ? 0 : changeNumberToStartFrom; } + internal static async Task GetCrossProcessSemaphore(string objectName) { + if (string.IsNullOrEmpty(objectName)) { + throw new ArgumentNullException(nameof(objectName)); + } + + string resourceName = OS.GetOsResourceName(objectName); + + if ((ActivePlugins == null) || (ActivePlugins.Count == 0)) { + return new CrossProcessFileBasedSemaphore(resourceName); + } + + IList responses; + + try { + responses = await Utilities.InParallel(ActivePlugins.OfType().Select(plugin => plugin.GetCrossProcessSemaphore(resourceName))).ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + + return new CrossProcessFileBasedSemaphore(resourceName); + } + + return responses.FirstOrDefault(response => response != null) ?? new CrossProcessFileBasedSemaphore(resourceName); + } + internal static bool InitPlugins() { if (ActivePlugins != null) { return false; diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index 0919505c6..04caa4ba8 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -227,7 +227,7 @@ namespace ArchiSteamFarm { globalConfig = new GlobalConfig(); } - ASF.InitGlobalConfig(globalConfig); + await ASF.InitGlobalConfig(globalConfig).ConfigureAwait(false); if (Debugging.IsDebugConfigured) { ASF.ArchiLogger.LogGenericDebug(globalConfigFile + ": " + JsonConvert.SerializeObject(ASF.GlobalConfig, Formatting.Indented));