Compare commits

..

36 Commits

Author SHA1 Message Date
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
Tom
5e33d594d2 Fixed a lot of compiler warnings. Fixed 7tv connection. 2025-01-17 00:54:47 +00:00
Tom
b8d0e8cfd8 Removed 3 unused classes. 2025-01-16 17:06:24 +00:00
Tom
f3d7c33b83 Removed ! on instances of Search property of TTSWordFilter. 2025-01-16 17:00:01 +00:00
Tom
b8de9532e2 Fixed adding, modifying and fetching TTS filters. 2025-01-16 01:12:49 +00:00
Tom
5fc1b5f942 Version update check is optional. Removed reliance on web API. Fixed client reconnection due to redemptions. 2025-01-14 03:48:02 +00:00
Tom
b74b1d70f3 Update to version 4.6 2025-01-14 01:28:34 +00:00
Tom
86590f1c7f Fixed directory creation when no directory is mentioned for certain redeemable actions. Undid property name change for Twitch Redemption Id. 2025-01-14 01:27:25 +00:00
Tom
4099322ce2 Undo the connection change for Hermes client. 2025-01-14 01:21:11 +00:00
Tom
75fa154546 Removal of default policies. 2025-01-14 01:20:44 +00:00
Tom
b724cd00eb Added a simple method for subscribing to events. 2025-01-07 15:42:10 +00:00
Tom
64cb0c1f6d Added missing websocket support for Redemptions and Actions. Fixed Ad Break actions. Cleaned some code. 2025-01-07 15:30:13 +00:00
Tom
77b37f04b6 Added Actions & Redemptions updates via websocket messages. Updated RedemptionManager due to live changes. 2025-01-06 14:36:54 +00:00
Tom
d74b132c0f Added TTS Filter websocket requests. 2025-01-01 17:26:06 +00:00
Tom
4f5dd8f24e Fixed some of the compiler warnings. 2024-12-28 21:19:28 +00:00
Tom
db1d57c218 Version update to 4.5 2024-12-03 02:39:46 +00:00
Tom
850c09cfff Made Veadotube redemptions more user friendly 2024-12-03 02:39:27 +00:00
Tom
ea0550e99f Added slave client in configuration. 2024-12-02 21:25:12 +00:00
Tom
893cd6f192 Ignore messages that are in queue for too long 2024-12-02 21:00:50 +00:00
134 changed files with 2445 additions and 832 deletions

View File

@ -37,7 +37,7 @@ namespace TwitchChatTTS.Bus
{ {
if (!_topics.TryGetValue(topic, out var bus)) if (!_topics.TryGetValue(topic, out var bus))
{ {
bus = new ServiceBusObservable(topic, this); bus = new ServiceBusObservable(topic, this, _logger);
_topics.Add(topic, bus); _topics.Add(topic, bus);
} }
return bus; return bus;

View File

@ -1,4 +1,5 @@
using System.Reactive; using System.Reactive;
using Serilog;
namespace TwitchChatTTS.Bus namespace TwitchChatTTS.Bus
{ {
@ -6,11 +7,13 @@ namespace TwitchChatTTS.Bus
{ {
private readonly string _topic; private readonly string _topic;
private readonly ServiceBusCentral _central; private readonly ServiceBusCentral _central;
private readonly ILogger _logger;
public ServiceBusObservable(string topic, ServiceBusCentral central) public ServiceBusObservable(string topic, ServiceBusCentral central, ILogger logger)
{ {
_topic = topic; _topic = topic;
_central = central; _central = central;
_logger = logger;
} }
protected override IDisposable SubscribeCore(IObserver<ServiceBusData> observer) protected override IDisposable SubscribeCore(IObserver<ServiceBusData> observer)
@ -19,6 +22,10 @@ namespace TwitchChatTTS.Bus
return new ServiceBusUnsubscriber(_topic, _central, observer); return new ServiceBusUnsubscriber(_topic, _central, observer);
} }
public IDisposable Subscribe(Action<ServiceBusData> action) {
return Subscribe(new ServiceBusObserver(action, _logger));
}
private sealed class ServiceBusUnsubscriber : IDisposable private sealed class ServiceBusUnsubscriber : IDisposable
{ {
private readonly string _topic; private readonly string _topic;

View File

@ -14,9 +14,8 @@ namespace TwitchChatTTS.Chat.Commands
public class CommandManager : ICommandManager public class CommandManager : ICommandManager
{ {
private readonly User _user; private readonly User _user;
private ICommandSelector _commandSelector; private ICommandSelector? _commandSelector;
private readonly HermesSocketClient _hermes; private readonly HermesSocketClient _hermes;
//private readonly TwitchWebsocketClient _twitch;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
private readonly IUsagePolicy<long> _permissionPolicy; private readonly IUsagePolicy<long> _permissionPolicy;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -26,7 +25,6 @@ namespace TwitchChatTTS.Chat.Commands
public CommandManager( public CommandManager(
User user, User user,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes, [FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
//[FromKeyedServices("twitch")] SocketClient<TwitchWebsocketMessage> twitch,
IGroupPermissionManager permissionManager, IGroupPermissionManager permissionManager,
IUsagePolicy<long> limitManager, IUsagePolicy<long> limitManager,
ILogger logger ILogger logger
@ -34,7 +32,6 @@ namespace TwitchChatTTS.Chat.Commands
{ {
_user = user; _user = user;
_hermes = (hermes as HermesSocketClient)!; _hermes = (hermes as HermesSocketClient)!;
//_twitch = (twitch as TwitchWebsocketClient)!;
_permissionManager = permissionManager; _permissionManager = permissionManager;
_permissionPolicy = limitManager; _permissionPolicy = limitManager;
_logger = logger; _logger = logger;
@ -43,7 +40,7 @@ namespace TwitchChatTTS.Chat.Commands
public async Task<ChatCommandResult> Execute(string arg, ChannelChatMessage message, IEnumerable<string> groups) public async Task<ChatCommandResult> Execute(string arg, ChannelChatMessage message, IEnumerable<string> groups)
{ {
if (string.IsNullOrWhiteSpace(arg)) if (string.IsNullOrWhiteSpace(arg) || _commandSelector == null)
return ChatCommandResult.Unknown; return ChatCommandResult.Unknown;
arg = arg.Trim(); arg = arg.Trim();

View File

@ -12,7 +12,7 @@ namespace TwitchChatTTS.Chat.Commands.Limits
public UsagePolicy(ILogger logger) public UsagePolicy(ILogger logger)
{ {
_logger = logger; _logger = logger;
_root = new UsagePolicyNode<K>(string.Empty, null, null, logger); _root = new UsagePolicyNode<K>(string.Empty, null, null, logger, root: true);
} }
@ -102,22 +102,26 @@ namespace TwitchChatTTS.Chat.Commands.Limits
private IDictionary<T, UserUsageData> _usages { get; } private IDictionary<T, UserUsageData> _usages { get; }
private IList<UsagePolicyNode<T>> _children { get; } private IList<UsagePolicyNode<T>> _children { get; }
private ILogger _logger; private ILogger _logger;
private object _lock { get; } private ReaderWriterLockSlim _rwls { get; }
public UsagePolicyNode(string name, UsagePolicyLimit? data, UsagePolicyNode<T>? parent, ILogger logger) 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; Name = name;
Limit = data; Limit = data;
_parent = parent; _parent = parent;
_usages = new Dictionary<T, UserUsageData>(); _usages = new Dictionary<T, UserUsageData>();
_children = new List<UsagePolicyNode<T>>(); _children = new List<UsagePolicyNode<T>>();
_logger = logger; _logger = logger;
_lock = new object(); _rwls = new ReaderWriterLockSlim();
} }
public UsagePolicyNode<T>? Get(IEnumerable<string> path) public UsagePolicyNode<T>? Get(IEnumerable<string> path)
{
_rwls.EnterReadLock();
try
{ {
if (!path.Any()) if (!path.Any())
return this; return this;
@ -128,8 +132,16 @@ namespace TwitchChatTTS.Chat.Commands.Limits
return this; return this;
return next.Get(path.Skip(1)); return next.Get(path.Skip(1));
} }
finally
{
_rwls.ExitReadLock();
}
}
public UsagePolicyNode<T>? Remove(IEnumerable<string> path) public UsagePolicyNode<T>? Remove(IEnumerable<string> path)
{
_rwls.EnterWriteLock();
try
{ {
if (!path.Any()) if (!path.Any())
{ {
@ -147,8 +159,16 @@ namespace TwitchChatTTS.Chat.Commands.Limits
return null; return null;
return next.Remove(path.Skip(1)); return next.Remove(path.Skip(1));
} }
finally
{
_rwls.ExitWriteLock();
}
}
public void Set(IEnumerable<string> path, int count, TimeSpan span) public void Set(IEnumerable<string> path, int count, TimeSpan span)
{
_rwls.EnterWriteLock();
try
{ {
if (!path.Any()) if (!path.Any())
{ {
@ -166,8 +186,16 @@ namespace TwitchChatTTS.Chat.Commands.Limits
} }
next.Set(path.Skip(1), count, span); next.Set(path.Skip(1), count, span);
} }
finally
{
_rwls.ExitWriteLock();
}
}
public bool TryUse(T key, DateTime timestamp) public bool TryUse(T key, DateTime timestamp)
{
_rwls.EnterUpgradeableReadLock();
try
{ {
if (_parent == null) if (_parent == null)
return false; return false;
@ -175,23 +203,35 @@ namespace TwitchChatTTS.Chat.Commands.Limits
return _parent.TryUse(key, timestamp); return _parent.TryUse(key, timestamp);
UserUsageData? usage; UserUsageData? usage;
lock (_lock)
{
if (!_usages.TryGetValue(key, out usage)) if (!_usages.TryGetValue(key, out usage))
{
_rwls.EnterWriteLock();
try
{ {
usage = new UserUsageData(Limit.Count, 1 % Limit.Count); usage = new UserUsageData(Limit.Count, 1 % Limit.Count);
usage.Uses[0] = timestamp; usage.Uses[0] = timestamp;
_usages.Add(key, usage); _usages.Add(key, usage);
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug($"internal use node create"); _logger.Debug($"internal use node create");
return true; return true;
} }
if (usage.Uses.Length != Limit.Count) if (usage.Uses.Length != Limit.Count)
{
_rwls.EnterWriteLock();
try
{ {
var sizeDiff = Math.Max(0, usage.Uses.Length - Limit.Count); var sizeDiff = Math.Max(0, usage.Uses.Length - Limit.Count);
var temp = usage.Uses.Skip(sizeDiff); var temp = usage.Uses.Skip(sizeDiff);
var tempSize = usage.Uses.Length - sizeDiff; var tempSize = usage.Uses.Length - sizeDiff;
usage.Uses = temp.Union(new DateTime[Math.Max(0, Limit.Count - tempSize)]).ToArray(); usage.Uses = temp.Union(new DateTime[Math.Max(0, Limit.Count - tempSize)]).ToArray();
finally
{
_rwls.ExitWriteLock();
} }
} }
@ -203,11 +243,21 @@ namespace TwitchChatTTS.Chat.Commands.Limits
} }
_logger.Debug($"internal use node normal [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]"); _logger.Debug($"internal use node normal [span: {(timestamp - usage.Uses[usage.Index]).TotalMilliseconds}][index: {usage.Index}]");
lock (_lock) _rwls.EnterWriteLock();
try
{ {
usage.Uses[usage.Index] = timestamp; usage.Uses[usage.Index] = timestamp;
usage.Index = (usage.Index + 1) % Limit.Count; usage.Index = (usage.Index + 1) % Limit.Count;
} }
finally
{
_rwls.ExitWriteLock();
}
}
finally
{
_rwls.ExitWriteLock();
}
return true; return true;
} }

View File

@ -107,7 +107,7 @@ namespace TwitchChatTTS.Chat.Commands
_logger.Information("Cleared Nightbot queue."); _logger.Information("Cleared Nightbot queue.");
} }
} }
catch (HttpRequestException e) catch (HttpRequestException)
{ {
_logger.Warning("Ensure your Nightbot account is linked to your TTS account."); _logger.Warning("Ensure your Nightbot account is linked to your TTS account.");
} }

View File

@ -104,10 +104,11 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger; _logger = logger;
} }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes) public Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{ {
_obsManager.ClearCache(); _obsManager.ClearCache();
_logger.Information("Cleared the cache used for OBS."); _logger.Information("Cleared the cache used for OBS.");
return Task.CompletedTask;
} }
} }

View File

@ -52,15 +52,16 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger; _logger = logger;
} }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes) public Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{ {
if (_player.Playing == null) if (_player.Playing == null)
return; return Task.CompletedTask;
_playback.RemoveMixerInput(_player.Playing.Audio!); _playback.RemoveMixerInput(_player.Playing.Audio!);
_player.Playing = null; _player.Playing = null;
_logger.Information("Skipped current tts."); _logger.Information("Skipped current tts.");
return Task.CompletedTask;
} }
} }
@ -79,17 +80,18 @@ namespace TwitchChatTTS.Chat.Commands
_logger = logger; _logger = logger;
} }
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes) public Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{ {
_player.RemoveAll(); _player.RemoveAll();
if (_player.Playing == null) if (_player.Playing == null)
return; return Task.CompletedTask;
_playback.RemoveMixerInput(_player.Playing.Audio!); _playback.RemoveMixerInput(_player.Playing.Audio!);
_player.Playing = null; _player.Playing = null;
_logger.Information("Skipped all queued and playing tts."); _logger.Information("Skipped all queued and playing tts.");
return Task.CompletedTask;
} }
} }
} }

View File

@ -1,7 +1,6 @@
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Serilog; using Serilog;
using TwitchChatTTS.Hermes.Socket; using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.Twitch.Socket;
using TwitchChatTTS.Twitch.Socket.Messages; using TwitchChatTTS.Twitch.Socket.Messages;
using static TwitchChatTTS.Chat.Commands.TTSCommands; using static TwitchChatTTS.Chat.Commands.TTSCommands;
@ -40,9 +39,9 @@ namespace TwitchChatTTS.Chat.Commands
public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes) public async Task Execute(IDictionary<string, string> values, ChannelChatMessage message, HermesSocketClient hermes)
{ {
_logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}"); _logger.Information($"TTS Version: {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.{TTS.PATCH_VERSION}");
await hermes.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}."); await hermes.SendLoggingMessage(HermesLoggingLevel.Info, $"{_user.TwitchUsername} [twitch id: {_user.TwitchUserId}] using version {TTS.MAJOR_VERSION}.{TTS.MINOR_VERSION}.{TTS.PATCH_VERSION}.");
} }
} }
} }

View File

@ -3,7 +3,6 @@ namespace TwitchChatTTS.Chat.Emotes
public class EmoteDatabase : IEmoteDatabase public class EmoteDatabase : IEmoteDatabase
{ {
private readonly IDictionary<string, string> _emotes; private readonly IDictionary<string, string> _emotes;
public IDictionary<string, string> Emotes { get => _emotes.AsReadOnly(); }
public EmoteDatabase() public EmoteDatabase()
{ {
@ -36,20 +35,20 @@ namespace TwitchChatTTS.Chat.Emotes
public class EmoteSet public class EmoteSet
{ {
public string Id { get; set; } public required string Id { get; set; }
public string Name { get; set; } public required string Name { get; set; }
public int Flags { get; set; } public int Flags { get; set; }
public bool Immutable { get; set; } public bool Immutable { get; set; }
public bool Privileged { get; set; } public bool Privileged { get; set; }
public IList<Emote> Emotes { get; set; } public required IList<Emote> Emotes { get; set; }
public int EmoteCount { get; set; } public int EmoteCount { get; set; }
public int Capacity { get; set; } public int Capacity { get; set; }
} }
public class Emote public class Emote
{ {
public string Id { get; set; } public required string Id { get; set; }
public string Name { get; set; } public required string Name { get; set; }
public int Flags { get; set; } public int Flags { get; set; }
} }
} }

View File

@ -21,23 +21,30 @@ namespace TwitchChatTTS.Chat.Groups
public void Add(Group group) public void Add(Group group)
{ {
_groups.Add(group.Name, group); _groups.Add(group.Id, group);
} }
public void Add(long chatter, string groupName) public void Add(long chatterId, string groupId)
{ {
_chatters.Add(chatter, new List<string>() { groupName }); if (_chatters.TryGetValue(chatterId, out var list))
{
if (!list.Contains(groupId))
list.Add(groupId);
}
else
_chatters.Add(chatterId, new List<string>() { groupId });
} }
public void Add(long chatter, ICollection<string> groupNames) public void Add(long chatter, ICollection<string> groupIds)
{ {
if (_chatters.TryGetValue(chatter, out var list)) if (_chatters.TryGetValue(chatter, out var list))
{ {
foreach (var group in groupNames) foreach (var groupId in groupIds)
list.Add(group); if (!list.Contains(groupId))
list.Add(groupId);
} }
else else
_chatters.Add(chatter, groupNames); _chatters.Add(chatter, groupIds);
} }
public void Clear() public void Clear()
@ -46,9 +53,9 @@ namespace TwitchChatTTS.Chat.Groups
_chatters.Clear(); _chatters.Clear();
} }
public Group? Get(string groupName) public Group? Get(string groupId)
{ {
if (_groups.TryGetValue(groupName, out var group)) if (_groups.TryGetValue(groupId, out var group))
return group; return group;
return null; return null;
} }
@ -56,7 +63,9 @@ namespace TwitchChatTTS.Chat.Groups
public IEnumerable<string> GetGroupNamesFor(long chatter) public IEnumerable<string> GetGroupNamesFor(long chatter)
{ {
if (_chatters.TryGetValue(chatter, out var groups)) if (_chatters.TryGetValue(chatter, out var groups))
return groups.Select(g => _groups[g].Name); 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>();
} }
@ -69,23 +78,39 @@ namespace TwitchChatTTS.Chat.Groups
return GetPriorityFor(groups); return GetPriorityFor(groups);
} }
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); var values = groupIds.Select(g => _groups.TryGetValue(g, out var group) ? group : null).Where(g => g != null);
if (values.Any()) if (values.Any())
return values.Max(g => g.Priority); return values.Max(g => g!.Priority);
return 0; return 0;
} }
public void Modify(Group group)
{
_groups[group.Id] = group;
}
public bool Remove(string groupId)
{
if (_groups.Remove(groupId))
{
foreach (var entry in _chatters)
entry.Value.Remove(groupId);
return true;
}
return false;
}
public bool Remove(long chatterId, string groupId) public bool Remove(long chatterId, string groupId)
{ {
if (_chatters.TryGetValue(chatterId, out var groups)) if (_chatters.TryGetValue(chatterId, out var groups))
{ {
groups.Remove(groupId); groups.Remove(groupId);
_logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId]}][group id: {groupId}]"); _logger.Debug($"Removed chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return true; return true;
} }
_logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId]}][group id: {groupId}]"); _logger.Debug($"Failed to remove chatter from group [chatter id: {chatterId}][group name: {_groups[groupId].Name}][group id: {groupId}]");
return false; return false;
} }
} }

View File

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

View File

@ -62,7 +62,7 @@ namespace TwitchChatTTS.Chat.Messaging
var emoteUsage = GetEmoteUsage(fragments); var emoteUsage = GetEmoteUsage(fragments);
var tasks = new List<Task>(); var tasks = new List<Task>();
if (_obs.Streaming) if (_obs.Streaming && !_user.Slave)
{ {
if (emoteUsage.NewEmotes.Any()) if (emoteUsage.NewEmotes.Any())
tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes)); tasks.Add(_hermes.SendEmoteDetails(emoteUsage.NewEmotes));
@ -84,6 +84,7 @@ namespace TwitchChatTTS.Chat.Messaging
var msg = FilterMessage(fragments, reply); var msg = FilterMessage(fragments, reply);
string voiceSelected = chatterId == null ? _user.DefaultTTSVoice : GetSelectedVoiceFor(chatterId.Value); string voiceSelected = chatterId == null ? _user.DefaultTTSVoice : GetSelectedVoiceFor(chatterId.Value);
var messages = GetPartialTTSMessages(msg, voiceSelected).ToList(); 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); var groupedMessage = new TTSGroupedMessage(broadcasterId, chatterId, messageId, messages, DateTime.UtcNow, priority);
_player.Add(groupedMessage, groupedMessage.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; var m = match.Groups["message"].Value;
if (string.IsNullOrWhiteSpace(m)) if (string.IsNullOrWhiteSpace(m))
@ -233,6 +234,13 @@ namespace TwitchChatTTS.Chat.Messaging
voiceSelected = voiceSelected[0].ToString().ToUpper() + voiceSelected.Substring(1).ToLower(); voiceSelected = voiceSelected[0].ToString().ToUpper() + voiceSelected.Substring(1).ToLower();
return HandlePartialMessage(voiceSelected, m); 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) private string GetSelectedVoiceFor(long chatterId)

View File

@ -14,7 +14,6 @@ namespace TwitchChatTTS
} }
public class TwitchConfiguration { public class TwitchConfiguration {
public IEnumerable<string>? Channels;
public bool TtsWhenOffline; public bool TtsWhenOffline;
public string? WebsocketUrl; public string? WebsocketUrl;
public string? ApiUrl; public string? ApiUrl;

View File

@ -1,6 +0,0 @@
public class Account {
public string Id { get; set; }
public string Username { get; set; }
public string Role { get; set; }
public string BroadcasterId { get; set; }
}

View File

@ -1,49 +0,0 @@
namespace TwitchChatTTS.Hermes
{
public interface ICustomDataManager
{
void Add(string key, object value, string type);
void Change(string key, object value);
void Delete(string key);
object? Get(string key);
}
public class CustomDataManager : ICustomDataManager
{
private IDictionary<string, DataInfo> _data;
public CustomDataManager()
{
_data = new Dictionary<string, DataInfo>();
}
public void Add(string key, object value, string type)
{
throw new NotImplementedException();
}
public void Change(string key, object value)
{
throw new NotImplementedException();
}
public void Delete(string key)
{
throw new NotImplementedException();
}
public object? Get(string key)
{
throw new NotImplementedException();
}
}
// type: text (string), whole number (int), number (double), boolean, formula (string, data type of number)
public struct DataInfo
{
public string Id { get; set; }
public string Type { get; set; }
public object Value { get; set; }
}
}

View File

@ -31,15 +31,4 @@ public class HermesApiClient
{ {
return await _web.GetJson<TTSVersion>($"https://{BASE_URL}/api/info/version"); return await _web.GetJson<TTSVersion>($"https://{BASE_URL}/api/info/version");
} }
public async Task<Account> FetchHermesAccountDetails()
{
var account = await _web.GetJson<Account>($"https://{BASE_URL}/api/account", new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
if (account == null || account.Id == null || account.Username == null)
throw new NullReferenceException("Invalid value found while fetching for hermes account data.");
return account;
}
} }

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

@ -1,8 +1,10 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Serilog; using Serilog;
using TwitchChatTTS.Bus;
namespace TwitchChatTTS.Hermes.Socket.Handlers namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
@ -10,13 +12,15 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
{ {
private readonly User _user; private readonly User _user;
private readonly NightbotApiClient _nightbot; private readonly NightbotApiClient _nightbot;
private readonly ServiceBusCentral _bus;
private readonly ILogger _logger; private readonly ILogger _logger;
public int OperationCode { get; } = 2; public int OperationCode { get; } = 2;
public LoginAckHandler(User user, NightbotApiClient nightbot, ILogger logger) public LoginAckHandler(User user, NightbotApiClient nightbot, ServiceBusCentral bus, ILogger logger)
{ {
_user = user; _user = user;
_nightbot = nightbot; _nightbot = nightbot;
_bus = bus;
_logger = logger; _logger = logger;
} }
@ -39,24 +43,37 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
return; return;
} }
_user.Slave = message.Slave;
_logger.Information(_user.Slave ? "This client is not responsible for reacting to chat messages." : "This client is responsible for reacting to chat messages.");
_user.HermesUserId = message.UserId; _user.HermesUserId = message.UserId;
_user.HermesUsername = message.UserName;
_user.TwitchUsername = message.UserName;
_user.TwitchUserId = long.Parse(message.ProviderAccountId);
_user.OwnerId = message.OwnerId; _user.OwnerId = message.OwnerId;
_user.DefaultTTSVoice = message.DefaultTTSVoice; _user.DefaultTTSVoice = message.DefaultTTSVoice;
_user.VoicesAvailable = message.TTSVoicesAvailable; _user.VoicesAvailable = new ConcurrentDictionary<string, string>(message.TTSVoicesAvailable);
_user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices); _user.VoicesEnabled = new HashSet<string>(message.EnabledTTSVoices);
_user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch"); _user.TwitchConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "twitch") ?? message.Connections.FirstOrDefault(c => c.Type == "twitch");
_user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot"); _user.NightbotConnection = message.Connections.FirstOrDefault(c => c.Default && c.Type == "nightbot") ?? message.Connections.FirstOrDefault(c => c.Type == "nightbot");
if (_user.TwitchConnection != null)
{
_logger.Information("Twitch connection: " + _user.TwitchConnection.Name + " / " + _user.TwitchConnection.AccessToken);
}
var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToArray(); var filters = message.WordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
foreach (var filter in filters) foreach (var filter in filters)
{ {
try try
{ {
var re = new Regex(filter.Search!, RegexOptions.Compiled); var re = new Regex(filter.Search, ((RegexOptions)filter.Flag) | RegexOptions.Compiled);
re.Match(string.Empty); re.Match(string.Empty);
filter.Regex = re; filter.Regex = re;
} }
catch (Exception) { } catch (Exception)
{
_logger.Warning($"Failed to create a regular expression for a TTS filter [filter id: {filter.Search}]");
}
} }
_user.RegexFilters = filters; _user.RegexFilters = filters;
@ -84,6 +101,7 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
} }
_logger.Information("TTS is now ready."); _logger.Information("TTS is now ready.");
_bus.Send(this, "tts_connected", _user);
client.Ready = true; client.Ready = true;
} }
} }

View File

@ -1,4 +1,3 @@
using System.Text.Json;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
@ -24,23 +23,30 @@ namespace TwitchChatTTS.Hermes.Socket.Handlers
_logger = logger; _logger = logger;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not RequestAckMessage message || message == null) if (data is not RequestAckMessage message || message == null)
return; return Task.CompletedTask;
if (message.Request == null) if (message.Request == null)
{ {
_logger.Warning("Received a Hermes request message without a proper request."); _logger.Warning("Received a Hermes request message without a proper request.");
return; return Task.CompletedTask;
} }
_logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty<string>())}]"); _logger.Debug($"Received a Hermes request message [type: {message.Request.Type}][data: {string.Join(',', message.Request.Data?.Select(entry => entry.Key + '=' + entry.Value) ?? Array.Empty<string>())}]");
var json = message.Data?.ToString(); var json = message.Data?.ToString();
if (message.Request.Type == null) if (message.Request.Type == null)
{ {
return; _logger.Warning("Request type is null. Unknown what acknowlegement this is for.");
return Task.CompletedTask;
}
if (!string.IsNullOrWhiteSpace(message.Error))
{
_logger.Warning("An error occured while processing the request on the server: " + message.Error);
return Task.CompletedTask;
} }
_manager.Fulfill(message.Request.Type, message.Request.RequestId, json, message.Request.Data); _manager.Fulfill(message.Request.Type, message.Request.RequestId, json, message.Request.Data);
return Task.CompletedTask;
} }
} }

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

@ -1,4 +1,3 @@
using System.Net.WebSockets;
using System.Text.Json; using System.Text.Json;
using System.Timers; using System.Timers;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
@ -9,6 +8,7 @@ using HermesSocketLibrary.Requests.Messages;
using HermesSocketLibrary.Socket.Data; using HermesSocketLibrary.Socket.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using TwitchChatTTS.Bus;
using TwitchChatTTS.Hermes.Socket.Handlers; using TwitchChatTTS.Hermes.Socket.Handlers;
namespace TwitchChatTTS.Hermes.Socket namespace TwitchChatTTS.Hermes.Socket
@ -19,6 +19,7 @@ namespace TwitchChatTTS.Hermes.Socket
private readonly User _user; private readonly User _user;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ServiceBusCentral _bus;
private readonly ICallbackManager<HermesRequestData> _callbackManager; private readonly ICallbackManager<HermesRequestData> _callbackManager;
private string URL; private string URL;
@ -27,16 +28,18 @@ namespace TwitchChatTTS.Hermes.Socket
public string? UserId { get; set; } public string? UserId { get; set; }
private readonly System.Timers.Timer _heartbeatTimer; private readonly System.Timers.Timer _heartbeatTimer;
private readonly IBackoff _backoff; private readonly IBackoff _backoff;
private readonly object _lock; private readonly ReaderWriterLockSlim _rwls;
public bool Connected { get; set; } public bool Connected { get; set; }
public bool LoggedIn { get; set; } public bool LoggedIn { get; set; }
public bool Ready { get; set; } public bool Ready { get; set; }
public HermesSocketClient( public HermesSocketClient(
User user, User user,
Configuration configuration, Configuration configuration,
ServiceBusCentral bus,
ICallbackManager<HermesRequestData> callbackManager, ICallbackManager<HermesRequestData> callbackManager,
[FromKeyedServices("hermes")] IBackoff backoff, [FromKeyedServices("hermes")] IBackoff backoff,
[FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers, [FromKeyedServices("hermes")] IEnumerable<IWebSocketHandler> handlers,
@ -50,6 +53,7 @@ namespace TwitchChatTTS.Hermes.Socket
{ {
_user = user; _user = user;
_configuration = configuration; _configuration = configuration;
_bus = bus;
_callbackManager = callbackManager; _callbackManager = callbackManager;
_backoff = backoff; _backoff = backoff;
_heartbeatTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15)); _heartbeatTimer = new System.Timers.Timer(TimeSpan.FromSeconds(15));
@ -58,32 +62,56 @@ namespace TwitchChatTTS.Hermes.Socket
LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow; LastHeartbeatReceived = LastHeartbeatSent = DateTime.UtcNow;
URL = $"wss://{BASE_URL}"; URL = $"wss://{BASE_URL}";
_lock = new object(); _rwls = new ReaderWriterLockSlim();
var ttsCreateUserVoice = _bus.GetTopic("tts.user.voice.create");
ttsCreateUserVoice.Subscribe(async data => await Send(3, new RequestMessage()
{
Type = "create_tts_user",
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!
}));
} }
public override async Task Connect() public override async Task Connect()
{ {
lock (_lock) _rwls.EnterWriteLock();
try
{ {
if (Connected) if (Connected)
return; return;
}
_logger.Debug($"Attempting to connect to {URL}"); _logger.Debug($"Attempting to connect to {URL}");
await ConnectAsync(URL); await ConnectAsync(URL);
} }
finally
{
_rwls.ExitWriteLock();
}
}
private async Task Disconnect() private async Task Disconnect()
{ {
lock (_lock) _rwls.EnterWriteLock();
try
{ {
if (!Connected) if (!Connected)
return; return;
}
await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed")); await DisconnectAsync(new SocketDisconnectionEventArgs("Normal disconnection", "Disconnection was executed"));
} }
finally
{
_rwls.ExitWriteLock();
}
}
public async Task CreateTTSVoice(string voiceName) public async Task CreateTTSVoice(string voiceName)
{ {
@ -229,17 +257,15 @@ namespace TwitchChatTTS.Hermes.Socket
public void Initialize() public void Initialize()
{ {
_logger.Information("Initializing Hermes websocket client."); _logger.Information("Initializing Tom to Speech websocket client.");
OnConnected += async (sender, e) => OnConnected += async (sender, e) =>
{
lock (_lock)
{ {
if (Connected) if (Connected)
return; return;
Connected = true; Connected = true;
}
_logger.Information("Hermes websocket client connected."); _logger.Information("Tom to Speech websocket client connected.");
_heartbeatTimer.Enabled = true; _heartbeatTimer.Enabled = true;
LastHeartbeatReceived = DateTime.UtcNow; LastHeartbeatReceived = DateTime.UtcNow;
@ -249,21 +275,21 @@ namespace TwitchChatTTS.Hermes.Socket
ApiKey = _configuration.Hermes!.Token!, ApiKey = _configuration.Hermes!.Token!,
MajorVersion = TTS.MAJOR_VERSION, MajorVersion = TTS.MAJOR_VERSION,
MinorVersion = TTS.MINOR_VERSION, MinorVersion = TTS.MINOR_VERSION,
PatchVersion = TTS.PATCH_VERSION,
}); });
}; };
OnDisconnected += async (sender, e) => OnDisconnected += async (sender, e) =>
{
lock (_lock)
{ {
if (!Connected) if (!Connected)
return; return;
Connected = false;
}
Connected = false;
LoggedIn = false; LoggedIn = false;
Ready = false; Ready = false;
_logger.Warning("Hermes websocket client disconnected."); _user.Slave = true;
_logger.Warning("Tom to Speech websocket client disconnected.");
_heartbeatTimer.Enabled = false; _heartbeatTimer.Enabled = false;
await Reconnect(_backoff); await Reconnect(_backoff);
@ -378,7 +404,7 @@ namespace TwitchChatTTS.Hermes.Socket
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to send a heartbeat back to the Hermes websocket server."); _logger.Error(ex, "Failed to send a heartbeat back to the Tom to Speech websocket server.");
} }
} }
else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120)) else if (signalTime - LastHeartbeatReceived > TimeSpan.FromSeconds(120))
@ -389,7 +415,7 @@ namespace TwitchChatTTS.Hermes.Socket
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to disconnect from Hermes websocket server."); _logger.Error(ex, "Failed to disconnect from Tom to Speech websocket server.");
Ready = false; Ready = false;
LoggedIn = false; LoggedIn = false;
Connected = false; Connected = false;
@ -402,14 +428,22 @@ namespace TwitchChatTTS.Hermes.Socket
} }
public new async Task Send<T>(int opcode, T message) public new async Task Send<T>(int opcode, T message)
{
_rwls.EnterReadLock();
try
{ {
if (!Connected) if (!Connected)
{ {
_logger.Warning("Hermes websocket client is not connected. Not sending a message."); _logger.Warning("Tom to Speech websocket client is not connected. Not sending a message.");
return; return;
} }
await base.Send(opcode, message); await base.Send(opcode, message);
} }
finally
{
_rwls.ExitReadLock();
}
}
} }
} }

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

@ -1,5 +1,5 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketServer.Models; using HermesSocketServer.Messages;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
@ -22,31 +22,30 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (string.IsNullOrWhiteSpace(json)) {
{ _logger.Warning($"Policy JSON data is null.");
_logger.Warning("Request data is null.");
return; return;
} }
var policy = JsonSerializer.Deserialize<PolicyMessage>(json, _options); var policy = JsonSerializer.Deserialize<Policy>(json, _options);
if (policy == null) if (policy == null)
{ {
_logger.Warning($"Policy data failed: null"); _logger.Warning($"Policy data is null.");
return; return;
} }
var group = _groups.Get(policy.GroupId.ToString()); var group = _groups.Get(policy.GroupId.ToString());
if (group == null) 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; return;
} }
_logger.Debug($"Policy data [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]"); _logger.Debug($"Policy data [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]");
_policies.Set(group.Name, policy.Path, policy.Usage, TimeSpan.FromMilliseconds(policy.Span)); _policies.Set(group.Name, policy.Path, policy.Usage, TimeSpan.FromMilliseconds(policy.Span));
_logger.Information($"Policy has been updated [policy id: {policy.Id}]"); _logger.Information($"Policy has been created [policy id: {policy.Id}]");
} }
} }
} }

View File

@ -0,0 +1,41 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateRedeemableActionAck : IRequestAck
{
public string Name => "create_redeemable_action";
private readonly IRedemptionManager _redemptions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreateRedeemableActionAck(IRedemptionManager redemptions, JsonSerializerOptions options, ILogger logger)
{
_redemptions = redemptions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning($"Redeemable action JSON data received is null.");
return;
}
var action = JsonSerializer.Deserialize<RedeemableAction>(json, _options);
if (action == null)
{
_logger.Warning($"Redeemable action data received is null.");
return;
}
_redemptions.Add(action);
_logger.Information($"A new redeemable action has been created [action name: {action.Name}]");
}
}
}

View File

@ -0,0 +1,41 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateRedemptionAck : IRequestAck
{
public string Name => "create_redemption";
private readonly IRedemptionManager _redemptions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreateRedemptionAck(IRedemptionManager redemptions, JsonSerializerOptions options, ILogger logger)
{
_redemptions = redemptions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning($"Redemption JSON data received is null.");
return;
}
var redemption = JsonSerializer.Deserialize<Redemption>(json, _options);
if (redemption == null)
{
_logger.Warning($"Redemption data received is null.");
return;
}
_redemptions.Add(redemption);
_logger.Information($"A new redemption has been created [redemption id: {redemption.Id}][twitch redemption id: {redemption.RedemptionId}]");
}
}
}

View File

@ -0,0 +1,59 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class CreateTTSFilterAck : IRequestAck
{
public string Name => "create_tts_filter";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public CreateTTSFilterAck(User user, JsonSerializerOptions options, ILogger logger)
{
_user = user;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null)
{
_logger.Warning($"TTS Filter JSON data received is null.");
return;
}
var filter = JsonSerializer.Deserialize<TTSWordFilter>(json, _options);
if (filter == null)
{
_logger.Warning($"TTS Filter data received is null.");
return;
}
if (_user.RegexFilters.Any(f => f.Id == filter.Id))
{
_logger.Warning($"Filter already exists [filter id: {filter.Id}]");
return;
}
try
{
var re = new Regex(filter.Search, ((RegexOptions)filter.Flag) | RegexOptions.Compiled);
re.Match(string.Empty);
filter.Regex = re;
}
catch (Exception)
{
_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}]");
_user.RegexFilters.Add(filter);
_logger.Information($"Filter has been created [filter id: {filter.Id}]");
}
}
}

View File

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

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (requestData == null)
{ {

View File

@ -0,0 +1,48 @@
using System.Text.Json;
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 JsonSerializerOptions _options;
private readonly ILogger _logger;
public DeleteGroupAck(IChatterGroupManager groups, JsonSerializerOptions options, ILogger logger)
{
_groups = groups;
_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;
}
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

@ -18,8 +18,14 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) 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('/'); var data = json.Split('/');
if (data.Length != 2) if (data.Length != 2)
{ {

View File

@ -0,0 +1,39 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteRedeemableActionAck : IRequestAck
{
public string Name => "delete_redeemable_action";
private readonly IRedemptionManager _redemptions;
private readonly ILogger _logger;
public DeleteRedeemableActionAck(IRedemptionManager redemptions, ILogger logger)
{
_redemptions = redemptions;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (requestData == null)
{
_logger.Warning("Request data is null.");
return;
}
var name = requestData["name"].ToString();
if (string.IsNullOrEmpty(name))
{
_logger.Warning($"Action name is invalid [action name: {name}]");
return;
}
if (_redemptions.RemoveAction(name))
_logger.Information($"Deleted a redeemable action [action name: {name}]");
else
_logger.Warning($"Failed to delete a redeemable action [action name: {name}]");
}
}
}

View File

@ -0,0 +1,39 @@
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteRedemptionAck : IRequestAck
{
public string Name => "delete_redemption";
private readonly IRedemptionManager _redemptions;
private readonly ILogger _logger;
public DeleteRedemptionAck(IRedemptionManager redemptions, ILogger logger)
{
_redemptions = redemptions;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (requestData == null)
{
_logger.Warning("Request data is null.");
return;
}
var id = requestData["id"].ToString();
if (string.IsNullOrEmpty(id))
{
_logger.Warning($"Redemption Id is invalid [redemption id: {id}]");
return;
}
if (_redemptions.RemoveRedemption(id))
_logger.Information($"Deleted a redemption [redemption id: {id}]");
else
_logger.Warning($"Failed to delete a redemption [redemption id: {id}]");
}
}
}

View File

@ -0,0 +1,42 @@
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class DeleteTTSFilterAck : IRequestAck
{
public string Name => "delete_tts_filter";
private readonly User _user;
private readonly ILogger _logger;
public DeleteTTSFilterAck(User user, ILogger logger)
{
_user = user;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (requestData == null)
{
_logger.Warning("Request data is null.");
return;
}
var id = requestData["id"].ToString();
if (string.IsNullOrEmpty(id))
{
_logger.Warning($"Filter Id is invalid [filter id: {id}]");
return;
}
var filter = _user.RegexFilters.FirstOrDefault(f => f.Id == id);
if (filter == null)
{
_logger.Warning($"Filter Id does not exist [filter id: {id}]");
return;
}
_user.RegexFilters.Remove(filter);
_logger.Information($"Deleted a filter [filter id: {id}][search: {filter.Search}][replace: {filter.Replace}]");
}
}
}

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (requestData == null)
{ {

View File

@ -17,8 +17,14 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (json == null)
{
_logger.Warning("Chatters JSON is null.");
return;
}
var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(json, _options); var chatters = JsonSerializer.Deserialize<IEnumerable<long>>(json, _options);
if (chatters == null) if (chatters == null)
{ {

View File

@ -29,8 +29,14 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (json == null)
{
_logger.Error("Connections JSON is null.");
return;
}
var connections = JsonSerializer.Deserialize<IEnumerable<Connection>>(json, _options); var connections = JsonSerializer.Deserialize<IEnumerable<Connection>>(json, _options);
if (connections == null) if (connections == null)
{ {

View File

@ -1,4 +1,3 @@
using System.Text.Json;
using Serilog; using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests namespace TwitchChatTTS.Hermes.Socket.Requests
@ -7,17 +6,15 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
{ {
public string Name => "get_default_tts_voice"; public string Name => "get_default_tts_voice";
private readonly User _user; private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
public GetDefaultTTSVoiceAck(User user, JsonSerializerOptions options, ILogger logger) public GetDefaultTTSVoiceAck(User user, ILogger logger)
{ {
_user = user; _user = user;
_options = options;
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
string? defaultVoice = json; string? defaultVoice = json;
if (defaultVoice != null) if (defaultVoice != null)

View File

@ -19,8 +19,14 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (json == null)
{
_logger.Warning("Emotes JSON is null.");
return;
}
var data = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(json, _options); var data = JsonSerializer.Deserialize<IEnumerable<EmoteInfo>>(json, _options);
if (data == null) if (data == null)
{ {

View File

@ -17,8 +17,14 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (json == null)
{
_logger.Warning("TTS Voices JSON is null.");
return;
}
var enabledTTSVoices = JsonSerializer.Deserialize<IEnumerable<string>>(json, _options); var enabledTTSVoices = JsonSerializer.Deserialize<IEnumerable<string>>(json, _options);
if (enabledTTSVoices == null) if (enabledTTSVoices == null)
{ {

View File

@ -26,12 +26,18 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (json == null)
{
_logger.Error("Failed to load groups & permissions: JSON is null.");
return;
}
var groupInfo = JsonSerializer.Deserialize<GroupInfo>(json, _options); var groupInfo = JsonSerializer.Deserialize<GroupInfo>(json, _options);
if (groupInfo == null) if (groupInfo == null)
{ {
_logger.Error("Failed to load groups & permissions."); _logger.Error("Failed to load groups & permissions: object is null.");
return; return;
} }
@ -40,12 +46,15 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g); var groupsById = groupInfo.Groups.ToDictionary(g => g.Id, g => g);
foreach (var group in groupInfo.Groups) foreach (var group in groupInfo.Groups)
{
_logger.Debug($"Adding group [group id: {group.Id}][name: {group.Name}][priority: {group.Priority}]");
_groups.Add(group); _groups.Add(group);
}
foreach (var permission in groupInfo.GroupPermissions) foreach (var permission in groupInfo.GroupPermissions)
{ {
_logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]"); _logger.Debug($"Adding group permission [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}][allow: {permission.Allow?.ToString() ?? "null"}]");
if (!groupsById.TryGetValue(permission.GroupId, out var group)) if (!groupsById.TryGetValue(permission.GroupId.ToString(), out var group))
{ {
_logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]"); _logger.Warning($"Failed to find group by id [permission id: {permission.Id}][group id: {permission.GroupId}][path: {permission.Path}]");
continue; continue;
@ -60,7 +69,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
foreach (var chatter in groupInfo.GroupChatters) foreach (var chatter in groupInfo.GroupChatters)
if (groupsById.TryGetValue(chatter.GroupId, out var group)) 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."); _logger.Information($"Users in each group [count: {groupInfo.GroupChatters.Count()}] have been loaded.");
} }
} }

View File

@ -1,5 +1,5 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketServer.Models; using HermesSocketServer.Messages;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
@ -26,13 +26,18 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
var policies = JsonSerializer.Deserialize<IEnumerable<PolicyMessage>>(json, _options); if (json == null)
{
_logger.Error($"No policies have been found: JSON is null.");
return;
}
var policies = JsonSerializer.Deserialize<IEnumerable<Policy>>(json, _options);
if (policies == null || !policies.Any()) if (policies == null || !policies.Any())
{ {
_logger.Information($"No policies have been found. Policies have been set to default."); _logger.Error($"No policies have been found: object is null or empty.");
_policies.Set("everyone", "tts", 25, TimeSpan.FromSeconds(15));
return; return;
} }
@ -41,7 +46,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
var group = _groups.Get(policy.GroupId.ToString()); var group = _groups.Get(policy.GroupId.ToString());
if (group == null) 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; continue;
} }
_logger.Debug($"Policy data loaded [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]"); _logger.Debug($"Policy data loaded [policy id: {policy.Id}][path: {policy.Path}][group id: {policy.GroupId}][group name: {group.Name}]");

View File

@ -23,7 +23,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (requestData == null)
{ {
@ -46,7 +46,8 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
} }
_logger.Information($"Redeemable actions loaded [count: {actions.Count()}]"); _logger.Information($"Redeemable actions loaded [count: {actions.Count()}]");
_bus.Send(this, "redemptions_initiation", new RedemptionInitiation() { _bus.Send(this, "redemptions_initiation", new RedemptionInitiation()
{
Redemptions = redemptions, Redemptions = redemptions,
Actions = actions.ToDictionary(a => a.Name, a => a) Actions = actions.ToDictionary(a => a.Name, a => a)
}); });

View File

@ -23,7 +23,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
HermesRequestData? hermesRequestData = null; HermesRequestData? hermesRequestData = null;
if (!string.IsNullOrEmpty(requestId)) if (!string.IsNullOrEmpty(requestId))

View File

@ -18,7 +18,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
var users = JsonSerializer.Deserialize<IDictionary<long, string>>(json, _options); var users = JsonSerializer.Deserialize<IDictionary<long, string>>(json, _options);
if (users == null) if (users == null)

View File

@ -19,9 +19,9 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
var voices = JsonSerializer.Deserialize<IEnumerable<VoiceDetails>>(json, _options); var voices = JsonSerializer.Deserialize<IEnumerable<TTSVoice>>(json, _options);
if (voices == null) if (voices == null)
{ {
_logger.Warning("Voices received is null."); _logger.Warning("Voices received is null.");

View File

@ -19,7 +19,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
var wordFilters = JsonSerializer.Deserialize<IEnumerable<TTSWordFilter>>(json, _options); var wordFilters = JsonSerializer.Deserialize<IEnumerable<TTSWordFilter>>(json, _options);
if (wordFilters == null) if (wordFilters == null)
@ -28,19 +28,22 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
return; return;
} }
var filters = wordFilters.Where(f => f.Search != null && f.Replace != null).ToArray(); var filters = wordFilters.Where(f => f.Search != null && f.Replace != null).ToList();
foreach (var filter in filters) foreach (var filter in filters)
{ {
try try
{ {
var re = new Regex(filter.Search!, RegexOptions.Compiled); var re = new Regex(filter.Search, ((RegexOptions)filter.Flag) | RegexOptions.Compiled);
re.Match(string.Empty); re.Match(string.Empty);
filter.Regex = re; filter.Regex = re;
} }
catch (Exception) { } catch (Exception)
{
_logger.Warning($"Failed to create a regular expression for a TTS filter [filter id: {filter.Search}]");
}
} }
_user.RegexFilters = filters; _user.RegexFilters = filters;
_logger.Information($"TTS word filters [count: {_user.RegexFilters.Count()}] have been refreshed."); _logger.Information($"TTS word filters [count: {_user.RegexFilters.Count}] have been refreshed.");
} }
} }
} }

View File

@ -3,6 +3,6 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
public interface IRequestAck public interface IRequestAck
{ {
string Name { get; } string Name { get; }
void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData); void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData);
} }
} }

View File

@ -13,7 +13,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Fulfill(string type, string requestId, string data, IDictionary<string, object>? requestData) public void Fulfill(string type, string requestId, string? data, IDictionary<string, object>? requestData)
{ {
if (!_acknowledgements.TryGetValue(type, out var ack)) if (!_acknowledgements.TryGetValue(type, out var ack))
{ {

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (requestData == null)
{ {

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

@ -1,5 +1,5 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketServer.Models; using HermesSocketServer.Messages;
using Serilog; using Serilog;
using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
@ -22,18 +22,24 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
var policy = JsonSerializer.Deserialize<PolicyMessage>(json, _options); if (json == null)
{
_logger.Warning($"Policy JSON data is null.");
return;
}
var policy = JsonSerializer.Deserialize<Policy>(json, _options);
if (policy == null) if (policy == null)
{ {
_logger.Warning($"Policy data failed: null"); _logger.Warning($"Policy data is null.");
return; return;
} }
var group = _groups.Get(policy.GroupId.ToString()); var group = _groups.Get(policy.GroupId.ToString());
if (group == null) 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; return;
} }

View File

@ -0,0 +1,37 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateRedeemableActionAck : IRequestAck
{
public string Name => "update_redeemable_action";
private readonly IRedemptionManager _redemptions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdateRedeemableActionAck(IRedemptionManager redemptions, JsonSerializerOptions options, ILogger logger)
{
_redemptions = redemptions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
var action = JsonSerializer.Deserialize<RedeemableAction>(json, _options);
if (action == null)
{
_logger.Warning($"Redeemable action data received is null.");
return;
}
if (_redemptions.Update(action))
_logger.Information($"A redeemable action has been updated [action name: {action.Name}]");
else
_logger.Warning($"Failed to update an existing redeemable action [action name: {action.Name}]");
}
}
}

View File

@ -0,0 +1,42 @@
using System.Text.Json;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
using TwitchChatTTS.Twitch.Redemptions;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateRedemptionAck : IRequestAck
{
public string Name => "update_redemption";
private readonly IRedemptionManager _redemptions;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdateRedemptionAck(IRedemptionManager redemptions, JsonSerializerOptions options, ILogger logger)
{
_redemptions = redemptions;
_options = options;
_logger = logger;
}
public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{
if (json == null) {
_logger.Warning($"Redemption JSON data received is null.");
return;
}
var redemption = JsonSerializer.Deserialize<Redemption>(json, _options);
if (redemption == null)
{
_logger.Warning($"Redemption data received is null.");
return;
}
if (_redemptions.Update(redemption))
_logger.Information($"A redemption has been updated [redemption id: {redemption.Id}][twitch redemption id: {redemption.RedemptionId}]");
else
_logger.Warning($"Failed to update an existing redemption [redemption id: {redemption.Id}][twitch redemption id: {redemption.RedemptionId}]");
}
}
}

View File

@ -0,0 +1,63 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using HermesSocketLibrary.Requests.Messages;
using Serilog;
namespace TwitchChatTTS.Hermes.Socket.Requests
{
public class UpdateTTSFilterAck : IRequestAck
{
public string Name => "update_tts_filter";
private readonly User _user;
private readonly JsonSerializerOptions _options;
private readonly ILogger _logger;
public UpdateTTSFilterAck(User user, JsonSerializerOptions options, ILogger logger)
{
_user = user;
_options = options;
_logger = logger;
}
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 is null.");
return;
}
_logger.Debug($"Filter data [filter id: {filter.Id}][search: {filter.Search}][group id: {filter.Replace}]");
var current = _user.RegexFilters.FirstOrDefault(f => f.Id == filter.Id);
if (current == null)
{
_logger.Warning($"TTS Filter doest exist by id [filter id: {filter.Id}]");
return;
}
current.Search = filter.Search;
current.Replace = filter.Replace;
current.Flag = filter.Flag;
try
{
var re = new Regex(current.Search, ((RegexOptions)current.Flag) | RegexOptions.Compiled);
re.Match(string.Empty);
current.Regex = re;
}
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

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

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (requestData == null)
{ {

View File

@ -14,7 +14,7 @@ namespace TwitchChatTTS.Hermes.Socket.Requests
_logger = logger; _logger = logger;
} }
public void Acknowledge(string requestId, string json, IDictionary<string, object>? requestData) public void Acknowledge(string requestId, string? json, IDictionary<string, object>? requestData)
{ {
if (requestData == null) if (requestData == null)
{ {

View File

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

View File

@ -1,13 +0,0 @@
public class TTSVoice
{
public string Label { get; set; }
public int Value { get; set; }
public string? Gender { get; set; }
public string? Language { get; set; }
}
public class TTSChatterSelectedVoice
{
public long ChatterId { get; set; }
public string Voice { get; set; }
}

View File

@ -1,7 +0,0 @@
public class TwitchBotToken {
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? BroadcasterId { get; set; }
}

View File

@ -1,7 +0,0 @@
public class TwitchConnection {
public string? Id { get; set; }
public string? Secret { get; set; }
public string? BroadcasterId { get; set; }
public string? Username { get; set; }
public string? UserId { get; set; }
}

View File

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

View File

@ -2,13 +2,13 @@ namespace TwitchChatTTS.OBS.Socket.Data
{ {
public class HelloMessage public class HelloMessage
{ {
public string ObsWebSocketVersion { get; set; } public required string ObsWebSocketVersion { get; set; }
public int RpcVersion { get; set; } public int RpcVersion { get; set; }
public AuthenticationMessage Authentication { get; set; } public AuthenticationMessage? Authentication { get; set; }
} }
public class AuthenticationMessage { public class AuthenticationMessage {
public string Challenge { get; set; } public required string Challenge { get; set; }
public string Salt { get; set; } public required string Salt { get; set; }
} }
} }

View File

@ -2,9 +2,9 @@ namespace TwitchChatTTS.OBS.Socket.Data
{ {
public class OBSSceneItem public class OBSSceneItem
{ {
public string SourceUuid { get; set; } public required string SourceUuid { get; set; }
public string SourceName { get; set; } public required string SourceName { get; set; }
public string SourceType { get; set; } public required string SourceType { get; set; }
public int SceneItemId { get; set; } public int SceneItemId { get; set; }
} }
} }

View File

@ -1,5 +1,3 @@
using Newtonsoft.Json;
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
public class RequestBatchMessage public class RequestBatchMessage

View File

@ -2,7 +2,7 @@ namespace TwitchChatTTS.OBS.Socket.Data
{ {
public class RequestBatchResponseMessage public class RequestBatchResponseMessage
{ {
public string RequestId { get; set; } public required string RequestId { get; set; }
public IEnumerable<object> Results { get; set; } public required IEnumerable<object> Results { get; set; }
} }
} }

View File

@ -3,7 +3,7 @@ namespace TwitchChatTTS.OBS.Socket.Data
public class RequestMessage public class RequestMessage
{ {
public string RequestType { get; set; } public string RequestType { get; set; }
public string RequestId { get; set; } public string? RequestId { get; set; }
public Dictionary<string, object> RequestData { get; set; } public Dictionary<string, object> RequestData { get; set; }
public RequestMessage(string type, string id, Dictionary<string, object> data) public RequestMessage(string type, string id, Dictionary<string, object> data)

View File

@ -1,12 +1,10 @@
using Newtonsoft.Json;
namespace TwitchChatTTS.OBS.Socket.Data namespace TwitchChatTTS.OBS.Socket.Data
{ {
public class RequestResponseMessage public class RequestResponseMessage
{ {
public string RequestType { get; set; } public required string RequestType { get; set; }
public string RequestId { get; set; } public required string RequestId { get; set; }
public object RequestStatus { get; set; } public required object RequestStatus { get; set; }
public Dictionary<string, object> ResponseData { get; set; } public Dictionary<string, object>? ResponseData { get; set; }
} }
} }

View File

@ -5,7 +5,7 @@ namespace TwitchChatTTS.OBS.Socket.Data
public int Alignment { get; set; } public int Alignment { get; set; }
public int BoundsAlignment { get; set; } public int BoundsAlignment { get; set; }
public double BoundsHeight { get; set; } public double BoundsHeight { get; set; }
public string BoundsType { get; set; } public required string BoundsType { get; set; }
public double BoundsWidth { get; set; } public double BoundsWidth { get; set; }
public int CropBottom { get; set; } public int CropBottom { get; set; }
public int CropLeft { get; set; } public int CropLeft { get; set; }

View File

@ -17,19 +17,17 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
_logger = logger; _logger = logger;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not EventMessage message || message == null) if (data is not EventMessage message || message == null)
return; return Task.CompletedTask;
if (sender is not OBSSocketClient obs) if (sender is not OBSSocketClient obs)
return; return Task.CompletedTask;
switch (message.EventType) switch (message.EventType)
{ {
case "StreamStateChanged": case "StreamStateChanged":
if (sender is not OBSSocketClient client) if (sender is not OBSSocketClient client)
return; return Task.CompletedTask;
string? raw_state = message.EventData["outputState"].ToString(); string? raw_state = message.EventData["outputState"].ToString();
string? state = raw_state?.Substring(21).ToLower(); string? state = raw_state?.Substring(21).ToLower();
obs.Streaming = message.EventData["outputActive"].ToString()!.ToLower() == "true"; obs.Streaming = message.EventData["outputActive"].ToString()!.ToLower() == "true";
@ -44,6 +42,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
_logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? Array.Empty<string>())); _logger.Debug(message.EventType + " EVENT: " + string.Join(" | ", message.EventData?.Select(x => x.Key + "=" + x.Value?.ToString()) ?? Array.Empty<string>()));
break; break;
} }
return Task.CompletedTask;
} }
} }
} }

View File

@ -18,12 +18,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
_logger = logger; _logger = logger;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not RequestResponseMessage message || message == null) if (data is not RequestResponseMessage message || message == null)
return; return Task.CompletedTask;
if (sender is not OBSSocketClient obs) if (sender is not OBSSocketClient obs)
return; return Task.CompletedTask;
_logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]"); _logger.Debug($"Received an OBS request response [obs request id: {message.RequestId}]");
@ -31,13 +31,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (requestData == null) if (requestData == null)
{ {
_logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]"); _logger.Warning($"OBS Request Response not being processed: request not stored [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
var request = requestData.Message; var request = requestData.Message;
if (request == null) if (request == null)
return; return Task.CompletedTask;
try try
{ {
switch (request.RequestType) switch (request.RequestType)
@ -50,26 +49,26 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
{ {
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!request.RequestData.TryGetValue("sourceName", out object? sourceName) || sourceName == null) if (!request.RequestData.TryGetValue("sourceName", out object? sourceName) || sourceName == null)
{ {
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (message.ResponseData == null) if (message.ResponseData == null)
{ {
_logger.Warning($"OBS Response is null [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]"); _logger.Warning($"OBS Response is null [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!message.ResponseData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) if (!message.ResponseData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
{ {
_logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to fetch the scene item id [scene: {sceneName}][scene item: {sourceName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Found the scene item by name [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][obs request id: {message.RequestId}]."); _logger.Debug($"Found the scene item by name [scene: {sceneName}][source: {sourceName}][id: {sceneItemId}][obs request id: {message.RequestId}].");
//_manager.AddSourceId(sceneName.ToString(), sourceName.ToString(), (long) sceneItemId); obs.AddSourceId(sourceName.ToString()!, long.Parse(sceneItemId.ToString()!));
requestData.ResponseValues = message.ResponseData; requestData.ResponseValues = message.ResponseData;
break; break;
@ -79,22 +78,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
{ {
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
{ {
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (message.ResponseData == null) if (message.ResponseData == null)
{ {
_logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); _logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null) if (!message.ResponseData.TryGetValue("sceneItemTransform", out object? transformData) || transformData == null)
{ {
_logger.Warning($"Failed to fetch the OBS transformation data [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to fetch the OBS transformation data [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Fetched OBS transformation data [scene: {sceneName}][scene item id: {sceneItemId}][transformation: {transformData}][obs request id: {message.RequestId}]"); _logger.Debug($"Fetched OBS transformation data [scene: {sceneName}][scene item id: {sceneItemId}][transformation: {transformData}][obs request id: {message.RequestId}]");
@ -106,22 +105,22 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
{ {
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
{ {
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (message.ResponseData == null) if (message.ResponseData == null)
{ {
_logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); _logger.Warning($"OBS Response is null [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!message.ResponseData.TryGetValue("sceneItemEnabled", out object? sceneItemVisibility) || sceneItemVisibility == null) if (!message.ResponseData.TryGetValue("sceneItemEnabled", out object? sceneItemVisibility) || sceneItemVisibility == null)
{ {
_logger.Warning($"Failed to fetch the scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to fetch the scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Fetched OBS scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][visibility: {sceneItemVisibility}][obs request id: {message.RequestId}]"); _logger.Debug($"Fetched OBS scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][visibility: {sceneItemVisibility}][obs request id: {message.RequestId}]");
@ -133,12 +132,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
{ {
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
{ {
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Received response from OBS for updating scene item transformation [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); _logger.Debug($"Received response from OBS for updating scene item transformation [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
break; break;
@ -148,12 +147,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
{ {
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null) if (!request.RequestData.TryGetValue("sceneItemId", out object? sceneItemId) || sceneItemId == null)
{ {
_logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene item name that was requested [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Received response from OBS for updating scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]"); _logger.Debug($"Received response from OBS for updating scene item visibility [scene: {sceneName}][scene item id: {sceneItemId}][obs request id: {message.RequestId}]");
break; break;
@ -163,12 +162,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (message.ResponseData == null) if (message.ResponseData == null)
{ {
_logger.Warning($"OBS Response is null [obs request id: {message.RequestId}]"); _logger.Warning($"OBS Response is null [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!message.ResponseData.TryGetValue("groups", out object? value) || value == null) if (!message.ResponseData.TryGetValue("groups", out object? value) || value == null)
{ {
_logger.Warning($"Failed to fetch the scene item visibility [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to fetch the scene item visibility [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
var groups = JsonSerializer.Deserialize<IEnumerable<string>>(value.ToString()); var groups = JsonSerializer.Deserialize<IEnumerable<string>>(value.ToString());
_logger.Debug($"Fetched OBS groups [obs request id: {message.RequestId}]"); _logger.Debug($"Fetched OBS groups [obs request id: {message.RequestId}]");
@ -183,17 +182,17 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null) if (!request.RequestData.TryGetValue("sceneName", out object? sceneName) || sceneName == null)
{ {
_logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the scene name that was requested [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (message.ResponseData == null) if (message.ResponseData == null)
{ {
_logger.Warning($"OBS Response is null [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"OBS Response is null [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!message.ResponseData.TryGetValue("sceneItems", out object? value) || value == null) if (!message.ResponseData.TryGetValue("sceneItems", out object? value) || value == null)
{ {
_logger.Warning($"Failed to fetch the scene item visibility [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to fetch the scene item visibility [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Fetched OBS scene items in group [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Debug($"Fetched OBS scene items in group [scene: {sceneName}][obs request id: {message.RequestId}]");
var sceneItems = JsonSerializer.Deserialize<IEnumerable<OBSSceneItem>>(value.ToString()!, new JsonSerializerOptions() var sceneItems = JsonSerializer.Deserialize<IEnumerable<OBSSceneItem>>(value.ToString()!, new JsonSerializerOptions()
@ -203,7 +202,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (sceneItems == null) if (sceneItems == null)
{ {
_logger.Warning($"Failed to deserialize the data received [scene: {sceneName}][obs request id: {message.RequestId}]"); _logger.Warning($"Failed to deserialize the data received [scene: {sceneName}][obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
foreach (var sceneItem in sceneItems) foreach (var sceneItem in sceneItems)
@ -220,7 +219,7 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (!request.RequestData.TryGetValue("sleepMillis", out object? sleepMillis) || sleepMillis == null) if (!request.RequestData.TryGetValue("sleepMillis", out object? sleepMillis) || sleepMillis == null)
{ {
_logger.Warning($"Failed to find the amount of time to sleep for [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to find the amount of time to sleep for [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
_logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]"); _logger.Debug($"Received response from OBS for sleeping [sleep: {sleepMillis}][obs request id: {message.RequestId}]");
break; break;
@ -230,12 +229,12 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (message.ResponseData == null) if (message.ResponseData == null)
{ {
_logger.Warning($"OBS Response is null [obs request id: {message.RequestId}]"); _logger.Warning($"OBS Response is null [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
if (!message.ResponseData.TryGetValue("outputActive", out object? outputActive) || outputActive == null) if (!message.ResponseData.TryGetValue("outputActive", out object? outputActive) || outputActive == null)
{ {
_logger.Warning($"Failed to fetch the scene item visibility [obs request id: {message.RequestId}]"); _logger.Warning($"Failed to fetch the scene item visibility [obs request id: {message.RequestId}]");
return; return Task.CompletedTask;
} }
obs.Streaming = outputActive?.ToString()!.ToLower() == "true"; obs.Streaming = outputActive?.ToString()!.ToLower() == "true";
@ -257,6 +256,8 @@ namespace TwitchChatTTS.OBS.Socket.Handlers
if (requestData.Callback != null) if (requestData.Callback != null)
requestData.Callback(requestData.ResponseValues); requestData.Callback(requestData.ResponseValues);
} }
return Task.CompletedTask;
} }
} }
} }

View File

@ -5,8 +5,7 @@ using Serilog;
using System.Text.Json; using System.Text.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using System.Timers; using CommonSocketLibrary.Backoff;
using System.Net.WebSockets;
namespace TwitchChatTTS.OBS.Socket namespace TwitchChatTTS.OBS.Socket
{ {
@ -16,8 +15,8 @@ namespace TwitchChatTTS.OBS.Socket
private readonly IDictionary<string, long> _sourceIds; private readonly IDictionary<string, long> _sourceIds;
private string? URL; private string? URL;
private readonly IBackoff _backoff;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private System.Timers.Timer _reconnectTimer;
public bool Connected { get; set; } public bool Connected { get; set; }
public bool Identified { get; set; } public bool Identified { get; set; }
@ -26,6 +25,7 @@ namespace TwitchChatTTS.OBS.Socket
public OBSSocketClient( public OBSSocketClient(
Configuration configuration, Configuration configuration,
[FromKeyedServices("hermes")] IBackoff backoff,
[FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers, [FromKeyedServices("obs")] IEnumerable<IWebSocketHandler> handlers,
[FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager, [FromKeyedServices("obs")] MessageTypeManager<IWebSocketHandler> typeManager,
ILogger logger ILogger logger
@ -35,12 +35,9 @@ namespace TwitchChatTTS.OBS.Socket
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}, logger) }, logger)
{ {
_backoff = backoff;
_configuration = configuration; _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>(); _requests = new ConcurrentDictionary<string, RequestData>();
_sourceIds = new Dictionary<string, long>(); _sourceIds = new Dictionary<string, long>();
} }
@ -51,18 +48,19 @@ namespace TwitchChatTTS.OBS.Socket
OnConnected += (sender, e) => OnConnected += (sender, e) =>
{ {
Connected = true; Connected = true;
_reconnectTimer.Enabled = false;
_logger.Information("OBS websocket client connected."); _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.")); _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; Connected = false;
Identified = false; Identified = false;
Streaming = false; Streaming = false;
if (Identified)
await Reconnect(_backoff);
}; };
if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null) if (!string.IsNullOrWhiteSpace(_configuration.Obs?.Host) && _configuration.Obs?.Port != null)
@ -115,34 +113,6 @@ namespace TwitchChatTTS.OBS.Socket
await handler.Execute(this, message); 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) public async Task Send(IEnumerable<RequestMessage> messages)
{ {
if (!Connected) if (!Connected)
@ -167,7 +137,7 @@ namespace TwitchChatTTS.OBS.Socket
await Send(8, new RequestBatchMessage(uid, list)); await Send(8, new RequestBatchMessage(uid, list));
} }
public async Task Send(RequestMessage message, Action<Dictionary<string, object>>? callback = null) public async Task Send(RequestMessage message, Action<Dictionary<string, object>?>? callback = null)
{ {
if (!Connected) if (!Connected)
{ {
@ -335,7 +305,7 @@ namespace TwitchChatTTS.OBS.Socket
} }
var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } }); var m = new RequestMessage("GetSceneItemId", string.Empty, new Dictionary<string, object>() { { "sceneName", sceneName }, { "sourceName", sceneItemName } });
await Send(m, async (d) => await Send(m, (d) =>
{ {
if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId)) if (d == null || !d.TryGetValue("sceneItemId", out object? value) || value == null || !long.TryParse(value.ToString(), out long sceneItemId))
return; return;
@ -357,7 +327,7 @@ namespace TwitchChatTTS.OBS.Socket
public RequestMessage Message { get; } public RequestMessage Message { get; }
public string ParentId { get; } public string ParentId { get; }
public Dictionary<string, object>? ResponseValues { get; set; } public Dictionary<string, object>? ResponseValues { get; set; }
public Action<Dictionary<string, object>>? Callback { get; set; } public Action<Dictionary<string, object>?>? Callback { get; set; }
public RequestData(RequestMessage message, string parentId) public RequestData(RequestMessage message, string parentId)
{ {

View File

@ -23,10 +23,17 @@ public class SevenApiClient
}); });
} }
public async Task<EmoteSet?> FetchChannelEmoteSet(string twitchId) public async Task<EmoteSet?> FetchChannelEmoteSet(long twitchId)
{ {
if (twitchId <= 0)
{
_logger.Warning("No valid Twitch Id was given for 7tv emotes.");
return null;
}
try try
{ {
_logger.Debug($"Fetching 7tv information using Twitch Id [twitch id: {twitchId}]");
var details = await _web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId); var details = await _web.GetJson<UserDetails>($"{API_URL}/users/twitch/" + twitchId);
return details?.EmoteSet; return details?.EmoteSet;
} }

View File

@ -2,10 +2,10 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class ChangeMapMessage public class ChangeMapMessage
{ {
public object Id { get; set; } public required object Id { get; set; }
public byte Kind { get; set; } public byte Kind { get; set; }
public bool? Contextual { get; set; } public bool? Contextual { get; set; }
public object Actor { get; set; } public required object Actor { get; set; }
public IEnumerable<ChangeField>? Added { get; set; } public IEnumerable<ChangeField>? Added { get; set; }
public IEnumerable<ChangeField>? Updated { get; set; } public IEnumerable<ChangeField>? Updated { get; set; }
public IEnumerable<ChangeField>? Removed { get; set; } public IEnumerable<ChangeField>? Removed { get; set; }
@ -14,17 +14,17 @@ namespace TwitchChatTTS.Seven.Socket.Data
} }
public class ChangeField { public class ChangeField {
public string Key { get; set; } public required string Key { get; set; }
public int? Index { get; set; } public int? Index { get; set; }
public bool Nested { get; set; } public bool Nested { get; set; }
public object OldValue { get; set; } public required object OldValue { get; set; }
public object Value { get; set; } public required object Value { get; set; }
} }
public class EmoteField { public class EmoteField {
public string Id { get; set; } public required string Id { get; set; }
public string Name { get; set; } public required string Name { get; set; }
public string ActorId { get; set; } public required string ActorId { get; set; }
public int Flags { get; set; } public int Flags { get; set; }
} }
} }

View File

@ -2,7 +2,7 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class DispatchMessage public class DispatchMessage
{ {
public object EventType { get; set; } public required object EventType { get; set; }
public ChangeMapMessage Body { get; set; } public required ChangeMapMessage Body { get; set; }
} }
} }

View File

@ -3,6 +3,6 @@ namespace TwitchChatTTS.Seven.Socket.Data
public class EndOfStreamMessage public class EndOfStreamMessage
{ {
public int Code { get; set; } public int Code { get; set; }
public string Message { get; set; } public required string Message { get; set; }
} }
} }

View File

@ -2,6 +2,7 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class ErrorMessage public class ErrorMessage
{ {
public Exception? Exception { get; set; }
public string? Message { get; set; }
} }
} }

View File

@ -2,6 +2,6 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class ReconnectMessage public class ReconnectMessage
{ {
public string Reason { get; set; } public required string Reason { get; set; }
} }
} }

View File

@ -2,6 +2,6 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class ResumeMessage public class ResumeMessage
{ {
public string SessionId { get; set; } public required string SessionId { get; set; }
} }
} }

View File

@ -3,7 +3,7 @@ namespace TwitchChatTTS.Seven.Socket.Data
public class SevenHelloMessage public class SevenHelloMessage
{ {
public uint HeartbeatInterval { get; set; } public uint HeartbeatInterval { get; set; }
public string SessionId { get; set; } public required string SessionId { get; set; }
public int SubscriptionLimit { get; set; } public int SubscriptionLimit { get; set; }
} }
} }

View File

@ -2,7 +2,7 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class SubscribeMessage public class SubscribeMessage
{ {
public string? Type { get; set; } public required string Type { get; set; }
public IDictionary<string, string>? Condition { get; set; } public IDictionary<string, string>? Condition { get; set; }
} }
} }

View File

@ -2,7 +2,7 @@ namespace TwitchChatTTS.Seven.Socket.Data
{ {
public class UnsubscribeMessage public class UnsubscribeMessage
{ {
public string Type { get; set; } public required string Type { get; set; }
public IDictionary<string, string>? Condition { get; set; } public IDictionary<string, string>? Condition { get; set; }
} }
} }

View File

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

View File

@ -1,4 +1,3 @@
using System.Net.WebSockets;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using CommonSocketLibrary.Common; using CommonSocketLibrary.Common;
using TwitchChatTTS.Seven.Socket.Data; using TwitchChatTTS.Seven.Socket.Data;
@ -15,7 +14,7 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
return; return;
var code = message.Code - 4000; var code = message.Code - 4000;
await sender.DisconnectAsync(new SocketDisconnectionEventArgs(WebSocketCloseStatus.Empty.ToString(), code.ToString())); await sender.DisconnectAsync(new SocketDisconnectionEventArgs(message.Message, code.ToString()));
} }
} }
} }

View File

@ -15,10 +15,16 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
_logger = logger; _logger = logger;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not ErrorMessage message || message == null) if (data is not ErrorMessage message || message == null || message.Exception == null && message.Message == null)
return; return Task.CompletedTask;
if (message.Exception != null)
_logger.Error(message.Exception, message.Message ?? string.Empty);
else
_logger.Error(message.Message!);
return Task.CompletedTask;
} }
} }
} }

View File

@ -15,15 +15,16 @@ namespace TwitchChatTTS.Seven.Socket.Handlers
_logger = logger; _logger = logger;
} }
public async Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data) public Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data)
{ {
if (data is not SevenHelloMessage message || message == null) if (data is not SevenHelloMessage message || message == null)
return; return Task.CompletedTask;
if (sender is not SevenSocketClient seven || seven == null) if (sender is not SevenSocketClient seven || seven == null)
return; return Task.CompletedTask;
seven.ConnectionDetails = message; seven.ConnectionDetails = message;
_logger.Debug("Received hello handshake ack."); _logger.Debug("Received hello handshake ack.");
return Task.CompletedTask;
} }
} }
} }

View File

@ -4,18 +4,18 @@ namespace TwitchChatTTS.Seven
{ {
public class UserDetails public class UserDetails
{ {
public string Id { get; set; } public required string Id { get; set; }
public string Platform { get; set; } public required string Platform { get; set; }
public string Username { get; set; } public required string DisplayName { get; set; }
public int EmoteCapacity { get; set; } public int EmoteCapacity { get; set; }
public string EmoteSetId { get; set; } public required string EmoteSetId { get; set; }
public EmoteSet EmoteSet { get; set; } public required EmoteSet EmoteSet { get; set; }
public SevenUser User { get; set; } public required SevenUser User { get; set; }
} }
public class SevenUser public class SevenUser
{ {
public string Id { get; set; } public required string Id { get; set; }
public string Username { get; set; } public required string Username { get; set; }
} }
} }

View File

@ -36,6 +36,7 @@ using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Hermes.Socket.Requests; using TwitchChatTTS.Hermes.Socket.Requests;
using TwitchChatTTS.Bus; using TwitchChatTTS.Bus;
using TwitchChatTTS.Veadotube; using TwitchChatTTS.Veadotube;
using TwitchChatTTS.Veadotube.Handlers;
// dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r linux-x64 -p:PublishSingleFile=true --self-contained true
// dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true // dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained true
@ -86,10 +87,22 @@ s.AddTransient<ICommandFactory, CommandFactory>();
// Request acks // Request acks
s.AddSingleton<RequestAckManager>(); s.AddSingleton<RequestAckManager>();
s.AddTransient<IRequestAck, CreateGroupAck>();
s.AddTransient<IRequestAck, CreateGroupChatterAck>();
s.AddTransient<IRequestAck, CreateGroupPermissionAck>();
s.AddTransient<IRequestAck, CreatePolicyAck>(); s.AddTransient<IRequestAck, CreatePolicyAck>();
s.AddTransient<IRequestAck, CreateRedeemableActionAck>();
s.AddTransient<IRequestAck, CreateRedemptionAck>();
s.AddTransient<IRequestAck, CreateTTSFilterAck>();
s.AddTransient<IRequestAck, CreateTTSUserAck>(); s.AddTransient<IRequestAck, CreateTTSUserAck>();
s.AddTransient<IRequestAck, CreateTTSVoiceAck>(); s.AddTransient<IRequestAck, CreateTTSVoiceAck>();
s.AddTransient<IRequestAck, DeleteGroupAck>();
s.AddTransient<IRequestAck, DeleteGroupChatterAck>();
s.AddTransient<IRequestAck, DeleteGroupPermissionAck>();
s.AddTransient<IRequestAck, DeletePolicyAck>(); s.AddTransient<IRequestAck, DeletePolicyAck>();
s.AddTransient<IRequestAck, DeleteRedeemableActionAck>();
s.AddTransient<IRequestAck, DeleteRedemptionAck>();
s.AddTransient<IRequestAck, DeleteTTSFilterAck>();
s.AddTransient<IRequestAck, DeleteTTSVoiceAck>(); s.AddTransient<IRequestAck, DeleteTTSVoiceAck>();
s.AddTransient<IRequestAck, GetChatterIdsAck>(); s.AddTransient<IRequestAck, GetChatterIdsAck>();
s.AddTransient<IRequestAck, GetConnectionsAck>(); s.AddTransient<IRequestAck, GetConnectionsAck>();
@ -103,8 +116,14 @@ s.AddTransient<IRequestAck, GetRedemptionsAck>();
s.AddTransient<IRequestAck, GetTTSUsersAck>(); s.AddTransient<IRequestAck, GetTTSUsersAck>();
s.AddTransient<IRequestAck, GetTTSVoicesAck>(); s.AddTransient<IRequestAck, GetTTSVoicesAck>();
s.AddTransient<IRequestAck, GetTTSWordFiltersAck>(); s.AddTransient<IRequestAck, GetTTSWordFiltersAck>();
s.AddTransient<IRequestAck, UpdateGroupAck>();
s.AddTransient<IRequestAck, UpdateGroupChatterAck>();
s.AddTransient<IRequestAck, UpdateGroupPermissionAck>();
s.AddTransient<IRequestAck, UpdateDefaultTTSVoiceAck>(); s.AddTransient<IRequestAck, UpdateDefaultTTSVoiceAck>();
s.AddTransient<IRequestAck, UpdatePolicyAck>(); s.AddTransient<IRequestAck, UpdatePolicyAck>();
s.AddTransient<IRequestAck, UpdateRedeemableActionAck>();
s.AddTransient<IRequestAck, UpdateRedemptionAck>();
s.AddTransient<IRequestAck, UpdateTTSFilterAck>();
s.AddTransient<IRequestAck, UpdateTTSUserAck>(); s.AddTransient<IRequestAck, UpdateTTSUserAck>();
s.AddTransient<IRequestAck, UpdateTTSVoiceAck>(); s.AddTransient<IRequestAck, UpdateTTSVoiceAck>();
s.AddTransient<IRequestAck, UpdateTTSVoiceStateAck>(); s.AddTransient<IRequestAck, UpdateTTSVoiceStateAck>();
@ -128,6 +147,7 @@ s.AddSingleton<IChatterGroupManager, ChatterGroupManager>();
s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>(); s.AddSingleton<IGroupPermissionManager, GroupPermissionManager>();
// OBS websocket // OBS websocket
s.AddKeyedSingleton<IBackoff>("obs", new ExponentialBackoff(1000, 120 * 1000));
s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs"); s.AddKeyedSingleton<IWebSocketHandler, HelloHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs"); s.AddKeyedSingleton<IWebSocketHandler, IdentifiedHandler>("obs");
s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs"); s.AddKeyedSingleton<IWebSocketHandler, RequestResponseHandler>("obs");
@ -149,6 +169,8 @@ s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, SevenMessageTypeManag
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, SevenSocketClient>("7tv");
// Veadotube // Veadotube
s.AddKeyedSingleton<IVeadotubeMessageHandler, FetchStatesHandler>("veadotube");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, VeadoMessageTypeManager>("veadotube"); s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, VeadoMessageTypeManager>("veadotube");
s.AddKeyedSingleton<SocketClient<object>, VeadoSocketClient>("veadotube"); s.AddKeyedSingleton<SocketClient<object>, VeadoSocketClient>("veadotube");
@ -162,7 +184,6 @@ s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>
{ {
var factory = sp.GetRequiredService<ITwitchConnectionManager>(); var factory = sp.GetRequiredService<ITwitchConnectionManager>();
var client = factory.GetWorkingClient(); var client = factory.GetWorkingClient();
client.Connect().Wait();
return client; return client;
}); });
s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create"); s.AddKeyedTransient<SocketClient<TwitchWebsocketMessage>, TwitchWebsocketClient>("twitch-create");
@ -183,6 +204,7 @@ s.AddKeyedSingleton<ITwitchSocketHandler, ChannelFollowHandler>("twitch-notifica
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelRaidHandler>("twitch-notifications"); s.AddKeyedSingleton<ITwitchSocketHandler, ChannelRaidHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelResubscriptionHandler>("twitch-notifications"); s.AddKeyedSingleton<ITwitchSocketHandler, ChannelResubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications"); s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionEndHandler>("twitch-notifications");
s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionGiftHandler>("twitch-notifications"); s.AddKeyedSingleton<ITwitchSocketHandler, ChannelSubscriptionGiftHandler>("twitch-notifications");
// hermes websocket // hermes websocket
@ -190,6 +212,8 @@ s.AddKeyedSingleton<IBackoff>("hermes", new ExponentialBackoff(1000, 15 * 1000))
s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes"); s.AddKeyedSingleton<IWebSocketHandler, HeartbeatHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes"); s.AddKeyedSingleton<IWebSocketHandler, LoginAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes"); s.AddKeyedSingleton<IWebSocketHandler, RequestAckHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, LoggingHandler>("hermes");
s.AddKeyedSingleton<IWebSocketHandler, SlaveHandler>("hermes");
s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes"); s.AddKeyedSingleton<MessageTypeManager<IWebSocketHandler>, HermesMessageTypeManager>("hermes");
s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes"); s.AddKeyedSingleton<SocketClient<WebSocketMessage>, HermesSocketClient>("hermes");

133
TTS.cs
View File

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

View File

@ -30,6 +30,14 @@ namespace TwitchChatTTS
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_playback.AddOnMixerInputEnded((object? s, SampleProviderEventArgs e) =>
{
if (_player.Playing?.Audio == e.SampleProvider)
{
_player.Playing = null;
}
});
Task.Run(async () => Task.Run(async () =>
{ {
while (true) while (true)
@ -55,6 +63,12 @@ namespace TwitchChatTTS
return; return;
} }
if (DateTime.UtcNow - group.Timestamp > TimeSpan.FromSeconds(60))
{
_logger.Debug("Ignored message due to being in queue for too long.");
continue;
}
FetchMasterAudio(group); FetchMasterAudio(group);
} }
catch (COMException e) catch (COMException e)

View File

@ -1,11 +1,18 @@
using HermesSocketLibrary.Requests.Messages; using HermesSocketLibrary.Requests.Messages;
using TwitchChatTTS.Twitch.Socket.Messages;
namespace TwitchChatTTS.Twitch.Redemptions namespace TwitchChatTTS.Twitch.Redemptions
{ {
public interface IRedemptionManager public interface IRedemptionManager
{ {
Task Execute(RedeemableAction action, string senderDisplayName, long senderId); void Add(RedeemableAction action);
IList<RedeemableAction> Get(string twitchRedemptionId); void Add(Redemption redemption);
void Initialize(IEnumerable<Redemption> redemptions, IDictionary<string, RedeemableAction> actions); Task Execute(RedeemableAction action, string senderDisplayName, long senderId, string senderMessage);
IEnumerable<RedeemableAction> Get(string twitchRedemptionId);
void Initialize();
bool RemoveAction(string actionName);
bool RemoveRedemption(string redemptionId);
bool Update(Redemption redemption);
bool Update(RedeemableAction action);
} }
} }

View File

@ -7,7 +7,6 @@ using org.mariuszgromada.math.mxparser;
using Serilog; using Serilog;
using TwitchChatTTS.Bus; using TwitchChatTTS.Bus;
using TwitchChatTTS.Bus.Data; using TwitchChatTTS.Bus.Data;
using TwitchChatTTS.Hermes.Socket;
using TwitchChatTTS.OBS.Socket; using TwitchChatTTS.OBS.Socket;
using TwitchChatTTS.OBS.Socket.Data; using TwitchChatTTS.OBS.Socket.Data;
using TwitchChatTTS.Veadotube; using TwitchChatTTS.Veadotube;
@ -16,58 +15,168 @@ namespace TwitchChatTTS.Twitch.Redemptions
{ {
public class RedemptionManager : IRedemptionManager public class RedemptionManager : IRedemptionManager
{ {
private readonly IDictionary<string, IList<RedeemableAction>> _store; private readonly IDictionary<string, RedeemableAction> _actions;
private readonly IDictionary<string, Redemption> _redemptions;
// twitch redemption id -> redemption ids
private readonly IDictionary<string, IList<string>> _redeems;
private readonly ServiceBusCentral _bus; private readonly ServiceBusCentral _bus;
private readonly User _user; private readonly User _user;
private readonly OBSSocketClient _obs; private readonly OBSSocketClient _obs;
private readonly HermesSocketClient _hermes;
private readonly VeadoSocketClient _veado; private readonly VeadoSocketClient _veado;
private readonly NightbotApiClient _nightbot; private readonly NightbotApiClient _nightbot;
private readonly AudioPlaybackEngine _playback; private readonly AudioPlaybackEngine _playback;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Random _random; private readonly Random _random;
private bool _isReady; private readonly ReaderWriterLockSlim _rwls;
public RedemptionManager( public RedemptionManager(
ServiceBusCentral bus, ServiceBusCentral bus,
User user, User user,
[FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs, [FromKeyedServices("obs")] SocketClient<WebSocketMessage> obs,
[FromKeyedServices("hermes")] SocketClient<WebSocketMessage> hermes,
[FromKeyedServices("veadotube")] SocketClient<object> veado, [FromKeyedServices("veadotube")] SocketClient<object> veado,
NightbotApiClient nightbot, NightbotApiClient nightbot,
AudioPlaybackEngine playback, AudioPlaybackEngine playback,
ILogger logger) ILogger logger)
{ {
_store = new Dictionary<string, IList<RedeemableAction>>(); _actions = new Dictionary<string, RedeemableAction>();
_redemptions = new Dictionary<string, Redemption>();
_redeems = new Dictionary<string, IList<string>>();
_bus = bus; _bus = bus;
_user = user; _user = user;
_obs = (obs as OBSSocketClient)!; _obs = (obs as OBSSocketClient)!;
_hermes = (hermes as HermesSocketClient)!;
_veado = (veado as VeadoSocketClient)!; _veado = (veado as VeadoSocketClient)!;
_nightbot = nightbot; _nightbot = nightbot;
_playback = playback; _playback = playback;
_logger = logger; _logger = logger;
_random = new Random(); _random = new Random();
_isReady = false; _rwls = new ReaderWriterLockSlim();
var topic = _bus.GetTopic("redemptions_initiation"); var topic = _bus.GetTopic("redemptions_initiation");
topic.Subscribe(new ServiceBusObserver(data => { topic.Subscribe(data =>
if (data.Value is RedemptionInitiation obj)
Initialize(obj.Redemptions, obj.Actions);
}, _logger));
}
private void Add(string twitchRedemptionId, RedeemableAction action)
{ {
if (!_store.TryGetValue(twitchRedemptionId, out var actions)) if (data.Value is not RedemptionInitiation init)
_store.Add(twitchRedemptionId, actions = new List<RedeemableAction>()); return;
actions.Add(action); if (init.Actions == null)
_logger.Debug($"Added redemption action [name: {action.Name}][type: {action.Type}]"); init.Actions = new Dictionary<string, RedeemableAction>();
if (init.Redemptions == null)
init.Redemptions = new List<Redemption>();
if (!init.Actions.Any())
_logger.Warning("No redeemable actions were loaded.");
if (!init.Redemptions.Any())
_logger.Warning("No redemptions were loaded.");
foreach (var action in init.Actions.Values)
Add(action);
foreach (var redemption in init.Redemptions)
Add(redemption);
Initialize();
});
} }
public async Task Execute(RedeemableAction action, string senderDisplayName, long senderId) public void Add(RedeemableAction action)
{
if (!_actions.ContainsKey(action.Name))
{
_actions.Add(action.Name, action);
_logger.Debug($"Added redeemable action to redemption manager [action name: {action.Name}]");
}
else
{
_actions[action.Name] = action;
_logger.Debug($"Updated redeemable action to redemption manager [action name: {action.Name}]");
}
}
public void Add(Redemption redemption)
{
if (!_redemptions.ContainsKey(redemption.Id))
{
_redemptions.Add(redemption.Id, redemption);
_logger.Debug($"Added redemption to redemption manager [redemption id: {redemption.Id}]");
}
else
{
_redemptions[redemption.Id] = redemption;
_logger.Debug($"Updated redemption to redemption manager [redemption id: {redemption.Id}]");
}
Add(redemption.RedemptionId, redemption);
}
private void Add(string twitchRedemptionId, string redemptionId)
{
_rwls.EnterWriteLock();
try
{
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[i] != null && _redemptions.TryGetValue(redeems[i], out var rr))
{
if (item.Order > rr.Order)
{
redeems.Insert(i, redemptionId);
added = true;
break;
}
}
}
if (!added)
redeems.Add(redemptionId);
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Debug($"Added redemption action [redemption id: {redemptionId}][twitch redemption id: {twitchRedemptionId}]");
}
private void Add(string twitchRedemptionId, Redemption item)
{
_rwls.EnterWriteLock();
try
{
if (!_redeems.TryGetValue(twitchRedemptionId, out var redemptionNames))
_redeems.Add(twitchRedemptionId, redemptionNames = new List<string>());
var redemptions = redemptionNames.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null);
bool added = false;
for (int i = 0; i < redemptionNames.Count; i++)
{
if (redemptionNames[i] != null && _redemptions.TryGetValue(redemptionNames[i], out var rr))
{
if (item.Order > rr.Order)
{
redemptionNames.Insert(i, item.Id);
added = true;
break;
}
}
}
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, string senderMessage)
{ {
_logger.Debug($"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); _logger.Debug($"Executing an action for a redemption [action: {action.Name}][action type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
@ -82,15 +191,33 @@ namespace TwitchChatTTS.Twitch.Redemptions
switch (action.Type) switch (action.Type)
{ {
case "WRITE_TO_FILE": case "WRITE_TO_FILE":
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); {
await File.WriteAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); string path = action.Data["file_path"];
if (string.IsNullOrWhiteSpace(path))
return;
string? directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory))
Directory.CreateDirectory(directory);
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}]"); _logger.Debug($"Overwritten text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
}
case "APPEND_TO_FILE": case "APPEND_TO_FILE":
Directory.CreateDirectory(Path.GetDirectoryName(action.Data["file_path"])); {
await File.AppendAllTextAsync(action.Data["file_path"], ReplaceContentText(action.Data["file_content"], senderDisplayName)); string path = action.Data["file_path"];
if (string.IsNullOrWhiteSpace(path))
return;
string? directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory))
Directory.CreateDirectory(directory);
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}]"); _logger.Debug($"Appended text to file [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
}
case "OBS_TRANSFORM": case "OBS_TRANSFORM":
var type = typeof(OBSTransformationData); var type = typeof(OBSTransformationData);
await _obs.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) => await _obs.UpdateTransformation(action.Data["scene_name"], action.Data["scene_item_name"], (d) =>
@ -146,6 +273,11 @@ namespace TwitchChatTTS.Twitch.Redemptions
break; break;
case "SPECIFIC_TTS_VOICE": case "SPECIFIC_TTS_VOICE":
case "RANDOM_TTS_VOICE": case "RANDOM_TTS_VOICE":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
string voiceId = string.Empty; string voiceId = string.Empty;
bool specific = action.Type == "SPECIFIC_TTS_VOICE"; bool specific = action.Type == "SPECIFIC_TTS_VOICE";
@ -191,13 +323,13 @@ namespace TwitchChatTTS.Twitch.Redemptions
if (_user.VoicesSelected.ContainsKey(senderId)) if (_user.VoicesSelected.ContainsKey(senderId))
{ {
await _hermes.UpdateTTSUser(senderId, voiceId); _bus.Send(this, "tts.user.voice.update", new Dictionary<string, object>() { { "chatter", senderId }, { "voice", voiceId } });
_logger.Debug($"Sent request to create chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]"); _logger.Debug($"Sent request to update chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
} }
else else
{ {
await _hermes.CreateTTSUser(senderId, voiceId); _bus.Send(this, "tts.user.voice.create", new Dictionary<string, object>() { { "chatter", senderId }, { "voice", voiceId } });
_logger.Debug($"Sent request to update chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]"); _logger.Debug($"Sent request to create chat TTS voice [voice: {voiceName}][chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
} }
break; break;
case "AUDIO_FILE": case "AUDIO_FILE":
@ -210,31 +342,80 @@ namespace TwitchChatTTS.Twitch.Redemptions
_logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]"); _logger.Debug($"Played an audio file for channel point redeem [file: {action.Data["file_path"]}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
case "NIGHTBOT_PLAY": case "NIGHTBOT_PLAY":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.Play(); await _nightbot.Play();
break; break;
case "NIGHTBOT_PAUSE": case "NIGHTBOT_PAUSE":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.Pause(); await _nightbot.Pause();
break; break;
case "NIGHTBOT_SKIP": case "NIGHTBOT_SKIP":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.Skip(); await _nightbot.Skip();
break; break;
case "NIGHTBOT_CLEAR_PLAYLIST": case "NIGHTBOT_CLEAR_PLAYLIST":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.ClearPlaylist(); await _nightbot.ClearPlaylist();
break; break;
case "NIGHTBOT_CLEAR_QUEUE": case "NIGHTBOT_CLEAR_QUEUE":
if (_user.Slave)
{
_logger.Debug($"Ignoring channel redemption due to being a slave client [chatter id: {senderId}][source: redemption][chatter: {senderDisplayName}][chatter id: {senderId}]");
break;
}
await _nightbot.ClearQueue(); await _nightbot.ClearQueue();
break; break;
case "VEADOTUBE_SET_STATE": case "VEADOTUBE_SET_STATE":
await _veado.SetCurrentState(action.Data["state"]); {
var state = _veado.GetStateId(action.Data["state"]);
if (state == null)
{
_logger.Warning($"Could not find the state named '{action.Data["state"]}'.");
break; break;
}
await _veado.SetCurrentState(state);
break;
}
case "VEADOTUBE_PUSH_STATE": case "VEADOTUBE_PUSH_STATE":
await _veado.PushState(action.Data["state"]); {
var state = _veado.GetStateId(action.Data["state"]);
if (state == null)
{
_logger.Warning($"Could not find the state named '{action.Data["state"]}'.");
break; break;
}
await _veado.PushState(state);
break;
}
case "VEADOTUBE_POP_STATE": case "VEADOTUBE_POP_STATE":
await _veado.PopState(action.Data["state"]); {
var state = _veado.GetStateId(action.Data["state"]);
if (state == null)
{
_logger.Warning($"Could not find the state named '{action.Data["state"]}'.");
break; break;
}
await _veado.PopState(state);
break;
}
default: default:
_logger.Warning($"Unknown redeemable action has occured. Update needed? [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]"); _logger.Warning($"Unknown redeemable action has occured [type: {action.Type}][chatter: {senderDisplayName}][chatter id: {senderId}]");
break; break;
} }
} }
@ -244,21 +425,33 @@ namespace TwitchChatTTS.Twitch.Redemptions
} }
} }
public IList<RedeemableAction> Get(string twitchRedemptionId) public IEnumerable<RedeemableAction> Get(string twitchRedemptionId)
{ {
if (!_isReady) _rwls.EnterReadLock();
throw new InvalidOperationException("Not ready"); try
{
if (_store.TryGetValue(twitchRedemptionId, out var actions)) if (_redeems.TryGetValue(twitchRedemptionId, out var redemptionIds))
return actions; return redemptionIds.Select(r => _redemptions.TryGetValue(r, out var redemption) ? redemption : null)
return new List<RedeemableAction>(0); .Where(r => r != null)
.Select(r => _actions.TryGetValue(r!.ActionName, out var action) ? action : null)
.Where(a => a != null)!;
}
finally
{
_rwls.ExitReadLock();
}
return [];
} }
public void Initialize(IEnumerable<Redemption> redemptions, IDictionary<string, RedeemableAction> actions) public void Initialize()
{ {
_store.Clear(); _rwls.EnterWriteLock();
try
{
_logger.Debug($"Redemption manager is about to initialize [redemption count: {_redemptions.Count()}][action count: {_actions.Count}]");
_redeems.Clear();
var ordered = redemptions.Where(r => r != null).OrderBy(r => r.Order); var ordered = _redemptions.Select(r => r.Value).Where(r => r != null).OrderBy(r => r.Order);
foreach (var redemption in ordered) foreach (var redemption in ordered)
{ {
if (redemption.ActionName == null) if (redemption.ActionName == null)
@ -269,28 +462,152 @@ namespace TwitchChatTTS.Twitch.Redemptions
try try
{ {
if (actions.TryGetValue(redemption.ActionName, out var action) && action != null) if (_actions.ContainsKey(redemption.ActionName))
{ {
_logger.Debug($"Fetched a redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); _logger.Debug($"Fetched a redeemable action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]");
Add(redemption.RedemptionId, action); Add(redemption.RedemptionId, redemption.Id);
} }
else else
_logger.Warning($"Could not find redemption action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); _logger.Warning($"Could not find redeemable action [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]");
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Error(e, $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]"); _logger.Error(e, $"Failed to add a redemption [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}][order: {redemption.Order}]");
} }
} }
}
finally
{
_rwls.ExitWriteLock();
}
_isReady = true;
_logger.Debug("All redemptions added. Redemption Manager is ready."); _logger.Debug("All redemptions added. Redemption Manager is ready.");
} }
private string ReplaceContentText(string content, string username) public bool RemoveAction(string actionName)
{ {
return content.Replace("%USER%", username) _rwls.EnterWriteLock();
try
{
return _actions.Remove(actionName);
}
finally
{
_rwls.ExitWriteLock();
}
}
public bool RemoveRedemption(string redemptionId)
{
_rwls.EnterWriteLock();
try
{
if (!_redemptions.TryGetValue(redemptionId, out var redemption))
{
return false;
}
_redemptions.Remove(redemptionId);
if (_redeems.TryGetValue(redemption.RedemptionId, out var redeem))
{
redeem.Remove(redemptionId);
if (!redeem.Any())
_redeems.Remove(redemption.RedemptionId);
return true;
}
}
finally
{
_rwls.ExitWriteLock();
}
return false;
}
private string ReplaceContentText(string content, string chatter, long chatterId, string message)
{
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"); .Replace("\\n", "\n");
} }
public bool Update(Redemption redemption)
{
_rwls.EnterWriteLock();
try
{
if (_redemptions.TryGetValue(redemption.Id, out var r))
{
if (r.Order != redemption.Order && _redeems.TryGetValue(redemption.RedemptionId, out var redeems) && redeems.Count > 1)
{
var redemptions = redeems.Select(r => _redemptions.TryGetValue(r, out var rr) ? rr : null).ToArray();
int index = redeems.IndexOf(redemption.Id), i;
if (r.Order < redemption.Order)
{
for (i = index; i >= 1; i--)
{
if (redemptions[i - 1] == null || redemption.Order < redemptions[i - 1]!.Order)
redeems[i] = redeems[i - 1];
else
break;
}
}
else
{
for (i = index; i < redeems.Count - 1; i++)
{
if (redemptions[i + 1] == null || redemption.Order > redemptions[i + 1]!.Order)
redeems[i] = redeems[i + 1];
else
break;
}
}
redeems[i] = redemption.Id;
}
else
{
r.ActionName = redemption.ActionName;
r.State = redemption.State;
r.RedemptionId = redemption.RedemptionId;
r.Order = redemption.Order;
}
_logger.Debug($"Updated redemption in redemption manager [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]");
return true;
}
}
finally
{
_rwls.ExitWriteLock();
}
_logger.Warning($"Cannot find redemption by name [redemption id: {redemption.Id}][redemption action: {redemption.ActionName}]");
return false;
}
public bool Update(RedeemableAction action)
{
_rwls.EnterWriteLock();
try
{
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}]");
return false;
}
} }
} }

View File

@ -25,19 +25,19 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
if (message.IsAutomatic) if (message.IsAutomatic)
_logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][automatic: true]"); _logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][automatic: true]");
else else
_logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][requester: {message.RequesterUserLogin}][requester id: {message.RequesterUserId}]"); _logger.Information($"Ad break has begun [duration: {message.DurationSeconds} seconds][automatic: false][requester: {message.RequesterUserLogin}][requester id: {message.RequesterUserId}]");
try try
{ {
var actions = _redemptionManager.Get("adbreak_begin"); var actions = _redemptionManager.Get("adbreak_begin");
if (!actions.Any()) if (actions.Any())
{ {
_logger.Debug($"Found {actions.Count} actions for this Twitch ad break"); _logger.Debug($"Found {actions.Count()} actions for this Twitch ad break begin");
foreach (var action in actions) foreach (var action in actions)
try 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) catch (Exception ex)
{ {
@ -46,24 +46,32 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
} }
else else
_logger.Debug($"No redeemable actions for ad break begin was found"); _logger.Debug($"No redeemable actions for ad break begin was found");
}
catch (Exception ex)
{
_logger.Error(ex, $"Failed to fetch the redeemable actions for ad break begin");
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Run(async () => Task.Run(async () =>
{
try
{ {
await Task.Delay(TimeSpan.FromSeconds(message.DurationSeconds)); await Task.Delay(TimeSpan.FromSeconds(message.DurationSeconds));
if (message.IsAutomatic) if (message.IsAutomatic)
_logger.Information($"Ad break has ended [duration: {message.DurationSeconds} seconds][automatic: true]"); _logger.Information($"Ad break has ended [duration: {message.DurationSeconds} seconds][automatic: true]");
else else
_logger.Information($"Ad break has ended [duration: {message.DurationSeconds} seconds][requester: {message.RequesterUserLogin}][requester id: {message.RequesterUserId}]"); _logger.Information($"Ad break has ended [duration: {message.DurationSeconds} seconds][automatic: false][requester: {message.RequesterUserLogin}][requester id: {message.RequesterUserId}]");
actions = _redemptionManager.Get("adbreak_end"); var actions = _redemptionManager.Get("adbreak_end");
if (!actions.Any()) if (actions.Any())
{ {
_logger.Debug($"Found {actions.Count} actions for this Twitch ad break"); _logger.Debug($"Found {actions.Count()} actions for this Twitch ad break end");
foreach (var action in actions) foreach (var action in actions)
try 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) catch (Exception ex)
{ {
@ -72,12 +80,13 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
} }
else else
_logger.Debug($"No redeemable actions for ad break end was found"); _logger.Debug($"No redeemable actions for ad break end was found");
});
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, $"Failed to fetch the redeemable actions for ad break begin"); _logger.Error(ex, $"Failed to fetch the redeemable actions for ad break end");
} }
});
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
} }
} }
} }

View File

@ -1,4 +1,5 @@
using Serilog; using Serilog;
using TwitchChatTTS.Bus;
using TwitchChatTTS.Chat.Commands; using TwitchChatTTS.Chat.Commands;
using TwitchChatTTS.Chat.Commands.Limits; using TwitchChatTTS.Chat.Commands.Limits;
using TwitchChatTTS.Chat.Groups; using TwitchChatTTS.Chat.Groups;
@ -13,11 +14,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
public string Name => "channel.chat.message"; public string Name => "channel.chat.message";
private readonly IChatMessageReader _reader; private readonly IChatMessageReader _reader;
private readonly User _user;
private readonly ICommandManager _commands; private readonly ICommandManager _commands;
private readonly IGroupPermissionManager _permissionManager; private readonly IGroupPermissionManager _permissionManager;
private readonly IUsagePolicy<long> _permissionPolicy; private readonly IUsagePolicy<long> _permissionPolicy;
private readonly IChatterGroupManager _chatterGroupManager; private readonly IChatterGroupManager _chatterGroupManager;
private readonly ServiceBusCentral _bus;
private readonly User _user;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -27,21 +29,19 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
IGroupPermissionManager permissionManager, IGroupPermissionManager permissionManager,
IUsagePolicy<long> permissionPolicy, IUsagePolicy<long> permissionPolicy,
IChatterGroupManager chatterGroupManager, IChatterGroupManager chatterGroupManager,
ServiceBusCentral bus,
User user, User user,
ILogger logger ILogger logger
) )
{ {
_reader = reader; _reader = reader;
_user = user;
_commands = commands; _commands = commands;
_permissionManager = permissionManager; _permissionManager = permissionManager;
_permissionPolicy = permissionPolicy; _permissionPolicy = permissionPolicy;
_chatterGroupManager = chatterGroupManager; _chatterGroupManager = chatterGroupManager;
_bus = bus;
_user = user;
_logger = logger; _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 groups = GetGroups(message.Badges, chatterId);
var bits = GetTotalBits(fragments); var bits = GetTotalBits(fragments);
if (message.ChannelPointsCustomRewardId == null && !_user.Slave)
{
var commandResult = await CheckForChatCommand(message.Message.Text, message, groups); var commandResult = await CheckForChatCommand(message.Message.Text, message, groups);
if (commandResult != ChatCommandResult.Unknown) if (commandResult != ChatCommandResult.Unknown)
return; return;
}
else if (message.ChannelPointsCustomRewardId != null)
{
_bus.Send(this, "chat_message_redemption", message);
}
string permission = GetPermissionPath(message.ChannelPointsCustomRewardId, bits); string permission = GetPermissionPath(message.ChannelPointsCustomRewardId, bits);
if (!HasPermission(chatterId, groups, permission)) if (!HasPermission(chatterId, groups, permission))
@ -106,7 +113,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
{ {
var defaultGroups = new string[] { "everyone" }; var defaultGroups = new string[] { "everyone" };
var badgesGroups = badges.Select(b => b.SetId).Select(GetGroupNameByBadgeName); 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); return defaultGroups.Union(badgesGroups).Union(customGroups);
} }
@ -129,7 +136,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers
private bool HasPermission(long chatterId, IEnumerable<string> groups, string permissionPath) 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;
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More