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:
parent
75fcb8e0f8
commit
8014c12bc5
@ -1,310 +0,0 @@
|
||||
// using System.Text.RegularExpressions;
|
||||
// using TwitchLib.Client.Events;
|
||||
// using Serilog;
|
||||
// using TwitchChatTTS;
|
||||
// using TwitchChatTTS.Chat.Commands;
|
||||
// using TwitchChatTTS.Hermes.Socket;
|
||||
// using TwitchChatTTS.Chat.Groups.Permissions;
|
||||
// using TwitchChatTTS.Chat.Groups;
|
||||
// using TwitchChatTTS.Chat.Emotes;
|
||||
// using Microsoft.Extensions.DependencyInjection;
|
||||
// using CommonSocketLibrary.Common;
|
||||
// using CommonSocketLibrary.Abstract;
|
||||
// using TwitchChatTTS.OBS.Socket;
|
||||
|
||||
|
||||
// public class ChatMessageHandler
|
||||
// {
|
||||
// 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 Regex _sfxRegex;
|
||||
// private HashSet<long> _chatters;
|
||||
|
||||
// public HashSet<long> Chatters { get => _chatters; set => _chatters = value; }
|
||||
|
||||
|
||||
// public ChatMessageHandler(
|
||||
// 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;
|
||||
|
||||
// _chatters = new HashSet<long>();
|
||||
// _sfxRegex = new Regex(@"\(([A-Za-z0-9_-]+)\)");
|
||||
// }
|
||||
|
||||
|
||||
// public async Task<MessageResult> Handle(OnMessageReceivedArgs e)
|
||||
// {
|
||||
// var m = e.ChatMessage;
|
||||
|
||||
// if (_hermes.Connected && !_hermes.Ready)
|
||||
// {
|
||||
// _logger.Debug($"TTS is not yet ready. Ignoring chat messages [message id: {m.Id}]");
|
||||
// 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: {m.Id}]");
|
||||
// return new MessageResult(MessageStatus.NotReady, -1, -1);
|
||||
// }
|
||||
|
||||
|
||||
// var msg = e.ChatMessage.Message;
|
||||
// var chatterId = long.Parse(m.UserId);
|
||||
// var tasks = new List<Task>();
|
||||
|
||||
// var checks = new bool[] { true, m.IsSubscriber, m.IsVip, m.IsModerator, m.IsBroadcaster };
|
||||
// var defaultGroups = new string[] { "everyone", "subscribers", "vip", "moderators", "broadcaster" };
|
||||
// var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
|
||||
// var groups = defaultGroups.Where((e, i) => checks[i]).Union(customGroups);
|
||||
|
||||
// try
|
||||
// {
|
||||
// var commandResult = await _commands.Execute(msg, m, 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: {m.Username}][chatter id: {m.UserId}][message id: {m.Id}]");
|
||||
// }
|
||||
|
||||
// var permissionPath = "tts.chat.messages.read";
|
||||
// if (!string.IsNullOrWhiteSpace(m.CustomRewardId))
|
||||
// permissionPath = "tts.chat.redemptions.read";
|
||||
|
||||
// var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath);
|
||||
// if (permission != true)
|
||||
// {
|
||||
// _logger.Debug($"Blocked message by {m.Username}: {msg}");
|
||||
// return new MessageResult(MessageStatus.Blocked, -1, -1);
|
||||
// }
|
||||
|
||||
// if (_obs.Streaming && !_chatters.Contains(chatterId))
|
||||
// {
|
||||
// tasks.Add(_hermes.SendChatterDetails(chatterId, m.Username));
|
||||
// _chatters.Add(chatterId);
|
||||
// }
|
||||
|
||||
// // Filter highly repetitive words (like emotes) from the message.
|
||||
// int totalEmoteUsed = 0;
|
||||
// var emotesUsed = new HashSet<string>();
|
||||
// var words = msg.Split(' ');
|
||||
// var wordCounter = new Dictionary<string, int>();
|
||||
// string filteredMsg = string.Empty;
|
||||
// var newEmotes = new Dictionary<string, string>();
|
||||
// foreach (var w in words)
|
||||
// {
|
||||
// if (wordCounter.ContainsKey(w))
|
||||
// wordCounter[w]++;
|
||||
// else
|
||||
// wordCounter.Add(w, 1);
|
||||
|
||||
// var emoteId = _emotes.Get(w);
|
||||
// if (emoteId == null)
|
||||
// {
|
||||
// emoteId = m.EmoteSet.Emotes.FirstOrDefault(e => e.Name == w)?.Id;
|
||||
// if (emoteId != null)
|
||||
// {
|
||||
// newEmotes.Add(emoteId, w);
|
||||
// _emotes.Add(w, emoteId);
|
||||
// }
|
||||
// }
|
||||
// if (emoteId != null)
|
||||
// {
|
||||
// emotesUsed.Add(emoteId);
|
||||
// totalEmoteUsed++;
|
||||
// }
|
||||
|
||||
// if (wordCounter[w] <= 4 && (emoteId == null || totalEmoteUsed <= 5))
|
||||
// filteredMsg += w + " ";
|
||||
// }
|
||||
// if (_obs.Streaming && newEmotes.Any())
|
||||
// tasks.Add(_hermes.SendEmoteDetails(newEmotes));
|
||||
// msg = filteredMsg;
|
||||
|
||||
// // Replace filtered words.
|
||||
// if (_user.RegexFilters != null)
|
||||
// {
|
||||
// foreach (var wf in _user.RegexFilters)
|
||||
// {
|
||||
// if (wf.Search == null || wf.Replace == null)
|
||||
// continue;
|
||||
|
||||
// if (wf.IsRegex)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var regex = new Regex(wf.Search);
|
||||
// msg = regex.Replace(msg, wf.Replace);
|
||||
// continue;
|
||||
// }
|
||||
// catch (Exception)
|
||||
// {
|
||||
// wf.IsRegex = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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 || m.IsStaff)
|
||||
// {
|
||||
// voiceSelected = voiceName;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Determine additional voices used
|
||||
// var matches = _user.WordFilterRegex?.Matches(msg).ToArray();
|
||||
// if (matches == null || matches.FirstOrDefault() == null || matches.First().Index < 0)
|
||||
// {
|
||||
// HandlePartialMessage(priority, voiceSelected, msg.Trim(), e);
|
||||
// return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||
// }
|
||||
|
||||
// HandlePartialMessage(priority, voiceSelected, msg.Substring(0, matches.First().Index).Trim(), e);
|
||||
// foreach (Match match in matches)
|
||||
// {
|
||||
// var message = match.Groups[2].ToString();
|
||||
// if (string.IsNullOrWhiteSpace(message))
|
||||
// continue;
|
||||
|
||||
// var voice = match.Groups[1].ToString();
|
||||
// voice = voice[0].ToString().ToUpper() + voice.Substring(1).ToLower();
|
||||
// HandlePartialMessage(priority, voice, message.Trim(), e);
|
||||
// }
|
||||
|
||||
// if (tasks.Any())
|
||||
// await Task.WhenAll(tasks);
|
||||
|
||||
// return new MessageResult(MessageStatus.None, _user.TwitchUserId, chatterId, emotesUsed);
|
||||
// }
|
||||
|
||||
// private void HandlePartialMessage(int priority, string voice, string message, OnMessageReceivedArgs e)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(message))
|
||||
// return;
|
||||
|
||||
// var m = e.ChatMessage;
|
||||
// var parts = _sfxRegex.Split(message);
|
||||
// var badgesString = string.Join(", ", e.ChatMessage.Badges.Select(b => b.Key + " = " + b.Value));
|
||||
|
||||
// if (parts.Length == 1)
|
||||
// {
|
||||
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {message}; Month: {m.SubscribedMonthCount}; Reward Id: {m.CustomRewardId}; {badgesString}");
|
||||
// _player.Add(new TTSMessage()
|
||||
// {
|
||||
// Voice = voice,
|
||||
// Message = message,
|
||||
// Timestamp = DateTime.UtcNow,
|
||||
// Username = m.Username,
|
||||
// //Bits = m.Bits,
|
||||
// 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: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts[i * 2]}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
// _player.Add(new TTSMessage()
|
||||
// {
|
||||
// Voice = voice,
|
||||
// Message = parts[i * 2],
|
||||
// Moderator = m.IsModerator,
|
||||
// Timestamp = DateTime.UtcNow,
|
||||
// Username = m.Username,
|
||||
// Bits = m.Bits,
|
||||
// Badges = m.Badges,
|
||||
// Priority = priority
|
||||
// });
|
||||
// }
|
||||
|
||||
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; SFX: {sfxName}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
// _player.Add(new TTSMessage()
|
||||
// {
|
||||
// Voice = voice,
|
||||
// Message = sfxName,
|
||||
// File = $"sfx/{sfxName}.mp3",
|
||||
// Moderator = m.IsModerator,
|
||||
// Timestamp = DateTime.UtcNow,
|
||||
// Username = m.Username,
|
||||
// Bits = m.Bits,
|
||||
// Badges = m.Badges,
|
||||
// Priority = priority
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (!string.IsNullOrWhiteSpace(parts.Last()))
|
||||
// {
|
||||
// _logger.Information($"Username: {m.Username}; User ID: {m.UserId}; Voice: {voice}; Priority: {priority}; Message: {parts.Last()}; Month: {m.SubscribedMonthCount}; {badgesString}");
|
||||
// _player.Add(new TTSMessage()
|
||||
// {
|
||||
// Voice = voice,
|
||||
// Message = parts.Last(),
|
||||
// Moderator = m.IsModerator,
|
||||
// Timestamp = DateTime.UtcNow,
|
||||
// Username = m.Username,
|
||||
// Bits = m.Bits,
|
||||
// Badges = m.Badges,
|
||||
// Priority = priority
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -13,6 +13,6 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public interface IChatPartialCommand
|
||||
{
|
||||
bool AcceptCustomPermission { get; }
|
||||
Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client);
|
||||
Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
Success = 2,
|
||||
Permission = 3,
|
||||
Syntax = 4,
|
||||
Fail = 5
|
||||
Fail = 5,
|
||||
OtherRoom = 6,
|
||||
}
|
||||
}
|
@ -45,17 +45,18 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
if (_current == _root)
|
||||
throw new Exception("Cannot add permissions without a command name.");
|
||||
|
||||
|
||||
_current.AddPermission(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICommandBuilder AddAlias(string alias, string child) {
|
||||
public ICommandBuilder AddAlias(string alias, string child)
|
||||
{
|
||||
if (_current == _root)
|
||||
throw new Exception("Cannot add aliases without a command name.");
|
||||
if (_current.Children == null || !_current.Children.Any())
|
||||
throw new Exception("Cannot add alias if this has no parameter.");
|
||||
|
||||
|
||||
_current.AddAlias(alias, child);
|
||||
return this;
|
||||
}
|
||||
@ -327,7 +328,8 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
Permissions = Permissions.Union([path]).ToArray();
|
||||
}
|
||||
|
||||
public CommandNode AddAlias(string alias, string child) {
|
||||
public CommandNode AddAlias(string alias, string child)
|
||||
{
|
||||
var target = _children.FirstOrDefault(c => c.Parameter.Name == child);
|
||||
if (target == null)
|
||||
throw new Exception($"Cannot find child parameter [parameter: {child}][alias: {alias}]");
|
||||
@ -339,6 +341,8 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
var clone = target.MemberwiseClone() as CommandNode;
|
||||
var node = new CommandNode(new StaticParameter(alias, alias, target.Parameter.Optional));
|
||||
node._children = target._children;
|
||||
node.Permissions = target.Permissions;
|
||||
node.Command = target.Command;
|
||||
_children.Add(node);
|
||||
return this;
|
||||
}
|
||||
|
41
Chat/Commands/CommandFactory.cs
Normal file
41
Chat/Commands/CommandFactory.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Serilog;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class CommandFactory : ICommandFactory
|
||||
{
|
||||
private readonly IEnumerable<IChatCommand> _commands;
|
||||
private readonly ICommandBuilder _builder;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public CommandFactory(
|
||||
IEnumerable<IChatCommand> commands,
|
||||
ICommandBuilder builder,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_commands = commands;
|
||||
_builder = builder;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ICommandSelector Build()
|
||||
{
|
||||
foreach (var command in _commands)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Debug($"Creating command tree for '{command.Name}'.");
|
||||
command.Build(_builder);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, $"Failed to properly load a chat command [command name: {command.Name}]");
|
||||
}
|
||||
}
|
||||
|
||||
return _builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
@ -10,37 +10,30 @@ using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class CommandManager
|
||||
public class CommandManager : ICommandManager
|
||||
{
|
||||
private readonly User _user;
|
||||
private readonly ICommandSelector _commandSelector;
|
||||
private ICommandSelector _commandSelector;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
//private readonly TwitchWebsocketClient _twitch;
|
||||
private readonly IGroupPermissionManager _permissionManager;
|
||||
private readonly ILogger _logger;
|
||||
private string CommandStartSign { get; } = "!";
|
||||
|
||||
|
||||
public CommandManager(
|
||||
IEnumerable<IChatCommand> commands,
|
||||
ICommandBuilder commandBuilder,
|
||||
User user,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> socketClient,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||
//[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
|
||||
IGroupPermissionManager permissionManager,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_hermes = (socketClient as HermesSocketClient)!;
|
||||
_hermes = (hermes as HermesSocketClient)!;
|
||||
//_twitch = (twitch as TwitchWebsocketClient)!;
|
||||
_permissionManager = permissionManager;
|
||||
_logger = logger;
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
_logger.Debug($"Creating command tree for '{command.Name}'.");
|
||||
command.Build(commandBuilder);
|
||||
}
|
||||
|
||||
_commandSelector = commandBuilder.Build();
|
||||
}
|
||||
|
||||
|
||||
@ -54,9 +47,13 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
if (!arg.StartsWith(CommandStartSign))
|
||||
return ChatCommandResult.Unknown;
|
||||
|
||||
if (message.BroadcasterUserId != _user.TwitchUserId.ToString())
|
||||
return ChatCommandResult.OtherRoom;
|
||||
|
||||
string[] parts = Regex.Matches(arg.Substring(CommandStartSign.Length), "(?<match>[^\"\\n\\s]+|\"[^\"\\n]*\")")
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups["match"].Value)
|
||||
.Where(m => !string.IsNullOrEmpty(m))
|
||||
.Select(m => m.StartsWith('"') && m.EndsWith('"') ? m.Substring(1, m.Length - 2) : m)
|
||||
.ToArray();
|
||||
string[] args = parts.ToArray();
|
||||
@ -65,7 +62,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
CommandSelectorResult selectorResult = _commandSelector.GetBestMatch(args, message);
|
||||
if (selectorResult.Command == null)
|
||||
{
|
||||
_logger.Warning($"Could not match '{arg}' to any command.");
|
||||
_logger.Warning($"Could not match '{arg}' to any command [chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]");
|
||||
return ChatCommandResult.Missing;
|
||||
}
|
||||
|
||||
@ -111,10 +108,24 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
return ChatCommandResult.Success;
|
||||
}
|
||||
|
||||
public void Update(ICommandFactory factory)
|
||||
{
|
||||
_commandSelector = factory.Build();
|
||||
}
|
||||
|
||||
private bool CanExecute(long chatterId, IEnumerable<string> groups, string path, string[]? additionalPaths)
|
||||
{
|
||||
_logger.Debug($"Checking for permission [chatter id: {chatterId}][group: {string.Join(", ", groups)}][path: {path}]{(additionalPaths != null ? "[paths: " + string.Join('|', additionalPaths) + "]" : string.Empty)}");
|
||||
return _permissionManager.CheckIfAllowed(groups, path) != false && (additionalPaths == null || additionalPaths.All(p => _permissionManager.CheckIfAllowed(groups, p) != false));
|
||||
if (_permissionManager.CheckIfAllowed(groups, path) != false)
|
||||
{
|
||||
if (additionalPaths == null)
|
||||
return true;
|
||||
|
||||
// All direct allow must not be false and at least one of them must be true.
|
||||
if (additionalPaths.All(p => _permissionManager.CheckIfDirectAllowed(groups, p) != false) && additionalPaths.Any(p => _permissionManager.CheckIfDirectAllowed(groups, p) == true))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
9
Chat/Commands/ICommandFactory.cs
Normal file
9
Chat/Commands/ICommandFactory.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public interface ICommandFactory
|
||||
{
|
||||
ICommandSelector Build();
|
||||
}
|
||||
}
|
9
Chat/Commands/ICommandManager.cs
Normal file
9
Chat/Commands/ICommandManager.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
|
||||
namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public interface ICommandManager {
|
||||
Task<ChatCommandResult> Execute(string arg, ChannelChatMessage message, IEnumerable<string> groups);
|
||||
void Update(ICommandFactory factory);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.OBS.Socket.Data;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
@ -71,7 +72,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
string sceneName = values["sceneName"];
|
||||
string sourceName = values["sourceName"];
|
||||
@ -97,7 +98,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
string sceneName = values["sceneName"];
|
||||
string sourceName = values["sourceName"];
|
||||
@ -133,7 +134,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
string sceneName = values["sceneName"];
|
||||
string sourceName = values["sourceName"];
|
||||
|
@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
@ -44,9 +45,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
await client.FetchEnabledTTSVoices();
|
||||
await hermes.FetchEnabledTTSVoices();
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,9 +55,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
await client.FetchTTSWordFilters();
|
||||
await hermes.FetchTTSWordFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,9 +65,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
await client.FetchTTSChatterVoices();
|
||||
await hermes.FetchTTSChatterVoices();
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,9 +75,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
await client.FetchDefaultTTSVoice();
|
||||
await hermes.FetchDefaultTTSVoice();
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,9 +85,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
await client.FetchRedemptions();
|
||||
await hermes.FetchRedemptions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,12 +98,13 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public RefreshObs(OBSSocketClient obsManager, ILogger logger) {
|
||||
public RefreshObs(OBSSocketClient obsManager, ILogger logger)
|
||||
{
|
||||
_obsManager = obsManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
_obsManager.ClearCache();
|
||||
_logger.Information("Cleared the cache used for OBS.");
|
||||
@ -114,9 +116,9 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
await client.FetchPermissions();
|
||||
await hermes.FetchPermissions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
@ -51,7 +52,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
if (_player.Playing == null)
|
||||
return;
|
||||
@ -78,7 +79,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
_player.RemoveAll();
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
@ -7,13 +10,21 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
public class TTSCommand : IChatCommand
|
||||
{
|
||||
private readonly TwitchWebsocketClient _twitch;
|
||||
private readonly User _user;
|
||||
private readonly TwitchApiClient _client;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
||||
public TTSCommand(User user, ILogger logger)
|
||||
public TTSCommand(
|
||||
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
|
||||
User user,
|
||||
TwitchApiClient client,
|
||||
ILogger logger)
|
||||
{
|
||||
_twitch = (twitch as TwitchWebsocketClient)!;
|
||||
_user = user;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -51,7 +62,19 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
})
|
||||
.AddAlias("off", "disable")
|
||||
.AddAlias("disabled", "disable")
|
||||
.AddAlias("false", "disable");
|
||||
.AddAlias("false", "disable")
|
||||
.CreateStaticInputParameter("join", b =>
|
||||
{
|
||||
b.CreateMentionParameter("mention", true)
|
||||
.AddPermission("tts.commands.tts.join")
|
||||
.CreateCommand(new JoinRoomCommand(_twitch, _client, _user, _logger));
|
||||
})
|
||||
.CreateStaticInputParameter("leave", b =>
|
||||
{
|
||||
b.CreateMentionParameter("mention", true)
|
||||
.AddPermission("tts.commands.tts.leave")
|
||||
.CreateCommand(new LeaveRoomCommand(_twitch, _client, _user, _logger));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -119,7 +142,8 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
}
|
||||
|
||||
var voiceId = _user.VoicesAvailable.FirstOrDefault(v => v.Value.ToLower() == voiceNameLower).Key;
|
||||
if (voiceId == null) {
|
||||
if (voiceId == null)
|
||||
{
|
||||
_logger.Warning($"Could not find the identifier for the tts voice [voice name: {voiceName}]");
|
||||
return;
|
||||
}
|
||||
@ -157,5 +181,94 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger.Information($"Changed state for TTS voice [voice: {voiceName}][state: {_state}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class JoinRoomCommand : IChatPartialCommand
|
||||
{
|
||||
private readonly TwitchWebsocketClient _twitch;
|
||||
private readonly TwitchApiClient _client;
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public JoinRoomCommand(
|
||||
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
|
||||
TwitchApiClient client,
|
||||
User user,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_twitch = (twitch as TwitchWebsocketClient)!;
|
||||
_client = client;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
var mention = values["mention"].ToLower();
|
||||
var fragment = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Text.ToLower() == mention);
|
||||
if (fragment == null)
|
||||
{
|
||||
_logger.Warning("Cannot find the channel to join chat with.");
|
||||
return;
|
||||
}
|
||||
|
||||
await _client.CreateEventSubscription("channel.chat.message", "1", _twitch.SessionId, _user.TwitchUserId.ToString(), fragment.Mention!.UserId);
|
||||
_logger.Information($"Joined chat room [channel: {fragment.Mention.UserLogin}][channel id: {fragment.Mention.UserId}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LeaveRoomCommand : IChatPartialCommand
|
||||
{
|
||||
private readonly TwitchWebsocketClient _twitch;
|
||||
private readonly TwitchApiClient _client;
|
||||
private readonly User _user;
|
||||
private ILogger _logger;
|
||||
|
||||
public bool AcceptCustomPermission { get => true; }
|
||||
|
||||
public LeaveRoomCommand(
|
||||
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
|
||||
TwitchApiClient client,
|
||||
User user,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_twitch = (twitch as TwitchWebsocketClient)!;
|
||||
_client = client;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
{
|
||||
var mention = values["mention"].ToLower();
|
||||
var fragment = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Text.ToLower() == mention);
|
||||
if (fragment?.Mention == null)
|
||||
{
|
||||
_logger.Warning("Cannot find the channel to leave chat from.");
|
||||
return;
|
||||
}
|
||||
|
||||
var subscriptionId = _twitch.GetSubscriptionId(_user.TwitchUserId.ToString(), "channel.chat.message");
|
||||
if (subscriptionId == null)
|
||||
{
|
||||
_logger.Warning("Cannot find the subscription for that channel.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _client.DeleteEventSubscription(subscriptionId);
|
||||
_twitch.RemoveSubscription(fragment.Mention.UserId, "channel.chat.message");
|
||||
_logger.Information($"Joined chat room [channel: {fragment.Mention.UserLogin}][channel id: {fragment.Mention.UserId}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to delete the subscription from Twitch.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using Serilog;
|
||||
using TwitchChatTTS.Hermes.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
|
||||
@ -37,11 +38,11 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}");
|
||||
|
||||
await client.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
|
||||
await hermes.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,6 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
public class VoiceCommand : IChatCommand
|
||||
{
|
||||
private readonly User _user;
|
||||
// TODO: get permissions
|
||||
// TODO: validated parameter for username by including '@' and regex for username
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public VoiceCommand(User user, ILogger logger)
|
||||
@ -26,7 +24,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
{
|
||||
b.CreateVoiceNameParameter("voiceName", true)
|
||||
.CreateCommand(new TTSVoiceSelector(_user, _logger))
|
||||
.CreateUnvalidatedParameter("chatter", optional: true)
|
||||
.CreateMentionParameter("chatter", enabled: true, optional: true)
|
||||
.AddPermission("tts.command.voice.admin")
|
||||
.CreateCommand(new TTSVoiceSelectorAdmin(_user, _logger));
|
||||
});
|
||||
@ -45,7 +43,7 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
if (_user == null || _user.VoicesSelected == null)
|
||||
return;
|
||||
@ -57,12 +55,12 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||
{
|
||||
await client.UpdateTTSUser(chatterId, voice.Key);
|
||||
await hermes.UpdateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.CreateTTSUser(chatterId, voice.Key);
|
||||
await hermes.CreateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {message.ChatterUserLogin}][reason: command]");
|
||||
}
|
||||
}
|
||||
@ -81,13 +79,12 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient client)
|
||||
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
|
||||
{
|
||||
if (_user == null || _user.VoicesSelected == null)
|
||||
return;
|
||||
|
||||
var chatterLogin = values["chatter"].Substring(1);
|
||||
var mention = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Mention.UserLogin == chatterLogin)?.Mention;
|
||||
var mention = message.Message.Fragments.FirstOrDefault(f => f.Mention != null && f.Text == values["chatter"])?.Mention;
|
||||
if (mention == null)
|
||||
{
|
||||
_logger.Warning("Failed to find the chatter to apply voice command to.");
|
||||
@ -101,12 +98,12 @@ namespace TwitchChatTTS.Chat.Commands
|
||||
|
||||
if (_user.VoicesSelected.ContainsKey(chatterId))
|
||||
{
|
||||
await client.UpdateTTSUser(chatterId, voice.Key);
|
||||
await hermes.UpdateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to create chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.CreateTTSUser(chatterId, voice.Key);
|
||||
await hermes.CreateTTSUser(chatterId, voice.Key);
|
||||
_logger.Debug($"Sent request to update chat TTS voice [voice: {voice.Value}][username: {mention.UserLogin}][reason: command]");
|
||||
}
|
||||
}
|
||||
|
@ -12,62 +12,75 @@ namespace TwitchChatTTS.Chat.Groups
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
||||
public ChatterGroupManager(ILogger logger) {
|
||||
public ChatterGroupManager(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_groups = new ConcurrentDictionary<string, Group>();
|
||||
_chatters = new ConcurrentDictionary<long, ICollection<string>>();
|
||||
}
|
||||
|
||||
public void Add(Group group) {
|
||||
public void Add(Group group)
|
||||
{
|
||||
_groups.Add(group.Name, group);
|
||||
}
|
||||
|
||||
public void Add(long chatter, string groupName) {
|
||||
public void Add(long chatter, string groupName)
|
||||
{
|
||||
_chatters.Add(chatter, new List<string>() { groupName });
|
||||
}
|
||||
|
||||
public void Add(long chatter, ICollection<string> groupNames) {
|
||||
if (_chatters.TryGetValue(chatter, out var list)) {
|
||||
public void Add(long chatter, ICollection<string> groupNames)
|
||||
{
|
||||
if (_chatters.TryGetValue(chatter, out var list))
|
||||
{
|
||||
foreach (var group in groupNames)
|
||||
list.Add(group);
|
||||
} else
|
||||
}
|
||||
else
|
||||
_chatters.Add(chatter, groupNames);
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
public void Clear()
|
||||
{
|
||||
_groups.Clear();
|
||||
_chatters.Clear();
|
||||
}
|
||||
|
||||
public Group? Get(string groupName) {
|
||||
public Group? Get(string groupName)
|
||||
{
|
||||
if (_groups.TryGetValue(groupName, out var group))
|
||||
return group;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetGroupNamesFor(long chatter) {
|
||||
public IEnumerable<string> GetGroupNamesFor(long chatter)
|
||||
{
|
||||
if (_chatters.TryGetValue(chatter, out var groups))
|
||||
return groups.Select(g => _groups[g].Name);
|
||||
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int GetPriorityFor(long chatter) {
|
||||
public int GetPriorityFor(long chatter)
|
||||
{
|
||||
if (!_chatters.TryGetValue(chatter, out var groups))
|
||||
return 0;
|
||||
|
||||
|
||||
return GetPriorityFor(groups);
|
||||
}
|
||||
|
||||
public int GetPriorityFor(IEnumerable<string> groupNames) {
|
||||
public int GetPriorityFor(IEnumerable<string> groupNames)
|
||||
{
|
||||
var values = groupNames.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
|
||||
if (values.Any())
|
||||
return values.Max(g => g.Priority);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool Remove(long chatterId, string groupId) {
|
||||
if (_chatters.TryGetValue(chatterId, out var groups)) {
|
||||
public bool Remove(long chatterId, string groupId)
|
||||
{
|
||||
if (_chatters.TryGetValue(chatterId, out var groups))
|
||||
{
|
||||
groups.Remove(groupId);
|
||||
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId]}][group id: {groupId}]");
|
||||
return true;
|
||||
|
@ -23,6 +23,13 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
return res;
|
||||
}
|
||||
|
||||
public bool? CheckIfDirectAllowed(string path)
|
||||
{
|
||||
var res = Get(path)?.DirectAllow;
|
||||
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
|
||||
return res;
|
||||
}
|
||||
|
||||
public bool? CheckIfAllowed(IEnumerable<string> groups, string path)
|
||||
{
|
||||
bool overall = false;
|
||||
@ -37,6 +44,20 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
return overall ? true : null;
|
||||
}
|
||||
|
||||
public bool? CheckIfDirectAllowed(IEnumerable<string> groups, string path)
|
||||
{
|
||||
bool overall = false;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var result = CheckIfDirectAllowed($"{group}.{path}");
|
||||
if (result == false)
|
||||
return false;
|
||||
if (result == true)
|
||||
overall = true;
|
||||
}
|
||||
return overall ? true : null;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_root.Clear();
|
||||
@ -104,6 +125,7 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
}
|
||||
set => _allow = value;
|
||||
}
|
||||
public bool? DirectAllow { get => _allow; }
|
||||
|
||||
internal PermissionNode? Parent { get => _parent; }
|
||||
public IList<PermissionNode>? Children { get => _children == null ? null : new ReadOnlyCollection<PermissionNode>(_children); }
|
||||
|
@ -5,6 +5,8 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
|
||||
void Set(string path, bool? allow);
|
||||
bool? CheckIfAllowed(string path);
|
||||
bool? CheckIfAllowed(IEnumerable<string> groups, string path);
|
||||
bool? CheckIfDirectAllowed(string path);
|
||||
bool? CheckIfDirectAllowed(IEnumerable<string> groups, string path);
|
||||
void Clear();
|
||||
bool Remove(string path);
|
||||
}
|
||||
|
@ -101,13 +101,14 @@ public class TTSPlayer
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAll(long chatterId)
|
||||
public void RemoveAll(long broadcasterId, long chatterId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_mutex2.WaitOne();
|
||||
if (_buffer.UnorderedItems.Any(i => i.Element.ChatterId == chatterId)) {
|
||||
var list = _buffer.UnorderedItems.Where(i => i.Element.ChatterId != chatterId).ToArray();
|
||||
if (_buffer.UnorderedItems.Any(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId == chatterId))
|
||||
{
|
||||
var list = _buffer.UnorderedItems.Where(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId != chatterId).ToArray();
|
||||
_buffer.Clear();
|
||||
foreach (var item in list)
|
||||
_buffer.Enqueue(item.Element, item.Element.Priority);
|
||||
@ -121,8 +122,9 @@ public class TTSPlayer
|
||||
try
|
||||
{
|
||||
_mutex.WaitOne();
|
||||
if (_messages.UnorderedItems.Any(i => i.Element.ChatterId == chatterId)) {
|
||||
var list = _messages.UnorderedItems.Where(i => i.Element.ChatterId != chatterId).ToArray();
|
||||
if (_messages.UnorderedItems.Any(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId == chatterId))
|
||||
{
|
||||
var list = _messages.UnorderedItems.Where(i => i.Element.RoomId == broadcasterId && i.Element.ChatterId != chatterId).ToArray();
|
||||
_messages.Clear();
|
||||
foreach (var item in list)
|
||||
_messages.Enqueue(item.Element, item.Element.Priority);
|
||||
@ -139,7 +141,8 @@ public class TTSPlayer
|
||||
try
|
||||
{
|
||||
_mutex2.WaitOne();
|
||||
if (_buffer.UnorderedItems.Any(i => i.Element.MessageId == messageId)) {
|
||||
if (_buffer.UnorderedItems.Any(i => i.Element.MessageId == messageId))
|
||||
{
|
||||
var list = _buffer.UnorderedItems.Where(i => i.Element.MessageId != messageId).ToArray();
|
||||
_buffer.Clear();
|
||||
foreach (var item in list)
|
||||
@ -155,7 +158,8 @@ public class TTSPlayer
|
||||
try
|
||||
{
|
||||
_mutex.WaitOne();
|
||||
if (_messages.UnorderedItems.Any(i => i.Element.MessageId == messageId)) {
|
||||
if (_messages.UnorderedItems.Any(i => i.Element.MessageId == messageId))
|
||||
{
|
||||
var list = _messages.UnorderedItems.Where(i => i.Element.MessageId != messageId).ToArray();
|
||||
_messages.Clear();
|
||||
foreach (var item in list)
|
||||
@ -182,6 +186,7 @@ public class TTSPlayer
|
||||
public class TTSMessage
|
||||
{
|
||||
public string? Voice { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public long ChatterId { get; set; }
|
||||
public string MessageId { get; set; }
|
||||
public string? Message { get; set; }
|
||||
|
@ -43,5 +43,15 @@ namespace TwitchChatTTS.Helpers
|
||||
{
|
||||
return await _client.PostAsJsonAsync(uri, new object(), _options);
|
||||
}
|
||||
|
||||
public async Task<T?> Delete<T>(string uri)
|
||||
{
|
||||
return await _client.DeleteFromJsonAsync<T>(uri, _options);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> Delete(string uri)
|
||||
{
|
||||
return await _client.DeleteAsync(uri);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
public class Account {
|
||||
public string Id { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Role { get; set; }
|
||||
public string? BroadcasterId { get; set; }
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
namespace TwitchChatTTS.Hermes
|
||||
{
|
||||
public interface ICustomDataManager {
|
||||
public interface ICustomDataManager
|
||||
{
|
||||
void Add(string key, object value, string type);
|
||||
void Change(string key, object value);
|
||||
void Delete(string key);
|
||||
@ -11,7 +12,8 @@ namespace TwitchChatTTS.Hermes
|
||||
{
|
||||
private IDictionary<string, DataInfo> _data;
|
||||
|
||||
public CustomDataManager() {
|
||||
public CustomDataManager()
|
||||
{
|
||||
_data = new Dictionary<string, DataInfo>();
|
||||
}
|
||||
|
||||
@ -37,8 +39,9 @@ namespace TwitchChatTTS.Hermes
|
||||
}
|
||||
}
|
||||
|
||||
// type: text (string), whole number (int), number (double), boolean, formula (string, data type of number)
|
||||
public struct DataInfo {
|
||||
// type: text (string), whole number (int), number (double), boolean, formula (string, data type of number)
|
||||
public struct DataInfo
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
public object Value { get; set; }
|
||||
|
@ -10,7 +10,7 @@ public class HermesApiClient
|
||||
private readonly TwitchBotAuth _token;
|
||||
private readonly WebClientWrap _web;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
||||
public const string BASE_URL = "tomtospeech.com";
|
||||
|
||||
public HermesApiClient(TwitchBotAuth token, Configuration configuration, ILogger logger)
|
||||
@ -31,7 +31,7 @@ public class HermesApiClient
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> AuthorizeTwitch()
|
||||
public async Task<TwitchBotAuth?> AuthorizeTwitch()
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -51,10 +51,9 @@ public class HermesApiClient
|
||||
else if (authorize != null)
|
||||
{
|
||||
_logger.Error("Twitch API Authorization failed: " + authorize.AccessToken + " | " + authorize.RefreshToken + " | " + authorize.UserId + " | " + authorize.BroadcasterId);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
_logger.Debug($"Authorized Twitch API.");
|
||||
return true;
|
||||
return _token;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
@ -64,7 +63,7 @@ public class HermesApiClient
|
||||
{
|
||||
_logger.Error(e, "Failed to authorize to Twitch API.");
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<TTSVersion?> GetLatestTTSVersion()
|
||||
@ -74,7 +73,10 @@ public class HermesApiClient
|
||||
|
||||
public async Task<Account> FetchHermesAccountDetails()
|
||||
{
|
||||
var account = await _web.GetJson<Account>($"https://{BASE_URL}/api/account");
|
||||
var account = await _web.GetJson<Account>($"https://{BASE_URL}/api/account", new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
if (account == null || account.Id == null || account.Username == null)
|
||||
throw new NullReferenceException("Invalid value found while fetching for hermes account data.");
|
||||
return account;
|
||||
|
@ -18,7 +18,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
public class RequestAckHandler : IWebSocketHandler
|
||||
{
|
||||
private User _user;
|
||||
//private readonly RedemptionManager _redemptionManager;
|
||||
private readonly ICallbackManager<HermesRequestData> _callbackManager;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
@ -30,16 +29,16 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
|
||||
|
||||
public RequestAckHandler(
|
||||
User user,
|
||||
ICallbackManager<HermesRequestData> callbackManager,
|
||||
IServiceProvider serviceProvider,
|
||||
User user,
|
||||
JsonSerializerOptions options,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_callbackManager = callbackManager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_user = user;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
@ -263,15 +262,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
re.Match(string.Empty);
|
||||
filter.Regex = re;
|
||||
}
|
||||
catch (Exception e) { }
|
||||
catch (Exception) { }
|
||||
}
|
||||
_user.RegexFilters = filters;
|
||||
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count()}] have been refreshed.");
|
||||
}
|
||||
else if (message.Request.Type == "update_tts_voice_state")
|
||||
{
|
||||
string voiceId = message.Request.Data["voice"].ToString();
|
||||
bool state = message.Request.Data["state"].ToString().ToLower() == "true";
|
||||
string voiceId = message.Request.Data?["voice"].ToString()!;
|
||||
bool state = message.Request.Data?["state"].ToString()!.ToLower() == "true";
|
||||
|
||||
if (!_user.VoicesAvailable.TryGetValue(voiceId, out string? voiceName) || voiceName == null)
|
||||
{
|
||||
@ -305,14 +304,14 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
|
||||
_logger.Warning("Failed to read the redeemable actions for redemptions.");
|
||||
return;
|
||||
}
|
||||
if (hermesRequestData?.Data == null || !(hermesRequestData.Data["redemptions"] is IEnumerable<Redemption> redemptions))
|
||||
if (hermesRequestData?.Data == null || hermesRequestData.Data["redemptions"] is not IEnumerable<Redemption> redemptions)
|
||||
{
|
||||
_logger.Warning("Failed to read the redemptions while updating redemption actions.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Information($"Redeemable actions [count: {actions.Count()}] loaded.");
|
||||
var redemptionManager = _serviceProvider.GetRequiredService<RedemptionManager>();
|
||||
var redemptionManager = _serviceProvider.GetRequiredService<IRedemptionManager>();
|
||||
redemptionManager.Initialize(redemptions, actions.ToDictionary(a => a.Name, a => a));
|
||||
}
|
||||
else if (message.Request.Type == "get_default_tts_voice")
|
||||
|
@ -104,7 +104,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchChatterIdentifiers() {
|
||||
public async Task FetchChatterIdentifiers()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_chatter_ids",
|
||||
@ -112,7 +113,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchDefaultTTSVoice() {
|
||||
public async Task FetchDefaultTTSVoice()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_default_tts_voice",
|
||||
@ -120,7 +122,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchEmotes() {
|
||||
public async Task FetchEmotes()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_emotes",
|
||||
@ -128,7 +131,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchEnabledTTSVoices() {
|
||||
public async Task FetchEnabledTTSVoices()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_enabled_tts_voices",
|
||||
@ -136,7 +140,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchTTSVoices() {
|
||||
public async Task FetchTTSVoices()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_voices",
|
||||
@ -144,7 +149,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchTTSChatterVoices() {
|
||||
public async Task FetchTTSChatterVoices()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_users",
|
||||
@ -152,7 +158,8 @@ namespace TwitchChatTTS.Hermes.Socket
|
||||
});
|
||||
}
|
||||
|
||||
public async Task FetchTTSWordFilters() {
|
||||
public async Task FetchTTSWordFilters()
|
||||
{
|
||||
await Send(3, new RequestMessage()
|
||||
{
|
||||
Type = "get_tts_word_filters",
|
||||
|
@ -23,7 +23,7 @@ namespace TwitchChatTTS.OBS.Socket
|
||||
public bool Identified { get; set; }
|
||||
public bool Streaming { get; set; }
|
||||
|
||||
|
||||
|
||||
public OBSSocketClient(
|
||||
Configuration configuration,
|
||||
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
|
||||
@ -104,7 +104,8 @@ namespace TwitchChatTTS.OBS.Socket
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteRequest(RequestResponseMessage message) {
|
||||
public async Task ExecuteRequest(RequestResponseMessage message)
|
||||
{
|
||||
if (!_handlers.TryGetValue(7, out var handler) || handler == null)
|
||||
{
|
||||
_logger.Error("Failed to find the request response handler for OBS.");
|
||||
|
@ -54,7 +54,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
{
|
||||
if (removing)
|
||||
{
|
||||
if (_emotes.Get(o.Name) != o.Id) {
|
||||
if (_emotes.Get(o.Name) != o.Id)
|
||||
{
|
||||
_logger.Warning("Mismatched emote found while removing a 7tv emote.");
|
||||
continue;
|
||||
}
|
||||
@ -63,7 +64,8 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
|
||||
}
|
||||
else if (updater != null)
|
||||
{
|
||||
if (_emotes.Get(o.Name) != o.Id) {
|
||||
if (_emotes.Get(o.Name) != o.Id)
|
||||
{
|
||||
_logger.Warning("Mismatched emote found while updating a 7tv emote.");
|
||||
continue;
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ namespace TwitchChatTTS.Seven.Socket
|
||||
_logger.Warning($"Received end of stream message for 7tv websocket [reason: {_errorCodes[code]}][code: {code}]");
|
||||
else
|
||||
_logger.Warning($"Received end of stream message for 7tv websocket [code: {code}]");
|
||||
|
||||
|
||||
if (code < 0 || code >= _reconnectDelay.Length)
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
else if (_reconnectDelay[code] < 0)
|
||||
@ -131,7 +131,8 @@ namespace TwitchChatTTS.Seven.Socket
|
||||
else if (_reconnectDelay[code] > 0)
|
||||
await Task.Delay(_reconnectDelay[code]);
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
_logger.Warning("Unknown 7tv disconnection.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
31
Startup.cs
31
Startup.cs
@ -28,6 +28,7 @@ using static TwitchChatTTS.Chat.Commands.TTSCommands;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using TwitchChatTTS.Twitch.Socket.Handlers;
|
||||
using CommonSocketLibrary.Backoff;
|
||||
|
||||
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
|
||||
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
|
||||
@ -43,13 +44,10 @@ var deserializer = new DeserializerBuilder()
|
||||
|
||||
var configContent = File.ReadAllText("tts.config.yml");
|
||||
var configuration = deserializer.Deserialize<Configuration>(configContent);
|
||||
s.AddSingleton<Configuration>(configuration);
|
||||
s.AddSingleton(configuration);
|
||||
|
||||
var logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
//.MinimumLevel.Override("TwitchLib.Communication.Clients.WebSocketClient", LogEventLevel.Warning)
|
||||
//.MinimumLevel.Override("TwitchLib.PubSub.TwitchPubSub", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("TwitchLib", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("mariuszgromada", LogEventLevel.Error)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.File("logs/log-.log", restrictedToMinimumLevel: LogEventLevel.Debug, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 3)
|
||||
@ -57,11 +55,11 @@ var logger = new LoggerConfiguration()
|
||||
.CreateLogger();
|
||||
|
||||
s.AddSerilog(logger);
|
||||
s.AddSingleton<User>(new User());
|
||||
s.AddSingleton<User>();
|
||||
s.AddSingleton<AudioPlaybackEngine>();
|
||||
s.AddSingleton<ICallbackManager<HermesRequestData>, CallbackManager<HermesRequestData>>();
|
||||
|
||||
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
|
||||
s.AddSingleton(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
@ -77,10 +75,11 @@ s.AddSingleton<IChatCommand, VersionCommand>();
|
||||
s.AddSingleton<ICommandBuilder, CommandBuilder>();
|
||||
s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
|
||||
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
|
||||
s.AddSingleton<CommandManager>();
|
||||
s.AddSingleton<ICommandFactory, CommandFactory>();
|
||||
s.AddSingleton<ICommandManager, CommandManager>();
|
||||
|
||||
s.AddSingleton<TTSPlayer>();
|
||||
s.AddSingleton<RedemptionManager>();
|
||||
s.AddSingleton<IRedemptionManager, RedemptionManager>();
|
||||
s.AddSingleton<HermesApiClient>();
|
||||
s.AddSingleton<TwitchBotAuth>();
|
||||
s.AddSingleton<TwitchApiClient>();
|
||||
@ -109,19 +108,33 @@ s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManag
|
||||
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
|
||||
|
||||
// twitch websocket
|
||||
s.AddKeyedSingleton<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch");
|
||||
s.AddKeyedSingleton<IBackoff>("twitch", new ExponentialBackoff(1000, 120 * 1000));
|
||||
s.AddSingleton<ITwitchConnectionManager, TwitchConnectionManager>();
|
||||
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch", (sp, _) =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<ITwitchConnectionManager>();
|
||||
var client = factory.GetWorkingClient();
|
||||
client.Connect().Wait();
|
||||
return client;
|
||||
});
|
||||
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create");
|
||||
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, SessionKeepAliveHandler>("twitch");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, SessionWelcomeHandler>("twitch");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, SessionReconnectHandler>("twitch");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, NotificationHandler>("twitch");
|
||||
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelAdBreakHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelBanHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatMessageHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatClearUserHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatClearHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelChatDeleteMessageHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelCustomRedemptionHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelFollowHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelResubscriptionHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications");
|
||||
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionGiftHandler>("twitch-notifications");
|
||||
|
||||
// hermes websocket
|
||||
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
|
||||
|
83
TTS.cs
83
TTS.cs
@ -13,6 +13,7 @@ using CommonSocketLibrary.Common;
|
||||
using TwitchChatTTS.OBS.Socket;
|
||||
using TwitchChatTTS.Twitch.Socket.Messages;
|
||||
using TwitchChatTTS.Twitch.Socket;
|
||||
using TwitchChatTTS.Chat.Commands;
|
||||
|
||||
namespace TwitchChatTTS
|
||||
{
|
||||
@ -24,45 +25,51 @@ namespace TwitchChatTTS
|
||||
private readonly User _user;
|
||||
private readonly HermesApiClient _hermesApiClient;
|
||||
private readonly SevenApiClient _sevenApiClient;
|
||||
private readonly TwitchApiClient _twitchApiClient;
|
||||
private readonly HermesSocketClient _hermes;
|
||||
private readonly OBSSocketClient _obs;
|
||||
private readonly SevenSocketClient _seven;
|
||||
private readonly TwitchWebsocketClient _twitch;
|
||||
private readonly ICommandFactory _commandFactory;
|
||||
private readonly ICommandManager _commandManager;
|
||||
private readonly IEmoteDatabase _emotes;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly TTSPlayer _player;
|
||||
private readonly AudioPlaybackEngine _playback;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public TTS(
|
||||
User user,
|
||||
HermesApiClient hermesApiClient,
|
||||
SevenApiClient sevenApiClient,
|
||||
TwitchApiClient twitchApiClient,
|
||||
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
|
||||
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
|
||||
[FromKeyedServices("7tv")] SocketClient<WebSocketMessage> seven,
|
||||
[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
|
||||
ICommandFactory commandFactory,
|
||||
ICommandManager commandManager,
|
||||
IEmoteDatabase emotes,
|
||||
Configuration configuration,
|
||||
TTSPlayer player,
|
||||
AudioPlaybackEngine playback,
|
||||
IServiceProvider serviceProvider,
|
||||
Configuration configuration,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_user = user;
|
||||
_hermesApiClient = hermesApiClient;
|
||||
_sevenApiClient = sevenApiClient;
|
||||
_twitchApiClient = twitchApiClient;
|
||||
_hermes = (hermes as HermesSocketClient)!;
|
||||
_obs = (obs as OBSSocketClient)!;
|
||||
_seven = (seven as SevenSocketClient)!;
|
||||
_twitch = (twitch as TwitchWebsocketClient)!;
|
||||
_commandFactory = commandFactory;
|
||||
_commandManager = commandManager;
|
||||
_emotes = emotes;
|
||||
_configuration = configuration;
|
||||
_player = player;
|
||||
_playback = playback;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -91,6 +98,7 @@ namespace TwitchChatTTS
|
||||
await Task.Delay(15 * 1000);
|
||||
}
|
||||
|
||||
await _twitch.Connect();
|
||||
await InitializeHermesWebsocket();
|
||||
try
|
||||
{
|
||||
@ -98,29 +106,33 @@ namespace TwitchChatTTS
|
||||
_user.HermesUserId = hermesAccount.Id;
|
||||
_user.HermesUsername = hermesAccount.Username;
|
||||
_user.TwitchUsername = hermesAccount.Username;
|
||||
_user.TwitchUserId = long.Parse(hermesAccount.BroadcasterId);
|
||||
}
|
||||
catch (ArgumentNullException)
|
||||
{
|
||||
_logger.Error("Ensure you have your Twitch account linked to TTS.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
return;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
_logger.Error("Ensure you have your Twitch account linked to TTS.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to initialize properly. Restart app please.");
|
||||
await Task.Delay(30 * 1000);
|
||||
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||
return;
|
||||
}
|
||||
|
||||
await _hermesApiClient.AuthorizeTwitch();
|
||||
var twitchBotToken = await _hermesApiClient.FetchTwitchBotToken();
|
||||
_user.TwitchUserId = long.Parse(twitchBotToken.BroadcasterId!);
|
||||
_logger.Information($"Username: {_user.TwitchUsername} [id: {_user.TwitchUserId}]");
|
||||
|
||||
var twitchapiclient2 = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
||||
twitchapiclient2.Initialize(twitchBotToken);
|
||||
|
||||
_twitch.Initialize();
|
||||
await _twitch.Connect();
|
||||
|
||||
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId.ToString());
|
||||
if (emoteSet != null)
|
||||
_user.SevenEmoteSetId = emoteSet.Id;
|
||||
|
||||
_commandManager.Update(_commandFactory);
|
||||
|
||||
await InitializeEmotes(_sevenApiClient, emoteSet);
|
||||
await InitializeSevenTv();
|
||||
await InitializeObs();
|
||||
@ -265,43 +277,6 @@ namespace TwitchChatTTS
|
||||
}
|
||||
}
|
||||
|
||||
// private async Task<TwitchApiClient?> InitializeTwitchApiClient(string username)
|
||||
// {
|
||||
// _logger.Debug("Initializing twitch client.");
|
||||
|
||||
// var hermesapiclient = _serviceProvider.GetRequiredService<HermesApiClient>();
|
||||
// if (!await hermesapiclient.AuthorizeTwitch())
|
||||
// {
|
||||
// _logger.Error("Cannot connect to Twitch API.");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// var twitchapiclient = _serviceProvider.GetRequiredService<TwitchApiClient>();
|
||||
// var channels = _configuration.Twitch?.Channels ?? [username];
|
||||
// _logger.Information("Twitch channels: " + string.Join(", ", channels));
|
||||
// twitchapiclient.InitializeClient(username, channels);
|
||||
// twitchapiclient.InitializePublisher();
|
||||
|
||||
// var handler = _serviceProvider.GetRequiredService<ChatMessageHandler>();
|
||||
// twitchapiclient.AddOnNewMessageReceived(async (object? s, OnMessageReceivedArgs e) =>
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var result = await handler.Handle(e);
|
||||
// if (result.Status != MessageStatus.None || result.Emotes == null || !result.Emotes.Any())
|
||||
// return;
|
||||
|
||||
// await _hermes.SendEmoteUsage(e.ChatMessage.Id, result.ChatterId, result.Emotes);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.Error(ex, "Unable to either execute a command or to send emote usage message.");
|
||||
// }
|
||||
// });
|
||||
|
||||
// return twitchapiclient;
|
||||
// }
|
||||
|
||||
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
|
||||
{
|
||||
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();
|
||||
|
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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user