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:
11
Twitch/Redemptions/IRedemptionManager.cs
Normal file
11
Twitch/Redemptions/IRedemptionManager.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
public interface IRedemptionManager
|
||||
{
|
||||
Task Execute(RedeemableAction action, string senderDisplayName, long senderId);
|
||||
IList<RedeemableAction> Get(string twitchRedemptionId);
|
||||
void Initialize(IEnumerable<Redemption> redemptions, IDictionary<string, RedeemableAction> actions);
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ using TwitchChatTTS.OBS.Socket.Data;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Redemptions
|
||||
{
|
||||
public class RedemptionManager
|
||||
public class RedemptionManager : IRedemptionManager
|
||||
{
|
||||
private readonly IDictionary<string, IList<RedeemableAction>> _store;
|
||||
private readonly User _user;
|
||||
|
57
Twitch/Socket/Handlers/ChannelAdBreakHandler.cs
Normal file
57
Twitch/Socket/Handlers/ChannelAdBreakHandler.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
52
Twitch/Socket/Handlers/ChannelFollowHandler.cs
Normal file
52
Twitch/Socket/Handlers/ChannelFollowHandler.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs
Normal file
52
Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs
Normal 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}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs
Normal file
52
Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
12
Twitch/Socket/Handlers/SessionKeepAliveHandler.cs
Normal file
12
Twitch/Socket/Handlers/SessionKeepAliveHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
15
Twitch/Socket/Messages/ChannelAdBreakMessage.cs
Normal file
15
Twitch/Socket/Messages/ChannelAdBreakMessage.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
{
|
||||
public class ChannelAdBreakMessage
|
||||
{
|
||||
public string DurationSeconds { get; set; }
|
||||
public DateTime StartedAt { get; set; }
|
||||
public string IsAutomatic { get; set; }
|
||||
public string BroadcasterUserId { get; set; }
|
||||
public string BroadcasterUserLogin { get; set; }
|
||||
public string BroadcasterUserName { get; set; }
|
||||
public string RequesterUserId { get; set; }
|
||||
public string RequesterUserLogin { get; set; }
|
||||
public string RequesterUserName { get; set; }
|
||||
}
|
||||
}
|
13
Twitch/Socket/Messages/ChannelFollowMessage.cs
Normal file
13
Twitch/Socket/Messages/ChannelFollowMessage.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
{
|
||||
public class ChannelFollowMessage
|
||||
{
|
||||
public string BroadcasterUserId { get; set; }
|
||||
public string BroadcasterUserLogin { get; set; }
|
||||
public string BroadcasterUserName { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string UserLogin { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public DateTime FollowedAt { get; set; }
|
||||
}
|
||||
}
|
10
Twitch/Socket/Messages/ChannelResubscriptionMessage.cs
Normal file
10
Twitch/Socket/Messages/ChannelResubscriptionMessage.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
{
|
||||
public class ChannelResubscriptionMessage : ChannelSubscriptionData
|
||||
{
|
||||
public TwitchChatMessageInfo Message { get; set; }
|
||||
public int CumulativeMonths { get; set; }
|
||||
public int StreakMonths { get; set; }
|
||||
public int DurationMonths { get; set; }
|
||||
}
|
||||
}
|
9
Twitch/Socket/Messages/ChannelSubscriptionGiftMessage.cs
Normal file
9
Twitch/Socket/Messages/ChannelSubscriptionGiftMessage.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
{
|
||||
public class ChannelSubscriptionGiftMessage : ChannelSubscriptionData
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int? CumulativeTotal { get; set; }
|
||||
public bool IsAnonymous { get; set; }
|
||||
}
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
{
|
||||
public class ChannelSubscriptionMessage
|
||||
public class ChannelSubscriptionData
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
public string UserLogin { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string BroadcasterUserId { get; set; }
|
||||
public string BroadcasterUserLogin { get; set; }
|
||||
public string BroadcasterUserName { get; set; }
|
||||
public string ChatterUserId { get; set; }
|
||||
public string ChatterUserLogin { get; set; }
|
||||
public string ChatterUserName { get; set; }
|
||||
public string Tier { get; set; }
|
||||
public TwitchChatMessageInfo Message { get; set; }
|
||||
public int CumulativeMonths { get; set; }
|
||||
public int StreakMonths { get; set; }
|
||||
public int DurationMonths { get; set; }
|
||||
}
|
||||
|
||||
public class ChannelSubscriptionMessage : ChannelSubscriptionData
|
||||
{
|
||||
public bool IsGifted { get; set; }
|
||||
}
|
||||
}
|
@ -6,5 +6,10 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
public int Total { get; set; }
|
||||
public int TotalCost { get; set; }
|
||||
public int MaxTotalCost { get; set; }
|
||||
public EventResponsePagination? Pagination { get; set; }
|
||||
}
|
||||
|
||||
public class EventResponsePagination {
|
||||
public string Cursor { get; set; }
|
||||
}
|
||||
}
|
@ -11,7 +11,8 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? Cost { get; set; }
|
||||
|
||||
public EventSubscriptionMessage() {
|
||||
public EventSubscriptionMessage()
|
||||
{
|
||||
Type = string.Empty;
|
||||
Version = string.Empty;
|
||||
Condition = new Dictionary<string, string>();
|
||||
@ -45,7 +46,8 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? SessionId { get; }
|
||||
|
||||
public EventSubTransport() {
|
||||
public EventSubTransport()
|
||||
{
|
||||
Method = string.Empty;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,6 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
public string Id { get; set; }
|
||||
public string Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public object Event { get; set; }
|
||||
public object? Event { get; set; }
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
|
||||
public string Id { get; set; }
|
||||
public string Status { get; set; }
|
||||
public DateTime ConnectedAt { get; set; }
|
||||
public int KeepaliveTimeoutSeconds { get; set; }
|
||||
public int? KeepaliveTimeoutSeconds { get; set; }
|
||||
public string? ReconnectUrl { get; set; }
|
||||
public string? RecoveryUrl { get; set; }
|
||||
}
|
||||
|
119
Twitch/Socket/TwitchConnectionManager.cs
Normal file
119
Twitch/Socket/TwitchConnectionManager.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket
|
||||
{
|
||||
public interface ITwitchConnectionManager
|
||||
{
|
||||
TwitchWebsocketClient GetWorkingClient();
|
||||
TwitchWebsocketClient GetBackupClient();
|
||||
}
|
||||
|
||||
public class TwitchConnectionManager : ITwitchConnectionManager
|
||||
{
|
||||
private TwitchWebsocketClient? _identified;
|
||||
private TwitchWebsocketClient? _backup;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly object _lock;
|
||||
|
||||
public TwitchConnectionManager(IServiceProvider serviceProvider, ILogger logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
|
||||
public TwitchWebsocketClient GetBackupClient()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_identified == null)
|
||||
throw new InvalidOperationException("Cannot get backup Twitch client yet. Waiting for identification.");
|
||||
if (_backup != null)
|
||||
return _backup;
|
||||
|
||||
return CreateNewClient();
|
||||
}
|
||||
}
|
||||
|
||||
public TwitchWebsocketClient GetWorkingClient()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_identified == null)
|
||||
{
|
||||
return CreateNewClient();
|
||||
}
|
||||
|
||||
return _identified;
|
||||
}
|
||||
}
|
||||
|
||||
private TwitchWebsocketClient CreateNewClient()
|
||||
{
|
||||
if (_backup != null)
|
||||
return _backup;
|
||||
|
||||
var client = (_serviceProvider.GetRequiredKeyedService<SocketClient<TwitchWebsocketMessage>>("twitch-create") as TwitchWebsocketClient)!;
|
||||
client.Initialize();
|
||||
_backup = client;
|
||||
|
||||
client.OnIdentified += async (s, e) =>
|
||||
{
|
||||
bool clientDisconnect = false;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_identified == client)
|
||||
{
|
||||
_logger.Error("Twitch client has been re-identified.");
|
||||
return;
|
||||
}
|
||||
if (_backup != client)
|
||||
{
|
||||
_logger.Warning("Twitch client has been identified, but isn't backup. Disconnecting.");
|
||||
clientDisconnect = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_identified != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_identified = _backup;
|
||||
_backup = null;
|
||||
}
|
||||
|
||||
if (clientDisconnect)
|
||||
await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client."));
|
||||
|
||||
_logger.Information("Twitch client has been identified.");
|
||||
};
|
||||
client.OnDisconnected += (s, e) =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_identified == client)
|
||||
{
|
||||
_identified = null;
|
||||
}
|
||||
else if (_backup == client)
|
||||
{
|
||||
_backup = null;
|
||||
}
|
||||
else
|
||||
_logger.Error("Twitch client disconnection from unknown source.");
|
||||
}
|
||||
};
|
||||
|
||||
_logger.Debug("Created a Twitch websocket client.");
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,26 +6,32 @@ using System.Net.WebSockets;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using System.Text;
|
||||
using TwitchChatTTS.Twitch.Socket.Handlers;
|
||||
using CommonSocketLibrary.Backoff;
|
||||
|
||||
namespace TwitchChatTTS.Twitch.Socket
|
||||
{
|
||||
public class TwitchWebsocketClient : SocketClient<TwitchWebsocketMessage>
|
||||
{
|
||||
private readonly IDictionary<string, ITwitchSocketHandler> _handlers;
|
||||
private readonly IDictionary<string, Type> _messageTypes;
|
||||
private readonly IDictionary<string, string> _subscriptions;
|
||||
private readonly IBackoff _backoff;
|
||||
private DateTime _lastReceivedMessageTimestamp;
|
||||
private bool _disconnected;
|
||||
private readonly object _lock;
|
||||
|
||||
public event EventHandler<EventArgs> OnIdentified;
|
||||
|
||||
public string URL;
|
||||
|
||||
private IDictionary<string, ITwitchSocketHandler> _handlers;
|
||||
private IDictionary<string, Type> _messageTypes;
|
||||
private readonly Configuration _configuration;
|
||||
private System.Timers.Timer _reconnectTimer;
|
||||
|
||||
public bool Connected { get; set; }
|
||||
public bool Identified { get; set; }
|
||||
public string SessionId { get; set; }
|
||||
public bool Connected { get; private set; }
|
||||
public bool Identified { get; private set; }
|
||||
public string SessionId { get; private set; }
|
||||
public bool ReceivedReconnecting { get; set; }
|
||||
|
||||
|
||||
public TwitchWebsocketClient(
|
||||
Configuration configuration,
|
||||
[FromKeyedServices("twitch")] IEnumerable<ITwitchSocketHandler> handlers,
|
||||
[FromKeyedServices("twitch")] IBackoff backoff,
|
||||
ILogger logger
|
||||
) : base(logger, new JsonSerializerOptions()
|
||||
{
|
||||
@ -34,14 +40,12 @@ namespace TwitchChatTTS.Twitch.Socket
|
||||
})
|
||||
{
|
||||
_handlers = handlers.ToDictionary(h => h.Name, h => h);
|
||||
_configuration = configuration;
|
||||
|
||||
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
|
||||
_reconnectTimer.AutoReset = false;
|
||||
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect();
|
||||
_reconnectTimer.Enabled = false;
|
||||
_backoff = backoff;
|
||||
_subscriptions = new Dictionary<string, string>();
|
||||
_lock = new object();
|
||||
|
||||
_messageTypes = new Dictionary<string, Type>();
|
||||
_messageTypes.Add("session_keepalive", typeof(object));
|
||||
_messageTypes.Add("session_welcome", typeof(SessionWelcomeMessage));
|
||||
_messageTypes.Add("session_reconnect", typeof(SessionWelcomeMessage));
|
||||
_messageTypes.Add("notification", typeof(NotificationMessage));
|
||||
@ -50,23 +54,56 @@ namespace TwitchChatTTS.Twitch.Socket
|
||||
}
|
||||
|
||||
|
||||
public void AddSubscription(string broadcasterId, string type, string id)
|
||||
{
|
||||
if (_subscriptions.ContainsKey(broadcasterId + '|' + type))
|
||||
_subscriptions[broadcasterId + '|' + type] = id;
|
||||
else
|
||||
_subscriptions.Add(broadcasterId + '|' + type, id);
|
||||
}
|
||||
|
||||
public string? GetSubscriptionId(string broadcasterId, string type)
|
||||
{
|
||||
if (_subscriptions.TryGetValue(broadcasterId + '|' + type, out var id))
|
||||
return id;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveSubscription(string broadcasterId, string type)
|
||||
{
|
||||
_subscriptions.Remove(broadcasterId + '|' + type);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_logger.Information($"Initializing OBS websocket client.");
|
||||
_logger.Information($"Initializing Twitch websocket client.");
|
||||
OnConnected += (sender, e) =>
|
||||
{
|
||||
Connected = true;
|
||||
_reconnectTimer.Enabled = false;
|
||||
_logger.Information("Twitch websocket client connected.");
|
||||
_disconnected = false;
|
||||
};
|
||||
|
||||
OnDisconnected += (sender, e) =>
|
||||
OnDisconnected += async (sender, e) =>
|
||||
{
|
||||
_reconnectTimer.Enabled = Identified;
|
||||
_logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disconnected)
|
||||
return;
|
||||
|
||||
_disconnected = true;
|
||||
}
|
||||
|
||||
_logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}]");
|
||||
|
||||
Connected = false;
|
||||
Identified = false;
|
||||
|
||||
if (!ReceivedReconnecting)
|
||||
{
|
||||
_logger.Information("Attempting to reconnect to Twitch websocket server.");
|
||||
await Reconnect(_backoff, async () => await Connect());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -79,42 +116,14 @@ namespace TwitchChatTTS.Twitch.Socket
|
||||
}
|
||||
|
||||
_logger.Debug($"Twitch websocket client attempting to connect to {URL}");
|
||||
try
|
||||
{
|
||||
await ConnectAsync(URL);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning("Connecting to twitch failed. Skipping Twitch websockets.");
|
||||
}
|
||||
await ConnectAsync(URL);
|
||||
}
|
||||
|
||||
private async Task Reconnect()
|
||||
public void Identify(string sessionId)
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Error("Failed to disconnect from Twitch websocket server.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Connect();
|
||||
}
|
||||
catch (WebSocketException wse) when (wse.Message.Contains("502"))
|
||||
{
|
||||
_logger.Error("Twitch websocket server cannot be found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to reconnect to Twitch websocket server.");
|
||||
}
|
||||
Identified = true;
|
||||
SessionId = sessionId;
|
||||
OnIdentified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected TwitchWebsocketMessage GenerateMessage<T>(string messageType, T data)
|
||||
@ -134,14 +143,17 @@ namespace TwitchChatTTS.Twitch.Socket
|
||||
|
||||
protected override async Task OnResponseReceived(TwitchWebsocketMessage? message)
|
||||
{
|
||||
if (message == null || message.Metadata == null) {
|
||||
if (message == null || message.Metadata == null)
|
||||
{
|
||||
_logger.Information("Twitch message is null");
|
||||
return;
|
||||
}
|
||||
|
||||
_lastReceivedMessageTimestamp = DateTime.UtcNow;
|
||||
|
||||
string content = message.Payload?.ToString() ?? string.Empty;
|
||||
if (message.Metadata.MessageType != "session_keepalive")
|
||||
_logger.Information("Twitch RX #" + message.Metadata.MessageType + ": " + content);
|
||||
_logger.Debug("Twitch RX #" + message.Metadata.MessageType + ": " + content);
|
||||
|
||||
if (!_messageTypes.TryGetValue(message.Metadata.MessageType, out var type) || type == null)
|
||||
{
|
||||
@ -156,6 +168,11 @@ namespace TwitchChatTTS.Twitch.Socket
|
||||
}
|
||||
|
||||
var data = JsonSerializer.Deserialize(content, type, _options);
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Warning("Twitch websocket message payload is null.");
|
||||
return;
|
||||
}
|
||||
await handler.Execute(this, data);
|
||||
}
|
||||
|
||||
@ -180,7 +197,7 @@ namespace TwitchChatTTS.Twitch.Socket
|
||||
await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token);
|
||||
current += size;
|
||||
}
|
||||
_logger.Information("TX #" + type + ": " + content);
|
||||
_logger.Debug("Twitch TX #" + type + ": " + content);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -1,29 +0,0 @@
|
||||
// using System.Text.RegularExpressions;
|
||||
// using HermesSocketLibrary.Request.Message;
|
||||
// using TwitchChatTTS.Hermes;
|
||||
|
||||
// namespace TwitchChatTTS.Twitch
|
||||
// {
|
||||
// public class TTSContext
|
||||
// {
|
||||
// public string DefaultVoice;
|
||||
// public IEnumerable<TTSVoice>? EnabledVoices;
|
||||
// public IDictionary<string, TTSUsernameFilter>? UsernameFilters;
|
||||
// public IEnumerable<TTSWordFilter>? WordFilters;
|
||||
// public IList<VoiceDetails>? AvailableVoices { get => _availableVoices; set { _availableVoices = value; EnabledVoicesRegex = GenerateEnabledVoicesRegex(); } }
|
||||
// public IDictionary<long, string>? SelectedVoices;
|
||||
// public Regex? EnabledVoicesRegex;
|
||||
|
||||
// private IList<VoiceDetails>? _availableVoices;
|
||||
|
||||
|
||||
// private Regex? GenerateEnabledVoicesRegex() {
|
||||
// if (AvailableVoices == null || AvailableVoices.Count() <= 0) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// var enabledVoicesString = string.Join("|", AvailableVoices.Select(v => v.Name));
|
||||
// return new Regex($@"\b({enabledVoicesString})\:(.*?)(?=\Z|\b(?:{enabledVoicesString})\:)", RegexOptions.IgnoreCase);
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -24,35 +24,40 @@ public class TwitchApiClient
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<EventResponse<EventSubscriptionMessage>?> CreateEventSubscription(string type, string version, string userId)
|
||||
public async Task<EventResponse<NotificationInfo>?> CreateEventSubscription(string type, string version, string sessionId, string userId, string? broadcasterId = null)
|
||||
{
|
||||
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", userId }, { "moderator_user_id", userId } };
|
||||
var subscriptionData = new EventSubscriptionMessage(type, version, "https://hermes.goblincaves.com/api/account/authorize", "isdnmjfopsdfmsf4390", conditions);
|
||||
var response = await _web.Post("https://api.twitch.tv/helix/eventsub/subscriptions", subscriptionData);
|
||||
if (response.StatusCode == HttpStatusCode.Accepted)
|
||||
{
|
||||
_logger.Debug("Twitch API call [type: create event subscription]: " + await response.Content.ReadAsStringAsync());
|
||||
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<EventSubscriptionMessage>)) as EventResponse<EventSubscriptionMessage>;
|
||||
}
|
||||
_logger.Warning("Twitch api failed to create event subscription: " + await response.Content.ReadAsStringAsync());
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<EventResponse<EventSubscriptionMessage>?> CreateEventSubscription(string type, string version, string sessionId, string userId)
|
||||
{
|
||||
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", userId }, { "moderator_user_id", userId } };
|
||||
var conditions = new Dictionary<string, string>() { { "user_id", userId }, { "broadcaster_user_id", broadcasterId ?? userId }, { "moderator_user_id", broadcasterId ?? userId } };
|
||||
var subscriptionData = new EventSubscriptionMessage(type, version, sessionId, conditions);
|
||||
var response = await _web.Post("https://api.twitch.tv/helix/eventsub/subscriptions", subscriptionData);
|
||||
if (response.StatusCode == HttpStatusCode.Accepted)
|
||||
{
|
||||
_logger.Debug("Twitch API call [type: create event subscription]: " + await response.Content.ReadAsStringAsync());
|
||||
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<EventSubscriptionMessage>)) as EventResponse<EventSubscriptionMessage>;
|
||||
return await response.Content.ReadFromJsonAsync(typeof(EventResponse<NotificationInfo>)) as EventResponse<NotificationInfo>;
|
||||
}
|
||||
_logger.Error("Twitch api failed to create event subscription: " + await response.Content.ReadAsStringAsync());
|
||||
_logger.Error("Twitch api failed to create event subscription for websocket: " + await response.Content.ReadAsStringAsync());
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Initialize(TwitchBotToken token) {
|
||||
public async Task DeleteEventSubscription(string subscriptionId)
|
||||
{
|
||||
await _web.Delete("https://api.twitch.tv/helix/eventsub/subscriptions?id=" + subscriptionId);
|
||||
}
|
||||
|
||||
public async Task<EventResponse<NotificationInfo>?> GetSubscriptions(string? status = null, string? broadcasterId = null, string? after = null)
|
||||
{
|
||||
List<string> queryParams = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
queryParams.Add("status=" + status);
|
||||
if (!string.IsNullOrWhiteSpace(broadcasterId))
|
||||
queryParams.Add("user_id=" + broadcasterId);
|
||||
if (!string.IsNullOrWhiteSpace(after))
|
||||
queryParams.Add("after=" + after);
|
||||
var query = queryParams.Any() ? '?' + string.Join('&', queryParams) : string.Empty;
|
||||
return await _web.GetJson<EventResponse<NotificationInfo>>("https://api.twitch.tv/helix/eventsub/subscriptions" + query);
|
||||
}
|
||||
|
||||
public void Initialize(TwitchBotToken token)
|
||||
{
|
||||
_web.AddHeader("Authorization", "Bearer " + token.AccessToken);
|
||||
_web.AddHeader("Client-Id", token.ClientId);
|
||||
}
|
||||
|
Reference in New Issue
Block a user