diff --git a/ArchiSteamFarm/ArchiHandler.cs b/ArchiSteamFarm/ArchiHandler.cs index f9a5bf3d8..7030e7835 100644 --- a/ArchiSteamFarm/ArchiHandler.cs +++ b/ArchiSteamFarm/ArchiHandler.cs @@ -785,6 +785,76 @@ namespace ArchiSteamFarm { } } + public sealed class UserNotificationsCallback : CallbackMsg { + internal readonly Dictionary Notifications; + + internal UserNotificationsCallback([JetBrains.Annotations.NotNull] JobID jobID, [JetBrains.Annotations.NotNull] CMsgClientUserNotifications msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + + if (msg.notifications == null) { + // We might get null body here, and that means there are no notifications related to trading + Notifications = new Dictionary(1) { { EUserNotification.Trading, 0 } }; + + return; + } + + Notifications = new Dictionary(msg.notifications.Count); + + foreach (CMsgClientUserNotifications.Notification notification in msg.notifications) { + EUserNotification type = (EUserNotification) notification.user_notification_type; + + switch (type) { + case EUserNotification.AccountAlerts: + case EUserNotification.Chat: + case EUserNotification.Comments: + case EUserNotification.GameTurns: + case EUserNotification.Gifts: + case EUserNotification.HelpRequestReplies: + case EUserNotification.Invites: + case EUserNotification.Items: + case EUserNotification.ModeratorMessages: + case EUserNotification.Trading: + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(type), type)); + + break; + } + + Notifications[type] = notification.count; + } + } + + internal UserNotificationsCallback([JetBrains.Annotations.NotNull] JobID jobID, [JetBrains.Annotations.NotNull] CMsgClientItemAnnouncements msg) { + if ((jobID == null) || (msg == null)) { + throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); + } + + JobID = jobID; + Notifications = new Dictionary(1) { { EUserNotification.Items, msg.count_new_items } }; + } + + [PublicAPI] + public enum EUserNotification : byte { + Unknown, + Trading, + GameTurns, + ModeratorMessages, + Comments, + Items, + Invites, + Unknown7, // No clue what 7 stands for, and I doubt we can find out + Gifts, + Chat, + HelpRequestReplies, + AccountAlerts + } + } + internal sealed class PlayingSessionStateCallback : CallbackMsg { internal readonly bool PlayingBlocked; @@ -829,74 +899,6 @@ namespace ArchiSteamFarm { } } - internal sealed class UserNotificationsCallback : CallbackMsg { - internal readonly Dictionary Notifications; - - internal UserNotificationsCallback([JetBrains.Annotations.NotNull] JobID jobID, [JetBrains.Annotations.NotNull] CMsgClientUserNotifications msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - - // We might get null body here, and that means there are no notifications related to trading - Notifications = new Dictionary { { EUserNotification.Trading, 0 } }; - - if (msg.notifications == null) { - return; - } - - foreach (CMsgClientUserNotifications.Notification notification in msg.notifications) { - EUserNotification type = (EUserNotification) notification.user_notification_type; - - switch (type) { - case EUserNotification.AccountAlerts: - case EUserNotification.Chat: - case EUserNotification.Comments: - case EUserNotification.GameTurns: - case EUserNotification.Gifts: - case EUserNotification.HelpRequestReplies: - case EUserNotification.Invites: - case EUserNotification.Items: - case EUserNotification.ModeratorMessages: - case EUserNotification.Trading: - break; - default: - ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(type), type)); - - continue; - } - - Notifications[type] = notification.count; - } - } - - internal UserNotificationsCallback([JetBrains.Annotations.NotNull] JobID jobID, [JetBrains.Annotations.NotNull] CMsgClientItemAnnouncements msg) { - if ((jobID == null) || (msg == null)) { - throw new ArgumentNullException(nameof(jobID) + " || " + nameof(msg)); - } - - JobID = jobID; - Notifications = new Dictionary(1) { { EUserNotification.Items, msg.count_new_items } }; - } - - [PublicAPI] - internal enum EUserNotification : byte { - Unknown, - Trading, - GameTurns, - ModeratorMessages, - Comments, - Items, - Invites, - Unknown7, // No clue what 7 stands for, and I doubt we can find out - Gifts, - Chat, - HelpRequestReplies, - AccountAlerts - } - } - internal sealed class VanityURLChangedCallback : CallbackMsg { internal readonly string VanityURL; diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 8bb7ec8d7..a4f3be675 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -118,6 +118,7 @@ namespace ArchiSteamFarm { private readonly Timer HeartBeatTimer; private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim MessagingSemaphore = new SemaphoreSlim(1, 1); + private readonly ConcurrentDictionary PastNotifications = new ConcurrentDictionary(); private readonly SemaphoreSlim PICSSemaphore = new SemaphoreSlim(1, 1); private readonly Statistics Statistics; private readonly SteamClient SteamClient; @@ -182,9 +183,7 @@ namespace ArchiSteamFarm { private string DeviceID; private bool FirstTradeSent; private Timer GamesRedeemerInBackgroundTimer; - private uint GiftsCount; private byte HeartBeatFailures; - private uint ItemsCount; private EResult LastLogOnResult; private DateTime LastLogonSessionReplaced; private bool LibraryLocked; @@ -194,7 +193,6 @@ namespace ArchiSteamFarm { private Timer SendItemsTimer; private bool SteamParentalActive = true; private SteamSaleEvent SteamSaleEvent; - private uint TradesCount; private string TwoFactorCode; private byte TwoFactorCodeFailures; @@ -2045,7 +2043,7 @@ namespace ArchiSteamFarm { EResult lastLogOnResult = LastLogOnResult; LastLogOnResult = EResult.Invalid; - ItemsCount = TradesCount = HeartBeatFailures = 0; + HeartBeatFailures = 0; SteamParentalActive = true; StopConnectionFailureTimer(); StopPlayingWasBlockedTimer(); @@ -2053,6 +2051,7 @@ namespace ArchiSteamFarm { ArchiLogger.LogGenericInfo(Strings.BotDisconnected); OwnedPackageIDs.Clear(); + PastNotifications.Clear(); Actions.OnDisconnected(); ArchiWebHandler.OnDisconnected(); @@ -2720,44 +2719,48 @@ namespace ArchiSteamFarm { return; } + HashSet newPluginNotifications = new HashSet(); + foreach ((ArchiHandler.UserNotificationsCallback.EUserNotification notification, uint count) in callback.Notifications) { + bool newNotification; + + if (count > 0) { + newNotification = !PastNotifications.TryGetValue(notification, out uint previousCount) || (count > previousCount); + PastNotifications[notification] = count; + + if (newNotification) { + newPluginNotifications.Add(notification); + } + } else { + newNotification = false; + PastNotifications.TryRemove(notification, out _); + } + + ArchiLogger.LogGenericTrace(notification + " = " + count); + switch (notification) { - case ArchiHandler.UserNotificationsCallback.EUserNotification.Gifts: - bool newGifts = count > GiftsCount; - GiftsCount = count; + case ArchiHandler.UserNotificationsCallback.EUserNotification.Gifts when newNotification && BotConfig.AcceptGifts: + Utilities.InBackground(Actions.AcceptDigitalGiftCards); - if (newGifts && BotConfig.AcceptGifts) { - ArchiLogger.LogGenericTrace(nameof(ArchiHandler.UserNotificationsCallback.EUserNotification.Gifts)); - Utilities.InBackground(Actions.AcceptDigitalGiftCards); + break; + case ArchiHandler.UserNotificationsCallback.EUserNotification.Items when newNotification: + Utilities.InBackground(CardsFarmer.OnNewItemsNotification); + + if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.DismissInventoryNotifications)) { + Utilities.InBackground(ArchiWebHandler.MarkInventory); } break; - case ArchiHandler.UserNotificationsCallback.EUserNotification.Items: - bool newItems = count > ItemsCount; - ItemsCount = count; - - if (newItems) { - ArchiLogger.LogGenericTrace(nameof(ArchiHandler.UserNotificationsCallback.EUserNotification.Items)); - Utilities.InBackground(CardsFarmer.OnNewItemsNotification); - - if (BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.DismissInventoryNotifications)) { - Utilities.InBackground(ArchiWebHandler.MarkInventory); - } - } - - break; - case ArchiHandler.UserNotificationsCallback.EUserNotification.Trading: - bool newTrades = count > TradesCount; - TradesCount = count; - - if (newTrades) { - ArchiLogger.LogGenericTrace(nameof(ArchiHandler.UserNotificationsCallback.EUserNotification.Trading)); - Utilities.InBackground(Trading.OnNewTrade); - } + case ArchiHandler.UserNotificationsCallback.EUserNotification.Trading when newNotification: + Utilities.InBackground(Trading.OnNewTrade); break; } } + + if (newPluginNotifications.Count > 0) { + Utilities.InBackground(() => PluginsCore.OnBotUserNotifications(this, newPluginNotifications)); + } } private void OnVanityURLChangedCallback(ArchiHandler.VanityURLChangedCallback callback) { diff --git a/ArchiSteamFarm/Plugins/IBotUserNotifications.cs b/ArchiSteamFarm/Plugins/IBotUserNotifications.cs new file mode 100644 index 000000000..d7b6b9c56 --- /dev/null +++ b/ArchiSteamFarm/Plugins/IBotUserNotifications.cs @@ -0,0 +1,35 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2019 Ɓ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.Collections.Generic; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Plugins { + [PublicAPI] + public interface IBotUserNotifications : IPlugin { + /// + /// ASF will call this method when number of notifications for one or more notification types changes. + /// + /// Bot object related to this callback. + /// Collection containing those notification types that are new (that is, when new count > previous count of that notification type). + void OnBotUserNotifications([NotNull] Bot bot, [NotNull] IReadOnlyCollection newNotifications); + } +} diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 57321afc3..9914eca24 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -234,6 +234,24 @@ namespace ArchiSteamFarm.Plugins { } } + internal static async Task OnBotFarmingFinished(Bot bot, bool farmedSomething) { + if (bot == null) { + ASF.ArchiLogger.LogNullError(nameof(bot)); + + return; + } + + if (!HasActivePluginsLoaded) { + return; + } + + try { + await Utilities.InParallel(ActivePlugins.OfType().Select(plugin => Task.Run(() => plugin.OnBotFarmingFinished(bot, farmedSomething)))).ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + } + internal static async Task OnBotFarmingStarted(Bot bot) { if (bot == null) { ASF.ArchiLogger.LogNullError(nameof(bot)); @@ -270,24 +288,6 @@ namespace ArchiSteamFarm.Plugins { } } - internal static async Task OnBotFarmingFinished(Bot bot, bool farmedSomething) { - if (bot == null) { - ASF.ArchiLogger.LogNullError(nameof(bot)); - - return; - } - - if (!HasActivePluginsLoaded) { - return; - } - - try { - await Utilities.InParallel(ActivePlugins.OfType().Select(plugin => Task.Run(() => plugin.OnBotFarmingFinished(bot, farmedSomething)))).ConfigureAwait(false); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } - } - internal static async Task OnBotFriendRequest(Bot bot, ulong steamID) { if ((bot == null) || (steamID == 0)) { ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(steamID)); @@ -475,6 +475,24 @@ namespace ArchiSteamFarm.Plugins { } } + internal static async Task OnBotUserNotifications(Bot bot, IReadOnlyCollection newNotifications) { + if ((bot == null) || (newNotifications == null) || (newNotifications.Count == 0)) { + ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(newNotifications)); + + return; + } + + if (!HasActivePluginsLoaded) { + return; + } + + try { + await Utilities.InParallel(ActivePlugins.OfType().Select(plugin => Task.Run(() => plugin.OnBotUserNotifications(bot, newNotifications)))).ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + } + private static HashSet LoadAssembliesFrom(string path) { if (string.IsNullOrEmpty(path)) { ASF.ArchiLogger.LogNullError(nameof(path));