Compare commits

..

19 Commits

Author SHA1 Message Date
Tom
aa89578297 Fixed TTS using StreamElements. Fixed several issues. 2026-01-03 05:19:33 +00:00
Tom
fb04f4003f Changed various locking mechanisms. 2025-03-29 20:28:36 +00:00
Tom
eddd9e6403 Added support for group permission messages. 2025-03-29 20:27:55 +00:00
Tom
622b359b12 Added proper slave mode - additional clients after the first connection. Fixed a few issues. Updated to version 4.8.2. 2025-03-06 16:05:15 +00:00
Tom
cbdca1c008 Twitch connection now relies on events to connect. Added logging for when TTS filter is not a regex. Minor code clean up. 2025-01-19 01:23:29 +00:00
Tom
86fc6bc24d Version update to v4.7 2025-01-18 22:59:12 +00:00
Tom
c4e651ff7f Removed useless logging line. 2025-01-18 22:51:26 +00:00
Tom
6e6f20b097 Fixed explicit TTS voices in messages when there is text before the first voice name. 2025-01-18 22:47:15 +00:00
Tom
03d24b0905 Fixed more group stuffs. 2025-01-18 22:45:36 +00:00
Tom
48ac5c4fa0 Updated NuGet packages. 2025-01-18 21:52:33 +00:00
Tom
d13cd71ac0 Minor logging changes. 2025-01-18 21:52:15 +00:00
Tom
5067ffe119 Added chat message to redemptions. Added Subscription End to Twitch. Added more variables to certain redemptions. 2025-01-18 21:51:50 +00:00
Tom
9a17ad16b3 Fixed groups & their websocket support. 2025-01-18 21:41:00 +00:00
Tom
c21890b55d Removing groups from chatters when deleting groups. 2025-01-18 17:56:15 +00:00
Tom
3b24208acc Added connection backoff for OBS. 2025-01-18 17:43:29 +00:00
Tom
c373af5281 Fixed some minor things. 2025-01-18 17:34:02 +00:00
Tom
9f884f71ae Added group chatters support to websocket. 2025-01-18 17:33:15 +00:00
Tom
a49e52a6bb Added group support for websockets. 2025-01-18 17:31:51 +00:00
Tom
aed0421843 Fixed getting scene item id from OBS. 2025-01-18 16:37:04 +00:00
63 changed files with 1446 additions and 419 deletions

View File

@@ -12,7 +12,7 @@ namespace TwitchChatTTS.Chat.Commands.Limits
public UsagePolicy(ILogger logger)
{
_logger = logger;
_root = new UsagePolicyNode<K>(string.Empty, null, null, logger);
_root = new UsagePolicyNode<K>(string.Empty, null, null, logger, root: true);
}
@@ -102,111 +102,162 @@ namespace TwitchChatTTS.Chat.Commands.Limits
private IDictionary<T, UserUsageData> _usages { get; }
private IList<UsagePolicyNode<T>> _children { get; }
private ILogger _logger;
private object _lock { get; }
private ReaderWriterLockSlim _rwls { get; }
public UsagePolicyNode(string name, UsagePolicyLimit? data, UsagePolicyNode<T>? parent, ILogger logger)
public UsagePolicyNode(string name, UsagePolicyLimit? data, UsagePolicyNode<T>? parent, ILogger logger, bool root = false)
{
//ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
if (!root)
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
Name = name;
Limit = data;
_parent = parent;
_usages = new Dictionary<T, UserUsageData>();
_children = new List<UsagePolicyNode<T>>();
_logger = logger;
_lock = new object();
_rwls = new ReaderWriterLockSlim();
}
public UsagePolicyNode<T>? Get(IEnumerable<string> path)
{
if (!path.Any())
return this;
_rwls.EnterReadLock();
try
{
if (!path.Any())
return this;
var nextName = path.First();
var next = _children.FirstOrDefault(c => c.Name == nextName);
if (next == null)
return this;
return next.Get(path.Skip(1));
var nextName = path.First();
var next = _children.FirstOrDefault(c => c.Name == nextName);
if (next == null)
return this;
return next.Get(path.Skip(1));
}
finally
{
_rwls.ExitReadLock();
}
}
public UsagePolicyNode<T>? Remove(IEnumerable<string> path)
{
if (!path.Any())
_rwls.EnterWriteLock();
try
{
if (_parent == null)
throw new InvalidOperationException("Cannot remove root node");
if (!path.Any())
{
if (_parent == null)
throw new InvalidOperationException("Cannot remove root node");
_parent._children.Remove(this);
return this;
_parent._children.Remove(this);
return this;
}
var nextName = path.First();
var next = _children.FirstOrDefault(c => c.Name == nextName);
_logger.Debug($"internal remove node [is null: {next == null}][path: {string.Join('.', path)}]");
if (next == null)
return null;
return next.Remove(path.Skip(1));
}
finally
{
_rwls.ExitWriteLock();
}
var nextName = path.First();
var next = _children.FirstOrDefault(c => c.Name == nextName);
_logger.Debug($"internal remove node [is null: {next == null}][path: {string.Join('.', path)}]");
if (next == null)
return null;
return next.Remove(path.Skip(1));
}
public void Set(IEnumerable<string> path, int count, TimeSpan span)
{
if (!path.Any())
_rwls.EnterWriteLock();
try
{
Limit = new UsagePolicyLimit(count, span);
return;
}
if (!path.Any())
{
Limit = new UsagePolicyLimit(count, span);
return;
}
var nextName = path.First();
var next = _children.FirstOrDefault(c => c.Name == nextName);
_logger.Debug($"internal set node [is null: {next == null}][path: {string.Join('.', path)}]");
if (next == null)
{
next = new UsagePolicyNode<T>(nextName, null, this, _logger);
_children.Add(next);
var nextName = path.First();
var next = _children.FirstOrDefault(c => c.Name == nextName);
_logger.Debug($"internal set node [is null: {next == null}][path: {string.Join('.', path)}]");
if (next == null)
{
next = new UsagePolicyNode<T>(nextName, null, this, _logger);
_children.Add(next);
}
next.Set(path.Skip(1), count, span);
}
finally
{
_rwls.ExitWriteLock();
}
next.Set(path.Skip(1), count, span);
}
public bool TryUse(T key, DateTime timestamp)
{
if (_parent == null)
return false;
if (Limit == null || Limit.Count <= 0)
return _parent.TryUse(key, timestamp);
UserUsageData? usage;
lock (_lock)
_rwls.EnterUpgradeableReadLock();
try
{
if (_parent == null)
return false;
if (Limit == null || Limit.Count <= 0)
return _parent.TryUse(key, timestamp);
UserUsageData? usage;
if (!_usages.TryGetValue(key, out usage))
{
usage = new UserUsageData(Limit.Count, 1 % Limit.Count);
usage.Uses[0] = timestamp;
_usages.Add(key, usage);
_rwls.EnterWriteLock();
try
{
usage = new UserUsageData(Limit.Count, 1 % Limit.Count);
usage.Uses[0] = timestamp;
_usages.Add(key, usage);
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug($"internal use node create");
return true;
}
if (usage.Uses.Length != Limit.Count)
{
var sizeDiff = Math.Max(0, usage.Uses.Length - Limit.Count);
var temp = usage.Uses.Skip(sizeDiff);
var tempSize = usage.Uses.Length - sizeDiff;
usage.Uses = temp.Union(new DateTime[Math.Max(0, Limit.Count - tempSize)]).ToArray();
_rwls.EnterWriteLock();
try
{
var sizeDiff = Math.Max(0, usage.Uses.Length - Limit.Count);
var temp = usage.Uses.Skip(sizeDiff);
var tempSize = usage.Uses.Length - sizeDiff;
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.
if (timestamp - usage.Uses[usage.Index] < Limit.Span)
{
_logger.Debug($"internal use node spam [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]");
return _parent.TryUse(key, timestamp);
}
_logger.Debug($"internal use node normal [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]");
_rwls.EnterWriteLock();
try
{
usage.Uses[usage.Index] = timestamp;
usage.Index = (usage.Index + 1) % Limit.Count;
}
finally
{
_rwls.ExitWriteLock();
}
}
// Attempt on parent node if policy has been abused.
if (timestamp - usage.Uses[usage.Index] < Limit.Span)
finally
{
_logger.Debug($"internal use node spam [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]");
return _parent.TryUse(key, timestamp);
}
_logger.Debug($"internal use node normal [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]");
lock (_lock)
{
usage.Uses[usage.Index] = timestamp;
usage.Index = (usage.Index + 1) % Limit.Count;
_rwls.ExitUpgradeableReadLock();
}
return true;

View File

@@ -1,7 +1,6 @@
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;
@@ -40,9 +39,9 @@ namespace TwitchChatTTS.Chat.Commands
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
{
private readonly IDictionary<string, string> _emotes;
public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); }
public EmoteDatabase()
{

View File

@@ -1,5 +1,3 @@
using System.Collections.Concurrent;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
@@ -9,84 +7,199 @@ namespace TwitchChatTTS.Chat.Groups
{
private readonly IDictionary<string, Group> _groups;
private readonly IDictionary<long, ICollection<string>> _chatters;
private readonly ReaderWriterLockSlim _rwls;
private readonly ILogger _logger;
public ChatterGroupManager(ILogger logger)
{
_logger = logger;
_groups = new ConcurrentDictionary<string, Group>();
_chatters = new ConcurrentDictionary<long, ICollection<string>>();
_groups = new Dictionary<string, Group>();
_chatters = new Dictionary<long, ICollection<string>>();
_rwls = new ReaderWriterLockSlim();
}
public void Add(Group group)
{
_groups.Add(group.Name, group);
}
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))
_rwls.EnterWriteLock();
try
{
foreach (var group in groupNames)
list.Add(group);
_groups.Add(group.Id, group);
}
finally
{
_rwls.ExitWriteLock();
}
}
public void Add(long chatterId, string groupId)
{
_rwls.EnterWriteLock();
try
{
if (_chatters.TryGetValue(chatterId, out var list))
{
if (!list.Contains(groupId))
list.Add(groupId);
}
else
_chatters.Add(chatterId, new List<string>() { groupId });
}
finally
{
_rwls.ExitWriteLock();
}
}
public void Add(long chatter, ICollection<string> groupIds)
{
_rwls.EnterWriteLock();
try
{
if (_chatters.TryGetValue(chatter, out var list))
{
foreach (var groupId in groupIds)
if (!list.Contains(groupId))
list.Add(groupId);
}
else
_chatters.Add(chatter, groupIds);
}
finally
{
_rwls.ExitWriteLock();
}
else
_chatters.Add(chatter, groupNames);
}
public void Clear()
{
_groups.Clear();
_chatters.Clear();
_rwls.EnterWriteLock();
try
{
_groups.Clear();
_chatters.Clear();
}
finally
{
_rwls.ExitWriteLock();
}
}
public Group? Get(string groupName)
public Group? Get(string groupId)
{
if (_groups.TryGetValue(groupName, out var group))
return group;
return null;
_rwls.EnterReadLock();
try
{
if (_groups.TryGetValue(groupId, out var group))
return group;
return null;
}
finally
{
_rwls.ExitReadLock();
}
}
public IEnumerable<string> GetGroupNamesFor(long chatter)
{
if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups[g].Name);
_rwls.EnterReadLock();
try
{
if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups.TryGetValue(g, out var group) ? group.Name : null)
.Where(g => g != null)
.Cast<string>();
return Array.Empty<string>();
return Array.Empty<string>();
}
finally
{
_rwls.ExitReadLock();
}
}
public int GetPriorityFor(long chatter)
{
if (!_chatters.TryGetValue(chatter, out var groups))
return 0;
_rwls.EnterReadLock();
try
{
if (!_chatters.TryGetValue(chatter, out var groups))
return 0;
return GetPriorityFor(groups);
return GetPriorityFor(groups);
}
finally
{
_rwls.ExitReadLock();
}
}
public int GetPriorityFor(IEnumerable<string> groupNames)
public int GetPriorityFor(IEnumerable<string> groupIds)
{
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;
_rwls.EnterReadLock();
try
{
var values = groupIds.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;
}
finally
{
_rwls.ExitReadLock();
}
}
public void Modify(Group group)
{
_rwls.EnterWriteLock();
try
{
_groups[group.Id] = group;
}
finally
{
_rwls.ExitWriteLock();
}
}
public bool Remove(string groupId)
{
_rwls.EnterWriteLock();
try
{
if (_groups.Remove(groupId))
{
foreach (var entry in _chatters)
entry.Value.Remove(groupId);
return true;
}
return false;
}
finally
{
_rwls.ExitReadLock();
}
}
public bool Remove(long chatterId, string groupId)
{
if (_chatters.TryGetValue(chatterId, out var groups))
_rwls.EnterWriteLock();
try
{
groups.Remove(groupId);
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId]}][group id: {groupId}]");
return true;
if (_chatters.TryGetValue(chatterId, out var groups))
{
groups.Remove(groupId);
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return true;
}
_logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return false;
}
finally
{
_rwls.ExitReadLock();
}
_logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId]}][group id: {groupId}]");
return false;
}
}
}

View File

@@ -12,6 +12,8 @@ namespace TwitchChatTTS.Chat.Groups
IEnumerable<string> GetGroupNamesFor(long chatter);
int GetPriorityFor(long chatter);
int GetPriorityFor(IEnumerable<string> groupIds);
void Modify(Group group);
bool Remove(string groupId);
bool Remove(long chatter, string groupId);
}
}

View File

@@ -5,33 +5,51 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
{
public class GroupPermissionManager : IGroupPermissionManager
{
private PermissionNode _root;
private ILogger _logger;
private readonly PermissionNode _root;
private readonly ILogger _logger;
private readonly ReaderWriterLockSlim _rwls;
public GroupPermissionManager(ILogger logger)
{
_logger = logger;
_root = new PermissionNode(string.Empty, null, null);
_rwls = new ReaderWriterLockSlim();
}
public bool? CheckIfAllowed(string path)
{
var res = Get(path)!.Allow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"}");
return res;
_rwls.EnterReadLock();
try
{
var res = Get(path)!.Allow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"}");
return res;
}
finally
{
_rwls.ExitReadLock();
}
}
public bool? CheckIfDirectAllowed(string path)
{
var node = Get(path, nullIfMissing: true);
if (node == null)
return null;
_rwls.EnterReadLock();
try
{
var node = Get(path, nullIfMissing: true);
if (node == null)
return null;
var res = node.DirectAllow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
return res;
var res = node.DirectAllow;
_logger.Debug($"Permission Node GET {path} = {res?.ToString() ?? "null"} [direct]");
return res;
}
finally
{
_rwls.ExitReadLock();
}
}
public bool? CheckIfAllowed(IEnumerable<string> groups, string path)
@@ -64,31 +82,63 @@ namespace TwitchChatTTS.Chat.Groups.Permissions
public void Clear()
{
_root.Clear();
_rwls.EnterWriteLock();
try
{
_root.Clear();
}
finally
{
_rwls.ExitWriteLock();
}
}
public bool Remove(string path)
{
var node = Get(path);
if (node == null || node.Parent == null)
return false;
var parts = path.Split('.');
var last = parts.Last();
if (parts.Length > 1 && parts[parts.Length - 1] == node.Parent.Name || parts.Length == 1 && node.Parent.Name == null)
_rwls.EnterUpgradeableReadLock();
try
{
node.Parent.Remove(last);
_logger.Debug($"Permission Node REMOVE priv {path}");
return true;
var node = Get(path);
if (node == null || node.Parent == null)
return false;
_rwls.EnterWriteLock();
try
{
var parts = path.Split('.');
var last = parts.Last();
if (parts.Length > 1 && parts[parts.Length - 1] == node.Parent.Name || parts.Length == 1 && node.Parent.Name == null)
{
node.Parent.Remove(last);
_logger.Debug($"Permission Node REMOVE priv {path}");
return true;
}
return false;
}
finally
{
_rwls.ExitWriteLock();
}
}
finally
{
_rwls.ExitUpgradeableReadLock();
}
return false;
}
public void Set(string path, bool? allow)
{
var node = Get(path, true);
node!.Allow = allow;
_logger.Debug($"Permission Node ADD {path} = {allow?.ToString() ?? "null"}");
_rwls.EnterWriteLock();
try
{
var node = Get(path, true);
node!.Allow = allow;
_logger.Debug($"Permission Node ADD {path} = {allow?.ToString() ?? "null"}");
}
finally
{
_rwls.ExitWriteLock();
}
}
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 tasks = new List<Task>();
if (_obs.Streaming && _configuration.Twitch?.Slave != true)
if ((!_obs.Connected || _obs.Streaming) && !_user.Slave)
{
if (emoteUsage.NewEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes));
if (emoteUsage.EmotesUsed.Any() && messageId != null && chatterId != null)
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);
}
}
@@ -84,6 +84,7 @@ namespace TwitchChatTTS.Chat.Messaging
var msg = FilterMessage(fragments, reply);
string voiceSelected = chatterId == null ? _user.DefaultTTSVoice : GetSelectedVoiceFor(chatterId.Value);
var messages = GetPartialTTSMessages(msg, voiceSelected).ToList();
_logger.Debug("TTS messages separated as: " + string.Join(" || ", messages.Select(m => m.Message ?? "<" + m.File + ">")));
var groupedMessage = new TTSGroupedMessage(broadcasterId, chatterId, messageId, messages, DateTime.UtcNow, priority);
_player.Add(groupedMessage, groupedMessage.Priority);
@@ -223,7 +224,7 @@ namespace TwitchChatTTS.Chat.Messaging
}];
}
return matches.Cast<Match>().SelectMany(match =>
var messages = matches.Cast<Match>().SelectMany(match =>
{
var m = match.Groups["message"].Value;
if (string.IsNullOrWhiteSpace(m))
@@ -233,6 +234,13 @@ namespace TwitchChatTTS.Chat.Messaging
voiceSelected = voiceSelected[0].ToString().ToUpper() + voiceSelected.Substring(1).ToLower();
return HandlePartialMessage(voiceSelected, m);
});
string beforeMatch = message.Substring(0, matches.First().Index);
if (!string.IsNullOrEmpty(beforeMatch))
messages = HandlePartialMessage(defaultVoice, beforeMatch).Union(messages);
_logger.Debug("TTS message matches: " + string.Join(" || ", matches.Select(m => "(" + m.Length + " / " + m.Index + "): " + m.Value + " ")));
return messages;
}
private string GetSelectedVoiceFor(long chatterId)

View File

@@ -15,7 +15,6 @@ namespace TwitchChatTTS
public class TwitchConfiguration {
public bool TtsWhenOffline;
public bool Slave;
public string? WebsocketUrl;
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,8 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
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;
}
if (client.LoggedIn)
@@ -43,29 +42,38 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
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.HermesUsername = message.UserName;
_user.TwitchUsername = message.UserName;
_user.TwitchUserId = long.Parse(message.ProviderAccountId);
_user.OwnerId = message.OwnerId;
_user.StreamElementsOverlayKey = message.StreamElementsOverlayKey;
_user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = new ConcurrentDictionary<string, string>(message.TTSVoicesAvailable);
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
_user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch");
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot");
_bus.Send(this, "twitch id", _user.TwitchUserId);
_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") ?? message.Connections.FirstOrDefault(c => c.Type == "nightbot");
if (_user.TwitchConnection != null)
{
_logger.Debug("Twitch connection: " + _user.TwitchConnection.Name + " / " + _user.TwitchConnection.AccessToken);
}
var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
foreach (var filter in filters)
{
try
{
var re = new Regex(filter.Search, ((RegexOptions) filter.Flag) | RegexOptions.Compiled);
var re = new Regex(filter.Search, ((RegexOptions)filter.Flag) | RegexOptions.Compiled);
re.Match(string.Empty);
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;
@@ -94,6 +102,8 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_logger.Information("TTS is now ready.");
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; }
private readonly System.Timers.Timer _heartbeatTimer;
private readonly IBackoff _backoff;
private readonly object _lock;
private readonly ReaderWriterLockSlim _rwls;
public bool Connected { get; set; }
public bool LoggedIn { get; set; }
@@ -62,43 +62,54 @@ namespace TwitchChatTTS.Hermes.Socket
LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow;
URL = $"wss://{BASE_URL}";
_lock = new object();
_rwls = new ReaderWriterLockSlim();
var ttsCreateUserVoice = _bus.GetTopic("tts.user.voice.create");
ttsCreateUserVoice.Subscribe(async data => await Send(3, new RequestMessage()
{
Type = "create_tts_user",
Data = (IDictionary<string, object>) data.Value!
Data = (IDictionary<string, object>)data.Value!
}));
var ttsUpdateUserVoice = _bus.GetTopic("tts.user.voice.update");
ttsUpdateUserVoice.Subscribe(async data => await Send(3, new RequestMessage()
{
Type = "update_tts_user",
Data = (IDictionary<string, object>) data.Value!
Data = (IDictionary<string, object>)data.Value!
}));
}
public override async Task Connect()
{
lock (_lock)
_rwls.EnterReadLock();
try
{
if (Connected)
return;
}
finally
{
_rwls.ExitReadLock();
}
_logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL);
}
private async Task Disconnect()
{
lock (_lock)
_rwls.EnterReadLock();
try
{
if (!Connected)
return;
}
finally
{
_rwls.ExitReadLock();
}
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
}
@@ -247,17 +258,15 @@ namespace TwitchChatTTS.Hermes.Socket
public void Initialize()
{
_logger.Information("Initializing Hermes websocket client.");
_logger.Information("Initializing Tom to Speech websocket client.");
OnConnected += async (sender, e) =>
{
lock (_lock)
{
if (Connected)
return;
Connected = true;
}
_logger.Information("Hermes websocket client connected.");
if (Connected)
return;
Connected = true;
_logger.Information("Tom to Speech websocket client connected.");
_heartbeatTimer.Enabled = true;
LastHeartbeatReceived = DateTime.UtcNow;
@@ -267,21 +276,21 @@ namespace TwitchChatTTS.Hermes.Socket
ApiKey = _configuration.Hermes!.Token!,
MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION,
PatchVersion = TTS.PATCH_VERSION,
});
};
OnDisconnected += async (sender, e) =>
{
lock (_lock)
{
if (!Connected)
return;
Connected = false;
}
if (!Connected)
return;
Connected = false;
LoggedIn = false;
Ready = false;
_logger.Warning("Hermes websocket client disconnected.");
_user.Slave = true;
_logger.Warning("Tom to Speech websocket client disconnected.");
_heartbeatTimer.Enabled = false;
await Reconnect(_backoff);
@@ -396,7 +405,7 @@ namespace TwitchChatTTS.Hermes.Socket
}
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))
@@ -407,7 +416,7 @@ namespace TwitchChatTTS.Hermes.Socket
}
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;
LoggedIn = false;
Connected = false;
@@ -421,10 +430,18 @@ namespace TwitchChatTTS.Hermes.Socket
public new async Task Send<T>(int opcode, T message)
{
if (!Connected)
_rwls.EnterReadLock();
try
{
_logger.Warning("Hermes websocket client is not connected. Not sending a message.");
return;
if (!Connected)
{
_logger.Warning("Tom to Speech websocket client is not connected. Not sending a message.");
return;
}
}
finally
{
_rwls.ExitReadLock();
}
await base.Send(opcode, message);

View File

@@ -0,0 +1,49 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateGroupAck : IRequestAck
{
public string Name => "create_group";
private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreateGroupAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_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 group = JsonSerializer.Deserialize<Group>(json, _options);
if (group == null)
{
_logger.Warning($"Group data is null.");
return;
}
var exists = _groups.Get(group.Id);
if (exists != null)
{
_logger.Warning($"Group id already exists [group id: {exists.Id}][group name: {exists.Name} / {group.Name}][group priority: {exists.Priority} / {group.Priority}]");
return;
}
_logger.Debug($"Adding group [group id: {group.Id}][group name: {group.Name}][group priority: {group.Priority}]");
_groups.Add(group);
_logger.Information($"Group has been created [group id: {group.Id}]");
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateGroupChatterAck : IRequestAck
{
public string Name => "create_group_chatter";
private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreateGroupChatterAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (string.IsNullOrWhiteSpace(json))
{
_logger.Warning($"Group Chatter JSON data is null.");
return;
}
var groupChatter = JsonSerializer.Deserialize<GroupChatter>(json, _options);
if (groupChatter == null)
{
_logger.Warning($"Group Chatter data is null.");
return;
}
var group = _groups.Get(groupChatter.GroupId);
if (group == null)
{
_logger.Warning($"Group id for chatter does not exists [group id: {groupChatter.GroupId}]");
return;
}
_logger.Debug($"Adding chatter to group [group id: {groupChatter.GroupId}][group name: {group.Name}][chatter id: {groupChatter.ChatterId}][chatter label: {groupChatter.ChatterLabel}]");
_groups.Add(groupChatter.ChatterId, groupChatter.GroupId);
_logger.Information($"Chatter has been added to group [group id: {groupChatter.GroupId}][group name: {group.Name}][chatter id: {groupChatter.ChatterId}][chatter label: {groupChatter.ChatterLabel}]");
}
}
}

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

@@ -24,17 +24,22 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (string.IsNullOrWhiteSpace(json)) {
_logger.Warning($"Policy JSON data is null.");
return;
}
var policy = JsonSerializer.Deserialize<Policy>(json, _options);
if (policy == null)
{
_logger.Warning($"Policy JSON data is null.");
_logger.Warning($"Policy data is null.");
return;
}
var group = _groups.Get(policy.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Policy data is failed: group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
_logger.Warning($"Policy's group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
return;
}

View File

@@ -48,7 +48,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
}
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}]");

View File

@@ -33,13 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
return;
}
var userId = requestData["user"].ToString();
var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(userId))
{
_logger.Warning("User Id is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
@@ -52,7 +46,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
}
_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

@@ -0,0 +1,45 @@
using Serilog;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteGroupAck : IRequestAck
{
public string Name => "delete_group";
private readonly IChatterGroupManager _groups;
private readonly ILogger _logger;
public DeleteGroupAck(IChatterGroupManager groups, ILogger logger)
{
_groups = groups;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (requestData == null)
{
_logger.Warning("Request data is null.");
return;
}
var groupId = requestData["group"].ToString();
if (string.IsNullOrEmpty(groupId))
{
_logger.Warning($"Action name is invalid [action name: {groupId}]");
return;
}
var group = _groups.Get(groupId);
if (group == null)
{
_logger.Warning($"Group id does not exist [group id: {group}]");
return;
}
_logger.Debug($"Removing group [group id: {group.Id}][group name: {group.Name}][group priority: {group.Priority}]");
_groups.Remove(group.Id);
_logger.Information($"Group has been updated [group id: {group.Id}]");
}
}
}

View File

@@ -0,0 +1,51 @@
using Serilog;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteGroupChatterAck : IRequestAck
{
public string Name => "delete_group_chatter";
private readonly IChatterGroupManager _groups;
private readonly ILogger _logger;
public DeleteGroupChatterAck(IChatterGroupManager groups, ILogger logger)
{
_groups = groups;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (requestData == null)
{
_logger.Warning("Request data is null.");
return;
}
if (!long.TryParse(requestData["chatter"].ToString(), out var chatterId))
{
_logger.Warning($"Chatter Id is invalid [chatter id: {chatterId}]");
return;
}
var groupId = requestData["group"].ToString();
if (string.IsNullOrWhiteSpace(groupId))
{
_logger.Warning($"Group Id is invalid [group id: {groupId}]");
return;
}
var group = _groups.Get(groupId);
if (group == null)
{
_logger.Warning($"Group id does not exist [group id: {groupId}]");
return;
}
_logger.Debug($"Deleting chatter from group [group id: {group.Id}][chatter id: {chatterId}][group name: {group.Name}][group priority: {group.Priority}]");
_groups.Remove(chatterId, groupId);
_logger.Information($"Chatter has been deleted from group [group id: {group.Id}][chatter id: {chatterId}]");
}
}
}

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

@@ -20,6 +20,12 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning("Policy JSON data is null.");
return;
}
var data = json.Split('/');
if (data.Length != 2)
{

View File

@@ -46,12 +46,15 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groupInfo.Groups)
{
_logger.Debug($"Adding group [group id: {group.Id}][name: {group.Name}][priority: {group.Priority}]");
_groups.Add(group);
}
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"}]");
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}]");
continue;
@@ -66,7 +69,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
foreach (var chatter in groupInfo.GroupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group))
_groups.Add(chatter.ChatterId, group.Name);
_groups.Add(chatter.ChatterId, group.Id);
_logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded.");
}
}

View File

@@ -46,7 +46,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
var group = _groups.Get(policy.GroupId.ToString());
if (group == null)
{
_logger.Debug($"Policy data failed: group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
_logger.Debug($"Policy's group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
continue;
}
_logger.Debug($"Policy data loaded [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]");

View File

@@ -33,11 +33,14 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
{
try
{
var re = new Regex(filter.Search, ((RegexOptions) filter.Flag) | RegexOptions.Compiled);
var re = new Regex(filter.Search, ((RegexOptions)filter.Flag) | RegexOptions.Compiled);
re.Match(string.Empty);
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;
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");

View File

@@ -0,0 +1,49 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateGroupAck : IRequestAck
{
public string Name => "update_group";
private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdateGroupAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_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 group = JsonSerializer.Deserialize<Group>(json, _options);
if (group == null)
{
_logger.Warning($"Group data is null.");
return;
}
var exists = _groups.Get(group.Id);
if (exists == null)
{
_logger.Warning($"Group id does not exist [group id: {group.Id}][group name: {group.Name}][group priority: {group.Priority}]");
return;
}
_logger.Debug($"Updating group [group id: {group.Id}][group name: {group.Name}][group priority: {group.Priority}]");
_groups.Modify(group);
_logger.Information($"Group has been updated [group id: {group.Id}]");
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Chat.Groups;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateGroupChatterAck : IRequestAck
{
public string Name => "update_group_chatter";
private readonly IChatterGroupManager _groups;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdateGroupChatterAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (string.IsNullOrWhiteSpace(json)) {
_logger.Warning($"Group Chatter JSON data is null.");
return;
}
var groupChatter = JsonSerializer.Deserialize<GroupChatter>(json, _options);
if (groupChatter == null)
{
_logger.Warning($"Group Chatter data is null.");
return;
}
_groups.Add(groupChatter.ChatterId, groupChatter.GroupId);
_logger.Information($"Chatter has been updated [chatter label: {groupChatter.ChatterLabel}]");
}
}
}

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

@@ -24,16 +24,22 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning($"Policy JSON data is null.");
return;
}
var policy = JsonSerializer.Deserialize<Policy>(json, _options);
if (policy == null)
{
_logger.Warning($"Policy data failed: null");
_logger.Warning($"Policy data is null.");
return;
}
var group = _groups.Get(policy.GroupId.ToString());
if (group == null)
{
_logger.Warning($"Policy data failed: group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
_logger.Warning($"Policy's group id not found [group id: {policy.GroupId}][policy id: {policy.Id}]");
return;
}

View File

@@ -21,10 +21,16 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning($"TTS Filter JSON data is null.");
return;
}
var filter = JsonSerializer.Deserialize<TTSWordFilter>(json, _options);
if (filter == null)
{
_logger.Warning($"TTS Filter data failed: null");
_logger.Warning($"TTS Filter data is null.");
return;
}
@@ -46,7 +52,10 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
re.Match(string.Empty);
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}]");
}

View File

@@ -33,13 +33,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
return;
}
var userId = requestData["user"].ToString();
var voiceId = requestData["voice"].ToString();
if (string.IsNullOrEmpty(userId))
{
_logger.Warning("User Id is invalid.");
return;
}
if (string.IsNullOrEmpty(voiceId))
{
_logger.Warning("Voice Id is invalid.");
@@ -52,7 +46,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
}
_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 MinorVersion { get; set; }
public int? PatchVersion { get; set; }
public required string Download { get; set; }
public required string Changelog { get; set; }
}

View File

@@ -4,6 +4,6 @@ namespace TwitchChatTTS.OBS.Socket.Data
{
public required string EventType { get; set; }
public int EventIntent { get; set; }
public required Dictionary<string, object> EventData { get; set; }
public Dictionary<string, object>? EventData { get; set; }
}
}

View File

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

View File

@@ -5,6 +5,6 @@ namespace TwitchChatTTS.OBS.Socket.Data
public required string RequestType { get; set; }
public required string RequestId { get; set; }
public required object RequestStatus { get; set; }
public required Dictionary<string, object> ResponseData { get; set; }
public Dictionary<string, object>? ResponseData { get; set; }
}
}

View File

@@ -68,7 +68,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
}
_logger.Debug($"Found the scene item by name [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][obs request id: {message.RequestId}].");
obs.AddSourceId(sourceName.ToString()!, (long)sceneItemId);
obs.AddSourceId(sourceName.ToString()!, long.Parse(sceneItemId.ToString()!));
requestData.ResponseValues = message.ResponseData;
break;

View File

@@ -5,8 +5,7 @@ using Serilog;
using System.Text.Json;
using System.Collections.Concurrent;
using TwitchChatTTS.OBS.Socket.Data;
using System.Timers;
using System.Net.WebSockets;
using CommonSocketLibrary.Backoff;
namespace TwitchChatTTS.OBS.Socket
{
@@ -16,8 +15,8 @@ namespace TwitchChatTTS.OBS.Socket
private readonly IDictionary<string, long> _sourceIds;
private string? URL;
private readonly IBackoff _backoff;
private readonly Configuration _configuration;
private System.Timers.Timer _reconnectTimer;
public bool Connected { get; set; }
public bool Identified { get; set; }
@@ -26,6 +25,7 @@ namespace TwitchChatTTS.OBS.Socket
public OBSSocketClient(
Configuration configuration,
[FromKeyedServices("hermes")] IBackoff backoff,
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger
@@ -35,12 +35,9 @@ namespace TwitchChatTTS.OBS.Socket
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}, logger)
{
_backoff = backoff;
_configuration = configuration;
_reconnectTimer = new System.Timers.Timer(TimeSpan.FromSeconds(30));
_reconnectTimer.Elapsed += async (sender, e) => await Reconnect(e);
_reconnectTimer.Enabled = false;
_requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, long>();
}
@@ -51,18 +48,19 @@ namespace TwitchChatTTS.OBS.Socket
OnConnected += (sender, e) =>
{
Connected = true;
_reconnectTimer.Enabled = false;
_logger.Information("OBS websocket client connected.");
};
OnDisconnected += (sender, e) =>
OnDisconnected += async (sender, e) =>
{
_reconnectTimer.Enabled = Identified;
_logger.Information($"OBS websocket client disconnected [status: {e.Status}][reason: {e.Reason}] " + (Identified ? "Will be attempting to reconnect every 30 seconds." : "Will not be attempting to reconnect."));
Connected = false;
Identified = false;
Streaming = false;
if (Identified)
await Reconnect(_backoff);
};
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
@@ -115,34 +113,6 @@ namespace TwitchChatTTS.OBS.Socket
await handler.Execute(this, message);
}
private async Task Reconnect(ElapsedEventArgs e)
{
if (Connected)
{
try
{
await DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), ""));
}
catch (Exception)
{
_logger.Error("Failed to disconnect from OBS websocket server.");
}
}
try
{
await Connect();
}
catch (WebSocketException wse) when (wse.Message.Contains("502"))
{
_logger.Error($"OBS websocket server cannot be found. Be sure the server is on by looking at OBS > Tools > Websocket Server Settings [code: {wse.ErrorCode}]");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to reconnect to OBS websocket server.");
}
}
public async Task Send(IEnumerable<RequestMessage> messages)
{
if (!Connected)

View File

@@ -35,7 +35,13 @@ public class SevenApiClient
{
_logger.Debug($"Fetching 7tv information using Twitch Id [twitch id: {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)
{

View File

@@ -9,25 +9,28 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
{
public class DispatchHandler : IWebSocketHandler
{
public int OperationCode { get; } = 0;
private readonly ILogger _logger;
private readonly IEmoteDatabase _emotes;
private readonly object _lock = new object();
public int OperationCode { get; } = 0;
private readonly Mutex _lock;
public DispatchHandler(IEmoteDatabase emotes, ILogger logger)
{
_emotes = emotes;
_logger = logger;
_lock = new Mutex();
}
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;
ApplyChanges(message?.Body?.Pulled, cf => cf.OldValue, true);
ApplyChanges(message?.Body?.Pushed, cf => cf.Value, false);
ApplyChanges(message?.Body?.Removed, cf => cf.OldValue, true);
ApplyChanges(message?.Body?.Updated, cf => cf.OldValue, false, cf => cf.Value);
ApplyChanges(message.Body.Pulled, cf => cf.OldValue, true);
ApplyChanges(message.Body.Pushed, cf => cf.Value, false);
ApplyChanges(message.Body.Removed, cf => cf.OldValue, true);
ApplyChanges(message.Body.Updated, cf => cf.OldValue, false, cf => cf.Value);
return Task.CompletedTask;
}
@@ -42,7 +45,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (value == null)
continue;
var o = JsonSerializer.Deserialize<EmoteField>(value.ToString(), new JsonSerializerOptions()
var o = JsonSerializer.Deserialize<EmoteField>(value.ToString()!, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
@@ -50,8 +53,9 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
if (o == null)
continue;
lock (_lock)
try
{
_lock.WaitOne();
if (removing)
{
if (_emotes.Get(o.Name) != o.Id)
@@ -71,8 +75,10 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
}
_emotes.Remove(o.Name);
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,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
@@ -94,6 +100,10 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
_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.Common;
using TwitchChatTTS.Seven.Socket.Data;

View File

@@ -87,12 +87,18 @@ s.AddTransient<ICommandFactory, CommandFactory>();
// Request acks
s.AddSingleton<RequestAckManager>();
s.AddTransient<IRequestAck, CreateGroupAck>();
s.AddTransient<IRequestAck, CreateGroupChatterAck>();
s.AddTransient<IRequestAck, CreateGroupPermissionAck>();
s.AddTransient<IRequestAck, CreatePolicyAck>();
s.AddTransient<IRequestAck, CreateRedeemableActionAck>();
s.AddTransient<IRequestAck, CreateRedemptionAck>();
s.AddTransient<IRequestAck, CreateTTSFilterAck>();
s.AddTransient<IRequestAck, CreateTTSUserAck>();
s.AddTransient<IRequestAck, CreateTTSVoiceAck>();
s.AddTransient<IRequestAck, DeleteGroupAck>();
s.AddTransient<IRequestAck, DeleteGroupChatterAck>();
s.AddTransient<IRequestAck, DeleteGroupPermissionAck>();
s.AddTransient<IRequestAck, DeletePolicyAck>();
s.AddTransient<IRequestAck, DeleteRedeemableActionAck>();
s.AddTransient<IRequestAck, DeleteRedemptionAck>();
@@ -110,6 +116,9 @@ s.AddTransient<IRequestAck, GetRedemptionsAck>();
s.AddTransient<IRequestAck, GetTTSUsersAck>();
s.AddTransient<IRequestAck, GetTTSVoicesAck>();
s.AddTransient<IRequestAck, GetTTSWordFiltersAck>();
s.AddTransient<IRequestAck, UpdateGroupAck>();
s.AddTransient<IRequestAck, UpdateGroupChatterAck>();
s.AddTransient<IRequestAck, UpdateGroupPermissionAck>();
s.AddTransient<IRequestAck, UpdateDefaultTTSVoiceAck>();
s.AddTransient<IRequestAck, UpdatePolicyAck>();
s.AddTransient<IRequestAck, UpdateRedeemableActionAck>();
@@ -138,6 +147,7 @@ s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
// OBS websocket
s.AddKeyedSingleton<IBackoff>("obs", new ExponentialBackoff(1000, 120 * 1000));
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs");
@@ -174,7 +184,6 @@ s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>
{
var factory = sp.GetRequiredService<ITwitchConnectionManager>();
var client = factory.GetWorkingClient();
client.Connect().Wait();
return client;
});
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create");
@@ -195,6 +204,7 @@ s.AddKeyedSingleton<ITwitchSocketHandler, ChannelFollowHandler>("twitch-notifica
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelRaidHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelResubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionEndHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionGiftHandler>("twitch-notifications");
// hermes websocket
@@ -202,6 +212,8 @@ s.AddKeyedSingleton<IBackoff>("hermes", new ExponentialBackoff(1000, 15 * 1000))
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoggingHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, SlaveHandler>("hermes");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");

119
TTS.cs
View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using NAudio.Wave.SampleProviders;
using org.mariuszgromada.math.mxparser;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Seven.Socket;
@@ -13,17 +12,18 @@ using TwitchChatTTS.Twitch.Socket.Messages;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Chat.Commands;
using System.Text;
using TwitchChatTTS.Chat.Speech;
using TwitchChatTTS.Veadotube;
using TwitchChatTTS.Bus;
using System.Reactive.Linq;
using System.Net.WebSockets;
namespace TwitchChatTTS
{
public class TTS : IHostedService
{
public const int MAJOR_VERSION = 4;
public const int MINOR_VERSION = 6;
public const int MINOR_VERSION = 9;
public const int PATCH_VERSION = 3;
private readonly User _user;
private readonly HermesApiClient _hermesApiClient;
@@ -36,8 +36,6 @@ namespace TwitchChatTTS
private readonly ICommandFactory _commandFactory;
private readonly ICommandManager _commandManager;
private readonly IEmoteDatabase _emotes;
private readonly TTSPlayer _player;
private readonly AudioPlaybackEngine _playback;
private readonly ServiceBusCentral _bus;
private readonly Configuration _configuration;
private readonly ILogger _logger;
@@ -54,8 +52,6 @@ namespace TwitchChatTTS
ICommandFactory commandFactory,
ICommandManager commandManager,
IEmoteDatabase emotes,
TTSPlayer player,
AudioPlaybackEngine playback,
ServiceBusCentral bus,
Configuration configuration,
ILogger logger
@@ -72,8 +68,6 @@ namespace TwitchChatTTS
_commandFactory = commandFactory;
_commandManager = commandManager;
_emotes = emotes;
_player = player;
_playback = playback;
_bus = bus;
_configuration = configuration;
_logger = logger;
@@ -84,10 +78,13 @@ namespace TwitchChatTTS
Console.Title = "TTS - Twitch Chat";
Console.OutputEncoding = Encoding.UTF8;
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))
{
_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;
}
@@ -99,9 +96,9 @@ namespace TwitchChatTTS
_logger.Error("Failed to fetch latest TTS version. Something went wrong.");
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");
if (changes != null && changes.Any())
_logger.Information("Changelog:\n - " + string.Join("\n - ", changes) + "\n\n");
@@ -113,56 +110,75 @@ namespace TwitchChatTTS
_logger.Warning("Failed to check for version updates.");
}
var disposables = new List<IDisposable>();
var connected = _bus.GetTopic("tts_connected");
// 7tv
var twitchTopic = _bus.GetTopic("twitch id");
twitchTopic.FirstAsync().Subscribe(async (data) =>
disposables.Add(connected.FirstAsync().Subscribe(async (data) =>
{
var twitchId = data.Value?.ToString();
if (twitchId == null)
if (data.Value is not User user)
{
_logger.Warning("Something went wrong. Unable to fetch 7tv data.");
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);
if (emoteSet != null)
{
_user.SevenEmoteSetId = emoteSet.Id;
_logger.Debug($"Fetched the 7tv emote set id [emote set id: {emoteSet.Id}]");
}
await InitializeEmotes(_sevenApiClient, emoteSet);
await InitializeSevenTv();
});
}));
await InitializeHermesWebsocket();
_playback.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
disposables.Add(connected.FirstAsync().Subscribe(async (data) =>
{
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)
{
_logger.Warning("Not connecting to Twitch. If this is wrong, ensure your Tom to Speech token is valid.");
return;
}
});
try
{
_veado.Initialize();
await _veado.Connect();
}
catch (Exception e)
{
_logger.Warning(e, "Failed to connect to Veado websocket server.");
}
try
{
await _twitch.Connect();
}
catch (Exception e)
{
_logger.Error(e, "Failed to connect to Twitch websocket server.");
await Task.Delay(TimeSpan.FromSeconds(30));
return;
}
try
{
await _twitch.Connect();
}
catch (Exception e)
{
_logger.Error(e, "Failed to connect to Twitch websocket server.");
}
}));
_commandManager.Update(_commandFactory);
await InitializeVeadotube();
await InitializeHermesWebsocket();
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)
@@ -181,6 +197,10 @@ namespace TwitchChatTTS
_hermes.Initialize();
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)
{
_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)
{
var globalEmotes = await sevenapi.FetchGlobalSevenEmotes();

View File

@@ -11,25 +11,37 @@ namespace TwitchChatTTS
{
public class TTSListening : IHostedService
{
private const string TTS_API_URL = "https://api.streamelements.com/kappa/v2/speech";
private readonly AudioPlaybackEngine _playback;
private readonly TTSPlayer _player;
private readonly TTSConsumer _consumer;
private readonly IDisposable _subscription;
private readonly User _user;
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;
_player = player;
_consumer = consumer;
_subscription = publisher.Subscribe(consumer);
_user = user;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_playback.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{
if (_player.Playing?.Audio == e.SampleProvider)
{
_player.Playing = null;
}
});
Task.Run(async () =>
{
while (true)
@@ -107,7 +119,7 @@ namespace TwitchChatTTS
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 provider = new CachedWavProvider(nws);
var data = _playback.ConvertSound(provider);

View File

@@ -1,4 +1,5 @@
using HermesSocketLibrary.Requests.Messages;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Redemptions
{
@@ -6,7 +7,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
{
void Add(RedeemableAction action);
void Add(Redemption redemption);
Task Execute(RedeemableAction action, string senderDisplayName, long senderId);
Task Execute(RedeemableAction action, string senderDisplayName, long senderId, string senderMessage);
IEnumerable<RedeemableAction> Get(string twitchRedemptionId);
void Initialize();
bool RemoveAction(string actionName);

View File

@@ -27,7 +27,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger;
private readonly Random _random;
private readonly object _lock;
private readonly ReaderWriterLockSlim _rwls;
public RedemptionManager(
@@ -50,7 +50,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
_playback = playback;
_logger = logger;
_random = new Random();
_lock = new object();
_rwls = new ReaderWriterLockSlim();
var topic = _bus.GetTopic("redemptions_initiation");
topic.Subscribe(data =>
@@ -99,7 +99,9 @@ namespace TwitchChatTTS.Twitch.Redemptions
{
_redemptions.Add(redemption.Id, redemption);
_logger.Debug($"Added redemption to redemption manager [redemption id: {redemption.Id}]");
} else {
}
else
{
_redemptions[redemption.Id] = redemption;
_logger.Debug($"Updated redemption to redemption manager [redemption id: {redemption.Id}]");
}
@@ -108,40 +110,36 @@ namespace TwitchChatTTS.Twitch.Redemptions
private void Add(string twitchRedemptionId, string redemptionId)
{
lock (_lock)
if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems))
_redeems.Add(twitchRedemptionId, redeems = new List<string>());
var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null;
if (item == null)
return;
var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null);
bool added = false;
for (int i = 0; i < redeems.Count; i++)
{
if (!_redeems.TryGetValue(twitchRedemptionId, out var redeems))
_redeems.Add(twitchRedemptionId, redeems = new List<string>());
var item = _redemptions.TryGetValue(redemptionId, out var r) ? r : null;
if (item == null)
if (redeems[i] != null && _redemptions.TryGetValue(redeems[i], out var rr))
{
return;
}
var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null);
bool added = false;
for (int i = 0; i < redeems.Count; i++)
{
if (redeems[i] != null && _redemptions.TryGetValue(redeems[i], out var rr))
if (item.Order > rr.Order)
{
if (item.Order > rr.Order)
{
redeems.Insert(i, redemptionId);
added = true;
break;
}
redeems.Insert(i, redemptionId);
added = true;
break;
}
}
if (!added)
redeems.Add(redemptionId);
}
if (!added)
redeems.Add(redemptionId);
_logger.Debug($"Added redemption action [redemption id: {redemptionId}][twitch redemption id: {twitchRedemptionId}]");
}
private void Add(string twitchRedemptionId, Redemption item)
{
lock (_lock)
_rwls.EnterWriteLock();
try
{
if (!_redeems.TryGetValue(twitchRedemptionId, out var redemptionNames))
_redeems.Add(twitchRedemptionId, redemptionNames = new List<string>());
@@ -163,10 +161,14 @@ namespace TwitchChatTTS.Twitch.Redemptions
if (!added)
redemptionNames.Add(item.Id);
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug($"Added redemption action [redemption id: {item.Id}][twitch redemption id: {twitchRedemptionId}]");
}
public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId)
public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId, string senderMessage)
{
_logger.Debug($"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
@@ -190,7 +192,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
if (!string.IsNullOrWhiteSpace(directory))
Directory.CreateDirectory(directory);
await File.WriteAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName));
await File.WriteAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName, senderId, senderMessage));
_logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
@@ -204,7 +206,7 @@ namespace TwitchChatTTS.Twitch.Redemptions
if (!string.IsNullOrWhiteSpace(directory))
Directory.CreateDirectory(directory);
await File.AppendAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName));
await File.AppendAllTextAsync(path, ReplaceContentText(action.Data["file_content"], senderDisplayName, senderId, senderMessage));
_logger.Debug($"Appended text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
@@ -263,6 +265,11 @@ namespace TwitchChatTTS.Twitch.Redemptions
break;
case "SPECIFIC_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;
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}]");
break;
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();
break;
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();
break;
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();
break;
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();
break;
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();
break;
case "VEADOTUBE_SET_STATE":
@@ -387,7 +419,8 @@ namespace TwitchChatTTS.Twitch.Redemptions
public IEnumerable<RedeemableAction> Get(string twitchRedemptionId)
{
lock (_lock)
_rwls.EnterReadLock();
try
{
if (_redeems.TryGetValue(twitchRedemptionId, out var redemptionIds))
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)
.Where(a => a != null)!;
}
finally
{
_rwls.ExitReadLock();
}
return [];
}
public void Initialize()
{
_logger.Debug($"Redemption manager is about to initialize [redemption count: {_redemptions.Count()}][action count: {_actions.Count}]");
lock (_lock)
_rwls.EnterWriteLock();
try
{
_logger.Debug($"Redemption manager is about to initialize [redemption count: {_redemptions.Count()}][action count: {_actions.Count}]");
_redeems.Clear();
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.");
}
public bool RemoveAction(string actionName)
{
return _actions.Remove(actionName);
_rwls.EnterWriteLock();
try
{
return _actions.Remove(actionName);
}
finally
{
_rwls.ExitWriteLock();
}
}
public bool RemoveRedemption(string redemptionId)
{
lock (_lock)
_rwls.EnterWriteLock();
try
{
if (!_redemptions.TryGetValue(redemptionId, out var redemption))
{
@@ -458,19 +508,29 @@ namespace TwitchChatTTS.Twitch.Redemptions
return true;
}
}
finally
{
_rwls.ExitWriteLock();
}
return false;
}
private string ReplaceContentText(string content, string username)
private string ReplaceContentText(string content, string chatter, long chatterId, string message)
{
return content.Replace("%USER%", username)
return content.Replace("%USER%", chatter)
.Replace("%chatter%", chatter)
.Replace("%chatterid%", chatterId.ToString())
.Replace("%broadcaster%", _user.TwitchUsername)
.Replace("%broadcasterid%", _user.TwitchUserId.ToString())
.Replace("%message%", message)
.Replace("\\n", "\n");
}
public bool Update(Redemption redemption)
{
lock (_lock)
_rwls.EnterWriteLock();
try
{
if (_redemptions.TryGetValue(redemption.Id, out var r))
{
@@ -511,6 +571,10 @@ namespace TwitchChatTTS.Twitch.Redemptions
return true;
}
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Warning($"Cannot find redemption by name [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]");
return false;
@@ -518,12 +582,20 @@ namespace TwitchChatTTS.Twitch.Redemptions
public bool Update(RedeemableAction action)
{
if (_actions.TryGetValue(action.Name, out var a))
_rwls.EnterWriteLock();
try
{
a.Type = action.Type;
a.Data = action.Data;
_logger.Debug($"Updated redeemable action in redemption manager [action name: {action.Name}]");
return true;
if (_actions.TryGetValue(action.Name, out var a))
{
a.Type = action.Type;
a.Data = action.Data;
_logger.Debug($"Updated redeemable action in redemption manager [action name: {action.Name}]");
return true;
}
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Warning($"Cannot find redeemable action by name [action name: {action.Name}]");

View File

@@ -37,7 +37,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId));
await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId), string.Empty);
}
catch (Exception ex)
{
@@ -71,7 +71,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId));
await _redemptionManager.Execute(action, message.RequesterUserLogin, long.Parse(message.RequesterUserId), string.Empty);
}
catch (Exception ex)
{

View File

@@ -1,4 +1,5 @@
using Serilog;
using TwitchChatTTS.Bus;
using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups;
@@ -13,11 +14,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public string Name => "channel.chat.message";
private readonly IChatMessageReader _reader;
private readonly User _user;
private readonly ICommandManager _commands;
private readonly IGroupPermissionManager _permissionManager;
private readonly IUsagePolicy<long> _permissionPolicy;
private readonly IChatterGroupManager _chatterGroupManager;
private readonly ServiceBusCentral _bus;
private readonly User _user;
private readonly ILogger _logger;
@@ -27,21 +29,19 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
IGroupPermissionManager permissionManager,
IUsagePolicy<long> permissionPolicy,
IChatterGroupManager chatterGroupManager,
ServiceBusCentral bus,
User user,
ILogger logger
)
{
_reader = reader;
_user = user;
_commands = commands;
_permissionManager = permissionManager;
_permissionPolicy = permissionPolicy;
_chatterGroupManager = chatterGroupManager;
_bus = bus;
_user = user;
_logger = logger;
_permissionPolicy.Set("everyone", "tts", 100, TimeSpan.FromSeconds(15));
_permissionPolicy.Set("everyone", "tts.chat.messages.read", 3, TimeSpan.FromMilliseconds(15000));
}
@@ -58,9 +58,16 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
var groups = GetGroups(message.Badges, chatterId);
var bits = GetTotalBits(fragments);
var commandResult = await CheckForChatCommand(message.Message.Text, message, groups);
if (commandResult != ChatCommandResult.Unknown)
return;
if (message.ChannelPointsCustomRewardId == null && !_user.Slave)
{
var commandResult = await CheckForChatCommand(message.Message.Text, message, groups);
if (commandResult != ChatCommandResult.Unknown)
return;
}
else if (message.ChannelPointsCustomRewardId != null)
{
_bus.Send(this, "chat_message_redemption", message);
}
string permission = GetPermissionPath(message.ChannelPointsCustomRewardId, bits);
if (!HasPermission(chatterId, groups, permission))
@@ -106,15 +113,14 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
var defaultGroups = new string[] { "everyone" };
var badgesGroups = badges.Select(b => b.SetId).Select(GetGroupNameByBadgeName);
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId);
var customGroups = _chatterGroupManager.GetGroupNamesFor(chatterId).ToArray();
return defaultGroups.Union(badgesGroups).Union(customGroups);
}
private int GetTotalBits(TwitchChatFragment[] fragments)
{
return fragments.Where(f => f.Type == "cheermote" && f.Cheermote != null)
.Select(f => f.Cheermote!.Bits)
.Sum();
.Sum(f => f.Cheermote!.Bits);
}
private string GetPermissionPath(string? customRewardId, int bits)
@@ -129,7 +135,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
private bool HasPermission(long chatterId, IEnumerable<string> groups, string permissionPath)
{
return chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath) == true;
return chatterId == _user.OwnerId || _permissionManager.CheckIfAllowed(groups, permissionPath) == true;
}
}
}

View File

@@ -1,4 +1,5 @@
using Serilog;
using TwitchChatTTS.Bus;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
@@ -8,16 +9,38 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public string Name => "channel.channel_points_custom_reward_redemption.add";
private readonly IDictionary<string, ChannelChatMessage> _messages;
private readonly IRedemptionManager _redemptionManager;
private readonly ServiceBusCentral _bus;
private readonly ILogger _logger;
public ChannelCustomRedemptionHandler(
IRedemptionManager redemptionManager,
ServiceBusCentral bus,
ILogger logger
)
{
_messages = new Dictionary<string, ChannelChatMessage>();
_redemptionManager = redemptionManager;
_bus = bus;
_logger = logger;
var topic = _bus.GetTopic("chat_message_redemption");
topic.Subscribe((d) =>
{
var message = d.Value as ChannelChatMessage;
if (message != null && !string.IsNullOrEmpty(message.ChannelPointsCustomRewardId))
{
_messages.Add(message.ChannelPointsCustomRewardId, message);
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(5000));
_messages.Remove(message.ChannelPointsCustomRewardId);
});
}
});
}
public async Task Execute(TwitchWebsocketClient sender, object data)
@@ -37,10 +60,32 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
}
_logger.Debug($"Found {actions.Count()} actions for this Twitch channel point redemption [redeem: {message.Reward.Title}][redeem id: {message.Reward.Id}][transaction: {message.Id}]");
ChannelChatMessage? chatMessage = null;
if (actions.Any(a => a.HasMessage))
{
if (!_messages.TryGetValue(message.Reward.Id, out chatMessage))
{
DateTime start = DateTime.Now;
_logger.Debug("Waiting on redemption message...");
while (DateTime.Now - start < TimeSpan.FromMilliseconds(1000) && !_messages.ContainsKey(message.Reward.Id))
await Task.Delay(100);
if (!_messages.TryGetValue(message.Reward.Id, out chatMessage))
{
_logger.Warning("Chat message was not found within a second of the channel redemption being received. Skipping all redeemable actions.");
return;
}
}
if (chatMessage != null)
_logger.Information($"Linked redemption to chat message [redemption id: {message.Reward.Id}][message id: {chatMessage.MessageId}].");
}
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId), message.UserInput);
}
catch (Exception ex)
{

View File

@@ -36,7 +36,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId));
await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId), string.Empty);
}
catch (Exception ex)
{

View File

@@ -38,15 +38,15 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
var actions = _redemptionManager.Get("subscription");
if (!actions.Any())
{
_logger.Debug($"No redeemable actions for this subscription was found [message: {message.Message.Text}]");
_logger.Debug($"No redeemable actions for this subscription was found [chatter id: {message.UserId}][chatter: {message.UserName}][message: {message.Message.Text}]");
return;
}
_logger.Debug($"Found {actions.Count()} actions for this Twitch subscription [message: {message.Message.Text}]");
_logger.Debug($"Found {actions.Count()} actions for this Twitch subscription [chatter id: {message.UserId}][chatter: {message.UserName}][message: {message.Message.Text}]");
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!));
await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!), message.Message.Text);
}
catch (Exception ex)
{

View File

@@ -0,0 +1,52 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Socket.Handlers
{
public class ChannelSubscriptionEndHandler : ITwitchSocketHandler
{
public string Name => "channel.subscription.end";
private readonly IRedemptionManager _redemptionManager;
private readonly ILogger _logger;
public ChannelSubscriptionEndHandler(IRedemptionManager redemptionManager, ILogger logger)
{
_redemptionManager = redemptionManager;
_logger = logger;
}
public async Task Execute(TwitchWebsocketClient sender, object data)
{
if (data is not ChannelSubscriptionEndMessage message)
return;
_logger.Information($"Subscription ended [chatter: {message.UserLogin}][chatter id: {message.UserId}][Tier: {message.Tier}]");
try
{
var actions = _redemptionManager.Get("subscription.end");
if (!actions.Any())
{
_logger.Debug($"No redeemable 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!), string.Empty);
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to execute redeemable action [action: {action.Name}][action type: {action.Type}][redeem: subscription][subscriber: {message.UserLogin}][subscriber id: {message.UserId}]");
}
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for subscription [subscriber: {message.UserLogin}][subscriber id: {message.UserId}]");
}
}
}
}

View File

@@ -40,7 +40,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName ?? "Anonymous", message.UserId == null ? 0 : long.Parse(message.UserId));
await _redemptionManager.Execute(action, message.UserName ?? "Anonymous", message.UserId == null ? 0 : long.Parse(message.UserId), string.Empty);
}
catch (Exception ex)
{

View File

@@ -38,7 +38,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
foreach (var action in actions)
try
{
await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!));
await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!), string.Empty);
}
catch (Exception ex)
{

View File

@@ -37,11 +37,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
_messageTypes.Add("channel.chat.clear_user_messages", typeof(ChannelChatClearUserMessage));
_messageTypes.Add("channel.chat.message_delete", typeof(ChannelChatDeleteMessage));
_messageTypes.Add("channel.channel_points_custom_reward_redemption.add", typeof(ChannelCustomRedemptionMessage));
_messageTypes.Add("channel.raid", typeof(ChannelRaidMessage));
_messageTypes.Add("channel.follow", typeof(ChannelFollowMessage));
_messageTypes.Add("channel.raid", typeof(ChannelRaidMessage));
_messageTypes.Add("channel.subscribe", typeof(ChannelSubscriptionMessage));
_messageTypes.Add("channel.subscription.message", typeof(ChannelResubscriptionMessage));
_messageTypes.Add("channel.subscription.end", typeof(ChannelSubscriptionEndMessage));
_messageTypes.Add("channel.subscription.gift", typeof(ChannelSubscriptionGiftMessage));
_messageTypes.Add("channel.subscription.message", typeof(ChannelResubscriptionMessage));
}
public Task Execute(TwitchWebsocketClient sender, object data)

View File

@@ -29,18 +29,9 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
return;
}
int waited = 0;
while ((_user.TwitchUserId <= 0 || _user.TwitchConnection == null) && ++waited < 5)
await Task.Delay(TimeSpan.FromSeconds(1));
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 twitchConnection = _user.TwitchConnection!;
_api.Initialize(twitchConnection.ClientId, twitchConnection.AccessToken);
var span = twitchConnection.ExpiresAt - DateTime.Now;
var timeLeft = span.Days >= 2 ? span.Days + " days" : (span.Hours >= 2 ? span.Hours + " hours" : span.Minutes + " minutes");
if (span.Days >= 3)
_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.");
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;
}
@@ -61,6 +52,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
"channel.chat.clear",
"channel.chat.clear_user_messages",
"channel.subscribe",
"channel.subscription.end",
"channel.subscription.gift",
"channel.subscription.message",
"channel.ad_break.begin",

View File

@@ -15,6 +15,12 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
public TwitchReplyInfo? Reply { get; set; }
public string? ChannelPointsCustomRewardId { get; set; }
public string? ChannelPointsAnimationId { get; set; }
public string? SourceBroadcasterUserId { get; set; }
public string? SourceBroadcasterUserName { get; set; }
public string? SourceBroadcasterUserLogin { get; set; }
public string? SourceMessageId { get; set; }
public TwitchBadge[]? SourceBadges { get; set; }
}
public class TwitchChatMessageInfo

View File

@@ -9,6 +9,7 @@ namespace TwitchChatTTS.Twitch.Socket.Messages
public required string UserId { get; set; }
public required string UserLogin { get; set; }
public required string UserName { get; set; }
public required string UserInput { get; set; }
public required string Status { get; set; }
public DateTime RedeemedAt { get; set; }
public required RedemptionReward Reward { get; set; }

View File

@@ -0,0 +1,7 @@
namespace TwitchChatTTS.Twitch.Socket.Messages
{
public class ChannelSubscriptionEndMessage : ChannelSubscriptionData
{
public bool IsGift { get; set; }
}
}

View File

@@ -18,21 +18,22 @@ namespace TwitchChatTTS.Twitch.Socket
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private readonly object _lock;
private readonly Mutex _mutex;
public TwitchConnectionManager(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_lock = new object();
_mutex = new Mutex();
}
public TwitchWebsocketClient GetBackupClient()
{
lock (_lock)
try
{
_mutex.WaitOne();
if (_identified == null)
throw new InvalidOperationException("Cannot get backup Twitch client yet. Waiting for identification.");
if (_backup != null)
@@ -40,12 +41,17 @@ namespace TwitchChatTTS.Twitch.Socket
return CreateNewClient();
}
finally
{
_mutex.ReleaseMutex();
}
}
public TwitchWebsocketClient GetWorkingClient()
{
lock (_lock)
try
{
_mutex.WaitOne();
if (_identified == null)
{
return CreateNewClient();
@@ -53,6 +59,10 @@ namespace TwitchChatTTS.Twitch.Socket
return _identified;
}
finally
{
_mutex.ReleaseMutex();
}
}
private TwitchWebsocketClient CreateNewClient()
@@ -74,8 +84,9 @@ namespace TwitchChatTTS.Twitch.Socket
private async Task OnDisconnection(TwitchWebsocketClient client)
{
bool reconnecting = false;
lock (_lock)
try
{
_mutex.WaitOne();
if (_identified?.UID == client.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
_logger.Warning($"Twitch client disconnected from unknown source [client: {client.UID}][main: {_identified?.UID}][backup: {_backup?.UID}]");
}
finally
{
_mutex.ReleaseMutex();
}
if (reconnecting)
{
var newClient = GetWorkingClient();
await newClient.Reconnect();
}
}
private async Task OnIdentified(TwitchWebsocketClient client)
{
bool clientDisconnect = false;
lock (_lock)
try
{
_mutex.WaitOne();
if (_identified == null || _identified.ReceivedReconnecting)
{
if (_backup != null && _backup.UID == client.UID)
@@ -126,6 +143,10 @@ namespace TwitchChatTTS.Twitch.Socket
clientDisconnect = true;
}
}
finally
{
_mutex.ReleaseMutex();
}
if (clientDisconnect)
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 IBackoff _backoff;
private readonly Configuration _configuration;
private bool _disconnected;
private readonly object _lock;
public event EventHandler<EventArgs>? OnIdentified;
public string UID { get; }
public string URL;
public bool Connected { get; private set; }
public bool Identified { get; private set; }
public string? SessionId { get; private set; }
public bool ReceivedReconnecting { get; set; }
public bool TwitchReconnected { get; set; }
@@ -46,13 +42,14 @@ namespace TwitchChatTTS.Twitch.Socket
_backoff = backoff;
_configuration = configuration;
_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));
_messageTypes = new Dictionary<string, Type>
{
{ "session_keepalive", typeof(object) },
{ "session_welcome", typeof(SessionWelcomeMessage) },
{ "session_reconnect", typeof(SessionWelcomeMessage) },
{ "notification", typeof(NotificationMessage) }
};
UID = Guid.NewGuid().ToString("D");
@@ -88,25 +85,12 @@ namespace TwitchChatTTS.Twitch.Socket
_logger.Information($"Initializing Twitch websocket client.");
OnConnected += (sender, e) =>
{
Connected = true;
_logger.Information("Twitch websocket client connected.");
_disconnected = false;
};
OnDisconnected += (sender, e) =>
{
lock (_lock)
{
if (_disconnected)
return;
_disconnected = true;
}
_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)
{
Identified = true;
SessionId = sessionId;
OnIdentified?.Invoke(this, EventArgs.Empty);
}
@@ -200,7 +183,7 @@ namespace TwitchChatTTS.Twitch.Socket
while (current < total)
{
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;
}
_logger.Debug("Twitch TX #" + type + ": " + content);

View File

@@ -9,21 +9,21 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="NAudio.Extras" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.2" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="System.Reactive" Version="6.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
<PackageReference Include="System.Text.Json" Version="9.0.1" />
<PackageReference Include="NAudio.Core" Version="2.2.1" />
<PackageReference Include="YamlDotNet" Version="16.0.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -178,7 +178,7 @@ namespace TwitchChatTTS.Veadotube
while (current < total)
{
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;
}
_logger.Debug($"Veado TX [message type: {typeof(T).Name}]: " + content);