diff --git a/Startup.cs b/Startup.cs index 1705714..4b00f08 100644 --- a/Startup.cs +++ b/Startup.cs @@ -202,6 +202,7 @@ s.AddKeyedSingleton("twitch-notifica s.AddKeyedSingleton("twitch-notifications"); s.AddKeyedSingleton("twitch-notifications"); s.AddKeyedSingleton("twitch-notifications"); +s.AddKeyedSingleton("twitch-notifications"); s.AddKeyedSingleton("twitch-notifications"); // hermes websocket diff --git a/Twitch/Redemptions/IRedemptionManager.cs b/Twitch/Redemptions/IRedemptionManager.cs index 866636b..872d0da 100644 --- a/Twitch/Redemptions/IRedemptionManager.cs +++ b/Twitch/Redemptions/IRedemptionManager.cs @@ -1,4 +1,5 @@ using HermesSocketLibrary.Requests.Messages; +using TwitchChatTTS.Twitch.Socket.Messages; namespace TwitchChatTTS.Twitch.Redemptions { @@ -6,7 +7,7 @@ namespace TwitchChatTTS.Twitch.Redemptions { void Add(RedeemableAction action); void Add(Redemption redemption); - Task Execute(RedeemableAction action, string senderDisplayName, long senderId); + Task Execute(RedeemableAction action, string senderDisplayName, long senderId, string senderMessage); IEnumerable Get(string twitchRedemptionId); void Initialize(); bool RemoveAction(string actionName); diff --git a/Twitch/Redemptions/RedemptionManager.cs b/Twitch/Redemptions/RedemptionManager.cs index c5a4a67..d19eb53 100644 --- a/Twitch/Redemptions/RedemptionManager.cs +++ b/Twitch/Redemptions/RedemptionManager.cs @@ -166,7 +166,7 @@ namespace TwitchChatTTS.Twitch.Redemptions _logger.Debug($"Added redemption action [redemption id: {item.Id}][twitch redemption id: {twitchRedemptionId}]"); } - public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId) + public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId, string senderMessage) { _logger.Debug($"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); @@ -190,7 +190,7 @@ namespace TwitchChatTTS.Twitch.Redemptions if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); - await File.WriteAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName)); + await File.WriteAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName, senderId, senderMessage)); _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; } @@ -204,7 +204,7 @@ namespace TwitchChatTTS.Twitch.Redemptions if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); - await File.AppendAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName)); + await File.AppendAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName, senderId, senderMessage)); _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); break; } @@ -462,9 +462,14 @@ namespace TwitchChatTTS.Twitch.Redemptions return false; } - private string ReplaceContentText(string content, string username) + private string ReplaceContentText(string content, string chatter, long chatterId, string message) { - return content.Replace("%USER%", username) + return content.Replace("%USER%", chatter) + .Replace("%chatter%", chatter) + .Replace("%chatterid%", chatterId.ToString()) + .Replace("%broadcaster%", _user.TwitchUsername) + .Replace("%broadcasterid%", _user.TwitchUserId.ToString()) + .Replace("%message%", message) .Replace("\\n", "\n"); } diff --git a/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs b/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs index 77b112d..5dd3eb9 100644 --- a/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs +++ b/Twitch/Socket/Handlers/ChannelAdBreakBeginHandler.cs @@ -37,7 +37,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId)); + await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId), string.Empty); } catch (Exception ex) { @@ -71,7 +71,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId)); + await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId), string.Empty); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs b/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs index beb7a0e..236d5f4 100644 --- a/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs +++ b/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs @@ -1,4 +1,5 @@ using Serilog; +using TwitchChatTTS.Bus; using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Groups; @@ -18,6 +19,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers private readonly IGroupPermissionManager _permissionManager; private readonly IUsagePolicy _permissionPolicy; private readonly IChatterGroupManager _chatterGroupManager; + private readonly ServiceBusCentral _bus; private readonly ILogger _logger; @@ -27,6 +29,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers IGroupPermissionManager permissionManager, IUsagePolicy permissionPolicy, IChatterGroupManager chatterGroupManager, + ServiceBusCentral bus, User user, ILogger logger ) @@ -36,12 +39,9 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _commands = commands; _permissionManager = permissionManager; _permissionPolicy = permissionPolicy; - _chatterGroupManager = chatterGroupManager; + _bus = bus; _logger = logger; - - _permissionPolicy.Set("everyone", "tts", 100, TimeSpan.FromSeconds(15)); - _permissionPolicy.Set("everyone", "tts.chat.messages.read", 3, TimeSpan.FromMilliseconds(15000)); } @@ -58,9 +58,16 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers var groups = GetGroups(message.Badges, chatterId); var bits = GetTotalBits(fragments); - var commandResult = await CheckForChatCommand(message.Message.Text, message, groups); - if (commandResult != ChatCommandResult.Unknown) - return; + if (message.ChannelPointsCustomRewardId == null) + { + var commandResult = await CheckForChatCommand(message.Message.Text, message, groups); + if (commandResult != ChatCommandResult.Unknown) + return; + } + else + { + _bus.Send(this, "chat_message_redemption", message); + } string permission = GetPermissionPath(message.ChannelPointsCustomRewardId, bits); if (!HasPermission(chatterId, groups, permission)) @@ -106,7 +113,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers { var defaultGroups = new string[] { "everyone" }; var badgesGroups = badges.Select(b => b.SetId).Select(GetGroupNameByBadgeName); - var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId); + var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId).ToArray(); return defaultGroups.Union(badgesGroups).Union(customGroups); } diff --git a/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs b/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs index 2d67bb0..a500452 100644 --- a/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs @@ -1,4 +1,5 @@ using Serilog; +using TwitchChatTTS.Bus; using TwitchChatTTS.Twitch.Redemptions; using TwitchChatTTS.Twitch.Socket.Messages; @@ -8,16 +9,38 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers { public string Name => "channel.channel_points_custom_reward_redemption.add"; + private readonly IDictionary _messages; private readonly IRedemptionManager _redemptionManager; + private readonly ServiceBusCentral _bus; private readonly ILogger _logger; public ChannelCustomRedemptionHandler( IRedemptionManager redemptionManager, + ServiceBusCentral bus, ILogger logger ) { + _messages = new Dictionary(); _redemptionManager = redemptionManager; + _bus = bus; _logger = logger; + + var topic = _bus.GetTopic("chat_message_redemption"); + topic.Subscribe((d) => + { + var message = d.Value as ChannelChatMessage; + + if (message != null && !string.IsNullOrEmpty(message.ChannelPointsCustomRewardId)) + { + _messages.Add(message.ChannelPointsCustomRewardId, message); + + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(5000)); + _messages.Remove(message.ChannelPointsCustomRewardId); + }); + } + }); } public async Task Execute(TwitchWebsocketClient sender, object data) @@ -37,10 +60,32 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers } _logger.Debug($"Found {actions.Count()} actions for this Twitch channel point redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]"); + ChannelChatMessage? chatMessage = null; + if (actions.Any(a => a.HasMessage)) + { + if (!_messages.TryGetValue(message.Reward.Id, out chatMessage)) + { + DateTime start = DateTime.Now; + _logger.Debug("Waiting on redemption message..."); + while (DateTime.Now - start < TimeSpan.FromMilliseconds(1000) && !_messages.ContainsKey(message.Reward.Id)) + await Task.Delay(100); + + if (!_messages.TryGetValue(message.Reward.Id, out chatMessage)) + { + _logger.Warning("Chat message was not found within a second of the channel redemption being received. Skipping all redeemable actions."); + return; + } + } + + if (chatMessage != null) + _logger.Information($"Linked redemption to chat message [redemption id: {message.Reward.Id}][message id: {chatMessage.MessageId}]."); + } + foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId)); + + await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId), message.UserInput); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelFollowHandler.cs b/Twitch/Socket/Handlers/ChannelFollowHandler.cs index d0fb167..6c310b0 100644 --- a/Twitch/Socket/Handlers/ChannelFollowHandler.cs +++ b/Twitch/Socket/Handlers/ChannelFollowHandler.cs @@ -36,7 +36,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId)); + await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId), string.Empty); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs b/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs index e203ab1..e898057 100644 --- a/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs @@ -38,15 +38,15 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers var actions = _redemptionManager.Get("subscription"); if (!actions.Any()) { - _logger.Debug($"No redeemable actions for this subscription was found [message: {message.Message.Text}]"); + _logger.Debug($"No redeemable actions for this subscription was found [chatter id: {message.UserId}][chatter: {message.UserName}][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 [chatter id: {message.UserId}][chatter: {message.UserName}][message: {message.Message.Text}]"); foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!)); + await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!), message.Message.Text); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionEndHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionEndHandler.cs new file mode 100644 index 0000000..cc51a04 --- /dev/null +++ b/Twitch/Socket/Handlers/ChannelSubscriptionEndHandler.cs @@ -0,0 +1,52 @@ +using Serilog; +using TwitchChatTTS.Twitch.Redemptions; +using TwitchChatTTS.Twitch.Socket.Messages; + +namespace TwitchChatTTS.Twitch.Socket.Handlers +{ + public class ChannelSubscriptionEndHandler : ITwitchSocketHandler + { + public string Name => "channel.subscription.end"; + + private readonly IRedemptionManager _redemptionManager; + private readonly ILogger _logger; + + public ChannelSubscriptionEndHandler(IRedemptionManager redemptionManager, ILogger logger) + { + _redemptionManager = redemptionManager; + _logger = logger; + } + + public async Task Execute(TwitchWebsocketClient sender, object data) + { + if (data is not ChannelSubscriptionEndMessage message) + return; + + _logger.Information($"Subscription ended [chatter: {message.UserLogin}][chatter id: {message.UserId}][Tier: {message.Tier}]"); + try + { + var actions = _redemptionManager.Get("subscription.end"); + if (!actions.Any()) + { + _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}]"); + + foreach (var action in actions) + try + { + await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!), string.Empty); + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to execute redeemable action [action: {action.Name}][action type: {action.Type}][redeem: subscription][subscriber: {message.UserLogin}][subscriber id: {message.UserId}]"); + } + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to fetch the redeemable actions for subscription [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]"); + } + } + } +} \ No newline at end of file diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs index 9337a80..1fe98bd 100644 --- a/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs +++ b/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs @@ -40,7 +40,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName ?? "Anonymous", message.UserId == null ? 0 : long.Parse(message.UserId)); + await _redemptionManager.Execute(action, message.UserName ?? "Anonymous", message.UserId == null ? 0 : long.Parse(message.UserId), string.Empty); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs index 659220b..daabe8d 100644 --- a/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs @@ -38,7 +38,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!)); + await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!), string.Empty); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/NotificationHandler.cs b/Twitch/Socket/Handlers/NotificationHandler.cs index c73b29e..5b40889 100644 --- a/Twitch/Socket/Handlers/NotificationHandler.cs +++ b/Twitch/Socket/Handlers/NotificationHandler.cs @@ -37,11 +37,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers _messageTypes.Add("channel.chat.clear_user_messages", typeof(ChannelChatClearUserMessage)); _messageTypes.Add("channel.chat.message_delete", typeof(ChannelChatDeleteMessage)); _messageTypes.Add("channel.channel_points_custom_reward_redemption.add", typeof(ChannelCustomRedemptionMessage)); - _messageTypes.Add("channel.raid", typeof(ChannelRaidMessage)); _messageTypes.Add("channel.follow", typeof(ChannelFollowMessage)); + _messageTypes.Add("channel.raid", typeof(ChannelRaidMessage)); _messageTypes.Add("channel.subscribe", typeof(ChannelSubscriptionMessage)); - _messageTypes.Add("channel.subscription.message", typeof(ChannelResubscriptionMessage)); + _messageTypes.Add("channel.subscription.end", typeof(ChannelSubscriptionEndMessage)); _messageTypes.Add("channel.subscription.gift", typeof(ChannelSubscriptionGiftMessage)); + _messageTypes.Add("channel.subscription.message", typeof(ChannelResubscriptionMessage)); } public Task Execute(TwitchWebsocketClient sender, object data) diff --git a/Twitch/Socket/Handlers/SessionWelcomeHandler.cs b/Twitch/Socket/Handlers/SessionWelcomeHandler.cs index 9afbe70..7ad51d5 100644 --- a/Twitch/Socket/Handlers/SessionWelcomeHandler.cs +++ b/Twitch/Socket/Handlers/SessionWelcomeHandler.cs @@ -61,6 +61,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers "channel.chat.clear", "channel.chat.clear_user_messages", "channel.subscribe", + "channel.subscription.end", "channel.subscription.gift", "channel.subscription.message", "channel.ad_break.begin", diff --git a/Twitch/Socket/Messages/ChannelChatMessage.cs b/Twitch/Socket/Messages/ChannelChatMessage.cs index 3edf8bc..607418d 100644 --- a/Twitch/Socket/Messages/ChannelChatMessage.cs +++ b/Twitch/Socket/Messages/ChannelChatMessage.cs @@ -15,6 +15,12 @@ namespace TwitchChatTTS.Twitch.Socket.Messages public TwitchReplyInfo? Reply { get; set; } public string? ChannelPointsCustomRewardId { get; set; } public string? ChannelPointsAnimationId { get; set; } + public string? SourceBroadcasterUserId { get; set; } + public string? SourceBroadcasterUserName { get; set; } + public string? SourceBroadcasterUserLogin { get; set; } + public string? SourceMessageId { get; set; } + public TwitchBadge[]? SourceBadges { get; set; } + } public class TwitchChatMessageInfo diff --git a/Twitch/Socket/Messages/ChannelCustomRedemptionMessage.cs b/Twitch/Socket/Messages/ChannelCustomRedemptionMessage.cs index f643fdd..9d76acb 100644 --- a/Twitch/Socket/Messages/ChannelCustomRedemptionMessage.cs +++ b/Twitch/Socket/Messages/ChannelCustomRedemptionMessage.cs @@ -9,6 +9,7 @@ namespace TwitchChatTTS.Twitch.Socket.Messages public required string UserId { get; set; } public required string UserLogin { get; set; } public required string UserName { get; set; } + public required string UserInput { get; set; } public required string Status { get; set; } public DateTime RedeemedAt { get; set; } public required RedemptionReward Reward { get; set; } diff --git a/Twitch/Socket/Messages/ChannelSubscriptionEndMessage.cs b/Twitch/Socket/Messages/ChannelSubscriptionEndMessage.cs new file mode 100644 index 0000000..1f00b1e --- /dev/null +++ b/Twitch/Socket/Messages/ChannelSubscriptionEndMessage.cs @@ -0,0 +1,7 @@ +namespace TwitchChatTTS.Twitch.Socket.Messages +{ + public class ChannelSubscriptionEndMessage : ChannelSubscriptionData + { + public bool IsGift { get; set; } + } +} \ No newline at end of file