Compare commits

...

5 Commits

37 changed files with 920 additions and 345 deletions

View File

@@ -102,7 +102,7 @@ namespace TwitchChatTTS.Chat.Commands.Limits
private IDictionary<T, UserUsageData> _usages { get; } private IDictionary<T, UserUsageData> _usages { get; }
private IList<UsagePolicyNode<T>> _children { get; } private IList<UsagePolicyNode<T>> _children { get; }
private ILogger _logger; private ILogger _logger;
private object _lock { get; } private ReaderWriterLockSlim _rwls { get; }
public UsagePolicyNode(string name, UsagePolicyLimit? data, UsagePolicyNode<T>? parent, ILogger logger, bool root = false) public UsagePolicyNode(string name, UsagePolicyLimit? data, UsagePolicyNode<T>? parent, ILogger logger, bool root = false)
{ {
@@ -114,11 +114,14 @@ namespace TwitchChatTTS.Chat.Commands.Limits
_usages = new Dictionary<T, UserUsageData>(); _usages = new Dictionary<T, UserUsageData>();
_children = new List<UsagePolicyNode<T>>(); _children = new List<UsagePolicyNode<T>>();
_logger = logger; _logger = logger;
_lock = new object(); _rwls = new ReaderWriterLockSlim();
} }
public UsagePolicyNode<T>? Get(IEnumerable<string> path) public UsagePolicyNode<T>? Get(IEnumerable<string> path)
{
_rwls.EnterReadLock();
try
{ {
if (!path.Any()) if (!path.Any())
return this; return this;
@@ -129,8 +132,16 @@ namespace TwitchChatTTS.Chat.Commands.Limits
return this; return this;
return next.Get(path.Skip(1)); return next.Get(path.Skip(1));
} }
finally
{
_rwls.ExitReadLock();
}
}
public UsagePolicyNode<T>? Remove(IEnumerable<string> path) public UsagePolicyNode<T>? Remove(IEnumerable<string> path)
{
_rwls.EnterWriteLock();
try
{ {
if (!path.Any()) if (!path.Any())
{ {
@@ -148,8 +159,16 @@ namespace TwitchChatTTS.Chat.Commands.Limits
return null; return null;
return next.Remove(path.Skip(1)); return next.Remove(path.Skip(1));
} }
finally
{
_rwls.ExitWriteLock();
}
}
public void Set(IEnumerable<string> path, int count, TimeSpan span) public void Set(IEnumerable<string> path, int count, TimeSpan span)
{
_rwls.EnterWriteLock();
try
{ {
if (!path.Any()) if (!path.Any())
{ {
@@ -167,8 +186,16 @@ namespace TwitchChatTTS.Chat.Commands.Limits
} }
next.Set(path.Skip(1), count, span); next.Set(path.Skip(1), count, span);
} }
finally
{
_rwls.ExitWriteLock();
}
}
public bool TryUse(T key, DateTime timestamp) public bool TryUse(T key, DateTime timestamp)
{
_rwls.EnterUpgradeableReadLock();
try
{ {
if (_parent == null) if (_parent == null)
return false; return false;
@@ -176,24 +203,37 @@ namespace TwitchChatTTS.Chat.Commands.Limits
return _parent.TryUse(key, timestamp); return _parent.TryUse(key, timestamp);
UserUsageData? usage; UserUsageData? usage;
lock (_lock)
{
if (!_usages.TryGetValue(key, out usage)) if (!_usages.TryGetValue(key, out usage))
{
_rwls.EnterWriteLock();
try
{ {
usage = new UserUsageData(Limit.Count, 1 % Limit.Count); usage = new UserUsageData(Limit.Count, 1 % Limit.Count);
usage.Uses[0] = timestamp; usage.Uses[0] = timestamp;
_usages.Add(key, usage); _usages.Add(key, usage);
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug($"internal use node create"); _logger.Debug($"internal use node create");
return true; return true;
} }
if (usage.Uses.Length != Limit.Count) if (usage.Uses.Length != Limit.Count)
{
_rwls.EnterWriteLock();
try
{ {
var sizeDiff = Math.Max(0, usage.Uses.Length - Limit.Count); var sizeDiff = Math.Max(0, usage.Uses.Length - Limit.Count);
var temp = usage.Uses.Skip(sizeDiff); var temp = usage.Uses.Skip(sizeDiff);
var tempSize = usage.Uses.Length - sizeDiff; var tempSize = usage.Uses.Length - sizeDiff;
usage.Uses = temp.Union(new DateTime[Math.Max(0, Limit.Count - tempSize)]).ToArray(); usage.Uses = temp.Union(new DateTime[Math.Max(0, Limit.Count - tempSize)]).ToArray();
} }
finally
{
_rwls.ExitWriteLock();
}
} }
// Attempt on parent node if policy has been abused. // Attempt on parent node if policy has been abused.
@@ -204,11 +244,21 @@ namespace TwitchChatTTS.Chat.Commands.Limits
} }
_logger.Debug($"internal use node normal [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]"); _logger.Debug($"internal use node normal [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]");
lock (_lock) _rwls.EnterWriteLock();
try
{ {
usage.Uses[usage.Index] = timestamp; usage.Uses[usage.Index] = timestamp;
usage.Index = (usage.Index + 1) % Limit.Count; usage.Index = (usage.Index + 1) % Limit.Count;
} }
finally
{
_rwls.ExitWriteLock();
}
}
finally
{
_rwls.ExitUpgradeableReadLock();
}
return true; return true;
} }

View File

@@ -1,7 +1,6 @@
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages; using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands; using static TwitchChatTTS.Chat.Commands.TTSCommands;
@@ -40,9 +39,9 @@ namespace TwitchChatTTS.Chat.Commands
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes) public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{ {
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}"); _logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.{TTS.PATCH_VERSION}");
await hermes.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}.{TTS.PATCH_VERSION}.");
} }
} }
} }

View File

@@ -3,7 +3,6 @@ namespace TwitchChatTTS.Chat.Emotes
public class EmoteDatabase : IEmoteDatabase public class EmoteDatabase : IEmoteDatabase
{ {
private readonly IDictionary<string, string> _emotes; private readonly IDictionary<string, string> _emotes;
public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); }
public EmoteDatabase() public EmoteDatabase()
{ {

View File

@@ -1,5 +1,3 @@
using System.Collections.Concurrent;
using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Requests.Messages;
using Serilog; using Serilog;
@@ -9,22 +7,35 @@ namespace TwitchChatTTS.Chat.Groups
{ {
private readonly IDictionary<string, Group> _groups; private readonly IDictionary<string, Group> _groups;
private readonly IDictionary<long, ICollection<string>> _chatters; private readonly IDictionary<long, ICollection<string>> _chatters;
private readonly ReaderWriterLockSlim _rwls;
private readonly ILogger _logger; private readonly ILogger _logger;
public ChatterGroupManager(ILogger logger) public ChatterGroupManager(ILogger logger)
{ {
_logger = logger; _logger = logger;
_groups = new ConcurrentDictionary<string, Group>(); _groups = new Dictionary<string, Group>();
_chatters = new ConcurrentDictionary<long, ICollection<string>>(); _chatters = new Dictionary<long, ICollection<string>>();
_rwls = new ReaderWriterLockSlim();
} }
public void Add(Group group) public void Add(Group group)
{
_rwls.EnterWriteLock();
try
{ {
_groups.Add(group.Id, group); _groups.Add(group.Id, group);
} }
finally
{
_rwls.ExitWriteLock();
}
}
public void Add(long chatterId, string groupId) public void Add(long chatterId, string groupId)
{
_rwls.EnterWriteLock();
try
{ {
if (_chatters.TryGetValue(chatterId, out var list)) if (_chatters.TryGetValue(chatterId, out var list))
{ {
@@ -34,8 +45,16 @@ namespace TwitchChatTTS.Chat.Groups
else else
_chatters.Add(chatterId, new List<string>() { groupId }); _chatters.Add(chatterId, new List<string>() { groupId });
} }
finally
{
_rwls.ExitWriteLock();
}
}
public void Add(long chatter, ICollection<string> groupIds) public void Add(long chatter, ICollection<string> groupIds)
{
_rwls.EnterWriteLock();
try
{ {
if (_chatters.TryGetValue(chatter, out var list)) if (_chatters.TryGetValue(chatter, out var list))
{ {
@@ -46,21 +65,45 @@ namespace TwitchChatTTS.Chat.Groups
else else
_chatters.Add(chatter, groupIds); _chatters.Add(chatter, groupIds);
} }
finally
{
_rwls.ExitWriteLock();
}
}
public void Clear() public void Clear()
{
_rwls.EnterWriteLock();
try
{ {
_groups.Clear(); _groups.Clear();
_chatters.Clear(); _chatters.Clear();
} }
finally
{
_rwls.ExitWriteLock();
}
}
public Group? Get(string groupId) public Group? Get(string groupId)
{
_rwls.EnterReadLock();
try
{ {
if (_groups.TryGetValue(groupId, out var group)) if (_groups.TryGetValue(groupId, out var group))
return group; return group;
return null; return null;
} }
finally
{
_rwls.ExitReadLock();
}
}
public IEnumerable<string> GetGroupNamesFor(long chatter) public IEnumerable<string> GetGroupNamesFor(long chatter)
{
_rwls.EnterReadLock();
try
{ {
if (_chatters.TryGetValue(chatter, out var groups)) if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups.TryGetValue(g, out var group) ? group.Name : null) return groups.Select(g => _groups.TryGetValue(g, out var group) ? group.Name : null)
@@ -69,29 +112,61 @@ namespace TwitchChatTTS.Chat.Groups
return Array.Empty<string>(); return Array.Empty<string>();
} }
finally
{
_rwls.ExitReadLock();
}
}
public int GetPriorityFor(long chatter) public int GetPriorityFor(long chatter)
{
_rwls.EnterReadLock();
try
{ {
if (!_chatters.TryGetValue(chatter, out var groups)) if (!_chatters.TryGetValue(chatter, out var groups))
return 0; return 0;
return GetPriorityFor(groups); return GetPriorityFor(groups);
} }
finally
{
_rwls.ExitReadLock();
}
}
public int GetPriorityFor(IEnumerable<string> groupIds) public int GetPriorityFor(IEnumerable<string> groupIds)
{
_rwls.EnterReadLock();
try
{ {
var values = groupIds.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null); var values = groupIds.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
if (values.Any()) if (values.Any())
return values.Max(g => g!.Priority); return values.Max(g => g!.Priority);
return 0; return 0;
} }
finally
{
_rwls.ExitReadLock();
}
}
public void Modify(Group group) public void Modify(Group group)
{
_rwls.EnterWriteLock();
try
{ {
_groups[group.Id] = group; _groups[group.Id] = group;
} }
finally
{
_rwls.ExitWriteLock();
}
}
public bool Remove(string groupId) public bool Remove(string groupId)
{
_rwls.EnterWriteLock();
try
{ {
if (_groups.Remove(groupId)) if (_groups.Remove(groupId))
{ {
@@ -101,8 +176,16 @@ namespace TwitchChatTTS.Chat.Groups
} }
return false; return false;
} }
finally
{
_rwls.ExitReadLock();
}
}
public bool Remove(long chatterId, string groupId) public bool Remove(long chatterId, string groupId)
{
_rwls.EnterWriteLock();
try
{ {
if (_chatters.TryGetValue(chatterId, out var groups)) if (_chatters.TryGetValue(chatterId, out var groups))
{ {
@@ -113,5 +196,10 @@ namespace TwitchChatTTS.Chat.Groups
_logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]"); _logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return false; return false;
} }
finally
{
_rwls.ExitReadLock();
}
}
} }
} }

View File

@@ -5,25 +5,38 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
{ {
public class GroupPermissionManager : IGroupPermissionManager public class GroupPermissionManager : IGroupPermissionManager
{ {
private PermissionNode _root; private readonly PermissionNode _root;
private ILogger _logger; private readonly ILogger _logger;
private readonly ReaderWriterLockSlim _rwls;
public GroupPermissionManager(ILogger logger) public GroupPermissionManager(ILogger logger)
{ {
_logger = logger; _logger = logger;
_root = new PermissionNode(string.Empty, null, null); _root = new PermissionNode(string.Empty, null, null);
_rwls = new ReaderWriterLockSlim();
} }
public bool? CheckIfAllowed(string path) public bool? CheckIfAllowed(string path)
{
_rwls.EnterReadLock();
try
{ {
var res = Get(path)!.Allow; var res = Get(path)!.Allow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"}"); _logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"}");
return res; return res;
} }
finally
{
_rwls.ExitReadLock();
}
}
public bool? CheckIfDirectAllowed(string path) public bool? CheckIfDirectAllowed(string path)
{
_rwls.EnterReadLock();
try
{ {
var node = Get(path, nullIfMissing: true); var node = Get(path, nullIfMissing: true);
if (node == null) if (node == null)
@@ -33,6 +46,11 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]"); _logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
return res; return res;
} }
finally
{
_rwls.ExitReadLock();
}
}
public bool? CheckIfAllowed(IEnumerable<string> groups, string path) public bool? CheckIfAllowed(IEnumerable<string> groups, string path)
{ {
@@ -63,16 +81,30 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
} }
public void Clear() public void Clear()
{
_rwls.EnterWriteLock();
try
{ {
_root.Clear(); _root.Clear();
} }
finally
{
_rwls.ExitWriteLock();
}
}
public bool Remove(string path) public bool Remove(string path)
{
_rwls.EnterUpgradeableReadLock();
try
{ {
var node = Get(path); var node = Get(path);
if (node == null || node.Parent == null) if (node == null || node.Parent == null)
return false; return false;
_rwls.EnterWriteLock();
try
{
var parts = path.Split('.'); var parts = path.Split('.');
var last = parts.Last(); var last = parts.Last();
if (parts.Length > 1 && parts[parts.Length - 1] == node.Parent.Name || parts.Length == 1 && node.Parent.Name == null) if (parts.Length > 1 && parts[parts.Length - 1] == node.Parent.Name || parts.Length == 1 && node.Parent.Name == null)
@@ -83,13 +115,31 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
} }
return false; return false;
} }
finally
{
_rwls.ExitWriteLock();
}
}
finally
{
_rwls.ExitUpgradeableReadLock();
}
}
public void Set(string path, bool? allow) public void Set(string path, bool? allow)
{
_rwls.EnterWriteLock();
try
{ {
var node = Get(path, true); var node = Get(path, true);
node!.Allow = allow; node!.Allow = allow;
_logger.Debug($"Permission Node ADD {path} = {allow?.ToString() ?? "null"}"); _logger.Debug($"Permission Node ADD {path} = {allow?.ToString() ?? "null"}");
} }
finally
{
_rwls.ExitWriteLock();
}
}
private PermissionNode? Get(string path, bool edit = false, bool nullIfMissing = false) private PermissionNode? Get(string path, bool edit = false, bool nullIfMissing = false)
{ {

View File

@@ -62,15 +62,15 @@ namespace TwitchChatTTS.Chat.Messaging
var emoteUsage = GetEmoteUsage(fragments); var emoteUsage = GetEmoteUsage(fragments);
var tasks = new List<Task>(); var tasks = new List<Task>();
if (_obs.Streaming && _configuration.Twitch?.Slave != true) if ((!_obs.Connected || _obs.Streaming) && !_user.Slave)
{ {
if (emoteUsage.NewEmotes.Any()) if (emoteUsage.NewEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes)); tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes));
if (emoteUsage.EmotesUsed.Any() && messageId != null && chatterId != null) if (emoteUsage.EmotesUsed.Any() && messageId != null && chatterId != null)
tasks.Add(_hermes.SendEmoteUsage(messageId, chatterId.Value, emoteUsage.EmotesUsed)); tasks.Add(_hermes.SendEmoteUsage(messageId, chatterId.Value, emoteUsage.EmotesUsed));
if (!string.IsNullOrEmpty(chatterLogin) && chatterId != null && !_user.Chatters.Contains(chatterId.Value)) if (chatterId.HasValue && !_user.Chatters.Contains(chatterId.Value))
{ {
tasks.Add(_hermes.SendChatterDetails(chatterId.Value, chatterLogin)); tasks.Add(_hermes.SendChatterDetails(chatterId.Value, chatterLogin!));
_user.Chatters.Add(chatterId.Value); _user.Chatters.Add(chatterId.Value);
} }
} }

View File

@@ -15,7 +15,6 @@ namespace TwitchChatTTS
public class TwitchConfiguration { public class TwitchConfiguration {
public bool TtsWhenOffline; public bool TtsWhenOffline;
public bool Slave;
public string? WebsocketUrl; public string? WebsocketUrl;
public string? ApiUrl; public string? ApiUrl;
} }

View File

@@ -0,0 +1,45 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Handlers
{
public class LoggingHandler : IWebSocketHandler
{
private readonly ILogger _logger;
public int OperationCode { get; } = 5;
public LoggingHandler(ILogger logger)
{
_logger = logger;
}
public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (data is not LoggingMessage message || message == null)
return Task.CompletedTask;
Action<Exception?, string> logging;
if (message.Level == HermesLoggingLevel.Trace)
logging = _logger.Verbose;
else if (message.Level == HermesLoggingLevel.Debug)
logging = _logger.Debug;
else if (message.Level == HermesLoggingLevel.Info)
logging = _logger.Information;
else if (message.Level == HermesLoggingLevel.Warn)
logging = _logger.Warning;
else if (message.Level == HermesLoggingLevel.Error)
logging = _logger.Error;
else if (message.Level == HermesLoggingLevel.Critical)
logging = _logger.Fatal;
else {
_logger.Warning("Failed to receive a logging level from client.");
return Task.CompletedTask;
}
logging.Invoke(message.Exception, message.Message);
return Task.CompletedTask;
}
}
}

View File

@@ -33,7 +33,6 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
if (message.AnotherClient) if (message.AnotherClient)
{ {
if (client.LoggedIn)
_logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}."); _logger.Warning($"Another client has connected to the same account via {(message.WebLogin ? "web login" : "application")}.");
return; return;
} }
@@ -43,18 +42,24 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
return; return;
} }
_user.Slave = message.Slave;
_logger.Information(_user.Slave ? "This client is not responsible for reacting to chat messages." : "This client is responsible for reacting to chat messages.");
_user.HermesUserId = message.UserId; _user.HermesUserId = message.UserId;
_user.HermesUsername = message.UserName; _user.HermesUsername = message.UserName;
_user.TwitchUsername = message.UserName; _user.TwitchUsername = message.UserName;
_user.TwitchUserId = long.Parse(message.ProviderAccountId); _user.TwitchUserId = long.Parse(message.ProviderAccountId);
_user.OwnerId = message.OwnerId; _user.OwnerId = message.OwnerId;
_user.StreamElementsOverlayKey = message.StreamElementsOverlayKey;
_user.DefaultTTSVoice = message.DefaultTTSVoice; _user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = new ConcurrentDictionary<string, string>(message.TTSVoicesAvailable); _user.VoicesAvailable = new ConcurrentDictionary<string, string>(message.TTSVoicesAvailable);
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices); _user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
_user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch"); _user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch") ?? message.Connections.FirstOrDefault(c => c.Type == "twitch");
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot"); _user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot") ?? message.Connections.FirstOrDefault(c => c.Type == "nightbot");
if (_user.TwitchConnection != null)
_bus.Send(this, "twitch id", _user.TwitchUserId); {
_logger.Debug("Twitch connection: " + _user.TwitchConnection.Name + " / " + _user.TwitchConnection.AccessToken);
}
var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToList(); var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
foreach (var filter in filters) foreach (var filter in filters)
@@ -65,7 +70,10 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
re.Match(string.Empty); re.Match(string.Empty);
filter.Regex = re; filter.Regex = re;
} }
catch (Exception) { } catch (Exception)
{
_logger.Warning($"Failed to create a regular expression for a TTS filter [filter id: {filter.Search}]");
}
} }
_user.RegexFilters = filters; _user.RegexFilters = filters;
@@ -94,6 +102,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_logger.Information("TTS is now ready."); _logger.Information("TTS is now ready.");
client.Ready = true; client.Ready = true;
_bus.Send(this, "tts_connected", _user);
} }
} }
} }

View File

@@ -0,0 +1,30 @@
using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Handlers
{
public class SlaveHandler : IWebSocketHandler
{
private readonly User _user;
private readonly ILogger _logger;
public int OperationCode { get; } = 9;
public SlaveHandler(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{
if (data is not SlaveMessage message || message == null)
return Task.CompletedTask;
_user.Slave = message.Slave;
_logger.Information(_user.Slave ? "Total chat message ownership was revoked." : "This client is now responsible for reacting to chat messages. Potential chat messages were missed while changing ownership.");
return Task.CompletedTask;
}
}
}

View File

@@ -28,7 +28,7 @@ namespace TwitchChatTTS.Hermes.Socket
public string? UserId { get; set; } public string? UserId { get; set; }
private readonly System.Timers.Timer _heartbeatTimer; private readonly System.Timers.Timer _heartbeatTimer;
private readonly IBackoff _backoff; private readonly IBackoff _backoff;
private readonly object _lock; private readonly ReaderWriterLockSlim _rwls;
public bool Connected { get; set; } public bool Connected { get; set; }
public bool LoggedIn { get; set; } public bool LoggedIn { get; set; }
@@ -62,7 +62,7 @@ namespace TwitchChatTTS.Hermes.Socket
LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow; LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow;
URL = $"wss://{BASE_URL}"; URL = $"wss://{BASE_URL}";
_lock = new object(); _rwls = new ReaderWriterLockSlim();
var ttsCreateUserVoice = _bus.GetTopic("tts.user.voice.create"); var ttsCreateUserVoice = _bus.GetTopic("tts.user.voice.create");
ttsCreateUserVoice.Subscribe(async data => await Send(3, new RequestMessage() ttsCreateUserVoice.Subscribe(async data => await Send(3, new RequestMessage()
@@ -82,23 +82,34 @@ namespace TwitchChatTTS.Hermes.Socket
public override async Task Connect() public override async Task Connect()
{ {
lock (_lock) _rwls.EnterReadLock();
try
{ {
if (Connected) if (Connected)
return; return;
} }
finally
{
_rwls.ExitReadLock();
}
_logger.Debug($"Attempting to connect to {URL}"); _logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL); await ConnectAsync(URL);
} }
private async Task Disconnect() private async Task Disconnect()
{ {
lock (_lock) _rwls.EnterReadLock();
try
{ {
if (!Connected) if (!Connected)
return; return;
} }
finally
{
_rwls.ExitReadLock();
}
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed")); await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
} }
@@ -247,17 +258,15 @@ namespace TwitchChatTTS.Hermes.Socket
public void Initialize() public void Initialize()
{ {
_logger.Information("Initializing Hermes websocket client."); _logger.Information("Initializing Tom to Speech websocket client.");
OnConnected += async (sender, e) => OnConnected += async (sender, e) =>
{
lock (_lock)
{ {
if (Connected) if (Connected)
return; return;
Connected = true; Connected = true;
}
_logger.Information("Hermes websocket client connected."); _logger.Information("Tom to Speech websocket client connected.");
_heartbeatTimer.Enabled = true; _heartbeatTimer.Enabled = true;
LastHeartbeatReceived = DateTime.UtcNow; LastHeartbeatReceived = DateTime.UtcNow;
@@ -267,21 +276,21 @@ namespace TwitchChatTTS.Hermes.Socket
ApiKey = _configuration.Hermes!.Token!, ApiKey = _configuration.Hermes!.Token!,
MajorVersion = TTS.MAJOR_VERSION, MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION, MinorVersion = TTS.MINOR_VERSION,
PatchVersion = TTS.PATCH_VERSION,
}); });
}; };
OnDisconnected += async (sender, e) => OnDisconnected += async (sender, e) =>
{
lock (_lock)
{ {
if (!Connected) if (!Connected)
return; return;
Connected = false;
}
Connected = false;
LoggedIn = false; LoggedIn = false;
Ready = false; Ready = false;
_logger.Warning("Hermes websocket client disconnected."); _user.Slave = true;
_logger.Warning("Tom to Speech websocket client disconnected.");
_heartbeatTimer.Enabled = false; _heartbeatTimer.Enabled = false;
await Reconnect(_backoff); await Reconnect(_backoff);
@@ -396,7 +405,7 @@ namespace TwitchChatTTS.Hermes.Socket
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to send a heartbeat back to the Hermes websocket server."); _logger.Error(ex, "Failed to send a heartbeat back to the Tom to Speech websocket server.");
} }
} }
else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120)) else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120))
@@ -407,7 +416,7 @@ namespace TwitchChatTTS.Hermes.Socket
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to disconnect from Hermes websocket server."); _logger.Error(ex, "Failed to disconnect from Tom to Speech websocket server.");
Ready = false; Ready = false;
LoggedIn = false; LoggedIn = false;
Connected = false; Connected = false;
@@ -420,12 +429,20 @@ namespace TwitchChatTTS.Hermes.Socket
} }
public new async Task Send<T>(int opcode, T message) public new async Task Send<T>(int opcode, T message)
{
_rwls.EnterReadLock();
try
{ {
if (!Connected) if (!Connected)
{ {
_logger.Warning("Hermes websocket client is not connected. Not sending a message."); _logger.Warning("Tom to Speech websocket client is not connected. Not sending a message.");
return; return;
} }
}
finally
{
_rwls.ExitReadLock();
}
await base.Send(opcode, message); await base.Send(opcode, message);
} }

View File

@@ -0,0 +1,52 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateGroupPermissionAck : IRequestAck
{
public string Name => "create_group_permission";
private readonly IChatterGroupManager _groups;
private readonly IGroupPermissionManager _permissions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreateGroupPermissionAck(IChatterGroupManager groups, IGroupPermissionManager permissions, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_permissions = permissions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (string.IsNullOrWhiteSpace(json))
{
_logger.Warning($"Group JSON data is null.");
return;
}
var permission = JsonSerializer.Deserialize<GroupPermission>(json, _options);
if (permission == null)
{
_logger.Warning($"Permission data is null.");
return;
}
var group = _groups.Get(permission.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Group id does not exist [group id: {permission.GroupId}][permission id: {permission.Id}]");
return;
}
_logger.Debug($"Adding permission to group [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][state: {permission.Allow?.ToString() ?? "Inherited"}]");
_permissions.Set(permission.Path, permission.Allow);
_logger.Information($"Permission has been added to group [path: {permission.Path}][state: {permission.Allow?.ToString() ?? "Inherited"}][group name: {group.Name}]");
}
}
}

View File

@@ -48,7 +48,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
} }
catch (Exception) catch (Exception)
{ {
_logger.Warning($"Failed to generate a Regular Expression using '{filter.Search}' [filter id: {filter.Id}]"); _logger.Warning($"Failed to create a regular expression for a TTS filter [filter id: {filter.Search}]");
} }
_logger.Debug($"Filter data [filter id: {filter.Id}][search: {filter.Search}][replace: {filter.Replace}]"); _logger.Debug($"Filter data [filter id: {filter.Id}][search: {filter.Search}][replace: {filter.Replace}]");

View File

@@ -33,13 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
return; return;
} }
var userId = requestData["user"].ToString();
var voiceId = requestData["voice"].ToString(); var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(userId))
{
_logger.Warning("User Id is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId)) if (string.IsNullOrEmpty(voiceId))
{ {
_logger.Warning("Voice Id is invalid."); _logger.Warning("Voice Id is invalid.");
@@ -52,7 +46,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
} }
_user.VoicesSelected.Add(chatterId, voiceId); _user.VoicesSelected.Add(chatterId, voiceId);
_logger.Information($"Created a new TTS user [user id: {userId}][voice id: {voiceId}][voice name: {voiceName}]."); _logger.Information($"Created a new TTS user [chatter id: {_user.TwitchUserId}][chatter id: {chatterId}][voice id: {voiceId}][voice name: {voiceName}].");
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System.Text.Json;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
@@ -8,13 +7,11 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
{ {
public string Name => "delete_group"; public string Name => "delete_group";
private readonly IChatterGroupManager _groups; private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
public DeleteGroupAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger) public DeleteGroupAck(IChatterGroupManager groups, ILogger logger)
{ {
_groups = groups; _groups = groups;
_options = options;
_logger = logger; _logger = logger;
} }

View File

@@ -23,7 +23,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
return; return;
} }
if (long.TryParse(requestData["chatter"].ToString(), out var chatterId)) if (!long.TryParse(requestData["chatter"].ToString(), out var chatterId))
{ {
_logger.Warning($"Chatter Id is invalid [chatter id: {chatterId}]"); _logger.Warning($"Chatter Id is invalid [chatter id: {chatterId}]");
return; return;

View File

@@ -0,0 +1,64 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteGroupPermissionAck : IRequestAck
{
public string Name => "delete_group_permission";
private readonly IChatterGroupManager _groups;
private readonly IGroupPermissionManager _permissions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public DeleteGroupPermissionAck(IChatterGroupManager groups, IGroupPermissionManager permissions, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_permissions = permissions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (requestData == null)
{
_logger.Warning("Request data is null.");
return;
}
if (!requestData.TryGetValue("id", out var permissionId))
{
_logger.Warning($"Permission Id could not be found.");
return;
}
if (string.IsNullOrWhiteSpace(json))
{
_logger.Warning($"Group JSON data is null.");
return;
}
var permission = JsonSerializer.Deserialize<GroupPermission>(json, _options);
if (permission == null)
{
_logger.Warning($"Permission data is null.");
return;
}
var group = _groups.Get(permission.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Group id does not exist [group id: {permission.GroupId}][permission id: {permission.Id}]");
return;
}
_logger.Debug($"Removing permission from group [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][state: {permission.Allow?.ToString() ?? "Inherited"}]");
_permissions.Remove(permissionId.ToString()!);
_logger.Information($"Permission has been removed from group [path: {permission.Path}][state: {permission.Allow?.ToString() ?? "Inherited"}][group name: {group.Name}]");
}
}
}

View File

@@ -54,7 +54,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
foreach (var permission in groupInfo.GroupPermissions) foreach (var permission in groupInfo.GroupPermissions)
{ {
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]"); _logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (!groupsById.TryGetValue(permission.GroupId, out var group)) if (!groupsById.TryGetValue(permission.GroupId.ToString(), out var group))
{ {
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); _logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue; continue;

View File

@@ -37,7 +37,10 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
re.Match(string.Empty); re.Match(string.Empty);
filter.Regex = re; filter.Regex = re;
} }
catch (Exception) { } catch (Exception)
{
_logger.Warning($"Failed to create a regular expression for a TTS filter [filter id: {filter.Search}]");
}
} }
_user.RegexFilters = filters; _user.RegexFilters = filters;
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed."); _logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");

View File

@@ -0,0 +1,52 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
using TwitchChatTTS.Chat.Groups.Permissions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateGroupPermissionAck : IRequestAck
{
public string Name => "update_group_permission";
private readonly IChatterGroupManager _groups;
private readonly IGroupPermissionManager _permissions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdateGroupPermissionAck(IChatterGroupManager groups, IGroupPermissionManager permissions, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_permissions = permissions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (string.IsNullOrWhiteSpace(json))
{
_logger.Warning($"Group JSON data is null.");
return;
}
var permission = JsonSerializer.Deserialize<GroupPermission>(json, _options);
if (permission == null)
{
_logger.Warning($"Permission data is null.");
return;
}
var group = _groups.Get(permission.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Group id does not exist [group id: {permission.GroupId}][permission id: {permission.Id}]");
return;
}
_logger.Debug($"Updating permission to group [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][state: {permission.Allow?.ToString() ?? "Inherited"}]");
_permissions.Set(permission.Path, permission.Allow);
_logger.Information($"Permission on group has been updated [path: {permission.Path}][state: {permission.Allow?.ToString() ?? "Inherited"}][group name: {group.Name}]");
}
}
}

View File

@@ -52,7 +52,10 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
re.Match(string.Empty); re.Match(string.Empty);
current.Regex = re; current.Regex = re;
} }
catch (Exception) { } catch (Exception)
{
_logger.Warning($"Failed to create a regular expression for a TTS filter [filter id: {filter.Search}]");
}
_logger.Information($"Filter has been updated [filter id: {filter.Id}]"); _logger.Information($"Filter has been updated [filter id: {filter.Id}]");
} }

View File

@@ -33,13 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
return; return;
} }
var userId = requestData["user"].ToString();
var voiceId = requestData["voice"].ToString(); var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(userId))
{
_logger.Warning("User Id is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId)) if (string.IsNullOrEmpty(voiceId))
{ {
_logger.Warning("Voice Id is invalid."); _logger.Warning("Voice Id is invalid.");
@@ -52,7 +46,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
} }
_user.VoicesSelected[chatterId] = voiceId; _user.VoicesSelected[chatterId] = voiceId;
_logger.Information($"Updated a TTS user's voice [user id: {userId}][voice: {voiceId}][voice name: {voiceName}]"); _logger.Information($"Updated a TTS user's voice [user id: {_user.TwitchUserId}][voice: {voiceId}][voice name: {voiceName}]");
} }
} }
} }

View File

@@ -4,6 +4,7 @@ namespace TwitchChatTTS.Hermes
{ {
public int MajorVersion { get; set; } public int MajorVersion { get; set; }
public int MinorVersion { get; set; } public int MinorVersion { get; set; }
public int? PatchVersion { get; set; }
public required string Download { get; set; } public required string Download { get; set; }
public required string Changelog { get; set; } public required string Changelog { get; set; }
} }

View File

@@ -4,7 +4,7 @@ namespace TwitchChatTTS.OBS.Socket.Data
{ {
public required string ObsWebSocketVersion { get; set; } public required string ObsWebSocketVersion { get; set; }
public int RpcVersion { get; set; } public int RpcVersion { get; set; }
public required AuthenticationMessage Authentication { get; set; } public AuthenticationMessage? Authentication { get; set; }
} }
public class AuthenticationMessage { public class AuthenticationMessage {

View File

@@ -35,7 +35,13 @@ public class SevenApiClient
{ {
_logger.Debug($"Fetching 7tv information using Twitch Id [twitch id: {twitchId}]"); _logger.Debug($"Fetching 7tv information using Twitch Id [twitch id: {twitchId}]");
var details = await _web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId); var details = await _web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
return details?.EmoteSet; _logger.Information($"Fetched 7tv emotes [count: {details?.EmoteSet.EmoteCount ?? -1}]");
if (details?.EmoteSet == null)
{
_logger.Warning("Could not find 7tv emotes linked to your Twitch account.");
return null;
}
return details.EmoteSet;
} }
catch (JsonException e) catch (JsonException e)
{ {

View File

@@ -9,25 +9,28 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{ {
public class DispatchHandler : IWebSocketHandler public class DispatchHandler : IWebSocketHandler
{ {
public int OperationCode { get; } = 0;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IEmoteDatabase _emotes; private readonly IEmoteDatabase _emotes;
private readonly object _lock = new object(); private readonly Mutex _lock;
public int OperationCode { get; } = 0;
public DispatchHandler(IEmoteDatabase emotes, ILogger logger) public DispatchHandler(IEmoteDatabase emotes, ILogger logger)
{ {
_emotes = emotes; _emotes = emotes;
_logger = logger; _logger = logger;
_lock = new Mutex();
} }
public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not DispatchMessage message || message == null) if (data is not DispatchMessage message || message == null || message.Body == null)
return Task.CompletedTask; return Task.CompletedTask;
ApplyChanges(message?.Body?.Pulled, cf => cf.OldValue, true);
ApplyChanges(message?.Body?.Pushed, cf => cf.Value, false); ApplyChanges(message.Body.Pulled, cf => cf.OldValue, true);
ApplyChanges(message?.Body?.Removed, cf => cf.OldValue, true); ApplyChanges(message.Body.Pushed, cf => cf.Value, false);
ApplyChanges(message?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value); ApplyChanges(message.Body.Removed, cf => cf.OldValue, true);
ApplyChanges(message.Body.Updated, cf => cf.OldValue, false, cf => cf.Value);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -42,7 +45,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (value == null) if (value == null)
continue; continue;
var o = JsonSerializer.Deserialize<EmoteField>(value.ToString(), new JsonSerializerOptions() var o = JsonSerializer.Deserialize<EmoteField>(value.ToString()!, new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
@@ -50,8 +53,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (o == null) if (o == null)
continue; continue;
lock (_lock) try
{ {
_lock.WaitOne();
if (removing) if (removing)
{ {
if (_emotes.Get(o.Name) != o.Id) if (_emotes.Get(o.Name) != o.Id)
@@ -71,8 +75,10 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
} }
_emotes.Remove(o.Name); _emotes.Remove(o.Name);
var update = updater(val); var update = updater(val);
if (update == null)
continue;
var u = JsonSerializer.Deserialize<EmoteField>(update.ToString(), new JsonSerializerOptions() var u = JsonSerializer.Deserialize<EmoteField>(update.ToString()!, new JsonSerializerOptions()
{ {
PropertyNameCaseInsensitive = false, PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
@@ -94,6 +100,10 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
_logger.Information($"Added 7tv emote [name: {o.Name}][id: {o.Id}]"); _logger.Information($"Added 7tv emote [name: {o.Name}][id: {o.Id}]");
} }
} }
finally
{
_lock.ReleaseMutex();
}
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System.Net.WebSockets;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;

View File

@@ -89,6 +89,7 @@ s.AddTransient<ICommandFactory, CommandFactory>();
s.AddSingleton<RequestAckManager>(); s.AddSingleton<RequestAckManager>();
s.AddTransient<IRequestAck, CreateGroupAck>(); s.AddTransient<IRequestAck, CreateGroupAck>();
s.AddTransient<IRequestAck, CreateGroupChatterAck>(); s.AddTransient<IRequestAck, CreateGroupChatterAck>();
s.AddTransient<IRequestAck, CreateGroupPermissionAck>();
s.AddTransient<IRequestAck, CreatePolicyAck>(); s.AddTransient<IRequestAck, CreatePolicyAck>();
s.AddTransient<IRequestAck, CreateRedeemableActionAck>(); s.AddTransient<IRequestAck, CreateRedeemableActionAck>();
s.AddTransient<IRequestAck, CreateRedemptionAck>(); s.AddTransient<IRequestAck, CreateRedemptionAck>();
@@ -97,6 +98,7 @@ s.AddTransient<IRequestAck, CreateTTSUserAck>();
s.AddTransient<IRequestAck, CreateTTSVoiceAck>(); s.AddTransient<IRequestAck, CreateTTSVoiceAck>();
s.AddTransient<IRequestAck, DeleteGroupAck>(); s.AddTransient<IRequestAck, DeleteGroupAck>();
s.AddTransient<IRequestAck, DeleteGroupChatterAck>(); s.AddTransient<IRequestAck, DeleteGroupChatterAck>();
s.AddTransient<IRequestAck, DeleteGroupPermissionAck>();
s.AddTransient<IRequestAck, DeletePolicyAck>(); s.AddTransient<IRequestAck, DeletePolicyAck>();
s.AddTransient<IRequestAck, DeleteRedeemableActionAck>(); s.AddTransient<IRequestAck, DeleteRedeemableActionAck>();
s.AddTransient<IRequestAck, DeleteRedemptionAck>(); s.AddTransient<IRequestAck, DeleteRedemptionAck>();
@@ -116,6 +118,7 @@ s.AddTransient<IRequestAck, GetTTSVoicesAck>();
s.AddTransient<IRequestAck, GetTTSWordFiltersAck>(); s.AddTransient<IRequestAck, GetTTSWordFiltersAck>();
s.AddTransient<IRequestAck, UpdateGroupAck>(); s.AddTransient<IRequestAck, UpdateGroupAck>();
s.AddTransient<IRequestAck, UpdateGroupChatterAck>(); s.AddTransient<IRequestAck, UpdateGroupChatterAck>();
s.AddTransient<IRequestAck, UpdateGroupPermissionAck>();
s.AddTransient<IRequestAck, UpdateDefaultTTSVoiceAck>(); s.AddTransient<IRequestAck, UpdateDefaultTTSVoiceAck>();
s.AddTransient<IRequestAck, UpdatePolicyAck>(); s.AddTransient<IRequestAck, UpdatePolicyAck>();
s.AddTransient<IRequestAck, UpdateRedeemableActionAck>(); s.AddTransient<IRequestAck, UpdateRedeemableActionAck>();
@@ -181,7 +184,6 @@ s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>
{ {
var factory = sp.GetRequiredService<ITwitchConnectionManager>(); var factory = sp.GetRequiredService<ITwitchConnectionManager>();
var client = factory.GetWorkingClient(); var client = factory.GetWorkingClient();
client.Connect().Wait();
return client; return client;
}); });
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create"); s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create");
@@ -210,6 +212,8 @@ s.AddKeyedSingleton<IBackoff>("hermes", new ExponentialBackoff(1000, 15 * 1000))
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes"); s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes"); s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes"); s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoggingHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, SlaveHandler>("hermes");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes"); s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");

99
TTS.cs
View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Serilog; using Serilog;
using NAudio.Wave.SampleProviders;
using org.mariuszgromada.math.mxparser; using org.mariuszgromada.math.mxparser;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Seven.Socket; using TwitchChatTTS.Seven.Socket;
@@ -13,17 +12,18 @@ using TwitchChatTTS.Twitch.Socket.Messages;
using TwitchChatTTS.Twitch.Socket; using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Chat.Commands;
using System.Text; using System.Text;
using TwitchChatTTS.Chat.Speech;
using TwitchChatTTS.Veadotube; using TwitchChatTTS.Veadotube;
using TwitchChatTTS.Bus; using TwitchChatTTS.Bus;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Net.WebSockets;
namespace TwitchChatTTS namespace TwitchChatTTS
{ {
public class TTS : IHostedService public class TTS : IHostedService
{ {
public const int MAJOR_VERSION = 4; public const int MAJOR_VERSION = 4;
public const int MINOR_VERSION = 7; public const int MINOR_VERSION = 9;
public const int PATCH_VERSION = 3;
private readonly User _user; private readonly User _user;
private readonly HermesApiClient _hermesApiClient; private readonly HermesApiClient _hermesApiClient;
@@ -36,8 +36,6 @@ namespace TwitchChatTTS
private readonly ICommandFactory _commandFactory; private readonly ICommandFactory _commandFactory;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly IEmoteDatabase _emotes; private readonly IEmoteDatabase _emotes;
private readonly TTSPlayer _player;
private readonly AudioPlaybackEngine _playback;
private readonly ServiceBusCentral _bus; private readonly ServiceBusCentral _bus;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger _logger; private readonly ILogger _logger;
@@ -54,8 +52,6 @@ namespace TwitchChatTTS
ICommandFactory commandFactory, ICommandFactory commandFactory,
ICommandManager commandManager, ICommandManager commandManager,
IEmoteDatabase emotes, IEmoteDatabase emotes,
TTSPlayer player,
AudioPlaybackEngine playback,
ServiceBusCentral bus, ServiceBusCentral bus,
Configuration configuration, Configuration configuration,
ILogger logger ILogger logger
@@ -72,8 +68,6 @@ namespace TwitchChatTTS
_commandFactory = commandFactory; _commandFactory = commandFactory;
_commandManager = commandManager; _commandManager = commandManager;
_emotes = emotes; _emotes = emotes;
_player = player;
_playback = playback;
_bus = bus; _bus = bus;
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
@@ -84,10 +78,13 @@ namespace TwitchChatTTS
Console.Title = "TTS - Twitch Chat"; Console.Title = "TTS - Twitch Chat";
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
License.iConfirmCommercialUse("abcdef"); License.iConfirmCommercialUse("abcdef");
_user.Slave = true;
_logger.Information($"This is running on version {MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}.");
if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token)) if (string.IsNullOrWhiteSpace(_configuration.Hermes?.Token))
{ {
_logger.Error("Hermes API token not set in the configuration file."); _logger.Error("Tom to Speech API token not set in the configuration file.");
return; return;
} }
@@ -99,9 +96,9 @@ namespace TwitchChatTTS
_logger.Error("Failed to fetch latest TTS version. Something went wrong."); _logger.Error("Failed to fetch latest TTS version. Something went wrong.");
return; return;
} }
if (hermesVersion.MajorVersion > TTS.MAJOR_VERSION || hermesVersion.MajorVersion == TTS.MAJOR_VERSION && hermesVersion.MinorVersion > TTS.MINOR_VERSION) if (hermesVersion.MajorVersion > MAJOR_VERSION || hermesVersion.MajorVersion == MAJOR_VERSION && (hermesVersion.MinorVersion > MINOR_VERSION || hermesVersion.MinorVersion == MINOR_VERSION && (hermesVersion.PatchVersion == null || hermesVersion.PatchVersion > PATCH_VERSION)))
{ {
_logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion} is available at {hermesVersion.Download}"); _logger.Information($"A new update for TTS is avaiable! Version {hermesVersion.MajorVersion}.{hermesVersion.MinorVersion}.{hermesVersion.PatchVersion} is available at {hermesVersion.Download}");
var changes = hermesVersion.Changelog.Split("\n"); var changes = hermesVersion.Changelog.Split("\n");
if (changes != null && changes.Any()) if (changes != null && changes.Any())
_logger.Information("Changelog:\n - " + string.Join("\n - ", changes) + "\n\n"); _logger.Information("Changelog:\n - " + string.Join("\n - ", changes) + "\n\n");
@@ -113,40 +110,45 @@ namespace TwitchChatTTS
_logger.Warning("Failed to check for version updates."); _logger.Warning("Failed to check for version updates.");
} }
var disposables = new List<IDisposable>();
var connected = _bus.GetTopic("tts_connected");
// 7tv // 7tv
var twitchTopic = _bus.GetTopic("twitch id"); disposables.Add(connected.FirstAsync().Subscribe(async (data) =>
twitchTopic.FirstAsync().Subscribe(async (data) =>
{ {
var twitchId = data.Value?.ToString(); if (data.Value is not User user)
if (twitchId == null) {
_logger.Warning("Something went wrong. Unable to fetch 7tv data.");
return; return;
}
if (user.TwitchUserId == default)
{
_logger.Warning("Unable to fetch 7tv data. If this is wrong, ensure your Tom to Speech token is valid.");
return;
}
var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId); var emoteSet = await _sevenApiClient.FetchChannelEmoteSet(_user.TwitchUserId);
if (emoteSet != null) if (emoteSet != null)
{
_user.SevenEmoteSetId = emoteSet.Id; _user.SevenEmoteSetId = emoteSet.Id;
_logger.Debug($"Fetched the 7tv emote set id [emote set id: {emoteSet.Id}]");
}
await InitializeEmotes(_sevenApiClient, emoteSet); await InitializeEmotes(_sevenApiClient, emoteSet);
await InitializeSevenTv(); await InitializeSevenTv();
}); }));
await InitializeHermesWebsocket(); disposables.Add(connected.FirstAsync().Subscribe(async (data) =>
_playback.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{ {
if (_player.Playing?.Audio == e.SampleProvider) if (data.Value is not User user)
{ {
_player.Playing = null; _logger.Warning("Something went wrong. Not connecting to Twitch.");
return;
} }
}); if (user.TwitchUserId == default)
try
{ {
_veado.Initialize(); _logger.Warning("Not connecting to Twitch. If this is wrong, ensure your Tom to Speech token is valid.");
await _veado.Connect(); return;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to connect to Veado websocket server.");
} }
try try
@@ -156,13 +158,27 @@ namespace TwitchChatTTS
catch (Exception e) catch (Exception e)
{ {
_logger.Error(e, "Failed to connect to Twitch websocket server."); _logger.Error(e, "Failed to connect to Twitch websocket server.");
await Task.Delay(TimeSpan.FromSeconds(30));
return;
} }
}));
_commandManager.Update(_commandFactory); _commandManager.Update(_commandFactory);
await InitializeVeadotube();
await InitializeHermesWebsocket();
await InitializeObs(); await InitializeObs();
// Check if user has successfully connected.
await Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(5));
if (_user.TwitchUserId == default)
{
_logger.Warning("Ensure your Tom to Speech token in the tts.config.yml file is valid.");
_logger.Warning("Re-open the application once you have made sure the token is valid.");
}
disposables.ForEach(d => d.Dispose());
});
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
@@ -181,6 +197,10 @@ namespace TwitchChatTTS
_hermes.Initialize(); _hermes.Initialize();
await _hermes.Connect(); await _hermes.Connect();
} }
catch (WebSocketException e) when (e.Message.Contains("The server returned status code '502'"))
{
_logger.Error("Could not connect to Tom to Speech server.");
}
catch (Exception e) catch (Exception e)
{ {
_logger.Error(e, "Connecting to hermes failed. Skipping hermes websockets."); _logger.Error(e, "Connecting to hermes failed. Skipping hermes websockets.");
@@ -213,6 +233,19 @@ namespace TwitchChatTTS
} }
} }
private async Task InitializeVeadotube()
{
try
{
_veado.Initialize();
await _veado.Connect();
}
catch (Exception e)
{
_logger.Warning(e, "Failed to connect to Veado websocket server.");
}
}
private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes) private async Task InitializeEmotes(SevenApiClient sevenapi, EmoteSet? channelEmotes)
{ {
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes(); var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();

View File

@@ -11,25 +11,37 @@ namespace TwitchChatTTS
{ {
public class TTSListening : IHostedService public class TTSListening : IHostedService
{ {
private const string TTS_API_URL = "https://api.streamelements.com/kappa/v2/speech";
private readonly AudioPlaybackEngine _playback; private readonly AudioPlaybackEngine _playback;
private readonly TTSPlayer _player; private readonly TTSPlayer _player;
private readonly TTSConsumer _consumer; private readonly TTSConsumer _consumer;
private readonly IDisposable _subscription; private readonly IDisposable _subscription;
private readonly User _user;
private readonly ILogger _logger; private readonly ILogger _logger;
public TTSListening(AudioPlaybackEngine playback, TTSPlayer player, TTSPublisher publisher, TTSConsumer consumer, ILogger logger) public TTSListening(AudioPlaybackEngine playback, TTSPlayer player, TTSPublisher publisher, TTSConsumer consumer, User user, ILogger logger)
{ {
_playback = playback; _playback = playback;
_player = player; _player = player;
_consumer = consumer; _consumer = consumer;
_subscription = publisher.Subscribe(consumer); _subscription = publisher.Subscribe(consumer);
_user = user;
_logger = logger; _logger = logger;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_playback.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{
if (_player.Playing?.Audio == e.SampleProvider)
{
_player.Playing = null;
}
});
Task.Run(async () => Task.Run(async () =>
{ {
while (true) while (true)
@@ -107,7 +119,7 @@ namespace TwitchChatTTS
try try
{ {
string url = $"https://api.streamelements.com/kappa/v2/speech?voice={message.Voice}&text={HttpUtility.UrlEncode(message.Message.Trim())}"; string url = $"{TTS_API_URL}?key={_user.StreamElementsOverlayKey}&voice={message.Voice}&text={HttpUtility.UrlEncode(message.Message.Trim())}";
var nws = new NetworkWavSound(url); var nws = new NetworkWavSound(url);
var provider = new CachedWavProvider(nws); var provider = new CachedWavProvider(nws);
var data = _playback.ConvertSound(provider); var data = _playback.ConvertSound(provider);

View File

@@ -27,7 +27,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
private readonly AudioPlaybackEngine _playback; private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Random _random; private readonly Random _random;
private readonly object _lock; private readonly ReaderWriterLockSlim _rwls;
public RedemptionManager( public RedemptionManager(
@@ -50,7 +50,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
_playback = playback; _playback = playback;
_logger = logger; _logger = logger;
_random = new Random(); _random = new Random();
_lock = new object(); _rwls = new ReaderWriterLockSlim();
var topic = _bus.GetTopic("redemptions_initiation"); var topic = _bus.GetTopic("redemptions_initiation");
topic.Subscribe(data => topic.Subscribe(data =>
@@ -99,7 +99,9 @@ namespace TwitchChatTTS.Twitch.Redemptions
{ {
_redemptions.Add(redemption.Id, redemption); _redemptions.Add(redemption.Id, redemption);
_logger.Debug($"Added redemption to redemption manager [redemption id: {redemption.Id}]"); _logger.Debug($"Added redemption to redemption manager [redemption id: {redemption.Id}]");
} else { }
else
{
_redemptions[redemption.Id] = redemption; _redemptions[redemption.Id] = redemption;
_logger.Debug($"Updated redemption to redemption manager [redemption id: {redemption.Id}]"); _logger.Debug($"Updated redemption to redemption manager [redemption id: {redemption.Id}]");
} }
@@ -107,17 +109,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
} }
private void Add(string twitchRedemptionId, string redemptionId) private void Add(string twitchRedemptionId, string redemptionId)
{
lock (_lock)
{ {
if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems)) if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems))
_redeems.Add(twitchRedemptionId, redeems = new List<string>()); _redeems.Add(twitchRedemptionId, redeems = new List<string>());
var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null; var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null;
if (item == null) if (item == null)
{
return; return;
}
var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null); var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null);
bool added = false; bool added = false;
@@ -135,13 +133,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
} }
if (!added) if (!added)
redeems.Add(redemptionId); redeems.Add(redemptionId);
}
_logger.Debug($"Added redemption action [redemption id: {redemptionId}][twitch redemption id: {twitchRedemptionId}]"); _logger.Debug($"Added redemption action [redemption id: {redemptionId}][twitch redemption id: {twitchRedemptionId}]");
} }
private void Add(string twitchRedemptionId, Redemption item) private void Add(string twitchRedemptionId, Redemption item)
{ {
lock (_lock) _rwls.EnterWriteLock();
try
{ {
if (!_redeems.TryGetValue(twitchRedemptionId, out var redemptionNames)) if (!_redeems.TryGetValue(twitchRedemptionId, out var redemptionNames))
_redeems.Add(twitchRedemptionId, redemptionNames = new List<string>()); _redeems.Add(twitchRedemptionId, redemptionNames = new List<string>());
@@ -163,6 +161,10 @@ namespace TwitchChatTTS.Twitch.Redemptions
if (!added) if (!added)
redemptionNames.Add(item.Id); redemptionNames.Add(item.Id);
} }
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug($"Added redemption action [redemption id: {item.Id}][twitch redemption id: {twitchRedemptionId}]"); _logger.Debug($"Added redemption action [redemption id: {item.Id}][twitch redemption id: {twitchRedemptionId}]");
} }
@@ -263,6 +265,11 @@ namespace TwitchChatTTS.Twitch.Redemptions
break; break;
case "SPECIFIC_TTS_VOICE": case "SPECIFIC_TTS_VOICE":
case "RANDOM_TTS_VOICE": case "RANDOM_TTS_VOICE":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
string voiceId = string.Empty; string voiceId = string.Empty;
bool specific = action.Type == "SPECIFIC_TTS_VOICE"; bool specific = action.Type == "SPECIFIC_TTS_VOICE";
@@ -327,18 +334,43 @@ namespace TwitchChatTTS.Twitch.Redemptions
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
case "NIGHTBOT_PLAY": case "NIGHTBOT_PLAY":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.Play(); await _nightbot.Play();
break; break;
case "NIGHTBOT_PAUSE": case "NIGHTBOT_PAUSE":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.Pause(); await _nightbot.Pause();
break; break;
case "NIGHTBOT_SKIP": case "NIGHTBOT_SKIP":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.Skip(); await _nightbot.Skip();
break; break;
case "NIGHTBOT_CLEAR_PLAYLIST": case "NIGHTBOT_CLEAR_PLAYLIST":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.ClearPlaylist(); await _nightbot.ClearPlaylist();
break; break;
case "NIGHTBOT_CLEAR_QUEUE": case "NIGHTBOT_CLEAR_QUEUE":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.ClearQueue(); await _nightbot.ClearQueue();
break; break;
case "VEADOTUBE_SET_STATE": case "VEADOTUBE_SET_STATE":
@@ -387,7 +419,8 @@ namespace TwitchChatTTS.Twitch.Redemptions
public IEnumerable<RedeemableAction> Get(string twitchRedemptionId) public IEnumerable<RedeemableAction> Get(string twitchRedemptionId)
{ {
lock (_lock) _rwls.EnterReadLock();
try
{ {
if (_redeems.TryGetValue(twitchRedemptionId, out var redemptionIds)) if (_redeems.TryGetValue(twitchRedemptionId, out var redemptionIds))
return redemptionIds.Select(r => _redemptions.TryGetValue(r, out var redemption) ? redemption : null) return redemptionIds.Select(r => _redemptions.TryGetValue(r, out var redemption) ? redemption : null)
@@ -395,15 +428,19 @@ namespace TwitchChatTTS.Twitch.Redemptions
.Select(r => _actions.TryGetValue(r!.ActionName, out var action) ? action : null) .Select(r => _actions.TryGetValue(r!.ActionName, out var action) ? action : null)
.Where(a => a != null)!; .Where(a => a != null)!;
} }
finally
{
_rwls.ExitReadLock();
}
return []; return [];
} }
public void Initialize() public void Initialize()
{ {
_logger.Debug($"Redemption manager is about to initialize [redemption count: {_redemptions.Count()}][action count: {_actions.Count}]"); _rwls.EnterWriteLock();
try
lock (_lock)
{ {
_logger.Debug($"Redemption manager is about to initialize [redemption count: {_redemptions.Count()}][action count: {_actions.Count}]");
_redeems.Clear(); _redeems.Clear();
var ordered = _redemptions.Select(r => r.Value).Where(r => r != null).OrderBy(r => r.Order); var ordered = _redemptions.Select(r => r.Value).Where(r => r != null).OrderBy(r => r.Order);
@@ -431,18 +468,31 @@ namespace TwitchChatTTS.Twitch.Redemptions
} }
} }
} }
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug("All redemptions added. Redemption Manager is ready."); _logger.Debug("All redemptions added. Redemption Manager is ready.");
} }
public bool RemoveAction(string actionName) public bool RemoveAction(string actionName)
{
_rwls.EnterWriteLock();
try
{ {
return _actions.Remove(actionName); return _actions.Remove(actionName);
} }
finally
{
_rwls.ExitWriteLock();
}
}
public bool RemoveRedemption(string redemptionId) public bool RemoveRedemption(string redemptionId)
{ {
lock (_lock) _rwls.EnterWriteLock();
try
{ {
if (!_redemptions.TryGetValue(redemptionId, out var redemption)) if (!_redemptions.TryGetValue(redemptionId, out var redemption))
{ {
@@ -458,6 +508,10 @@ namespace TwitchChatTTS.Twitch.Redemptions
return true; return true;
} }
} }
finally
{
_rwls.ExitWriteLock();
}
return false; return false;
} }
@@ -475,7 +529,8 @@ namespace TwitchChatTTS.Twitch.Redemptions
public bool Update(Redemption redemption) public bool Update(Redemption redemption)
{ {
lock (_lock) _rwls.EnterWriteLock();
try
{ {
if (_redemptions.TryGetValue(redemption.Id, out var r)) if (_redemptions.TryGetValue(redemption.Id, out var r))
{ {
@@ -516,12 +571,19 @@ namespace TwitchChatTTS.Twitch.Redemptions
return true; return true;
} }
} }
finally
{
_rwls.ExitWriteLock();
}
_logger.Warning($"Cannot find redemption by name [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]"); _logger.Warning($"Cannot find redemption by name [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]");
return false; return false;
} }
public bool Update(RedeemableAction action) public bool Update(RedeemableAction action)
{
_rwls.EnterWriteLock();
try
{ {
if (_actions.TryGetValue(action.Name, out var a)) if (_actions.TryGetValue(action.Name, out var a))
{ {
@@ -530,6 +592,11 @@ namespace TwitchChatTTS.Twitch.Redemptions
_logger.Debug($"Updated redeemable action in redemption manager [action name: {action.Name}]"); _logger.Debug($"Updated redeemable action in redemption manager [action name: {action.Name}]");
return true; return true;
} }
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Warning($"Cannot find redeemable action by name [action name: {action.Name}]"); _logger.Warning($"Cannot find redeemable action by name [action name: {action.Name}]");
return false; return false;

View File

@@ -14,12 +14,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public string Name => "channel.chat.message"; public string Name => "channel.chat.message";
private readonly IChatMessageReader _reader; private readonly IChatMessageReader _reader;
private readonly User _user;
private readonly ICommandManager _commands; private readonly ICommandManager _commands;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
private readonly IUsagePolicy<long> _permissionPolicy; private readonly IUsagePolicy<long> _permissionPolicy;
private readonly IChatterGroupManager _chatterGroupManager; private readonly IChatterGroupManager _chatterGroupManager;
private readonly ServiceBusCentral _bus; private readonly ServiceBusCentral _bus;
private readonly User _user;
private readonly ILogger _logger; private readonly ILogger _logger;
@@ -35,12 +35,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
) )
{ {
_reader = reader; _reader = reader;
_user = user;
_commands = commands; _commands = commands;
_permissionManager = permissionManager; _permissionManager = permissionManager;
_permissionPolicy = permissionPolicy; _permissionPolicy = permissionPolicy;
_chatterGroupManager = chatterGroupManager; _chatterGroupManager = chatterGroupManager;
_bus = bus; _bus = bus;
_user = user;
_logger = logger; _logger = logger;
} }
@@ -58,13 +58,13 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
var groups = GetGroups(message.Badges, chatterId); var groups = GetGroups(message.Badges, chatterId);
var bits = GetTotalBits(fragments); var bits = GetTotalBits(fragments);
if (message.ChannelPointsCustomRewardId == null) if (message.ChannelPointsCustomRewardId == null && !_user.Slave)
{ {
var commandResult = await CheckForChatCommand(message.Message.Text, message, groups); var commandResult = await CheckForChatCommand(message.Message.Text, message, groups);
if (commandResult != ChatCommandResult.Unknown) if (commandResult != ChatCommandResult.Unknown)
return; return;
} }
else else if (message.ChannelPointsCustomRewardId != null)
{ {
_bus.Send(this, "chat_message_redemption", message); _bus.Send(this, "chat_message_redemption", message);
} }
@@ -120,8 +120,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
private int GetTotalBits(TwitchChatFragment[] fragments) private int GetTotalBits(TwitchChatFragment[] fragments)
{ {
return fragments.Where(f => f.Type == "cheermote" && f.Cheermote != null) return fragments.Where(f => f.Type == "cheermote" && f.Cheermote != null)
.Select(f => f.Cheermote!.Bits) .Sum(f => f.Cheermote!.Bits);
.Sum();
} }
private string GetPermissionPath(string? customRewardId, int bits) private string GetPermissionPath(string? customRewardId, int bits)

View File

@@ -29,18 +29,9 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
return; return;
} }
int waited = 0; var twitchConnection = _user.TwitchConnection!;
while ((_user.TwitchUserId <= 0 || _user.TwitchConnection == null) && ++waited < 5) _api.Initialize(twitchConnection.ClientId, twitchConnection.AccessToken);
await Task.Delay(TimeSpan.FromSeconds(1)); var span = twitchConnection.ExpiresAt - DateTime.Now;
if (_user.TwitchConnection == null)
{
_logger.Error("Ensure you have linked either your Twitch account or TTS' bot to your TTS account. Twitch client will not be connecting.");
return;
}
_api.Initialize(_user.TwitchConnection.ClientId, _user.TwitchConnection.AccessToken);
var span = _user.TwitchConnection.ExpiresAt - DateTime.Now;
var timeLeft = span.Days >= 2 ? span.Days + " days" : (span.Hours >= 2 ? span.Hours + " hours" : span.Minutes + " minutes"); var timeLeft = span.Days >= 2 ? span.Days + " days" : (span.Hours >= 2 ? span.Hours + " hours" : span.Minutes + " minutes");
if (span.Days >= 3) if (span.Days >= 3)
_logger.Information($"Twitch connection has {timeLeft} before it is revoked."); _logger.Information($"Twitch connection has {timeLeft} before it is revoked.");
@@ -48,7 +39,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_logger.Warning($"Twitch connection has {timeLeft} before it is revoked. Refreshing the token is soon required."); _logger.Warning($"Twitch connection has {timeLeft} before it is revoked. Refreshing the token is soon required.");
else else
{ {
_logger.Error("Twitch connection has its permissions revoked. Refresh the token. Twith client will not be connecting."); _logger.Error($"Twitch connection has its permissions revoked. Refresh the token. Twitch client will not be connecting. [expired at: {twitchConnection.ExpiresAt}]");
return; return;
} }

View File

@@ -18,21 +18,22 @@ namespace TwitchChatTTS.Twitch.Socket
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly object _lock; private readonly Mutex _mutex;
public TwitchConnectionManager(IServiceProvider serviceProvider, ILogger logger) public TwitchConnectionManager(IServiceProvider serviceProvider, ILogger logger)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
_lock = new object(); _mutex = new Mutex();
} }
public TwitchWebsocketClient GetBackupClient() public TwitchWebsocketClient GetBackupClient()
{ {
lock (_lock) try
{ {
_mutex.WaitOne();
if (_identified == null) if (_identified == null)
throw new InvalidOperationException("Cannot get backup Twitch client yet. Waiting for identification."); throw new InvalidOperationException("Cannot get backup Twitch client yet. Waiting for identification.");
if (_backup != null) if (_backup != null)
@@ -40,12 +41,17 @@ namespace TwitchChatTTS.Twitch.Socket
return CreateNewClient(); return CreateNewClient();
} }
finally
{
_mutex.ReleaseMutex();
}
} }
public TwitchWebsocketClient GetWorkingClient() public TwitchWebsocketClient GetWorkingClient()
{ {
lock (_lock) try
{ {
_mutex.WaitOne();
if (_identified == null) if (_identified == null)
{ {
return CreateNewClient(); return CreateNewClient();
@@ -53,6 +59,10 @@ namespace TwitchChatTTS.Twitch.Socket
return _identified; return _identified;
} }
finally
{
_mutex.ReleaseMutex();
}
} }
private TwitchWebsocketClient CreateNewClient() private TwitchWebsocketClient CreateNewClient()
@@ -74,8 +84,9 @@ namespace TwitchChatTTS.Twitch.Socket
private async Task OnDisconnection(TwitchWebsocketClient client) private async Task OnDisconnection(TwitchWebsocketClient client)
{ {
bool reconnecting = false; bool reconnecting = false;
lock (_lock) try
{ {
_mutex.WaitOne();
if (_identified?.UID == client.UID) if (_identified?.UID == client.UID)
{ {
_logger.Debug($"Identified Twitch client has disconnected [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]"); _logger.Debug($"Identified Twitch client has disconnected [client: {client.UID}][main: {_identified.UID}][backup: {_backup?.UID}]");
@@ -92,19 +103,25 @@ namespace TwitchChatTTS.Twitch.Socket
else else
_logger.Warning($"Twitch client disconnected from unknown source [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]"); _logger.Warning($"Twitch client disconnected from unknown source [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
} }
finally
{
_mutex.ReleaseMutex();
}
if (reconnecting) if (reconnecting)
{ {
var newClient = GetWorkingClient(); var newClient = GetWorkingClient();
await newClient.Reconnect(); await newClient.Reconnect();
} }
} }
private async Task OnIdentified(TwitchWebsocketClient client) private async Task OnIdentified(TwitchWebsocketClient client)
{ {
bool clientDisconnect = false; bool clientDisconnect = false;
lock (_lock) try
{ {
_mutex.WaitOne();
if (_identified == null || _identified.ReceivedReconnecting) if (_identified == null || _identified.ReceivedReconnecting)
{ {
if (_backup != null && _backup.UID == client.UID) if (_backup != null && _backup.UID == client.UID)
@@ -126,6 +143,10 @@ namespace TwitchChatTTS.Twitch.Socket
clientDisconnect = true; clientDisconnect = true;
} }
} }
finally
{
_mutex.ReleaseMutex();
}
if (clientDisconnect) if (clientDisconnect)
await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client.")); await client.DisconnectAsync(new SocketDisconnectionEventArgs("Closed", "No need for a tertiary client."));

View File

@@ -17,15 +17,11 @@ namespace TwitchChatTTS.Twitch.Socket
private readonly IDictionary<string, string> _subscriptions; private readonly IDictionary<string, string> _subscriptions;
private readonly IBackoff _backoff; private readonly IBackoff _backoff;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private bool _disconnected;
private readonly object _lock;
public event EventHandler<EventArgs>? OnIdentified; public event EventHandler<EventArgs>? OnIdentified;
public string UID { get; } public string UID { get; }
public string URL; public string URL;
public bool Connected { get; private set; }
public bool Identified { get; private set; }
public string? SessionId { get; private set; } public string? SessionId { get; private set; }
public bool ReceivedReconnecting { get; set; } public bool ReceivedReconnecting { get; set; }
public bool TwitchReconnected { get; set; } public bool TwitchReconnected { get; set; }
@@ -46,13 +42,14 @@ namespace TwitchChatTTS.Twitch.Socket
_backoff = backoff; _backoff = backoff;
_configuration = configuration; _configuration = configuration;
_subscriptions = new Dictionary<string, string>(); _subscriptions = new Dictionary<string, string>();
_lock = new object();
_messageTypes = new Dictionary<string, Type>(); _messageTypes = new Dictionary<string, Type>
_messageTypes.Add("session_keepalive", typeof(object)); {
_messageTypes.Add("session_welcome", typeof(SessionWelcomeMessage)); { "session_keepalive", typeof(object) },
_messageTypes.Add("session_reconnect", typeof(SessionWelcomeMessage)); { "session_welcome", typeof(SessionWelcomeMessage) },
_messageTypes.Add("notification", typeof(NotificationMessage)); { "session_reconnect", typeof(SessionWelcomeMessage) },
{ "notification", typeof(NotificationMessage) }
};
UID = Guid.NewGuid().ToString("D"); UID = Guid.NewGuid().ToString("D");
@@ -88,25 +85,12 @@ namespace TwitchChatTTS.Twitch.Socket
_logger.Information($"Initializing Twitch websocket client."); _logger.Information($"Initializing Twitch websocket client.");
OnConnected += (sender, e) => OnConnected += (sender, e) =>
{ {
Connected = true;
_logger.Information("Twitch websocket client connected."); _logger.Information("Twitch websocket client connected.");
_disconnected = false;
}; };
OnDisconnected += (sender, e) => OnDisconnected += (sender, e) =>
{ {
lock (_lock)
{
if (_disconnected)
return;
_disconnected = true;
}
_logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}][client: {UID}]"); _logger.Information($"Twitch websocket client disconnected [status: {e.Status}][reason: {e.Reason}][client: {UID}]");
Connected = false;
Identified = false;
}; };
} }
@@ -126,7 +110,6 @@ namespace TwitchChatTTS.Twitch.Socket
public void Identify(string sessionId) public void Identify(string sessionId)
{ {
Identified = true;
SessionId = sessionId; SessionId = sessionId;
OnIdentified?.Invoke(this, EventArgs.Empty); OnIdentified?.Invoke(this, EventArgs.Empty);
} }
@@ -200,7 +183,7 @@ namespace TwitchChatTTS.Twitch.Socket
while (current < total) while (current < total)
{ {
var size = Encoding.UTF8.GetBytes(content.Substring(current), array); var size = Encoding.UTF8.GetBytes(content.Substring(current), array);
await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token); await _socket!.SendAsync(array, WebSocketMessageType.Text, current + size >= total, CancellationToken.None);
current += size; current += size;
} }
_logger.Debug("Twitch TX #" + type + ": " + content); _logger.Debug("Twitch TX #" + type + ": " + content);

View File

@@ -15,6 +15,7 @@ namespace TwitchChatTTS
public required string TwitchUsername { get; set; } public required string TwitchUsername { get; set; }
public required string SevenEmoteSetId { get; set; } public required string SevenEmoteSetId { get; set; }
public long? OwnerId { get; set; } public long? OwnerId { get; set; }
public required string StreamElementsOverlayKey { get; set; }
public Connection? TwitchConnection { get; set; } public Connection? TwitchConnection { get; set; }
public Connection? NightbotConnection { get; set; } public Connection? NightbotConnection { get; set; }
@@ -33,6 +34,8 @@ namespace TwitchChatTTS
[JsonIgnore] [JsonIgnore]
public Regex? VoiceNameRegex { get; set; } public Regex? VoiceNameRegex { get; set; }
public required bool Slave { get; set; }
private IDictionary<string, string> _voicesAvailable; private IDictionary<string, string> _voicesAvailable;
private HashSet<string> _voicesEnabled; private HashSet<string> _voicesEnabled;

View File

@@ -178,7 +178,7 @@ namespace TwitchChatTTS.Veadotube
while (current < total) while (current < total)
{ {
var size = Encoding.UTF8.GetBytes(content.Substring(current), array); var size = Encoding.UTF8.GetBytes(content.Substring(current), array);
await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts!.Token); await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, CancellationToken.None);
current += size; current += size;
} }
_logger.Debug($"Veado TX [message type: {typeof(T).Name}]: " + content); _logger.Debug($"Veado TX [message type: {typeof(T).Name}]: " + content);