Fixed command permissions. Moved to using Twitch's EventSub via websockets. Cleaned some code up. Added detection for subscription messages (no TTS), message deletion, full or partial chat clear. Removes messages from TTS queue if applicable. Added command aliases for static parameters. Word filters use compiled regex if possible. Fixed TTS voice deletion.
This commit is contained in:
26
Twitch/Socket/Handlers/ChannelBanHandler.cs
Normal file
26
Twitch/Socket/Handlers/ChannelBanHandler.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelBanHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.ban";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChannelBanHandler(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (data is not ChannelBanMessage message)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_logger.Warning($"Chatter banned [chatter: {message.UserLogin}][chatter id: {message.UserId}][End: {(message.IsPermanent ? "Permanent" : message.EndsAt.ToString())}]");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
37
Twitch/Socket/Handlers/ChannelChatClearHandler.cs
Normal file
37
Twitch/Socket/Handlers/ChannelChatClearHandler.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelChatClearHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.chat.clear";
|
||||
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly AudioPlaybackEngine _playback;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChannelChatClearHandler(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||
{
|
||||
_player = player;
|
||||
_playback = playback;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (data is not ChannelChatClearMessage message)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_player.RemoveAll();
|
||||
if (_player.Playing != null)
|
||||
{
|
||||
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||
_player.Playing = null;
|
||||
}
|
||||
|
||||
_logger.Information($"Chat cleared [broadcaster: {message.BroadcasterUserLogin}][broadcaster id: {message.BroadcasterUserId}]");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
37
Twitch/Socket/Handlers/ChannelChatClearUserHandler.cs
Normal file
37
Twitch/Socket/Handlers/ChannelChatClearUserHandler.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelChatClearUserHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.chat.clear_user_messages";
|
||||
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly AudioPlaybackEngine _playback;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChannelChatClearUserHandler(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||
{
|
||||
_player = player;
|
||||
_playback = playback;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (data is not ChannelChatClearUserMessage message)
|
||||
return Task.CompletedTask;
|
||||
|
||||
long chatterId = long.Parse(message.TargetUserId);
|
||||
_player.RemoveAll(chatterId);
|
||||
if (_player.Playing?.ChatterId == chatterId) {
|
||||
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||
_player.Playing = null;
|
||||
}
|
||||
|
||||
_logger.Information($"Cleared all messages by user [target chatter: {message.TargetUserLogin}][target chatter id: {chatterId}][broadcaster: {message.BroadcasterUserLogin}][broadcaster id: {message.BroadcasterUserId}]");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
39
Twitch/Socket/Handlers/ChannelChatDeleteMessageHandler.cs
Normal file
39
Twitch/Socket/Handlers/ChannelChatDeleteMessageHandler.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelChatDeleteMessageHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.chat.message_delete";
|
||||
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly AudioPlaybackEngine _playback;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChannelChatDeleteMessageHandler(TTSPlayer player, AudioPlaybackEngine playback, ILogger logger)
|
||||
{
|
||||
_player = player;
|
||||
_playback = playback;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (data is not ChannelChatDeleteMessage message)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_player.Playing?.MessageId == message.MessageId)
|
||||
{
|
||||
_playback.RemoveMixerInput(_player.Playing.Audio!);
|
||||
_player.Playing = null;
|
||||
}
|
||||
else
|
||||
_player.RemoveMessage(message.MessageId);
|
||||
|
||||
|
||||
_logger.Information($"Deleted chat message [message id: {message.MessageId}][target chatter: {message.TargetUserLogin}][target chatter id: {message.TargetUserId}][broadcaster: {message.BroadcasterUserLogin}][broadcaster id: {message.BroadcasterUserId}]");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
319
Twitch/Socket/Handlers/ChannelChatMessageHandler.cs
Normal file
319
Twitch/Socket/Handlers/ChannelChatMessageHandler.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using CommonSocketLibrary.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Chat.Commands;
|
||||
using TwitchChatTTS.Chat.Emotes;
|
||||
using TwitchChatTTS.Chat.Groups;
|
||||
using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelChatMessageHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.chat.message";
|
||||
|
||||
private readonly User _user;
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly CommandManager _commands;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly IChatterGroupManager _chatterGroupManager;
|
||||
private readonly IEmoteDatabase _emotes;
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly Regex _sfxRegex;
|
||||
|
||||
|
||||
public ChannelChatMessageHandler(
|
||||
User user,
|
||||
TTSPlayer player,
|
||||
CommandManager commands,
|
||||
IGroupPermissionManager permissionManager,
|
||||
IChatterGroupManager chatterGroupManager,
|
||||
IEmoteDatabase emotes,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
Configuration configuration,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_player = player;
|
||||
_commands = commands;
|
||||
_permissionManager = permissionManager;
|
||||
_chatterGroupManager = chatterGroupManager;
|
||||
_emotes = emotes;
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_hermes = (hermes as HermesSocketClient)!;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
|
||||
_sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)", RegexOptions.Compiled);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (sender == null)
|
||||
return;
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Warning("Twitch websocket message data is null.");
|
||||
return;
|
||||
}
|
||||
if (data is not ChannelChatMessage message)
|
||||
return;
|
||||
|
||||
if (_hermes.Connected && !_hermes.Ready)
|
||||
{
|
||||
_logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {message.MessageId}]");
|
||||
return; // new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
}
|
||||
if (_configuration.Twitch?.TtsWhenOffline != true && !_obs.Streaming)
|
||||
{
|
||||
_logger.Debug($"OBS is not streaming. Ignoring chat messages [message id: {message.MessageId}]");
|
||||
return; // new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
}
|
||||
|
||||
var msg = message.Message.Text;
|
||||
var chatterId = long.Parse(message.ChatterUserId);
|
||||
var tasks = new List<Task>();
|
||||
|
||||
var defaultGroups = new string[] { "everyone" };
|
||||
var badgesGroups = message.Badges.Select(b => b.SetId).Select(GetGroupNameByBadgeName);
|
||||
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
|
||||
var groups = defaultGroups.Union(badgesGroups).Union(customGroups);
|
||||
|
||||
try
|
||||
{
|
||||
var commandResult = await _commands.Execute(msg, message, groups);
|
||||
if (commandResult != ChatCommandResult.Unknown)
|
||||
return; // new MessageResult(MessageStatus.Command, -1, -1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Failed executing a chat command [message: {msg}][chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}][message id: {message.MessageId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Reply != null)
|
||||
msg = msg.Substring(message.Reply.ParentUserLogin.Length + 2);
|
||||
|
||||
var permissionPath = "tts.chat.messages.read";
|
||||
if (!string.IsNullOrWhiteSpace(message.ChannelPointsCustomRewardId))
|
||||
permissionPath = "tts.chat.redemptions.read";
|
||||
|
||||
var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
|
||||
if (permission != true)
|
||||
{
|
||||
_logger.Debug($"Blocked message by {message.ChatterUserLogin}: {msg}");
|
||||
return; // new MessageResult(MessageStatus.Blocked, -1, -1);
|
||||
}
|
||||
|
||||
// Keep track of emotes usage
|
||||
var emotesUsed = new HashSet<string>();
|
||||
var newEmotes = new Dictionary<string, string>();
|
||||
foreach (var fragment in message.Message.Fragments)
|
||||
{
|
||||
if (fragment.Emote != null)
|
||||
{
|
||||
if (_emotes.Get(fragment.Text) == null)
|
||||
{
|
||||
newEmotes.Add(fragment.Text, fragment.Emote.Id);
|
||||
_emotes.Add(fragment.Text, fragment.Emote.Id);
|
||||
}
|
||||
emotesUsed.Add(fragment.Emote.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fragment.Mention != null)
|
||||
continue;
|
||||
|
||||
var text = fragment.Text.Trim();
|
||||
var textFragments = text.Split(' ');
|
||||
foreach (var f in textFragments)
|
||||
{
|
||||
var emoteId = _emotes.Get(f);
|
||||
if (emoteId != null)
|
||||
{
|
||||
emotesUsed.Add(emoteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_obs.Streaming)
|
||||
{
|
||||
if (newEmotes.Any())
|
||||
tasks.Add(_hermes.SendEmoteDetails(newEmotes));
|
||||
if (emotesUsed.Any())
|
||||
tasks.Add(_hermes.SendEmoteUsage(message.MessageId, chatterId, emotesUsed));
|
||||
if (!_user.Chatters.Contains(chatterId))
|
||||
{
|
||||
tasks.Add(_hermes.SendChatterDetails(chatterId, message.ChatterUserLogin));
|
||||
_user.Chatters.Add(chatterId);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace filtered words.
|
||||
if (_user.RegexFilters != null)
|
||||
{
|
||||
foreach (var wf in _user.RegexFilters)
|
||||
{
|
||||
if (wf.Search == null || wf.Replace == null)
|
||||
continue;
|
||||
|
||||
if (wf.Regex != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
msg = wf.Regex.Replace(msg, wf.Replace);
|
||||
continue;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
wf.Regex = null;
|
||||
}
|
||||
}
|
||||
|
||||
msg = msg.Replace(wf.Search, wf.Replace);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the priority of this message
|
||||
int priority = _chatterGroupManager.GetPriorityFor(groups); // + m.SubscribedMonthCount * (m.IsSubscriber ? 10 : 5);
|
||||
|
||||
// Determine voice selected.
|
||||
string voiceSelected = _user.DefaultTTSVoice;
|
||||
if (_user.VoicesSelected?.ContainsKey(chatterId) == true)
|
||||
{
|
||||
var voiceId = _user.VoicesSelected[chatterId];
|
||||
if (_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) && voiceName != null)
|
||||
{
|
||||
if (_user.VoicesEnabled.Contains(voiceName) || chatterId == _user.OwnerId)
|
||||
voiceSelected = voiceName;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine additional voices used
|
||||
var matches = _user.VoiceNameRegex?.Matches(msg).ToArray();
|
||||
if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
||||
{
|
||||
HandlePartialMessage(priority, voiceSelected, msg.Trim(), message);
|
||||
return; // new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||
}
|
||||
|
||||
HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), message);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var m = match.Groups[2].ToString();
|
||||
if (string.IsNullOrWhiteSpace(m))
|
||||
continue;
|
||||
|
||||
var voice = match.Groups[1].ToString();
|
||||
voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
||||
HandlePartialMessage(priority, voice, m.Trim(), message);
|
||||
}
|
||||
|
||||
if (tasks.Any())
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private void HandlePartialMessage(int priority, string voice, string message, ChannelChatMessage e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return;
|
||||
|
||||
var parts = _sfxRegex.Split(message);
|
||||
var chatterId = long.Parse(e.ChatterUserId);
|
||||
var badgesString = string.Join(", ", e.Badges.Select(b => b.SetId + '|' + b.Id + '=' + b.Info));
|
||||
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Reward Id: {e.ChannelPointsCustomRewardId}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = message,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
ChatterId = chatterId,
|
||||
MessageId = e.MessageId,
|
||||
Badges = e.Badges,
|
||||
Priority = priority
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var sfxMatches = _sfxRegex.Matches(message);
|
||||
var sfxStart = sfxMatches.FirstOrDefault()?.Index ?? message.Length;
|
||||
|
||||
for (var i = 0; i < sfxMatches.Count; i++)
|
||||
{
|
||||
var sfxMatch = sfxMatches[i];
|
||||
var sfxName = sfxMatch.Groups[1]?.ToString()?.ToLower();
|
||||
|
||||
if (!File.Exists("sfx/" + sfxName + ".mp3"))
|
||||
{
|
||||
parts[i * 2 + 2] = parts[i * 2] + " (" + parts[i * 2 + 1] + ")" + parts[i * 2 + 2];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parts[i * 2]))
|
||||
{
|
||||
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = parts[i * 2],
|
||||
Timestamp = DateTime.UtcNow,
|
||||
ChatterId = chatterId,
|
||||
MessageId = e.MessageId,
|
||||
Badges = e.Badges,
|
||||
Priority = priority
|
||||
});
|
||||
}
|
||||
|
||||
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
File = $"sfx/{sfxName}.mp3",
|
||||
Timestamp = DateTime.UtcNow,
|
||||
ChatterId = chatterId,
|
||||
MessageId = e.MessageId,
|
||||
Badges = e.Badges,
|
||||
Priority = priority
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parts.Last()))
|
||||
{
|
||||
_logger.Information($"Username: {e.ChatterUserLogin}; User ID: {e.ChatterUserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; {badgesString}");
|
||||
_player.Add(new TTSMessage()
|
||||
{
|
||||
Voice = voice,
|
||||
Message = parts.Last(),
|
||||
Timestamp = DateTime.UtcNow,
|
||||
ChatterId = chatterId,
|
||||
MessageId = e.MessageId,
|
||||
Badges = e.Badges,
|
||||
Priority = priority
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private string GetGroupNameByBadgeName(string badgeName)
|
||||
{
|
||||
if (badgeName == "subscriber")
|
||||
return "subscribers";
|
||||
if (badgeName == "moderator")
|
||||
return "moderators";
|
||||
return badgeName.ToLower();
|
||||
}
|
||||
}
|
||||
}
|
56
Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs
Normal file
56
Twitch/Socket/Handlers/ChannelCustomRedemptionHandler.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Redemptions;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelCustomRedemptionHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.channel_points_custom_reward_redemption.add";
|
||||
|
||||
private readonly RedemptionManager _redemptionManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChannelCustomRedemptionHandler(
|
||||
RedemptionManager redemptionManager,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_redemptionManager = redemptionManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (data is not ChannelCustomRedemptionMessage message)
|
||||
return;
|
||||
|
||||
_logger.Information($"Channel Point Reward Redeemed [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||
|
||||
try
|
||||
{
|
||||
var actions = _redemptionManager.Get(message.Reward.Id);
|
||||
if (!actions.Any())
|
||||
{
|
||||
_logger.Debug($"No redemable 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}]");
|
||||
|
||||
foreach (var action in actions)
|
||||
try
|
||||
{
|
||||
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Failed to fetch the redeemable actions for a redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs
Normal file
33
Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class ChannelSubscriptionHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "channel.subscription.message";
|
||||
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChannelSubscriptionHandler(TTSPlayer player, ILogger logger) {
|
||||
_player = player;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (sender == null)
|
||||
return;
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Warning("Twitch websocket message data is null.");
|
||||
return;
|
||||
}
|
||||
if (data is not ChannelSubscriptionMessage message)
|
||||
return;
|
||||
|
||||
_logger.Debug("Subscription occured.");
|
||||
}
|
||||
}
|
||||
}
|
8
Twitch/Socket/Handlers/ITwitchSocketHandler.cs
Normal file
8
Twitch/Socket/Handlers/ITwitchSocketHandler.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public interface ITwitchSocketHandler
|
||||
{
|
||||
string Name { get; }
|
||||
Task Execute(TwitchWebsocketClient sender, object? data);
|
||||
}
|
||||
}
|
69
Twitch/Socket/Handlers/NotificationHandler.cs
Normal file
69
Twitch/Socket/Handlers/NotificationHandler.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public sealed class NotificationHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "notification";
|
||||
|
||||
private IDictionary<string, ITwitchSocketHandler> _handlers;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private IDictionary<string, Type> _messageTypes;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
public NotificationHandler(
|
||||
[FromKeyedServices("twitch-notifications")] IEnumerable<ITwitchSocketHandler> handlers,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_handlers = handlers.ToDictionary(h => h.Name, h => h);
|
||||
_logger = logger;
|
||||
|
||||
_options = new JsonSerializerOptions() {
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
};
|
||||
|
||||
_messageTypes = new Dictionary<string, Type>();
|
||||
_messageTypes.Add("channel.ban", typeof(ChannelBanMessage));
|
||||
_messageTypes.Add("channel.chat.message", typeof(ChannelChatMessage));
|
||||
_messageTypes.Add("channel.chat.clear_user_messages", typeof(ChannelChatClearUserMessage));
|
||||
_messageTypes.Add("channel.chat.clear", typeof(ChannelChatClearMessage));
|
||||
_messageTypes.Add("channel.chat.message_delete", typeof(ChannelChatDeleteMessage));
|
||||
_messageTypes.Add("channel.channel_points_custom_reward_redemption.add", typeof(ChannelCustomRedemptionMessage));
|
||||
_messageTypes.Add("channel.subscription.message", typeof(ChannelSubscriptionMessage));
|
||||
}
|
||||
|
||||
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (sender == null)
|
||||
return;
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Warning("Twitch websocket message data is null.");
|
||||
return;
|
||||
}
|
||||
if (data is not NotificationMessage message)
|
||||
return;
|
||||
|
||||
if (!_messageTypes.TryGetValue(message.Subscription.Type, out var type) || type == null)
|
||||
{
|
||||
_logger.Warning($"Could not find Twitch notification type [message type: {message.Subscription.Type}]");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_handlers.TryGetValue(message.Subscription.Type, out ITwitchSocketHandler? handler) || handler == null)
|
||||
{
|
||||
_logger.Warning($"Could not find Twitch notification handler [message type: {message.Subscription.Type}]");
|
||||
return;
|
||||
}
|
||||
|
||||
var d = JsonSerializer.Deserialize(message.Event.ToString()!, type, _options);
|
||||
await handler.Execute(sender, d);
|
||||
}
|
||||
}
|
||||
}
|
47
Twitch/Socket/Handlers/SessionReconnectHandler.cs
Normal file
47
Twitch/Socket/Handlers/SessionReconnectHandler.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class SessionReconnectHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "session_reconnect";
|
||||
|
||||
private readonly TwitchApiClient _api;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SessionReconnectHandler(TwitchApiClient api, ILogger logger)
|
||||
{
|
||||
_api = api;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (sender == null)
|
||||
return;
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Warning("Twitch websocket message data is null.");
|
||||
return;
|
||||
}
|
||||
if (data is not SessionWelcomeMessage message)
|
||||
return;
|
||||
if (_api == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(message.Session.Id))
|
||||
{
|
||||
_logger.Warning($"No session info provided by Twitch [status: {message.Session.Status}]");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Be able to handle multiple websocket connections.
|
||||
sender.URL = message.Session.ReconnectUrl;
|
||||
await Task.Delay(TimeSpan.FromSeconds(29));
|
||||
await sender.DisconnectAsync(new SocketDisconnectionEventArgs("Close", "Twitch asking to reconnect."));
|
||||
await sender.Connect();
|
||||
}
|
||||
}
|
||||
}
|
94
Twitch/Socket/Handlers/SessionWelcomeHandler.cs
Normal file
94
Twitch/Socket/Handlers/SessionWelcomeHandler.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket.Handlers
|
||||
{
|
||||
public class SessionWelcomeHandler : ITwitchSocketHandler
|
||||
{
|
||||
public string Name => "session_welcome";
|
||||
|
||||
private readonly TwitchApiClient _api;
|
||||
private readonly User _user;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SessionWelcomeHandler(TwitchApiClient api, User user, ILogger logger)
|
||||
{
|
||||
_api = api;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(TwitchWebsocketClient sender, object? data)
|
||||
{
|
||||
if (sender == null)
|
||||
return;
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Warning("Twitch websocket message data is null.");
|
||||
return;
|
||||
}
|
||||
if (data is not SessionWelcomeMessage message)
|
||||
return;
|
||||
if (_api == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(message.Session.Id))
|
||||
{
|
||||
_logger.Warning($"No session info provided by Twitch [status: {message.Session.Status}]");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] subscriptionsv1 = [
|
||||
"channel.chat.message",
|
||||
"channel.chat.message_delete",
|
||||
"channel.chat.notification",
|
||||
"channel.chat.clear",
|
||||
"channel.chat.clear_user_messages",
|
||||
"channel.ad_break.begin",
|
||||
"channel.subscription.message",
|
||||
"channel.ban",
|
||||
"channel.channel_points_custom_reward_redemption.add"
|
||||
];
|
||||
string[] subscriptionsv2 = [
|
||||
"channel.follow",
|
||||
];
|
||||
string broadcasterId = _user.TwitchUserId.ToString();
|
||||
foreach (var subscription in subscriptionsv1)
|
||||
await Subscribe(subscription, message.Session.Id, broadcasterId, "1");
|
||||
foreach (var subscription in subscriptionsv2)
|
||||
await Subscribe(subscription, message.Session.Id, broadcasterId, "2");
|
||||
|
||||
sender.SessionId = message.Session.Id;
|
||||
sender.Identified = sender.SessionId != null;
|
||||
}
|
||||
|
||||
private async Task Subscribe(string subscriptionName, string sessionId, string broadcasterId, string version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _api.CreateEventSubscription(subscriptionName, version, sessionId, broadcasterId);
|
||||
if (response == null)
|
||||
{
|
||||
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: response is null]");
|
||||
return;
|
||||
}
|
||||
if (response.Data == null)
|
||||
{
|
||||
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is null]");
|
||||
return;
|
||||
}
|
||||
if (!response.Data.Any())
|
||||
{
|
||||
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is empty]");
|
||||
return;
|
||||
}
|
||||
_logger.Information($"Sucessfully added subscription to Twitch websockets [subscription type: {subscriptionName}]");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Failed to create an event subscription [subscription type: {subscriptionName}][reason: exception]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user