diff --git a/Hermes/Socket/Requests/CreatePolicyAck.cs b/Hermes/Socket/Requests/CreatePolicyAck.cs index 64e48a0..e077ec7 100644 --- a/Hermes/Socket/Requests/CreatePolicyAck.cs +++ b/Hermes/Socket/Requests/CreatePolicyAck.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using HermesSocketServer.Models; +using HermesSocketServer.Messages; using Serilog; using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Groups; @@ -24,7 +24,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests public void Acknowledge(string requestId, string json, IDictionary? requestData) { - var policy = JsonSerializer.Deserialize(json, _options); + var policy = JsonSerializer.Deserialize(json, _options); if (policy == null) { _logger.Warning($"Policy data failed: null"); @@ -40,7 +40,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests _logger.Debug($"Policy data [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]"); _policies.Set(group.Name, policy.Path, policy.Usage, TimeSpan.FromMilliseconds(policy.Span)); - _logger.Information($"Policy has been updated [policy id: {policy.Id}]"); + _logger.Information($"Policy has been created [policy id: {policy.Id}]"); } } } \ No newline at end of file diff --git a/Hermes/Socket/Requests/CreateRedeemableActionAck.cs b/Hermes/Socket/Requests/CreateRedeemableActionAck.cs new file mode 100644 index 0000000..8aee692 --- /dev/null +++ b/Hermes/Socket/Requests/CreateRedeemableActionAck.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using HermesSocketLibrary.Requests.Messages; +using Serilog; +using TwitchChatTTS.Twitch.Redemptions; + +namespace TwitchChatTTS.Hermes.Socket.Requests +{ + public class CreateRedeemableActionAck : IRequestAck + { + public string Name => "create_redeemable_action"; + private readonly IRedemptionManager _redemptions; + private readonly JsonSerializerOptions _options; + private readonly ILogger _logger; + + public CreateRedeemableActionAck(IRedemptionManager redemptions, JsonSerializerOptions options, ILogger logger) + { + _redemptions = redemptions; + _options = options; + _logger = logger; + } + + public void Acknowledge(string requestId, string json, IDictionary? requestData) + { + var action = JsonSerializer.Deserialize(json, _options); + if (action == null) + { + _logger.Warning($"Redeemable action data received is null."); + return; + } + + _redemptions.Add(action); + _logger.Information($"A new redeemable action has been created [action name: {action.Name}]"); + } + } +} \ No newline at end of file diff --git a/Hermes/Socket/Requests/CreateTTSFilterAck.cs b/Hermes/Socket/Requests/CreateTTSFilterAck.cs index a86ec79..6c2308f 100644 --- a/Hermes/Socket/Requests/CreateTTSFilterAck.cs +++ b/Hermes/Socket/Requests/CreateTTSFilterAck.cs @@ -35,7 +35,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests _logger.Debug($"Filter data [filter id: {filter.Id}][search: {filter.Search}][replace: {filter.Replace}]"); _user.RegexFilters.Add(filter); - _logger.Information($"Filter has been updated [filter id: {filter.Id}]"); + _logger.Information($"Filter has been created [filter id: {filter.Id}]"); } } } \ No newline at end of file diff --git a/Hermes/Socket/Requests/GetPoliciesAck.cs b/Hermes/Socket/Requests/GetPoliciesAck.cs index 1a5a3cc..4b1a4f0 100644 --- a/Hermes/Socket/Requests/GetPoliciesAck.cs +++ b/Hermes/Socket/Requests/GetPoliciesAck.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using HermesSocketServer.Models; +using HermesSocketServer.Messages; using Serilog; using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Groups; @@ -28,7 +28,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests public void Acknowledge(string requestId, string json, IDictionary? requestData) { - var policies = JsonSerializer.Deserialize>(json, _options); + var policies = JsonSerializer.Deserialize>(json, _options); if (policies == null || !policies.Any()) { _logger.Information($"No policies have been found. Policies have been set to default."); diff --git a/Hermes/Socket/Requests/UpdatePolicyAck.cs b/Hermes/Socket/Requests/UpdatePolicyAck.cs index e591988..8b626be 100644 --- a/Hermes/Socket/Requests/UpdatePolicyAck.cs +++ b/Hermes/Socket/Requests/UpdatePolicyAck.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using HermesSocketServer.Models; +using HermesSocketServer.Messages; using Serilog; using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Groups; @@ -24,7 +24,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests public void Acknowledge(string requestId, string json, IDictionary? requestData) { - var policy = JsonSerializer.Deserialize(json, _options); + var policy = JsonSerializer.Deserialize(json, _options); if (policy == null) { _logger.Warning($"Policy data failed: null"); diff --git a/Hermes/Socket/Requests/UpdateRedeemableActionAck.cs b/Hermes/Socket/Requests/UpdateRedeemableActionAck.cs new file mode 100644 index 0000000..6e1dfa1 --- /dev/null +++ b/Hermes/Socket/Requests/UpdateRedeemableActionAck.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using HermesSocketLibrary.Requests.Messages; +using Serilog; +using TwitchChatTTS.Twitch.Redemptions; + +namespace TwitchChatTTS.Hermes.Socket.Requests +{ + public class UpdateRedeemableActionAck : IRequestAck + { + public string Name => "update_redeemable_action"; + private readonly IRedemptionManager _redemptions; + private readonly JsonSerializerOptions _options; + private readonly ILogger _logger; + + public UpdateRedeemableActionAck(IRedemptionManager redemptions, JsonSerializerOptions options, ILogger logger) + { + _redemptions = redemptions; + _options = options; + _logger = logger; + } + + public void Acknowledge(string requestId, string json, IDictionary? requestData) + { + var action = JsonSerializer.Deserialize(json, _options); + if (action == null) + { + _logger.Warning($"Redeemable action data received is null."); + return; + } + + _redemptions.Update(action); + _logger.Information($"A new redeemable action has been created [action name: {action.Name}]"); + } + } +} \ No newline at end of file diff --git a/Twitch/Redemptions/IRedemptionManager.cs b/Twitch/Redemptions/IRedemptionManager.cs index e0d13ef..866636b 100644 --- a/Twitch/Redemptions/IRedemptionManager.cs +++ b/Twitch/Redemptions/IRedemptionManager.cs @@ -4,8 +4,14 @@ namespace TwitchChatTTS.Twitch.Redemptions { public interface IRedemptionManager { + void Add(RedeemableAction action); + void Add(Redemption redemption); Task Execute(RedeemableAction action, string senderDisplayName, long senderId); - IList Get(string twitchRedemptionId); - void Initialize(IEnumerable redemptions, IDictionary actions); + IEnumerable Get(string twitchRedemptionId); + void Initialize(); + bool RemoveAction(string actionName); + bool RemoveRedemption(string redemptionId); + bool Update(Redemption redemption); + bool Update(RedeemableAction action); } } \ No newline at end of file diff --git a/Twitch/Redemptions/RedemptionManager.cs b/Twitch/Redemptions/RedemptionManager.cs index eff0984..708d00a 100644 --- a/Twitch/Redemptions/RedemptionManager.cs +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -16,7 +16,10 @@ namespace TwitchChatTTS.Twitch.Redemptions { public class RedemptionManager : IRedemptionManager { - private readonly IDictionary> _store; + private readonly IDictionary _actions; + private readonly IDictionary _redemptions; + // twitch redemption id -> redemption ids + private readonly IDictionary> _redeems; private readonly ServiceBusCentral _bus; private readonly User _user; private readonly OBSSocketClient _obs; @@ -26,7 +29,7 @@ namespace TwitchChatTTS.Twitch.Redemptions private readonly AudioPlaybackEngine _playback; private readonly ILogger _logger; private readonly Random _random; - private bool _isReady; + private readonly object _lock; public RedemptionManager( @@ -39,7 +42,9 @@ namespace TwitchChatTTS.Twitch.Redemptions AudioPlaybackEngine playback, ILogger logger) { - _store = new Dictionary>(); + _actions = new Dictionary(); + _redemptions = new Dictionary(); + _redeems = new Dictionary>(); _bus = bus; _user = user; _obs = (obs as OBSSocketClient)!; @@ -49,23 +54,110 @@ namespace TwitchChatTTS.Twitch.Redemptions _playback = playback; _logger = logger; _random = new Random(); - _isReady = false; + _lock = new object(); var topic = _bus.GetTopic("redemptions_initiation"); topic.Subscribe(new ServiceBusObserver(data => { - if (data.Value is RedemptionInitiation obj) - Initialize(obj.Redemptions, obj.Actions); + if (data.Value is not RedemptionInitiation init) + return; + + if (init.Actions == null) + init.Actions = new Dictionary(); + if (init.Redemptions == null) + init.Redemptions = new List(); + + if (!init.Actions.Any()) + _logger.Warning("No redeemable actions were loaded."); + + if (!init.Redemptions.Any()) + _logger.Warning("No redemptions were loaded."); + + foreach (var action in init.Actions.Values) + Add(action); + + foreach (var redemption in init.Redemptions) + Add(redemption); + + Initialize(); }, _logger)); } - private void Add(string twitchRedemptionId, RedeemableAction action) + public void Add(RedeemableAction action) { - if (!_store.TryGetValue(twitchRedemptionId, out var actions)) - _store.Add(twitchRedemptionId, actions = new List()); + if (!_actions.ContainsKey(action.Name)) + { + _actions.Add(action.Name, action); + _logger.Debug($"Added redeemable action to redemption manager [action name: {action.Name}]"); + } + else + _logger.Debug($"Redemption manager already has this action stored [action name: {action.Name}]"); + } - actions.Add(action); - _logger.Debug($"Added redemption action [name: {action.Name}][type: {action.Type}]"); + public void Add(Redemption redemption) + { + _redemptions.Add(redemption.Id, redemption); + Add(redemption.TwitchRedemptionId, redemption); + } + + private void Add(string twitchRedemptionId, string redemptionId) + { + lock (_lock) + { + if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems)) + _redeems.Add(twitchRedemptionId, redeems = new List()); + + var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null; + if (item == null) + { + return; + } + + var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null); + bool added = false; + for (int i = 0; i < redeems.Count; i++) + { + if (redeems[i] != null && _redemptions.TryGetValue(redeems[i], out var rr)) + { + if (item.Order > rr.Order) + { + redeems.Insert(i, redemptionId); + added = true; + break; + } + } + } + if (!added) + redeems.Add(redemptionId); + } + _logger.Debug($"Added redemption action [redemption id: {redemptionId}][twitch redemption id: {twitchRedemptionId}]"); + } + + private void Add(string twitchRedemptionId, Redemption item) + { + lock (_lock) + { + if (!_redeems.TryGetValue(twitchRedemptionId, out var redemptionNames)) + _redeems.Add(twitchRedemptionId, redemptionNames = new List()); + + var redemptions = redemptionNames.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null); + bool added = false; + for (int i = 0; i < redemptionNames.Count; i++) + { + if (redemptionNames[i] != null && _redemptions.TryGetValue(redemptionNames[i], out var rr)) + { + if (item.Order > rr.Order) + { + redemptionNames.Insert(i, item.Id); + added = true; + break; + } + } + } + if (!added) + redemptionNames.Add(item.Id); + } + _logger.Debug($"Added redemption action [redemption id: {item.Id}][twitch redemption id: {twitchRedemptionId}]"); } public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId) @@ -228,7 +320,8 @@ namespace TwitchChatTTS.Twitch.Redemptions case "VEADOTUBE_SET_STATE": { var state = _veado.GetStateId(action.Data["state"]); - if (state == null) { + if (state == null) + { _logger.Warning($"Could not find the state named '{action.Data["state"]}'."); break; } @@ -238,7 +331,8 @@ namespace TwitchChatTTS.Twitch.Redemptions case "VEADOTUBE_PUSH_STATE": { var state = _veado.GetStateId(action.Data["state"]); - if (state == null) { + if (state == null) + { _logger.Warning($"Could not find the state named '{action.Data["state"]}'."); break; } @@ -248,7 +342,8 @@ namespace TwitchChatTTS.Twitch.Redemptions case "VEADOTUBE_POP_STATE": { var state = _veado.GetStateId(action.Data["state"]); - if (state == null) { + if (state == null) + { _logger.Warning($"Could not find the state named '{action.Data["state"]}'."); break; } @@ -256,7 +351,7 @@ namespace TwitchChatTTS.Twitch.Redemptions break; } default: - _logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); + _logger.Warning($"Unknown redeemable action has occured [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; } } @@ -266,53 +361,149 @@ namespace TwitchChatTTS.Twitch.Redemptions } } - public IList Get(string twitchRedemptionId) + public IEnumerable Get(string twitchRedemptionId) { - if (!_isReady) - throw new InvalidOperationException("Not ready"); - - if (_store.TryGetValue(twitchRedemptionId, out var actions)) - return actions; - return new List(0); + lock (_lock) + { + if (_redeems.TryGetValue(twitchRedemptionId, out var redemptionIds)) + return redemptionIds.Select(r => _redemptions.TryGetValue(r, out var redemption) ? redemption : null) + .Where(r => r != null) + .Select(r => _actions.TryGetValue(r!.ActionName, out var action) ? action : null) + .Where(a => a != null)!; + } + return []; } - public void Initialize(IEnumerable redemptions, IDictionary actions) + public void Initialize() { - _store.Clear(); + _logger.Debug($"Redemption manager is about to initialize [redemption count: {_redemptions.Count()}][action count: {_actions.Count}]"); - var ordered = redemptions.Where(r => r != null).OrderBy(r => r.Order); - foreach (var redemption in ordered) + lock (_lock) { - if (redemption.ActionName == null) - { - _logger.Warning("Null value found for the action name of a redemption."); - continue; - } + _redeems.Clear(); - try + var ordered = _redemptions.Select(r => r.Value).Where(r => r != null).OrderBy(r => r.Order); + foreach (var redemption in ordered) { - if (actions.TryGetValue(redemption.ActionName, out var action) && action != null) + if (redemption.ActionName == null) { - _logger.Debug($"Fetched a redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); - Add(redemption.RedemptionId, action); + _logger.Warning("Null value found for the action name of a redemption."); + continue; + } + + try + { + if (_actions.ContainsKey(redemption.ActionName)) + { + _logger.Debug($"Fetched a redeemable action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); + Add(redemption.TwitchRedemptionId, redemption.Id); + } + else + _logger.Warning($"Could not find redeemable action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); + } + catch (Exception e) + { + _logger.Error(e, $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); } - else - _logger.Warning($"Could not find redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); - } - catch (Exception e) - { - _logger.Error(e, $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); } } - _isReady = true; _logger.Debug("All redemptions added. Redemption Manager is ready."); } + public bool RemoveAction(string actionName) + { + return _actions.Remove(actionName); + } + + public bool RemoveRedemption(string redemptionId) + { + lock (_lock) + { + if (!_redemptions.TryGetValue(redemptionId, out var redemption)) + { + return false; + } + + _redemptions.Remove(redemptionId); + if (_redeems.TryGetValue(redemption.TwitchRedemptionId, out var redeem)) + { + redeem.Remove(redemptionId); + if (!redeem.Any()) + _redeems.Remove(redemption.TwitchRedemptionId); + return true; + } + } + + return false; + } + private string ReplaceContentText(string content, string username) { return content.Replace("%USER%", username) .Replace("\\n", "\n"); } + + public bool Update(Redemption redemption) + { + lock (_lock) + { + if (_redemptions.TryGetValue(redemption.Id, out var r)) + { + if (r.Order != redemption.Order && _redeems.TryGetValue(redemption.TwitchRedemptionId, out var redeems) && redeems.Count > 1) + { + var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null).ToArray(); + int index = redeems.IndexOf(redemption.Id), i; + if (r.Order < redemption.Order) + { + for (i = index; i >= 1; i--) + { + if (redemptions[i - 1] == null || redemption.Order < redemptions[i - 1]!.Order) + redeems[i] = redeems[i - 1]; + else + break; + } + } + else + { + for (i = index; i < redeems.Count - 1; i++) + { + if (redemptions[i + 1] == null || redemption.Order > redemptions[i + 1]!.Order) + redeems[i] = redeems[i + 1]; + else + break; + } + } + redeems[i] = redemption.Id; + } + else + { + r.ActionName = redemption.ActionName; + r.State = redemption.State; + r.TwitchRedemptionId = redemption.TwitchRedemptionId; + r.Order = redemption.Order; + } + _logger.Debug($"Updated redemption in redemption manager [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]"); + return true; + } + } + + _logger.Warning($"Cannot find redemption by name [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]"); + return false; + } + + public bool Update(RedeemableAction action) + { + if (_actions.TryGetValue(action.Name, out var a)) + { + a.Type = action.Type; + a.Data = action.Data; + _logger.Debug($"Updated redeemable action in redemption manager [action name: {action.Name}]"); + return true; + } + + _logger.Warning($"Cannot find redeemable action by name [action name: {action.Name}]"); + return false; + } } } \ No newline at end of file diff --git a/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs b/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs index 7c9b8c1..0c0270f 100644 --- a/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs +++ b/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs @@ -32,7 +32,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers var actions = _redemptionManager.Get("adbreak_begin"); if (!actions.Any()) { - _logger.Debug($"Found {actions.Count} actions for this Twitch ad break"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch ad break"); foreach (var action in actions) try @@ -59,7 +59,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers actions = _redemptionManager.Get("adbreak_end"); if (!actions.Any()) { - _logger.Debug($"Found {actions.Count} actions for this Twitch ad break"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch ad break"); foreach (var action in actions) try diff --git a/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs b/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs index f203fa2..2d67bb0 100644 --- a/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs @@ -35,7 +35,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _logger.Debug($"No redeemable actions for this redeem was found [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]"); return; } - _logger.Debug($"Found {actions.Count} actions for this Twitch channel point redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch channel point redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]"); foreach (var action in actions) try diff --git a/Twitch/Socket/Handlers/ChannelFollowHandler.cs b/Twitch/Socket/Handlers/ChannelFollowHandler.cs index c6fae0b..d0fb167 100644 --- a/Twitch/Socket/Handlers/ChannelFollowHandler.cs +++ b/Twitch/Socket/Handlers/ChannelFollowHandler.cs @@ -31,7 +31,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _logger.Debug($"No redeemable actions for follow was found"); return; } - _logger.Debug($"Found {actions.Count} actions for this Twitch follow"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch follow"); foreach (var action in actions) try diff --git a/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs b/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs index d1287ec..e203ab1 100644 --- a/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs @@ -41,7 +41,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _logger.Debug($"No redeemable actions for this subscription was found [message: {message.Message.Text}]"); return; } - _logger.Debug($"Found {actions.Count} actions for this Twitch subscription [message: {message.Message.Text}]"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch subscription [message: {message.Message.Text}]"); foreach (var action in actions) try diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs index 9a4ea23..9337a80 100644 --- a/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs +++ b/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs @@ -35,7 +35,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _logger.Debug($"No redeemable actions for this gifted subscription was found"); return; } - _logger.Debug($"Found {actions.Count} actions for this Twitch gifted subscription [gifted: {message.UserLogin}][gifted id: {message.UserId}][Anonymous: {message.IsAnonymous}][cumulative: {message.CumulativeTotal ?? -1}]"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch gifted subscription [gifted: {message.UserLogin}][gifted id: {message.UserId}][Anonymous: {message.IsAnonymous}][cumulative: {message.CumulativeTotal ?? -1}]"); foreach (var action in actions) try diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs index 878c4d2..659220b 100644 --- a/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs @@ -33,7 +33,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _logger.Debug($"No redeemable actions for this subscription was found [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]"); return; } - _logger.Debug($"Found {actions.Count} actions for this Twitch subscription [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]"); + _logger.Debug($"Found {actions.Count()} actions for this Twitch subscription [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]"); foreach (var action in actions) try