Fixed 7tv & Twitch reconnection. Added adbreak, follow, subscription handlers for Twitch. Added multi-chat support. Added support to unsubscribe from Twitch event subs.

This commit is contained in:
Tom
2024-08-06 19:29:29 +00:00
parent 75fcb8e0f8
commit 95d879f511
60 changed files with 1063 additions and 671 deletions

View File

@ -0,0 +1,57 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelAdBreakHandler : ITwitchSocketHandler
{
public string Name => "channel.ad_break.begin";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelAdBreakHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelAdBreakMessage message)
return;
bool isAutomatic = message.IsAutomatic == "true";
if (isAutomatic)
_logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][automatic: {isAutomatic}]");
else
_logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][requester: {message.RequesterUserLogin}][requester id: {message.RequesterUserId}]");
try
{
var actions = _redemptionManager.Get("adbreak");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for ad break was found");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch ad break");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable action [action: {action.Name}][action type: {action.Type}][redeem: ad break]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for ad break");
}
}
}
}

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelBanMessage message)
return Task.CompletedTask;

View File

@ -18,7 +18,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelChatClearMessage message)
return Task.CompletedTask;

View File

@ -18,14 +18,16 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelChatClearUserMessage message)
return Task.CompletedTask;
long broadcasterId = long.Parse(message.BroadcasterUserId);
long chatterId = long.Parse(message.TargetUserId);
_player.RemoveAll(chatterId);
if (_player.Playing?.ChatterId == chatterId) {
_player.RemoveAll(broadcasterId, chatterId);
if (_player.Playing != null && _player.Playing.RoomId == broadcasterId && _player.Playing.ChatterId == chatterId)
{
_playback.RemoveMixerInput(_player.Playing.Audio!);
_player.Playing = null;
}

View File

@ -18,7 +18,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public Task Execute(TwitchWebsocketClient sender, object? data)
public Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelChatDeleteMessage message)
return Task.CompletedTask;

View File

@ -19,7 +19,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
private readonly User _user;
private readonly TTSPlayer _player;
private readonly CommandManager _commands;
private readonly ICommandManager _commands;
private readonly IGroupPermissionManager _permissionManager;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly IEmoteDatabase _emotes;
@ -34,7 +34,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public ChannelChatMessageHandler(
User user,
TTSPlayer player,
CommandManager commands,
ICommandManager commands,
IGroupPermissionManager permissionManager,
IChatterGroupManager chatterGroupManager,
IEmoteDatabase emotes,
@ -59,15 +59,10 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
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;
@ -231,6 +226,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
var parts = _sfxRegex.Split(message);
var chatterId = long.Parse(e.ChatterUserId);
var broadcasterId = long.Parse(e.BroadcasterUserId);
var badgesString = string.Join(", ", e.Badges.Select(b => b.SetId + '|' + b.Id + '=' + b.Info));
if (parts.Length == 1)
@ -241,6 +237,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
Message = message,
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,
@ -271,6 +268,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
Message = parts[i * 2],
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,
@ -284,6 +282,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
File = $"sfx/{sfxName}.mp3",
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,
@ -299,6 +298,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
Voice = voice,
Message = parts.Last(),
Timestamp = DateTime.UtcNow,
RoomId = broadcasterId,
ChatterId = chatterId,
MessageId = e.MessageId,
Badges = e.Badges,

View File

@ -8,11 +8,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "channel.channel_points_custom_reward_redemption.add";
private readonly RedemptionManager _redemptionManager;
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelCustomRedemptionHandler(
RedemptionManager redemptionManager,
IRedemptionManager redemptionManager,
ILogger logger
)
{
@ -20,7 +20,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelCustomRedemptionMessage message)
return;

View File

@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelFollowHandler : ITwitchSocketHandler
{
public string Name => "channel.follow";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelFollowHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelFollowMessage message)
return;
_logger.Information($"User followed [chatter: {message.UserLogin}][chatter id: {message.UserId}]");
try
{
var actions = _redemptionManager.Get("follow");
if (!actions.Any())
{
_logger.Debug($"No redemable actions for follow was found");
return;
}
_logger.Debug($"Found {actions.Count} actions for this Twitch follow");
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: follow]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for follow");
}
}
}
}

View File

@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelResubscriptionHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.message";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelResubscriptionHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelResubscriptionMessage message)
return;
_logger.Debug("Resubscription occured.");
try
{
var actions = _redemptionManager.Get("subscription");
if (!actions.Any())
{
_logger.Debug($"No redemable 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}]");
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: subscription][message: {message.Message.Text}]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for subscription [message: {message.Message.Text}]");
}
}
}
}

View File

@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelSubscriptionGiftHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.gift";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelSubscriptionGiftHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelSubscriptionGiftMessage message)
return;
_logger.Debug("Gifted subscription occured.");
try
{
var actions = _redemptionManager.Get("subscription.gift");
if (!actions.Any())
{
_logger.Debug($"No redemable 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}]");
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: gifted subscription]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for gifted subscription");
}
}
}
}

View File

@ -1,33 +1,54 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelSubscriptionHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.message";
public string Name => "channel.subscription";
private readonly TTSPlayer _player;
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelSubscriptionHandler(TTSPlayer player, ILogger logger) {
_player = player;
public ChannelSubscriptionHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
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;
if (message.IsGifted)
return;
_logger.Debug("Subscription occured.");
try
{
var actions = _redemptionManager.Get("subscription");
if (!actions.Any())
{
_logger.Debug($"No redemable 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));
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeeemable 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}]");
}
}
}
}

View File

@ -3,6 +3,6 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public interface ITwitchSocketHandler
{
string Name { get; }
Task Execute(TwitchWebsocketClient sender, object? data);
Task Execute(TwitchWebsocketClient sender, object data);
}
}

View File

@ -23,30 +23,30 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_handlers = handlers.ToDictionary(h => h.Name, h => h);
_logger = logger;
_options = new JsonSerializerOptions() {
_options = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
_messageTypes = new Dictionary<string, Type>();
_messageTypes.Add("channel.adbreak.begin", typeof(ChannelAdBreakMessage));
_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.follow", typeof(ChannelFollowMessage));
_messageTypes.Add("channel.resubscription", typeof(ChannelResubscriptionMessage));
_messageTypes.Add("channel.subscription.message", typeof(ChannelSubscriptionMessage));
_messageTypes.Add("channel.subscription.gift", typeof(ChannelSubscriptionGiftMessage));
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
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;

View File

@ -0,0 +1,12 @@
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class SessionKeepAliveHandler : ITwitchSocketHandler
{
public string Name => "session_keepalive";
public Task Execute(TwitchWebsocketClient sender, object data)
{
return Task.CompletedTask;
}
}
}

View File

@ -8,40 +8,45 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "session_reconnect";
private readonly TwitchApiClient _api;
private readonly ITwitchConnectionManager _manager;
private readonly ILogger _logger;
public SessionReconnectHandler(TwitchApiClient api, ILogger logger)
public SessionReconnectHandler(ITwitchConnectionManager manager, ILogger logger)
{
_api = api;
_manager = manager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
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}]");
_logger.Warning($"No session id 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();
if (message.Session.ReconnectUrl == null)
{
_logger.Warning($"No reconnection info provided by Twitch [status: {message.Session.Status}]");
return;
}
sender.ReceivedReconnecting = true;
var backup = _manager.GetBackupClient();
var identified = _manager.GetWorkingClient();
if (identified != null && backup != identified)
{
await identified.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "Reconnection from another client."));
}
backup.URL = message.Session.ReconnectUrl;
await backup.Connect();
}
}
}

View File

@ -1,4 +1,3 @@
using CommonSocketLibrary.Abstract;
using Serilog;
using TwitchChatTTS.Twitch.Socket.Messages;
@ -8,26 +7,23 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "session_welcome";
private readonly HermesApiClient _hermes;
private readonly TwitchApiClient _api;
private readonly User _user;
private readonly ILogger _logger;
public SessionWelcomeHandler(TwitchApiClient api, User user, ILogger logger)
public SessionWelcomeHandler(HermesApiClient hermes, TwitchApiClient api, User user, ILogger logger)
{
_hermes = hermes;
_api = api;
_user = user;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object? data)
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)
@ -39,6 +35,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
return;
}
await _hermes.AuthorizeTwitch();
var token = await _hermes.FetchTwitchBotToken();
_api.Initialize(token);
string broadcasterId = _user.TwitchUserId.ToString();
string[] subscriptionsv1 = [
"channel.chat.message",
"channel.chat.message_delete",
@ -53,17 +54,36 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
string[] subscriptionsv2 = [
"channel.follow",
];
string broadcasterId = _user.TwitchUserId.ToString();
string? pagination = null;
int size = 0;
do
{
var subscriptionsData = await _api.GetSubscriptions(status: "enabled", broadcasterId: broadcasterId, after: pagination);
var subscriptionNames = subscriptionsData?.Data == null ? [] : subscriptionsData.Data.Select(s => s.Type).ToArray();
if (subscriptionNames.Length == 0)
break;
foreach (var d in subscriptionsData!.Data!)
sender.AddSubscription(broadcasterId, d.Type, d.Id);
subscriptionsv1 = subscriptionsv1.Except(subscriptionNames).ToArray();
subscriptionsv2 = subscriptionsv2.Except(subscriptionNames).ToArray();
pagination = subscriptionsData?.Pagination?.Cursor;
size = subscriptionNames.Length;
} while (size >= 100 && pagination != null && subscriptionsv1.Length + subscriptionsv2.Length > 0);
foreach (var subscription in subscriptionsv1)
await Subscribe(subscription, message.Session.Id, broadcasterId, "1");
await Subscribe(sender, 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;
await Subscribe(sender, subscription, message.Session.Id, broadcasterId, "2");
sender.Identify(message.Session.Id);
}
private async Task Subscribe(string subscriptionName, string sessionId, string broadcasterId, string version)
private async Task Subscribe(TwitchWebsocketClient sender, string subscriptionName, string sessionId, string broadcasterId, string version)
{
try
{
@ -83,6 +103,10 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: data is empty]");
return;
}
foreach (var d in response.Data)
sender.AddSubscription(broadcasterId, d.Type, d.Id);
_logger.Information($"Sucessfully added subscription to Twitch websockets [subscription type: {subscriptionName}]");
}
catch (Exception ex)